Every single MCP message — every tool call, every resource fetch, every handshake — is a JSON-RPC 2.0 object. Before you write your first server, you must understand this protocol at the byte level. Today you'll dissect every field, trace the full initialization handshake, map all 15+ MCP methods, and understand why this 2010 spec was the perfect foundation for AI integration.
JSON-RPC 2.0 is a stateless, lightweight remote procedure call (RPC) protocol published in 2010. It uses JSON as its data format and is transport-agnostic — meaning it doesn't care whether messages travel over TCP sockets, HTTP, pipes, or WebSockets. The entire specification fits on two printed pages.
Anthropic chose JSON-RPC 2.0 as MCP's wire format for four reasons:
Think of JSON-RPC 2.0 as a standardised government postal form. Every letter must have: a tracking number (the id), the action you're requesting (the method), the details of your request (the params), and it must declare it follows the standard (the jsonrpc: "2.0" header).
When the post office responds, they put the same tracking number on the reply (so you know which request it answers), and either write result (approved) or error (rejected with a reason code). No ambiguity. No confusion. Pure structure.
The beauty: any post office in any country can process these letters because the format is universal. That's why JSON-RPC 2.0 made MCP language-agnostic — a Python server and a TypeScript client speak the same letter format.
The spec deliberately leaves out things like authentication, state management, and session handling — MCP adds its own conventions on top for those. JSON-RPC 2.0 is the minimal, stable bedrock. Understanding it means understanding every MCP interaction at the lowest level.
gRPC requires Protobuf schemas and binary encoding — harder to debug, not human-readable. GraphQL is query-focused, not RPC-focused. REST lacks a standard call-and-response pattern for remote functions. JSON-RPC 2.0 is minimal, readable, language-agnostic, and maps perfectly to the "call a function, get a result" model that MCP needs.
Every JSON-RPC 2.0 message — whether it's a request, a response, or a notification — is built from the same small set of fields. Let's dissect each one with surgical precision.
tools/call or resources/list. MCP defines exactly which methods are valid.tools/call, this is an array of content blocks. A response has EITHER result OR error — never both.result. Never have both in one message.result OR error — but never both, never neither. A message with both fields present is malformed and MUST be rejected. This mutual exclusivity is the clearest signal of success vs. failure in the entire protocol.
JSON-RPC 2.0 defines exactly four message types. Every MCP interaction is a combination of these four. Know them well — the distinction between a Request and a Notification will save you hours of debugging.
Sent by either Client or Server. Requires a response. The id field links the response back to this request. The sender waits (or polls) for the corresponding response before considering the operation complete.
Always sent in reply to a Request. Echoes the same id. Contains either result (success) or error (failure). You must send exactly one Response per Request — no more, no less.
A special case of Response where the operation failed. Uses the error field instead of result. The code is a signed integer — standard codes are in the -32768 to -32000 range. MCP defines additional codes.
A Request without an id. The sender does NOT expect a response and the recipient MUST NOT send one. Used for events: resource changes, progress updates, logging. Critical for real-time server-push scenarios.
If you accidentally send a Response to a Notification, you've violated the spec. The recipient wasn't expecting a response and has no logic to handle it — causing subtle bugs or connection resets. Always check: does this incoming message have an id? If not, it's a Notification. Do NOT reply.
Before any tool can be called or any resource fetched, the Client and Server must complete a three-step initialization handshake. This handshake is mandatory — it negotiates capabilities, confirms protocol versions, and establishes the session. Skipping or getting it wrong means everything else fails silently.
initialize Requestinitialize request. It tells the Server: "I speak this version of MCP, and here are the capabilities I support." This is the Client's capability advertisement.initialize resultinitialized Notificationid — no response expected). After the Server receives this, the session is fully open. Both sides may now exchange any MCP method calls freely.The capabilities exchange is not just metadata — it's a binding contract. If a Server doesn't advertise "resources": {} in step 2, the Client MUST NOT send resources/list requests. If a Client doesn't advertise "sampling": {} in step 1, the Server MUST NOT send sampling/createMessage requests. Always check capabilities before calling methods.
MCP defines a precise set of method names. Every method follows the namespace/action naming convention. Here's the complete map — save this as your daily reference for the next 30 days.
| Method | Direction | Type | Description |
|---|---|---|---|
| initialize | C→S | Request | Opens session; exchanges capabilities and versions |
| notifications/initialized | C→S | Notification | Client confirms handshake complete; session is now open |
| ping | Both | Request | Health check — other side must respond with empty result |
| Method | Direction | Type | Description |
|---|---|---|---|
| tools/list | C→S | Request | Returns all tools the server exposes (name, description, inputSchema) |
| tools/call | C→S | Request | Execute a tool by name with given arguments; returns content array |
| notifications/tools/list_changed | S→C | Notification | Server tells client that available tools have changed; client should re-call tools/list |
| Method | Direction | Type | Description |
|---|---|---|---|
| resources/list | C→S | Request | List all available resources (paginated via cursor) |
| resources/read | C→S | Request | Fetch resource contents by URI |
| resources/templates/list | C→S | Request | List URI templates for dynamic resources |
| resources/subscribe | C→S | Request | Subscribe to change notifications for a specific resource URI |
| resources/unsubscribe | C→S | Request | Unsubscribe from a resource's change notifications |
| notifications/resources/updated | S→C | Notification | A subscribed resource has changed; client should re-fetch |
| notifications/resources/list_changed | S→C | Notification | The available resource list has changed |
| Method | Direction | Type | Description |
|---|---|---|---|
| prompts/list | C→S | Request | List all prompt templates (name, description, arguments) |
| prompts/get | C→S | Request | Retrieve a prompt template, resolved with given argument values |
| notifications/prompts/list_changed | S→C | Notification | Available prompt templates have changed |
| Method | Direction | Type | Description |
|---|---|---|---|
| logging/setLevel | C→S | Request | Set server log verbosity: debug, info, warning, error |
| notifications/message | S→C | Notification | Server emits a structured log message to the client |
| sampling/createMessage | S→C | Request | Server asks the client to generate an LLM completion (agentic loop) |
| roots/list | S→C | Request | Server queries the client for workspace root URIs |
| notifications/roots/list_changed | C→S | Notification | Client tells server the workspace roots have changed |
| $/cancelRequest | Both | Notification | Cancel a pending in-flight request by its id |
| $/progress | Both | Notification | Report progress on a long-running operation |
All MCP method names use a category/verb pattern. The $ prefix (like $/cancelRequest) indicates a protocol-level meta operation. The notifications/ prefix always signals a fire-and-forget notification. This consistent naming makes MCP methods instantly recognisable once you learn the pattern.
Error codes are the diagnostic language of MCP. When something goes wrong — and it will — the error code is your first clue. The codes divide into two groups: standard JSON-RPC codes that any JSON-RPC system uses, and MCP-specific codes for AI-integration-specific failures.
-32768 to -32000 — protocol-level issues
-32000 to -32099 — MCP-specific issues
jsonrpc or method, or wrong value types.jsonrpc: "2.0" is present and method is a string.inputSchema before calling. Include field name in error.data.isError: true in tool content for recoverable errors.prompts/list first to check available prompts before calling prompts/get.When a tool is called correctly but the underlying action fails (e.g., GitHub API returns 404), you have two choices:
(1) Return a JSON-RPC error (code -32002) — the AI sees a protocol-level failure and may not retry gracefully.
(2) Return a success response with isError: true in the content — the AI sees it as tool output and can reason about the failure, retry, or take an alternative action.
Option 2 is the MCP-recommended pattern for recoverable tool failures.
JSON-RPC 2.0 is transport-agnostic, but MCP defines two official transports. Understanding the difference is essential — you'll choose one on Day 8 when you build your first server, and your choice affects architecture, security, and deployment.
The MCP spec is evolving toward a new Streamable HTTP transport that replaces the SSE model with a unified bidirectional streaming approach. It supports connection resumption, lower overhead, and is better suited for high-frequency tool interactions. We cover this in Week 5 (Day 31). For now, learn Stdio first — it's what you'll use for Days 1–30.
These are the exact mistakes engineers make when working with JSON-RPC 2.0 and MCP for the first time. Learn them now so you recognise them instantly when they happen in your code.
id (i.e., a Notification). This violates the spec and confuses the sender.id before sending a response.id for two in-flight requests. The client can't tell which response belongs to which request.result and error in the same response object. This is explicitly forbidden by the JSON-RPC 2.0 spec.result, failure gets error. Never both.jsonrpc: "1.0", jsonrpc: 2, or omitting the field. Recipients validate this strictly — all three cause a -32600 error."2.0". Not a number. Not 1.0. Always "2.0".tools/list or tools/call before the initialization handshake (Steps 1–3) is complete. The server will reject them.notifications/initialized confirmation before sending any operational requests.resources/subscribe when the server didn't include "resources": {"subscribe": true} in its capabilities. Guaranteed -32601.id field. What type is it, and what MUST the recipient do?id is a Notification. The recipient processes it but MUST NOT send any response — the sender isn't waiting for one and has no handler for it.initialize, the client sends notifications/initialized (a Notification — no id). This is the final step. Only after the server receives this notification is the session fully operational.isError: true for recoverable execution failures. This lets the AI model see the error as tool output, reason about it, and potentially take alternative actions — much more graceful than a protocol-level error.-32601. What does this mean?method field in your request references a method that doesn't exist on the server. First check: did the server advertise the relevant capability? Second check: is your method name spelled correctly?