JSON-RPC in Model Context Protocol: How Messages Are Structured Under the Hood
A deep dive into the wire format of the Model Context Protocol: Requests, Responses, Notifications, and the MCP lifecycle.
Mohammed Kafeel
Machine Learning Researcher
On this page
TL;DR
MCP uses JSON-RPC 2.0 as its wire format for every message between AI clients and servers.
There are exactly three MCP message types: Requests, Responses, and Notifications.
MCP adds tighter constraints on top of base JSON-RPC 2.0 - the most important:
idmust never benull, and must be unique per session.Every MCP session starts with a mandatory 3-step handshake before any tools, resources, or prompts can be used.
The JSON-RPC message format is identical across transports - whether you're running over STDIO or HTTP + SSE.
Why MCP Chose JSON-RPC 2.0
JSON-RPC 2.0 solves one problem cleanly: lightweight, transport-agnostic remote procedure calls over plain JSON.
Before MCP, every AI tool integration was bespoke. One SDK spoke REST, another used gRPC, a third invented its own envelope format. Debugging was a nightmare. Interoperability was zero.
MCP needed a standard message format so that any AI client - Claude Desktop, a custom agent, a VS Code extension - could talk to any MCP server without custom adapters. (For a full primer, see our Model Context Protocol overview.) JSON-RPC 2.0 was the right fit because:
It's transport-agnostic - works over STDIO, HTTP, WebSockets, or anything that carries bytes.
It's minimal - the spec fits on a single page.
It's well-understood - tooling, parsers, and debuggers already exist for it.
It defines request/response correlation via
idfields, which is exactly what you need for async agent communication.
The key insight: MCP is JSON-RPC 2.0 plus a specific set of constraints. It doesn't reinvent the wheel. It narrows the spec to make AI agent communication predictable and debuggable.
The Three MCP Message Types
MCP has exactly three message types: Requests, Responses, and Notifications.
You can tell them apart by which fields are present. No guessing, no flags - the shape of the object tells you everything.
Message Type | Has | Has | Has |
|---|---|---|---|
Request | ✅ Yes | ✅ Yes | ❌ No |
Response | ✅ Yes | ❌ No | ✅ Yes |
Notification | ❌ No | ✅ Yes | ❌ No |
1. Requests - What They Look Like
A Request initiates an operation and always expects a Response back. It's the "call" half of a remote procedure call.
Required fields:
jsonrpc- always"2.0", signals the protocol versionid- a string or integer that uniquely identifies this request within the sessionmethod- the operation to invoke (e.g.,tools/list,tools/call,resources/read)params- optional object carrying the method's arguments
Here's a real tools/list request, annotated:
{
"jsonrpc": "2.0", // Required. Always "2.0" - never omit this.
"id": "req-42", // Required. String or integer. MUST be unique in this session.
// MCP constraint: MUST NOT be null (unlike base JSON-RPC).
"method": "tools/list", // Required. The operation the client wants to invoke.
"params": {} // Optional. Empty object is fine for methods with no arguments.
}
MCP constraint on id: Base JSON-RPC 2.0 allows null as an id. MCP explicitly forbids it. Every request id must be a non-null string or integer, and it must not be reused by the same sender within a session. This matters for debugging - you can always trace a response back to exactly one request.
2. Responses - Success and Error
A Response is always sent in reply to a Request. It carries either a result (success) or an error (failure) - never both.
Required fields:
jsonrpc-"2.0"againid- must match theidfrom the original Request exactlyresultORerror- exactly one of these
Successful response:
{
"jsonrpc": "2.0", // Required. Protocol version.
"id": "req-42", // Required. Mirrors the id from the Request.
"result": { // Present on success. NEVER alongside "error".
"tools": [
{
"name": "search_web",
"description": "Search the web for current information"
}
]
}
}
Error response:
{
"jsonrpc": "2.0", // Required. Protocol version.
"id": "req-42", // Required. Same id as the failed Request.
"error": { // Present on failure. NEVER alongside "result".
"code": -32602, // Required integer. Standard JSON-RPC error codes apply.
"message": "Invalid params: 'query' is required", // Required human-readable string.
"data": { // Optional. Any JSON value with extra context.
"field": "query",
"received": null
}
}
}
Standard MCP error codes (inherited from JSON-RPC 2.0):
Code | Meaning |
|---|---|
-32700 | Parse error - invalid JSON |
-32600 | Invalid request |
-32601 | Method not found |
-32602 | Invalid params |
-32603 | Internal error |
-32000 to -32099 | MCP-specific server errors |
3. Notifications - Fire and Forget
A Notification is a one-way message. The receiver must not send a response. There's no id - that's how you know it's a notification.
Required fields:
jsonrpc-"2.0"method- the event nameparams- optional
Here's a logging/message notification from a server:
{
"jsonrpc": "2.0", // Required. Protocol version.
"method": "logging/message", // Required. No id field - this is the key distinction.
"params": {
"level": "info", // Log severity level.
"logger": "tool-runner", // Which component sent this.
"data": "Tool 'search_web' completed in 342ms"
}
}
When do you use notifications?
Progress updates - tell the client a long-running tool is 60% done
Log events - stream server logs to the client in real time
Cancellations - signal that an in-progress operation should stop
List-changed events - notify the client that available tools or resources have changed
Notifications are what make MCP feel responsive. Your agent doesn't have to poll - the server pushes updates as they happen.
The MCP Connection Lifecycle
Every MCP session follows a strict 3-step lifecycle before any real work happens.
You can't call tools/list on a fresh connection. You have to complete the handshake first. Skip it, and the server will reject your messages.
Step 1: Initialize - Negotiate Capabilities
The client sends an initialize Request. This is where both sides declare what they can do. (For how the client, server, and host fit together, see our breakdown of MCP architecture.)
Client → Server:
{
"jsonrpc": "2.0",
"id": "1", // First request in the session.
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26", // The MCP spec version the client targets.
"capabilities": {
"sampling": {}, // Client can handle sampling/createMessage requests.
"roots": {
"listChanged": true // Client can handle roots/list_changed notifications.
}
},
"clientInfo": {
"name": "my-agent",
"version": "1.0.0"
}
}
}
Server → Client (Response):
{
"jsonrpc": "2.0",
"id": "1", // Mirrors the Request id.
"result": {
"protocolVersion": "2025-03-26", // Agreed protocol version.
"capabilities": {
"tools": {}, // Server provides tools.
"resources": {
"subscribe": true, // Server supports resource subscriptions.
"listChanged": true // Server will send resources/list_changed notifications.
},
"logging": {} // Server supports logging.
},
"serverInfo": {
"name": "my-mcp-server",
"version": "2.1.0"
}
}
}
Capability negotiation is the contract. If the server doesn't advertise tools, the client must not send tools/call. If the client doesn't advertise sampling, the server must not send sampling/createMessage. Violating this causes errors that are hard to trace - get the handshake right.
Step 2: Initialized - Client Confirms
The client sends an initialized Notification. No id, no response expected. This tells the server: "I've processed your capabilities. We're good to go."
{
"jsonrpc": "2.0",
"method": "initialized" // No id. This is a notification, not a request.
}
Step 3: Normal Operation
Now both sides can exchange Requests, Responses, and Notifications freely - tools/call, resources/read, prompts/get, progress notifications, log events, everything.
Why session-unique IDs matter for debugging: MCP requires that each sender never reuses a request id within a session. In practice, this means you can dump every message to a log file and reconstruct the full call graph. When something breaks, you match the error Response's id to the original Request and see exactly what was asked and what failed. No guessing.
How MCP Extends JSON-RPC 2.0 - The Key Differences
MCP doesn't just use JSON-RPC 2.0 - it adds specific constraints that make AI agent communication more reliable.
Here's the full comparison:
Feature | JSON-RPC 2.0 Base | MCP Constraint |
|---|---|---|
| String, number, or | Must be string or number - never |
| Not specified | Must be unique per sender per session |
Batching (arrays of requests) | Fully supported | Implementations MUST support receiving batches; MAY support sending them |
Transport | Not specified | STDIO or HTTP + SSE (both defined in spec) |
Session lifecycle | Not specified | Mandatory initialize → initialized → operation sequence |
Encoding | Not specified | UTF-8 JSON only |
Why the batching nuance matters: Base JSON-RPC 2.0 lets you send an array of requests in one go and get an array of responses back. MCP's spec says implementations must be able to receive batches (so your server won't choke if a client sends one) but only may send them. In practice, most MCP clients send requests one at a time. Don't assume batching works both ways until you've tested it.
Transport Layer - Where JSON-RPC Messages Actually Travel
The JSON-RPC message format is identical regardless of transport. That's the whole point of the abstraction.
MCP defines two official transports (we compare the full set of MCP transport options separately):
STDIO - Local Subprocess
The client spawns the MCP server as a child process. Messages flow over stdin and stdout. Each JSON-RPC message is a UTF-8 line terminated by \n.
When to use it:
Local development and testing
CLI tools and IDE extensions (Claude Desktop, VS Code Copilot)
Situations where you control both client and server on the same machine
How it works: Client writes to server's stdin. Server writes responses to stdout. Server logs go to stderr - they're not part of the protocol. Shutdown is simple: close stdin, the server process exits.
HTTP + SSE - Remote and Cloud
The client sends JSON-RPC Requests via HTTP POST. The server can push Responses and Notifications back via Server-Sent Events (SSE).
When to use it:
Remote MCP servers hosted in the cloud
Multi-tenant SaaS architectures
Situations where the server needs to push events to many clients
How it works: Client POSTs to /mcp with Content-Type: application/json. Server responds with either a plain JSON body (for simple responses) or opens an SSE stream (Content-Type: text/event-stream) for streaming and server-initiated messages.
The key point: Whether you're on STDIO or HTTP + SSE, the JSON-RPC envelope - jsonrpc, id, method, params, result, error - is exactly the same. Swap the transport, keep the messages. This is what makes MCP servers portable.
What This Means for Building AI Agents
Understanding MCP's message structure is the difference between debugging for hours and fixing in minutes.
Here are three practical implications that matter when you're actually building:
1. Every tool call is a JSON-RPC Request - you can log and inspect it.
tools/call is just a Request with method: "tools/call" and a params object containing the tool name and arguments. Log every outbound Request and every inbound Response. When your agent calls the wrong tool or passes bad arguments, you'll see it immediately in the params field - no black box.
2. Errors are structured - you always know what failed and why.
MCP error Responses always carry a numeric code, a human-readable message, and an optional data field for extra context. Code -32602 means invalid params. Code -32601 means method not found. You don't have to parse free-text error strings - you can branch on the code in your error handler and surface the right message to the user.
3. Notifications let your agent send real-time updates without blocking.
Long-running tool calls - web searches, database queries, file processing - can emit progress Notifications while they run. Your agent doesn't have to wait for the final Response to show the user something's happening. Send a notification at 25%, 50%, 75%, and the UX feels alive. No polling, no extra endpoints, no custom event system - it's built into the MCP protocol mechanics.
FAQ
What is JSON-RPC 2.0?
JSON-RPC 2.0 is a lightweight remote procedure call protocol that uses JSON as its data format. It defines how to structure requests, responses, and one-way notifications between a client and a server, and it works over any transport that can carry text. The full specification is at jsonrpc.org/specification.
Why does MCP use JSON-RPC 2.0?
MCP needed a standard, transport-agnostic message format so any AI client could talk to any MCP server without custom adapters. JSON-RPC 2.0 is minimal, well-understood, and already has tooling support. MCP builds on it by adding constraints - like non-null id fields and a mandatory session lifecycle - that make AI agent communication more predictable.
What are the three MCP message types?
The three MCP message types are Requests (client or server initiates an operation, expects a response), Responses (reply to a request, carries either result or error), and Notifications (one-way fire-and-forget messages with no id and no response expected). You can identify the type by which fields are present in the JSON object.
What is the difference between an MCP Request and a Notification?
A Request has an id field and always expects a Response. A Notification has no id field and the receiver must not send a response. Use Requests when you need confirmation or a return value. Use Notifications for events like progress updates, log messages, and cancellations where you don't need a reply.
Can MCP use JSON-RPC batching?
Partially. The MCP spec says implementations must support receiving JSON-RPC batches (arrays of requests/notifications) but only may support sending them. In practice, most MCP clients send requests one at a time. Don't rely on batch sending unless you've confirmed both sides support it.
What does the MCP initialize handshake look like?
The handshake has three steps: (1) the client sends an initialize Request with its protocol version and capabilities; (2) the server responds with its own capabilities; (3) the client sends an initialized Notification to confirm. Only after this sequence can either side send tool calls, resource reads, or other operational messages.
How do I debug MCP messages?
Log every JSON-RPC message - both sent and received - with a timestamp. Match Response id values back to their Request id to reconstruct the call graph. For STDIO transport, you can pipe stderr separately since it's not part of the protocol. For HTTP + SSE, use a proxy like mitmproxy to inspect the raw POST bodies and SSE events. Structured error codes (-32602, -32601, etc.) tell you exactly what category of failure occurred.
Useful Sources
- Model Context Protocol - Base Protocol Specification (2025-03-26) - the official source of truth for MCP message types, lifecycle, and constraints
- JSON-RPC 2.0 Specification - the base spec MCP builds on
- Portkey - MCP Message Types: Complete JSON-RPC Reference Guide - comprehensive reference table with every MCP method
Keep reading
Designing MCP Servers for Autonomous AI Agents: Tools, State, and Policy Enforcement
A senior-engineer guide to designing MCP servers for autonomous AI agents — architecture, tool design, state management, policy enforcement, security threats, and multi-agent patterns.
How MCP Solves the N×M Integration Problem for AI Agents
10 models and 10 tools means 100 custom integrations. MCP changes the math from N×M to N+M — one protocol, any model, any tool. Here's exactly how it works.
MCP vs A2A Protocol: What's the Difference and When You Need Both
MCP and A2A solve different problems in agentic AI. Here's the clearest breakdown of both protocols, when each falls short on its own, and why most production systems end up needing both.



