{"slug": "mcp-went-stateless-what-changed-in-the-2026-07-28-spec", "title": "MCP went stateless: What changed in the 2026-07-28 spec", "summary": "The Model Context Protocol's 2026-07-28 revision removed protocol-level sessions, the initialize handshake, stream resumability, ping, and logging/setLevel, making every request self-contained so any request can hit any instance behind a round-robin load balancer with no shared session store or sticky routing. The spec's two costliest changes are the removal of the Last-Event-ID header and SSE event IDs from Streamable HTTP, which silently loses in-flight requests and can duplicate side effects on retry, and the replacement of the 2025-11-25 elicitation design — including notifications/elicitation/complete and the elicitationId field, removed outright rather than deprecated — with multi round-trip requests that turn a callback into a retry loop. All four Tier 1 SDKs have caught up since the revision landed at the end of July, and the spec requires side-effecting tools to supply an idempotency key as a parameter, which the protocol does not provide itself.", "body_md": "# MCP went stateless: What changed in the 2026-07-28 spec\n\nSessions, the initialize handshake, and stream resumability are all gone. Here is what replaced them, and what breaks if you ignore it.\n\nThe 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`.\n\nWhat 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.\n\nThe 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.\n\nHere is what changed, what it costs you, and what to do about it.\n\n## The two changes that will cost you the most\n\nMost 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.\n\n**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.\n\nNothing 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.\n\n**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.\n\nThe 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.\n\nEverything 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.\n\n## The stateless core\n\nUntil 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.\n\nAll 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`:\n\nThe 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`.\n\nOne 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.\n\n### server/discover, and who it is optional for\n\nThe 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.**\n\nSo 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.\n\n### How to hold state now\n\nDropping the protocol session does not mean your application has to be stateless. It means state has to be explicit.\n\nThe 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.\n\nThis 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.\n\n## Multi round-trip requests\n\nSessions 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.\n\nMulti 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:\n\nThe 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.\n\nTwo details worth pinning down:\n\n- **`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\"` .\n- **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` .\n\nIf you shipped against the `2025-11-25` elicitation design, this is the part of the migration that will take real work.\n\n## Header-based routing\n\nStreamable HTTP POST requests must now include two standard headers: `Mcp-Method` and `Mcp-Name`. In the example above, those are `tools/call` and `search`.\n\nThis 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.\n\nThe revision also adds `x-mcp-header`, which lets tool parameters supply custom headers.\n\n## List results are cacheable\n\n`tools/list`, `prompts/list`, `resources/list`, `resources/read`, and `resources/templates/list` now return two required fields through a new `CacheableResult` interface:\n\n- `ttlMs` , a freshness hint in milliseconds, so clients can cache instead of polling.\n- `cacheScope` , either`\"public\"` or`\"private\"` , controlling whether shared intermediaries are allowed to cache the response.\n\nBoth 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.\n\n## Notifications moved to subscriptions/listen\n\nThe 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.\n\nClients opt in per type: `toolsListChanged`, `promptsListChanged`, `resourcesListChanged`, and `resourceSubscriptions`. The server acknowledges and tags each notification with `io.modelcontextprotocol/subscriptionId`.\n\nRequest-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.\n\n## Removed outright\n\nThese are gone now, not deprecated:\n\n- **`ping`.** Use ordinary transport-level health checking.\n- **`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.\n- **`notifications/roots/list_changed`.**\n- **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.\n\nThe 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.\n\nThere 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`.\n\n## Deprecated, with a twelve-month clock\n\nThe 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.\n\nNewly deprecated:\n\n- **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.\n- **The HTTP+SSE transport** , soft-deprecated since`2025-03-26` , is now formally Deprecated. Migrate to Streamable HTTP.\n- **The `includeContext` values `\"thisServer\"` and `\"allServers\"`.** Omit the field or use`\"none\"` . They will be removed no later than Sampling itself.\n- **OAuth 2.0 Dynamic Client Registration** , covered below.\n\n## Dynamic client registration gives way to CIMD\n\n[Dynamic Client Registration](https://workos.com/blog/dynamic-client-registration-dcr-mcp-oauth) (RFC 7591) is deprecated as a client registration mechanism, in favor of [Client ID Metadata Documents](https://workos.com/blog/client-id-metadata-documents-cimd-oauth-client-registration-mcp). DCR remains available for backward compatibility with authorization servers that have not implemented CIMD yet.\n\nThe rest of the authorization work in this revision is hardening, and it reflects where implementers were actually losing time:\n\n- Authorization servers should include the `iss` parameter in authorization responses per[RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207) , and clients must validate a present`iss` against the recorded issuer before redeeming the code. This closes an authorization-server mix-up attack.\n- 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.\n- 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.\n\nIf you are choosing between the two registration mechanisms, we wrote up the tradeoffs in [CIMD vs. DCR](https://workos.com/blog/mcp-client-registration-cimd-vs-dcr).\n\n## Tasks became an extension\n\nExperimental 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.\n\nThe 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](https://workos.com/blog/mcp-async-tasks-ai-agent-workflows).\n\n## Your migration checklist\n\nIn rough order of how likely it is to bite:\n\n1. **Remove every dependency on `Mcp-Session-Id`.** Replace session-scoped state with server-minted handles passed as tool arguments.\n2. **Delete the `initialize` handshake.** Implement`server/discover` on the server side. Decide whether your client needs to call it at all.\n3. **Set `resultType` on every result you return.** Treat a missing`resultType` from an older server as`\"complete\"` .\n4. **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` .\n5. **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.\n6. **Return `ttlMs` and `cacheScope`** on all five list and read endpoints, and make your tool ordering deterministic.\n7. **Move change notifications to `subscriptions/listen`.** Leave` notifications/progress` and`notifications/message` on their originating request stream.\n8. **Add idempotency keys to side-effecting tools** , since a dropped stream is no longer resumable.\n9. **Stop adopting Roots, Sampling, and Logging** in anything new, and plan migrations inside the twelve-month window.\n10. **Move client registration to CIMD** , and add`iss` validation plus per-issuer credential binding.\n\nAll 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.\n\n## What this says about where MCP is going\n\nRead 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.\n\nThat 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.\n\nIt also changes how you should think about choosing between the two. We updated [MCP vs. REST](https://workos.com/blog/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.\n\nIf authorization is the part you are staring at, [AuthKit](https://workos.com/docs/authkit/mcp) 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.\n\n## Frequently asked questions\n\n**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`.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.", "url": "https://wpnews.pro/news/mcp-went-stateless-what-changed-in-the-2026-07-28-spec", "canonical_source": "https://workos.com/blog/mcp-stateless-spec-2026-07-28", "published_at": "2026-09-16 00:00:00+00:00", "updated_at": "2026-09-23 02:24:37.847715+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Model Context Protocol", "Streamable HTTP", "notifications/elicitation/complete", "Last-Event-ID", "Mcp-Session-Id", "io.modelcontextprotocol/protocolVersion", "io.modelcontextprotocol/clientCapabilities", "io.modelcontextprotocol/clientInfo"], "alternates": {"html": "https://wpnews.pro/news/mcp-went-stateless-what-changed-in-the-2026-07-28-spec", "markdown": "https://wpnews.pro/news/mcp-went-stateless-what-changed-in-the-2026-07-28-spec.md", "text": "https://wpnews.pro/news/mcp-went-stateless-what-changed-in-the-2026-07-28-spec.txt", "jsonld": "https://wpnews.pro/news/mcp-went-stateless-what-changed-in-the-2026-07-28-spec.jsonld"}}