Research
What the 2026-07-28 MCP revision changes for security
MCP shipped its largest revision on 28 July 2026. Coverage has focused on ergonomics — fewer round trips, stateless transport, simpler clients. This is the same revision read from a different seat: someone who has to inspect servers rather than write them. We grade all 27,164 in the official registry.
The short version
| Change | Direction | Why it matters |
|---|---|---|
Sessions + Mcp-Session-Id removed |
🟢 better | No session left to steal. Kills fixation, prediction, leakage. |
initialize handshake removed |
🟢 better | No half-authorized connection state. |
| State → handles in tool arguments | 🔴 worse | Moves a bearer credential into reach of prompt injection. |
x-mcp-header (headers from tool params) |
🔴 worse | Model-controlled input at the HTTP layer. |
| Schemas → full JSON Schema 2020-12 | 🟡 harder to check | $ref / oneOf can hide a credential parameter. |
OAuth: iss validation, issuer-bound creds |
🟢 better | Closes a real mix-up attack. |
| Sampling, Roots, Logging deprecated | 🟢 better | Removes a confused-deputy design outright. |
SSE resumability, ping, Last-Event-ID gone |
🟢 better | Less state, less replay surface. |
Two of these are our reading, not the spec’s framing — the two marked 🔴. They’re argued below.
Maintaining a server? The practical companion to this piece is Migrating an MCP server to 2026-07-28 — a checklist, and the one decision that matters (don’t migrate; go dual-era, or you break every client that hasn’t moved).
What actually changed on the wire
BEFORE (2025-11-25 and earlier)
client → initialize ─┐
client → notifications/initialized │ session established
server → Mcp-Session-Id: abc123 ─┘
client → tools/call (+ session header)
state lives in the transport
AFTER (2026-07-28)
client → tools/call
_meta: { protocolVersion, clientCapabilities, clientInfo }
every request stands alone
state lives in tool arguments
- No handshake. Every request carries its own version + capabilities in
_meta. - Version mismatch →
UnsupportedProtocolVersionError, listing what the server supports. - New mandatory RPC:
server/discover— versions, capabilities and identity in one call.
🔴 State didn’t disappear. It moved somewhere worse
Servers needing cross-call state now use server-minted handles passed as ordinary tool arguments.
Read that with an attacker’s hat on:
- A tool argument is not a private channel.
- It sits in the model’s context, next to whatever untrusted text the model just read — a web page, an email, another tool’s output.
- Anything that can influence the model can influence a tool argument.
So a capability that used to live in a transport header, invisible to the model, now lives in the one place prompt injection is best at reaching. A handle that grants access is a bearer token wearing a different hat, and this revision hands it to the model to carry.
Not an argument against the change — sessions had their own problems, and explicit handles are at least visible. But “visible to auditors” and “visible to the model” are the same property here, and only one of those is good.
🔴 The change we’d most like people to notice
Filed under minor changes: Streamable HTTP now supports custom HTTP headers derived from tool parameters, via x-mcp-header.
That’s model-controlled input reaching the HTTP layer. Well-trodden category:
- header injection
- cache poisoning via unkeyed headers
- authorization confusion, when a header the server trusts can be set by a caller who shouldn’t be
The spec isn’t wrong to allow it — there are legitimate uses and implementations can constrain it. But it’s the change most likely to produce a real incident, and it arrived with the least fanfare.
If you accept
x-mcp-header, answer this before shipping: which headers can a tool argument set, and what happens if a malicious web page picks their values?
🟡 Expressive schemas are harder to inspect — including by us
inputSchema and outputSchema now accept any JSON Schema 2020-12 keywords, including $ref and composition.
Good for authors. Bad for anyone reading them:
- Our credential-hygiene check catches servers asking the model to supply an API key — a mistake we still find regularly.
- It reads schemas fairly flatly.
- A credential nested in a
oneOf, or reached via$ref, is a parameter we can currently miss.
Stated plainly because it’s true today, and the same limitation applies to every other tool reading these schemas. Expressive schemas need resolving schemas, not pattern-matching ones. Until that lands, a clean credential-hygiene result on a composition-heavy server is weaker evidence than it looks.
🟢 Authorization got meaningfully stronger
The quiet win. Four changes land together:
issvalidation (RFC 9207) — clients must validate the issuer before redeeming a code. Closes a mix-up attack where a code from one authorization server is redeemed at another.- Credentials bound to their issuer — key them by issuer, never reuse across servers, re-register when the authorization server changes.
- Dynamic Client Registration deprecated in favour of Client ID Metadata Documents.
application_typerequired at registration, avoiding OpenID Connect redirect-URI conflicts.
None exciting. All the kind of thing that turns a plausible-looking OAuth integration into a correct one. Running a remote server behind OAuth? Read this part of the changelog twice.
🟢 Security by deletion
The best news in a spec is often a removal:
- Sampling deprecated — it let a server borrow the client’s model. A confused deputy by construction: third party using your model, your context, your bill. Migration is for servers to call an LLM provider directly, with their own credentials, where that trust belonged.
- Roots and Logging deprecated.
- Gone outright:
ping,logging/setLevel, SSE stream resumability,Last-Event-IDreplay.
Each was a little state or a little server-initiated control. Less of both is better.
Nothing breaks tomorrow: deprecated features stay for at least twelve months under the new lifecycle policy.
What this meant for us
Our probe advertised 2025-06-18 until recently — two revisions behind what live servers were answering with.
- No check ever graded a server on its protocol version, so no grade was affected.
- But a server strictly refusing our revision would have read as unreachable and been capped at insufficient through no fault of its own.
- Fixed by making the handshake version-agnostic rather than picking a newer constant: the probe reads the revision a server reports when it refuses one, and retries with that.
This revision — and the next — is now a non-event for the scanner rather than an incident.
The question we can answer, and will
How much of the ecosystem actually moved?
Not enthusiasm — how many servers, of 27,164 in the registry, answer a live connection with 2026-07-28.
Nothing declares this. No manifest states it; the value only exists on a live connection. Probing is the only way to know, which is what makes the question hard for anyone else to answer.
Here is the answer, from live connections, updated whenever this page is built:
| MCP revision | Servers | Share |
|---|---|---|
2026-07-28 (current) | 125 | 1.4% |
2025-11-25 | 5,347 | 57.8% |
2025-11-05 | 5 | 0.1% |
2025-06-18 | 1,247 | 13.5% |
2025-03-26 | 1,716 | 18.5% |
2024-11-05 | 812 | 8.8% |
Of the 9,252 servers whose revision we have observed, 125 answer with 2026-07-28 — 1.4%.
Two things worth saying about that table.
Adoption of the new revision is, so far, approximately nobody. Three weeks after the largest revision in MCP’s history, essentially no reachable server speaks it. That’s not a criticism — see the migration guide for why moving early actively breaks your users — but it is the reality behind the announcement.
The more interesting number is the old tail. A large share of live, reachable servers answer with revisions from a year or more ago. Those are running implementations that predate several rounds of authorization hardening. If you are picking servers to depend on, “which revision does it actually speak” is a question worth asking, and until now nobody was asking it.
The denominator is honest: only remote servers we can reach report a revision at all, so the base is servers we actually observed, never the registry total. Coverage grows as the rolling re-scan works through the corpus, and this table moves with it.
Figures here are read from the register at build time. Methodology is on the rubric page, including a changelog of corrections to our own checks.