The Model Context Protocol shipped revision 2026-07-28 on 28 July 2026, and it is the largest change to the protocol since it launched. Sessions are gone. The initialize handshake is gone. Servers can no longer initiate requests at all.
If you run an MCP server, the useful thing to know first is that nothing breaks today on its own. A server speaking the old revision keeps working with clients speaking the old revision, and Anthropic has said support is rolling out across Claude products without announcing general availability or per-product dates. The break arrives when a client upgrades underneath you, and when it arrives it is total in both directions, because a modern client cannot talk to a legacy server and a legacy client has no mechanism at all for talking forward to a modern one.
Also gone in this revision: ping, logging/setLevel, SSE stream resumability, resources/subscribe, resources/unsubscribe, tasks/list and tasks/result. Roots, sampling, logging and Dynamic Client Registration are deprecated with a twelve month clock running on them.
Below is the full diff, every error you will hit while upgrading with the fix for each, and the one deadline where the specification and the release blog give different answers, which matters because the blog is the document most people will read.
What changed in MCP 2026-07-28
Eight things shipped, and they are easier to understand as one decision with seven consequences than as eight independent features.
Everything traces back to one decision, SEP-2575 and SEP-2567, which together make the protocol stateless. There is no handshake and no session identifier. Every request carries its own protocol version and client capabilities in _meta, and a new server/discover method replaces the capability exchange that used to happen at connection time. Servers must implement server/discover; clients may skip it entirely and handle the error instead.
Everything else follows from that. Because servers can no longer hold a connection open, they can no longer initiate requests, so Multi Round-Trip Requests (SEP-2322) replace sampling/createMessage, elicitation/create and roots/list with a pattern where the server returns resultType: "input_required" and the client re-sends the original call with the answers attached. Because the operation can no longer be inferred from a session, it moves into HTTP headers, so Mcp-Method and Mcp-Name (SEP-2243) are now required and a load balancer can route on them without parsing the body. Because list results no longer vary per connection, they become cacheable, so ttlMs and cacheScope (SEP-2549) are now non-optional fields on six result types.
Four further changes are less structural but worth knowing. Authorization hardens across six SEPs, with RFC 9207 issuer validation now mandatory on the client side and Dynamic Client Registration formally deprecated in favour of Client ID Metadata Documents. Tasks moves out of the core protocol into an opt-in extension (SEP-2663) with a redesigned polling API. A formal extensions framework (SEP-2133) lets anyone ship a protocol feature under a reverse-DNS identifier without a specification change. And a real deprecation policy (SEP-2596) arrives with a twelve month minimum window, a published registry, and SDK-level obligations to mark deprecated surfaces.
Here is the whole revision on one screen.
|
|
|
|---|---|---|
Handshake |
|
None. Per-request |
Sessions |
|
Removed. Explicit application-level handles |
Capabilities |
Negotiated once per connection |
Declared on every request |
Discovery |
|
|
Transport |
POST plus GET SSE, resumable via |
POST only. A modern-only server SHOULD return |
Routing |
Operation in the JSON body. |
Two new required headers, |
Server to client requests |
|
MRTR: |
Subscriptions |
|
|
Caching |
None |
|
Long running work |
Tasks, experimental, in core. Blocking |
|
Client registration |
DCR (MAY) plus CIMD (SHOULD) |
DCR deprecated. CIMD is the path. |
Issuer validation |
Not required |
RFC 9207 |
Logging |
|
Deprecated. Per-request log level in |
Liveness |
|
Removed |
Resource not found |
|
|
MCP error codes |
|
Three new codes, |
Tool schemas |
JSON Schema 2020-12 as default dialect |
Full 2020-12 keyword set. Network |
Tool lists |
May vary per connection |
MUST NOT vary per connection. SHOULD be deterministically ordered |
Extensibility |
|
Formal extensions framework, reverse-DNS identifiers, opt-in, independently versioned |
Deprecation |
Ad hoc |
Formal policy. Twelve month floor, published registry, SDK obligations, ninety day security exception |
Result envelope |
Bare result object |
Required |
What breaks, and what does not
Compatibility is published in the specification itself, and that matrix has seven rows rather than the four most write-ups have been reproducing. The two rows that are usually dropped are the actionable ones.
Client |
Server |
Outcome |
|---|---|---|
Modern |
Modern |
Works |
Modern |
Legacy |
Fails |
Dual-era |
Modern |
Works |
Dual-era |
Legacy |
Works |
Legacy |
Modern |
Fails. Legacy clients have no fall-forward mechanism |
Legacy |
Dual-era |
Works |
Legacy |
Legacy |
Works per the legacy revision |
Modern means 2026-07-28 and later, with per-request metadata. Legacy means 2025-11-25 and earlier, with the initialize handshake. Dual-era means an implementation that speaks both, which is the only posture that is safe if you have real users.
The Python SDK v2 gives you this by default, and it is worth understanding the mechanism rather than assuming it. Server.run drives a dual-era loop in which the client's first request decides the connection's era, once. A request carrying the 2026-07-28 per-request _meta envelope opens a modern connection; anything else, including an initialize handshake, opens a legacy one. A later claim from the other era is then refused: initialize on a modern connection returns UNSUPPORTED_PROTOCOL_VERSION naming the versions served, and an enveloped request on a legacy connection returns INVALID_REQUEST. So a v2 server serves both eras, per connection, not per request. One caveat that is not in the release notes and that I found in the source: HTTP era routing is header-based today, with body-primary classification marked as a follow-up.
Twenty things break. Here they are by severity, where total means the implementation stops working entirely rather than degrading.
Change |
What it breaks |
Severity |
|---|---|---|
|
Every client and server implementing the handshake |
Total |
|
Any server holding per-session state, any client resuming a session |
Total |
Per-request |
Any request without them returns |
Total |
|
Mismatch returns |
Total |
|
Any hand-rolled HTTP client |
Total |
Server-initiated requests forbidden, MRTR replaces them |
Every server using sampling, elicitation or roots |
Total |
The GET SSE stream is gone and resumability is removed. A modern-only server SHOULD answer GET and DELETE with |
Any client relying on the GET stream or on |
Total |
|
Any resource-subscription client |
Total |
Tasks redesigned and moved to an extension |
Everyone on the experimental |
Total, for tasks users |
|
Clients not defaulting an absent value to |
High |
Resource not found moves from |
Any client matching on either literal code |
High |
|
Any HTTP client that sends it, and any server that only cancels on receiving it |
High |
|
Health-check loops and log-level control |
High |
|
URL-mode elicitation correlation |
High |
Tool lists MUST NOT vary per connection |
Any server personalising |
High |
Any JSON Schema 2020-12 keyword is now allowed, but network |
Servers relying on remote |
Medium |
An invalid |
Silent tool disappearance |
Medium |
|
Servers omitting them are non-conformant |
Medium |
DCR deprecated and |
OIDC-strict authorization servers rejecting native redirects |
Medium |
RFC 9207 |
Clients that skip it are non-conformant |
Medium |
What does not break is worth stating too, because it is more than you would expect. The stdio message framing is unchanged. Tools, resources, prompts, completion, pagination and progress keep their shapes. Elicitation survives in both form and URL modes, just delivered differently. And roots, sampling and logging are deprecated rather than removed, which means they are annotation-only and continue working for at least twelve months.
There is one trap in era detection that is worth its own paragraph, because it is the thing implementations will get wrong, and the rules differ by transport.
On HTTP you attempt a modern request and fall back on 400 Bad Request, except that a modern server also returns 400 for UnsupportedProtocolVersionError, for MissingRequiredClientCapabilityError, and for header validation failures, so the specification says you SHOULD inspect the body before concluding anything. On stdio you probe with server/discover, and there the rule is stronger: the fallback MUST NOT be keyed to one specific error code. Era is a property of the server rather than of an individual request, so cache the determination for the lifetime of the server process on stdio, or of the origin on HTTP.
One consequence of choosing dual-era that is easy to miss: the 405 guidance for GET and DELETE is written for servers that support only this revision. If you are serving both eras you still need the GET stream for legacy clients, so do not blanket-405 it.
It is also worth being straight about a tension in the messaging around all this. The operator of Glama, who runs the largest open source MCP server index, wrote on Hacker News that "the new protocol is wire-incompatible in both directions. This means that many of the servers/clients will need to be refactored (not enough to just update the SDK). It will take time and it will be messy." A maintainer in the same thread wrote that "this protocol change doesn't require you to do anything to existing running MCP code." Both statements are true, about different situations. The second is true of a deployment nobody touches. The first is true the moment anything on the other end of the wire moves.
Every error you will hit upgrading to MCP 2026-07-28
These are ordered roughly by how likely you are to see them.
Unsupported protocol version after upgrading, error code -32022
This is the two eras failing to talk, and it is the most common thing you will see this week. The error carries the useful data in error.data:
{
"jsonrpc": "2.0", "id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
}
}
If data.supported contains only old revisions, the server has not been upgraded. If it contains only 2026-07-28, the client has not. The fix in both directions is to serve both eras rather than to pick one, and the practical route to that is an SDK bump rather than hand-rolling the negotiation, because the negotiation has more edge cases than it looks like.
One thing you cannot fix from the client side: a legacy client hitting a modern-only server. There is no fall-forward path in the older revisions, so that combination fails and will keep failing until the client updates. If you operate the server, this is the argument for staying dual-era well past the point where it feels necessary.
400 Bad Request with a HeaderMismatch error, code -32020
Three headers are now required on every Streamable HTTP POST, and their values must match the body:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
It is worth stating the scope of these precisely, because it is easy to over-apply them. Mcp-Method is required on all requests. In the core protocol Mcp-Name is required only on tools/call, resources/read and prompts/get, where it carries params.name or params.uri. Sending Mcp-Name on a tools/list is not what the specification asks for.
The tasks extension then adds three more. SEP-2663 requires that tasks/get, tasks/update and tasks/cancel over Streamable HTTP set Mcp-Name to params.taskId, so that "transport intermediaries and load balancers [can] route subsequent requests for the same task to the server instance holding its state, which is typically required for correctness." Worth sitting with that sentence for a moment. It is discussed further below.
Note also that MCP-Protocol-Version is not new. It has been required on HTTP since 2025-06-18. The two genuinely new mandatory headers are Mcp-Method and Mcp-Name, plus any Mcp-Param-* your tools opt into.
Header names are case-insensitive and header values are case-sensitive, and for integers the comparison should be numeric rather than string, so 42.0 and 42 match.
There is a real caveat, and it was published a week before the specification landed. Christian Posta at Solo.io pointed out that headers can lie: an attacker sends Mcp-Name: echo in the header while the body calls printEnv, and a proxy that allowlists on headers alone waves it through.
Two mitigations exist. A server that processes the body MUST reject mismatches with -32020. And intermediaries enforcing policy on mirrored headers SHOULD verify that MCP-Protocol-Version indicates a revision requiring header-body validation, and SHOULD reject rather than trust the headers otherwise. Both are correct. The second is also a SHOULD placed on the component with the least incentive to comply, and it requires the intermediary to be MCP-version-aware, which is precisely the MCP-native dataplane Posta is arguing for. His post ends with a working fix rather than a dead end. Read it that way, and treat these headers as a routing hint rather than an authorization fact.
There is an operational failure here that has nothing to do with your code. Check whether your reverse proxy, your CDN, or your API gateway strips unknown request headers, because these headers will vanish silently on the way to your server and every request will fail validation for a reason that is invisible from either end.
Your server-initiated requests stopped working
Servers cannot originate requests any more. This is not a restriction on when they may do it; there is no ServerRequest union in the schema at all. sampling/createMessage, elicitation/create and roots/list still exist as types, but only ever as payloads inside a result.
MRTR is the replacement. Your server returns an incomplete result naming what it needs:
{
"jsonrpc": "2.0", "id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
},
"requestState": "AEAD-protected blob"
}
}
The client gathers the input and re-sends the original request with inputResponses and the requestState echoed back byte for byte, using a new JSON-RPC id, because the specification treats the retry as an independent request.
Six rules will bite you if you skip them. InputRequiredResult is permitted only on tools/call, prompts/get and resources/read, and servers MUST NOT send it on anything else. Clients MUST NOT inspect, parse or modify requestState, and MUST NOT include one on the retry if the server did not send one. Servers MUST NOT send an inputRequest type the client did not declare on that particular request. Servers MUST NOT assume the client will ever fulfil the request or retry at all, which means every MRTR exchange needs a timeout and a clean path to giving up. And inputRequests and requestState apply only to the retry of that one request, never to anything the client has in flight in parallel.
The sixth is the one that will produce a crash rather than a bug report. inputRequests is optional. The schema requires at least one of inputRequests or requestState, and it names the case where only the state is present: load shedding. A server under pressure can hand back requestState alone, and the specification says the client MAY then simply retry immediately. Any client that writes for key in result.inputRequests throws on that response, at precisely the moment the server is least able to absorb a retry storm.
MissingRequiredClientCapability, error code -32021
Capabilities are now declared per request rather than once per connection, and a server may not rely on one the client did not send on that specific request. When it needs something undeclared it returns -32021 with the missing capabilities listed in data.requiredCapabilities, and on HTTP the status is 400.
On the client side, stop treating capabilities as connection setup. They belong in _meta on every call:
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} },
"io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0" }
}
protocolVersion and clientCapabilities are required. clientInfo is optional but you should send it, and the specification is clear that neither clientInfo nor serverInfo is verified by the protocol, so nothing should make a security decision based on either.
Resource not found now returns -32602, not -32002
The error code layout changed underneath everyone. Two codes that shipped in 2025-11-25 are retired, and three new ones arrive in a newly reserved block.
Code |
Shipped in |
Status in |
|---|---|---|
|
|
Retired. Use |
|
|
Retired. Implementations of this revision MUST NOT emit it |
|
new |
Reserved block |
|
new |
Reserved block |
|
new |
Reserved block |
Codes from -32020 to -32099 are now reserved for the specification, and implementations must not emit codes in it that the specification has not defined. The range -32000 to -32019 is legacy, and apart from -32002 receivers must not assume any specific meaning for codes in it.
One clarification, because several write-ups have this wrong, including an earlier version of this one. The three new codes were briefly numbered -32001, -32003 and -32004 inside the 2026-07-28 draft cycle and renumbered before release. They were never part of 2025-11-25, whose schema defines exactly one non-standard code, -32042. So if you are migrating from a shipped 2025-11-25 implementation, the codes that actually change under you are -32002 and -32042. If you built against a release candidate, check for the old three.
Your tool disappeared from tools/list
This is the most obscure genuinely breaking behaviour in the release, because it produces no JSON-RPC error, and because it is transport-dependent in a way that will waste somebody an afternoon.
x-mcp-header is a new annotation that lets a server mark individual tool parameters to be mirrored into Mcp-Param-{Name} HTTP headers, so that a gateway can route or rate-limit on a parameter value without reading the body. It is optional for servers and mandatory for clients to support. The constraints on it are strict: primitive types only, with number explicitly not permitted while integer, string and boolean are; the property must be statically reachable from the schema root through a chain of properties keys only, so nothing behind items, $ref, oneOf, anyOf, allOf, not, or if/then/else; the value must be case-insensitively unique across the whole inputSchema; annotated integers must sit within the JavaScript safe range; and the value must be a valid HTTP field-name token with no control characters.
If any of those is violated, the client MUST exclude the tool from the result of tools/list. There is no JSON-RPC error. The specification does ask clients to log a warning naming the tool and the reason, but that is a SHOULD and it surfaces in your client's log rather than to the model or the user, so the tool simply is not there any more and nothing in the conversation says why.
What will actually cost you the afternoon is that this rule binds Streamable HTTP clients only. Clients on other transports MAY ignore x-mcp-header entirely. So the same server, with the same tool definition, can expose the tool over stdio on your laptop and hide it over HTTP in production.
400 with no obvious cause, and a missing Accept header
Not new, but hand-rolled clients get it wrong constantly and this revision gives them more opportunities to. The client MUST include an Accept header listing both application/json and text/event-stream, because the server chooses between a single JSON response and an SSE stream and the client has to be ready for either.
404 Not Found on a method the server does not implement
This one is a trap for gateways and for anyone with retry logic. An unimplemented RPC method returns HTTP 404 with JSON-RPC -32601, which is deliberately distinguishable from a transport-level failure. Anything that treats 404 as "this server is gone" and tears down the connection or fails over will misbehave against a perfectly healthy server that simply does not implement the method you asked for. Check the body before concluding anything about a 404, exactly as you already have to for a 400.
Your list results are being served to the wrong tenant
Not an error message, which is why it belongs here. ttlMs and cacheScope are now non-optional on six result types: server/discover, tools/list, prompts/list, resources/list, resources/templates/list and resources/read. Worth noting that the release blog lists four of those and the changelog lists five; the schema and the caching page say six, and they are the ones that count.
cacheScope takes "public", meaning any client, gateway or caching proxy may serve the response to any user, or "private", meaning it may be reused only within the same authorization context and MUST NOT be shared across contexts. Get this wrong on an authenticated endpoint and you have built a cross-tenant data leak with the protocol's blessing. The specification says so almost in those words: servers must be aware that a "public" response may be shared between callers even when it comes from an authenticated endpoint.
Two rules that follow. Results produced by an MRTR retry must never be cached, because they depend on inputs that are not part of the cache key. And clients SHOULD NOT treat ttlMs as a polling interval; those that do poll MUST apply jitter and backoff, which is the single most likely way a naive client turns a TTL into a thundering herd.
Long-running jobs broke when sessions went away
One of the sharper reactions on Hacker News was from someone who uses MCP servers to monitor workflows that run for days, and who wrote simply that "i use them to monitor workflows that takes days and now i have to make some changes."
Tasks, now an extension, is the replacement, and it is a second migration for anyone who adopted the experimental API in 2025-11-25. What changed:
|
|
|---|---|
Client opts in per request with a |
The server decides, unilaterally, per request |
|
|
No client-to-server input method |
|
|
Removed. There is no way to enumerate tasks |
|
|
|
|
|
|
Both parties can be requestors |
|
Two consequences of that table deserve calling out. Because tasks/list is gone, if your client loses a task id the task is unreachable, so persist task ids durably rather than holding them in memory. And because the extension must be negotiated in _meta on each request, a server MUST NOT return a task handle to a client that did not declare the extension on that specific call.
One detail that will catch people moving off resources/subscribe: request-scoped notifications, meaning notifications/progress and notifications/message, still flow on the response stream of the request they belong to, not on the subscriptions/listen stream. And on stdio, if the connection drops the client MUST re-send subscriptions/listen, because the server holds no subscription state across reconnections.
One caveat before you build on this, and it is a real one. The extension's normative schema file is not published. The repository states it "is under development and is working towards implementing the changes described in SEP-2663", and carries a banner reading "not an official extension and may change significantly or be discontinued", while the 2026-07-28 changelog describes tasks as having moved into "an official extension." SEP-2663 itself still cites -32003, an error code the shipped core renumbered to -32021. Treat this part of the ecosystem as unfinished and budget for a second pass.
Your traces and your latency numbers went wrong
This one is quiet and it affects everyone who has observability wired up. Because the MRTR retry MUST use a different JSON-RPC id from the original request, one logical operation now appears on the wire as several id-distinct requests with no correlation identifier between them.
Every tool that keys on request id, which is all of them, therefore reports an inflated call count and, worse, excludes the interval where a human was actually deciding something. That interval is precisely the thing you care about for an elicitation-based confirmation. An analysis published on release day measured a six second operation reporting as two calls totalling one second. That is one scripted exchange against the author's own proxy rather than a survey, so treat it as an illustration of the mechanism rather than a measurement of the blast radius. The mechanism is the part that generalises.
There is no protocol-level fix. Linkage has to be inferred, either from the opaque requestState blob or by matching the key sets in inputResponses against the inputRequests that produced them. It is worth noticing that the same revision standardised W3C trace context propagation, with traceparent, tracestate and baggage explicitly exempted from the reverse-DNS prefix rule in _meta. The plumbing got better and the semantics got worse in the same release.
Authorization started failing on desktop and CLI clients
If a client is going to break against a strict enterprise authorization server on day one, this is why. Clients MUST now specify application_type during Dynamic Client Registration, and omitting it defaults to "web" under OpenID Connect, which conflicts with the localhost redirect URIs that desktop applications, mobile applications and CLI tools use. Native clients should send application_type: "native"; genuinely remote browser-based clients should send "web".
Two other authorization changes are worth checking before you ship. RFC 9207 issuer validation is now mandatory on the client side and it applies to error responses as well as success responses. You do decode the iss value from the form-encoded response, and then you compare it as a literal string: no scheme or host case folding, no default-port elision, no trailing-slash handling and no percent-encoding normalisation. Note that authorization servers only SHOULD send iss, so whether its absence is fatal depends on the server advertising authorization_response_iss_parameter_supported. And persisted client credentials must now be keyed to the issuing authorization server's issuer, so when the authorization server changes you must re-register rather than reuse.
SDK readiness, as of 29 July 2026
SDK |
Tier |
Version with |
|---|---|---|
TypeScript |
1 |
|
Python |
1 |
|
Go |
1 |
|
C# |
1 |
|
Rust |
2 |
|
Java |
2 |
No support yet |
Swift, Ruby, PHP, Kotlin |
3 |
No support, no committed timeline |
Two notes on that table. Java does have a 2.0.0, published on 11 June 2026, and it simply predates this revision; the jar contains 2025-11-25 and earlier and no 2026-07-28 at all. Java is a Tier 2 SDK, which carries a six month obligation, so support is due around January 2027. And on TypeScript, v2 is ESM-only, requires Node 20 or later, and splits the old monolith into separate server and client packages, which is a larger change than the version number suggests. There is a codemod: npx @modelcontextprotocol/codemod v1-to-v2 .
If you maintain a library that depends on an MCP SDK, put an upper bound on it today, mcp>=1.28,<2 or @modelcontextprotocol/sdk@^1, so that v2 does not land on your users before you have decided how to handle it.
Before and after, in two pictures
If you came here with a red terminal, that is the practical half. What follows is why any of it happened.
Two diagrams, each showing the same two things: how the protocol deploys, and what actually goes over the wire. Those turned out to be the same question, which is the whole reason this revision exists.
That orange band running down the middle of the first diagram is the argument for the entire release. It is the session lifetime, and the line written across it says that every request underneath it must reach server instance number one. That constraint is not a deployment choice you made. It is a property of the protocol, because the negotiated capabilities and every in-flight request lived in one server process, so the load balancer had no option but to pin. Two of the three instances in that picture are unreachable for the duration of the session, the shared session store hanging off the side is pure protocol overhead you provisioned and paid for, and the gateway in the middle cannot route on anything useful because every call is POST /mcp with the operation buried in the body.
The payoff in the second diagram is small and easy to miss, so it is worth pointing at directly. Step three is a tools/call that lands on instance three. Step four is that same logical call being retried, after the client gathers the input the server asked for, and it lands on instance two. A different box, mid-operation, and nothing breaks. That is what statelessness actually buys, and it is very hard to convey in prose.
Two wins and two new burdens sit in the consequence strip at the bottom of the second diagram, and the mix is deliberate, because that is an accurate picture of the trade. Autoscale and serverless become native. In exchange, requestState is now an attacker-controlled input you have to authenticate yourself, and the MRTR retry carries a new JSON-RPC id, which quietly breaks anything measuring latency by request id.
What the release fixed, and what it left alone
MCP has been shipping for twenty months and accumulating a fairly specific critique record the whole time. The interesting way to read 2026-07-28 is not as a list of features but as a response to that record, ranked by whether the specification deleted the thing being complained about. A protocol that removes the feature people objected to is conceding the argument in the plainest language available.
Strongest evidence that this is what happened: the maintainers wrote the critique themselves, ten months before shipping the fix. From "Exploring the Future of MCP Transports", published 19 December 2025:
"Stateful connections force 'sticky' routing that pins traffic to specific servers, preventing effective auto-scaling."
"Developers building simple, ephemeral tools are often required to manage complex backend storage to support basic multi-turn interactions."
"Load balancers and API gateways must parse full JSON-RPC payloads to route traffic, rather than using standard HTTP patterns."
An earlier post, "Update on the Next MCP Protocol Release" from September 2025, had already named horizontal scaling, long-running tasks, discovery, extensions and SDK compliance as the top five gaps.
Five external critiques were louder than the rest. Statefulness and session affinity, whose root cause was precise: the handshake negotiated capabilities per connection, the session identifier lived in the transport, and both meant every request had to reach the same process. The New Stack put it as teams "paying to solve a distributed-systems problem the protocol had created for them." The tool-definition context tax, where Apideck measured three MCP servers consuming 143,000 of a 200,000 token window before a single user message, and where the most damning source is Anthropic itself, which published a method for taking 150,000 tokens down to 2,000, roughly a 98.7% reduction, by not using the protocol the way it was designed. Prompt injection, where Invariant Labs demonstrated data exfiltration through the GitHub MCP server and concluded that "GitHub alone cannot resolve this vulnerability through server-side patches," which is what makes it architectural rather than a bug. The absence of an authorization model, summarised by Pomerium as "access is binary. Either a tool is exposed, or it isn't." And gateway hostility, which is the same root cause as the first one seen from the operations side.
Below those sit the rest: Microsoft Research measuring performance degradation of up to 85% as the tool space grows, along with 775 colliding tool names and the word search appearing across 32 distinct servers; the OAuth and Dynamic Client Registration burden that Descope catalogued as an enterprise blocker; the missing distributed-systems contract around idempotency, deadlines and tracing; and the absence of any asynchronous operation model.
One disclosure belongs here rather than in a footer, because it changes how you should read half of those links. Speakeasy, Scalekit, Descope, WorkOS, Pomerium, Docker and Apideck all sell products that exist because of the gaps they document. Their measurements are checkable and I have used several of them. Their framing is not neutral, and neither is mine, since Maximem ships tooling in this space too. Microsoft Research, Trail of Bits, Invariant Labs and Anthropic's own engineering blog are the cleanest sources in that list.
So, the scorecard. Eighteen critiques, mapped against what actually shipped.
Critique |
What |
Verdict |
|---|---|---|
Statefulness and session affinity |
Sessions, |
Solved |
Tool-definition context tax |
Deterministic tool ordering, cacheable |
Untouched |
Prompt injection and the lethal trifecta |
Nothing |
Untouched |
No authorization model |
Nothing. Scope step-up, which is often credited to this release, shipped in |
Untouched |
Tool poisoning, rug pulls, line jumping |
Nothing. Descriptions remain unsigned and mutable |
Untouched |
Tool-space interference, no namespacing |
Name-format rules, and clients "SHOULD implement a disambiguation strategy" |
Weak partial |
Auth complexity and the DCR burden |
|
Partial |
Gateway, WAF, metering and caching hostility |
|
Solved |
Missing distributed-systems contract |
Tracing yes, schema strictness yes, versioning discipline yes. Idempotency, deadlines and per-tool versioning no |
Partial |
No long-running or async model |
Tasks extension with polling, updates, cancellation and a durability guarantee. But its normative schema is unpublished and its own repository says it is not an official extension |
Partial |
No multi-tenancy primitive |
Nothing, beyond |
Untouched |
Transport churn |
One transport story now, but this release is churn: the third breaking transport shape in sixteen months. The improvement is that churn is finally governed by a deprecation policy |
Neutral |
Configuration fragility |
Removes session config as a failure class, adds header validation as a new one |
Neutral |
Latency at the call level |
No handshake, so the first-call penalty disappears. Cacheable lists cut repeat cost |
Improved |
Registry and discovery |
|
Partial |
Observability gap |
Trace context standardised, then MRTR breaks id-keyed measurement |
Mixed |
Risk stratification and cost opacity |
Nothing in core |
Untouched |
SDK quality and compliance opacity |
SDK tiering itself is SEP-1730, from October 2025. This release adds Tier 1 deprecation-marking obligations and conformance tests as a gate to Final |
Partial |
Three clear wins, nine partial or mixed, six untouched.
I want to be explicit that I graded this twice, because the first pass was too generous. It credited this release with a scope step-up flow that shipped eight months earlier in 2025-11-25, rated long-running work solved on an extension whose normative schema does not exist, called transport churn solved in the release that caused it, and counted the registration changes as a fix when CIMD was already SHOULD and DCR already MAY. Correcting all four made the pattern sharper rather than softer, which is usually a sign the pattern is real rather than an artifact of how you drew the table.
2026-07-28 is an infrastructure release, not an agent-experience release. Every problem that lives between a load balancer and a server process got fixed. Every problem that lives between a model and a tool description did not.
Those untouched rows are worth sitting with, because they are the ones that most consistently kept MCP out of production. The context tax got deterministic tool ordering, which improves prompt-cache hit rates at the margin and is the entire response to a complaint that produced six separate front-page Hacker News threads and a first-party Anthropic post documenting a 98.7% workaround. Prompt injection got nothing, and tool descriptions are still unsigned, still mutable after a user has approved them, and still injected into the model's context at tools/list time, which is the mechanism behind what Trail of Bits named line jumping and the reason invocation-time approval was never the protection people assumed it was. Authorization modelling got nothing, and it is worth correcting a claim I have seen in several write-ups including my own first draft: the step-up flow with insufficient_scope shipped in 2025-11-25, not here. What this release adds is authorization hardening, which is real. Authorization modelling, meaning per-tool scope and identity propagation into the tool, went to an extensions repository.
Some fair objections, before the conclusion.
No single revision could clear eighteen months of accumulated complaints, and that is true. The point was never the count. It is which half got cleared.
A better objection is that statelessness is an agent-experience change, and this one has more force than I would like. Fewer failure modes, no reconnection storms, no session dying mid-conversation, cheaper cold starts: reliability is part of the experience, and this release improves it. What it does not improve is anything the model can perceive. Same tool count, same trust in the descriptions, same token bill.
Strongest of all is the argument that agent-experience work is happening in extensions now, and that the framework this release formalised is precisely what makes that possible. Go and look at the official extension support matrix, though, which today lists nine clients. All nine support MCP Apps. Exactly one supports anything beyond it, and none supports OAuth Client Credentials. Extensions are opt-in on both ends, and the layer above the protocol is not there yet.
That pattern is not carelessness. It is a boundary decision, and it is defensible. The protocol has drawn a line and put infrastructure concerns on its own side of it while leaving agent-experience concerns to the layer above. There is a serious argument that this is exactly what a protocol should do. It also means that if MCP was expensive for you last week because of tokens, or risky for you last week because of injection, this release changed nothing about either.
What the release created
A piece that only lists what got better is a piece worth distrusting, so here is the other column.
requestState moves a security boundary out of the protocol and into your application code. The specification is unusually direct about this. Servers MUST treat requestState as attacker-controlled input. If it influences authorization, resource access or business logic, servers MUST protect its integrity with HMAC or AEAD and MUST reject state that fails verification. Servers SHOULD embed the authenticated principal, a short expiry, and a digest of the originating request inside the protected payload. And then, having said all that, the specification concedes that these measures "do not by themselves guarantee single-use." Maxim Zavodchik at Akamai put the consequence plainly: "critical security boundaries are now entirely dependent on how developers implement them." Set against Equixly's finding that 43% of the MCP servers they assessed shipped command injection, and that 45% of the vendors they disclosed to dismissed the risk as theoretical, that is a large bet on developer diligence.
Headers can lie, which is covered above, and the component making the policy decision is the one not reading the body.
And sessions are not quite as gone as the headline suggests. SEP-2663 requires that tasks/get, tasks/update and tasks/cancel over Streamable HTTP carry Mcp-Name: <taskId>, explicitly so that intermediaries can "route subsequent requests for the same task to the server instance holding its state, which is typically required for correctness." That is instance affinity, reinstated by the flagship extension for long-running work, keyed to a task ID instead of a session ID. It is a better design than the old one, because the affinity is now explicit, addressable, and scoped to a unit of work rather than to a connection. But anyone reading "sessions are gone" as "you will never need sticky routing again" should read that sentence in SEP-2663 first.
Roots deprecation removes the only structural filesystem boundary the protocol had. The prescribed migration is to pass directories through tool parameters, resource URIs, or server configuration, which converts a client-enforced limit into a server-trusted argument. Scope enforcement now lives in each developer's code and is applied inconsistently by construction.
Sampling deprecation quietly moves cost onto server operators. SEP-2577 gives three reasons and leads with the design one: "correct sampling implementation requires human-in-the-loop approval, model selection logic, security considerations, and (since SEP-1577) tool loop support," which it says contributed to low client adoption. The lead maintainer's version, to The Register, is that "the semantics were confusing and hard to implement." That reasoning is sound.
The part nobody has priced is the migration. The prescribed replacement is to integrate directly with LLM provider APIs, which moves model credentials, billing, rate-limit handling and model-choice policy from the client to the server operator. That is my reading of the consequence rather than a complaint anyone has organised around, and I could not find one.
And the specification is silent on two things it probably should not be. There is no CORS guidance anywhere in it, despite four new mandatory headers that are all non-simple and therefore all trigger a browser preflight. To be precise about the claim, I grepped all thirty-one specification pages and the schema: zero mentions of CORS, preflight, OPTIONS or Access-Control-Allow-Headers.
Documentation is a different matter, and the way it mentions CORS is the more interesting finding. The 2026-07-28 authorization tutorial ships sample code containing cors({ origin: "*", exposedHeaders: ["Mcp-Session-Id"] }), exposing a header this revision deleted, and two of its live handler blocks still read req.headers["mcp-session-id"]. That page is not stale in one snippet. It is stale throughout, under a 2026-07-28 URL. The extension support matrix has the same problem in miniature, still instructing client authors to "declare support in the extensions field of your initialize capabilities" for a handshake that no longer exists. There is also no size bound on requestState, which round-trips through the client on every retry.
Trilogy AI makes the sharpest counter-argument to the whole release, and it is worth taking seriously: statelessness relocates complexity rather than deleting it. Explicit handles consume token budget. Models operating with garbled handles fail differently from expired sessions, but they still fail. And nobody has specified what recovery looks like when a model loses a handle in the middle of a conversation. The session was at least a thing the infrastructure understood. The handle is a string the model has to keep track of.
When does HTTP+SSE actually stop working, and why do two official pages disagree?
Every write-up on this release is leading with July 2027, which is when roots, sampling, logging and Dynamic Client Registration become eligible for removal. That is correct and it is not the nearest deadline.
Nearest is the legacy HTTP+SSE transport, deprecated back in 2025-03-26 and only now given a removal clock. Two official sources describe that clock differently.
Source |
Implied earliest removal |
|---|---|
The deprecated features registry, "three months after SEP-2596 reaches Final", with PR #2596 merged under the |
around 18 August 2026 |
The release blog, "officially deprecated, with a year-long offramp" |
around July 2027 |
The registry's number is the reasoned one, and SEP-2596 explains it in a section called Transition that is worth quoting because it is better thinking than the blog's summary:
"Both features were publicly deprecated well over twelve months before this SEP, so the minimum deprecation window has in practice already been served; re-anchoring their clock to a future revision release would restart a window the ecosystem has already had. Each is therefore given a three-month grace period from the date this SEP reaches Final before it is eligible for removal, matching the floor the Expedited removal clause sets for the shortest permissible window."
So the SEP-Final anchoring is deliberate and reasoned, not an anomaly. HTTP+SSE and the includeContext values are the two features grandfathered in from before the policy existed, and the SEP is explicit that "the deprecation-SEP requirements in Deprecating a feature are not applied retroactively" to them.
What that leaves is smaller than a governance story and more useful than one: the document most people will read is wrong about the date. The blog says twelve months. The registry and the SEP say three, from a date that has already passed.
Before you act on that, one thing the SEP says that the coverage has also missed, and that cuts the other way:
"Removal still follows Removing a feature: a Core Maintainer decision at release preparation, not an automatic event when the grace period ends."
Removal also requires a new Current revision to ship, and none is scheduled. So there is no August cliff. What there is, from around 18 August 2026, is eligibility, meaning the point from which any future revision may drop the transport without further notice. If you are still on HTTP+SSE, the planning implication is not that something breaks next month. It is that you no longer have a guaranteed window, and you should stop assuming the twelve months the blog implies.
Now the governance oddity, which is a different feature. The lifecycle policy that shipped in this revision says plainly that "deprecation is a specification change and therefore requires a SEP per the SEP guidelines." Dynamic Client Registration's deprecation is recorded against PR #2858, titled "Authorization spec split," a documentation reorganisation merged on 4 June 2026 whose description is about page load time and navigation. It is the only row in the deprecated registry without a SEP number.
Three rebuttals suggest themselves and none of them holds. That CIMD arrived via SEP-991 and the deprecation merely followed it: SEP-991 contains zero occurrences of the string "deprecat". That SEP numbers are PR-derived under SEP-1850, so #2858 is effectively the SEP number: there is no file in seps/, no SEP label and no SEP status, and the registry's own column heading reads "Deprecation SEP" while the cell reads "PR #2858". That the grandfathering clause covers it: it does not, because that clause covers only the two features deprecated before the policy existed, and DCR is a new deprecation in this revision.
I do not think anything was slipped through. DCR was already only a MAY in 2025-11-25 and CIMD was already a SHOULD, so the substantive change is close to a label, which is probably exactly why nobody stopped to write a SEP. That is the interesting version of the finding: the first process the policy governed was skipped, on a change small enough that skipping it felt reasonable.
How much of this matters in practice. I paginated the official registry on 29 July 2026. There are 1,888 published version entries declaring an sse remote, which is 761 distinct servers, 709 of them the latest version of their server, against 20,173 entries and 9,045 distinct servers on streamable-http. Note the distinction between entries and servers, because a naive count inflates the number by more than double.
A better measure of who actually gets hurt comes from Glama's index of 62,726 open-source servers, shared on Hacker News the day the spec shipped: roughly 88% have been pushed to within the last year, which means about 12% have not been touched in twelve months. Those are the servers that will still be speaking a dead transport when someone finally removes it.
What to do, by what kind of MCP you run
Four situations cover most people. The migration skill linked below covers the rest.
If you run a local stdio server, you are the lowest-risk group, because there are no headers, no sessions and no OAuth in your path. Implement server/discover, since it is a MUST for servers and it doubles as the era probe on stdio. Move logging to stderr, because logging/setLevel is gone and notifications/message is deprecated and must not be emitted unless the request carried a log level in _meta. Stop writing JSON-RPC requests to stdout entirely. And plan for in-flight requests simply being lost when the process dies, because the protocol no longer pretends otherwise.
If you run a multi-tenant SaaS MCP, this is the highest-stakes cluster and the first thing to audit is cacheScope, before anything else, because that is the field that leaks. Anything user-scoped, tenant-scoped or permission-scoped must be "private". Then check tool visibility, and here is the carve-out most write-ups are missing: tool lists MUST NOT vary per connection, but they MAY still vary by the authorization presented on the request, since credentials are per-request input rather than connection state. Per-tenant tool filtering is still entirely legal. It simply has to key off the token instead of the session. After that, work through the authorization changes: iss validation, CIMD support, application_type, credential binding, and single-challenge scope emission.
If you run on serverless or the edge, you are the biggest winner and most of your work is deletion. Remove the Durable Object, Redis or DynamoDB session pinning, remove session affinity from the load balancer, and then actually test round-robin by firing a sequence of requests and asserting that none of them depends on the previous one landing anywhere in particular. Re-tune your cold-start and concurrency budgets while you are there, because a one-shot tool call is now literally one HTTP request with no handshake in front of it, and your per-invocation cost model has changed. If you are on Cloudflare, note that McpAgent is now feature-frozen and createMcpHandler(v1Server) goes away in the next major.
If you run a gateway, proxy or aggregator, three rules. Do not make policy decisions on Mcp-Method and Mcp-Name alone. Handle resultType values you do not recognise, and never pass "task" through to a client that did not negotiate the extension. And remember that the MRTR retry arrives with a different JSON-RPC id, so any metering or rate limiting keyed on id will double-count a single logical operation. There is also an opportunity here that the Glama operator named on Hacker News, which is that protocol-incompatible servers and clients are going to need translating between for at least a year, and the gateway layer is the natural place for that to live.
The twelve-point compatibility checklist
Run this against your server before you call the migration done.
Can a request land on a cold, empty instance and succeed?
Do you implement
server/discover? It is a MUST for servers.Do you accept and validate
MCP-Protocol-Version,Mcp-MethodandMcp-Name, and does your proxy forward them?Do you ignore inbound
Mcp-Session-IdandLast-Event-ID? (And if you serve only this revision, do you answer GET and DELETE with405? If you are dual-era, keep the GET stream for legacy clients.)Is
resultTypepresent on every result, and does your client handle"input_required"?Are
ttlMsandcacheScopepresent on all six cacheable result types, and iscacheScopecorrect for authenticated data?Does
tools/listavoid varying per connection, and return a deterministic order?Have you replaced every server-initiated request with MRTR, and is
requestStateintegrity-protected?Have you moved off
-32002and-32042, and if you built against a release candidate, off the pre-release-32001,-32003and-32004numbering?Does your
inputSchemavalidate as JSON Schema 2020-12, with network$refresolution disabled by default?Have you inventoried your use of roots, sampling, logging and DCR against the 2027-07-28 clock?
If you are still on HTTP+SSE, is your Streamable HTTP migration scheduled? Eligibility for removal opened around 18 August 2026, not in twelve months, whatever the release blog says.
The tool
There is a gap in the ecosystem that you can verify with one curl. The official MCP registry has no protocol-version field: server records carry a $schema, the server's own semantic version and its remote types, and nothing at all declaring which protocol revision the server speaks. So there are tens of thousands of published servers, a wire incompatibility that runs in both directions, and no machine-readable compatibility signal anywhere.
Client-side information is thinner still, and in a way that has already had consequences. SEP-2577 justifies deprecating roots, sampling and logging partly on low client adoption, and cites modelcontextprotocol.io/clients#feature-support-matrix as its evidence. That page and that anchor no longer resolve; the URL now redirects to the getting-started introduction. The evidence base for three deprecations is a dead link.
We built mcp-2026-migrate to close that for ourselves and it is easier to give away than to keep. It is a skill for Claude Code and Codex that reads your repository, identifies which of the twenty breaking changes apply to your code, and applies the mechanical ones, plus a standalone probe that determines a server's protocol era from the outside, which works on servers whose source you do not have. Free, open source, and built by Maximem, which is the same reason to read the scorecard above with the disclosure in mind.
Frequently asked questions
What is MRTR in MCP? Multi Round-Trip Requests, introduced in the 2026-07-28 MCP specification, is the pattern that replaced server-initiated requests. The server returns resultType: "input_required" with the inputs it needs and an opaque requestState blob, and the client re-sends the original request with the answers attached. It works only on tools/call, prompts/get and resources/read.
What does resultType: "input_required" mean? As of the 2026-07-28 MCP specification, every result carries a resultType, and "input_required" means the operation is incomplete because the server needs something from the client. An absent resultType must be treated as "complete" for backward compatibility with older servers, and an unrecognised value must be treated as invalid.
Can I keep using MCP 1.x for now? Yes. A server on 2025-11-25 keeps working with clients on 2025-11-25, and the SDK v1 lines are still published. What you cannot do is mix eras, because a modern client cannot talk to a legacy server and a legacy client cannot talk to a modern one.
Can a server support both the old and new MCP spec at the same time? Yes, and this is the recommended posture if you have real users. The 2026-07-28 specification calls it dual-era. The Python SDK v2 does it by default: the client's first request decides that connection's era, once, and a later claim from the other era is refused. So in many cases an SDK upgrade gets you there without application changes.
Why am I getting "unsupported protocol version" after upgrading? Error -32022 means the two ends do not share a supported revision. Check error.data.supported to see which side is behind. The fix is to make one side dual-era rather than to force both to the same revision at the same moment.
Do I need to update my MCP server? Not today, if nothing on the other end of your wire has moved. The 2026-07-28 revision does not break a running deployment by itself. You need to update before your clients do, and Anthropic has not yet shipped support across Claude, so most people have some runway.
What replaced Mcp-Session-Id? Nothing at the protocol level. The 2026-07-28 specification removed sessions entirely. State that must span requests is now an explicit, opaque, server-minted handle returned in a tool result and passed back as an ordinary tool argument, which the server re-authorizes on every call. One nuance: the tasks extension asks clients to send Mcp-Name: <taskId> so intermediaries can route back to the instance holding a task's state, so instance affinity has not disappeared, it has become explicit and scoped to a unit of work.
Was initialize removed from MCP? Yes. As of 2026-07-28 the initialize request and the notifications/initialized notification are both removed and nothing replaced them. Protocol version and client capabilities travel in _meta on every request, and server/discover covers the discovery case.
When will the Java SDK support 2026-07-28? It does not yet. Java is a Tier 2 SDK, which carries a six-month obligation from the specification release, so support is due around January 2027. Note that a Java 2.0.0 does exist, published 11 June 2026, but it predates this revision and contains no 2026-07-28 support.
When will Claude clients support the new MCP spec? Anthropic has said support is rolling out across Claude products and has not announced general availability or per-product dates. Check the Claude release notes rather than relying on this sentence, which ages badly by design.
Is sampling still supported in MCP? Sampling is deprecated as of 2026-07-28, not removed. It continues to work for at least twelve months, with the earliest removal being the first revision released on or after 28 July 2027.
What do I use instead of sampling now that it is deprecated? The specification's prescribed migration is to integrate directly with an LLM provider API. Budget for the consequence, which is that model credentials, billing and rate-limit handling move from the client to your server.
How do I handle workflows that take days now that MCP is stateless? Use the io.modelcontextprotocol/tasks extension. The server returns a task handle, the client polls tasks/get honouring pollIntervalMs, and sends input through tasks/update. Persist task ids durably, because tasks/list was removed and a lost id means an unreachable task.
What are the Mcp-Method and Mcp-Name headers for? They let load balancers, gateways and rate limiters route and meter on the operation without parsing the JSON-RPC body. Mcp-Method is required on all requests as of 2026-07-28; Mcp-Name is required only on tools/call, resources/read and prompts/get.
How long until HTTP+SSE stops working? Eligibility for removal opened around 18 August 2026, three months after SEP-2596 reached Final. The release blog's "year-long offramp" does not match the registry or the SEP. Removal is still a maintainer decision at release preparation and requires a new revision to ship, so nothing breaks on a date, but you no longer have a guaranteed window.
Is 2026-07-28 the same thing as MCP 2.0? There is no MCP 2.0. The string does not appear anywhere in the official documentation and the release announcement never uses it. It is community shorthand, and it exists because the SDKs went to 2.0.0 and 3.0.0 while the specification stayed on date-based revisions, which is a real inconsistency that people on Hacker News have been asking about for over a year.
The line the protocol drew
The thing worth taking away from 2026-07-28 is not any individual change. It is where the boundary landed.
This revision decided that problems living between a load balancer and a server process belong to the protocol, and that problems living between a model and a tool description belong to the layer above it. Sessions, routing, caching, discovery and transport are the protocol's job now, and they are handled well. Token economics, prompt injection, tool-space collisions and authorization modelling are somebody else's, explicitly and by design.
That is a coherent position and quite possibly the right one, because protocols that try to solve everything tend to solve nothing. The layer above is where extensions live, and extensions are exactly the right shape for the work that remains.
But nine clients appear in the official extension support matrix today, and exactly one of them supports anything beyond MCP Apps. So the expensive parts of running an MCP server and the risky parts of running an MCP server are now, formally, somebody else's problem, and that somebody has not shown up yet.
Worth knowing who you are waiting for.
Last updated 29 July 2026. Registry figures measured against registry.modelcontextprotocol.io on the same date. If something here is wrong, tell me and I will fix it in place rather than publishing a correction nobody reads.
Top comments (0)