Status: Accepted Date: 2026-03-19
When a tool execution fails (file not found, git command error, invalid input after schema validation), the bridge must communicate the failure back to the client. There are two mechanisms:
-
JSON-RPC error response —
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"..."}}. Standard for protocol-level failures. -
Successful response with
isError: true—{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Error: file not found"}],"isError":true}}. The response is technically successful at the protocol level, but the content indicates the tool failed.
Early implementations used JSON-RPC errors for everything. This caused two problems:
-
LLMs can't reason about JSON-RPC errors. Claude Code treats JSON-RPC errors as infrastructure failures and may abort the entire tool sequence. With
isErrorcontent, Claude reads the error message, understands what went wrong, and can retry with different parameters or try an alternative approach. -
MCP spec compliance. The Model Context Protocol specification explicitly states that tool execution errors should be returned as content with
isError: true, reserving JSON-RPC error codes for protocol-level issues (parse error, method not found, rate limiting).
Separate error handling into two tiers:
Tier 1 — Protocol errors (ErrorCodes, JSON-RPC -32xxx):
Used for issues the LLM cannot meaningfully act on:
-32700Parse error (malformed JSON)-32601Method not found-32602Invalid params (AJV schema validation failure)-32000Rate limit exceeded-32001Session errors (not found, capacity reached)
Tier 2 — Tool errors (ToolErrorCodes, isError: true in content):
Used for failures the LLM can understand and potentially recover from:
- File not found, permission denied
- Git command failed (bad ref, merge conflict)
- Command execution error (non-zero exit code)
- Extension disconnected (for
extensionRequiredtools)
In code, tool handlers catch errors and return them as content:
handler: async (args) => {
try {
const result = await doWork(args);
return { content: [{ type: "text", text: result }] };
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${err.message}` }],
isError: true,
};
}
};Tool handlers must never throw. Uncaught exceptions are caught by the transport layer and converted to isError content as a safety net, but this path logs a warning.
Positive:
- LLMs can read error messages and adapt (retry with different args, try alternative tool, explain the issue to the user).
- Clean separation: protocol errors for infrastructure, content errors for tool logic.
- MCP spec compliant — interoperable with other MCP clients.
Negative:
- Contributors must remember: never use JSON-RPC error codes for tool failures. This is the most common mistake when adding new tools.
- Error content is free-form text — no structured error codes for programmatic handling by non-LLM clients. Mitigated by consistent error message prefixes.
Audit rule:
Grep for ErrorCodes. usage in src/tools/ — it should appear only in the transport layer, never in individual tool handlers.