Guide
Why a valid MCP request still gets a 400 from pdfops.dev
A JSON-RPC body that is fully valid for the 2026-07-28 MCP spec, with a real tool name and valid arguments, can still get a flat 400 from pdfops.dev's hosted MCP endpoint on the very first call. The same request against a local MCP server works fine. The cause is an HTTP header the spec requires alongside the body, separate from the protocol version field already sitting in the JSON-RPC payload, and it is easy to leave off because the body alone looks complete.
The request that looks right and still fails
The 2026-07-28 revision of MCP moved protocol negotiation out of
an initialize handshake and into fields carried on
every request: params._meta carries
io.modelcontextprotocol/protocolVersion and
io.modelcontextprotocol/clientCapabilities in the
JSON-RPC body itself. A client built against that revision fills
those fields in correctly. What the spec also asks for,
separately, is an MCP-Protocol-Version HTTP header on
the same request, and pdfops.dev's server checks that the two
agree rather than trusting either one alone.
curl -X POST https://pdfops.dev/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "pdfops_usage",
"arguments": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'
# {"jsonrpc":"2.0","id":1,"error":{"code":-32020,
# "message":"Header mismatch: MCP-Protocol-Version header is required"}}
# HTTP 400
That is the exact response from a live call: the body is valid
JSON-RPC, the tool name and arguments are fine, and the only
problem is a header that never made it onto the wire. Over the
last 30 days of production traffic, /api/mcp logged
451 separate 400 responses from 55 distinct callers, the whole
measured 4xx volume for this endpoint. The log does not record
which of the server's several 400 causes fired on each of those
rows, so the exact split between a missing header and the other
causes below is not something the log can answer. What it does
show is that 400 is the only 4xx /api/mcp produces at
any real volume, and a missing MCP-Protocol-Version
header is one confirmed way to land on it.
The fix is one header, matched to the body
Adding MCP-Protocol-Version: 2026-07-28 to the
request, with a value that matches
_meta["io.modelcontextprotocol/protocolVersion"]
exactly, turns the same body into a normal result:
curl -X POST https://pdfops.dev/api/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2026-07-28" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "pdfops_usage",
"arguments": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'
# {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete",
# "content":[{"type":"text","text":"PDFops API error 401
# (missing_api_key): pass your key as X-API-Key"}],"isError":true, ...}}
# HTTP 200
For the 2026-07-28 revision, the server treats the header and the body field as two halves of one claim about the protocol version. If the header says 2026-07-28, or there is no header and the body field says 2026-07-28, both must be present and agree, or the request fails before any tool runs. A client wired to send only one of the two needs the other line added, not a retry or a different tool call. A header naming an older supported revision skips this check.
A few more ways to land on the same 400
A few other request shapes get rejected before any tool runs,
with the same style of error. Sending a protocol version the
server does not support at all returns -32022 with
the list of versions it does accept, so a client pinned to an old
draft of the spec gets told exactly what to change to. A body
that is not valid JSON at all returns -32700, same as
any JSON-RPC server. And a JSON array of several calls in one
POST returns -32600: MCP dropped batching support in
the 2025-06-18 revision, so the modern spec does not allow it
either, and pdfops.dev's endpoint rejects it the same way a
spec-compliant client would expect.
# malformed JSON body
curl -s -X POST https://pdfops.dev/api/mcp -d '{not valid json'
# {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}
# batched array body
curl -s -X POST https://pdfops.dev/api/mcp \
-d '[{"jsonrpc":"2.0","id":1,"method":"tools/list"}]'
# {"jsonrpc":"2.0","id":null,"error":{"code":-32600,
# "message":"Batched requests are not supported"}}
Try it
The descriptor at GET https://pdfops.dev/api/mcp and
a free key from /docs/signup work
without any of this. The header check itself is not limited to
tools/list or tools/call: under the
modern protocol revision the server checks the header against the
body for any method call that carries an id, including
server/discover. Sending
MCP-Protocol-Version on every call, matched to
whatever
_meta["io.modelcontextprotocol/protocolVersion"]
says, clears the header-mismatch error above. An unsupported
version, malformed JSON, or a batched array still get rejected
regardless, since those are checked before the header comparison
runs.