Paste a captured MCP stdio session and find the framing errors: a log line on stdout that corrupts the stream, a request that was never answered, a reused id, and a handshake in the wrong order.
Paste below, or drop a file anywhere on this panel
Or drop a file anywhere on this panel. Nothing is uploaded: the analysis
runs in this tab.
The answer appears here
Paste on the left and press Debug. Nothing leaves this tab.
Formatted
Results
Nothing else to flag.
No formatting problems, and nothing the rules object to. Worth
remembering what that covers: this reads the file you pasted, not the
account or cluster it will be applied to.
No finding matches that filter.
Examples
Real input you can load into the tool above. Each one shows a different
thing going wrong, because that is what the tool is for.
A log line on stdout
One banner in the middle of the stream desynchronises the reader for everything after it
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1"}}}
Server running on stdio
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"s","version":"1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
A request nobody answered
The usual cause is a handler that threw inside an async function, rejecting a promise nobody awaited
These are the ones that fail silently. The config is accepted, nothing
raises an error, and the consequence arrives later.
1
Logging to stdout
stdout is the transport. A banner, a progress bar or a dependency's warning on import lands mid-frame, and most clients stay desynchronised for everything after it rather than recovering.
Instead:Every log line to stderr. In Node that means `console.error`, and it includes anything printed by a package on import.
2
Pretty-printing messages for readability
The framing is one message per line. A multi-line JSON document is read as several broken frames, not as one nicely formatted message.
Instead:Serialize compactly. Read the log through a formatter afterwards if you need it readable.
3
Resetting the request id counter
MCP requires ids unique for the whole session, not merely unique among the requests currently open. A reused id means a late response is matched against the wrong caller, which returns another request's answer as yours.
Instead:One monotonically increasing counter for the session lifetime.
The stdio transport, and the one write that breaks it
stdio is newline-delimited JSON over the process's own stdout. That is a smaller contract than it sounds, and the commonest way to break it is a line of perfectly reasonable logging.
stdout is the wire
Not a place the server also writes to. Every byte on stdout is read as part of a message, so a startup banner, a progress bar, a leftover console.log or a dependency printing a deprecation warning on import all arrive in the middle of a frame. The client fails to parse it, and most implementations do not resynchronise afterwards: everything from that point is garbage. Route logs to stderr, which clients read separately and surface as server logs.
This is why it works in the inspector and fails in the client
A manual run only exercises the paths you took. The noisy line often lives in an error branch, a cache miss, or a dependency that only prints on a cold start. The first time a real client hits that path, a server that has worked for weeks stops.
One message per line, and no newlines inside one
A pretty-printed JSON message is a framing error, not a formatting preference: the reader takes the first line, fails to parse it, and the remaining lines arrive as further broken frames. Serialize compactly.
The handshake is three messages, not two
initialize from the client, its result from the server, then notifications/initialized from the client. Servers are entitled to refuse work until the third arrives, so a client that skips it gets an empty tool list from a server that is behaving correctly. Nothing else may be exchanged before it completes, apart from ping.
Request ids are unique for the whole session
Not merely unique among the requests currently in flight. Reusing an id after its response has arrived is still a violation, and the reason is a late or duplicated response being matched against the wrong caller. A counter that resets, or one that is per-connection in a server that reconnects, produces exactly this.
An unanswered request is usually an uncaught rejection
Every request gets exactly one response, success or error. A handler that throws inside an async function without a catch rejects a promise nobody awaited, so no response is ever written and the caller hangs until its timeout. Wrap the dispatch, not the individual handlers.
What this cannot see
The server's stderr, its exit code, and anything it did before the first byte reached stdout. Those three are where the cause usually is when initialize is never answered, and they are not in this log. It also assumes the paste is one session in order; interleaving two sessions produces false reports of reused ids. Everything here is read from the text in your browser, so nothing is uploaded and nothing is executed.