skip to content

In MCP 2026-07-28, why is an open stdio connection to a server not a session?

level: seniorimportance: must knowfreq 58%

answer

  1. the request is the unit, not the pipe
  2. a live process is not a conversation
  3. re-read _meta on every call
  4. no handshake to inherit terms from
  5. connection framing yes, memory no

basics

~20 s

Revision 2026-07-28 made MCP stateless: every request is self-contained and carries its own protocol version and capabilities. A connection, including a live stdio process, is only a transport channel, so a server must not build context from earlier requests on it.

solid answer

~40 s

The 2026-07-28 spec says it directly: MCP is a stateless protocol, every request is self-contained and carries its own protocol version and capabilities, and an open connection such as a stdio process is not a conversation or a session. Concretely, a server must read `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` from `params._meta` on **every** request rather than caching what the first one declared, and must not infer identity, locale, working directory or "current object" from earlier traffic on the same pipe. That is also why the `initialize` handshake, `notifications/initialized` and the `Mcp-Session-Id` header were removed in this revision. Anything that must survive across calls is passed explicitly — as tool arguments, resource URIs or server configuration. The connection still matters for framing, cancellation and shutdown; it just carries no memory.

code

json · 13 lines
json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "search_issues",
    "arguments": { "query": "flaky test", "project": "payments" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

go deeper

for a junior

Remember the headline: in revision 2026-07-28 MCP is stateless and a connection is not a session. Each request repeats its own protocol version and capabilities instead of inheriting them from a first message.

for a middle

Explain what params._meta must carry on every request and why the initialize handshake and Mcp-Session-Id were removed. Be able to say what a server may still use the connection for: framing, cancellation and shutdown.

for a senior

Diagnose real implementations against the rule — a cached capability view, a per-connection user context, an implicit current-directory — and say what each one breaks. Know that a dropped stream costs the in-flight request, which is re-issued with a new id.

for a principal

Own the design consequence: statelessness is what makes any node able to serve any request and a restarted subprocess immediately usable, so continuity must be modelled explicitly in the payload, with authorization checked per request rather than inferred from a connection.

## The rule, in the spec's own words Revision 2026-07-28 states that MCP is a stateless protocol: every request is self-contained and carries its own protocol version and capabilities, servers must not rely on prior requests over the same connection to establish context, and an open connection — such as a stdio process — is not a conversation or a session. That sentence exists because stdio makes the opposite assumption so tempting. The client spawned the process, the process lives for as long as the host wants it, and one pipe carries all traffic in order. It looks exactly like a session. It is not one, and the spec spells it out for stdio specifically because that is where implementers get it wrong. ## What a request carries on its own Every request's `params._meta` (type `RequestMetaObject`) must include `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`; an empty capabilities object is legal. `io.modelcontextprotocol/clientInfo` is optional and SHOULD be sent, and results SHOULD carry `io.modelcontextprotocol/serverInfo` in `_meta`. Both info objects are self-reported and explicitly untrusted. `io.modelcontextprotocol/logLevel` is likewise per request; without it on a given request, the server must not emit `notifications/message` for it. So the unit of protocol context is the request, not the connection. There is no first message that establishes terms for the ones after it — which is why the `initialize` request and the `notifications/initialized` notification were removed in this revision, along with the `Mcp-Session-Id` header and the HTTP `DELETE` that used to terminate a session. ## What this forbids a server implementation from doing - Caching the protocol version or capabilities the first request declared and applying them to later ones. Read `_meta` each time; a conforming client sends it every time, and a client that legitimately changes its capabilities between requests must be honoured. - Treating the connection as an authenticated principal. On Streamable HTTP, authorization travels per request in the `Authorization` header with an audience-bound token; the process or socket is not the identity. On stdio, identity is whatever the launching host arranged out of band, and it still is not established by "having connected". - Keeping a "current directory", "current document", "selected project" or similar cursor that a later request implicitly uses. If a call depends on it, the call must say so in its arguments. - Assuming request N+1 follows N. Requests may interleave, and on Streamable HTTP the next one may reach a different node entirely. ## What the connection is still for Dropping sessions does not make the connection meaningless. On stdio it is the framing and lifecycle unit: newline-delimited UTF-8 JSON-RPC on stdin and stdout with no embedded newlines, `stderr` reserved for logging (a client should not read stderr output as an error signal), stdout carrying nothing but MCP messages, and shutdown performed by closing stdin, waiting, then force-terminating. The direction rule added in 2026-07-28 is also connection-level: the client must not write JSON-RPC responses to stdin and the server must not write JSON-RPC requests to stdout. Cancellation is `notifications/cancelled`. It is also the correlation unit for anything explicitly scoped to it — an in-flight request's `progressToken`, or a `subscriptions/listen` stream whose notifications are tagged with `io.modelcontextprotocol/subscriptionId`. Those are per-request or per-subscription scopes that happen to ride the connection, not connection-derived context. On stdio the client must re-send `subscriptions/listen` after a reconnect, precisely because nothing about the previous connection survives. ## Where cross-call continuity goes instead If work genuinely spans calls, the continuity is made explicit and travels in the message: values passed as ordinary tool arguments, addresses expressed as resource URIs, or a handle the server minted and returned for the client to pass back on the next call. Because such a handle is data on the wire and not a proof of identity, the spec is emphatic that possessing one must never be treated as authentication — the same authorization check runs on the request that presents it. ## Why the change was made Statelessness is what lets the deployed fleet scale and recover simply. Any node behind a load balancer can serve any request, because no node holds context another node lacks. A dropped stream loses only the in-flight request, which the client re-issues as a new request with a new JSON-RPC id — 2026-07-28 also removed SSE resumability via `Last-Event-ID`, so there is nothing to resume and nothing to reconstruct. And on stdio, a restarted subprocess is immediately usable, because there is no handshake to redo and no accumulated context to rebuild. ## The interview tell A candidate trained on pre-2026 material will describe an `initialize` handshake that negotiates a version once and a session that everything afterwards belongs to. Saying plainly that the connection is transport and the request is the unit of context, and naming which revision changed it, is the answer that lands.

  • If the connection carries no context, what is it still good for on stdio?
    Framing and lifecycle. It defines the newline-delimited UTF-8 JSON-RPC stream on stdin and stdout, keeps stdout free of non-MCP output while `stderr` carries logs, fixes the direction rules added in 2026-07-28, and gives shutdown a procedure — close stdin, wait, then force-terminate. It also carries request-scoped things like progress notifications and an opted-in `subscriptions/listen` stream.
  • A server caches the capabilities from the first request on a connection to save parsing. What breaks?
    Conformance, and eventually behaviour. The spec forbids inferring capabilities from prior requests, and a client may legitimately send different capabilities per request — for example omitting elicitation for an unattended call. The cached view then makes the server return an input-required result the client will never fulfil, or skip one it would have answered. Read `params._meta` on every request.
  • Does statelessness mean an MCP server may not hold any state at all?
    No. A server can own plenty of state — a database, a cache, a long-running job — it simply may not key that state off the connection. State is addressed by something the request carries: an argument, a resource URI, or a handle the server minted and returned. That keeps any node able to serve any request, and it keeps authorization on the request rather than on the pipe.

saying these in an interview costs you the question

  • Describing an initialize handshake that negotiates the version once
  • Caching capabilities from the first request on a connection
  • Treating a live stdio process as an authenticated principal
  • Keeping a current-directory or current-document cursor between calls
  • Assuming the next request arrives on the same node or in order

context