Skip to content
MCP Five

The LedgerProject #538 / 51

Speaking the protocol by hand

The _meta envelope, the Mcp-Method headers, and what −32020, −32021 and −32022 each mean.

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 eramodern era
version2025-06-18 and earlier2026-07-28
negotiatedonce, at initializeper request, in a _meta envelope
server→client pushyes, over the open connectionremoved
samplingsampling/createMessage pushed downinput_required + a retry
works on serverlesstools onlyfully

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:

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": {} }
    }
  }
}
KeyWhat it does
io.modelcontextprotocol/protocolVersionselects the era, on this request
io.modelcontextprotocol/clientInfowho is calling
io.modelcontextprotocol/clientCapabilitieswhat 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:

bash
-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.

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

  1. The request headers and body disagree

    You sent an Mcp-Method that doesn't match method, or you omitted Mcp-Name on a request whose params carry a name or uri.

    Mechanical, and the message says so once you know to read the headers rather than the JSON.

  2. Client capabilities do not declare the required capability

    You didn't put sampling: {} in io.modelcontextprotocol/clientCapabilities.

  3. Unsupported protocol version

    You asked for an era the server does not speak. This is the one your try/catch is 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:

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)

If sampling silently never happens, look at that column first.