How to Debug a stdio MCP Server
The single most common way to break a stdio MCP server is to write to standard output. In the stdio transport the JSON-RPC protocol owns stdout, so a stray print or console.log lands in the same stream as the protocol frames and can corrupt the client's parser. This guide covers the rule that avoids that, how to capture the traffic cleanly, and how to read the request/response flow and error codes once you have it.
Short answer: A stdio MCP server speaks JSON-RPC 2.0 over standard output, so the first rule of debugging one is to never write anything but the protocol to stdout — send logs to stderr or a file. Once your logging is out of the way, capture the stream, then read it as a request/response conversation: match responses to requests by id, watch for requests that never get a reply, and decode the JSON-RPC error codes. A parser like the MCP log viewer separates the frames from stray output and does the pairing for you.
The one rule: keep stdout for the protocol
The Model Context Protocol's stdio transport launches your server as a subprocess and exchanges messages over its standard input and output: one JSON-RPC 2.0 message per line on stdout, with no embedded newlines inside a message. That design is what makes stdout sacred. The moment your server writes something else there — a print("loaded"), a framework's startup banner, a stack trace — that text is interleaved with the protocol frames. The client then tries to parse a log line as JSON-RPC and fails, or a frame gets split, and you see a confusing parse error that has nothing to do with your actual logic.
The fix is simple and non-negotiable: route all diagnostic output to stderr (or a log file). In most languages that is a one-line change — write to the error stream instead of the standard one. Keep stdout exclusively for the transport and a whole class of "my server randomly stops working" bugs disappears.
Capturing the traffic
To inspect what actually crossed the wire, capture the streams. Since the protocol is on stdout and your logs are on stderr, redirect them to separate files so the protocol capture stays clean. If all you have is a combined dump from your client's log viewer — where frames and stray lines are already mixed — you can still recover the conversation by parsing it: paste the capture into the MCP log viewer, which extracts the JSON-RPC messages wherever they appear (dense single-line frames, pretty-printed objects, or Content-Length-framed) and sets everything else aside as stray output.
Reading the request/response flow
MCP traffic is a conversation of four message shapes, and telling them apart is most of the battle:
- Request — has a
methodand anid; expects a reply. - Response — has a
resultand the sameidas its request. - Error — has an
errorobject (acodeandmessage) and the request'sid. - Notification — has a
methodbut noid; it is fire-and-forget and gets no reply.
Because responses are tied to requests by id, the fastest way to localize a failure is to follow the ids: a request whose id never gets a matching response or error means a handler hung, threw without replying, or the message was dropped. Waiting on a reply to something you actually sent as a notification is the mirror-image mistake — it will never arrive.
Decoding the error codes
MCP reuses the standard JSON-RPC 2.0 error codes, and each one points at a different layer:
-32700 Parse error invalid JSON received (often stray-stdout corruption)
-32600 Invalid request not a well-formed JSON-RPC message
-32601 Method not found client called a method the server doesn't implement
-32602 Invalid params arguments didn't match the tool's input schema
-32603 Internal error the handler threw
<= -32000 Server error server-defined codes
A -32700 in particular is worth reading as a hint to go back and check rule one: something non-JSON reached the parser, and a stray write to stdout is the usual culprit. A -32602 points you at the tool's input schema versus the arguments the client actually sent. Once you can see the codes lined up next to the requests that produced them, most stdio MCP bugs resolve into one of these categories.
Frequently Asked Questions
Why does my stdio MCP server break when I add a print statement?
Because the stdio transport uses standard output for the JSON-RPC protocol itself. Every message is a line of JSON on stdout, and messages must not contain embedded newlines. When your code writes anything else to stdout — a debug print, a library banner — that text is injected into the protocol stream, so the client either sees a line that is not valid JSON-RPC or a frame split by an unexpected newline. Send all logging to stderr or a file instead, and keep stdout exclusively for the protocol.
How do I capture the traffic to inspect it?
Redirect the streams. Because the protocol is on stdout and your logs should be on stderr, you can capture them separately — for example run the server (or the client that launches it) so that stdout goes to one file and stderr to another. If you only have a combined capture from your client's log pane, paste it into a parser that can separate the JSON-RPC frames from the stray lines, such as the MCP log viewer, which pulls out the protocol messages and buckets everything else as stray output.
What do the JSON-RPC error codes mean?
MCP uses standard JSON-RPC 2.0 error codes. -32700 is a parse error (the server received invalid JSON — often exactly the stray-stdout corruption above); -32600 is an invalid request; -32601 is method not found (the client called a method the server does not implement); -32602 is invalid params (the arguments did not match the tool's input schema); and -32603 is an internal error. Servers may also define their own codes below -32000. The error object carries a code and a human-readable message.
A request never got a response — what does that mean?
In JSON-RPC a response is matched to its request by the id field, so a request whose id never receives a response or error is a strong signal that the handler hung, threw without replying, or the message was dropped. Finding that by hand means scanning for matching id numbers across the log; a viewer that pairs requests to responses will flag the unanswered one for you, which is usually the fastest route to the bug.
How is a notification different from a request?
A request has both a method and an id and expects a reply. A notification has a method but no id, so by the JSON-RPC spec it must not receive a response — it is fire-and-forget. MCP uses notifications for events like notifications/initialized and progress updates. If you are waiting for a reply to something that was sent as a notification, that is your bug: it will never come.