laravel/mcp: Require the protocol metadata on every request
Contributed by pushpak1300
Part of the mcp v1.0.1 release
laravel/mcp now validates protocol metadata on every incoming request. Merged as PR #297, each JSON-RPC request must carry the MCP protocol version and the client's capabilities inside params._meta, matching the 2026-07-28 revision of the Model Context Protocol specification. That revision drops the assumption of a stateful initialize handshake: rather than negotiating once per session, every request is self-describing. If you drive the server with hand-rolled payloads — tests, scripts, proxies, or a custom client — this is a breaking change, and every request you send needs updating.
What changed
Server::handle() now calls a new validateProtocolMeta() check before it even looks up the requested method. Two members are mandatory inside _meta:
io.modelcontextprotocol/protocolVersion— must be a string, e.g."2026-07-28".io.modelcontextprotocol/clientCapabilities— must be a JSON object. An empty object is fine, but a sequential list such as["elicitation"]is rejected.
The fully-qualified member names live in a new MetaKey enum, and JsonRpcRequest::meta() was tightened to only recognise object-shaped _meta values — a list-shaped _meta is treated as absent. Because validation runs ahead of the method router, a request without metadata returns an invalid-params error even when the method name is also unknown. Malformed values (a null or integer version, a string where capabilities should be) report the same "missing required member" message as truly absent ones.
Why the spec moved this way
Until now, an MCP server learned the negotiated protocol version during initialize and had to remember it for the rest of the session. The 2026-07-28 revision removes that coupling: with no handshake, every request carries its own protocol version and capabilities. That makes the server genuinely stateless — no session store, no sticky routing, no ambiguity behind load balancers or in serverless deployments — and lets version negotiation happen per request instead of once per connection.
clientInfo remains optional because the specification marks it as SHOULD, not MUST, so the server does not reject requests that omit it.
The two new error responses
Missing or malformed members return -32602:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params: The request [_meta] is missing the required [io.modelcontextprotocol/protocolVersion] member."
}
}
A well-formed but unrecognized version returns -32022 with a machine-readable data payload so clients can discover what to retry with:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28"],
"requested": "2025-11-25"
}
}
}
What a valid request looks like
Every method — including initialize and ping — now needs the metadata block:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "say-hi-tool",
"arguments": { "name": "John Doe" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
Note that clientCapabilities is {} here — an empty object satisfies the requirement until you actually advertise capabilities.
Upgrade impact
- Official MCP client SDKs updated for the
2026-07-28revision already send this metadata; nothing changes for you. - Raw JSON-RPC callers — integration tests,
curlscripts, queues, custom transports — must add the_metablock to every request,initializeincluded. - Version strings must match exactly.
"2025-11-25"(or anything else) returns-32022; only"2026-07-28"is supported today. - PHP-side definitions are untouched. Tools, resources, and prompts need no changes; this is transport-level validation.
Still to come: Mcp-Method and the other mirrored header validation land in #286, and a follow-up PR adds resultType and server info on results. Thanks to @JordanDalton, whose #275 mapped out the 2026-07-28 work. Full details are in the spec changelog and the _meta documentation.
Takeaways
- Every request now requires
_metawith a stringio.modelcontextprotocol/protocolVersionand an object-shapedio.modelcontextprotocol/clientCapabilities. - Missing or malformed metadata returns
-32602; an unknown version returns-32022with asupportedlist indata. - This is a BC break: update any hand-built JSON-RPC payloads, tests, or custom clients before upgrading.
- Per-request metadata enables fully stateless operation under the
2026-07-28spec;clientInfostays optional.