F3.4 F3

Don't Disguise Errors as Empty Results

When a tool fails, the worst thing you can do is pretend it succeeded. MCP’s CallToolResult structure exists specifically to make error signaling explicit — but only if you use it correctly.

The structure: content + isError

A CallToolResult has two fields:

  • content — an array of content blocks (typically text) carrying the result data
  • isError — a boolean indicating success (false) or failure (true)

On success, content holds the result and isError is false (or omitted — it defaults to false). On failure, content describes what went wrong and isError is true.

The content field carries both success data and error descriptions. It’s the isError flag that tells the AI model how to interpret the content — not the content itself.

Why the explicit flag matters

Without isError: true, the model treats content as a successful result. If your tool returns {"content": [{"type": "text", "text": "Permission denied"}], "isError": false}, the model may try to extract data from “Permission denied” as if it were a valid response. It has no reliable way to infer failure from text alone.

Setting isError: true changes the model’s behavior: it knows the operation failed, reads the error description, and can reason about recovery — retry with different parameters, try an alternative tool, or inform the user.

The disguised error anti-pattern

The most dangerous mistake: converting errors into fake “not found” results. Imagine a customer lookup tool that hits a permission error. Returning {"content": [{"type": "text", "text": "No customer found"}], "isError": false} is wrong on two levels:

  1. The customer might exist — you just couldn’t access the data
  2. The model now tells the user “no customer found,” which is false

The honest response: {"content": [{"type": "text", "text": "Permission denied: insufficient API key permissions"}], "isError": true}. The model learns the real problem and can tell the user what actually happened.

Two layers of errors in MCP

MCP distinguishes between protocol errors and tool execution errors:

  • Protocol errors — calling a non-existent tool, malformed JSON-RPC request, invalid parameters. These are JSON-RPC errors at the transport level. They indicate client bugs, not runtime failures.

  • Tool execution errors — API timeout, permission denied, resource not found, rate limit hit. These use isError: true in CallToolResult and reach the AI model. The model can reason about them and attempt recovery.

The dividing line: protocol errors mean “you called this wrong” (developer problem). Tool execution errors mean “the operation failed at runtime” (potentially recoverable). Don’t throw JSON-RPC errors for runtime failures — the model needs to see those through isError: true.

No automatic retry

MCP does not automatically retry failed tool calls. When isError: true is returned, it goes straight to the client. Retry logic is the client’s responsibility — the protocol simply reports what happened. This is by design: the client (or the AI model) has the context to decide whether retrying makes sense.


One-liner: Use isError: true with descriptive content for tool failures — never disguise errors as empty results, and remember that protocol errors (JSON-RPC) and tool execution errors (isError) serve different layers.