In this article
September 16, 2026
September 16, 2026

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.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

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:

  
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"},
 "_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
  

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.

Two-panel before-and-after diagram. Before: an MCP client sends initialize, then notifications/initialized, then tools/call carrying an Mcp-Session-Id header, through a sticky load balancer to Instance A, which reads and writes a session store. Instances B and C are greyed out and unreachable, because every later request in the session must return to Instance A. After: the client sends a single self-describing tools/call whose _meta carries protocolVersion, clientCapabilities and clientInfo, through an ordinary round-robin load balancer that fans out equally to all three instances, with no session store.

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:

  
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": [ /* what the server still needs */ ]
  }
}
  

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-panel before-and-after sequence diagram between an MCP client and an MCP server. Before: inside a single request, tools/call goes to the server, then elicitation/create travels backwards from server to client, which requires the stream to stay open in both directions; the client returns an elicitation result and the server returns the final result. After: request one sends tools/call and the server replies with resultType input_required plus inputRequests; the connection then closes with nothing held open while the client gathers input; request two retries the same tools/call with inputResponses attached, and the server returns resultType complete.

Two details worth pinning down:

  • resultType is now required on every result, not just interim ones. Ordinary results carry resultType: "complete". Clients must treat a result from an older server that omits the field as "complete".
  • The elicitation completion signal is gone. notifications/elicitation/complete and the elicitationId field, both introduced in 2025-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 in requestState.

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 via io.modelcontextprotocol/logLevel in _meta. Servers must not emit notifications/message for 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-ID header 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

Two-part reference chart. The top group, labelled gone already with no window, lists ping, logging/setLevel, notifications/roots/list_changed, SSE resumability via the Last-Event-ID header, and notifications/elicitation/complete with its elicitationId field. The bottom group, labelled deprecated with a minimum twelve-month window, shows four bars spanning a timeline from 2026-07-28 when the spec ships to 2027-07-28 at the earliest: Roots, Sampling and Logging; the HTTP+SSE transport; OAuth 2.0 Dynamic Client Registration; and the includeContext values thisServer and allServers.

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 stderr on 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 includeContext values "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 iss parameter in authorization responses per RFC 9207, and clients must validate a present iss against 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_type during DCR, which is what stops authorization servers from rejecting localhost redirect URIs. If you have ever debugged a mystery redirect_uri error 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:

  1. Remove every dependency on Mcp-Session-Id. Replace session-scoped state with server-minted handles passed as tool arguments.
  2. Delete the initialize handshake. Implement server/discover on the server side. Decide whether your client needs to call it at all.
  3. Set resultType on every result you return. Treat a missing resultType from an older server as "complete".
  4. Rewrite elicitation and sampling flows onto MRTR. Return input_required with inputRequests, and handle a retry carrying inputResponses. If you used elicitationId, move that correlation into requestState.
  5. Emit Mcp-Method and Mcp-Name on every Streamable HTTP POST, and check whether your gateway can now do routing or authorization work it was doing in application code.
  6. Return ttlMs and cacheScope on all five list and read endpoints, and make your tool ordering deterministic.
  7. Move change notifications to subscriptions/listen. Leave notifications/progress and notifications/message on their originating request stream.
  8. Add idempotency keys to side-effecting tools, since a dropped stream is no longer resumable.
  9. Stop adopting Roots, Sampling, and Logging in anything new, and plan migrations inside the twelve-month window.
  10. Move client registration to CIMD, and add iss validation 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.