AI Blog

MCP went stateless, and the state just moved

The 2026-07-28 MCP spec deleted the initialize handshake and the session-id header, so a server can now run behind a plain round-robin load balancer. That operational win is real — but statelessness is a transport property, not a system property. The session did not disappear; its bookkeeping moved onto every request, and the durability that long-running agents actually need came back in through the AWS-contributed Tasks extension as explicit handles. Read the two together before you celebrate a simpler protocol.

By Agentic AI Wiki 13 min read

The 2026-07-28 revision of the Model Context Protocol deleted the two things that made an MCP server stateful: the initialize/initialized handshake and the Mcp-Session-Id header. A remote server can now sit behind a plain round-robin load balancer with no shared session store — a genuine operational win. But "stateless" describes the wire, not the system. The session's bookkeeping did not vanish; it moved onto every request and into your application. And the durability that long-running agents actually need came straight back through a new extension. Read the pivot as a relocation of state, not a deletion of it, and the migration gets a lot less surprising.

What changed

The headline of the 2026-07-28 spec is a stateless protocol core. Previously an MCP client opened a session with an initialize exchange, received an Mcp-Session-Id, and every later request rode on that session — which meant the transport was stateful and the infrastructure had to keep a client pinned to the server instance that held its session. That handshake and that header are both gone. Every request now carries its own protocol version, client identity, and capabilities inline, so any request can land on any instance.

ConcernBefore (stateful core)After (2026-07-28)
Session setupinitialize / initialized handshakeNone — every request is self-describing
Session identityMcp-Session-Id headerRemoved
Per-request metadataNegotiated once at initMCP-Protocol-Version, Mcp-Method, Mcp-Name, and a _meta object carrying clientInfo
Capability discoveryReturned by the handshakeOptional server/discover call, or cached by the client
Load balancingSticky sessions, shared session storePlain round-robin, route on Mcp-Method

Alongside the stateless core, the revision formalised an extensions framework (Tasks and MCP Apps join the existing Enterprise Managed Authorization), hardened authorization, and — quietly the most useful line for anyone operating a server — added a formal deprecation policy with a twelve-month minimum window. Roots, Sampling, and Logging are the first features to go on that clock.

The operational win is real

The stateful core versus the stateless core Two panes. On the left, the pre-2026-07-28 stateful server: a client performs an initialize handshake and receives a session id, so a sticky load balancer must route every later request to the one instance that holds that session, backed by a shared session store. On the right, the 2026-07-28 stateless server: each request is self-describing through headers and a meta field, a plain round-robin load balancer forwards it to any instance, and there is no session store. Stateful core (before) Client initialize → gets Mcp-Session-Id Sticky load balancer — session affinity Instance A Instance B holds session Instance C Shared session store WHAT IT COSTS restart drops every pinned session store is its own availability dependency Stateless core (2026-07-28) Client every request: headers + _meta Round-robin load balancer — no affinity Instance A Instance B Instance C WHAT IT BUYS any request → any instance, no store deploy is an ordinary rolling restart
Delete the handshake and the session id, and the request stops needing to find the instance that remembers it.

If you have ever run a stateful remote server at scale, the appeal is immediate. Sticky routing is a tax that shows up everywhere: the load balancer needs session affinity, a restarted instance drops every session pinned to it, autoscaling has to drain rather than just terminate, and a shared session store becomes a availability dependency of its own. A self-describing request removes all of it. Any instance can serve any call, a deploy is a rolling restart with no session migration, and the gateway can route on the Mcp-Method header without cracking open the body. For a server that is mostly tools/list and tools/call, this is the difference between running like a normal HTTP API and running like a WebSocket fleet.

None of that is marketing. It is the same reason REST APIs are stateless: statelessness at the transport is what lets horizontal scale be boring. The mistake is to conclude that because the transport forgot the session, the system no longer has one.

Statelessness is a property of the wire, not of the problem

A protocol can be stateless while the interaction it carries is not. The client still needs to know which protocol version and capabilities are in play; the spec's answer is that this now travels inline on every request, in _meta and the new headers, instead of being agreed once. That is not less state — it is the same state, re-sent, with the client responsible for carrying it and, if it wants to avoid a discovery round-trip, for caching the server's capabilities itself.

So the ledger of who-holds-what shifted rather than shrank:

Where each concern lives, before and after the pivot A matrix of four concerns across two protocol versions. Session identity: held by the server in the 2025 stateful core, carried inline by the client on every request in the 2026-07-28 stateless core. Capability discovery: returned at the handshake before, cached by the client or fetched on demand after. Long-running work: sat awkwardly in the core before, moved into the Tasks extension as durable handles after. Load balancing: needed sticky sessions before, plain round-robin after. Every row moved; none of the concerns disappeared. The session's job, redistributed 2025 · STATEFUL CORE 2026-07-28 · STATELESS + TASKS Session identity Held by the server, keyed by id Carried inline by the client per request Capability discovery Returned once at the handshake Client-cached or server/discover Long-running work Awkward in the core session Tasks extension, durable handles Load balancing Sticky sessions, shared store Plain round-robin, any instance Neutral fill = the protocol owned it. Accent fill = it moved to the client, an extension, or your app.
Every row moved. None of the concerns went away — the protocol stopped owning them and pushed them to the client, an extension, or your own tool contracts.

This is a familiar trade, and usually a good one: a simpler core with more responsibility at the edges. But it is worth being honest that the edges are now yours. The server no longer remembers your client between calls, which means anything the interaction needs to remember — a multi-step negotiation, a partially built request, the fact that this is the same agent as a second ago — is your application's job to thread through, or the extensions' job to hold.

Tasks bought the durability back — as handles

The clearest evidence that the state did not disappear is that the spec had to add a place to put it. Tasks — one of the first official extensions, contributed by AWS — moved out of the experimental core into io.modelcontextprotocol/tasks specifically to support reliable, long-running agents. A long-running tool call is the most stateful thing MCP does: you start work, you come back later, you expect it to still be there. A stateless core cannot express that on its own, so Tasks does, with poll-based tasks/get and tasks/update calls and an opt-in subscriptions/listen stream for change notifications.

The mechanism is the tell. The protocol stays stateless because the state lives behind an explicit handle: a tool returns a task handle, the client stores it, and passes it back to poll or update. The durable work is real and server-side; what makes it compatible with a stateless wire is that the reference to it is a value the client carries, exactly like the _meta block carries the session facts. The pattern is consistent across the whole revision — the model of "server remembers you" was replaced everywhere by "you carry a reference the server can resolve."

Three homes for what used to be one session Three columns showing where state lives after the pivot. The stateless core holds only per-request facts carried inline in headers and the meta field, with no memory between calls. The Tasks extension holds durable long-running work server-side, addressed by a task handle the client stores and passes back. Your application holds any multi-step or cross-call context the interaction needs, threaded through tool arguments and handles. STATELESS CORE Per-request facts carried inline in headers + _meta no memory between calls TASKS EXTENSION Durable work held server-side, addressed by a task handle the client stores and passes back YOUR APPLICATION Cross-call context any multi-step or between-call state, threaded through tool arguments and handles
Three homes for what used to be one session. The core forgets, the extension remembers on request, and the rest is yours.

This is a better design than a stateful core that tried to serve both the trivial tools/list call and the hour-long agent job with one session model. But "MCP is stateless now" is a half-sentence. The full one is: the core is stateless, durability is an opt-in extension, and cross-call context is a client responsibility — three answers where there used to be one, each better suited to its job, and none of them free.

The parts that will bite quietly

The stateless pivot is what everyone will talk about, but two less-loud changes in the same revision are the ones likely to break a real deployment.

  • Authorization hardening changes correct clients. Authorization servers must now return the iss parameter per RFC 9207 and clients must validate it before redeeming an authorization code — a real fix for authorization-server mix-up attacks, and a behavioural requirement your client either meets or fails. Dynamic Client Registration is now formally deprecated in favour of Client ID Metadata Documents (CIMD), though DCR still works for now, and credentials are bound to their issuing authorization server so you cannot reuse them across servers.
  • The deprecation clock is now a feature you can plan against — if you read it. A twelve-month minimum window is a gift compared to the old "it changed, react" cadence, but Roots, Sampling, and Logging are already on it. If your server or client leans on any of the three, the migration is now dated, and pretending it is not just means doing it in a hurry later.

Neither of these is a stateless-core problem; both ride in on the same revision, and both are the kind of change that passes CI and fails in production against a real authorization server or an old feature you forgot you used.

What to actually do

If you run an MCP server

Audit for session assumptions before you upgrade — anything that expected a client to be pinned to an instance, or that stashed per-client state keyed by session id, has to move to a shared store the client addresses by handle, or go away. The reward is that once it does, you can drop sticky routing and the session store entirely and run the server like a normal stateless service. Decide separately whether you need the Tasks extension: if your tools are fast request/response calls you may never touch it; if you expose long-running work, adopt it rather than reinventing durable jobs on the side.

If you build an MCP client or host

Your client now owns capability caching and the per-request metadata it used to receive once. Implement the iss validation now — it is not optional for a correct OAuth flow — and plan the move from DCR to CIMD on the twelve-month clock rather than the day it is removed. If you rely on Roots, Sampling, or Logging, put the migration on a roadmap with a date, because the spec now gives you one.

If you are choosing an MCP stack

Prefer implementations that already speak 2026-07-28 and ship it in their SDKs — the release came with updated TypeScript, Python, Go, and C# SDKs, so "supports the latest spec" is a checkable claim, not a promise. And treat statelessness as the operational feature it is, not as a reason to skip designing where your interaction's state will live. It will live somewhere; the only question the spec settled is that it will not be in the core.

The durable principle: a protocol going stateless relocates state, it does not remove it. The 2026-07-28 core is genuinely simpler to operate, and that simplicity is worth taking. But the session's job got redistributed — inline onto every request, into the Tasks extension as handles, and into your application as cross-call context — and the teams that get burned will be the ones who read "stateless" as "I no longer have to think about state." You do; you just get to choose where it lives now.

FAQ

Does upgrading to the 2026-07-28 spec break my existing MCP integration?

It can. The removed initialize handshake and Mcp-Session-Id header mean any client or server that assumed a persistent session must change, and the new iss validation is a hard requirement for a correct OAuth flow. Fast tool servers and simple clients port easily; anything that stored per-session state, or relied on Dynamic Client Registration, needs work.

Is a stateless protocol just worse for long-running agents?

No — that is what the Tasks extension is for. Long-running work stays durable and server-side; the stateless core is compatible with it because the client holds a task handle and polls or subscribes, rather than the server keeping the client pinned. You get durability without sticky sessions.

Do I have to adopt the Tasks extension?

Only if you expose long-running work. If your server is fast request/response tool calls, the stateless core alone is enough and you may never touch Tasks. Extensions are opt-in by design; the point of moving Tasks out of the core was so servers that do not need it do not carry it.

What is the practical benefit of dropping the session handshake?

A remote server can run behind a plain round-robin load balancer with no session affinity and no shared session store, route on the Mcp-Method header without parsing the body, and treat a deploy as an ordinary rolling restart. It is the same reason stateless REST APIs are easier to scale than stateful connections.

What is CIMD and why is DCR deprecated?

Client ID Metadata Documents let a client be identified by a URL that resolves to its metadata, rather than registering dynamically with each authorization server (Dynamic Client Registration). DCR still works for backward compatibility but is now formally deprecated; combined with binding credentials to their issuing server, the change closes a set of client-registration and mix-up weaknesses.

Further reading

On this wiki:

Sources: