📅 Day 2 🌱 Beginner 🔌 Wire Protocol

JSON-RPC 2.0 — The Wire That Carries Everything

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.

Imagine two people who speak different languages trying to collaborate. They can't until they agree on a common vocabulary and sentence structure — a grammar. JSON-RPC 2.0 is the universal grammar that MCP Clients and Servers use. Once both sides speak it fluently, the conversation flows perfectly. Today, you learn the grammar cold.
// Table of Contents
01 — Foundation

What is JSON-RPC 2.0?

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:

2010
Spec published — battle-tested
4
Message types in the whole spec
100%
Human-readable JSON
Any
Transport / language
📮

Analogy: JSON-RPC as a Formal Letter System

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.

📌
Why Not gRPC or GraphQL?

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.

02 — Anatomy

Anatomy of Every Message

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.

// Request Message — Field by Field
CLIENT → SERVER  ·  tools/call request
{
  "jsonrpc": "2.0",
  "id": "req-7f3a",
  "method": "tools/call",
  "params": {
    "name": "search_github",
    "arguments": {
      "query": "open bugs",
      "label": "bug"
    }
  }
}
jsonrpc
Always exactly "2.0" — declares the spec version. If this field is missing or wrong, the recipient MUST reject the message with a -32600 error.
id
Correlation ID — string, number, or null. The response echoes this exact value so you know which request it answers. Must be unique per session.
method
The RPC method name — a dot-namespace string like tools/call or resources/list. MCP defines exactly which methods are valid.
params
Optional arguments — a JSON object (named params) or array (positional params). MCP always uses named params (objects).
// Success Response — Field by Field
SERVER → CLIENT  ·  tools/call response
{
  "jsonrpc": "2.0",
  "id": "req-7f3a",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Found 7 issues..."
      }
    ],
    "isError": false
  }
}
jsonrpc
Always "2.0" — even in responses. Never omit it.
id
Must exactly match the request's id. This is how the client correlates async responses.
result
Present on success. Contains whatever data the method returns. For tools/call, this is an array of content blocks. A response has EITHER result OR error — never both.
// Error Response — Field by Field
SERVER → CLIENT  ·  error response
{
  "jsonrpc": "2.0",
  "id": "req-7f3a",
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "field": "query",
      "issue": "required"
    }
  }
}
error
Present on failure. Replaces result. Never have both in one message.
code
Integer error code. Standard range: -32768 to -32000. MCP-specific codes: -32001 and below. See Section 6 for the full map.
message
Human-readable short description. Keep it concise — one sentence.
data
Optional extra detail. Any JSON value. Use for field-level validation errors, stack traces in dev, or retry hints.
🔑 The Golden Rule: A JSON-RPC 2.0 response MUST contain either 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.
03 — Message Types

The 4 Message Types Explained

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.

📤
Request
Has id · Expects a response · Bidirectional

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.

// Client → Server request
{
  jsonrpc: "2.0",
  id: "1",
  method: "tools/list",
  params: {}
}
📥
Response
Has id · Answers a Request · result or error

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.

// Server → Client response
{
  jsonrpc: "2.0",
  id: "1",
  result: {
    tools: [...]
  }
}
Error Response
Has id · Failure variant · Numeric code

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.

// Error response
{
  jsonrpc: "2.0",
  id: "1",
  error: {
    code: -32601,
    message: "Method not found"
  }
}
🔔
Notification
No id · Fire-and-forget · No response expected

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.

// Server → Client notification
{
  jsonrpc: "2.0",
  // no "id" field!
  method: "notifications/tools/list_changed",
  params: {}
}
⚠️
The Notification Trap — Most Common Day-2 Mistake

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.

04 — Initialization

The MCP Handshake — How Sessions Begin

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.

MCP Initialization Handshake — Complete Flow
🔌
MCP CLIENT
Inside Host App
⚙️
MCP SERVER
Standalone Process
initialize →
protocolVersion, capabilities,
clientInfo
STEP 1
STEP 2
← initialize result
protocolVersion, capabilities,
serverInfo, instructions
initialized →
(notification — no params)
Session is now OPEN
STEP 3
✅ SESSION ESTABLISHED — Normal MCP operations may now begin
Client → Server (Request/Notification)
Server → Client (Response/Notification)
1
Client sends initialize Request
The Client opens the connection and immediately sends an initialize 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.
JSON-RPC 2.0step-1-initialize-request.json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      // What the CLIENT supports (for server→client calls)
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": {
      "name": "Claude Desktop",
      "version": "1.0.0"
    }
  }
}
2
Server replies with its initialize result
The Server responds, declaring the MCP version it will use, which capabilities it exposes (tools, resources, prompts, subscriptions), and optional instructions that the AI model should follow when using this server.
JSON-RPC 2.0step-2-initialize-response.json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      // What the SERVER exposes
      "tools": { "listChanged": true },
      "resources": {
        "subscribe": true,
        "listChanged": true
      },
      "prompts": { "listChanged": true },
      "logging": {}
    },
    "serverInfo": {
      "name": "my-github-server",
      "version": "1.2.0"
    },
    "instructions": "Always confirm destructive actions..."
  }
}
3
Client sends initialized Notification
The Client acknowledges completion with a Notification (no id — no response expected). After the Server receives this, the session is fully open. Both sides may now exchange any MCP method calls freely.
JSON-RPC 2.0step-3-initialized-notification.json
{
  "jsonrpc": "2.0",
  // Notice: NO "id" field — this is a Notification
  "method": "notifications/initialized",
  "params": {}
}

// ✅ From this point on, normal operations begin:
// tools/list, resources/list, tools/call, etc.
🔑
Capability Negotiation — Why It Matters

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.

05 — Methods Reference

All MCP Methods — Complete Reference

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.

📋 MCP Methods Explorer Interactive
Lifecycle Methods
MethodDirectionTypeDescription
initializeC→SRequestOpens session; exchanges capabilities and versions
notifications/initializedC→SNotificationClient confirms handshake complete; session is now open
pingBothRequestHealth check — other side must respond with empty result
Tools Methods
MethodDirectionTypeDescription
tools/listC→SRequestReturns all tools the server exposes (name, description, inputSchema)
tools/callC→SRequestExecute a tool by name with given arguments; returns content array
notifications/tools/list_changedS→CNotificationServer tells client that available tools have changed; client should re-call tools/list
Resources Methods
MethodDirectionTypeDescription
resources/listC→SRequestList all available resources (paginated via cursor)
resources/readC→SRequestFetch resource contents by URI
resources/templates/listC→SRequestList URI templates for dynamic resources
resources/subscribeC→SRequestSubscribe to change notifications for a specific resource URI
resources/unsubscribeC→SRequestUnsubscribe from a resource's change notifications
notifications/resources/updatedS→CNotificationA subscribed resource has changed; client should re-fetch
notifications/resources/list_changedS→CNotificationThe available resource list has changed
Prompts Methods
MethodDirectionTypeDescription
prompts/listC→SRequestList all prompt templates (name, description, arguments)
prompts/getC→SRequestRetrieve a prompt template, resolved with given argument values
notifications/prompts/list_changedS→CNotificationAvailable prompt templates have changed
Utility Methods
MethodDirectionTypeDescription
logging/setLevelC→SRequestSet server log verbosity: debug, info, warning, error
notifications/messageS→CNotificationServer emits a structured log message to the client
sampling/createMessageS→CRequestServer asks the client to generate an LLM completion (agentic loop)
roots/listS→CRequestServer queries the client for workspace root URIs
notifications/roots/list_changedC→SNotificationClient tells server the workspace roots have changed
$/cancelRequestBothNotificationCancel a pending in-flight request by its id
$/progressBothNotificationReport progress on a long-running operation
💡
The Namespace Pattern

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.

06 — Error Codes

Error Codes Deep Dive

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.

Standard JSON-RPC codes: -32768 to -32000 — protocol-level issues
Application codes: -32000 to -32099 — MCP-specific issues
-32700
Parse Error
The received JSON is syntactically invalid. The server could not parse it at all — malformed braces, invalid escape sequences, truncated message.
💡 Fix: Check for incomplete writes, encoding issues, or premature connection close.
-32600
Invalid Request
The JSON is valid but doesn't conform to JSON-RPC 2.0 — missing required fields like jsonrpc or method, or wrong value types.
💡 Fix: Ensure jsonrpc: "2.0" is present and method is a string.
-32601
Method Not Found
The method in the request doesn't exist on this server — either a typo, an unsupported MCP method, or calling a method the server didn't advertise in capabilities.
💡 Fix: Check capabilities negotiation first. Verify exact method name spelling.
-32602
Invalid Params
The method exists but the parameters don't match the expected schema — wrong type, missing required field, extra unexpected field.
💡 Fix: Validate against the tool's inputSchema before calling. Include field name in error.data.
-32603
Internal Error
The server encountered an unexpected internal error during processing — unhandled exception, null reference, or dependency failure.
💡 Fix: Log the full stack trace server-side. Return sanitised messages to client (no stack traces in prod).
-32001
Resource Not Found
MCP-specific: The requested resource URI does not exist or is no longer available. The URI was valid format but points to nothing.
💡 Fix: Check if resource was deleted between list and read calls. Implement graceful staleness handling.
-32002
Tool Execution Failed
MCP-specific: The tool's handler function threw an exception or returned an error state. The tool was called correctly but failed during execution.
💡 Fix: Distinguish tool execution errors from protocol errors — use isError: true in tool content for recoverable errors.
-32003
Prompt Not Found
MCP-specific: The requested prompt template name doesn't exist on this server.
💡 Fix: Call prompts/list first to check available prompts before calling prompts/get.
🧩
Tool Error vs. Protocol Error — Critical Distinction

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.

07 — Transports

Transport Layer — How Messages Travel

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.

📟
Stdio Transport
Local · Simple · Default for development
MCP messages sent over stdin / stdout pipes between processes
Server runs as a child process spawned by the Host
One connection per process — single client only
Messages are newline-delimited JSON (NDJSON)
No networking — zero authentication needed
Server dies when Host closes — automatic cleanup
✅ Best for: local tools, filesystem access, IDE plugins, development
🌐
HTTP + SSE Transport
Remote · Scalable · Auth required
Client sends requests via HTTP POST to server endpoint
Server sends responses/notifications via Server-Sent Events (SSE)
Supports multiple concurrent clients
Server runs as a standalone web service (Docker, cloud, etc.)
Requires authentication (API key, OAuth 2.0)
Survives Host restarts — persistent server lifecycle
✅ Best for: shared team servers, cloud APIs, production deployments
graph LR subgraph STDIO ["📟 Stdio Transport"] direction LR H1["🖥️ Host Process"] -->|spawn| S1["⚙️ Server Process"] H1 -->|stdin: JSON messages| S1 S1 -->|stdout: JSON responses| H1 end subgraph HTTP ["🌐 HTTP + SSE Transport"] direction LR H2["🖥️ Host / Client"] -->|HTTP POST /message| S2["⚙️ Remote Server"] S2 -->|GET /sse stream| H2 H3["🖥️ Another Client"] -->|HTTP POST /message| S2 end style STDIO fill:#1e1040,stroke:#8b5cf6,stroke-width:1px style HTTP fill:#062535,stroke:#06b6d4,stroke-width:1px
🚀
Upcoming: Streamable HTTP (Next-Gen Transport)

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.

08 — Common Gotchas

Common Gotchas — Save Your Future Self Hours

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.

💥
Responding to a Notification
Sending a response to a message that has no id (i.e., a Notification). This violates the spec and confuses the sender.
✅ Fix: Always check for the presence of id before sending a response.
💥
Duplicate Request IDs
Reusing the same id for two in-flight requests. The client can't tell which response belongs to which request.
✅ Fix: Use a UUID or monotonic counter. Never reuse an id while a request is pending.
💥
Both result AND error in Response
Including both result and error in the same response object. This is explicitly forbidden by the JSON-RPC 2.0 spec.
✅ Fix: Mutually exclusive — success gets result, failure gets error. Never both.
💥
Wrong jsonrpc Version
Setting jsonrpc: "1.0", jsonrpc: 2, or omitting the field. Recipients validate this strictly — all three cause a -32600 error.
✅ Fix: Always use the exact string "2.0". Not a number. Not 1.0. Always "2.0".
💥
Calling Methods Before Handshake
Sending tools/list or tools/call before the initialization handshake (Steps 1–3) is complete. The server will reject them.
✅ Fix: Always wait for the notifications/initialized confirmation before sending any operational requests.
💥
Calling Unadvertised Capabilities
Calling resources/subscribe when the server didn't include "resources": {"subscribe": true} in its capabilities. Guaranteed -32601.
✅ Fix: Always inspect the capabilities object from the initialize response before calling feature-gated methods.
📝 Knowledge Check — Day 2
5 questions on JSON-RPC 2.0 and the MCP handshake. Select your answer then hit Check.
Question 1 of 5
A JSON-RPC 2.0 message has no id field. What type is it, and what MUST the recipient do?
A It's a malformed Request — the recipient should return a -32600 error
B It's a Notification — the recipient MUST NOT send any response
C It's a Response — the id is optional for one-way responses
D It's a Batch — the recipient processes it as a group request
Notification (B). In JSON-RPC 2.0, a message without an 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.
Question 2 of 5
In the MCP initialization handshake, which message confirms that the session is fully open and operational?
A The Server's initialize response result
B The Client's first tools/list request
C The Client's notifications/initialized notification
D A separate session/open confirmation message
notifications/initialized (C). After the server responds to 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.
Question 3 of 5
A GitHub MCP server's tool throws an exception during execution (GitHub API returned 500). What is the MCP-recommended way to handle this?
A Send a JSON-RPC error response with code -32603 (Internal Error)
B Send a success result with isError: true in the content array
C Close the connection and restart the server
D Return an empty result and log the error silently
isError: true (B). The MCP spec recommends returning a tool result with 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.
Question 4 of 5
You receive an error with code -32601. What does this mean?
A The JSON could not be parsed — syntax error
B A required parameter is missing or has the wrong type
C The method name in your request doesn't exist on this server
D The resource you requested was not found
Method Not Found (C). Error code -32601 means the 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?
Question 5 of 5
Which transport should you use when building an MCP server that will be shared across a team and deployed to a cloud environment?
A Stdio — it's simpler and more reliable
B HTTP + SSE — it supports multiple clients and remote deployment
C Both are equal — it doesn't matter which you choose
D WebSockets — the preferred MCP transport for production
HTTP + SSE (B). Stdio only supports a single client (one process per connection) and requires the server to run locally as a child process. HTTP+SSE supports multiple concurrent clients, remote deployment, authentication, and persistent server lifecycle — exactly what a shared team server needs.
← Previous
Day 1: What is MCP?
Architecture, primitives, the big picture
Next →
Day 3: The TypeScript SDK
Install, configure, write your first server skeleton