The 2026-07-28 revision of the Model Context Protocol removed the initialize handshake (SEP-2575) and the Mcp-Session-Id header along with the protocol-level session it carried (SEP-2567). Every request now describes itself: protocol version, client info, and capabilities travel in a _meta field on each call, instead of being negotiated once at connection time.
Most of the write-ups are reading this as a scaling story. They are not wrong. Removing the session is what lets any request land on any server instance behind a plain round-robin load balancer, which is what makes MCP servers behave like ordinary HTTP services that run on serverless and edge and survive restarts. That part is real and it is the spec's own headline.
But the scaling is the consequence, not the idea. The interesting question is where the state went, because it did not disappear. It moved somewhere the model can see it.
What actually changed
Under the old model, talking to a server meant running a handshake, getting back a session id, and carrying that id on every later request. The session was a live object inside one specific server process, holding the negotiated state.
Under the new model, there is no handshake and no session id. A tool call carries everything it needs to be understood on its own:
POST /mcp HTTP/1.1
Host: mcp-server.example
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search",
"arguments": { "q": "otters" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0" }
}
}
}
There is nothing pinning this request to a prior connection. No session to resume, no earlier handshake to depend on. The request is self-describing.
The session was write-only, from the model's point of view
Here is the shift that matters, and it is easy to miss under the infrastructure talk.
A session id lived in a header. The protocol wrote it and the transport carried it. The model never touched it. It could not read the session id, could not thread it between tools, and could not recover with it after a failed call. It was state the system held about the conversation, invisible to the thing actually doing the reasoning.
The replacement is different in kind. When a server needs to carry state across calls, it now mints an explicit handle, a basket_id or a browser_id, returns it as an ordinary tool result, and the model passes it back as a normal argument on the next call. The spec is direct about this: removing the protocol-level session does not mean your application has to be stateless. It means stateful applications do what HTTP APIs have always done, which is hand back an explicit handle instead of hiding the state in the transport.
The session was state the model could not see. The handle is state the model reads, carries, and reasons about. Same job, opposite visibility.
You have seen this pattern before, one layer up
If you have designed MCP tools carefully, this move should feel familiar, because it is the same one you make inside a good server.
I wrote about this a while back in Stop Building MCP Wrappers, Build Domain-Specific Tools Instead. The argument there was that a read and a write should never be the same tool. A propose_resolution tool that is structurally read-only, and a separate apply_resolution tool that is the single narrow path to a state change and refuses to run without an explicit human-confirmation flag. The point was that an agent should never slide from read to write in one unsupervised call, and the way you enforce that is by making the write an explicit, visible, separate step.
The 2026-07-28 spec makes the same separation at the transport layer.
Reads got lighter and more shareable: list endpoints like tools/list no longer vary per connection, so their results are cacheable across clients and instances. There is nothing to keep private to a session, because reads carry no continuity.
Writes got explicit: anything that needs to persist across calls now travels as a handle the model passes forward, out in the open, rather than as a hidden session the transport manages behind its back.
Reads fan out and cache. Writes stay explicit and trackable. It is the same principle as splitting propose from apply, arrived at independently, one layer down. Reads and writes have different requirements, so the protocol stopped treating them the same way.
Visible state is auditable state
There is a governance payoff here, and it is the reason this is more than a plumbing change.
State you can see is state you can govern. A handle passed as an argument is something you can log, inspect, rate-limit, and checkpoint. The new Mcp-Method and Mcp-Name headers push this further: a gateway can route and meter traffic on the operation without ever parsing the JSON-RPC body, because the consequential facts about the request are visible at the edge instead of buried inside it.
Hidden session state is the opposite. When continuity lives in a session object inside one process, you cannot easily audit what an agent is carrying between steps, and you cannot cleanly interrupt it. A long autonomous run built on invisible state is exactly the kind of system that can go wrong without anything obvious to catch. Explicit handles turn that invisible thread into something a human, or a policy layer, can actually watch.
Explicit-over-hidden is the same instinct as read/write separation. Make the consequential thing visible to whoever has to reason about it.
What this does not mean
Two honest caveats, because the headline invites overreading.
Protocol-stateless does not mean your application has to be stateless. The spec says this plainly. State did not vanish; it changed location, from an invisible session to an explicit handle. If your server needs continuity, it still has it. It just holds it somewhere the model can see.
And nothing broke on July 28. This is a minimum twelve-month deprecation with backward-compatible negotiation, not a flag day. A v2 server can answer the legacy handshake alongside the new discovery method, so older clients keep connecting, and the SDKs negotiate down to the previous revision when a client does not speak the new one. The stateless path is opt-in until you choose it. Migration is real work for hand-rolled servers, but it is scheduled work, not an emergency.
Where this leaves you
The protocol layer keeps commoditizing. Every team gets the same transport, the same SDKs, the same client support, and now the same stateless core that scales on ordinary infrastructure. That was already the direction, and this revision accelerates it.
What does not commoditize is knowing why the state moved. The session became a handle for the same reason a careful apply_resolution tool needs a confirmation flag: consequential state should be visible to the thing that has to reason about it. The protocol just made that a rule instead of a recommendation.
Top comments (1)
The visibility shift is useful, but an explicit handle is not automatically safer than a hidden session. Once
basket_idorbrowser_identers model context, it can be copied into traces, memory, or another tool call. If possession alone grants access, the handle is a bearer capability with a much larger exposure surface.I’d keep the model-visible value opaque and non-secret, then enforce continuity server-side by binding it to the authenticated principal, tenant, client/audience, resource type, allowed operations, and expiry. Add rotation/revocation, a version or fencing token for stale concurrent calls, hard object/count limits, and redaction from ordinary logs. Negative tests should cover cross-user reuse, cross-tenant reuse, replay after expiry/revocation, and a handle passed to the wrong tool.
The same nuance applies to caching list results: protocol-stateless does not mean globally identical. Cache keys still need the effective principal, authorization scope, server/version digest, and policy revision whenever tool visibility is scoped. Explicit state is easier to govern only if the handle is treated as a reference to authority, not as the authority itself.