MCP 2026-07-28 one lesson per page
23 lessons
Fails quietlyArchitecture · SecurityTrack 2 · 4 / 14

Routing without reading the body: required HTTP headers

Every POST mirrors key body fields into headers, so load balancers, gateways and firewalls can route and rate-limit without parsing JSON. If a header disagrees with the body, the request is rejected.

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

Data residency at the edge

A bank runs one MCP endpoint for analysts worldwide. European customer data must never leave the EU.

Its execute_sql tool marks region with x-mcp-header, so every call arrives with Mcp-Param-Region: eu-west1. The edge gateway routes on that header alone and never parses the JSON body. A firewall rule blocks Mcp-Name: drop_* for contractor tokens.

Then an attacker crafts a request whose header says get_balance but whose body says transfer_funds, hoping the gateway approves one while the server runs the other. The server compares the two, finds a mismatch, and returns 400 with -32020. That one rule is why the routing is safe to trust.

  • MCP-Protocol-Version (around since 2025-06-18) MUST now equal the _meta protocolVersion.
  • Mcp-Method on every request. Mcp-Name (params.name or params.uri) on tools/call, resources/read and prompts/get.
  • A tool can mark a parameter with x-mcp-header. Clients MUST then mirror its value as Mcp-Param-{Name}. Values that aren't plain ASCII use =?base64?…?=.
  • Missing, mismatched or malformed headers get 400 and error -32020 HeaderMismatch. An unknown method gets 404 with -32601.

The danger here is a load balancer routing on the header while the server executes the body. If the two could disagree, an attacker could route a "harmless" call past a policy and run a dangerous one. Strict header-body equality closes that gap.

Message flow in 2026-07-28
ClientGatewayServerPOST Mcp-Method: tools/callRoutes and rate-limits on headers aloneforward (Mcp-Name: execute_sql)Server checks headers == body400 · -32020 HeaderMismatch (if not)
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

A tool call over HTTP

2026-07-28
POST /mcp HTTP/1.1Content-Type: application/jsonMCP-Protocol-Version: 2026-07-28Mcp-Method: tools/callMcp-Name: execute_sqlMcp-Param-Region: us-west1 {  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "_meta": {      "io.modelcontextprotocol/protocolVersion": "2026-07-28",      "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },      "io.modelcontextprotocol/clientCapabilities": {}    },    "name": "execute_sql",    "arguments": { "region": "us-west1", "query": "SELECT * FROM users" }  }}
What it used to look like (legacy, for comparison only)
Legacybefore
POST /mcp HTTP/1.1Content-Type: application/jsonMCP-Protocol-Version: 2025-11-25Mcp-Session-Id: 1868a90c-5f4e-4c1a-9d2b-7f3e1c0a9b44 {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "execute_sql",             "arguments": { "region": "us-west1", "query": "SELECT * FROM users" } }}

Verbatim from the spec. Mcp-Param-Region exists because the tool marked region with "x-mcp-header": "Region" in its inputSchema.

The tool definition that asks for the extra header

2026-07-28
{  "name": "execute_sql",  "inputSchema": {    "type": "object",    "properties": {      "region": {        "type": "string",        "description": "The region to execute the query in",        "x-mcp-header": "Region"      },      "query": { "type": "string", "description": "The SQL query to execute" }    },    "required": ["region", "query"]  }}

Only primitive types (string, integer, boolean, never number), reachable from the schema root through properties alone. A client must drop a tool whose annotation breaks these rules.

  • A proxy that rewrites or strips headers now produces mysterious 400s with -32020.
  • Your server must compare headers with the body, decoding base64 values first and comparing integers as numbers.
  • Still required as before: validate Origin (an invalid one gets 403, which stops DNS rebinding), and bind to localhost when running locally.
First time here? 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.
ShellTests
# 1. Header says one tool, body says another: expect HTTP 400 and -32020mcp tools/call '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"delete_all","arguments":{},'"$META"'}}' -H 'Mcp-Name: get_weather' # 2. Missing Mcp-Name on tools/call: expect 400 and -32020mcp tools/call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"location":"Seattle"},'"$META"'}}' # 3. x-mcp-header: execute_sql mirrors region into Mcp-Param-Regionmcp tools/call '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"execute_sql","arguments":{"region":"eu-west1","query":"SELECT 1"},'"$META"'}}' -H 'Mcp-Name: execute_sql' -H 'Mcp-Param-Region: eu-west1'# expect: 200. Now change the header to us-west1 (or drop it): expect 400 and -32020 # 4. Unknown method: expect 404 and -32601mcp nope/nope '{"jsonrpc":"2.0","id":4,"method":"nope/nope","params":{'"$META"'}}' 

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

Q1The Mcp-Name header says get_weather but the body's params.name is delete_all. What happens?

Q2Which tool parameter may carry an x-mcp-header annotation?

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.