# JSON-RPC 2.0

JSON-RPC 2.0 is a small convention for calling a method by name with JSON arguments and getting a JSON result back. It defines four message shapes and a set of reserved error codes, and nothing else. Both of Connect's agent endpoints — `/mcp` and `/a2a` — speak it.

- **Status:** Reference
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/protocols/json-rpc/

## Four shapes, and that is the whole protocol

- **Request** — `jsonrpc`, `method`, optional `params`, and an `id` the caller chooses. The `id` is how a reply is matched to its question.
- **Response (success)** — `jsonrpc`, the same `id`, and `result`.
- **Response (error)** — `jsonrpc`, the same `id`, and `error` with a `code`, a `message` and optional `data`.
- **Notification** — a request with no `id`. No reply is sent, ever. A caller that waits for one waits forever.

`params` may be an object or an array in the specification. Both of Connect's endpoints require an object and answer `-32602` for anything else, because named parameters survive a schema change and positional ones do not.

What JSON-RPC deliberately leaves out is as important as what it defines: no transport, no authentication, no discovery, no schema for `params`, no versioning of methods. Everything a real protocol needs is layered on top, which is why MCP and A2A can share this envelope and still be different protocols.

## The reserved error codes

| Code | Name | When you see it |
|---|---|---|
| -32700 | Parse error | The body was not valid JSON |
| -32600 | Invalid request | Valid JSON, wrong shape — a batch where one message was expected |
| -32601 | Method not found | The method name is not implemented |
| -32602 | Invalid params | The method exists; the arguments are wrong |
| -32603 | Internal error | The server failed for its own reasons |
| -32000 to -32099 | Server-defined | Reserved for the layer above: MCP's `-32020`, A2A's `-32004` |

The server-defined band is where the protocols above put their own meanings. MCP uses `-32020` when the `MCP-Protocol-Version`, `Mcp-Method` or `Mcp-Name` header disagrees with the body. A2A uses `-32004` for an operation it does not support — a skill that is not on the card, streaming when the card declares none, or the authenticated extended card that does not exist.

One distinction is worth internalising because it decides how a client should react. A JSON-RPC error means the *call* did not happen. A successful result that reports a problem — MCP's `isError: true` — means the call happened and the answer is bad news. The first is retried or fixed; the second is read.

## Does Connect use JSON-RPC?

Yes, on both agent endpoints, and nowhere else. `POST /mcp` and `POST /a2a` are JSON-RPC. The public HTTP API is ordinary REST over JSON and is described by [OpenAPI](/docs/protocols/openapi/) — it is not JSON-RPC and does not use these codes.

Two deliberate narrowings apply to both endpoints. Batching is refused: send one message per request, and a JSON array comes back as `-32600` with `400`. And `params` must be an object.

Connect also maps codes onto HTTP status rather than answering `200` for everything, because the transport in front of an endpoint — a proxy, a gateway, a metrics collector — can only see the status line.

| Situation | HTTP | JSON-RPC code |
|---|---|---|
| Malformed JSON | 400 | -32700 |
| A batch, or the wrong top-level shape | 400 | -32600 |
| Header disagrees with the body (MCP) | 400 | -32020 |
| Unknown method | 404 | -32601 |
| Unknown tool, resource or prompt | 200 | -32601 |
| Bad arguments | 200 | -32602 |
| Body over the size ceiling | 413 | -32600 |
| Rate limit reached | 429 | -32603 |
| A notification | 202 | no body at all |

The split on `-32601` is intentional. An unknown *method* is a routing failure a proxy should be able to see, so it gets `404`. An unknown tool name is an application-level answer to a well-formed call, so it stays `200` and the model reads the message.

## Writing a client that does not surprise you

1. Generate a fresh `id` per request and match replies by it. Reusing one across concurrent calls makes two answers indistinguishable.
2. Treat a missing `id` as intentional. If you send a notification, do not wait; if you receive `202`, that is the whole answer.
3. Read `error.data` when it is present — Connect uses it to list the supported protocol versions when a client asks for one that is not.
4. Do not parse `error.message` for meaning. The code is the contract; the message is for a human reading a log.
5. Retry on `429` with a backoff, and on `-32603` at most once. Never retry `-32602`: the arguments will be just as wrong the second time.

## Questions

### Why does Connect refuse JSON-RPC batches when the specification allows them?

Because a batch makes every limit ambiguous — rate, body size, partial failure — for a saving that matters only to chatty clients. One message per request keeps error handling honest, and neither MCP nor A2A needs batching to work.

### Is the public API JSON-RPC too?

No. [The public API](/developers/public-api/) is REST over JSON with ordinary HTTP status codes and its own error shape. JSON-RPC here is confined to the two agent endpoints.

### What does a code in the -32000 range mean?

It is defined by the protocol above JSON-RPC, not by JSON-RPC. Look it up in MCP's or A2A's specification — here, `-32020` is an MCP header mismatch and `-32004` is an A2A unsupported operation.

## Related

- [Model Context Protocol](https://connectbyjbrh.com/docs/protocols/mcp/)
- [A2A — the Agent2Agent protocol](https://connectbyjbrh.com/docs/protocols/a2a/)
- [MCP error shapes](https://connectbyjbrh.com/developers/mcp-errors/)
- [API error shapes](https://connectbyjbrh.com/developers/api-errors/)
- [Streamable HTTP](https://connectbyjbrh.com/docs/protocols/streamable-http/)

## What this page is based on

- https://www.jsonrpc.org/specification
- `backend/app/mcp_server.py` — the error codes and their HTTP statuses
- `backend/app/a2a_server.py` — UNSUPPORTED_OPERATION and the refusals
- `docs-source/facts.py` — PROTOCOLS['jsonrpc']
