The MCP stdio transport silently drops a tools/call when the serialized JSON-RPC message crosses ~1KB — the request never reaches the server's stdin, and no layer logs an error
A tools/call request over the MCP stdio transport is silently discarded once the serialized JSON-RPC message crosses roughly 1KB: the message never lands on the server's stdin, no error surfaces at any layer, and after about a minute the client shows a generic "no result" message. The tool arguments large enough to trip this are exactly the useful ones — a code patch, a document, a SQL query, a template — so the failure looks like "that one tool is flaky on big inputs" when the tool never ran at all.
Scope, stated up front (from the one primary report, [issue #36319](https://github.com/anthropics/claude-code/issues/36319), closed *as not planned*): observed on Claude Desktop, Windows 11, protocol 2024-11-05, against a custom Node.js stdio server. It is one reporter's byte-bisection, not a confirmed cross-platform or cross-client defect, and the root cause was never pinned down. Treat everything below as "here is a real, reproducible failure shape and how to bisect it," not "here is a fully diagnosed transport bug."
The signal that gives it away
- A specific tool "works on small inputs, fails on large ones" — but *fails* means no result, not an error. The size of the input, not its content, is what predicts success or failure.
- After the failed call the client sits for ~60 seconds, then shows a generic message — in the reported case, verbatim: *"No result received from client-side tool execution."* No stack, no error code, no indication a message was dropped.
- Nothing in any log. The client's own transport log (mcp.log, at %APPDATA%\Claude\logs\ on Windows) has no trace of the request. The MCP server's own logs have no trace either — because the bytes never reached its stdin, so the server genuinely never saw a request to log.
- The JSON-RPC `id` sequence skips. The client assigns sequential ids; the dropped request's id simply doesn't appear on the wire. A gap in the id sequence with no matching request/response pair on the server side is the fingerprint — it says the drop happened *client-side or in the pipe*, before the server, not in the server's handler.
- It is not a maxBuffer overflow and not a server-side timeout — both of those leave a trace (an error on stderr, a killed subprocess, a timeout log). This leaves none.
How to bisect it (and where the threshold sits)
The diagnosis that isolates this is a byte bisection of the serialized JSON-RPC message — not the argument string, the whole envelope. From the reported runs:
| Argument payload | Serialized JSON-RPC total | Result | |---|---|---| | 133 B | 277 B | success | | 501 B | 668 B | success | | 592 B | 766 B | success | | 919 B | 1,109 B | silent drop | | 1,664 B | 1,888 B | silent drop |
So the boundary sits between the largest observed success (766 B) and the smallest observed failure (1,109 B) — call it ~800–1,100 B of serialized JSON-RPC, envelope included, not just your argument text. When you suspect this: take the exact args the tool would send, serialize the full {"jsonrpc":"2.0","id":...,"method":"tools/call","params":{...}} message, and measure its byte length. If a failing call is over ~1KB serialized and a shrunk version of the *same* call under ~700 B succeeds, you're looking at this and not at the tool's own logic.
Rule out the framing lookalike first. The stdio transport delimits messages by newline and, per the MCP spec, messages MUST NOT contain embedded newlines (see provenance). A serializer that pretty-prints or otherwise emits a *literal* \n inside the payload will desync framing and can lose messages — that is a *different* bug with the same silent shape, and it correlates with content (does this payload contain a newline?), not with size. Note that this does not describe the reported runs: the failing arguments were multi-line patches, but their newlines were JSON-escaped (\n), so the serialized envelope carried no literal embedded newline to desync framing — and the drop still tracked byte size cleanly. That is why the threshold here is a serialized-message *size* threshold, not a framing violation. Check your serialization for stray *literal* newlines before blaming the byte count; if the drop tracks bytes and not newlines, it's this.
How to work around it
- Switch that server to Streamable HTTP. In the report, the identical payloads that vanished over stdio went through correctly over HTTP transport (same server, driven from a different client — Cursor IDE). This is the one *confirmed* fix in the source — if the affected tool routinely carries text arguments, HTTP is the transport that doesn't have this ceiling. - Keep the serialized `tools/call` under the safe boundary when you must stay on stdio. The data shows ~700 B of *total serialized message* going through reliably; budget for the ~180 B of envelope/method overhead and keep argument text well under that. This is a mitigation derived from the byte table, not an independently field-tested fix — it buys you working small calls, it does not remove the ceiling. - Chunk large text into multiple small calls where the tool's contract allows it (append-a-slice, apply-patch-hunk-by-hunk) so no single message approaches 1KB. Same caveat: derived from the size correlation, not separately verified. - Do not reach for retries or bigger buffers. There is nothing to retry against and no buffer to enlarge — the message never entered a buffer the server owns, and a retry re-sends the same over-threshold payload to the same silent fate.
What doesn't work
- Reading the server's logs to find the failure. The server never received the request, so its logs are correct in showing nothing. Time spent grepping server-side logs for a request that died client-side is time spent looking under the wrong lamppost — the id-sequence gap, seen from the client, is the real locator.
- Treating "no error" as "the tool is fine, the model just didn't call it." A silently dropped tools/call is indistinguishable, from the model's seat, from a call that was made and produced nothing — so this gets misreported as a prompting/agent-behavior problem. It isn't; it's a transport-layer drop one level below anything the model or the tool can see.
- Assuming maxBuffer / stdout size limits are the cause and raising them. The report explicitly rules this out: the message doesn't reach stdin at all, no ERR_CHILD_PROCESS_STDIO_MAXBUFFER is emitted, and no buffer error surfaces. Enlarging buffers changes nothing because no buffer is overflowing.
- Blaming the argument length in isolation. What crosses the threshold is the *serialized envelope*, not the raw argument — a 919 B argument failed at 1,109 B total, but the +190 B of jsonrpc/id/method/params framing is part of what pushed it over. Measure the whole message or you'll mis-locate the boundary.
- Waiting out the ~60s "no result" and calling it a timeout. The 60s is the client giving up, not the server timing out on work — there is no server-side work in flight to time out. Tuning any server timeout is a no-op here.
Related
- The tool stays 'running' for hours: a synchronous LLM call with no timeout blocks the event loop — the sibling silent failure one layer up: there a tool *runs* and never returns and nothing raises; here a tool *never starts* and nothing raises. Same "no exception ⇒ assumed fine" instinct failing, and the same tell — a ~minute of nothing followed by a generic "no result" — in a different place in the stack. - Application INFO logs vanish because nobody configured the root logger — the web server only configures its own — the same "check every log layer and find nothing" experience; there the message was emitted and dropped by a fallback handler, here it was dropped before any handler saw it. The discipline is identical: don't trust "no error logged" as "nothing went wrong." - Reconnect without replay loses events in the gap — the fix is Last-Event-ID, not a faster reconnect — another transport-level silent loss: messages disappear in a gap with no error surfaced to either end, and the fix is likewise about the transport contract, not the application code on top of it. - Two independent broadening phases compose combinatorially and explode a payload to an external API — the upstream cause you'll often find sitting behind this: something quietly inflates the payload until it crosses the ceiling, so a call that worked yesterday silently drops today with no code change to the tool itself. - Static verification doesn't prove runtime behavior — the SEED / ACTIVATE / VERIFY / NEGATIVE checklist — this class is invisible to type checks and unit tests that stub the transport; it only appears when a real, large payload crosses a real stdio pipe. The byte bisection *is* the runtime test. FONTI: ["https://github.com/anthropics/claude-code/issues/36319","https://modelcontextprotocol.io/specification/2025-06-18/basic/transports"]
Verified against
9 claims checked against these sources
Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.