Two eras: legacy and modern
Most tutorials describe a session that opens with initialize. Revision 2026-07-28 removed it. Knowing both eras, and why the change happened, is the key to reading any MCP traffic you meet.
By the end of this lesson you can
- name the two eras and the revisions they cover;
- list what 2026-07-28 removed and what replaced each piece;
- read the
_metablock every modern request carries; - explain
server/discoverand error-32022; - say why all of it exists: any replica can answer any request.
Legacy, modern, dual-era
The specification itself names the eras. Revisions up to and including 2025-11-25 are legacy; revision 2026-07-28 and later are modern. A client or server that speaks both is dual-era.
The same deploy, two eras
- Legacy opens with a handshake:
initialize, its result (with a session id on HTTP), thennotifications/initialized. The server now keeps a session in memory. - Later calls carry no version or capabilities: the session implies who this is. Another replica would not know.
- Mid-call, the legacy server sends its own request to the client, and keeps the half-finished call in one process's memory until the answer comes back.
- Modern has no handshake. Every request carries its version and capabilities in
_meta. - When the server needs input, it does not send a request. It ends the call with
input_requiredand hands its context to the client as a signedrequestState. - The client sends the call again with a new id and the answers. Nothing was kept in between, so any replica can answer any request.
What changed, row by row
| Legacy, up to 2025-11-25 | Modern, 2026-07-28 | |
|---|---|---|
| Opening | initialize then notifications/initialized | None. server/discover is optional |
| Version and capabilities | Negotiated once, held by the session | In _meta on every request |
| Sessions | Mcp-Session-Id on HTTP, ended with DELETE | None |
| Server needs input | Server sends its own request mid-call | Server returns input_required; client retries |
| Change notifications | Standalone GET stream | subscriptions/listen request |
| Version mismatch | Negotiated down in the handshake | Error -32022 listing supported versions |
Put another way, the modern protocol has no sessions, no Mcp-Session-Id, no GET stream and no SSE resumability, and servers MUST NOT initiate JSON-RPC requests. A server can still ask the client for something (a user confirmation, say), but only by ending its result with input_required: the ask is the server's, the request on the wire is always the client's. Change notifications such as notifications/tools/list_changed and notifications/resources/updated now arrive on a subscriptions/listen request the client opens, instead of a standalone GET stream. The server MUST first send notifications/subscriptions/acknowledged, and every notification on that stream carries the listen request's id in _meta as io.modelcontextprotocol/subscriptionId.
Common misconception
“Every MCP connection starts with initialize.” Only in the legacy era. A modern client can send tools/call as its very first message. If a tutorial opens with a handshake, check which revision it targets.
What every modern request carries
Because there is no session, each request describes itself in _meta:
{ "jsonrpc": "2.0", "id": 7, "method": "tools/list",
"params": { "_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} }
} } }
server/discover
server/discover returns the server's supported versions, capabilities and instructions. Servers MUST implement it; clients MAY call it. The result carries the caching hints ttlMs and cacheScope ("public" or "private"); servers MUST add the same hints to complete results of tools/list, prompts/list, resources/list, resources/templates/list and resources/read. It does not open a session: the server does not remember the call happened.
Error -32022
There is no negotiation round. If a request names a version the server does not support, the server answers with -32022 (UnsupportedProtocolVersionError) and lists what it does support. The client picks a mutually supported version and sends the request again.
{ "jsonrpc": "2.0", "id": 1,
"error": { "code": -32022, "message": "Unsupported protocol version",
"data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" } } }
It is one of three error codes the specification defines in its reserved range, all sent with HTTP 400: -32020 HeaderMismatch (an Mcp-* header disagrees with the body, lesson 5), -32021 MissingRequiredClientCapability (the request needs a capability the client did not declare in _meta; data.requiredCapabilities lists it) and -32022. Any of the three tells a dual-era client the server is modern.
Key idea
The point of all of it: any replica can answer any request, with nothing to remember in between. That means horizontal scaling with no sticky load balancing, which lesson 11 shows working with a simulator.
One call, two eras
The same tools/call, built the legacy way and the modern way. Switch era and transport and watch what appears and disappears.
On the wire
What to notice
Quick check
Which method MUST a modern server implement?
server/discover; clients MAY call it. initialize is the legacy handshake.A modern request arrives with protocol version 1900-01-01. What does the server do?
In the modern era, where does the server learn the client's capabilities?
_meta. server/discover tells the client about the server, not the other way round.Which of these did 2026-07-28 remove?
Mcp-Session-Id, the GET stream and server-initiated requests went. Primitives and both transports remain.Recap
- Legacy (≤ 2025-11-25):
initializehandshake, sessions, server-initiated requests, a GET stream. - Modern (2026-07-28): no handshake,
_metaon every request,server/discover,-32022on a bad version,input_requiredinstead of server requests,subscriptions/listenfor changes. - The goal: nothing to remember between requests, so any replica can serve any call.