Chapter 3 of 9 · Model Context Protocol
JSON-RPC, the wire that carries it
every MCP message is JSON-RPC 2.0. four fields are the whole story.
Two messages on the wire. One has an id. One doesn’t. Read both, then guess which one expects a response. Most readers don’t notice the difference on a first pass — and that one missed field is the lesson.
MCP didn’t invent a wire. It picked a calm one.
JSON-RPC predates MCP by 15+ years. Ethereum nodes speak it. WebSocket libraries lean on it. Half the desktop applications you ran in 2014 had a JSON-RPC server somewhere in their guts. When the designers of MCP sat down to pick a wire format, they didn’t reach for novelty— they reached for something the rest of the industry already understood.
The trade was deliberate. JSON-RPC has no schema language, no streams, no codegen, no batching anyone uses in practice. What it does have is four fields and a pairing rule. That’s the whole protocol. If you can read those four fields by sight, you can read every message in the rest of this course.
The four fields
Every message MCP puts on the wire is a JSON object with at most four keys. jsonrpc says which version of the wire format this is; it’s always the literal string "2.0". id says which conversation this message belongs to. method says what the sender is asking for. params carries the inputs — or, on the way back, the response uses result for success and error for failure.
Hover the keys below to read what each one does.
Four fields. Two outcomes per response. That’s the entire surface area— everything else is a method name and the shape of its params.
Request and response, paired by id
A request goes out with an id; a response comes back carrying the same id. That’s the pairing rule, and it’s the reason MCP can run many requests in flight on a single connection without confusing them. The client picks the id, the server echoes it — the two sides never have to agree on order, only on identity.
Here’s the four-message handshake from the spec, verbatim. Read it once; we’ll come back to it when we open the sandbox.
// 1. client → server
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2025-06-18",
"capabilities": { "elicitation": {} },
"clientInfo": { "name": "demo-client", "version": "0.1.0" } } }
// 2. server → client
{ "jsonrpc": "2.0", "id": 1,
"result": { "protocolVersion": "2025-06-18",
"capabilities": { "tools": {}, "resources": {} },
"serverInfo": { "name": "demo-server", "version": "0.1.0" } } }
// 3. client → server (no id — it's a notification)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
// 4. client → server
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }Messages 1 and 2 are paired by id: 1. Message 4 is a fresh request waiting on its own response (which would carry id: 2). Message 3 has no id, and that means something specific.
The one without an id
A message without an id is a notification. The receiver acts on it; the receiver does not reply. Notifications are how MCP carries events — the tool list changed, cancel that long-running call, I’m initialized and ready. There’s no id because there’s no response to pair with.
Toggle the widget below. Same shape on the wire, one missing field; watch the right pane go quiet.
The round-trip count only ticks on requests. Notifications are fire-and-forget— the wire goes one way, and the sender moves on.
The wire, in your hands
Reading is one thing. Editing is another. The widget below puts a live JSON-RPC request in the left pane and a deterministic server stub in the right. The reader edits the request — the response shape changes as you type. There’s no network here; the stub is a small switch on method that produces the same response a real server would.
From here on, the chapter pivots. We’re not going to describethe protocol any more — we’re going to ask you to do things with it. Try editing the method from tools/list to tools/cull and watch the response come back as a -32601. Delete the closing brace and watch the wire flunk before the server even sees the message. Drop the idfield and watch the response pane go quiet — you’ve just turned a request into a notification.
The four codes a server can refuse with
When something is wrong, JSON-RPC has a small dictionary of negative numbers it speaks. You’ll see the same four codes in every MCP debugger, in every error log, in every spec page from now on.
-32700 is a parse error— the bytes weren’t valid JSON. -32600 is invalid request— the JSON parsed, but the shape wasn’t a JSON-RPC message. -32601 is method not found— the shape was right, but the server doesn’t expose that method. -32602 is invalid params— the method exists, the arguments don’t match.
// parse error — the bytes weren't valid JSON
{ "jsonrpc": "2.0", "id": null,
"error": { "code": -32700, "message": "Parse error" } }
// invalid request — JSON parsed, but the shape was wrong
{ "jsonrpc": "2.0", "id": null,
"error": { "code": -32600, "message": "Invalid Request" } }
// method not found — the server doesn't expose that method
{ "jsonrpc": "2.0", "id": 4,
"error": { "code": -32601, "message": "Method not found" } }
// invalid params — the method exists, the arguments don't match
{ "jsonrpc": "2.0", "id": 5,
"error": { "code": -32602, "message": "Invalid params" } }The sandbox above will produce each of these on demand. Mangle the JSON for -32700. Drop jsonrpc for -32600. Rename the method for -32601.
What it isn’t
It’s not REST. There are no resources, no verbs, no path templates. It’s not GraphQL — the client doesn’t shape its query, the server names the operation. It’s not gRPC either; no protobuf, no HTTP/2 multiplexing required. It’s stateful— the connection carries context, and that context is what makes the next chapter’s handshake even necessary.
One last read
Here’s a message from a real MCP session:
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": { "requestId": 42, "reason": "user pressed escape" }
}The server processes it and... what? The answer falls out of the rules you already have. There’s no id, so this is a notification — the server acts on it but doesn’t reply. The server cancels the request whose id was 42 (probably a long-running tool call), and the wire stays quiet. No response. No error. No round-trip.
What’s next
Four fields can carry anything. So whatdoes MCP carry across them — and how do two strangers, fresh to a session, agree on what’s in scope? That’s the handshake, and it’s the next chapter: The handshake.