A host does not get to choose which version of MCP a server speaks. It connects to servers written by strangers, at whatever revision they were built against, and has to work anyway.
There are two eras. The difference between them is not cosmetic — a whole capability works in one and cannot work in the other.
Part 1 · The two eras
| legacy era | modern era | |
|---|---|---|
| versions | 2025-11-25 and earlier | 2026-07-28 |
| negotiated | once, by an initialize handshake | per request, in a _meta envelope |
| sessions | Mcp-Session-Id header, DELETE to end | removed |
| standalone SSE stream | opened with GET | removed |
| server→client requests | pushed down an open SSE stream | removed — replaced by a retry |
| resumable streams | Last-Event-ID | removed |
| works on serverless | tools, yes; pushes, no | fully |
Read the "removed" column as a single sentence: everything that assumed the server remembers you, or that a connection stays open, is gone.
Part 2 · What "per-request negotiation" actually looks like
In the legacy era, you shake hands once and the connection remembers.
In the modern era there is no connection to remember anything, so every single request carries who you are and what you can do:
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "summarise_week",
"arguments": { "depth": "brief" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "probe", "version": "1" },
"io.modelcontextprotocol/clientCapabilities": { "sampling": {} }
}
}
}Three namespaced keys, on every request:
| Key | What it does |
|---|---|
…/protocolVersion | selects the era, for this request |
…/clientInfo | who is calling |
…/clientCapabilities | what you are willing to be asked for |
That last one is the whole security story of the modern era, and it is worth dwelling on.
And because it is per-request rather than per-connection, a host can be
selective: declare sampling on the call to a server you trust, and withhold
it on the call to one you don't. That is not possible with a handshake.
Part 3 · Finding out which era you are talking to
You cannot ask "which version do you speak?" without already having picked a version to ask in. So the answer is: try the modern one and read the failure carefully.
Part 4 · Era negotiation is a try/catch, not a config flag
This is the design decision worth stealing, and project #5 got it right for a slightly cynical reason: you are talking to somebody else's deployment, and any guess you hard-code about it will eventually be wrong.
It paid off immediately.
So the toolbox records the era it landed on, per server, and prints it:
✓ The Kitchen (asks you to think)
era: 2026-07-28 (can ask you to think)
2 tool(s) · 2 resource(s) · 1 prompt(s)
✓ Cookie Jar (project #1, forgetful)
era: 2026-07-28 (can ask you to think)
3 tool(s) · 1 resource(s) · 1 prompt(s)Part 5 · A constant that lies
The last thing, and it is the one that cost project #5 a design detour.
node --input-type=module -e "
import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server';
console.log(LATEST_PROTOCOL_VERSION, JSON.stringify(SUPPORTED_PROTOCOL_VERSIONS));"2025-11-25 ["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]
No 2026-07-28 anywhere. A constant named LATEST_PROTOCOL_VERSION does not
contain the latest protocol version.
It is not a bug. Those constants describe the initialize-negotiated era
only — the modern era is not negotiated by a handshake at all, so it does not
appear in a list of handshake-negotiable versions. Two code paths, and the
constant only knows about one.
A constant named
LATEST_answers a narrower question than its name implies. When types and constants disagree, the wire is the tiebreaker.
What you now know
- Two eras: legacy (
initializehandshake, sessions, pushes) and modern (2026-07-28, per-request_meta, no sessions, no pushes). - Every removal in the modern era replaces stateful machinery with something self-contained.
clientCapabilitiestravels per request, which makes the dangerous capabilities both opt-in and selectable per server.- Detect the era by trying modern first and reading the error body — a
400alone does not mean legacy. - Negotiate with a
try/catchand print the era you landed on. LATEST_PROTOCOL_VERSIONdescribes only the handshake path.