JSON-RPC 2.0 in depth
Why this matters
Every MCP message is JSON-RPC 2.0, so this is the floor everything else in the phase stands on. The spec fits on one page, but the details decide whether a server works with every client or only with yours. Notifications get no answer, ids must match, and a parse error has no id. Bugs here show up as hangs and mysterious timeouts in someone else's app, which makes them painful to debug.
- Tell requests, results, errors and notifications apart, and know which get answers
- Use the standard error codes correctly, from -32700 to -32603
- Follow MCP's extra rules on ids, params and batches
- Build a JSON-RPC dispatcher that never crashes and never leaks internals
Four kinds of message
You met JSON-RPC briefly in Phase 1. Here's the full picture, and a busy restaurant is a good way to see it.
A waiter writes an order on a numbered ticket. The kitchen sends back a dish with that number, or a note saying "we're out of that", also with the number on it. Now and then someone calls out "table six is leaving", and nobody answers, because nobody asked anything.
JSON-RPC works the same way. Every message is a JSON object with "jsonrpc": "2.0", and there are exactly four shapes:
| message | has id? | has method? | has result or error? | gets a reply? |
|---|---|---|---|---|
| request | yes | yes | no | yes, exactly one |
| notification | no | yes | no | never |
| result response | yes (same as the request) | no | result | no |
| error response | yes (or null if unknown) | no | error | no |
{"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "get_campaign", "arguments": {"id": "cmp_103"}}}
{"jsonrpc": "2.0", "id": 7, "result": {"resultType": "complete", "content": [{"type": "text", "text": "..."}]}}
{"jsonrpc": "2.0", "id": 8, "error": {"code": -32601, "message": "Method not found: tools/delete"}}
{"jsonrpc": "2.0", "method": "notifications/progress", "params": {"progressToken": "t1", "progress": 40, "total": 100}}
The receiver tells them apart by which keys are present. A message with method and id is a request. With method and no id, it's a notification, and the receiver must not reply, not even with an error. Answering a notification is a classic bug: the other side gets a response for an id it never sent.
The id is how answers find their questions. Like tickets in a kitchen, a client can have many requests in flight at once, over one connection, and replies can come back in any order. So the id is the only link.
In plain words: an id means "someone is waiting for an answer to this". No id means nobody is, so stay quiet.