agentsdir.

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.

agentsdir.ai10 min read
A single thin horizontal line with its middle section drawn in burnt orange, marking the revision boundary that splits MCP into two incompatible eras

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:

  1. The initialize and notifications/initialized handshake is gone. Every request now carries its own protocol version and client capabilities in _meta, under the keys io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities.
  2. Protocol-level sessions and the Mcp-Session-Id header are gone from the Streamable HTTP transport. tools/list, resources/list and prompts/list no longer vary per connection. A server that needs state across calls mints an explicit handle and passes it as an ordinary tool argument.
  3. The HTTP GET endpoint, resources/subscribe and resources/unsubscribe are replaced by a single subscriptions/listen stream that a client opts into per notification type.
  4. ping, logging/setLevel and notifications/roots/list_changed are gone. Log level moves to io.modelcontextprotocol/logLevel in _meta, per request, and a server must not emit notifications/message for a request that did not ask for one.
  5. SSE stream resumability is gone, along with the Last-Event-ID header 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.

ClientServerOutcome
ModernModernWorks. Mismatched versions come back as UnsupportedProtocolVersionError and the client retries with a supported one.
ModernLegacyFails. The server may return an implementation-defined error, stay silent, or process an era-ambiguous method under legacy semantics.
Dual-eraModernWorks. The client stays modern.
Dual-eraLegacyWorks. The client falls back to initialize, and possibly further to the deprecated HTTP+SSE transport.
LegacyModernFails. On HTTP the request lacks required headers and is rejected with 400 Bad Request. Legacy clients have no fall-forward mechanism.
LegacyDual-eraWorks, under the negotiated legacy revision.
LegacyLegacyWorks, 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/discover and 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 Request before 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:

ToolTaxonomyMCP compatibleLast verified
Claude CodeFully Autonomous / Code / MCP-nativeYes2026-06-20
LangChainSupervised / Code / LangChainYes2026-06-15
CrewAISupervised / Code / CustomYes2026-06-02
LlamaIndexCopilot / API / LlamaIndexYes2026-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.

FeatureDocumented migrationEarliest removal
RootsPass directories or files via tool parameters, resource URIs or server configurationFirst revision released on or after 2027-07-28
SamplingIntegrate with LLM provider APIs directlyFirst revision released on or after 2027-07-28
LoggingLog to stderr on stdio, or use OpenTelemetryFirst revision released on or after 2027-07-28
Dynamic Client RegistrationClient ID Metadata DocumentsFirst revision released on or after 2027-07-28
HTTP+SSE transportStreamable HTTPThree 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:

  1. Call server/discover against every server you depend on. One request returns the supported versions. This is the only step that produces a fact rather than an assumption.
  2. 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.
  3. Grep for initialize in your own client code. If you send it unconditionally, you are a legacy client and a modern-only server will reject you with a 400.
  4. 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.
  5. 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.