Where does cross-call state live in MCP 2026-07-28 now that sessions are gone?
answer
- state became data, not connection
- the server mints, the client carries
- it rides in arguments, not headers
- every node must be able to resolve it
- holding one proves nothing about identity
basics
~20 sIn explicit, server-minted handles. A tool returns an opaque identifier for whatever the server is holding, and the client passes it back as an ordinary argument on later calls — application data in the tool's own schema, not a protocol field or a header.
solid answer
~50 sSince revision 2026-07-28 removed protocol sessions, continuity across calls is expressed as data rather than as connection state. The server mints a handle — a workspace id, a cursor, an upload id — returns it in a tool result, and the client supplies it back as an **ordinary argument declared in that tool's input schema**. There is no reserved `_meta` key and no header for it; from the protocol's point of view it is just a string in `arguments`, which keeps every request self-contained. Two consequences matter in production. First, the server must store whatever the handle points at somewhere every node can reach, because the follow-up request can land anywhere. Second, a handle is not a credential: the spec renamed the old "Session Hijacking" threat to "State Handle Hijacking" precisely to say that possession MUST NOT be treated as authentication, so every use must be authorized against the request's own credential.
code
json · 16 lines{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "run_query",
"arguments": {
"workspaceId": "wsp_9f3c2a7e",
"sql": "select count(*) from orders"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}go deeper
Remember the shape: a tool gives you back an id, and you send that id along on the next call. It is just another argument in the tool's schema, not something the transport handles for you.
Explain why the handle lives in arguments rather than in a header: it keeps each request self-contained, which is the property revision 2026-07-28 requires now that sessions are gone.
Demonstrate the operational half — shared storage so any node can resolve a handle, an explicit TTL and cleanup story, safe behaviour when a re-issued request presents the same handle, and authorization on every use rather than at mint time only.
Own the design rule for when state is worth minting at all versus re-sending, and set the fleet-wide conventions: handle format and lifetime, the store behind them, revocation, and the audit trail that makes handle use reviewable.
## The problem statelessness creates Real tools are not always one-shot. Opening a dataset, paginating a large result, running a multi-step import, holding a scratch workspace — all of these need something to persist between calls. Before revision 2026-07-28, an implementation could lean on the connection: `initialize` had happened, an `Mcp-Session-Id` identified the caller, and the server kept a map from that id to whatever it was holding. That revision removed sessions outright and declared that every request is self-contained and that servers MUST NOT rely on prior requests over the same connection. ## The replacement: explicit handles The answer the spec settled on is to make cross-call state a first-class part of the tool contract rather than an invisible property of the transport. The server mints a handle and hands it back in a tool result; the client passes it back as an ordinary argument on subsequent `tools/call` requests. The handle is described by the tool's own JSON Schema, exactly like any other parameter — there is no protocol slot for it, no header, no reserved `_meta` key. That is the point: because the handle rides in `arguments`, the request remains self-contained and any node can interpret it. A typical shape: 1. `tools/call` on `open_workspace` returns a result containing `workspaceId`. 2. Later `tools/call` on `run_query` takes `workspaceId` plus the query. 3. `close_workspace` (or a TTL on the server side) releases it. The names are the server's business — nothing about them is standardised. ## What this buys **Visibility.** The handle passes through the host and, usually, through the model's context. A user or an audit log can see that a call is scoped to a particular workspace. Connection state was invisible to everyone but the server. **Deployability.** Nothing about the handle ties the follow-up request to the node that minted it, so an MCP server behind a load balancer needs no session affinity. What it does need is shared storage: if `workspaceId` only exists in one process's memory, you have re-created sticky sessions without telling the load balancer, and a scale-in event or a rolling deploy will start returning unknown-handle errors. **Honesty about lifetime.** The server now has to decide, explicitly, how long the state lives, who owns it, and what cleanup looks like. Under the session model those questions were answered by accident — state died when the connection did. ## What this costs, and the traps **Storage and TTL become your problem.** Every minted handle is an allocation someone has to reclaim. Give handles an expiry, make expiry produce a clear tool error the model can act on, and keep the state small enough that a fleet-wide store is cheap. **Context cost.** Handles usually pass through the model, so keep them short and opaque. Do not encode secrets, credentials or personal data in them; anything embedded in a handle should be assumed to be readable by anyone who sees the transcript. **They are not authentication.** The 2026-07-28 security material replaced "Session Hijacking" with "State Handle Hijacking" and states that possession of a state handle MUST NOT be treated as authentication. Handles leak: into logs, into transcripts, into a model that may echo one to a different server. Bind the handle to the authenticated principal at mint time and re-check that binding on every use against the credential presented on the current request. For a remote server that credential is the audience-bound OAuth 2.1 access token, not `clientInfo` — which is self-reported and untrusted. **Guessability.** A handle is an object reference in a multi-tenant system. Make it unguessable, and treat an unknown or foreign handle exactly as you would treat any unauthorized object access. ## Relation to the rest of the model Handles do not reintroduce sessions by the back door, because they are scoped to the tool that defines them rather than to the connection or the client as a whole. They also do not change the rule that a server's tool, prompt and resource set MUST NOT vary per connection — a handle can change what a call *does*, never which tools exist. And because a dropped response stream in 2026-07-28 forces the client to re-issue the request with a new JSON-RPC id, a handle-taking tool should be designed so a repeat call is either idempotent or explicitly deduplicated; the retry will present the same handle. ## How to answer this in an interview Say that state moved from the connection into the payload, name the mechanism (server-minted handle passed as an ordinary tool argument), and then immediately go to the two operational consequences: shared storage across the fleet, and authorize-on-every-use because a handle is not a credential. That trio is what the question is testing.
- Does using handles quietly reintroduce the sticky routing that removing sessions was meant to eliminate?Only if you store the state in process memory. The handle itself carries no routing information and the client presents it on an ordinary POST that any node may receive, so the fleet stays homogeneous as long as the state lives in shared storage. Keeping it node-local recreates affinity without the load balancer knowing, which fails on scale-in, rolling deploys and node loss.
- What should happen when a client presents a handle that has expired or that it does not own?Treat it as an authorization decision first and a lifecycle one second. If the handle is not bound to the principal on the current request, respond as you would to any unauthorized object access and do not reveal whether it exists. If it is theirs but expired, return a clear tool error saying so, because the model can usefully recover by re-minting; a silent empty result teaches it to loop.
- How do handles interact with a response stream that drops mid-call?Revision 2026-07-28 removed resumability, so the client re-issues the call as a new request with a new JSON-RPC id — presenting the same handle. Design handle-taking tools so that repeat is safe: make the operation idempotent, or accept a client-supplied idempotency key in the arguments and deduplicate on it. Relying on `idempotentHint` does not help; it is an untrusted hint to the client, not enforcement.
- Should a handle ever be a signed token carrying the state itself?It can be, and it removes the shared-store dependency, but it changes the risk profile: the payload travels through model context and logs, it cannot be revoked before expiry unless you keep a denylist, and size grows with the state. Use it for small, non-sensitive, short-lived scope; use an opaque id plus server-side storage for anything large, mutable or sensitive.
A coat-check ticket. The cloakroom keeps the coat, you carry a stub, and the stub works at whichever attendant is on the desk — but the stub alone should not be enough to walk off with someone else's coat.
saying these in an interview costs you the question
- Says the handle belongs in the Mcp-Session-Id header
- Keeps handle state in one node's memory behind a load balancer
- Treats holding a valid handle as proof of identity
- Encodes credentials or personal data inside the handle string
- Thinks handles are a protocol field rather than tool arguments