Stateless MCP: What the 2026-07-28 Spec Changes for Tool Builders

Guilliano Molaire Guilliano Molaire 11 min read

Last updated: September 2026

The Model Context Protocol went stateless in its 2026-07-28 revision. The two things that made MCP stateful, the initialize handshake and the Mcp-Session-Id header, are gone. Every request now carries its own protocol version, client identity, and capabilities inline, so any request can land on any server instance behind a plain load balancer. If you built an MCP server on the last two years of the protocol and leaned on session state, you have real work to do: drop the session assumptions, and either make each call genuinely independent or move to explicit, server-minted state handles that the model passes back as ordinary tool arguments.

The short version:

  • No handshake, no session ID. Each request is self-describing.
  • Server-to-client requests (sampling, elicitation, roots) no longer ride an open stream. They use a new retry-based pattern called Multi Round-Trip Requests.
  • Application state does not disappear, but it stops being implicit. You mint a handle and receive it back on each call.
  • Old and new implementations can interoperate through explicit version negotiation, and the Tier 1 SDKs ship dual-era support.

What “stateless MCP” actually means

Under the previous transport (protocol versions 2025-03-26 through 2025-11-25), a client opened a connection by POSTing an initialize request, the server replied with an Mcp-Session-Id, and every later request had to carry that header back. That session was sticky: it had to return to the same server instance, because that instance held the state. The official rationale for killing it, from the MCP blog’s “The 2026-07-28 Specification” post (David Soria Parra and Den Delimarsky, July 2026) and the SEPs behind it, is that this made MCP hard to scale and, worse, that sessions never converged on a shared meaning.

SEP-2567 (“Sessionless MCP via Explicit State Handles,” 2026) puts the second problem bluntly: the spec never defined when a session begins or ends, so different clients scoped it differently, “some per tool call, some per application launch, some per page load, and almost none resume them.” A server author could not predict what a session even was when connected to an arbitrary client. That is a bad foundation for application state.

Stateless MCP removes that foundation. Per the 2026-07-28 changelog, the protocol is “sessionless at every layer.” A server no longer maintains per-client session state, and, in the words of SEP-2575 (“Make MCP Stateless,” 2026), “every request is self-contained and can be understood in isolation.”

Is it actually called “MCP 2.0”?

Sort of, but not by the spec. The specification uses date-based versions, and the current one is 2026-07-28. There is no document called “MCP 2.0.” The “2.0” label comes from the SDKs: the Python and TypeScript SDKs bumped to major version 2 for this release (the beta packages are mcp==2.0.0b1 for Python and @modelcontextprotocol/server@beta for TypeScript, per the MCP blog’s June 2026 SDK betas post), while the Go and C# SDKs kept lower numbers. So if someone says “MCP 2.0,” they almost always mean the 2026-07-28 stateless spec, but the precise name is the date.

What was removed

The 2026-07-28 changelog is a list of deletions as much as additions. The ones that will touch your code:

  • The initialize / notifications/initialized handshake. There is no negotiation phase. Version and capabilities travel on every request.
  • The Mcp-Session-Id header and protocol-level sessions. A server on this revision must ignore an Mcp-Session-Id if an older client sends one, and must not mint or echo session IDs.
  • The standalone HTTP GET stream for server-initiated messages, replaced by subscriptions/listen.
  • SSE stream resumability (Last-Event-ID and event IDs). If a response stream breaks, the in-flight request is lost and the client must re-issue it as a new request with a new ID.
  • ping, logging/setLevel, and notifications/roots/list_changed. Log level is now set per-request via io.modelcontextprotocol/logLevel in _meta.

Two features that are not removed but are now deprecated with a minimum 12-month window: Roots, Sampling, and Logging as features, and the legacy HTTP+SSE transport. Dynamic Client Registration is also deprecated in favor of Client ID Metadata Documents, which we come back to under authorization.

Where per-request metadata lives now

Because there is no handshake to carry it, identity and versioning move into every request. On the wire, the _meta object in a request’s params holds:

  • io.modelcontextprotocol/protocolVersion (required)
  • io.modelcontextprotocol/clientInfo (the client should identify itself)
  • io.modelcontextprotocol/clientCapabilities
  • io.modelcontextprotocol/logLevel (optional)

On the Streamable HTTP transport, the spec mirrors selected fields into HTTP headers so gateways and load balancers can route without parsing the JSON body. A tools/call now looks like this:

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": {}
    }
  }
}

The MCP-Protocol-Version header must match the value in _meta, or the server returns a 400 with a HeaderMismatch error (code -32020). That header-body match rule exists so a load balancer routing on the header and a server executing on the body cannot disagree. Servers also implement a new server/discover RPC that a client may call up front to learn supported versions, though a client is free to skip it and handle an UnsupportedProtocolVersionError inline.

How server-to-client requests work now: Multi Round-Trip Requests

This is the change most tool builders will feel. In the old model, when a server needed something from the client mid-call (an LLM completion via sampling, a user prompt via elicitation, or the client’s roots), it sent its own JSON-RPC request back over a held-open stream. That only works if the connection and its state stay pinned to one server instance, which is exactly what statelessness removes.

The replacement is Multi Round-Trip Requests (MRTR), defined in SEP-2322. The flow:

  1. The client calls a tool.
  2. If the server needs input, it does not open a stream. It returns an InputRequiredResult (resultType: "input_required") containing an inputRequests map (the elicitation, sampling, or roots requests it needs answered) and an opaque requestState blob.
  3. The client gathers the answers and retries the original request, this time including inputResponses and echoing back the exact requestState.
  4. The server reconstitutes what it needs from requestState and returns the final result.

The key idea is that the server carries no memory between the two calls. Everything it needs to resume rides in requestState, which the client must echo verbatim and must not inspect or modify. MRTR is allowed only on prompts/get, resources/read, and tools/call.

Because requestState round-trips through the client, the spec treats it as attacker-controlled. If it influences authorization or business logic, the server MUST integrity-protect it (HMAC or AEAD) and reject anything that fails verification, and SHOULD bind it to the authenticated principal, a short TTL, and an identifier for the originating request to prevent replay and cross-user reuse. If you are used to sampling and elicitation “just working” over a live connection, this is the part to rewrite carefully.

What replaces session-scoped state: explicit handles

Removing sessions does not force your application to be stateless. It forces your state to be explicit. SEP-2567’s guidance is to do what HTTP APIs have always done: mint a handle from a tool and have the model pass it back as an ordinary argument.

create_basket()            -> returns basket_id "bsk_a1b2c3"
add_item(basket_id, sku)   -> uses the handle
checkout(basket_id)        -> uses the handle

This is already how many production servers work: Linear returns an issue ID, GitHub returns a PR number, Stripe returns a customer ID, and later calls reference them. The design rules are the ones you would expect from good API design. Handles should be opaque, possession should never equal authorization (validate the handle against the caller’s identity on every call), durability should be documented in the tool description, and expired handles should return useful errors.

There is a security consequence worth naming. Handles end up in chat logs, subagent prompts, and copy-paste buffers, places session IDs did not. SEP-2567 calls this “a change in exposure surface, not a new class of vulnerability,” and the fix is the same posture Google Doc IDs and GitHub PR numbers take: the ID names the resource, and the auth context on the request decides access. The 2026-07-28 security best practices reflect this directly. The old “Session Hijacking” section has been replaced by a “State Handle Hijacking” section with the same core rule: bind state server-side to the authenticated user, for example keyed as <user_id>:<handle>, and never treat a handle as authentication.

Change notifications: subscriptions/listen

List changes and resource updates used to arrive on the GET SSE stream. That endpoint is gone. In its place, a client opens one long-lived stream by POSTing a subscriptions/listen request and opting in to specific notification types (toolsListChanged, resourcesListChanged, resourceSubscriptions, and so on). The server acknowledges and tags each notification with a subscription ID. Request-scoped notifications like notifications/progress still flow on the response stream of the request they belong to, not on the listen stream. If your server pushed tools/list_changed to clients, that path moves to subscriptions/listen.

What this means for authorization

Less than you might fear, and it is worth being precise. MCP authorization was already per-request: the client sends a bearer token on every HTTP request, and the server, acting as an OAuth 2.1 resource server, validates it. Statelessness reinforces that. SEP-2575’s security note is that “without a session handshake, every request must be independently authenticated and authorized,” and implementations must not let removing initialize become a way to skip auth. If your server was validating tokens per request, which it should have been, nothing breaks.

The genuinely new authorization item is client registration. The 2026-07-28 spec deprecates OAuth Dynamic Client Registration in favor of Client ID Metadata Documents (CIMD), where a client identifies itself with an HTTPS URL that resolves to its metadata. That is a moving target on the identity-provider side. Keycloak, for instance, added experimental CIMD support (behind --features=cimd) and documents itself as an MCP authorization server, though as of this writing it still does not honor the RFC 8707 resource parameter natively, so audience binding is done with a scope plus an audience mapper. If you run MCP servers behind Keycloak, that gap and its workaround are covered in our post on the Keycloak MCP server 401 and the audience claim, and the broader setup is in securing MCP servers with Keycloak OAuth 2.0 and authenticating AI agents with Keycloak.

For what it is worth, Skycloak’s own managed Keycloak MCP server speaks Streamable HTTP and authenticates over OAuth, so this is the transport and auth model we operate against on every release, not a hypothetical.

Backward compatibility and migration

You do not have to move everything at once, and clients and servers of different eras can interoperate.

  • Version negotiation is explicit. If a server does not support the requested version, it returns an UnsupportedProtocolVersionError listing what it does support, and the client retries with a mutually supported version. There is no negotiation handshake, just per-request accept-or-reject.
  • Dual-era servers can serve both. A request carrying modern _meta is served statelessly, while an initialize request selects legacy semantics, and a server may do both on the same endpoint.
  • Detection is transport-specific. On stdio, a client probes with server/discover and falls back on any non-modern error. On HTTP, it attempts a modern request and inspects the body of a 400 before falling back to initialize.
  • The SDKs help. The 2026-07-28 spec shipped as a release candidate on May 21, 2026 and was finalized on July 28, 2026, a 10-week window. The Tier 1 SDK betas (Python mcp==2.0.0b1, TypeScript @beta with a codemod for the API renames, Go v1.7.0-pre.1, C# 2.0.0-preview.1) implement dual-protocol support, and a Python v2 server can answer both revisions from one endpoint.

A practical migration order: confirm your server validates auth per request (it should already), replace any Mcp-Session-Id-keyed state map with a handle-keyed one and add a create_* tool, move any server-initiated sampling or elicitation onto MRTR with an integrity-protected requestState, move change notifications to subscriptions/listen, and drop reliance on SSE resumability. SEP-2567 notes an automated survey found roughly 90% of sampled open-source servers had no application-level session dependency at all, so for many servers the change is small. The ones that hurt are the servers that treated the session as a place to keep a cart, a browser, or a workflow.

Frequently asked questions

Is MCP 2.0 the same as the 2026-07-28 spec?
Effectively yes. The specification is versioned by date (2026-07-28), and “2.0” is the major version the Python and TypeScript SDKs adopted for it. There is no separate document named “MCP 2.0.”

Do I have to rewrite my MCP server?
Only if it depended on the session. If your server was already stateless or kept state keyed by an explicit identifier, the changes are mostly mechanical (per-request metadata, MRTR for server-initiated requests). Servers that stored per-session state need to move to explicit handles.

What replaces the initialize handshake?
Per-request metadata plus an optional server/discover RPC. Each request declares its protocol version and capabilities in _meta, and the server accepts or rejects it independently.

How do sampling and elicitation work without a session?
Through Multi Round-Trip Requests. The server returns an InputRequiredResult with the requests it needs and an opaque requestState, and the client retries the original call with the answers and the echoed requestState. The server keeps no memory between the two calls.

Does stateless MCP change how authorization works?
Not fundamentally. Auth was already per-request over OAuth 2.1, and statelessness reinforces that every request must be independently authenticated. The notable new item is that Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents.

Can old clients still talk to new servers?
Yes, if the server is dual-era. A server can implement both the initialize handshake for legacy clients and the stateless model for modern ones, and version negotiation resolves the rest. A modern-only server will reject a legacy initialize with an error that names its supported versions.

Key takeaways

The 2026-07-28 revision makes MCP stateless: no initialize handshake, no Mcp-Session-Id, every request self-describing. Server-to-client requests move to Multi Round-Trip Requests, session-scoped state moves to explicit handles you validate per call, and change notifications move to subscriptions/listen. Authorization stays per-request OAuth, with DCR giving way to Client ID Metadata Documents. Backward compatibility is handled by dual-era servers and explicit version negotiation, and the Tier 1 SDK betas already support both. Treat each call as independent, and your MCP server gets simpler to scale in the bargain.

If you run MCP servers that need an OAuth authorization server behind them, Skycloak provides managed Keycloak, which is identity management as a service. Start a free trial or talk to us.

Primary sources: the Model Context Protocol blog post “The 2026-07-28 Specification” (2026), the 2026-07-28 changelog and Streamable HTTP transport specification, SEP-2575 (“Make MCP Stateless”), SEP-2567 (“Sessionless MCP via Explicit State Handles”), and SEP-2322 (Multi Round-Trip Requests).

Identity management as a service, on open source

Skycloak does what Auth0 and Okta do, SSO, MFA, SCIM, audit logs and enterprise federation, on an open source core. Unlimited users and applications on every plan, no charge per monthly active user, and you can export and self-host whenever you want.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman