Skip to content
MCP Five

The HostProject #217 / 51

Two protocol eras

2026-07-28 removed sessions, the GET stream and server-initiated requests. How a client finds out which era it is talking to.

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 eramodern era
versions2025-11-25 and earlier2026-07-28
negotiatedonce, by an initialize handshakeper request, in a _meta envelope
sessionsMcp-Session-Id header, DELETE to endremoved
standalone SSE streamopened with GETremoved
server→client requestspushed down an open SSE streamremoved — replaced by a retry
resumable streamsLast-Event-IDremoved
works on serverlesstools, yes; pushes, nofully

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:

json
{
  "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:

KeyWhat it does
…/protocolVersionselects the era, for this request
…/clientInfowho is calling
…/clientCapabilitieswhat 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.

Attempt a modern request first. A 400 is not automatically 'this is a legacy server' — modern servers use 400 for three of their own errors, so the body has to be inspected before falling back.

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:

npm run readonly
  ✓ 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.

bash
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));"
terminal
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 (initialize handshake, 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.
  • clientCapabilities travels 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 400 alone does not mean legacy.
  • Negotiate with a try/catch and print the era you landed on.
  • LATEST_PROTOCOL_VERSION describes only the handshake path.