Skip to main content

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 TypeHas ID?Expects Response?Purpose
RequestYesYesInvokes a method on the server and expects a response
ResponseYes (same as request)NoReturns the result or error for a request
NotificationNoNoSends 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 invoke
  • params: 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 TypeIDExpects ResponseTypical MCP Usage
Request✅ String/Integer✅ Yesinitialize, tools/list, tools/call, resources/read, prompts/get
Response✅ Same as request❌ NoResult or error for any request
Notification❌ None❌ Nonotifications/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:

FieldTypeRequiredDescription
jsonrpcstringMust be "2.0"
idstring or integerUnique identifier for correlation
methodstringThe MCP method being invoked
paramsobject or arrayMethod 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:

  1. Correlation: The server echoes the same ID in the response, allowing the client to match responses to pending requests
  2. 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:

FieldTypeRequiredDescription
jsonrpcstringMust be "2.0"
idstring or integerThe same ID as the corresponding request
resultobject or arrayThe 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:

FieldTypeRequiredDescription
jsonrpcstringMust be "2.0"
idstring or integerThe same ID as the corresponding request
errorobjectError information
error.codeintegerError code (JSON-RPC or application-specific)
error.messagestringHuman-readable error description
error.dataanyAdditional 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

CodeNameDescription
-32700Parse errorInvalid JSON was received
-32600Invalid RequestThe JSON sent is not a valid Request object
-32601Method not foundThe method does not exist
-32602Invalid paramsInvalid method parameters
-32603Internal errorInternal JSON-RPC error
-32000..-32099Server errorReserved 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:

FieldTypeRequiredDescription
jsonrpcstringMust be "2.0"
methodstringThe notification method
paramsobject or arrayNotification 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/initialized confirms initialization
  • Cancellation: notifications/cancelled cancels an in-progress operation
  • List changes: notifications/tools/list_changed notifies that the tool list has changed
  • Resource changes: notifications/resources/updated notifies 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:

  1. Response correlation: The client knows which request a response belongs to
  2. Concurrent requests: Multiple requests can be in flight simultaneously
  3. 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:

OperationTypical TimeoutNotes
initialize5 secondsQuick handshake
tools/list5 secondsShould be cached
tools/call30-60 secondsMay be long-running
resources/read10 secondsUsually quick
prompts/get5 secondsTemplate rendering

Retryable vs Non-Retryable Errors

Error TypeRetryable?Strategy
Network timeout✅ YesRetry with backoff
Connection dropped✅ YesReconnect and retry
Invalid method❌ NoFix client code
Invalid params❌ NoFix call site
Tool execution failureDependsCheck 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

SymptomLikely CauseDiagnosisFix
No response to requestTransport issueCheck connection logsVerify transport configuration
-32700 Parse errorInvalid JSONValidate JSON syntaxFix JSON formatting
-32600 Invalid RequestMissing required fieldsCheck JSON structureAdd missing fields
-32601 Method not foundTypo in method nameCheck method spellingUse correct method name
-32602 Invalid paramsWrong argument typesCheck argument structureFix arguments
-32603 Internal errorServer bugCheck server logsFix server code
Response ID mismatchDuplicate IDCheck request IDsUse unique IDs
TimeoutSlow operationCheck operation durationIncrease timeout or optimize
"isError": trueTool execution failedCheck tool logic and dependenciesFix 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

RiskDescriptionMitigation
Untrusted parametersMalicious tool argumentsValidate all inputs
Oversized payloadsDenial of serviceEnforce size limits
InjectionCommand injection via argumentsInput sanitization
Sensitive dataSecrets in messagesRedact logs, use secure transport
Replay attacksReplaying valid requestsUse 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:

  • jsonrpc version is "2.0"
  • id is present for requests (not for notifications)
  • method is a valid MCP method
  • params match 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

MetricPurpose
Request countUnderstand usage patterns
Request latencyIdentify performance issues
Error rate by codeTrack error categories
Method usageUnderstand which operations are used
Tool call countTrack tool popularity
Concurrent requestsCapacity 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:

  1. Client generates a request ID
  2. Server includes the request ID in its logs
  3. 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

MistakeWhy It's ProblematicHow to Avoid
Missing IDsCannot correlate responsesAlways include IDs for requests
Duplicate IDsAmbiguous correlationUse unique IDs for each request
Malformed paramsRequest rejectedValidate params against schema
Treating notifications as requestsClients expect responsesUnderstand the difference
Ignoring error objectsMissed error conditionsCheck for error field
Assuming response orderingConcurrent responses may be out of orderUse IDs, not order
Mixing transport errors with protocol errorsConfusing debuggingSeparate layers in logs
Logging secretsSecurity breachRedact sensitive data
Assuming successful JSON parsing means successful tool executionTool errors are not JSON-RPC errorsCheck isError flag
Not handling timeoutsHanging requestsSet and handle timeouts

MCP JSON-RPC Best Practices

Protocol Compliance

  • Use jsonrpc: "2.0" in all messages
  • Include id in 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 jsonrpc version
  • 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: true for 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 TypeDescriptionPurpose
Schema validationValidate JSON-RPC structureEnsure protocol compliance
Protocol complianceTest all MCP methodsVerify interoperability
Contract testingTest request/response pairsEnsure compatibility
Negative testingTest invalid inputsEnsure error handling
Malformed message testingTest invalid JSONEnsure robustness
Interoperability testingTest with real clientsEnsure real-world compatibility
Regression testingTest changes don't break existing behaviorMaintain quality

Test Examples

Valid request test:

  • Send tools/list request
  • Verify response has result.tools

Invalid method test:

  • Send invalid/method request
  • 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: true in 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.