MCP 2026-07-28 one lesson per page
23 lessons
Breaks immediatelyInteractions & Execution · 26%Track 2 · 2 / 14

No more hello: every request introduces itself

initialize and notifications/initialized are removed. The version, capabilities and client identity now ride in _meta on every single request.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

The serverless cold start

You deploy your MCP server as a serverless function. Each request may wake up a brand-new instance with empty memory.

Under the legacy protocol that's a contradiction. Instance #1 handles initialize and then disappears; instance #2 gets tools/call and has no idea which version or capabilities were agreed. You end up bolting on a key-value store just to remember a handshake.

Now the first request is a complete request. Instance #2 reads the version and capabilities straight from _meta and answers. On a mobile agent with 300 ms of latency, dropping the initialize round trip also saves about a third of a second before the first useful answer.

  • io.modelcontextprotocol/protocolVersion: required on every request.
  • io.modelcontextprotocol/clientCapabilities: required. It covers only the capabilities relevant to this request.
  • io.modelcontextprotocol/clientInfo: clients SHOULD send it. It's for display and logs only; never use it for security decisions.
  • Servers SHOULD put io.modelcontextprotocol/serverInfo in every result's _meta.

There are three errors to know. A request missing a required field gets -32602 (Invalid params). An unsupported version gets -32022 UnsupportedProtocolVersion, with a supported list. A missing capability the server needs gets -32021 MissingRequiredClientCapability. Over HTTP, all three return status 400.

The handshake pinned negotiated facts (version, capabilities) to a connection. Any instance that later handled a request needed those facts, so you needed sticky routing or a shared store. Putting them in each request removes that dependency, and it lets capabilities differ from one request to the next.

Same job, two eras
Compare

This is how it works in 2026-07-28. Switch to Legacy only to see what it replaced.

ClientServertools/call _meta{version, caps, info}result _meta{serverInfo}Version the server does not speak?tools/call _meta{version: 1900-01-01}400 · -32022 {supported: [...]}
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

Calling a tool

2026-07-28
// One message. No warm-up.{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_weather",    "arguments": { "location": "Seattle, WA" },    "_meta": {      "io.modelcontextprotocol/protocolVersion": "2026-07-28",      "io.modelcontextprotocol/clientCapabilities": { "elicitation": {} },      "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" }    }  }}
What it used to look like (legacy, for comparison only)
Legacybefore
// ① client → server: open the session{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {   "protocolVersion": "2025-11-25",   "capabilities": { "elicitation": {} },   "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } }}// ② client → server: "I'm ready"{"jsonrpc": "2.0", "method": "notifications/initialized"}// ③ only now can real work start{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_weather",             "arguments": { "location": "Seattle, WA" } }}

Note that the request ID no longer has to be 2: there is no earlier request on this "connection" to count from.

The answer identifies the server, every time

2026-07-28
{  "jsonrpc": "2.0",  "id": 1,  "result": {    "resultType": "complete",    "content": [{ "type": "text", "text": "Seattle: 14°C, light rain" }],    "_meta": {      "io.modelcontextprotocol/serverInfo": { "name": "WeatherServer", "version": "2.1.0" }    }  }}
What it used to look like (legacy, for comparison only)
Legacybefore
// serverInfo arrived once, in the handshake{"jsonrpc": "2.0", "id": 1, "result": {  "protocolVersion": "2025-11-25",  "capabilities": { "tools": {} },  "serverInfo": { "name": "WeatherServer", "version": "2.1.0" }}}

Wrong version: the error tells the client what to retry with

2026-07-28
{  "jsonrpc": "2.0",  "id": 1,  "error": {    "code": -32022,    "message": "Unsupported protocol version",    "data": {      "supported": ["2026-07-28", "2025-11-25"],      "requested": "1900-01-01"    }  }}

Verbatim from the spec. The client SHOULD pick a version from supported and retry, or show an error if there is none in common.

  • A server that stores the negotiated version and capabilities at connect time has nothing to read: a modern client never sends initialize.
  • Middleware that rejects requests "before init" rejects every modern request.
  • A legacy client that sends initialize to a modern-only server fails. Over HTTP, its request lacks the required headers and gets 400. Legacy clients have no way to move forward on their own.
  • The fix: read version and capabilities from each request's _meta. To keep old clients working, go dual-era: answer initialize with legacy semantics, and serve requests carrying _meta statelessly. A modern-only server SHOULD name its supported versions in the error it returns to initialize; it's the only diagnostic an old client can show.
Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.

Run these against the reference server from the setup block, or point MCP at your own server. The helper adds the transport headers, which are explained in lesson 03.

ShellTests
# 1. A modern request with no initialize firstmcp tools/list '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{'"$META"'}}'# expect: HTTP 200 and "resultType":"complete" # 2. Drop the required capabilities fieldmcp tools/list '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'# expect: HTTP 400 and error code -32602 # 3. Ask for a version nobody speaks (header and body must agree)curl -sS "$MCP" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -H 'MCP-Protocol-Version: 1900-01-01' -H 'Mcp-Method: tools/list' -d '{"jsonrpc":"2.0","id":3,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"1900-01-01","io.modelcontextprotocol/clientCapabilities":{}}}}' -w '\nHTTP %{http_code}\n'# expect: HTTP 400, code -32022, data.supported lists real versions

Answer all 3 correctly and this lesson is marked as learned.

Q1Which _meta fields MUST be on every request?

Q2A client asks for a protocol version the server does not implement. What must the server return?

Q3A tools/call needs to ask the user for confirmation, but this request's clientCapabilities is {}. What should the server do?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.