MCP JSON-RPC Explained: Requests, Responses & Notifications
Introduction
If you have spent any time working with the Model Context Protocol, you have seen JSON-RPC messages flowing between clients and servers. But understanding why these messages look the way they do—and how to work with them correctly—requires a deeper look at the protocol's messaging foundation.
The Model Context Protocol uses JSON-RPC 2.0 as its message-level protocol. JSON-RPC defines the structure of requests, responses, and notifications, while the transport layer (stdio or Streamable HTTP) determines how these messages are delivered. Understanding this separation is essential for implementing MCP correctly, debugging communication issues, and building robust production systems.
This article explains MCP's use of JSON-RPC from first principles. We will cover the structure of requests, responses, and notifications; how MCP maps protocol operations onto JSON-RPC methods; how request IDs enable concurrency and correlation; how error handling works; and how to debug and secure JSON-RPC-based MCP communication.
This is a deep dive into the message layer. For broader architectural context, see the MCP Architecture guide. For implementation details, see the MCP Implementation Guide.
What Is JSON-RPC 2.0?
JSON-RPC 2.0 is a lightweight, language-agnostic remote procedure call protocol that uses JSON for message encoding. It defines a simple model for client-server communication:
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ 1. Request │
│ (method + params + id) │
│────────────────────────────────────────▶│
│ │
│ 2. Response │
│ (result or error + same id) │
│◀────────────────────────────────────────│
│ │
│ 3. Notification │
│ (method + params, no id) │
│────────────────────────────────────────▶│
│ (no response) │
│ │
Figure 1: JSON-RPC 2.0 message patterns—requests, responses, and notifications.
The Three Message Types
JSON-RPC 2.0 defines three fundamental message types:
| Message Type | Has ID? | Expects Response? | Purpose |
|---|---|---|---|
| Request | Yes | Yes | Invokes a method on the server and expects a response |
| Response | Yes (same as request) | No | Returns the result or error for a request |
| Notification | No | No | Sends a message without expecting a response |
Request
A request invokes a method on the server. It must include a unique ID so the client can correlate the response.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
jsonrpc: Must be exactly "2.0"id: A unique identifier (string or integer, never null)method: The name of the method to invokeparams: Optional parameters (object or array)
Response
A response returns the result of a request. It must include the same ID as the request.
Success response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [...]
}
}
Error response:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Method not found"
}
}
Notification
A notification sends a message without expecting a response. It does not include an ID.
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
How MCP Uses JSON-RPC
MCP builds its protocol operations on top of JSON-RPC's request/response/notification model. Each MCP operation maps to a JSON-RPC method name, and each method has defined request parameters and response structures.
┌─────────────────────────────────────────────────────────────────────────────┐
│ MCP Protocol Layer │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MCP Semantic Layer │ │
│ │ - Lifecycle: initialize, initialized │ │
│ │ - Tools: tools/list, tools/call │ │
│ │ - Resources: resources/list, resources/read │ │
│ │ - Prompts: prompts/list, prompts/get │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼───────────────────────────────────┐ │
│ │ JSON-RPC 2.0 Layer │ │
│ │ - Request/Response correlation │ │
│ │ - Error handling │ │
│ │ - Message framing │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘
│
▼
┌─────────────┐
│ Transport │
│ stdio / │
│ HTTP │
└─────────────┘
Figure 2: MCP's protocol stack—MCP semantics built on JSON-RPC, delivered over a transport.
The Relationship
- JSON-RPC defines the message structure and semantics (how to make a request, how to respond, how to handle errors)
- MCP defines the methods and data models (what operations exist, what parameters they take, what results they return)
- Transport defines how messages are delivered (stdio for local processes, Streamable HTTP for remote services)
A developer implementing MCP works with JSON-RPC messages directly or through an SDK that handles serialization and deserialization.
MCP JSON-RPC Message Types
Comparison Table
| Message Type | ID | Expects Response | Typical MCP Usage |
|---|---|---|---|
| Request | ✅ String/Integer | ✅ Yes | initialize, tools/list, tools/call, resources/read, prompts/get |
| Response | ✅ Same as request | ❌ No | Result or error for any request |
| Notification | ❌ None | ❌ No | notifications/initialized, notifications/cancelled |
Key Distinction
The presence or absence of an id field determines whether a message is a request/response (with an ID) or a notification (without an ID). Notifications are used for one-way communication where the sender does not need or expect a reply.
JSON-RPC Request
Request Structure
A JSON-RPC request has the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
jsonrpc | string | ✅ | Must be "2.0" |
id | string or integer | ✅ | Unique identifier for correlation |
method | string | ✅ | The MCP method being invoked |
params | object or array | ❌ | Method parameters |
Request Example
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/data/report.pdf"
}
}
}
Request IDs
Request IDs serve two critical purposes:
- Correlation: The server echoes the same ID in the response, allowing the client to match responses to pending requests
- Concurrency: Multiple requests can be in flight simultaneously, and the ID disambiguates which response belongs to which request
Parameter Encoding
MCP requests typically use named parameters (objects) rather than positional parameters (arrays). This makes messages more readable and less prone to ordering errors.
// Named parameters (recommended)
"params": {
"name": "read_file",
"arguments": {"path": "/data/file.txt"}
}
// Positional parameters (less common)
"params": ["read_file", {"path": "/data/file.txt"}]
JSON-RPC Response
Success Response
A successful response includes:
| Field | Type | Required | Description |
|---|---|---|---|
jsonrpc | string | ✅ | Must be "2.0" |
id | string or integer | ✅ | The same ID as the corresponding request |
result | object or array | ✅ | The method result |
Success Response Example
{
"jsonrpc": "2.0",
"id": "req-001",
"result": {
"resultType": "complete",
"tools": [
{
"name": "read_file",
"description": "Read a file from the filesystem",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
}
]
}
}
resultType Field
MCP responses include a resultType field to distinguish different kinds of results:
"complete": The operation completed successfully with a full result"input_required": The operation needs additional information from the user
JSON-RPC Error Response
Error Structure
An error response includes:
| Field | Type | Required | Description |
|---|---|---|---|
jsonrpc | string | ✅ | Must be "2.0" |
id | string or integer | ✅ | The same ID as the corresponding request |
error | object | ✅ | Error information |
error.code | integer | ✅ | Error code (JSON-RPC or application-specific) |
error.message | string | ✅ | Human-readable error description |
error.data | any | ❌ | Additional error data (for diagnostics) |
Error Response Example
{
"jsonrpc": "2.0",
"id": "req-001",
"error": {
"code": -32000,
"message": "Tool execution failed",
"data": {
"tool": "read_file",
"path": "/data/report.pdf",
"cause": "File not found"
}
}
}
Standard JSON-RPC Error Codes
| Code | Name | Description |
|---|---|---|
| -32700 | Parse error | Invalid JSON was received |
| -32600 | Invalid Request | The JSON sent is not a valid Request object |
| -32601 | Method not found | The method does not exist |
| -32602 | Invalid params | Invalid method parameters |
| -32603 | Internal error | Internal JSON-RPC error |
| -32000..-32099 | Server error | Reserved for implementation-defined server errors |
MCP-Specific Error Handling
MCP adds its own semantics on top of JSON-RPC errors:
- Protocol errors (invalid method, invalid params) return JSON-RPC error responses
- Application errors (tool execution failures) return
resultType: "complete"with an error field in the result - Authentication errors use HTTP status codes at the transport level (for HTTP transport)
This separation allows tools to return error results in the same structure as success results, making them easier for clients to handle consistently.
JSON-RPC Notifications
Notification Structure
A notification includes:
| Field | Type | Required | Description |
|---|---|---|---|
jsonrpc | string | ✅ | Must be "2.0" |
method | string | ✅ | The notification method |
params | object or array | ❌ | Notification parameters |
No id field is present.
Notification Example
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
When to Use Notifications
MCP uses notifications for one-way communication:
- Lifecycle:
notifications/initializedconfirms initialization - Cancellation:
notifications/cancelledcancels an in-progress operation - List changes:
notifications/tools/list_changednotifies that the tool list has changed - Resource changes:
notifications/resources/updatednotifies that a resource has changed
No Response Expected
Notifications are fire-and-forget. The server must not respond to a notification. If a client sends a notification and the server attempts to respond, the response is invalid JSON-RPC.
MCP Initialization Messages
The initialization sequence establishes the protocol version and capabilities before any other operations.
initialize Request
The client sends an initialize request with its capabilities and version:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2026-07-28",
"capabilities": {
"sampling": {},
"elicitation": {},
"roots": {"listChanged": true}
},
"clientInfo": {
"name": "MyMCPClient",
"version": "1.0.0"
}
}
}
initialize Response
The server responds with its capabilities:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2026-07-28",
"capabilities": {
"logging": {},
"prompts": {"listChanged": true},
"resources": {"subscribe": true, "listChanged": true},
"tools": {"listChanged": true}
},
"serverInfo": {
"name": "ExampleMCPServer",
"version": "1.0.0"
}
}
}
initialized Notification
After receiving the response, the client sends an initialized notification:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
Initialization Sequence Diagram
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ 1. initialize (version + capabilities) │
│────────────────────────────────────────▶│
│ │
│ 2. initialize response │
│ (version + server capabilities) │
│◀────────────────────────────────────────│
│ │
│ 3. initialized notification │
│────────────────────────────────────────▶│
│ │
│ ──── Normal Operation ──── │
│ │
Figure 3: MCP initialization sequence—three messages establish the protocol session.
MCP Capability Negotiation
Capabilities are exchanged during initialization. They determine which protocol features are available.
Client Capabilities
Client capabilities declare what the client supports:
"capabilities": {
"sampling": {}, // Client can make LLM sampling requests
"elicitation": {}, // Client can handle elicitation requests
"roots": { // Client can provide filesystem roots
"listChanged": true // Client supports root list change notifications
}
}
Server Capabilities
Server capabilities declare what the server provides:
"capabilities": {
"logging": {}, // Server can log messages
"prompts": { // Server provides prompts
"listChanged": true // Server supports prompt list change notifications
},
"resources": { // Server provides resources
"subscribe": true, // Server supports resource subscriptions
"listChanged": true // Server supports resource list change notifications
},
"tools": { // Server provides tools
"listChanged": true // Server supports tool list change notifications
}
}
Why Capability Negotiation Matters
Capability negotiation enables:
- Interoperability: Clients and servers can adapt to each other's capabilities
- Progressive enhancement: New capabilities can be added without breaking old clients
- Feature discovery: Clients know what operations are available
MCP Tools and JSON-RPC
tools/list
Lists all available tools.
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"tools": [
{
"name": "read_file",
"description": "Read the contents of a file",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to the file"}
},
"required": ["path"]
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true
}
}
]
}
}
tools/call
Invokes a tool with arguments.
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/data/report.pdf"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"content": "The report contains 42 pages of analysis...",
"isError": false
}
}
Tool Error Response
Request:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/nonexistent/file.txt"
}
}
}
Response (tool execution failure):
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": "File not found: /nonexistent/file.txt",
"isError": true
}
}
Note that tool execution errors are returned as a successful JSON-RPC response with isError: true, not as a JSON-RPC error. This is an important distinction: JSON-RPC errors indicate protocol-level problems, while tool errors are application-level results.
MCP Resources and JSON-RPC
resources/list
Lists all available resources.
Request:
{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "complete",
"resources": [
{
"uri": "file:///docs/readme.md",
"name": "README",
"description": "Project readme file",
"mimeType": "text/markdown"
}
]
}
}
resources/read
Reads a specific resource by URI.
Request:
{
"jsonrpc": "2.0",
"id": 6,
"method": "resources/read",
"params": {
"uri": "file:///docs/readme.md"
}
}
Response:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "# Project README\n\nThis is the project documentation...",
"mimeType": "text/markdown"
}
]
}
}
MCP Prompts and JSON-RPC
prompts/list
Lists all available prompts.
Request:
{
"jsonrpc": "2.0",
"id": 7,
"method": "prompts/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "complete",
"prompts": [
{
"name": "code_review",
"description": "Review code for best practices and issues",
"arguments": [
{
"name": "code",
"description": "The code to review",
"required": true
},
{
"name": "language",
"description": "Programming language",
"required": false
}
]
}
]
}
}
prompts/get
Retrieves a prompt by name.
Request:
{
"jsonrpc": "2.0",
"id": 8,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "def hello(): print('world')",
"language": "Python"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"resultType": "complete",
"messages": [
{
"role": "system",
"content": {
"type": "text",
"text": "You are an expert code reviewer..."
}
},
{
"role": "user",
"content": {
"type": "text",
"text": "Review this Python code:\n\ndef hello(): print('world')"
}
}
]
}
}
JSON-RPC IDs and Request Correlation
Why IDs Matter
Request IDs are the foundation of reliable client-server communication in MCP. They enable:
- Response correlation: The client knows which request a response belongs to
- Concurrent requests: Multiple requests can be in flight simultaneously
- Out-of-order responses: Responses can arrive in any order and still be correctly correlated
ID Types
JSON-RPC allows two types of IDs:
- String IDs:
"req-001","get-tools","call-123" - Integer IDs:
1,42,999
ID Requirements
- Each request must have a unique ID (no duplicate IDs for outstanding requests)
- The ID must not be
null - The ID must be included in the response (same value)
- Clients should not reuse IDs until the previous request has completed
Concurrent Request Example
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ Request A (id: 100) │
│────────────────────────────────────────▶│
│ │
│ Request B (id: 200) │
│────────────────────────────────────────▶│
│ │
│ Response B (id: 200) │
│◀────────────────────────────────────────│
│ │
│ Response A (id: 100) │
│◀────────────────────────────────────────│
│ │
Figure 4: Concurrent requests with correlation via IDs—responses can arrive out of order.
Request Tracking in Clients
Clients should maintain a map of outstanding requests:
# Conceptual request tracking
pending_requests = {}
async def send_request(method: str, params: dict):
request_id = generate_unique_id()
pending_requests[request_id] = PendingRequest(
method=method,
timestamp=time.now()
)
transport.send({
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": params
})
async def handle_response(response: dict):
request_id = response["id"]
pending = pending_requests.pop(request_id, None)
if pending:
pending.complete(response)
Concurrent MCP Requests
MCP supports multiple requests in flight simultaneously. This is essential for performance, especially when agents need to discover capabilities, call multiple tools, or perform parallel operations.
Concurrency Model
- Each request has a unique ID
- Responses can be returned in any order
- The client matches responses to requests by ID
- Timeouts apply to individual requests
Example: Parallel Tool Calls
A client might make multiple tool calls concurrently:
Client sends: tools/call (id: 101, tool: read_file)
Client sends: tools/call (id: 102, tool: query_db)
Client sends: tools/list (id: 103)
Server responds: tools/list (id: 103)
Server responds: tools/call (id: 101)
Server responds: tools/call (id: 102)
The client correlates each response using the ID.
Timeout Handling
Clients should set timeouts for each request:
async def send_request_with_timeout(method: str, params: dict, timeout_seconds: int = 30):
request_id = generate_unique_id()
pending = PendingRequest()
pending_requests[request_id] = pending
transport.send(build_request(request_id, method, params))
# Wait for response with timeout
try:
response = await asyncio.wait_for(pending.future, timeout_seconds)
return response
except asyncio.TimeoutError:
pending_requests.pop(request_id, None)
raise TimeoutError(f"Request {method} timed out after {timeout_seconds}s")
MCP Notifications vs Events
Understanding the distinction between JSON-RPC notifications and other types of events is important for correct implementation.
JSON-RPC Notifications
- Protocol-level: Defined by MCP specification
- One-way: No response expected
- No ID: Cannot be correlated
- Examples:
notifications/initialized,notifications/cancelled,notifications/tools/list_changed
Application Events
- Application-level: Defined by the specific server implementation
- Transport-dependent: May use WebSockets, SSE, etc.
- Examples: Tool execution progress, resource updated events
Transport Events
- Transport-level: Connection lifecycle events
- Examples: Connection established, connection closed, reconnecting
Key Point
Not every asynchronous event should be a JSON-RPC notification. Use JSON-RPC notifications only for protocol-defined one-way messages. For application-specific events, consider other mechanisms (or extend the protocol with new notification types).
JSON-RPC and MCP Lifecycle Management
The MCP lifecycle is expressed through JSON-RPC messages:
┌─────────────┐
│ Connect │
└──────┬──────┘
│
▼
┌─────────────┐
│ initialize │ ← Request/Response
│ handshake │
└──────┬──────┘
│
▼
┌─────────────┐
│ initialized│ ← Notification
│ notification│
└──────┬──────┘
│
▼
┌─────────────────────────┐
│ Normal Operation │
│ ┌─────────────────┐ │
│ │ tools/list │ │ ← Request/Response
│ ├─────────────────┤ │
│ │ tools/call │ │ ← Request/Response
│ ├─────────────────┤ │
│ │ resources/read │ │ ← Request/Response
│ ├─────────────────┤ │
│ │ prompts/get │ │ ← Request/Response
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ notifications/ │ │ ← Notification (optional)
│ │ list_changed │ │
│ └─────────────────┘ │
└─────────────────────────┘
│
▼
┌─────────────┐
│ Shutdown │ ← Transport close
└─────────────┘
Figure 5: MCP lifecycle expressed through JSON-RPC message types.
JSON-RPC and Transport
JSON-RPC messages are logically separate from the transport that delivers them. The same JSON-RPC message can be sent over different transports.
stdio Transport
For local communication, messages are sent via standard input/output:
- Each JSON-RPC message must be a single line (no embedded newlines)
- Messages are delimited by newlines
- Logging goes to stderr (to avoid interfering with protocol messages)
Client → stdin: {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
Server → stdout: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
Server → stderr: [Log] Tools listed successfully
Streamable HTTP Transport
For remote communication, messages are sent via HTTP:
- Each request is a single POST to the MCP endpoint
- Responses are returned in the HTTP body
- Server-sent events for streaming
Client → HTTP POST /mcp
Body: {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
Server → HTTP 200 OK
Body: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
Transport and JSON-RPC Independence
The same JSON-RPC message works identically over either transport. This allows MCP implementations to support local and remote deployments with the same protocol logic, changing only the transport layer.
MCP JSON-RPC Examples
1. Initialization
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2026-07-28",
"capabilities": {},
"clientInfo": {"name": "example-client", "version": "1.0.0"}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2026-07-28",
"capabilities": {"tools": {}},
"serverInfo": {"name": "example-server", "version": "1.0.0"}
}
}
2. Listing Tools
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"tools": [
{"name": "read_file", "description": "Read a file", "inputSchema": {...}}
]
}
}
3. Calling a Tool
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {"path": "/data/file.txt"}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"content": "File contents...",
"isError": false
}
}
4. Reading a Resource
Request:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {"uri": "file:///docs/readme.md"}
}
Response:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": [{"type": "text", "text": "# README"}]
}
}
5. Getting a Prompt
Request:
{
"jsonrpc": "2.0",
"id": 5,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {"code": "def hello(): pass"}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "complete",
"messages": [
{"role": "system", "content": {"type": "text", "text": "You are an expert..."}},
{"role": "user", "content": {"type": "text", "text": "Review this code..."}}
]
}
}
6. Notification
Request (notification—no ID):
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
7. Error Response
Request (invalid method):
{
"jsonrpc": "2.0",
"id": 6,
"method": "invalid/method",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 6,
"error": {
"code": -32601,
"message": "Method not found"
}
}
Tool Call Lifecycle
The complete lifecycle of a tool call, from client request to response:
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ 1. tools/call request │
│ (id: 100, name: read_file, │
│ args: {path: "/data/file.txt"}) │
│────────────────────────────────────────▶│
│ │
│ 2. Validate arguments
│ 3. Execute tool
│ 4. Generate result
│ │
│ 5. tools/call response │
│ (id: 100, result: {content: "..."}) │
│◀────────────────────────────────────────│
│ │
│ 6. Client processes result │
│ │
Figure 6: Tool call lifecycle showing request, server processing, and response.
Error Handling in Tool Calls
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ 1. tools/call request │
│ (id: 101, name: read_file, │
│ args: {path: "/nonexistent"}) │
│────────────────────────────────────────▶│
│ │
│ 2. Execute tool
│ 3. Tool fails
│ │
│ 4. tools/call response │
│ (id: 101, result: { │
│ content: "File not found", │
│ isError: true}) │
│◀────────────────────────────────────────│
│ │
Error Handling and Timeouts
Malformed Messages
A malformed JSON-RPC message (invalid JSON, missing required fields) should return a JSON-RPC error:
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32700,
"message": "Parse error: Invalid JSON"
}
}
Invalid Methods
A request with an invalid method returns an error:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found: invalid/method"
}
}
Invalid Parameters
A request with invalid parameters returns an error:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Invalid params: missing required argument 'path'"
}
}
Timeout Handling
Clients should set appropriate timeouts:
| Operation | Typical Timeout | Notes |
|---|---|---|
initialize | 5 seconds | Quick handshake |
tools/list | 5 seconds | Should be cached |
tools/call | 30-60 seconds | May be long-running |
resources/read | 10 seconds | Usually quick |
prompts/get | 5 seconds | Template rendering |
Retryable vs Non-Retryable Errors
| Error Type | Retryable? | Strategy |
|---|---|---|
| Network timeout | ✅ Yes | Retry with backoff |
| Connection dropped | ✅ Yes | Reconnect and retry |
| Invalid method | ❌ No | Fix client code |
| Invalid params | ❌ No | Fix call site |
| Tool execution failure | Depends | Check error type |
| Authentication failure | ❌ No (unless token refresh) | Refresh credentials |
Debugging MCP JSON-RPC
Debugging Workflow
┌─────────────────────────────────────────────────────────────────────────────┐
│ JSON-RPC Debugging Workflow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Capture the message (logs, network trace) │
│ 2. Validate JSON syntax │
│ 3. Check jsonrpc version is "2.0" │
│ 4. Check request ID is present and valid (for requests) │
│ 5. Check method name is valid │
│ 6. Check params match expected structure │
│ 7. Inspect server response │
│ 8. Check error code and message if error │
│ 9. Check transport logs for framing errors │
│ 10. Correlate with request ID for concurrent requests │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Troubleshooting Table
| Symptom | Likely Cause | Diagnosis | Fix |
|---|---|---|---|
| No response to request | Transport issue | Check connection logs | Verify transport configuration |
-32700 Parse error | Invalid JSON | Validate JSON syntax | Fix JSON formatting |
-32600 Invalid Request | Missing required fields | Check JSON structure | Add missing fields |
-32601 Method not found | Typo in method name | Check method spelling | Use correct method name |
-32602 Invalid params | Wrong argument types | Check argument structure | Fix arguments |
-32603 Internal error | Server bug | Check server logs | Fix server code |
| Response ID mismatch | Duplicate ID | Check request IDs | Use unique IDs |
| Timeout | Slow operation | Check operation duration | Increase timeout or optimize |
| "isError": true | Tool execution failed | Check tool logic and dependencies | Fix tool implementation |
Tools for Debugging
- mcp-inspector: Protocol message inspector
- mcp-cli: Command-line client for testing
- Wireshark/Chrome DevTools: Network inspection (for HTTP transport)
- Structured logging: Log all messages with correlation IDs
MCP JSON-RPC with SDKs
Raw JSON-RPC vs SDK Abstraction
Raw JSON-RPC:
# Conceptual raw JSON-RPC
message = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
transport.send(json.dumps(message))
raw_response = transport.receive()
response = json.loads(raw_response)
With SDK:
# Conceptual SDK usage
tools = await client.list_tools()
What SDKs Hide
- Message serialization: Converting to/from JSON
- Request IDs: Automatic generation and tracking
- Transport details: stdio vs HTTP abstraction
- Dispatch: Routing requests to handlers
- Response parsing: Extracting results and errors
Why Understanding JSON-RPC Still Matters
Even when using an SDK, understanding JSON-RPC is valuable for:
- Debugging: Reading raw protocol messages
- Interoperability: Understanding why messages fail
- Performance: Understanding message size and serialization costs
- Edge cases: Handling unusual responses
- Custom implementations: When you need to implement MCP without an SDK
Security Considerations
Message-Level Security Risks
| Risk | Description | Mitigation |
|---|---|---|
| Untrusted parameters | Malicious tool arguments | Validate all inputs |
| Oversized payloads | Denial of service | Enforce size limits |
| Injection | Command injection via arguments | Input sanitization |
| Sensitive data | Secrets in messages | Redact logs, use secure transport |
| Replay attacks | Replaying valid requests | Use nonces, short-lived tokens |
Logging Security
Never log full messages without redaction:
# ❌ Bad - may log secrets
logger.info(f"Request: {request}")
# ✅ Good - redact sensitive fields
logger.info(f"Request: {redact_sensitive(request)}")
Input Validation
At the message layer, validate:
jsonrpcversion is "2.0"idis present for requests (not for notifications)methodis a valid MCP methodparamsmatch the expected schema
Transport Security
- Use TLS for all HTTP transport
- Validate Origin headers
- Implement OAuth 2.1 for authentication
Observability and JSON-RPC
What to Observe
| Metric | Purpose |
|---|---|
| Request count | Understand usage patterns |
| Request latency | Identify performance issues |
| Error rate by code | Track error categories |
| Method usage | Understand which operations are used |
| Tool call count | Track tool popularity |
| Concurrent requests | Capacity planning |
Structured Logging
Include in every log entry:
{
"timestamp": "2026-08-28T10:00:00Z",
"request_id": "req-001",
"method": "tools/call",
"tool_name": "read_file",
"status": "success",
"duration_ms": 45,
"client_id": "client-123"
}
Tracing
Use request IDs for distributed tracing:
- Client generates a request ID
- Server includes the request ID in its logs
- Log aggregation correlates client and server logs
OpenTelemetry Integration
# Conceptual OpenTelemetry integration
from opentelemetry import trace
tracer = trace.get_tracer("mcp-client")
with tracer.start_as_current_span("mcp-request") as span:
span.set_attribute("mcp.method", method)
response = await send_request(method, params)
span.set_attribute("mcp.status", response.get("status", "unknown"))
Performance Considerations
Serialization Overhead
JSON serialization/deserialization has a cost. For high-throughput systems:
- Use efficient JSON libraries (orjson, ujson)
- Consider message size optimization
- Batch requests where possible (if supported)
Payload Size
Large messages increase latency:
- Limit tool result sizes
- Use pagination for large lists
- Use streaming for large resources
Concurrency
Use concurrent requests to improve throughput:
- Multiple tools in parallel
- Discovery and operation in parallel
- Proper connection management
Connection Reuse
For HTTP transport:
- Reuse HTTP connections (keep-alive)
- Use connection pooling
- Avoid creating new connections for each request
What to Measure
- p50, p95, p99 latency for each method
- Serialization time vs transport time
- Request size distribution
- Concurrent request count
Common JSON-RPC Mistakes
| Mistake | Why It's Problematic | How to Avoid |
|---|---|---|
| Missing IDs | Cannot correlate responses | Always include IDs for requests |
| Duplicate IDs | Ambiguous correlation | Use unique IDs for each request |
| Malformed params | Request rejected | Validate params against schema |
| Treating notifications as requests | Clients expect responses | Understand the difference |
| Ignoring error objects | Missed error conditions | Check for error field |
| Assuming response ordering | Concurrent responses may be out of order | Use IDs, not order |
| Mixing transport errors with protocol errors | Confusing debugging | Separate layers in logs |
| Logging secrets | Security breach | Redact sensitive data |
| Assuming successful JSON parsing means successful tool execution | Tool errors are not JSON-RPC errors | Check isError flag |
| Not handling timeouts | Hanging requests | Set and handle timeouts |
MCP JSON-RPC Best Practices
Protocol Compliance
- Use
jsonrpc: "2.0"in all messages - Include
idin all requests (not in notifications) - Use unique IDs for each outstanding request
- Respond with the same ID as the request
- Return JSON-RPC errors for protocol-level problems
Message Validation
- Validate JSON syntax
- Validate
jsonrpcversion - Validate request ID presence/absence
- Validate method name
- Validate params against schema
Request Correlation
- Track outstanding requests by ID
- Handle out-of-order responses
- Time out requests that take too long
- Clean up pending requests on error
Error Handling
- Distinguish protocol errors from application errors
- Return JSON-RPC errors for protocol-level issues
- Return
isError: truefor tool execution failures - Include helpful error messages
- Log errors with context
Timeouts
- Set appropriate timeouts for each operation
- Handle timeout errors gracefully
- Retry retryable errors with backoff
Security
- Validate all inputs
- Redact sensitive data from logs
- Use TLS for HTTP transport
- Implement authentication
Logging
- Log all requests with request ID
- Log responses with request ID
- Log errors with context
- Use structured logging
- Redact sensitive data
Testing
- Test valid requests
- Test malformed requests
- Test invalid methods
- Test invalid parameters
- Test error handling
- Test concurrency
- Test timeouts
Testing MCP JSON-RPC Messages
Test Types
| Test Type | Description | Purpose |
|---|---|---|
| Schema validation | Validate JSON-RPC structure | Ensure protocol compliance |
| Protocol compliance | Test all MCP methods | Verify interoperability |
| Contract testing | Test request/response pairs | Ensure compatibility |
| Negative testing | Test invalid inputs | Ensure error handling |
| Malformed message testing | Test invalid JSON | Ensure robustness |
| Interoperability testing | Test with real clients | Ensure real-world compatibility |
| Regression testing | Test changes don't break existing behavior | Maintain quality |
Test Examples
Valid request test:
- Send
tools/listrequest - Verify response has
result.tools
Invalid method test:
- Send
invalid/methodrequest - Verify error code
-32601
Missing ID test:
- Send request without ID
- Verify error or server behavior
Malformed JSON test:
- Send
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {(incomplete) - Verify parse error
Frequently Asked Questions
1. What is MCP JSON-RPC?
MCP JSON-RPC is the message-level protocol used by the Model Context Protocol. It defines how requests, responses, and notifications are structured using JSON-RPC 2.0.
2. Does MCP use JSON-RPC 2.0?
Yes, MCP uses JSON-RPC 2.0 as its foundation for all client-server messages.
3. What is an MCP JSON-RPC request?
A JSON-RPC request is a message with an ID, a method name, and optional parameters that expects a response.
4. What is an MCP JSON-RPC response?
A JSON-RPC response is a message with the same ID as the request, containing either a result or an error.
5. What is an MCP notification?
A JSON-RPC notification is a message without an ID that does not expect a response.
6. Why does MCP use request IDs?
Request IDs correlate responses with requests, enabling concurrent requests and out-of-order responses.
7. How are MCP tool calls represented?
Tool calls are represented as tools/call requests with tool name and arguments, returning results or errors.
8. How are MCP resources represented?
Resources are represented as resources/read requests with a URI, returning the resource content.
9. How are MCP prompts represented?
Prompts are represented as prompts/get requests with a name and arguments, returning a sequence of messages.
10. What happens when an MCP request fails?
For protocol-level errors, a JSON-RPC error response is returned. For tool execution failures, isError: true is returned in the result.
11. Can multiple MCP requests run concurrently?
Yes, multiple requests can be in flight simultaneously, correlated by their IDs.
12. How does JSON-RPC differ from HTTP?
JSON-RPC is a message format and semantics; HTTP is a transport protocol. MCP can use JSON-RPC over HTTP (or stdio).
13. Is JSON-RPC the same as the MCP transport?
No, JSON-RPC defines message semantics; transport defines how messages are delivered (stdio or Streamable HTTP).
14. How do I debug MCP JSON-RPC messages?
Capture messages, validate JSON, check IDs and methods, inspect responses, and use mcp-inspector.
15. How should MCP JSON-RPC messages be logged securely?
Use structured logging, include request IDs, redact sensitive data, and avoid logging full messages with secrets.
16. What is the difference between a JSON-RPC error and isError?
JSON-RPC errors are protocol-level issues (invalid method, invalid params). isError: true is an application-level tool execution failure returned in the result.
17. Can notifications have parameters?
Yes, notifications can include a params field with additional data.
18. What is the maximum message size?
MCP does not specify a maximum size. Implementations should enforce reasonable size limits to prevent DoS.
19. How do I handle timeouts for MCP requests?
Set timeouts per request, handle timeout errors, and retry retryable errors with backoff.
20. What is the most common JSON-RPC mistake in MCP?
Using duplicate request IDs or forgetting to include an ID, leading to response correlation failures.
Key Takeaways
- MCP uses JSON-RPC 2.0 as its message-level protocol foundation
- Requests, responses, and notifications have different semantics and use cases
- Request IDs enable reliable correlation of asynchronous and concurrent operations
- Protocol errors (invalid method, invalid params) use JSON-RPC error responses
- Tool execution failures use
isError: truein the result (not JSON-RPC errors) - Transport and JSON-RPC are separate architectural layers
- Correct JSON-RPC handling is essential for interoperability, debugging, reliability, and security
- Understanding the message layer helps even when using an SDK
Further Reading
- MCP Protocol Overview – An introduction to the Model Context Protocol, its architecture, core primitives, and how it connects agents to tools.
- MCP Architecture – A complete production-oriented guide to MCP architecture, covering hosts, clients, servers, JSON-RPC communication, and deployment patterns.
- MCP Implementation Guide – A practical guide to building MCP servers and clients with code examples.
- MCP Server Development – A step-by-step guide to building, configuring, and deploying your own MCP servers.
- MCP Client Development – Learn how to implement an MCP client that discovers and consumes tools, resources, and prompts.
- MCP Tools – Deep dive into the tool primitive: exposing functions, handling parameters, and executing actions.
- MCP Resources – Understanding resources as application-controlled contextual data with URI-based addressing.
- MCP Prompts – Learn how to expose reusable prompt templates that guide language model interactions.
- MCP Security – Best practices for securing MCP servers, including authentication, authorization, and tool-safety measures.
- MCP Deployment – Strategies for deploying MCP servers in production environments.
- MCP Best Practices – Production-ready recommendations for designing, building, and operating MCP-based systems.
- MCP vs A2A – A deep-dive comparison between the Model Context Protocol and the Agent-to-Agent Protocol.
- Production Testing – Strategies for testing AI agents in production environments.
- Production Observability – Comprehensive guide to logging, metrics, and tracing for production AI agent systems.
- Production Reliability – Building reliable AI agent systems with fault tolerance and graceful degradation.