On MCP stdio, where do the protocol version and client capabilities travel?
answer
- nothing wraps the line
- it has to ride inside params
- two keys are required every time
- spawning is not a handshake
- earlier requests establish nothing
basics
~10 sInside the message itself. Stdio has no header layer, so every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in its params._meta object, on every single call.
solid answer
~40 sNewline-delimited JSON gives stdio no envelope around the message — no `Content-Length` block, no headers, nothing the Streamable HTTP binding puts in `MCP-Protocol-Version`. So under revision 2026-07-28 the metadata rides *inside* `params._meta` on every request: `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` are **required** on each one (an empty capabilities object `{}` is legal), and `io.modelcontextprotocol/clientInfo` is optional, self-reported and explicitly untrusted. Results carry `io.modelcontextprotocol/serverInfo` in their own `_meta`, equally untrusted. This is per-request precisely because there is no handshake left to state it once: 2026-07-28 removed `initialize`, and spawning the subprocess establishes nothing — the spec is explicit that an open stdio process is not a conversation or a session. A server **MUST NOT** infer version or capabilities from an earlier request on the same pipe.
code
json · 1 line{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search","arguments":{"q":"mcp"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}go deeper
Remember that stdio has no headers, so every request carries the protocol version and client capabilities inside params._meta — both are required on each call.
Explain why it is per request: 2026-07-28 removed the initialize handshake, so nothing is established once, and a server MUST NOT infer version or capabilities from an earlier request on the same pipe.
Show the operating detail: inject _meta in the transport layer so no call site forgets it, expect -32022 or -32021 rather than a dropped connection, and never treat clientInfo or serverInfo as identity.
Own the framing. Self-contained requests are what make servers interchangeable and disposable; accept the per-request byte cost, and set the policy that trust comes from credentials and launch provenance, never from self-reported names.
## The gap stdio has to fill A stdio message is one compact line of JSON followed by a newline. That is the entire framing — there is no header block, no length prefix, no metadata envelope of any kind. Anything the transport needs to say about a message therefore has to be *in* the message. The Streamable HTTP binding has somewhere else to put such things and uses it: a `MCP-Protocol-Version` header on the request, which MUST match the value inside the body or the server rejects the call. Stdio has no equivalent surface, so the in-body copy is the only copy. ## What travels in `_meta` Under revision 2026-07-28 each request's `params._meta` object carries: - **`io.modelcontextprotocol/protocolVersion`** — REQUIRED on every request. The revision date string the client is speaking. - **`io.modelcontextprotocol/clientCapabilities`** — REQUIRED on every request. An empty object `{}` is perfectly legal and means "I support none of the optional client features". - **`io.modelcontextprotocol/clientInfo`** — optional, and clients SHOULD send it. It is self-reported and explicitly **untrusted**: useful for logs, never for an authorization decision. - **`io.modelcontextprotocol/logLevel`** — optional; without it a server MUST NOT emit `notifications/message`. Results travel the other way with `io.modelcontextprotocol/serverInfo` in their `_meta`, SHOULD-level and equally untrusted. A handful of `_meta` keys are reserved by the specification, among them `progressToken`, `io.modelcontextprotocol/subscriptionId`, and the W3C Trace Context names `traceparent`, `tracestate` and `baggage`. ## Why per request, not once at startup Before revision 2026-07-28 the answer was different: the client opened with an `initialize` request stating its version and capabilities, the server replied with its own, the client sent `notifications/initialized`, and everything afterwards was interpreted in the light of that exchange. 2026-07-28 removed that handshake and made MCP a stateless protocol — every request is self-contained and carries its own protocol version and capabilities, and servers **MUST NOT** rely on prior requests over the same connection to establish context. On stdio this is more counter-intuitive than on HTTP, because the subprocess really is long-lived and really does belong to one client. The spec closes that door explicitly: an open connection, such as a stdio process, is **not** a conversation or a session. Spawning the process is not a handshake. Nothing about request N may be inferred from request N-1, even though they came down the same pipe from the same client seconds apart. ## What a server may and may not do with it A server reads the version and capabilities from the request it is serving and answers that request accordingly. If the client's declared version is one the server cannot speak, it rejects that request with `UnsupportedProtocolVersionError` (`-32022`), whose `data` names both what was requested and what is supported — the client can retry at a supported version rather than tearing the process down. If the request needs a client capability the client has not declared, the server returns `MissingRequiredClientCapabilityError` (`-32021`), naming the capabilities it needs in `data.requiredCapabilities`. A missing required `_meta` field is invalid params, `-32602`. What a server must not do is cache the first request's `_meta` and apply it to the rest. Beyond being a rule violation, it is wrong in practice: a single client may legitimately vary what it declares between calls, and a stdio process may outlive the assumptions made at spawn time. ## Practical consequences on stdio **Every request is bigger.** A few hundred bytes of metadata rides on each call. On a local pipe that is irrelevant, and it is the price of self-contained requests. **Client libraries should inject it centrally.** Hand-assembling `_meta` at every call site guarantees someone forgets it and gets `-32602` back. Put it in the transport layer. **Identity is not authentication.** `clientInfo` and `serverInfo` are strings each side chose for itself. On stdio the meaningful trust boundary is which binary the client launched, with what arguments and environment — not what the process claims to be called. **Detecting the server's era takes a call.** Over HTTP a client can read a rejection's status and body to tell a legacy server from a modern one. On stdio there is no such out-of-band channel, so the client has to send something and interpret what comes back.
- The subprocess has already served fifty requests. May the server reuse the first request's declared capabilities?No. Revision 2026-07-28 states that servers MUST NOT rely on prior requests over the same connection to establish context, and that an open stdio process is not a conversation or a session. Each request stands alone: read the version and capabilities from the `_meta` of the request being served, and if a required capability is absent, return MissingRequiredClientCapabilityError (-32021) naming what is needed.
- Is an empty clientCapabilities object a mistake?No, it is legal and meaningful. `io.modelcontextprotocol/clientCapabilities` is required on every request, but `{}` is a valid value declaring that the client supports none of the optional client-side features. The field being present is what matters; omitting it altogether is what makes the request invalid params.
- Can a server trust clientInfo to decide what a caller is allowed to do?Never. clientInfo is self-reported and the specification marks it explicitly untrusted — the same goes for serverInfo travelling the other way. It is for logs and diagnostics. Authorization comes from credentials: on remote servers, an audience-bound OAuth token; on stdio, from the fact that the user's own client launched that binary with that environment.
saying these in an interview costs you the question
- Says the initialize handshake declares this once
- Caches the first request's _meta for the whole process
- Thinks the stdio process itself is a session
- Treats clientInfo as an authenticated identity
- Expects an MCP-Protocol-Version header on stdio