
MCP is going stateless, and the upgrade breaks in both directions
Revision 2026-07-28 deletes sessions, the initialize handshake, and stream resumability, then puts Roots, Sampling, and Logging on a deprecation clock. Old clients cannot talk to new servers.
tldr: MCP revision 2026-07-28 removes protocol-level sessions, the initialize handshake, the GET stream endpoint, and SSE resumability, and starts a twelve-month deprecation clock on Roots, Sampling, and Logging. Every request now carries its own protocol version and capabilities. A client speaking the new revision cannot talk to a server speaking the old one, and the reverse fails too, so supporting both is the only migration path. As of 28 July 2026 the revision is still a release candidate.
What the revision deletes
The release is best read backwards, through what is missing.
Protocol-level sessions are gone. The Mcp-Session-Id header is removed from Streamable HTTP, the HTTP DELETE that terminated a session is gone, and tools/list, resources/list, and prompts/list no longer vary per connection. A server that needs state across calls is told to mint an explicit handle and pass it back as an ordinary tool argument (SEP-2567).
The handshake is gone with it. There is no initialize, no notifications/initialized. Every request carries its own protocol version, client identity, and capabilities in _meta, under keys like io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. In its place servers MUST implement a new server/discover RPC that advertises supported versions and identity, which clients may call up front or skip entirely (SEP-2575).
The standalone GET stream is gone. Server-to-client change notifications now arrive on the response stream of a single long-lived subscriptions/listen POST, and clients opt in to specific notification types rather than receiving everything. Stream resumability went too: Last-Event-ID and SSE event IDs are removed, and a broken stream loses the in-flight request, which the client must reissue with a new request ID.
Server-initiated requests are gone. Sampling, elicitation, and roots used to arrive as JSON-RPC requests the server pushed at the client. They are now folded into results: a server returns an InputRequiredResult carrying inputRequests, and the client retries the original call with matching inputResponses. The spec calls the pattern Multi Round-Trip Requests (SEP-2322).
Also removed outright: ping, logging/setLevel, and notifications/roots/list_changed.
Why a protocol would delete its own session layer
The spec is unusually direct about the reasoning, and it is not about elegance.
Streamable HTTP now mirrors selected body fields into HTTP headers, and the transport page gives the reason plainly: so that intermediaries such as load balancers, gateways, and observability tooling can route and inspect requests without parsing the body. MCP-Protocol-Version and Mcp-Method are required on every POST, and Mcp-Name carries the tool name or resource URI on tools/call, resources/read, and prompts/get.
Read that alongside the session removal and the direction is obvious. Sessions meant a server instance held state, which meant sticky routing, which meant you could not put ordinary infrastructure in front of an MCP server. Take sessions out and put the routing information in headers, and an MCP endpoint becomes a plain POST endpoint that any proxy already knows how to handle. The new ttlMs and cacheScope fields required on list results point the same way, since they exist so intermediaries can cache tool lists instead of every client polling for them.

There is a security consequence the spec anticipates. Once a load balancer routes on a header while the server executes on the body, the two can disagree, which is a familiar way to smuggle a request past a policy layer. So servers MUST validate that headers match the body and reject mismatches with 400 Bad Request and JSON-RPC error -32020. That check is the price of the header mirroring, and it is the kind of boundary that tends to get skipped in a first implementation. Anyone thinking about what an autonomous agent can reach should treat it as load-bearing rather than ceremonial.
Roots, Sampling, and Logging are on the way out
The deprecations say more about what the last two years taught than the additions do. All three land in the same SEP-2577, remain functional through a twelve-month window, and come with suggested migrations:
- Instead of Roots, pass directories or files as tool parameters, resource URIs, or server configuration.
- Instead of Sampling, integrate directly with the LLM provider's API.
- Instead of Logging, write to
stderron stdio, or use OpenTelemetry.
Every one of those resolves to doing the ordinary thing directly. Sampling was the most conceptually interesting idea in early MCP, a server borrowing the client's model rather than holding its own key, and the recommended replacement is now to just call the provider yourself. A protocol that tells you to route around its own abstraction has learned something about how the abstraction was used.
The Tasks work moved rather than died. It leaves the core protocol for an official extension, io.modelcontextprotocol/tasks, with polling via tasks/get replacing the blocking tasks/result (SEP-2663).
The upgrade is a hard fork in both directions
This is the part that determines your week. The spec splits implementations into modern (per-request metadata, revision 2026-07-28 and later), legacy (the initialize handshake, 2025-11-25 and earlier), and dual-era. The compatibility matrix is blunt about what works:
| Client | Server | Outcome |
| Modern | Modern | Works |
| Modern | Legacy | Fails |
| Legacy | Modern | Fails |
| Dual-era | Modern | Works |
| Dual-era | Legacy | Works |
| Legacy | Dual-era | Works |
Both mixed pairs fail, and they fail differently. A legacy client hitting a modern server has no fall-forward mechanism at all: its initialize is an unknown method, its request is missing required headers, and on HTTP it gets a 400 it cannot interpret. That is why the spec asks modern-only servers to name their supported versions in whatever error they return to an initialize request, since that message may be the only diagnostic a user ever sees.
A modern client hitting a legacy server can fail worse. The server may reject the request, stay silent, or process an era-ambiguous method under legacy semantics, which is the failure mode that looks like it worked.
Since SDKs adopt revisions at their own pace, a mixed fleet is not an edge case. It is the normal condition for the next year, and dual-era support is what carries you through it.
What a server author changes
Concretely, for a Streamable HTTP server:
- Implement
server/discover. It is required, and it is what lets clients probe your era deterministically. - Read version, identity, and capabilities from
_metaon every request instead of holding them from a handshake. - Stop minting session IDs. Ignore
Mcp-Session-Idif an old client sends one, and return405 Method Not Allowedto GET and DELETE on the MCP endpoint. - Validate the three required headers against the body, and return
400with-32020on mismatch, decoding the Base64 sentinel form ofMcp-Namebefore comparing. - Move any cross-call state into explicit handles you return and accept as tool arguments.
- Replace server-initiated requests with
InputRequiredResult, and expect the client to retry the original call rather than answering in-band. - Add
ttlMsandcacheScopeto list and read results. - Decide on dual-era now. A request carrying modern
_metagets served statelessly, aninitializeselects legacy semantics, and one endpoint may serve both.
Clients have the mirror-image job, plus one piece of bookkeeping: era is a property of the server, not the request, so cache the result per server process on stdio or per origin on HTTP, and re-probe only when the cached assumption fails.
When this actually lands
The release candidate was cut on 29 May 2026 and carries the target revision date 2026-07-28. As of 28 July 2026 the MCP versioning page still names 2025-11-25 as the current protocol version, and the most recent tag in the specification repository is 2026-07-28-RC. The RC notes state that changes may still be introduced before the final release and that SDKs will adopt at their own pace.
The SDKs did not wait. On 28 July 2026 the TypeScript SDK published version 2.0.0 as its stable line, implementing this revision, under new package names: @modelcontextprotocol/server and @modelcontextprotocol/client replace the single @modelcontextprotocol/sdk, which stays on 1.x with bug fixes and security updates for at least six months. The Python SDK is a step behind, at 2.0.0rc1 published 27 July 2026, with 1.28.1 still the default install.
That gap is the compatibility matrix arriving early. A TypeScript server on v2 and a Python client on the current stable release are, today, the modern and legacy rows of that table, and they are the pair that fails. Building dual-era support against the RC is safe, since the fallback paths are what you need either way. Dropping legacy support is not.
FAQ
Does this break existing MCP servers already in production?
Not while they keep speaking 2025-11-25 to clients that also speak it. Breakage arrives with the client, not the calendar: the first time an updated client sends per-request _meta to a server that expects initialize, that pair fails. Since clients and servers update independently, the safe assumption is that your server will meet a modern client before you have planned for one.
Is a stateless MCP server actually stateless?
The protocol is; your server does not have to be. What changed is that state stops being implicit in a connection and becomes explicit in an argument. You return a handle, the client passes it back, and you look it up. The practical difference is that any instance can serve any request, which is what makes an MCP endpoint deployable behind a normal load balancer or on serverless infrastructure.
Should I migrate off Sampling now?
There is a twelve-month deprecation window and no removal date, so nothing forces your hand this quarter. But the suggested migration is to call the LLM provider directly, which most servers that wanted a model were already positioned to do, and new implementations are told not to adopt it. Treat it as closed for new work and schedule the replacement for existing work.
Written by
Software Developer @Bug0 | @Hashnode, Contributing writer to The Foreword by Hashnode







