MCP Backward Compatibility: What Still Talks to What
Updated October 4, 2026
MCP revision 2026-07-28 removed the initialize handshake, so a new client cannot talk to an old server. Here is the full compatibility matrix and the probe that settles it.

MCP backward compatibility now has one rule worth memorising: a client speaking revision 2026-07-28 cannot talk to a server stuck on 2025-11-25 or earlier, and a server speaking only 2026-07-28 cannot serve an older client. The revision removed the initialize handshake, so the two sides no longer share an opening move. Only a dual-era implementation bridges the gap, and the specification is explicit about which side carries that work: the client.
Nothing about this forces an upgrade. A legacy client and a legacy server keep working together, and older revisions stay published as Final. The thing that breaks is specifically a mixed pair, and it breaks on the first request.
What revision 2026-07-28 removed
The protocol version string is a date, and the date marks the last time backwards incompatible changes landed (MCP versioning guide). 2026-07-28 earned its date. The changelog for the revision lists nine major changes against 2025-11-25, and most of them are deletions (key changes, revision 2026-07-28).
The five that will break running code:
- The
initializeandnotifications/initializedhandshake is gone. Every request now carries its own protocol version and client capabilities in_meta, under the keysio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilities. - Protocol-level sessions and the
Mcp-Session-Idheader are gone from the Streamable HTTP transport.tools/list,resources/listandprompts/listno longer vary per connection. A server that needs state across calls mints an explicit handle and passes it as an ordinary tool argument. - The HTTP GET endpoint,
resources/subscribeandresources/unsubscribeare replaced by a singlesubscriptions/listenstream that a client opts into per notification type. ping,logging/setLevelandnotifications/roots/list_changedare gone. Log level moves toio.modelcontextprotocol/logLevelin_meta, per request, and a server must not emitnotifications/messagefor a request that did not ask for one.- SSE stream resumability is gone, along with the
Last-Event-IDheader and SSE event IDs. A broken response stream loses the in-flight request, and the client has to re-issue it under a new request ID.
There is a sixth change that is easy to miss because it is additive. Every result now carries a required resultType field, either "complete" or "input_required". A client reading a result from an older server has to treat a missing resultType as "complete", which is a small compatibility shim you have to write yourself if you are not using an SDK. The other value has a consequence of its own: "input_required" assumes a human is reachable, which is why it is the first thing to break under an always-on agent.
Which client and server combinations work
The specification publishes its own compatibility matrix, which is unusual and genuinely useful. Three eras are named: modern means 2026-07-28 and later, carrying version and capabilities as per-request metadata. Legacy means 2025-11-25 and earlier, establishing a session with initialize. Dual-era means an implementation that supports both.
| Client | Server | Outcome |
|---|---|---|
| Modern | Modern | Works. Mismatched versions come back as UnsupportedProtocolVersionError and the client retries with a supported one. |
| Modern | Legacy | Fails. The server may return an implementation-defined error, stay silent, or process an era-ambiguous method under legacy semantics. |
| Dual-era | Modern | Works. The client stays modern. |
| Dual-era | Legacy | Works. The client falls back to initialize, and possibly further to the deprecated HTTP+SSE transport. |
| Legacy | Modern | Fails. On HTTP the request lacks required headers and is rejected with 400 Bad Request. Legacy clients have no fall-forward mechanism. |
| Legacy | Dual-era | Works, under the negotiated legacy revision. |
| Legacy | Legacy | Works, under the legacy revision. |
Condensed from the versioning and compatibility page of revision 2026-07-28.
Read the two failing rows together and the asymmetry is the whole story. A modern client can at least recognise a modern error and retry, so adding dual-era support to a client fixes four combinations. A legacy client has nowhere to go, which is why the specification asks a modern-only server to name its supported versions in whatever error it returns to an initialize request. That error message may be the only diagnostic the old client can show a human.
How a client works out which era a server speaks
Servers must implement server/discover, an RPC that returns supported protocol versions, capabilities and identity in one request. Calling it is optional for the client, which can also fire any request inline and handle the error.
Detection is transport-specific, and the difference matters if you are writing the client:
- On stdio, probe with
server/discoverand fall back on any error that is not a recognised modern error. - On Streamable HTTP, send a modern request and inspect the body of a
400 Bad Requestbefore falling back.
In both cases a recognised modern JSON-RPC error identifies a modern server, so the client retries rather than falling back. Anything else identifies a legacy one.
One detail is worth lifting out because it affects your cache design. Era is a property of the server, not of a request. The specification says clients should cache the determination for the lifetime of the server process on stdio, or the origin on HTTP, and may persist it across restarts of the same configuration, re-probing if the cached assumption later fails.
A version mismatch between two modern parties looks like this:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}
The supported array is the useful part. It turns a failure into a retry instead of a support ticket.
Why "MCP compatible" stopped being a yes or no answer
Here is the part no migration guide will tell you, because it requires admitting something about directories, including this one.
Every directory that tracks MCP support records it as a boolean. This one does too: specs.mcpCompatible in src/lib/listings.ts is true or false, and the listing page renders an "MCP compatible" badge off it. Before July 2026 that boolean carried real information. After 2026-07-28 it does not, because true on both sides of a connection is now consistent with a hard failure. The modern-client-to-legacy-server row proves it.
The honest unit is the era, not the flag. A developer evaluating a tool needs to know whether it is modern, legacy or dual-era, and no vendor we track publishes that as a number on a product page. It lives in SDK release notes and changelogs, if it is written down at all.
We should say what that means for our own listings. The four tools in this directory marked MCP compatible carry these verification dates:
| Tool | Taxonomy | MCP compatible | Last verified |
|---|---|---|---|
| Claude Code | Fully Autonomous / Code / MCP-native | Yes | 2026-06-20 |
| LangChain | Supervised / Code / LangChain | Yes | 2026-06-15 |
| CrewAI | Supervised / Code / Custom | Yes | 2026-06-02 |
| LlamaIndex | Copilot / API / LlamaIndex | Yes | 2026-05-18 |
Every one of those dates falls before 2026-07-28. The flag is accurate as of the date beside it, and the date is older than the revision. Which era each of them speaks today is a question the probe above answers in one request, and a question we cannot answer from the vendor documentation alone. That is the useful version of the answer, and it is the reason to run server/discover against your own stack rather than trusting a badge, ours included. Our post on what an MCP server actually is made the narrower version of this argument a month ago: an MCP badge in a README proves nothing, so call the server.
The two listings marked false, Browserbase and OpenAI Computer Use, are unaffected. A tool with no MCP surface has no era.
What is deprecated, and the earliest it can disappear
Separately from the removals, revision 2026-07-28 adopted a feature lifecycle policy with three states, Active, Deprecated and Removed, and a minimum twelve-month window between the last two (feature lifecycle and deprecation policy). The window is counted from the release of the revision that first marks a feature Deprecated, not from the date the proposal reaches Final.
| Feature | Documented migration | Earliest removal |
|---|---|---|
| Roots | Pass directories or files via tool parameters, resource URIs or server configuration | First revision released on or after 2027-07-28 |
| Sampling | Integrate with LLM provider APIs directly | First revision released on or after 2027-07-28 |
| Logging | Log to stderr on stdio, or use OpenTelemetry | First revision released on or after 2027-07-28 |
| Dynamic Client Registration | Client ID Metadata Documents | First revision released on or after 2027-07-28 |
| HTTP+SSE transport | Streamable HTTP | Three months after SEP-2596 reaches Final |
From the deprecated features registry for revision 2026-07-28. Nothing has been removed under the policy yet.
The twelve-month floor has one exception, and it is narrow. It can be shortened only for a feature presenting an active security risk, meaning a vulnerability with a published advisory or documented in-the-wild exploitation for which no in-place mitigation exists, and even then the window must leave at least ninety days.
Two consequences for planning. Removal from the specification does not oblige an SDK to drop a feature, since that timeline belongs to the SDK's own revision-support policy, so your dependency may keep working past the date in that table. And Tier 1 SDKs must mark a deprecated API surface using the language's native mechanism in their next release, so @deprecated tags and runtime warnings are the signal to watch rather than the registry page.
If you are deciding how much of this to automate away, the deprecation of Sampling is the one with teeth. Sampling let a server ask the client for a model completion, which is a capability boundary question as much as a protocol one. Our breakdown of agent autonomy levels covers why that boundary is where oversight actually lives.
What to check before you upgrade
In order, and all five are cheap:
- Call
server/discoveragainst every server you depend on. One request returns the supported versions. This is the only step that produces a fact rather than an assumption. - Grep for
Mcp-Session-Id. Any gateway, load balancer or routing rule keyed on it needs rewriting, because the header is gone from the transport. - Grep for
initializein your own client code. If you send it unconditionally, you are a legacy client and a modern-only server will reject you with a400. - Check whether anything you run relies on Roots, Sampling or Logging. All three are Deprecated with a documented migration and no removal before 2027-07-28, so this is a planning item, not an emergency.
- Pin the revision you tested against in your own docs. The protocol version is a date and the date is the compatibility contract. "Supports MCP" is not a version.
If you only do one, do the first. Everything else in this post is a consequence of which era the thing on the other end of the connection speaks, and that is a single request away.
Start with the four MCP-compatible listings in this directory and the probe above: Claude Code, LangChain, CrewAI and LlamaIndex. Each listing page carries the pricing, the taxonomy placement and the verification date, and the verification date is the number this post is really about. If MCP as a protocol is still new to you, MCP versus function calling covers which of the two you actually need before any of this becomes your problem.


