# 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
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/developers/mcp-errors/

## The codes

| Code | HTTP | Cause | What to do |
|---|---|---|---|
| `-32700` | `400` | The body is not valid JSON | Fix the serialiser. Retrying sends the same broken bytes |
| `-32600` | `403` | A present `Origin` header is not recognised | Send no `Origin` from a non-browser client, or call from an allowed origin |
| `-32600` | `413` | The request body is over 256 KiB | Split the call. No documentation tool needs an argument that large |
| `-32600` | `400` | A JSON-RPC batch was sent | Send one message per request; batches are refused outright |
| `-32600` | `400` | An unsupported protocol version was asked for | Read `error.data.supported` and pick one of the four listed revisions |
| `-32020` | `400` | A header disagrees with the request body | Make the header match, or send neither |
| `-32601` | `404` | The JSON-RPC method does not exist | Check the spelling against `tools/list`, `resources/list`, `prompts/list` |
| `-32601` | `200` | The tool, resource or prompt named does not exist | List them and use a real name; the method itself was valid |
| `-32602` | `200` | `params` or `arguments` is not an object | Send an object, not an array or a string |
| `-32603` | `429` | Over 120 requests in the last minute | Back off, then retry with a widening delay |
| `-32603` | `200` | The server could not complete the request | Retry 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.

```json
{"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.

## Related

- [The Connect MCP server](https://connectbyjbrh.com/developers/mcp-server/)
- [Public MCP tools](https://connectbyjbrh.com/developers/mcp-public-tools/)
- [Using Connect from any MCP client](https://connectbyjbrh.com/developers/mcp-client-generic/)
- [Designing a resilient client](https://connectbyjbrh.com/developers/errors-and-retries/)
- [JSON-RPC 2.0](https://connectbyjbrh.com/docs/protocols/json-rpc/)

## What this page is based on

- `backend/app/mcp_server.py` — _rpc_error, mcp_endpoint, _handle
- JSON-RPC 2.0 — https://www.jsonrpc.org/specification
- MCP specification 2026-07-28 — https://modelcontextprotocol.io/specification/2026-07-28
