# MCP 2026-07-28: 20 Breaking Changes and the Errors They Cause

> MCP 2026-07-28 removed sessions, the initialize handshake, and the ability for servers to initiate requests at all. It is wire-incompatible in both directions, so nothing breaks until a client upgrades underneath you. This is the full diff, every error you will hit with the fix for each, the HTTP+SSE deadline that two official sources disagree about, and a scorecard of what the release left alone.

_Maximem Team · July 29, 2026_

The [Model Context Protocol](https://www.maximem.ai/glossary/mcp) 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](https://claude.com/blog/bringing-mcp-2026-07-28-to-claude) 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](https://blog.modelcontextprotocol.io/posts/2026-07-28/), 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](https://workos.com/blog/mcp-2026-spec-agent-authentication). 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.

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p></p></th><th colspan="1" rowspan="1"><p><code>2025-11-25</code></p></th><th colspan="1" rowspan="1"><p><code>2026-07-28</code></p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Handshake</strong></p></td><td colspan="1" rowspan="1"><p><code>initialize</code> plus <code>notifications/initialized</code></p></td><td colspan="1" rowspan="1"><p>None. Per-request <code>_meta</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Sessions</strong></p></td><td colspan="1" rowspan="1"><p><code>Mcp-Session-Id</code> header, 404 to reinitialise, DELETE to end</p></td><td colspan="1" rowspan="1"><p>Removed. Explicit application-level handles</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Capabilities</strong></p></td><td colspan="1" rowspan="1"><p>Negotiated once per connection</p></td><td colspan="1" rowspan="1"><p>Declared on every request</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Discovery</strong></p></td><td colspan="1" rowspan="1"><p><code>InitializeResult</code></p></td><td colspan="1" rowspan="1"><p><code>server/discover</code>, server MUST implement, client MAY call</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Transport</strong></p></td><td colspan="1" rowspan="1"><p>POST plus GET SSE, resumable via <code>Last-Event-ID</code></p></td><td colspan="1" rowspan="1"><p>POST only. A modern-only server SHOULD return <code>405</code> on GET and DELETE. No resumability</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Routing</strong></p></td><td colspan="1" rowspan="1"><p>Operation in the JSON body. <code>MCP-Protocol-Version</code> header required since <code>2025-06-18</code></p></td><td colspan="1" rowspan="1"><p>Two new required headers, <code>Mcp-Method</code> on every request and <code>Mcp-Name</code> on three of them</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Server to client requests</strong></p></td><td colspan="1" rowspan="1"><p><code>sampling/createMessage</code>, <code>elicitation/create</code>, <code>roots/list</code> on a live stream</p></td><td colspan="1" rowspan="1"><p>MRTR: <code>resultType: "input_required"</code> plus <code>requestState</code>, retried by the client with a new JSON-RPC id</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Subscriptions</strong></p></td><td colspan="1" rowspan="1"><p><code>resources/subscribe</code> plus the GET SSE stream</p></td><td colspan="1" rowspan="1"><p><code>subscriptions/listen</code> with an explicit notification filter</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Caching</strong></p></td><td colspan="1" rowspan="1"><p>None</p></td><td colspan="1" rowspan="1"><p><code>ttlMs</code> and <code>cacheScope</code> on six result types (the release blog says four and the changelog says five; the schema and the caching page say six)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Long running work</strong></p></td><td colspan="1" rowspan="1"><p>Tasks, experimental, in core. Blocking <code>tasks/result</code>, plus <code>tasks/list</code></p></td><td colspan="1" rowspan="1"><p><code>io.modelcontextprotocol/tasks</code> extension. Polling <code>tasks/get</code>, plus <code>tasks/update</code>. No <code>tasks/list</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Client registration</strong></p></td><td colspan="1" rowspan="1"><p>DCR (MAY) plus CIMD (SHOULD)</p></td><td colspan="1" rowspan="1"><p>DCR deprecated. CIMD is the path. <code>application_type</code> now MUST</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Issuer validation</strong></p></td><td colspan="1" rowspan="1"><p>Not required</p></td><td colspan="1" rowspan="1"><p>RFC 9207 <code>iss</code> validation, MUST, including on error responses</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Logging</strong></p></td><td colspan="1" rowspan="1"><p><code>logging</code> capability, <code>logging/setLevel</code>, <code>notifications/message</code></p></td><td colspan="1" rowspan="1"><p>Deprecated. Per-request log level in <code>_meta</code>, stderr, OpenTelemetry</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Liveness</strong></p></td><td colspan="1" rowspan="1"><p><code>ping</code></p></td><td colspan="1" rowspan="1"><p>Removed</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Resource not found</strong></p></td><td colspan="1" rowspan="1"><p><code>-32002</code></p></td><td colspan="1" rowspan="1"><p><code>-32602</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>MCP error codes</strong></p></td><td colspan="1" rowspan="1"><p><code>-32042</code> (URL elicitation required)</p></td><td colspan="1" rowspan="1"><p>Three new codes, <code>-32020</code> to <code>-32022</code>, inside a reserved <code>-32020</code> to <code>-32099</code> block. <code>-32042</code> retired</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Tool schemas</strong></p></td><td colspan="1" rowspan="1"><p>JSON Schema 2020-12 as default dialect</p></td><td colspan="1" rowspan="1"><p>Full 2020-12 keyword set. Network <code>$ref</code> forbidden by default. DoS bounds required</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Tool lists</strong></p></td><td colspan="1" rowspan="1"><p>May vary per connection</p></td><td colspan="1" rowspan="1"><p>MUST NOT vary per connection. SHOULD be deterministically ordered</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Extensibility</strong></p></td><td colspan="1" rowspan="1"><p><code>experimental</code> capability bag</p></td><td colspan="1" rowspan="1"><p>Formal extensions framework, reverse-DNS identifiers, opt-in, independently versioned</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Deprecation</strong></p></td><td colspan="1" rowspan="1"><p>Ad hoc</p></td><td colspan="1" rowspan="1"><p>Formal policy. Twelve month floor, published registry, SDK obligations, ninety day security exception</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Result envelope</strong></p></td><td colspan="1" rowspan="1"><p>Bare result object</p></td><td colspan="1" rowspan="1"><p>Required <code>resultType</code> on every result</p></td></tr></tbody></table>

* * *

## 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.

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Client</p></th><th colspan="1" rowspan="1"><p>Server</p></th><th colspan="1" rowspan="1"><p>Outcome</p></th></tr><tr><td colspan="1" rowspan="1"><p>Modern</p></td><td colspan="1" rowspan="1"><p>Modern</p></td><td colspan="1" rowspan="1"><p>Works</p></td></tr><tr><td colspan="1" rowspan="1"><p>Modern</p></td><td colspan="1" rowspan="1"><p>Legacy</p></td><td colspan="1" rowspan="1"><p><strong>Fails</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Dual-era</p></td><td colspan="1" rowspan="1"><p>Modern</p></td><td colspan="1" rowspan="1"><p>Works</p></td></tr><tr><td colspan="1" rowspan="1"><p>Dual-era</p></td><td colspan="1" rowspan="1"><p>Legacy</p></td><td colspan="1" rowspan="1"><p>Works</p></td></tr><tr><td colspan="1" rowspan="1"><p>Legacy</p></td><td colspan="1" rowspan="1"><p>Modern</p></td><td colspan="1" rowspan="1"><p><strong>Fails.</strong> Legacy clients have no fall-forward mechanism</p></td></tr><tr><td colspan="1" rowspan="1"><p>Legacy</p></td><td colspan="1" rowspan="1"><p>Dual-era</p></td><td colspan="1" rowspan="1"><p>Works</p></td></tr><tr><td colspan="1" rowspan="1"><p>Legacy</p></td><td colspan="1" rowspan="1"><p>Legacy</p></td><td colspan="1" rowspan="1"><p>Works per the legacy revision</p></td></tr></tbody></table>

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](https://pypi.org/project/mcp/) 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.

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Change</p></th><th colspan="1" rowspan="1"><p>What it breaks</p></th><th colspan="1" rowspan="1"><p>Severity</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>initialize</code> and <code>notifications/initialized</code> removed</p></td><td colspan="1" rowspan="1"><p>Every client and server implementing the handshake</p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Mcp-Session-Id</code> and sessions removed</p></td><td colspan="1" rowspan="1"><p>Any server holding per-session state, any client resuming a session</p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p>Per-request <code>_meta</code> with <code>protocolVersion</code> and <code>clientCapabilities</code> now required</p></td><td colspan="1" rowspan="1"><p>Any request without them returns <code>-32602</code> and HTTP <code>400</code></p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>MCP-Protocol-Version</code> header required and must match the body</p></td><td colspan="1" rowspan="1"><p>Mismatch returns <code>400</code> with <code>-32020</code></p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Mcp-Method</code> required on all requests, <code>Mcp-Name</code> on three of them</p></td><td colspan="1" rowspan="1"><p>Any hand-rolled HTTP client</p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p>Server-initiated requests forbidden, MRTR replaces them</p></td><td colspan="1" rowspan="1"><p>Every server using sampling, elicitation or roots</p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p>The GET SSE stream is gone and resumability is removed. A modern-only server SHOULD answer GET and DELETE with <code>405</code></p></td><td colspan="1" rowspan="1"><p>Any client relying on the GET stream or on <code>Last-Event-ID</code></p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/subscribe</code> and <code>resources/unsubscribe</code> removed</p></td><td colspan="1" rowspan="1"><p>Any resource-subscription client</p></td><td colspan="1" rowspan="1"><p>Total</p></td></tr><tr><td colspan="1" rowspan="1"><p>Tasks redesigned and moved to an extension</p></td><td colspan="1" rowspan="1"><p>Everyone on the experimental <code>2025-11-25</code> Tasks API</p></td><td colspan="1" rowspan="1"><p>Total, for tasks users</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resultType</code> required on every result</p></td><td colspan="1" rowspan="1"><p>Clients not defaulting an absent value to <code>"complete"</code>, or not handling <code>"input_required"</code></p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p>Resource not found moves from <code>-32002</code> to <code>-32602</code>, and <code>-32042</code> (URL elicitation required) must no longer be emitted</p></td><td colspan="1" rowspan="1"><p>Any client matching on either literal code</p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>notifications/cancelled</code> is stdio-only. Streamable HTTP defines no client-to-server notifications; closing the SSE response stream is the cancellation signal</p></td><td colspan="1" rowspan="1"><p>Any HTTP client that sends it, and any server that only cancels on receiving it</p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ping</code>, <code>logging/setLevel</code> and <code>notifications/roots/list_changed</code> removed</p></td><td colspan="1" rowspan="1"><p>Health-check loops and log-level control</p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>notifications/elicitation/complete</code> and <code>elicitationId</code> removed</p></td><td colspan="1" rowspan="1"><p>URL-mode elicitation correlation</p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p>Tool lists MUST NOT vary per connection</p></td><td colspan="1" rowspan="1"><p>Any server personalising <code>tools/list</code> per session rather than per token</p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p>Any JSON Schema 2020-12 keyword is now allowed, but network <code>$ref</code> resolution MUST be off by default and composition keywords need resource bounds</p></td><td colspan="1" rowspan="1"><p>Servers relying on remote <code>$ref</code> resolution</p></td><td colspan="1" rowspan="1"><p>Medium</p></td></tr><tr><td colspan="1" rowspan="1"><p>An invalid <code>x-mcp-header</code> forces the client to drop the tool from <code>tools/list</code></p></td><td colspan="1" rowspan="1"><p>Silent tool disappearance</p></td><td colspan="1" rowspan="1"><p>Medium</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ttlMs</code> and <code>cacheScope</code> are non-optional on six result types</p></td><td colspan="1" rowspan="1"><p>Servers omitting them are non-conformant</p></td><td colspan="1" rowspan="1"><p>Medium</p></td></tr><tr><td colspan="1" rowspan="1"><p>DCR deprecated and <code>application_type</code> now required</p></td><td colspan="1" rowspan="1"><p>OIDC-strict authorization servers rejecting native redirects</p></td><td colspan="1" rowspan="1"><p>Medium</p></td></tr><tr><td colspan="1" rowspan="1"><p>RFC 9207 <code>iss</code> validation now MUST on the client side</p></td><td colspan="1" rowspan="1"><p>Clients that skip it are non-conformant</p></td><td colspan="1" rowspan="1"><p>Medium</p></td></tr></tbody></table>

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](https://glama.ai), who runs the largest open source MCP server index, wrote on [Hacker News](https://news.ycombinator.com/item?id=49088058) 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`:

```json
{
  "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:

```http
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](https://agentgateway.dev/blog/2026-07-21-stateless-mcp-still-needs-mcp-native-dataplane/) 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:

```json
{
  "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:

```json
"_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.

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Code</p></th><th colspan="1" rowspan="1"><p>Shipped in</p></th><th colspan="1" rowspan="1"><p>Status in <code>2026-07-28</code></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>-32002</code>, resource not found</p></td><td colspan="1" rowspan="1"><p><code>2025-11-25</code> and earlier</p></td><td colspan="1" rowspan="1"><p>Retired. Use <code>-32602</code>. Clients SHOULD keep accepting it from older servers</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>-32042</code>, URL elicitation required</p></td><td colspan="1" rowspan="1"><p><code>2025-11-25</code> only</p></td><td colspan="1" rowspan="1"><p>Retired. Implementations of this revision MUST NOT emit it</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>-32020</code>, header mismatch</p></td><td colspan="1" rowspan="1"><p>new</p></td><td colspan="1" rowspan="1"><p>Reserved block</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>-32021</code>, missing required client capability</p></td><td colspan="1" rowspan="1"><p>new</p></td><td colspan="1" rowspan="1"><p>Reserved block</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>-32022</code>, unsupported protocol version</p></td><td colspan="1" rowspan="1"><p>new</p></td><td colspan="1" rowspan="1"><p>Reserved block</p></td></tr></tbody></table>

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:

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><code>2025-11-25</code>, experimental</p></th><th colspan="1" rowspan="1"><p><code>io.modelcontextprotocol/tasks</code> extension</p></th></tr><tr><td colspan="1" rowspan="1"><p>Client opts in per request with a <code>task: {ttl}</code> param</p></td><td colspan="1" rowspan="1"><p>The server decides, unilaterally, per request</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>tasks/get</code> plus a blocking <code>tasks/result</code></p></td><td colspan="1" rowspan="1"><p><code>tasks/result</code> removed; <code>tasks/get</code> absorbs its job and polls, honouring <code>pollIntervalMs</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>No client-to-server input method</p></td><td colspan="1" rowspan="1"><p><code>tasks/update</code> carrying <code>inputResponses</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>tasks/list</code></p></td><td colspan="1" rowspan="1"><p>Removed. There is no way to enumerate tasks</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>notifications/tasks/status</code></p></td><td colspan="1" rowspan="1"><p><code>notifications/tasks</code>, opt in via <code>subscriptions/listen</code> with a <code>taskIds</code> filter</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ttl</code>, <code>pollInterval</code></p></td><td colspan="1" rowspan="1"><p><code>ttlMs</code>, <code>pollIntervalMs</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>tasks/cancel</code> returns the task</p></td><td colspan="1" rowspan="1"><p><code>tasks/cancel</code> returns a bare acknowledgement; cancellation is cooperative and the server need only acknowledge</p></td></tr><tr><td colspan="1" rowspan="1"><p>Both parties can be requestors</p></td><td colspan="1" rowspan="1"><p><code>tools/call</code> only</p></td></tr></tbody></table>

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](https://www.maximem.ai/glossary/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](https://github.com/kerlenton/mcpsnoop/blob/main/docs/2026-07-28-mrtr-breaks-latency.md) 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

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>SDK</p></th><th colspan="1" rowspan="1"><p>Tier</p></th><th colspan="1" rowspan="1"><p>Version with <code>2026-07-28</code></p></th></tr><tr><td colspan="1" rowspan="1"><p>TypeScript</p></td><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p><code>@modelcontextprotocol/server@2.0.0</code> and <code>@modelcontextprotocol/client@2.0.0</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Python</p></td><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p><code>mcp==2.0.0</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Go</p></td><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p><code>go-sdk v1.7.0</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>C#</p></td><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p><code>ModelContextProtocol 2.0.0</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Rust</p></td><td colspan="1" rowspan="1"><p>2</p></td><td colspan="1" rowspan="1"><p><code>rmcp 3.0.0</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Java</strong></p></td><td colspan="1" rowspan="1"><p><strong>2</strong></p></td><td colspan="1" rowspan="1"><p><strong>No support yet</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Swift, Ruby, PHP, Kotlin</p></td><td colspan="1" rowspan="1"><p>3</p></td><td colspan="1" rowspan="1"><p>No support, no committed timeline</p></td></tr></tbody></table>

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.

![MCP client and server engagement before the 2026-07-28 revision: a stateful, connection-scoped pattern where a session is created at the handshake and pins every subsequent request to one server process](https://www.maximem.ai/img/mcp-before-2025-11-25.svg)

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](https://www.maximem.ai/glossary/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.

![MCP client and server engagement after the 2026-07-28 revision: a stateless, self-describing pattern where every request carries its own version and capabilities and any server instance can answer it](https://www.maximem.ai/img/mcp-after-2026-07-28.svg)

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"](https://blog.modelcontextprotocol.io/posts/2025-12-19-mcp-transport-future/), 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"](https://blog.modelcontextprotocol.io/posts/2025-09-26-mcp-next-version-update/) from September 2025, had already named horizontal scaling, long-running tasks, discovery, extensions and SDK [compliance](https://www.maximem.ai/glossary/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](https://thenewstack.io/mcp-release-candidate-rewrite/) 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](https://www.apideck.com/blog/mcp-server-eating-context-window-cli-alternative) 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](https://www.anthropic.com/engineering/code-execution-with-mcp), roughly a 98.7% reduction, by not using the protocol the way it was designed. **[Prompt injection](https://www.maximem.ai/glossary/prompt-injection)**, where [Invariant Labs demonstrated data exfiltration through the GitHub MCP server](https://invariantlabs.ai/blog/mcp-github-vulnerability) 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](https://www.pomerium.com/blog/why-the-managed-context-protocol-mcp-spec-still-leaves-gaping-security-holes) 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](https://www.microsoft.com/en-us/research/blog/tool-space-interference-in-the-mcp-era-designing-for-agent-compatibility-at-scale/) `search` [appearing across 32 distinct servers](https://www.microsoft.com/en-us/research/blog/tool-space-interference-in-the-mcp-era-designing-for-agent-compatibility-at-scale/); the OAuth and Dynamic Client Registration burden that [Descope catalogued as an enterprise blocker](https://www.descope.com/blog/post/enterprise-mcp); the [missing distributed-systems contract around idempotency, deadlines and tracing](https://julsimon.medium.com/why-mcps-disregard-for-40-years-of-rpc-best-practices-will-burn-enterprises-8ef85ce5bc9b); 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](https://www.speakeasy.com), [Scalekit](https://www.scalekit.com), Descope, [WorkOS](https://workos.com), Pomerium, [Docker](https://www.docker.com) 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](https://blog.trailofbits.com), 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.

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Critique</p></th><th colspan="1" rowspan="1"><p>What <code>2026-07-28</code> does</p></th><th colspan="1" rowspan="1"><p>Verdict</p></th></tr><tr><td colspan="1" rowspan="1"><p>Statefulness and session affinity</p></td><td colspan="1" rowspan="1"><p>Sessions, <code>initialize</code> and the GET stream deleted. State becomes an explicit handle</p></td><td colspan="1" rowspan="1"><p><strong>Solved</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Tool-definition context tax</p></td><td colspan="1" rowspan="1"><p>Deterministic tool ordering, cacheable <code>tools/list</code>. Neither reduces what the model sees</p></td><td colspan="1" rowspan="1"><p><strong>Untouched</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Prompt injection and the lethal trifecta</p></td><td colspan="1" rowspan="1"><p>Nothing</p></td><td colspan="1" rowspan="1"><p><strong>Untouched</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>No authorization model</p></td><td colspan="1" rowspan="1"><p>Nothing. Scope step-up, which is often credited to this release, shipped in <code>2025-11-25</code>. Modelling went to the <code>ext-auth</code> extensions repo</p></td><td colspan="1" rowspan="1"><p><strong>Untouched</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Tool poisoning, rug pulls, line jumping</p></td><td colspan="1" rowspan="1"><p>Nothing. Descriptions remain unsigned and mutable</p></td><td colspan="1" rowspan="1"><p><strong>Untouched</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Tool-space interference, no namespacing</p></td><td colspan="1" rowspan="1"><p>Name-format rules, and clients "SHOULD implement a disambiguation strategy"</p></td><td colspan="1" rowspan="1"><p>Weak partial</p></td></tr><tr><td colspan="1" rowspan="1"><p>Auth complexity and the DCR burden</p></td><td colspan="1" rowspan="1"><p><code>iss</code> validation, credential-issuer binding and <code>application_type</code> are real. But CIMD was already SHOULD and DCR already MAY in <code>2025-11-25</code>, so the registration delta is largely a Deprecated label</p></td><td colspan="1" rowspan="1"><p>Partial</p></td></tr><tr><td colspan="1" rowspan="1"><p>Gateway, WAF, metering and caching hostility</p></td><td colspan="1" rowspan="1"><p><code>Mcp-Method</code> and <code>Mcp-Name</code> headers, <code>ttlMs</code> and <code>cacheScope</code></p></td><td colspan="1" rowspan="1"><p><strong>Solved</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Missing distributed-systems contract</p></td><td colspan="1" rowspan="1"><p>Tracing yes, schema strictness yes, versioning discipline yes. Idempotency, deadlines and per-tool versioning no</p></td><td colspan="1" rowspan="1"><p>Partial</p></td></tr><tr><td colspan="1" rowspan="1"><p>No long-running or async model</p></td><td colspan="1" rowspan="1"><p>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</p></td><td colspan="1" rowspan="1"><p>Partial</p></td></tr><tr><td colspan="1" rowspan="1"><p>No multi-tenancy primitive</p></td><td colspan="1" rowspan="1"><p>Nothing, beyond <code>cacheScope: "private"</code> being authorization-context aware</p></td><td colspan="1" rowspan="1"><p><strong>Untouched</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>Transport churn</p></td><td colspan="1" rowspan="1"><p>One transport story now, but this release <strong>is</strong> churn: the third breaking transport shape in sixteen months. The improvement is that churn is finally governed by a deprecation policy</p></td><td colspan="1" rowspan="1"><p>Neutral</p></td></tr><tr><td colspan="1" rowspan="1"><p>Configuration fragility</p></td><td colspan="1" rowspan="1"><p>Removes session config as a failure class, adds header validation as a new one</p></td><td colspan="1" rowspan="1"><p>Neutral</p></td></tr><tr><td colspan="1" rowspan="1"><p>Latency at the call level</p></td><td colspan="1" rowspan="1"><p>No handshake, so the first-call penalty disappears. Cacheable lists cut repeat cost</p></td><td colspan="1" rowspan="1"><p>Improved</p></td></tr><tr><td colspan="1" rowspan="1"><p>Registry and discovery</p></td><td colspan="1" rowspan="1"><p><code>server/discover</code> per server. The registry itself still has no protocol-version field</p></td><td colspan="1" rowspan="1"><p>Partial</p></td></tr><tr><td colspan="1" rowspan="1"><p>Observability gap</p></td><td colspan="1" rowspan="1"><p>Trace context standardised, then MRTR breaks id-keyed measurement</p></td><td colspan="1" rowspan="1"><p>Mixed</p></td></tr><tr><td colspan="1" rowspan="1"><p>Risk stratification and cost opacity</p></td><td colspan="1" rowspan="1"><p>Nothing in core</p></td><td colspan="1" rowspan="1"><p><strong>Untouched</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p>SDK quality and compliance opacity</p></td><td colspan="1" rowspan="1"><p>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</p></td><td colspan="1" rowspan="1"><p>Partial</p></td></tr></tbody></table>

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](https://blog.trailofbits.com/2025/04/21/jumping-the-line-how-mcp-servers-can-attack-you-before-you-ever-use-them/) 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](https://www.maximem.ai/glossary/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](https://modelcontextprotocol.io/extensions/client-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](https://www.securityweek.com/new-enterprise-ready-mcp-specification-brings-new-security-challenges/): "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](https://equixly.com/blog/2025/03/29/mcp-server-new-security-nightmare/), 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](https://www.theregister.com/devops/2026/07/23/model-context-protocol-prepares-to-break-with-its-stateful-past/5276722), 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](https://www.maximem.ai/glossary/api), 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](https://trilogyai.substack.com/p/mcp-grows-up-what-the-july-28-spec) 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](https://www.maximem.ai/glossary/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.

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Source</p></th><th colspan="1" rowspan="1"><p>Implied earliest removal</p></th></tr><tr><td colspan="1" rowspan="1"><p>The <a target="_blank" rel="noopener noreferrer nofollow" class="text-blue-400 underline hover:text-blue-300" href="https://modelcontextprotocol.io/specification/2026-07-28/deprecated">deprecated features registry</a>, "three months after SEP-2596 reaches Final", with PR #2596 merged under the <code>final</code> label on 18 May 2026</p></td><td colspan="1" rowspan="1"><p>around 18 August 2026</p></td></tr><tr><td colspan="1" rowspan="1"><p>The <a target="_blank" rel="noopener noreferrer nofollow" class="text-blue-400 underline hover:text-blue-300" href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">release blog</a>, "officially deprecated, with a year-long offramp"</p></td><td colspan="1" rowspan="1"><p>around July 2027</p></td></tr></tbody></table>

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](https://news.ycombinator.com/item?id=49088058) 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](#the-tool) 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.

1.  Can a request land on a **cold, empty instance** and succeed?
    
2.  Do you implement `server/discover`? It is a MUST for servers.
    
3.  Do you accept and validate `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name`, and does your proxy forward them?
    
4.  Do you ignore inbound `Mcp-Session-Id` and `Last-Event-ID`? (And if you serve **only** this revision, do you answer GET and DELETE with `405`? If you are dual-era, keep the GET stream for legacy clients.)
    
5.  Is `resultType` present on every result, and does your client handle `"input_required"`?
    
6.  Are `ttlMs` and `cacheScope` present on all six cacheable result types, and is `cacheScope` **correct** for authenticated data?
    
7.  Does `tools/list` avoid varying per connection, and return a deterministic order?
    
8.  Have you replaced every server-initiated request with MRTR, and is `requestState` integrity-protected?
    
9.  Have you moved off `-32002` and `-32042`, and if you built against a release candidate, off the pre-release `-32001`, `-32003` and `-32004` numbering?
    
10.  Does your `inputSchema` validate as JSON Schema 2020-12, with network `$ref` resolution disabled by default?
     
11.  Have you inventoried your use of roots, sampling, logging and DCR against the 2027-07-28 clock?
     
12.  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](https://claude.com/product/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._

---

Source: [https://www.maximem.ai/blog/mcp-2026-07-28-migration-errors](https://www.maximem.ai/blog/mcp-2026-07-28-migration-errors)
