MCP went stateless: What changed in the 2026-07-28 spec
Sessions, the initialize handshake, and stream resumability are all gone. Here is what replaced them, and what breaks if you ignore it.
The Model Context Protocol has shipped four spec revisions in eighteen months, and most of them added things. The 2026-07-28 revision is different: its headline changes are subtractions. Protocol-level sessions are gone. The initialize handshake is gone. Stream resumability is gone. So are ping and logging/setLevel.
What is left is a protocol that behaves like an ordinary HTTP API. Every request carries everything the server needs to handle it, so any request can land on any instance behind a plain round-robin load balancer, with no shared session store and no sticky routing. That was the single most requested change from teams running MCP servers in production.
The revision landed at the end of July. All four Tier 1 SDKs have since caught up, the migration notes have settled, and it is now clear which of these changes are mechanical and which ones need real rework. Two of them need real rework, and neither is the change the release notes lead with.
Here is what changed, what it costs you, and what to do about it.
The two changes that will cost you the most
Most of this revision is find-and-replace work. Two items are not, and both are easy to miss because neither announces itself as a problem.
Stream resumability was removed, and nothing will tell you. The Last-Event-ID header and SSE event IDs are gone from Streamable HTTP, which means a broken response stream now loses the in-flight request outright. Under the old transport a dropped stream could be resumed and undelivered messages redelivered. Now the client has to re-issue the request with a new request ID.
Nothing errors when you migrate. Your tests pass. What you get instead is a quiet reliability regression that only shows up under real network conditions, as occasional lost tool calls. For an idempotent read that is harmless. For a tool that charges a card, sends an email, or provisions something, a lost request that the client then retries is a duplicated side effect. The spec does not solve this for you: side-effecting tools need an idempotency key supplied as a parameter, and that is application work nobody will prompt you to do.
The elicitation rewrite is an architectural inversion, not a rename. If you built on the 2025-11-25 elicitation design, notifications/elicitation/complete and the elicitationId field were removed outright, not deprecated. There is no twelve-month window on these.
The replacement, multi round-trip requests, turns a callback into a retry loop. The server no longer initiates a request back to the client and waits; it returns an interim result and the client comes back with the answers attached. That is a different control flow, and code written against the old model does not adapt to it incrementally. Budget accordingly.
Everything else in the release is closer to bookkeeping. The rest of this post walks the full change list, then gives you an ordered migration checklist.
The stateless core
Until this revision, an MCP connection opened with a handshake. The client sent initialize, the server replied with its capabilities, the client confirmed with notifications/initialized, and both sides carried an Mcp-Session-Id header for the life of the connection. The list endpoints could vary per connection, because the server knew who it was talking to.
All of that is removed. There is no handshake and no session header. Every request now carries its own protocol version and client capabilities inline, in _meta:
The relevant _meta keys are io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. Clients should also identify themselves on each request via io.modelcontextprotocol/clientInfo, and servers should identify themselves in each result's _meta via io.modelcontextprotocol/serverInfo. A version the server cannot speak comes back as UnsupportedProtocolVersionError.
One consequence that is easy to miss: because tools/list, resources/list, and prompts/list no longer vary per connection, you cannot use the session to serve a different tool catalog to different clients. If your server did that, the capability has to move into the request itself or into your authorization layer.

server/discover, and who it is optional for
The spec replaced the handshake with a new server/discover RPC that advertises supported protocol versions, capabilities, and identity. The asymmetry here matters and is often reported wrong: servers must implement server/discover. Clients may call it.
So a client is free to skip discovery entirely and go straight to tools/call. If it wants to pin a protocol version up front, or probe for backward compatibility over stdio, server/discover is there. But it is not a handshake, because nothing is negotiated and no state is retained on either side.
How to hold state now
Dropping the protocol session does not mean your application has to be stateless. It means state has to be explicit.
The pattern the maintainers recommend is that a server mints a handle and returns it from a tool, and the model passes that handle back as an ordinary argument on later calls. If you have a multi step workflow that used to lean on session context, this is the migration path: return an opaque workflowId or cursor from the first tool, and accept it as a parameter on the rest.
This is better than it sounds. Session state was invisible to the model, which meant the model could not reason about which context it was operating in, and a session dropping mid workflow was an unrecoverable surprise. A handle is a value the model can see, hold, and pass deliberately.
Multi round-trip requests
Sessions did buy one genuine capability: with a stream held open in both directions, a server could interrupt a call to ask the user something. That is how elicitation/create, sampling/createMessage, and roots/list worked. None of them survive a stateless transport.
Multi round-trip requests, or MRTR, replace all three. Instead of the server initiating a request back to the client, the server returns an interim result:
The client gathers the answers and retries the original request with them attached in inputResponses. The flow is a retry loop rather than a callback, which is exactly what makes it work without a persistent connection.

Two details worth pinning down:
resultTypeis now required on every result, not just interim ones. Ordinary results carryresultType: "complete". Clients must treat a result from an older server that omits the field as"complete".- The elicitation completion signal is gone.
notifications/elicitation/completeand theelicitationIdfield, both introduced in2025-11-25, were removed. Under MRTR the client learns the outcome by retrying, so a server-initiated completion notification no longer fits. A server that needs to correlate an out-of-band elicitation across retries encodes its own identifier inrequestState.
If you shipped against the 2025-11-25 elicitation design, this is the part of the migration that will take real work.
Header-based routing
Streamable HTTP POST requests must now include two standard headers: Mcp-Method and Mcp-Name. In the example above, those are tools/call and search.
This is a small change with outsized operational value. Your gateway, rate limiter, or WAF can now route, meter, and authorize on headers, without parsing a JSON-RPC body to find out what the request is trying to do. Per tool rate limits stop requiring body inspection. So do per tool authorization policies, which is the part most teams end up needing first.
The revision also adds x-mcp-header, which lets tool parameters supply custom headers.
List results are cacheable
tools/list, prompts/list, resources/list, resources/read, and resources/templates/list now return two required fields through a new CacheableResult interface:
ttlMs, a freshness hint in milliseconds, so clients can cache instead of polling.cacheScope, either"public"or"private", controlling whether shared intermediaries are allowed to cache the response.
Both complement the existing listChanged notifications rather than replacing them. Servers should also return tools from tools/list in a deterministic order, which helps client-side caching and, more usefully, keeps upstream LLM prompt caches stable across reconnects. If your tool ordering is currently nondeterministic, for example because it comes out of a map iteration, you are invalidating your clients' prompt caches on every reconnect for no reason.
Notifications moved to subscriptions/listen
The HTTP GET endpoint is gone, and so are resources/subscribe and resources/unsubscribe. In their place is a single subscriptions/listen stream: one long-lived POST response that carries opted-in server-to-client change notifications.
Clients opt in per type: toolsListChanged, promptsListChanged, resourcesListChanged, and resourceSubscriptions. The server acknowledges and tags each notification with io.modelcontextprotocol/subscriptionId.
Request-scoped notifications work differently and did not move. notifications/progress and notifications/message still flow on the response stream of the request they belong to, not on the subscriptions/listen stream.
Removed outright
These are gone now, not deprecated:
ping. Use ordinary transport-level health checking.logging/setLevel. Log level is set per request viaio.modelcontextprotocol/logLevelin_meta. Servers must not emitnotifications/messagefor a request that did not include the field, which is a meaningful behavior change if you were logging unconditionally.notifications/roots/list_changed.- SSE stream resumability. The
Last-Event-IDheader and SSE event IDs are removed from Streamable HTTP. A broken response stream now loses the in-flight request, and the client must re-issue it as a new request with a new request ID.
The resumability removal is the one to take seriously, for the reasons covered at the top of this post: it is a silent reliability regression, and protecting side-effecting tools against duplicated retries is now your job rather than the transport's.
There were also some error code changes. Resource not found moved from -32002 to -32602 (Invalid Params) to match JSON-RPC. And a new allocation policy splits the server-error range, reserving -32020 to -32099 for the spec and leaving -32000 to -32019 implementation-defined, with existing SDK usage grandfathered. Three codes introduced during the draft were renumbered accordingly: HeaderMismatch to -32020, MissingRequiredClientCapability to -32021, and UnsupportedProtocolVersion to -32022.
Deprecated, with a twelve-month clock

The revision also adopted a formal feature lifecycle: features are Active, Deprecated, or Removed, with a minimum twelve-month deprecation window and a public registry of everything currently deprecated. That is the governance change that makes the rest of this list plannable rather than alarming.
Newly deprecated:
- Roots, Sampling, and Logging. All three keep working through the window. The suggested migrations are concrete: pass directories or files as tool parameters, resource URIs, or server configuration instead of Roots; integrate directly with an LLM provider API instead of Sampling; log to
stderron stdio or use OpenTelemetry instead of Logging. - The HTTP+SSE transport, soft-deprecated since
2025-03-26, is now formally Deprecated. Migrate to Streamable HTTP. - The
includeContextvalues"thisServer"and"allServers". Omit the field or use"none". They will be removed no later than Sampling itself. - OAuth 2.0 Dynamic Client Registration, covered below.
Dynamic client registration gives way to CIMD
Dynamic Client Registration (RFC 7591) is deprecated as a client registration mechanism, in favor of Client ID Metadata Documents. DCR remains available for backward compatibility with authorization servers that have not implemented CIMD yet.
The rest of the authorization work in this revision is hardening, and it reflects where implementers were actually losing time:
- Authorization servers should include the
issparameter in authorization responses per RFC 9207, and clients must validate a presentissagainst the recorded issuer before redeeming the code. This closes an authorization-server mix-up attack. - Client credentials are bound to the authorization server that issued them. 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.
- Clients must specify an appropriate
application_typeduring DCR, which is what stops authorization servers from rejectinglocalhostredirect URIs. If you have ever debugged a mysteryredirect_urierror in a desktop or CLI client, this was usually why.
If you are choosing between the two registration mechanisms, we wrote up the tradeoffs in CIMD vs. DCR.
Tasks became an extension
Experimental tasks moved out of the core protocol into an official extension, io.modelcontextprotocol/tasks, and got redesigned on the way. The blocking tasks/result method is replaced by polling with tasks/get. A new tasks/update carries client-to-server input. tasks/list is removed. And servers can now return task handles unsolicited, without a per-request opt-in.
The extensions framework itself is the broader story here: ClientCapabilities and ServerCapabilities both gained an extensions field, and Tasks now sits alongside MCP Apps and Enterprise Managed Authorization as an official extension rather than an experimental core feature. Long-running agent work is covered in more depth in MCP async tasks.
Your migration checklist
In rough order of how likely it is to bite:
- Remove every dependency on
Mcp-Session-Id. Replace session-scoped state with server-minted handles passed as tool arguments. - Delete the
initializehandshake. Implementserver/discoveron the server side. Decide whether your client needs to call it at all. - Set
resultTypeon every result you return. Treat a missingresultTypefrom an older server as"complete". - Rewrite elicitation and sampling flows onto MRTR. Return
input_requiredwithinputRequests, and handle a retry carryinginputResponses. If you usedelicitationId, move that correlation intorequestState. - Emit
Mcp-MethodandMcp-Nameon every Streamable HTTP POST, and check whether your gateway can now do routing or authorization work it was doing in application code. - Return
ttlMsandcacheScopeon all five list and read endpoints, and make your tool ordering deterministic. - Move change notifications to
subscriptions/listen. Leavenotifications/progressandnotifications/messageon their originating request stream. - Add idempotency keys to side-effecting tools, since a dropped stream is no longer resumable.
- Stop adopting Roots, Sampling, and Logging in anything new, and plan migrations inside the twelve-month window.
- Move client registration to CIMD, and add
issvalidation plus per-issuer credential binding.
All four Tier 1 SDKs (TypeScript, Python, Go, and C#) now speak 2026-07-28, with Rust in beta, and all of them preserved backward compatibility. FastMCP 4.0 shipped support for stateless interactivity, background tasks, and enterprise auth.
What this says about where MCP is going
Read the change list as a whole and a pattern emerges. Statelessness, cacheability, a uniform interface, routing metadata in headers: those are the architectural constraints of HTTP. The most significant MCP release of 2026 made MCP look much more like a conventional web protocol, and it did so by removing the parts that were most distinctively its own.
That is a sign of maturity rather than retreat. The session-oriented design was what made MCP servers hard to run: it fought load balancers, it complicated horizontal scaling, and it made a deploy without dropping in-flight work into a project. Trading it away buys ordinary web infrastructure, and the capability that sessions genuinely provided came back in a form that survives a stateless transport.
It also changes how you should think about choosing between the two. We updated MCP vs. REST for this revision, because three of the distinctions in that comparison no longer hold. The short version: the differences that remain are runtime discovery, uniform tool semantics across every server, opinionated auth, and the ability for a tool to stop and ask before it acts. None of those are about transport.
If authorization is the part you are staring at, AuthKit supports OAuth 2.1 as a compatible authorization server for MCP, including CIMD, token validation, and Protected Resource Metadata, so the spec churn in that layer is someone else's problem.
Frequently asked questions
Is MCP stateful or stateless?Stateless, as of the 2026-07-28 revision. Protocol-level sessions and the initialize handshake were both removed, and every request carries its own protocol version, client identity, and capabilities in _meta.
What replaced the initialize handshake?A server/discover RPC. Servers must implement it; clients may call it before any other request, but are not required to.
What replaced MCP sessions for multi-step workflows?Server-minted handles returned from a tool and passed back as ordinary tool arguments, plus multi round-trip requests for anything that needs user input mid-call.
What are multi round-trip requests?A pattern that replaces server-initiated elicitation/create, sampling/createMessage, and roots/list. The server returns resultType: "input_required" with inputRequests, and the client retries the original request with inputResponses attached.
Is Dynamic Client Registration still supported?Yes, for backward compatibility, but it is deprecated in favor of Client ID Metadata Documents and will be removed in a future revision.
Can MCP responses be cached?Yes. tools/list, prompts/list, resources/list, resources/read, and resources/templates/list all return required ttlMs and cacheScope fields.
Which MCP features are deprecated in 2026-07-28?Roots, Sampling, Logging, the HTTP+SSE transport, the includeContext values "thisServer" and "allServers", and OAuth 2.0 Dynamic Client Registration. All have a minimum twelve-month window under the new deprecation policy.
Is the 2026-07-28 spec a breaking change?Yes, for anything that depended on session identifiers, the initialize handshake, SSE stream resumability, or the 2025-11-25 elicitation completion signal. The Tier 1 SDKs preserved backward compatibility, so clients and servers on older revisions keep working.