Why your MCP server's logs are corrupting the protocol
Your MCP server worked, then you added a log line to debug something, and now the client connects and immediately drops — or tools silently stop appearing. Nothing in your code threw. The culprit is almost always the same: you logged to stdout, and on the stdio transport, stdout isn't yours to write to.
stdout is the wire
When an MCP client launches a local server, it doesn't open a socket. It spawns your process and communicates with it over the process's standard streams. The client writes JSON-RPC requests to your stdin and reads JSON-RPC responses from your stdout. That's the entire transport. stdout is the protocol channel.
So when you do this:
print("calling weather API...") # Python
console.log("calling weather API...") # Node
…you inject the bytes calling weather API...\n directly into the JSON-RPC stream.
The client's parser hits a line that isn't valid JSON-RPC, the framing desyncs, and depending
on the client you get a hard disconnect, dropped messages, or tools that never register.
The fix: everything to stderr
stderr is a separate stream the MCP transport doesn't touch. The client typically pipes it to its own logs or ignores it — either way it can't corrupt the protocol. So all diagnostic output goes there.
Python
import logging, sys
logging.basicConfig(
stream=sys.stderr, # not stdout
level=logging.INFO,
format='{"level":"%(levelname)s","msg":"%(message)s"}',
)
log = logging.getLogger("mcp")
log.info("calling weather API") # safe
And ban bare print() in server code — it defaults to stdout. If a dependency
insists on printing, redirect it: sys.stdout = sys.stderr at startup is a blunt but
effective seatbelt for stdio servers.
Node / TypeScript
// console.log -> stdout (BAD on stdio transport)
// console.error -> stderr (safe)
console.error(JSON.stringify({ level: "info", msg: "calling weather API" }));
If you use a logger like pino, point its destination at file descriptor 2
(stderr). Audit any library that might log on its own.
How to catch it before users do
Add a test that runs the server over the real stdio transport and asserts that stdout contains only well-formed JSON-RPC frames. The moment a stray write sneaks in, the test fails — instead of a user filing a "tools disappeared" bug you can't reproduce.
Rule of thumb: on an MCP stdio server, treat stdout as read-only. The only thing that writes to it is the protocol layer. Everything you want to see goes to stderr.
Get a server that already enforces this
The Plinth MCP Server Starter Kit ships structured JSON logging pinned to stderr in both the Python and TypeScript templates, plus a test suite — 42 tests over the real protocol — that keeps stdout clean as you add your own tools.
Get the kit — $39