The MCP client in all five projects is hand-written — about two hundred lines
of fetch, deliberately, because the point of the series is that the protocol
is just JSON. Project #5 taught that file the second era.
This page is the reference you would otherwise have to reconstruct from error codes.
Two eras, and how a client picks one
| legacy era | modern era | |
|---|---|---|
| version | 2025-06-18 and earlier | 2026-07-28 |
| negotiated | once, at initialize | per request, in a _meta envelope |
| server→client push | yes, over the open connection | removed |
| sampling | sampling/createMessage pushed down | input_required + a retry |
| works on serverless | tools only | fully |
Era negotiation is a try/catch, not a configuration flag. That is a
deliberate choice: you are talking to somebody else's deployment, and a
hard-coded guess about which era it speaks is a guess that will be wrong.
The _meta envelope
Three keys, all namespaced, all sent inside params._meta on every
request:
{
"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": {} }
}
}
}| Key | What it does |
|---|---|
io.modelcontextprotocol/protocolVersion | selects the era, on this request |
io.modelcontextprotocol/clientInfo | who is calling |
io.modelcontextprotocol/clientCapabilities | what you are willing to be asked for |
That last one is the security control. It is the whole opt-in.
The headers
The modern era wants the routing information in the headers as well as the body, and it checks that they agree:
-H 'Content-Type: application/json'
-H 'Accept: application/json, text/event-stream'
-H 'Mcp-Method: tools/call'
-H 'Mcp-Name: summarise_week'Mcp-Name is required whenever params carries a name or a uri — so
tools/call and resources/read need it, and a bare tools/list does not.
Where the retry's answers go
This is the detail that is genuinely easy to get wrong, because the natural guess is wrong.
inputResponses and requestState are top-level members of params. Not
inside _meta, where the rest of the modern-era machinery lives.
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "summarise_week",
"arguments": { "depth": "brief" },
"inputResponses": { "summary": { "kind": "sampling", "result": { } } },
"requestState": "…the opaque string the server handed you…",
"_meta": { }
}
}Put them in _meta and the server does not see them, so it asks for the same
thing again — and each retry costs the agent another whole round trip. It looks
like a loop bug. It is a placement bug.
The three error codes
The request headers and body disagree
You sent an
Mcp-Methodthat doesn't matchmethod, or you omittedMcp-Nameon a request whoseparamscarry anameoruri.Mechanical, and the message says so once you know to read the headers rather than the JSON.
Client capabilities do not declare the required capability
You didn't put
sampling: {}inio.modelcontextprotocol/clientCapabilities.Unsupported protocol version
You asked for an era the server does not speak. This is the one your
try/catchis for: catch it, fall back to the legacy handshake, and record which era you ended up on — because a server on the legacy era cannot ask your host to think, no matter what it wants.
Reading the era off a real toolbox
Because none of the above is worth anything if you can't see which era you actually landed on, the checkpoint 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)If sampling silently never happens, look at that column first.