Anthropic ships a new MCP spec that finally lets you run servers in the cloud
- MCP servers had a nagging flaw: the client had to shake hands first to get a session ID, and every request after that had to land on the same machine. Adding servers didn't help, and serverless was off the table.
- The new spec throws that rule out. The handshake and the session ID are gone; every request carries its own identity, any machine can take it, and a plain load balancer is all you need.
- When a server needs to ask the user something mid-flight — "this will cost money — confirm?" — it now takes two round trips: return "I'm missing something," let the client ask, then resend the original request.
- State didn't disappear. It moved into the open: the server hands out a handle and lets the model pass it back as a parameter.
- A batch of features got cut or put on probation. The handshake, session IDs, and SSE resumption are gone outright; Roots, Sampling, Logging, and the old HTTP+SSE transport get at least 12 more months.
- An outside security analysis flags the trade: the protocol closes off session hijacking and its cousins, but pushes the security burden onto developers — one request can now spin up an expensive task and walk away.
- The most useful takeaway: July 28 is not a switch. Your current servers and clients won't break, and new clients fall back to the old handshake when they meet an old server.
What old MCP got stuck on: sessions pinned a server to one machine
On July 28, 2026, MCP published its fifth spec revision, 2026-07-28, converting the protocol core from a stateful bidirectional connection into stateless request/response, and introducing a formal extension framework and deprecation policy. Anthropic announced the same day that Claude products would roll it out across the board.
MCP (Model Context Protocol) connects AI assistants to outside tools and data. Anthropic open-sourced it in late 2024, and the whole industry runs on it now. The four first-tier SDKs — TypeScript, Python, Go, and C# — pull close to 500 million downloads a month combined, with TypeScript and Python each past a billion cumulative downloads.
What this revision sets out to fix is one very specific engineering knot: deploying a single MCP server to the cloud at scale is hard.
Old MCP is a phone call. You dial in (the initialize handshake), the operator gives you an extension (Mcp-Session-Id), and every word after that is tied to that extension. Want a different operator? Sorry — only the original one remembers what you just said.
New MCP is a shipped package. Every parcel carries the full sender, recipient, and contents on the label. Whoever picks it up can handle it, no introductions required.
The phone-call model created three knock-on problems.
First, adding machines doesn't help. The second machine has never heard of your extension, so a request routed there falls flat. To scale, you either make the load balancer remember which machine each user belongs to (a sticky session), or put every machine behind a shared store like Redis to look up sessions.
Second, serverless and edge nodes are out. Those environments are built on the premise that every invocation is fresh and forgets everything afterward, which is fundamentally at odds with remembering a session.
Third, gateways in the middle have no idea what you're doing. Everything lives in the JSON body, so any load balancer, rate limiter, or firewall that wants to route or throttle by "which tool is this calling" has to crack the JSON open and read it. That's expensive, and plenty of gateways simply don't bother.
(must remember who goes where)
idle
only one
idle
must come back to Node 2
(round-robin is fine)
any request
How the stateless core actually works
So how does the new spec untie the knot? Three moves.
The handshake is gone
The initialize and notifications/initialized opening ritual has been deleted outright, and the Mcp-Session-Id HTTP header went with it. Everything the handshake used to exchange — protocol version, who the client is, what the client supports — now rides along with each request inside the _meta field. Servers, for their part, are encouraged to report their own identity in the _meta of every result they return.
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"}}}}
There's a new way to ask a server what it can do
A new server/discover method reports which protocol versions a server supports, what its capabilities are, and who it is. Note the asymmetry: servers must implement it, clients don't have to use it. A client can query it before doing anything else, or skip it and fire off a request — both are perfectly legal. That's a real break from the old initialize, which was a mandatory opening ritual. This is just an ordinary query.
The spec says it flatly: a connection is not a session
A server must not rely on earlier requests over the same connection to infer context — that includes capabilities, protocol version, and client identity. Each request supplies these itself in _meta.
An open connection — even a long-lived local stdio process — is not a conversation. A client may push completely unrelated requests down the same pipe, and a server may not treat "connection identity" as "session identity."
Worth noting: a long-lived connection like subscriptions/listen doesn't break statelessness either. It's still an ordinary request/response — the "response" just happens to be a notification stream that stays open. Its state belongs to that request, not to the connection underneath.
The key point: state didn't vanish, it moved into the open
Here's where people get it wrong. No sessions at the protocol layer doesn't mean your application has to be stateless.
If your server genuinely needs to remember things across calls, the official approach is to emit an explicit handle from a tool and let the model pass it back as an ordinary parameter on later calls.
- The session ID lived in the transport layer, invisible to the model
- State followed the connection — drop the connection, lose the state
- Switch machines and everything is lost
- Hard to debug, because the state wasn't in your application code
- The server returns a handle from a tool — just an ordinary return value
- The model can see it and thread it between different tools on its own
- It travels as a tool argument, independent of any connection
- State belongs to your application layer, where you can inspect and debug it
The launch blog's argument is that this beats session state buried in the transport, precisely because the model can see it:
We've found this works better than hiding session state in the transport layer — the model can see the handle and thread it between tools itself.
Official MCP launch blog, 2026-07-28
There's a trade here worth naming. State management didn't go away; it moved from "the transport quietly handles it for you" to "you handle it explicitly in your application." You get transparency, debuggability, and horizontal scaling. You also get the bill.
MRTR: asking the user something now takes two round trips
If a server can no longer speak first, how does it ask the user a question mid-flight?
Picture the situation. A tool is halfway through and needs the user to confirm something: Supabase's MCP server is about to create a new project and needs to ask "this will incur charges — confirm?", or a SQL statement will delete data and needs a nod first.
The old way was for the server to send a request backwards down that always-open bidirectional stream — to ask a question (elicitation/create), borrow the client's model for a quick inference (sampling/createMessage), or ask where the file directories are (roots/list). All of it assumed the stream stayed open, and that stream is exactly what going stateless tears out.
The new way is MRTR (Multi Round-Trip Requests). Instead of sending a request, the server returns an unfinished result with its question packed inside; the client asks the user and resends the original request with the answer attached. Click through the five steps below:
(Claude and friends)
"params":{"name":"create_project", …}}
The client fires off a tool call as usual, request ID 1. Nothing new here.
"resultType":"input_required",
"inputRequests":{"confirm":{…}},
"requestState":"a signed blob"}}
The server realizes it's short on information and returns a result — note, not a request. resultType is set to input_required, and inputRequests holds the question it wants to ask.
requestState is an opaque blob it hands the client for safekeeping, encoding how far it got. That way the server stores nothing itself.
The client shows the user the question: "Creating this project will incur charges. Confirm?" The user confirms.
The server sits this step out entirely — it can even be recycled or swapped for another machine meanwhile. The spec says the client must not inspect, parse, or modify what's inside requestState, and must not assume anything about it. Hold it as-is, nothing more.
"params":{"name":"create_project", …,
"inputResponses":{"confirm":{…}},
"requestState":"returned untouched"}}
The client packs the answer into inputResponses, resends the entire original request, and returns requestState untouched.
The request ID must be new — 2 here. The spec is explicit about this, because as far as the protocol is concerned these are two independent requests.
The server now has what it needs, does the work, and returns the final result.
Not one step in that round trip depends on the same machine or the same connection.
Supabase's product lead says the pattern unlocked something they'd wanted for a while:
Asking the user has been on our roadmap for a while, but it was awkward because the Supabase MCP runs stateless. MRTR changes that. Our tools can now check with the user before acting — the cost of creating a project, say, or a query that will delete data.
Inian Parameshwaran, product lead, Supabase
Two costs
First, the whole original request gets resent. The server may have to redo the earlier work, unless it encodes its intermediate results into requestState too.
Second, requestState is a live grenade. It passes through the client's hands before coming back, and the spec is unusually firm about it:
A server must treat requestState as attacker-controlled input. If it can affect authorization, resource access, or business logic, the server must protect its integrity with something like HMAC or AEAD — a wax seal nobody else can forge — and reject anything that fails verification. There's exactly one exemption: when tampering can do no worse than fail that one request.
Replay protection only rises to a "should." The mechanism is to embed three things in the protected blob and check each one: who (reject if someone else presents it), how long it's valid (reject if expired), and which request it came from (the method name plus a digest of the key arguments — reject on mismatch).
The spec adds a candid caveat: these measures narrow the replay window and block cross-user, cross-request reuse, but they do not guarantee single use. If you truly need one-shot semantics — a redemption code, say — you have to enforce that server-side yourself.
The shared store the server no longer needs has become the cryptographic signing it now has to get right.
MRTR also has a limited scope: it works only on prompts/get, resources/read, and tools/call. On any other request, a server must not return input_required.
Gateways can finally read MCP (plus a security rule the launch blog skipped)
State is handled. That leaves the older problem: the gateways in between never knew what you were calling.
The new spec adds required HTTP headers to every POST. Mcp-Method rides on every request and names the method being called. Mcp-Name shows up only when a specific target is named — on tools/call, resources/read, and prompts/get — and identifies which tool, resource, or prompt. Together with the always-required MCP-Protocol-Version, those few lines are all a gateway, rate limiter, or firewall needs.
Finding out which tool this calls means parsing the entire JSON body. For a high-throughput gateway that overhead doesn't pay off, so many skip it entirely.
Routing, rate limiting, metering, and authorization by method and tool name all happen at the header layer. The body stays sealed.
It sounds like pure convenience, but it comes with a hard validation rule the launch blog doesn't mention:
A server must verify that the values in Mcp-Method and Mcp-Name match the corresponding values in the request body, and return a HeaderMismatch error (code -32020) when they don't.
Why it's non-negotiable: without the check, an attacker can put a harmless tool name in the headers to get past the gateway while the body calls something dangerous. The gateway reads the header and waves it through; the server reads the body and executes. That gap is the attack.
The spec also has a word for middle layers: if the protocol version is too old, or the headers are missing entirely, reject the request rather than trust an unvalidated header.
There's a companion feature called x-mcp-header: a server can mark a tool parameter as "mirror this into an HTTP header," named Mcp-Param-{name}, which makes routing by tenant and similar dimensions easy. It's optional for servers but mandatory for clients. Remember it — it comes back in the security section.
Tool lists are finally cacheable (and upstream prompt caches survive)
Statelessness brings one convenient side effect: list results no longer vary by connection, so they can be cached.
Results from five methods — tools/list, prompts/list, resources/list, resources/read, and resources/templates/list — must now carry two fields:
public or private — decides whether shared proxies along the way can cache on everyone's behalf.There's a lower-tier requirement that's worth far more money: servers should return tool lists in a deterministic order.
That one needs unpacking. The tool list gets spliced into the model's prompt, and prompt caching matches from the beginning, character by character. Change one character at the front and the entire cache behind it is invalidated and rebilled. If your tool list comes back in a different order each time, the upstream cache never hits.
A small "keep the order stable" rule, real money saved.
What got deleted, and what got a stay of execution
This is the part to check yourself against: which of your things will break. The changelog has 9 major changes, 12 minor ones, and 4 deprecations. The two lists below are the ones to watch.
- The
initialize/notifications/initializedhandshake - The
Mcp-Session-Idheader and protocol-level sessions - The HTTP GET endpoint,
resources/subscribe, andresources/unsubscribeReplaced by onesubscriptions/listenstream, with clients subscribing per notification type ping,logging/setLevel,notifications/roots/list_changedLog level now rides per request aslogLevelin_meta- SSE resumption (the
Last-Event-IDheader and SSE event IDs)Break the stream and the in-flight request is lost; the client must resend the whole thing with a fresh request ID tasks/resultandtasks/listReplaced bytasks/getpollingnotifications/elicitation/completeandelicitationIdin URL-mode elicitation- Server-initiated requests as a patternAll of it moves to MRTR
- Roots (telling a server which directories to look at)Pass them via tool arguments, resource URIs, or server config
- Sampling (a server borrowing the client's model)Call the model provider's API directly instead
- Logging (protocol-level logs)Write to
stderr, or use OpenTelemetry - The old HTTP+SSE transportSoftly deprecated back in 2025-03-26, now formally deprecated; migrate to Streamable HTTP
- The
thisServerandallServersvalues ofincludeContextOmit it, or usenone - Dynamic Client Registration (DCR)CIMD is the recommended path; DCR stays for compatibility and will be removed in a future revision
Error codes moved too
Resource-not-found changed from -32002 to the standard -32602 (invalid params), aligning with the JSON-RPC spec. Three new codes were renumbered: -32020 header/body mismatch, -32021 missing required capability, -32022 unsupported protocol version. The spec also drew a boundary: -32000 through -32019 is a legacy zone that implementations claimed for themselves before there was a policy, so no new codes may be assigned there and new implementations shouldn't use them; -32020 through -32099 is reserved for the spec.
If your client code hardcodes -32002, this one's on you — but don't overcorrect. The spec also requires clients to keep accepting -32002 from older servers; you just stop sending it yourself.
The deprecation policy is itself new here
Deprecation used to run on unwritten understanding. Now it's in writing: three feature states and one minimum window.
accepted
12 months
The 12 months runs from the release of the revision that marked the feature deprecated, not from when the proposal was finalized. Two things come with it. There's a single "pending removal" page, so you don't have to piece the picture together from old changelogs. And first-tier SDKs are required to surface deprecations idiomatically in their next release — TypeScript's @deprecated, C#'s [Obsolete], Java's @Deprecated, Go's Deprecated: comments — with a downgrade process for SDKs that don't.
Extensions became an official channel
With the core slimmed down, where do new capabilities go?
Adding anything to MCP used to mean stuffing it into the core protocol — Tasks lived there as an "experimental feature." This revision establishes a formal extension framework: the core stays lean, new capabilities ship as extensions, and clients and servers declare which extensions they support in their capabilities.
tasks/get.Tasks was contributed by AWS and is one of the first official extensions; AWS says the new spec and its stateless core are already available in Amazon Bedrock AgentCore. The mechanism deserves a closer look. When a server decides a request will run long, it returns a task ID instead of a final result, and that ID is a durable handle — the client can disconnect, restart, and keep polling with the same ID. Tasks have five states:
The three dark ones are terminal — once reached, they don't change. If the task needs to ask the user something, it moves to input_required and the client answers with the new tasks/update. No second connection, no server-initiated messages.
Four places where authorization got tighter
Deletions and additions covered. One more area the spec singles out: over the past year, implementers spent more time on authorization than anything else. Four things changed.
localhost callbacks, and throws a redirect_uri error.If you've ever wondered why your CLI client's OAuth flow throws a
Official MCP launch blog, on the application_type changeredirect_urierror, this is probably why.
Third-party security analysis: the burden shifts to developers
All of the above is the official account. Before the spec formally shipped, a third party studied the new design as an attack surface: Akamai published an analysis in June, and SecurityWeek reported its conclusions.
Their read: the new spec genuinely removes several classes of vulnerability, while opening a few new attack surfaces — ones that depend heavily on how developers implement them.
- Session hijacking — no sessions, nothing to hijack
- Server-initiated prompts — servers can no longer start a request; they wait to be asked
- Stronger auth standards — the four tightenings from the previous section
- Predictable state objects and tracking identifiers: hijack someone's in-flight workflow, read another agent's data, trigger cross-tenant privilege escalation
x-mcp-headerleakage: a developer who accidentally maps an API key, token, or personal data into an HTTP header pushes that secret straight into the headers, where every load balancer, proxy, and logging system along the path can see it- Protocol desync: mismatched headers and body bypass header-based security controls. This is exactly what the spec's mandatory check is meant to stop — and whether it does depends on whether the server implementer took it seriously
- MCP Apps drag the browser's old problems in: stored cross-site scripting (XSS), for one
- Tasks is a fire-and-forget denial-of-service channel: creating a task is cheap for the client and expensive for the server. One request can spin up work that eats CPU, memory, and database storage, and the attacker disconnects immediately
With the protocol moving to a stateless model and introducing rich UI applications and asynchronous tasks, the critical security boundaries now depend entirely on how developers implement them.
Maxim Zavodchik, senior director of threat research, Akamai, via SecurityWeek
Their bottom line is that these changes amount to more than incremental improvement. The revision fundamentally reshapes where security responsibility lands: decisions the protocol used to enforce are increasingly handed to MCP server developers and platform operators.
The report adds an equally important qualifier: the MCP protocol itself has not become more fragile. What grew is the attack surface of the MCP servers built on the new spec.
So do I need to act now
After all those breaking changes, the practical question. The answer runs opposite to the "biggest breaking update ever" impression.
July 28 is not a switch. Existing clients and servers won't break today and won't break on July 28. That date is when the spec text was published, not when the old protocol gets turned off.
There are three concrete compatibility paths.
One, new clients yield to old servers automatically. A client speaking 2026-07-28 that meets a server capable only of 2025-11-25 or older falls back to the original initialize handshake, and the two keep talking. Going the other way, an old server that receives a request with no protocol-version header can treat it as 2025-03-26.
Two, major SDK versions are a separate matter. Python and TypeScript both shipped v2, each a breaking major release, and when you upgrade is your call — nothing to do with July 28. The TypeScript SDK's v1.x will keep getting bug and security fixes for at least 6 months after v2 goes final, and Python's v1.x branch continues receiving critical bug fixes and security patches.
Three, upgrading the SDK doesn't necessarily change what you say on the wire — and the four SDKs behave differently, which is easy to trip over:
2026-07-28, you opt in explicitly during wiring.All four first-tier SDKs (TypeScript, Python, Go, C#) supported the new spec on day one; the Rust SDK has beta support. What actually needs work falls into the categories below — run down the list:
MCP dropped the opening handshake and session ID — any machine can take any request now
MCP published its fifth spec revision, 2026-07-28, rebuilding the core around "say it and forget it." One page, with diagrams, on what changed and what breaks.
↓ One page · one of the diagrams moves
MCP (Model Context Protocol) connects AI assistants to outside tools and data. Anthropic open-sourced it in late 2024, and the whole industry runs on it now. With that many users, one engineering knot got impossible to ignore: deploying a single MCP server to the cloud at scale is hard.
Download counts and directory numbers come from MCP and Anthropic respectively; the ~20% is honeycomb.io's own figure. None of it independently reproduced.
✔ Connection stays open, so the server can turn around and ask the user something
✘ Serverless (pay-per-call cloud services that get recycled after each run) and edge nodes are out: those environments start fresh every time and remember nothing by design
✘ Gateways along the way can't read it: figuring out which tool is being called means cracking the whole request open, which costs enough that most gateways skip it
The new spec deleted the opening handshake and the session ID together. Every request now carries its own protocol version, client identity, and supported capabilities, and the server forgets everything once it's done. The load balancer (the dispatcher that hands requests out across machines) no longer has to remember who belongs where — round-robin works.
No sessions at the protocol layer, but your application can still remember things — the bookkeeping just moved. The server emits a handle from a tool, in the open, and the model passes it back as an ordinary parameter on the next call.
The transport layer held the session ID, invisible to the model. Drop the connection and the state goes with it; switch machines and everything is lost; debugging is painful.
The handle is just a tool's return value. The model can see it and thread it between tools; it belongs to your application layer, where you can inspect and debug it, and it has nothing to do with connections.
State management didn't go away. It moved from "the transport quietly handles it for you" to "you handle it explicitly in your application."
Servers can't speak first anymore, so how do they ask the user something mid-flight? The new spec's answer is MRTR (Multi Round-Trip Requests): the server returns a half-finished "I'm missing something" result, the client asks the user, and the whole original request gets resent with the answer attached.
Two costs. The whole original request gets resent, so the server may redo earlier work. And the handle passes through the client's hands before returning, which is why the spec classifies it as input an attacker can freely modify: anything touching authorization or permissions must carry a signature nobody can forge, and anything that fails verification gets rejected. The shared store you saved becomes cryptographic signing you have to get right.
The change list runs to 9 major items, 12 minor ones, and 4 deprecations. These two lists are the ones to check yourself against.
✘ Stream resumption: break the stream and the in-flight request is lost — resend it whole with a new ID
✘ Server-initiated requests as a pattern; all of it moves to MRTR
✘ Resource-not-found changed from -32002 to the standard -32602, so hardcoded values need updating
✔ The old HTTP+SSE transport gets the same 12-month window, then migrate to Streamable HTTP
✔ Dynamic Client Registration (DCR) stays for compatibility; CIMD is the recommended path
gets a session ID.
Every request after
goes to that one box.
just idling
across four main SDKs
, cut
Session ID
, cut
edge nodes
work too
how does it ask me?
passed through
client hands.
The spec says treat
it as attacker-
editable input —
bad signature,
reject it.
- × The handshake and session ID
- × SSE resumption — resend it whole
- × Server-initiated requests
- × Error code -32002 becomes -32602
Logging and the old
HTTP+SSE transport
have that long
rewriting this tonight?
The cost is that state and security land in each developer's lap.
