Why must an MCP stdio server never write a JSON-RPC request to stdout?
answer
- the channel is one-way for requests
- something was deleted in 2026-07-28
- the server answers instead of asking
- a new id on the retry
- method plus id from a server is illegal
basics
~10 sBecause MCP revision 2026-07-28 removed server-initiated requests entirely. Requests flow one way on stdio: the client sends them on stdin, and the server may write only responses and notifications on stdout.
solid answer
~50 sRevision 2026-07-28 made the stdio channel explicitly asymmetric: the client **MUST NOT** write JSON-RPC *responses* to stdin, and the server **MUST NOT** write JSON-RPC *requests* to stdout. That is the transport-level expression of a protocol change — the revision deleted server-initiated requests, so there is no `ServerRequest` union left for a server to send. Legacy servers (2025-11-25 and earlier) could originate `sampling/createMessage`, `roots/list` or `elicitation/create` back at the client over the same pipes. A modern server that needs client input instead answers the call it is already serving with an interim result of `resultType: "input_required"`; the client fulfils the embedded input requests and retries the original `tools/call`, `prompts/get` or `resources/read` under a **new** JSON-RPC id. So the stdin direction carries requests and notifications, the stdout direction carries responses and notifications, and a client parsing stdout can reject anything with a `method` and an `id` as a protocol violation.
go deeper
Know the direction rule: on stdio the client sends requests on stdin and the server answers on stdout. A server does not send requests of its own.
Explain what changed — revision 2026-07-28 removed server-initiated requests — and what replaced them: an interim result the client satisfies and then retries under a new id.
Reason about consequences: reject a server message carrying both method and id, recognise it as a legacy-era server since there is no fall-forward, and note that state must ride in requestState rather than on the call stack.
Own the migration. Decide whether your servers ship dual-era, how the fleet is probed and rolled forward, and accept that removing symmetric requests is what lets any instance serve any request.
## The rule On stdio, both peers share one pipe in each direction, so the protocol has to say which message kinds are legal in which direction. Revision 2026-07-28 states it plainly: - the **client MUST NOT** write JSON-RPC *responses* to the server's stdin; - the **server MUST NOT** write JSON-RPC *requests* to stdout. What remains is: stdin carries requests and notifications from client to server; stdout carries responses and notifications from server to client. Requests travel in exactly one direction. ## Why the rule exists now The rule is downstream of a much larger change. Earlier revisions were symmetric: a server could originate a request back at the client, and the client would answer it with a response. That is how `sampling/createMessage`, `roots/list` and `elicitation/create` worked up to and including revision 2025-11-25 — a server serving a `tools/call` would, in the middle of it, send its own request and block on the client's response. Revision 2026-07-28 removed server-initiated requests entirely; there is no `ServerRequest` union any more. Since no legitimate server request exists, allowing one on stdout would only create ambiguity, and the mirror-image ban on client responses over stdin closes the loop. ## What replaced it A modern server that needs something from the client answers the request it is already handling with an interim result: `resultType: "input_required"`, carrying the input requests it needs satisfied plus an opaque `requestState`. The client satisfies them locally — that is where model sampling, directory roots, or a user prompt are produced — and then **retries the original request with a new JSON-RPC id**, supplying the responses and echoing `requestState` back verbatim. Only `tools/call`, `prompts/get` and `resources/read` may answer this way. If the user or client declines, it simply does not retry; there is no error message to send back. The shape of the exchange changes completely: what used to be a nested inner call inside one outer request becomes several complete request/response round trips at the top level, each one self-contained. ## Why an asymmetric channel is a better design **It matches statelessness.** Under 2026-07-28 every request carries its own protocol version and capabilities in `_meta`, and a server MUST NOT rely on prior requests over the same connection. A server that blocks mid-request waiting for a client's answer is the opposite of that: it holds live state pinned to one connection. Making each round trip a fresh top-level request lets any server instance serve any request — which is what makes the HTTP binding load-balancer friendly, and it keeps stdio and HTTP behaving identically. **It kills a class of deadlocks.** Symmetric request flow over one pipe means both sides can be blocked waiting for each other, and pipe buffers make that worse. One-way requests remove the cycle. **It simplifies the reader.** A client reading stdout dispatches on two shapes only: a message with an `id` plus `result`/`error` is a response to match against its pending map; a message with a `method` and no `id` is a notification. A message with both a `method` and an `id` is a protocol violation from a server, and the safest handling is to reject it and surface the incompatibility rather than try to serve it. ## The era trap in practice The deployed fleet straddles both eras, so this is a real interoperability failure, not a theoretical one. Point a modern client at a legacy stdio server and the server will eventually push a request onto stdout that the modern client has no machinery to answer; point a legacy client at a modern server and it will wait for handshake and inner-call behaviour that never comes. There is no fall-forward: era compatibility is a property of the server, and only a dual-era server works with both kinds of client. On stdio this is also harder to detect than over HTTP, because there is no status code or response header to inspect — the client has to probe by making a call and reading what comes back. ## Practical implementation notes If you are porting a server written against the older revisions, the mechanical change is: everywhere you used to send a request to the client and await it, return an interim result instead, and be able to resume from the state you encoded in `requestState` when the client retries. Because the retry arrives as a *new* request, the server can no longer keep the half-finished work on the call stack — whatever it needs on resumption has to be in `requestState` or in its own durable store.
- So what does a modern stdio server do when it genuinely needs a user's input mid-call?It answers the in-flight call with an interim result carrying `resultType: "input_required"`, the input requests it needs satisfied, and an opaque `requestState`. The client fulfils them locally and retries the original tools/call, prompts/get or resources/read under a new JSON-RPC id with the responses attached and `requestState` echoed verbatim. Declining just means not retrying.
- How should a modern client react to a message on stdout carrying both a method and an id?Treat it as a protocol violation and surface the incompatibility rather than attempting to answer it. Since 2026-07-28 there are no server-initiated requests, so such a message almost always means the subprocess is a legacy-era server. Modern client to legacy server does not work — there is no fall-forward — so the right outcome is a clear error, not a partial conversation.
- Why does the retry need a new JSON-RPC id rather than reusing the original?Because the retry is a genuinely new request, not a continuation. The first request was already answered — the interim result is a real response for that id — so the id is spent, and ids must stay unique for the connection. Continuity is carried entirely by the opaque requestState the client echoes back, not by the id.
saying these in an interview costs you the question
- Says the server calls sampling on the client mid-request
- Thinks stdin and stdout are symmetric request channels
- Believes the retry reuses the original request id
- Expects a legacy server to work with a modern client
- Calls the interim result an error the client must handle