Validate the mirrored MCP request headers over HTTP
Contributed by pushpak1300
Part of the mcp v1.0.1 release
Category: Upgrade notes (11)
Pull request #286 brings laravel/mcp in line with the 2026-07-28 MCP specification's request-metadata rules for the Streamable HTTP transport. The spec lets clients mirror parts of the JSON-RPC body into HTTP headers so proxies, gateways, and load balancers can route or observe traffic without parsing JSON. Because the body remains the source of truth, the server now validates the mirror and rejects any request where the two disagree. If you serve MCP over HTTP from v0.9.6, this is a breaking change: every POST to an MCP route must carry MCP-Protocol-Version and Mcp-Method — plus Mcp-Name for tools/call, prompts/get, and resources/read — and protocol errors now surface as real HTTP statuses instead of a uniform 200. Stdio transports are untouched.
What the server now checks
A new ValidateMcpHeaders middleware is registered automatically on every Mcp::web() route, between ReorderJsonAccept and AddWwwAuthenticateHeader. It decodes the JSON-RPC body and compares three headers against it:
MCP-Protocol-Versionmust match_meta['io.modelcontextprotocol/protocolVersion']Mcp-Methodmust match the body'smethodmemberMcp-Namemust matchparams.namefortools/callandprompts/get, orparams.uriforresources/read
The version and method headers are required even when the body omits the matching member, and a contradicting value is treated exactly like a missing one. Either failure short-circuits the request with a 400:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "Header mismatch: The [Mcp-Name] header value [some-other-tool] does not match the request body value [get_weather]."
}
}
Only Mcp-Name accepts the =?base64?...?= sentinel, which is strictly decoded before comparison so non-ASCII tool names can ride in a header; the sentinel is deliberately not honored on the other headers. Two exemptions keep the change humane: requests without a usable JSON-RPC method pass straight through, and initialize is skipped so a legacy client still receives the unsupported-protocol-version error naming the supported versions from #283 rather than a header complaint it cannot understand yet. Notifications are validated too and answer 202 on success.
Why the mirror matters
Under the new spec, headers are no longer decoration. A proxy can route a tools/call for get_weather to the right backend, or sample production traffic, by reading Mcp-Method and Mcp-Name alone. Since the body is still authoritative, a header that contradicts it signals a broken, stale, or hostile client — so the spec's server-validation rules require rejecting the request instead of quietly trusting one half of it.
Protocol errors now map to HTTP statuses
Until now, nearly everything over HTTP returned a 200 with a JSON-RPC error tucked inside. HttpTransport now derives the status from the reply: -32601 becomes 404, -32603 becomes 500, and every other error becomes 400; a reply-less notification stays 202 and a successful reply stays 200. That means an unknown method is a 404 instead of a 200, the rejected initialize handshake is a 404, and an unsupported protocol version is a 400 carrying -32022 with the supported list. One more rename to note: the header-mismatch error code moved from -32020 to -32001.
What to change when upgrading
Any code that POSTs directly to your MCP routes — scripts, health checks, integration tests, custom clients — must now send the headers:
curl -X POST https://your-app.test/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: get_weather' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"Lisbon"}}}'
Update anything matching on -32020, and make assertions tolerant of the new 400/404/500 statuses. For integration tests, the repository's suite now builds headers with a mcpHeaders($message) helper that mirrors the body for you — a good pattern to crib. If you use the framework's own client, nothing to do here: it learns to send these headers in the companion era-awareness PR above this one, and in legacy mode it deliberately sends none, since a legacy server should not receive them. Mcp-Param-* header validation waits for the spec's x-mcp-header support in a later PR. Finally, credit to @JordanDalton, whose #275 mapped out the 2026-07-28 work.
Takeaways
- HTTP MCP requests must now carry
MCP-Protocol-Version,Mcp-Method, and — for methods that name a target —Mcp-Namemirroring the body; omissions and contradictions return400with-32001. - Error responses now use real HTTP statuses:
404for unknown methods,400for most protocol errors,500for internal errors. HEADER_MISMATCHchanged from-32020to-32001; update any code matching on the old value.initializeis exempt, so legacy clients still get proper version negotiation errors.- Stdio transports are unaffected.