Development Choices

JSON-RPC Error or MCP Tool Failure?

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
6 min read3 sources cited

A JSON-RPC `error` means the server could not process the protocol method; a successful JSON-RPC `result` whose tool payload has `isError: true` means the tool ran and rejected the domain operation. Correlate responses by request ID, expect no reply to notifications, and preserve error codes and data.

Symptom: every failed call looks like a protocol error

Matrix comparing JSON-RPC errors with MCP tool results marked as errors
The same visible failure can require a different recovery path at each layer.

The trace says a tool failed, but it does not show whether the server rejected tools/call or the tool reported a problem after execution began.

Likely cause

The client treats these two wire shapes as equivalent:

{"jsonrpc":"2.0","id":17,"error":{"code":-32602,"message":"Invalid params"}}
{"jsonrpc":"2.0","id":17,"result":{"content":[{"type":"text","text":"The supplied value is outside the allowed range."}],"isError":true}}

They are different layers. The JSON-RPC 2.0 specification, updated 2013-01-04 says a response contains either result or error, never both. A top-level error means the request could not be processed successfully at the protocol method level. Its object contains an integer code, a short message, and optional data supplied by the server.

The MCP tools specification dated 2025-11-25 puts failures from the tool itself inside a successful JSON-RPC result, with isError: true. In that case, the tool ran and reported a domain failure. The specification names API failures, tool-input validation failures, and business-logic failures as examples. Unknown tools, malformed tools/call requests, unsupported tool calls, and exceptional server conditions belong in protocol errors.

That distinction controls recovery. A tool result can contain feedback that the model can use to change arguments and try again. A protocol error points first to the method name, request shape, server capability, or server implementation.

Check

Capture the unmodified response before an SDK wrapper, exception handler, or UI converts it. Inspect the top-level keys, not the wording of the message.

Wire shape Classification What to inspect next
Top-level error JSON-RPC protocol error error.code, error.message, optional error.data, and the request method and parameters
Top-level result with result.isError === true MCP tool failure Tool-result content, optional structuredContent, and the arguments sent to the tool
Top-level result without isError: true Successful tool result Validate and consume the returned content

Do not classify a response by searching its text for words such as “error” or “invalid.” Those words can appear inside tool content. If the envelope is correct but the tool output is unexpected, continue with the narrower process for debugging a failing MCP tool call.

Fix

Branch on the envelope before interpreting the payload:

if ("error" in response) {
  handleProtocolError(response.id, response.error);
} else if (response.result.isError === true) {
  handleToolFailure(response.id, response.result);
} else {
  handleToolSuccess(response.id, response.result);
}

Send tool-failure content to the model when it can act on that feedback. Route protocol errors to diagnostics for the request and server. Keep the raw response available in traces so operators can verify the classification instead of relying on the client’s rendered message.

Symptom: a response is attached to the wrong call—or never arrives

Concurrent calls appear to exchange results, or a client waits until timeout for a message that the server received.

Likely cause

The client discarded, changed, or incorrectly indexed the JSON-RPC request identifier. The identifier joins a result or error to the call that produced it: a response to a request carries the same id. This matters whenever more than one request is in flight and in batches, where responses need not be returned in request order.

The apparent timeout may instead involve a notification. A JSON-RPC notification has no id and receives no response. The server must not reply to it, including when it appears inside a batch. Waiting on a pending-response entry for a notification therefore creates a wait that valid protocol behavior cannot satisfy.

Check

Record these fields at the client’s send and receive boundaries:

send:    direction, method, id-present, id, timestamp
receive: direction, result-or-error, id, timestamp

For every ordinary request, find exactly one response carrying the same identifier. Compare both its value and type: MCP request identifiers are strings or numbers, so numeric 7 and string "7" should not collapse into the same pending-call key.

For every message without an identifier, confirm that the client classified it as a notification and did not create a response promise. If the message is intended to report server activity rather than answer a call, inspect it through the server’s structured logging notifications instead of treating it as a missing response.

Fix

Assign a distinct identifier to each request that expects a response. Preserve it unchanged through serialization, transport handling, logs, and the pending-call map. Match incoming results and errors by that identifier rather than by arrival order, tool name, or the position of a request in a batch.

Keep notification handling separate: do not add notifications to the pending-call map, do not start a response timeout for them, and do not make notification delivery depend on receiving an acknowledgement. For ordinary calls that genuinely remain in flight, handle their lifecycle explicitly rather than confusing notification behavior with request cancellation.

Symptom: logs and callers receive only “Tool call failed”

The raw response contained useful fields, but the dashboard, retry code, and model all see the same text string.

Likely cause

An adapter flattened both failure classes into one exception message. That throws away the difference between a protocol error and a tool result and may also discard machine-readable details.

The MCP schema reference dated 2025-11-25 defines a JSON-RPC error with code, message, and optional data. It separately defines CallToolResult with content, optional structuredContent, and optional isError. Clients should preserve these structures instead of reducing every failure to text.

The cost of flattening is diagnostic ambiguity. A code that identifies the protocol error type can no longer be inspected directly. Nested details in data disappear. Tool feedback that could support a corrected call becomes indistinguishable from a method-level rejection.

Check

Trace one known protocol error through every boundary: wire decoder, SDK adapter, application service, log event, UI, and model input. At each boundary, verify that the original request identifier, numeric error code, message, and complete data value remain available.

Repeat with a tool result whose isError is true. Verify that it remains a result rather than becoming a JSON-RPC error and that its content survives intact. Add assertions that compare structured values, not only rendered messages. The broader MCP server observability path should expose which request produced the failure and which layer classified it.

Fix

Use a discriminated application type that retains the protocol structure:

type CallOutcome =
  | { kind: "protocol-error"; id: string | number | null; code: number; message: string; data?: unknown }
  | { kind: "tool-error"; id: string | number; content: unknown[]; structuredContent?: object }
  | { kind: "tool-success"; id: string | number; content: unknown[]; structuredContent?: object };

A UI may derive a short sentence from this value, but that sentence should not replace the stored fields. Log kind, id, and code as separate fields; retain data as structured data. Keep tool content and structuredContent attached to tool outcomes. Make retry or escalation decisions from the discriminant and preserved fields, not substring matching against a single message.

Sources

  1. JSON-RPC 2.0 specification, updated 2013-01-04jsonrpc.org
  2. MCP tools specification dated 2025-11-25modelcontextprotocol.io
  3. MCP schema reference dated 2025-11-25modelcontextprotocol.io

See also