MCP Went Stateless: Everything That Changed in the 2026-07-28 Spec
Published July 29, 2026. Last verified August 7, 2026 against the published specification.
TL;DR: MCP is now stateless. In its 2026-07-28 revision the Model Context Protocol shipped nine major changes, twelve minor ones, four deprecations, and the first formal deprecation policy the protocol has had. The initialize / notifications/initialized handshake is gone. Protocol-level sessions and the Mcp-Session-Id header are gone. Every request now declares its own protocol version and client capabilities inside _meta, and the server accepts or rejects each request on its own terms. A new server/discover RPC, mandatory for servers to implement and optional for clients to call, returns everything the handshake used to. Server-initiated requests are gone too, replaced by a retry pattern called Multi Round-Trip Requests. Roots, Sampling, and Logging are deprecated, and the lifecycle policy now governing them matters more than any single removal.
I run a hosted MCP server, so I read these revisions the way a landlord reads a building code update.
What actually changed in MCP 2026-07-28
The headline is one sentence: MCP stopped being a connection protocol and became a request protocol.
Before this revision, a client opened a connection, ran initialize, exchanged capabilities once, and then made calls inside the state that handshake established. After it, there is no handshake and no connection-scoped state. Each request is self-describing. The versioning page puts it plainly: every request declares its protocol version in the io.modelcontextprotocol/protocolVersion key of its _meta field, "and the server accepts or rejects each request independently."
Here are the nine items the changelog files under Major changes:
| # | Change | What it means |
|---|---|---|
| 1 | Protocol-level sessions and Mcp-Session-Id removed from Streamable HTTP | List endpoints no longer vary per connection. Cross-call state moves to explicit, server-minted handles passed as ordinary tool arguments |
| 2 | initialize / notifications/initialized removed | Version and client capabilities ride in _meta on every request. Mismatch returns UnsupportedProtocolVersionError |
| 3 | server/discover added, mandatory for servers to implement | One request returns supported versions, capabilities, identity, and optional instructions |
| 4 | HTTP GET endpoint and resources/subscribe / resources/unsubscribe replaced by subscriptions/listen | One long-lived POST-response stream, opt-in per notification type |
| 5 | ping, logging/setLevel, notifications/roots/list_changed removed | Log level is now per-request via io.modelcontextprotocol/logLevel |
| 6 | Tasks moved out of core into the io.modelcontextprotocol/tasks extension | Blocking tasks/result replaced by polling tasks/get; new tasks/update; tasks/list removed |
| 7 | Multi Round-Trip Requests replace server-initiated requests | No more roots/list, sampling/createMessage, or elicitation/create from the server |
| 8 | All results carry a required resultType field | "complete" or "input_required" |
| 9 | SSE resumability and message redelivery removed | Last-Event-ID and SSE event IDs are gone; a broken stream loses the request |
That is not the whole breaking surface. Four items filed under Minor changes will also break a conforming implementation: Mcp-Method and Mcp-Name are now required on Streamable HTTP POSTs; ttlMs and cacheScope are now required on tools/list, prompts/list, resources/list, resources/read, and resources/templates/list; the resource-not-found error moved from -32002 to -32602 to align with JSON-RPC; and this revision's own new error codes were renumbered into a reserved range, so HeaderMismatch went -32001 → -32020, MissingRequiredClientCapability -32003 → -32021, and UnsupportedProtocolVersion -32004 → -32022. If you hardcoded any of those, they fail quietly.
Why remove the handshake at all
Statelessness buys operational properties that matter more as MCP servers move from someone's laptop to production infrastructure. A stateless request can be load-balanced to any instance. It can be retried without replaying a setup sequence. It can be cached, traced, and rate-limited by ordinary HTTP machinery that has no idea MCP exists. A handshake-scoped session cannot do any of that without sticky routing.
The cost is that state now has to be somewhere the client can see. That is the point of change #1: servers that genuinely need to remember something between calls mint a handle and hand it back as a normal tool argument. What used to be an invisible property of your connection becomes a visible value in your payload. That is a better default for exactly the reason it is more annoying — you can no longer accumulate state a client never agreed to.
The new negotiation flow
Version selection now happens one of two ways.
Optimistic. Send whatever request you actually want with your version in _meta. If the server can speak it, you are done in one round trip. If not, you get back UnsupportedProtocolVersionError listing the versions it does support, and you retry.
Up front. Call server/discover first. The discovery spec shows the shape:
{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
The reply carries resultType, supportedVersions, and capabilities at the top level of result, along with ttlMs and cacheScope. serverInfo sits inside result._meta under io.modelcontextprotocol/serverInfo, which servers SHOULD include. There is also an optional instructions string, described as "natural-language guidance for LLMs on how to use this server effectively."
Two details worth pulling out of that page, because they are easy to miss and both matter in practice.
First, server/discover results are cacheable, carrying ttlMs and cacheScope. Discovery is not meant to be a per-session ritual.
Second, the spec attaches an explicit note to serverInfo. The whole of it: the field "is self-reported by the server and is not verified by the protocol. It is intended for display, logging, and debugging. Clients SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for security decisions." Read both halves together — here are three jobs this field is for, and here is a fence around every job it looks qualified for. A name and version a server writes about itself is a marketing surface, not an identity claim, and putting that in the spec beats letting every client author find it out the hard way.
Multi Round-Trip Requests, in one paragraph
Servers used to be able to call back into the client — asking for filesystem roots, asking the client's model to generate something, asking the user a question. All of that is replaced by a retry pattern. The server returns a result with resultType: "input_required" and an inputRequests field describing what it still needs. The client gathers the answers and re-sends the original request with inputResponses attached. No server-initiated calls, no correlation identifiers, no completion notification. If a server needs to correlate an elicitation across retries, it encodes its own identifier in requestState.
What breaks, and for whom
If you build MCP servers
| You relied on | Now |
|---|---|
Mcp-Session-Id for per-connection state | Mint an explicit handle, return it, accept it as a tool argument |
initialize to learn client capabilities once | Read io.modelcontextprotocol/clientCapabilities on every request |
resources/subscribe + the HTTP GET stream | Implement subscriptions/listen, tag notifications with io.modelcontextprotocol/subscriptionId |
Server-initiated sampling/createMessage | Call the LLM provider API directly, or return input_required |
logging/setLevel | Per-request io.modelcontextprotocol/logLevel; and you MUST NOT emit notifications/message for requests that omit it |
Blocking tasks/result | Adopt the tasks extension: poll tasks/get, accept tasks/update |
Last-Event-ID resumption | Nothing. Design tool calls so a full retry is safe |
You also now MUST implement server/discover, MUST return resultType on every result, and MUST supply ttlMs and cacheScope on the cacheable list and read methods.
There are two smaller requirements that are easy to skip and worth not skipping. Streamable HTTP POSTs now require the Mcp-Method and Mcp-Name headers. And the spec says servers SHOULD return tools from tools/list in a deterministic order, with the stated reason being client-side caching and better prompt-cache hit rates on the model side.
If you build MCP clients
The obligation that will bite is on the compatibility side: clients MUST treat a result from an older server that omits resultType as "complete". And on stdio there is no HTTP status code to trigger fallback, so a client that wants to support both eras SHOULD lead with server/discover and fall back to initialize if that fails.
If you just use MCP servers
Today, nothing — but not for the reason you would guess.
Version negotiation happens between your client and the server, and right now essentially everything on both sides is still handshake-era, so everything still talks to everything. What does not work is a mix. The spec publishes a compatibility matrix covering every client/server combination, and two of its seven rows say Fails: a modern client against a legacy server, and a legacy client against a modern-only server. The combinations that survive need one side to be dual-era, speaking both the handshake and per-request metadata, and the spec makes that optional: a server that wants to serve both eras "MAY implement both behaviors."
So you are covered while your client and your server are on the same side of the change. The exposure is the middle of the migration, and that is the period we are entering.
What you may notice is second-order. Servers that leaned on connection state have real work to do, and that work can show up on your side as reconnect prompts or a rough week. If a connector starts misbehaving shortly after its maintainer ships a spec upgrade, that is the most likely reason, and the fix is almost always disconnect and reconnect. Our troubleshooting guide covers the usual failure modes, most of which are unchanged.
The deprecations tell you where MCP is going
Roots, Sampling, and Logging are all deprecated. Read the three suggested migrations together and a philosophy falls out:
- Instead of Roots, pass directories and files as tool parameters, resource URIs, or server configuration.
- Instead of Sampling, integrate directly with LLM provider APIs.
- Instead of Logging, write to
stderror use OpenTelemetry.
Sampling in particular was the feature that let a server borrow the client's model. Deprecating it says MCP is not trying to be an inference broker. It is trying to be a tool-calling protocol, and the surface area it accumulated on the way to that is being trimmed.
OAuth 2.0 Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents. It stays available for authorization servers that don't support the new mechanism, but the direction is set. If you are standing up authorization now, build toward Client ID Metadata Documents. The revision also tightens two things that were previously ambiguous: authorization servers SHOULD return iss per RFC 9207 and clients MUST validate a present iss against the recorded issuer before redeeming the code, and client credentials are now explicitly bound to the authorization server that issued them — key them by issuer, never reuse them elsewhere, re-register when the issuer changes.
HTTP+SSE, deprecated in prose since March 2025, is now formally Deprecated under the lifecycle policy. If you are still on it, Streamable HTTP is the destination.
There is finally a deprecation policy
Second-biggest change in the revision, behind statelessness, and the one I have seen discussed least.
MCP now has a feature lifecycle policy with three states — Active, Deprecated, Removed — a minimum twelve-month deprecation window, and a published registry of everything currently Deprecated or Removed. The twelve-month floor can be cut to ninety days, but only for a feature that "presents an active security risk, meaning a vulnerability with a published security advisory or documented in-the-wild exploitation for which no in-place mitigation exists." A deprecated feature must document a migration path or state that none is required.
For anyone deciding whether to build a business on top of this protocol, that is worth more than any individual feature. It converts "will this break" from a guess into a floor you can plan against.
A floor, not a date. The registry lists Roots, Sampling, Logging, and Dynamic Client Registration as eligible for removal in the first revision released on or after 2027-07-28, and eligibility is not a schedule. Removal "is executed at the discretion of the Core Maintainers after the minimum deprecation window has elapsed," and features "may remain Deprecated, without removal, for much longer than the minimum deprecation window." It cuts the other way too: HTTP+SSE, reclassified under the same policy, is eligible for removal three months after its proposal reaches Final, not twelve. Plan against the floor, not against a date the spec never promised.
What we changed on our side
Less than you would think, and not because we were fast.
I built our server stateless from the start for reasons that had nothing to do with foresight: it runs as an edge function, and edge functions do not get to assume a long-lived process is holding your session. It never minted an Mcp-Session-Id. Every request already carried what it needed. The single hardest part of this revision is one we skipped by accident.
Everything else is still ahead of us. In the spec's own vocabulary our server is legacy-era: it still answers an initialize handshake, and it does not yet implement server/discover, stamp resultType, or return ttlMs and cacheScope. I would rather say that plainly than let "built stateless from the start" imply a migration we have not done.
ttlMs is the decision I am actually chewing on. Short, and clients re-fetch a tool list that rarely changes, burning tokens on every reconnect. Long, and a newly shipped tool stays invisible to a client holding a cached list. listChanged is meant to close that gap and we do not implement it yet either, so for now the TTL is the only lever I have. A stale tool list is a worse failure than an extra request, so we will land short and revisit once listChanged is in. I will publish the number when it ships.
If you want the practical version of connecting a hosted MCP server to any of the thirty-plus clients that support them, that is a separate and much longer guide. It is written against the connect-and-authorize flow, which this revision did not change.
Specification is not adoption
Everything above is verified against the published specification as of August 7, 2026. That is a claim about the spec, not about your stack.
Clients and SDKs move on their own schedules. The most concrete evidence I have is our own: Tempreon's server is still legacy-era, and so is every client we see connecting to it. The spec plans for exactly this. It defines a dual-era implementation class, publishes a compatibility matrix for mixed deployments, and notes that removal from the specification does not oblige an SDK to drop the feature from its releases. Check what your specific client actually implements before you assume any of this is live for you.
Frequently asked questions
- What is the current MCP protocol version?
- 2026-07-28. MCP version identifiers are dates in YYYY-MM-DD form, and the date marks the last time a backwards-incompatible change was made. The version does not increment for backwards-compatible changes, so a server can keep shipping improvements without forcing clients to renegotiate. The previous revision was 2025-11-25.
- Did MCP remove the initialize handshake?
- Yes. The 2026-07-28 revision removed both initialize and notifications/initialized. Every request now carries its own protocol version and client capabilities inside the _meta field, under the keys io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. The server accepts or rejects each request independently, and a version the server cannot speak comes back as UnsupportedProtocolVersionError.
- What replaced Mcp-Session-Id in MCP?
- Nothing replaced it at the protocol level. Protocol-level sessions and the Mcp-Session-Id header were removed from the Streamable HTTP transport. Servers that need state across calls now mint their own handles and pass them back as ordinary tool arguments, which makes the state explicit and visible to the client rather than implicit in a connection.
- What is server/discover in MCP?
- server/discover is an RPC that returns a server's supported protocol versions, capabilities, identity, and optional natural-language instructions in a single request. Servers MUST implement it; clients are not required to call it and may invoke any RPC inline instead, handling UnsupportedProtocolVersionError if their preferred version is not supported. It is also the recommended way to probe a stdio server that might still be running the older handshake-based protocol, because stdio has no HTTP status code to drive fallback.
- Are Roots, Sampling, and Logging being removed from MCP?
- They are deprecated, not removed, and they remain fully functional. The changelog is explicit that these features remain fully functional during the deprecation window but new implementations should not add support for them. Under the new feature lifecycle policy a deprecated feature stays in the specification for at least twelve months before it becomes eligible for removal, and the registry lists all three as eligible in the first revision released on or after 2027-07-28. Eligibility is not a removal date. The suggested migrations are: pass directories and files as tool parameters, resource URIs, or server configuration instead of Roots; call the LLM provider API directly instead of Sampling; and write to stderr or use OpenTelemetry instead of Logging.
- Do I need to change anything if I only use a hosted MCP server?
- Probably nothing, and not on any deadline of your own, but cross-era compatibility is not automatic. The spec's compatibility matrix marks two combinations as failures: a modern (2026-07-28) client against a legacy (2025-11-25 or earlier) server, and a legacy client against a modern-only server. The combinations that work require one side to implement both eras, which the spec leaves optional. In practice you are covered while your client and your server are on the same side of the change. What changes for you is indirect: servers that used long-lived connection state have to rebuild it, and that work can surface as reconnect prompts or brief instability while it ships.
- What happens now if an MCP response stream breaks mid-request?
- The in-flight request is lost. SSE stream resumability and message redelivery were removed, which means the Last-Event-ID header and SSE event IDs are gone. A client MUST re-issue the work as a brand-new request with a new request ID rather than resuming where the stream stopped. If a tool call is expensive or has side effects, that retry needs to be safe to run twice.