Connect by JBRH Open Connect

MCP error shapes

The server returns standard JSON-RPC error objects, plus one MCP-specific code, -32020, for a header that disagrees with the body. The HTTP status carries information too: a transport problem gets a real 4xx, while a problem inside a valid call — an unknown tool, say — comes back as 200 with an error object in it.

Status
Available What this means
Audience
developer
Last verified
Product version
6.3.2

The codes#

CodeHTTPCauseWhat to do
-32700400The body is not valid JSONFix the serialiser. Retrying sends the same broken bytes
-32600403A present Origin header is not recognisedSend no Origin from a non-browser client, or call from an allowed origin
-32600413The request body is over 256 KiBSplit the call. No documentation tool needs an argument that large
-32600400A JSON-RPC batch was sentSend one message per request; batches are refused outright
-32600400An unsupported protocol version was asked forRead error.data.supported and pick one of the four listed revisions
-32020400A header disagrees with the request bodyMake the header match, or send neither
-32601404The JSON-RPC method does not existCheck the spelling against tools/list, resources/list, prompts/list
-32601200The tool, resource or prompt named does not existList them and use a real name; the method itself was valid
-32602200params or arguments is not an objectSend an object, not an array or a string
-32603429Over 120 requests in the last minuteBack off, then retry with a widening delay
-32603200The server could not complete the requestRetry once; if it persists the fault is not yours

Why an unknown tool is an HTTP 200#

The split is deliberate and it is the one most likely to catch a client out. 404 means *this JSON-RPC method is not a method here*. Naming a tool that does not exist is not that: tools/call is a perfectly valid method, the request reached the dispatcher and was understood, and the answer is an error object at HTTP 200. The same is true for resources/read with an unknown URI and prompts/get with an unknown name.

A client that treats HTTP status as the whole answer will therefore record a call to a hallucinated tool as a success. Parse the body: a response containing error is a failure whatever the status line says.

Two failures that are not protocol errors#

First, a tool that runs and cannot answer returns a *result*, not an error object: content carrying a sentence, and isError: true. A missing query, or a page reference matching nothing, arrives this way. That is the MCP convention, and it exists so a model reads the explanation and corrects itself instead of the client aborting the turn.

{"jsonrpc": "2.0", "id": 12, "result": {
  "content": [{"type": "text",
               "text": "No public page for '/docs/nope/'. Use search_public_docs first."}],
  "isError": true}}

Second, a notification — a message with no id — is answered with HTTP 202 and an empty body. There is no result and no error to read. A client that waits for a response to a notification waits forever, which looks exactly like a hung server and is not one.

The header-mismatch code#

-32020 fires in three situations, all of them the same underlying problem: MCP-Protocol-Version disagreeing with the version in params._meta, Mcp-Method disagreeing with the body's method, or Mcp-Name disagreeing with the body's name or uri. The Mcp-Name header may be Base64-wrapped as =?base64?…?=, and is decoded before the comparison, so an encoded header and a plain body still match.

The reason the specification asks for this check, and the reason it is enforced here rather than logged, is that infrastructure between client and server routes on headers while the server executes on the body. If the two can disagree, a proxy and an application can be looking at different requests. Sending no optional headers at all is a valid and simpler choice.

Questions#

Should I retry a -32603 at HTTP 200?

Once, after a short pause. It means the server caught something unexpected while handling a valid request; the message is deliberately generic and carries no internal detail. If it repeats with the same arguments, the problem is not in your client.

How do I tell a rate limit from a server fault? Both are -32603.

By the HTTP status: 429 is the limit, 200 is the fault. This is one place where the transport status is the more informative half of the answer, which is why a client should keep both.

Does the server ever return a partial result?

No. A call either returns a complete result object or an error. There is no streaming, no chunked tool output and no resumable stream — the transport removed those, so there is no half-delivered state to reconcile.