MCP Just Went Stateless: What Breaks and What Gets Much Easier

If you have ever tried to run a remote MCP server behind a load balancer and discovered that request #2 cannot tell what request #1 did, this post is for you. One of the largest changes to the protocol since launch is aimed squarely at that pain, and it does it by removing things.
Everything below describes the 2026-07-28 MCP specification, which was ratified on 28 July 2026 and is now the current stable revision. The session-removal work is SEP-2567 and SEP-2575. One practical caveat, and it is a Java one:
| SDK | Tier | Speaks 2026-07-28 |
|---|---|---|
| TypeScript, Python, C#, Go, Rust | 1 | yes |
| Java | 2 | not yet: the latest release, 2.0.1 of 19 August 2026, patches a 2.0.0 that tracks 2025-11-25 |
Tier 2 gives the Java SDK six months after a spec version ships to catch up. Its issues for SEP-2575, SEP-2577 and SEP-2663 are all still open, so the stateless protocol described here is not yet something you can adopt from Java. Check the Java SDK's release notes before planning a migration.
The session problem
Today's remote MCP relies on a session: the client and server do an initialization handshake, the server issues an Mcp-Session-Id, and subsequent requests carry it. That works beautifully on one process and miserably across many. The moment you want to scale horizontally, that session id becomes a sticky leash, because every request for a session has to land on the one instance that holds its state. So you reach for sticky load balancing, shared session stores and a lot of operational duct tape.
Session s1 exists only inside instance A, so under 2025-11-25 instance B answers 404 Not Found and the client must open a new session and repeat the handshake.
What changed
The 2026-07-28 revision removes session management: the Mcp-Session-Id header is gone, and so is the initialize/notifications/initialized handshake. Each request now carries its protocol version and client capabilities in _meta instead. Clients SHOULD send their identity there too. Servers gain a new server/discover method that returns their supported protocol versions, capabilities and identity in a single request. Note the asymmetry, because it is easy to read this as an optional extra: calling it is optional for a client, which is free to fire any request and handle a version error if one comes back. But implementing it is mandatory, and every 2026-07-28 server MUST answer it. The protocol becomes effectively stateless at the transport layer: any request can be served by any instance, because no instance is special.
The basket_id travels as an ordinary tool argument, so the second call succeeds on an instance that never saw the first.
This is one of the largest revisions since MCP launched, and it lines up with the roadmap that was in force while it was drafted, which named Transport Evolution and Scalability and Enterprise Readiness among its four priority areas. Notably, it does this without introducing a new transport: Streamable HTTP remains the one remote transport.
Don't read that as "nothing else moved", though. Streamable HTTP itself changed underneath. Side by side:
| What | 2025-11-25 | 2026-07-28 |
|---|---|---|
| Session identity | Mcp-Session-Id header, issued with the InitializeResult | removed, and durable state travels as a handle in tool arguments (SEP-2567) |
| Version and capabilities | negotiated once, by initialize and notifications/initialized | carried on every request in _meta (SEP-2575) |
| Server discovery | the initialize result | server/discover, which every server MUST implement |
| Server-to-client requests | JSON-RPC requests sent on an SSE stream | an InputRequiredResult with resultType: "input_required", answered when the client retries (SEP-2322) |
| Change notifications | HTTP GET stream, plus resources/subscribe and resources/unsubscribe | one long-lived subscriptions/listen stream, opted into per notification type, and the GET endpoint is gone |
| Broken stream | resumable with Last-Event-ID | re-issue the request with a new request id |
| Resource not found | -32002 | -32602 (Invalid Params) |
| Required POST headers | MCP-Protocol-Version | plus Mcp-Method on every request, and Mcp-Name on tools/call, resources/read and prompts/get (SEP-2243) |
| List and read results | nothing in the result described its cache lifetime | ttlMs and cacheScope are required (SEP-2549) |
Only the first row changes how you deploy. The rest change what your code sends and what it expects back. SSE, in the table, is the Server-Sent Events stream a server opens when it answers one request in pieces.
What breaks
Be honest with yourself about what leaned on the session:
- Server-held per-session state now needs somewhere explicit to live. If your server stashed "what we're doing" in memory keyed by session id, that assumption is gone.
- Code that reads or asserts
Mcp-Session-Idneeds to stop. - Init-handshake-dependent flows must tolerate a world without a handshake to hang setup on.
- Methods that no longer exist.
ping,logging/setLevelandnotifications/roots/list_changedwere removed outright. Log level is now set per request, viaio.modelcontextprotocol/logLevelin_meta. - Anything that leaned on stream resumption. The
Last-Event-IDheader and SSE event IDs are gone. Re-issue the request instead of resuming it. - Hardcoded error codes. Beyond resource-not-found moving to
-32602(Invalid Params), the codes introduced in this revision were renumbered into a reserved-32020to-32099band. - Every result needs a
resultType. It is a required field now:"complete"for an ordinary result,"input_required"for the interim result of a multi round-trip request. Clients must read a missing one from an older server as"complete", but a new server has to emit it. - List and read results need cache metadata.
ttlMsandcacheScopeare required on the results oftools/list,prompts/list,resources/list,resources/readandresources/templates/list(SEP-2549).ttlMsis a freshness hint in milliseconds;cacheScopeis"public"or"private", and decides whether a shared intermediary, a gateway or cache between client and server, may cache the response. - New required request headers. Mirroring
Mcp-MethodandMcp-Namefrom the body into headers lets intermediaries route and inspect requests without parsing the body (SEP-2243).
A complete tools/call POST from the Streamable HTTP page, with _meta carrying what the handshake used to settle once per session:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "Seattle, WA"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
A server that finds a header disagreeing with its body field must reject the request with 400 Bad Request and error code -32020, HeaderMismatch.
The authorization half
Session removal took the headline, and it is the bigger change to your architecture. If you run a remote server, though, the authorization changes in the same revision are likely to reach your code first, because they govern what happens before a client ever calls a tool.
- Dynamic Client Registration is deprecated. RFC 7591 registration gives way to Client ID Metadata Documents, where a client identifies itself with an HTTPS URL that serves its own metadata. DCR keeps working for authorization servers that cannot do CIMD yet, and it sits on the same twelve-month deprecation clock as the other features deprecated in this revision: Roots, Sampling and Logging. The outright removals, sessions included, had a clean break instead.
- Authorization servers should return
issin authorization responses (RFC 9207), and clients MUST validate a presentissagainst the issuer they recorded before redeeming the code. This is the mitigation for mix-up attacks, where an attacker-controlled authorization server gets a client to send it a code issued by an honest one. PKCE (Proof Key for Code Exchange) alone does not stop that, because the client hands itscode_verifierto whichever token endpoint it was steered towards. - Client credentials are bound to their issuer. Clients must key persisted credentials by issuer identifier, must not reuse them against a different authorization server, and must re-register when the authorization server changes.
What gets much easier
This is the trade you are being offered, and it's a good one:
- Horizontal scaling becomes boring: spin up N identical instances behind a plain round-robin balancer, no stickiness, no shared session store required for the protocol's sake.
- Resilience improves: an instance dying no longer orphans a session's worth of in-memory state.
- Deploys get simpler: rolling restarts stop being a session-eviction event.
State doesn't vanish; it just has to become explicit. Anything durable moves to your data layer and is referenced by a handle you pass in requests, instead of living implicitly in a session on one box.
Migrating, roughly
The migration shape is:
- Inventory everything that depends on the session or the handshake.
- Move per-session memory into an external store, keyed by an explicit handle you pass in requests.
- Stop emitting and requiring
Mcp-Session-Id. - Put your instances behind a stateless balancer and delete the sticky-session config you no longer need.
- Add
resultTypeto every result you return, andttlMsandcacheScopeto your list and read results. - Implement
server/discover.
The trap in step 2
Step 2 deserves slowing down over, because the whole migration lives in it and the vulnerable version of it looks identical to the safe one.
The spec gives the failure a name: State Handle Hijacking. Your server mints a handle for an authenticated user and returns it in a tool result. An attacker obtains or guesses that handle, calls your tools with it as an ordinary argument, and your server operates on the first user's state. The session id you just deleted was at least a secret the transport managed on your behalf. A handle is a plain string in a tool argument, and the model carries it around inside a context window.
Three rules, stated normatively in the spec's security best practices:
- Servers that implement authorization MUST verify every inbound request, and MUST NOT treat possession of a state handle as authentication. A handle is a name, not a capability.
- Servers SHOULD generate handles with a secure random number generator and avoid predictable or sequential identifiers. On an unauthenticated server the handle is necessarily a bearer token, a string that grants access to whoever holds it, so SEP-2567 asks there for at least 128 bits of cryptographically secure entropy and a bounded lifetime.
- Servers SHOULD bind the handle to the authenticated user on the server side. The pattern the spec suggests is keying stored state as
<user_id>:<handle>, where the user id comes from the verified token instead of from the client, and rejecting a handle presented by any other principal. With that in place, guessing a handle does not get an attacker anywhere.
With the three rules in place, an inbound handle takes this path:
The key lookup stops the attack: it is built from the caller's own verified user id, so a guessed bsk_a1b2c3 resolves to a key that does not exist.
Two smaller design notes from SEP-2567, which marks this guidance as non-normative, and both are cheap to follow. Keep handles opaque, because a handle that encodes internal structure, cart_user42_2026-03-11, invites clients to parse it and models to guess it. And state the retention policy in the creating tool's description, along the lines of "baskets expire after 24 hours of inactivity", so the model can see it at the moment it decides to create state.
The bottom line
MCP is trading a convenience (implicit session state) for the thing enterprises actually need (trivial horizontal scaling). It's the right trade, and the ink is now dry. The spec is no longer the constraint; in Java, the SDK still is. Make your state explicit now, because that part runs on today's Java SDK, and pick up the protocol-level changes when the Java SDK ships them.
This is the spine of the stateless half of our Building Remote MCP Servers course, which goes deeper on Streamable HTTP, explicit state handles, routing, and horizontal scaling.
Further Reading
- Key Changes in the 2026-07-28 specification: the full list of what this revision changed, with the SEP number behind each item.
- SEP-2567: Sessionless MCP via Explicit State Handles: the case for removing sessions, a worked shopping basket example, and the non-normative guidance on shaping handles.
- SEP-2575: Make MCP Stateless: the other half of the change, covering why the
initializehandshake went and whatserver/discoverdoes in its place. - Streamable HTTP: the transport as it now stands, with worked POST examples and the rules for a server that still meets older clients.
- Security Best Practices: the State Handle Hijacking section quoted in this post, alongside the confused deputy, token passthrough and mix-up attacks.
- Versioning and Compatibility: how a client and a server agree on a version without a handshake, with a matrix for every mix of old and new implementations.
- Client Registration: the three registration mechanisms in priority order, and an example Client ID Metadata Document.
- Roadmap: where the protocol goes after this revision, including HTTP over stdio and ETags on top of
ttlMs.
Sources
- Key Changes in the 2026-07-28 specification: that sessions and the
Mcp-Session-Idheader were removed along with theinitializehandshake. Also that the HTTP GET endpoint andresources/subscribebecamesubscriptions/listen, thatping,logging/setLevelandnotifications/roots/list_changedwere removed, that SSE resumability went, and thatresultType,ttlMsandcacheScopebecame required. - Overview: statelessness, error codes and
_meta: the reserved-32020to-32099band, the move of resource-not-found to-32602, and that_metarequires the protocol version and the client capabilities while the client's identity is a SHOULD. - Streamable HTTP: the quoted
tools/callPOST,Mcp-Methodon all requests withMcp-Nameontools/call,resources/readandprompts/get, the400 Bad Requestwith-32020on a header mismatch, and the sentence that resumable SSE streams viaLast-Event-IDare unsupported. - Transports: that this revision defines two standard bindings, stdio and Streamable HTTP, so Streamable HTTP is the remote one.
- Transports, 2025-11-25: the session model in the first diagram, including the
404 Not Founda server must return for a session id it no longer holds. - Versioning and Compatibility: that servers MUST implement
server/discoverwhile clients MAY call it and stay free to send any request inline. - Security Best Practices: the name State Handle Hijacking, the rules on verifying every inbound request and on binding a handle to the authenticated user as
<user_id>:<handle>, and that PKCE alone leaves the mix-up attack open. - Authorization: that an invalid or expired token MUST receive an HTTP
401, which is the first branch of the handle flowchart. - SEP-2567: Sessionless MCP via Explicit State Handles: the non-normative guidance on opaque handles and on documenting retention in the tool description. Also the 128 bits of entropy asked of an unauthenticated server, and the clean break that removed sessions without a deprecation window.
- Client Registration: that Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents, and the rules on keying credentials by issuer and re-registering when the authorization server changes.
- Deprecated Features registry: that Dynamic Client Registration, Roots, Sampling and Logging share an earliest removal of the first revision released on or after 2027-07-28.
- Feature Lifecycle and Deprecation Policy: the minimum deprecation window of at least twelve months, and how it is measured.
- SDKs and the SDK Tiering System: the five Tier 1 SDKs, the Java SDK's place in Tier 2, and the timeline each tier commits to for new protocol features.
- MCP Java SDK releases and the Java SDK issue search: that
2.0.1of 19 August 2026 is the latest release and that the 2.x line tracks2025-11-25. Issues #1011, #1012 and #1013, for SEP-2575, SEP-2577 and SEP-2663, are all still open. - Roadmap and its archived snapshot of 6 June 2026: the four priority areas the roadmap listed while this revision was drafted, Transport Evolution and Scalability and Enterprise Readiness among them.
- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification: the
issparameter and the mix-up attack it defends against. - RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol: the registration mechanism this revision deprecates.