When no shared protocol version exists, the MCP client should stop and report the mismatch, never proceed and hope runtime errors catch it.
Picture two people meeting who each speak a few languages. Before chatting, they swap lists and pick one both know. If the lists overlap, they talk in that shared language. If there is zero overlap, the sensible move is to stop and say 'we cannot understand each other' right away. The reckless move is to start talking in your own language and hope the other person guesses. MCP works the same way. The client names its version on every message. If the server does not speak it, the server replies with the list of versions it does speak. The client picks one from that list and tries again. If nothing matches, the honest move is to stop and report it. Do not push on and find the breakage halfway through a real task.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
MCP has no opening handshake: every request declares which protocol version it speaks. The protocol uses date-based version strings, for example 2026-07-28, rather than semantic version numbers. Each published revision of the spec gets a date, and a client or server carries the set of dates it knows how to speak. The edge case here is what happens when the client and server share no common version at all.
The client should stop and report an incompatibility error rather than proceed. The interesting part is why that is correct, why the other plausible-sounding options are wrong, and how version negotiation relates to the separate mechanism of capability negotiation.
The topic sits at the intersection of protocol design and operational reliability. Saying 'abort' is not enough without the failure mode behind it. The full picture names capability decay, ties the choice to the fail-fast principle, and separates the version axis from the capability axis. It also gives a clean mental model for forward and backward compatibility across an evolving server fleet.
Where the version lives in every request
Every MCP request carries a _meta field with the protocol version it uses (io.modelcontextprotocol/protocolVersion) and the client's declared capabilities. On Streamable HTTP the version also travels in the MCP-Protocol-Version header. There is no session to set up first: the server checks each request on its own.
If the server supports the declared version, it handles the request normally. If it does not, it must reject the request with an UnsupportedProtocolVersionError (code -32022). Its supported list names the versions the server does speak. The client then looks for a version in that list that it also supports. If it finds one, it retries the request with that version. If it finds none, it surfaces an error.
A client does not have to learn this by trial. Every server must implement server/discover, and a client may call it before any other request. The result lists the server's supported versions, its identity and its capabilities, so the client can check for overlap up front.
In this split, the server's job is to state which versions it can speak. The client's job is to pick a shared one or stop. The server never has to reason about the client's full supported set.
All of this happens before a single tool is called or a single resource is read. A request with an unsupported version is rejected before any work is done. That is why stopping is safe and cheap. No state has been mutated and no tool has run, so there is nothing half-done to clean up.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
| Aspect | Protocol version | Capabilities |
|---|---|---|
| What it selects | The wire contract and method set | Which optional features are live |
| Format | Date based string (2025-06-18) | Named flags (tools, resources, prompts) |
| Negotiated where | Declared in every request's _meta | Capabilities field (server/discover result, request _meta) |
| No agreement means | Abort the connection | Feature is unavailable |
| Failure mode if ignored | Wrong method set, broken contract | Calling an unadvertised feature |
Real products, models, and research that use this idea.
- On its v2 runtime, Claude Code asks each server whether it supports the newer protocol revision and uses it with the servers that do. With the others it connects on the older revision.
- The v1 TypeScript and Python SDKs implement the older initialize handshake and reject connections when the negotiated version is unsupported.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow does capability negotiation differ from protocol version negotiation?
Version picks the wire contract and method set. Capabilities are named flags that gate optional features like tools, resources, prompts, and sampling within that contract.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Assuming the client should silently fall back to an old version. The spec says abort and report when no shared version exists, not guess.
60 second bullets to scan on the way to the call.
Where each request declares its protocol version
The format of MCP version strings
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.