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. Change notifications such as tools/list_changed and resources/updated now arrive on a subscriptions/listen request the client opens, instead of a standalone GET stream.
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 is cacheable through ttlMs and cacheScope, and 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" } } }
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.