MCP isError and Claude's is_error: tool errors an agent can act on
When a tool fails, tell Claude in the tool result: is_error true in the Claude API, isError true in an MCP tool result, with a message that says what went wrong and what to try next. Claude reads it and can retry, fix its input or explain the problem. Never return an empty result for a failed lookup: 'no results' and 'couldn't check' are different answers, and an agent that confuses them makes promises it can't keep.
Video · MCP isError and Claude Tool Errors: Errors an Agent Can Act On (CCAR-F) · 4:10
Video · "No Results" vs "Couldn't Check": Never Hide a Tool Failure (CCAR-F) · 3:11
Returning a tool error to Claude
In the tool use loop, your code runs the tool and sends back a tool_result. When the tool fails, mark it:
json
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"is_error": true,
"content": "Booking B-2041 not found. Booking IDs look like B-2041; ask the customer to check the confirmation email."
}
The docs ask for instructive messages: "Instead of generic errors like "failed", include what went wrong and what Claude should try next." A bare "Operation failed" leaves Claude guessing; an instructive message gives it a next step.
MCP: protocol errors vs tool errors
The MCP specification has two error mechanisms:
| Protocol error | Tool execution error | |
|---|---|---|
| Examples | Unknown tool, malformed request, server error | API failure, invalid input, business rule broken |
| How it comes back | A JSON-RPC error | A normal result with isError: true and the message in content |
| Can the model fix it? | "Less likely to result in successful recovery" | Yes: clients "SHOULD provide tool execution errors to language models to enable self-correction" |
So in an MCP server, a failure the model could do something about belongs in the result, with isError: true, not in a protocol error.
Four kinds of failure, four different next steps
| Kind | Example in a bike-rental agent | Retry? | What the message should say |
|---|---|---|---|
| Transient | The payment service timed out | Yes, after a pause | That it timed out, and when to retry |
| Validation | 2041 sent where B-2041 was expected |
Yes, with corrected input | The expected format |
| Business | A $140 refund where the limit is $100 | No | The rule, and the next step (hand off to a person) |
| Permission | Asking for another customer's booking | No | That it isn't allowed, without revealing data |
Many teams add explicit fields so the agent doesn't have to infer: an error category and a retry flag, plus a customer-friendly explanation for business rules. These are your own conventions, not fields of the MCP spec. Put them in the result's content or structuredContent:
json
{"errorCategory": "transient", "isRetryable": true,
"message": "Payment service timed out. Retry in 30 seconds."}
Invalid calls also fix themselves up to a point: if a request is invalid or missing parameters, "Claude will retry 2-3 times with corrections before apologizing to the user". To remove invalid calls entirely, use strict tool use.
"No results" is not "couldn't check"
The costliest mistake is silent: a lookup that times out and returns an empty list, as if it had succeeded.
| The tool returns | The agent can say |
|---|---|
| A successful result with no matches | "There's no booking under that name." |
is_error: true, transient |
"I couldn't reach the booking system just now. Let me try again." |
| An empty result that hid a failure | "There's no booking", which is false, and the customer books and pays twice |
Keep the two apart in every tool: a query that ran and found nothing is a success; a query that couldn't run is an error. The same goes for parallel calls: a call you decided not to run still gets a tool_result with is_error: true.
Errors across agents
In a multi-agent system, a subagent should recover locally from what it can, such as a transient failure, and pass up only what it can't, with context: the failure type, what it tried, partial results, and alternatives. Two patterns fail: hiding errors behind empty "successful" results, and stopping the whole workflow on one failure. The final answer should say what it couldn't check, so a person knows where the gaps are. When a failure needs a decision, hand off to a human.
Tool errors on the Claude Certified Architect exam
The CCAR-F exam covers this in task 2.2 (structured errors for MCP tools: transient, validation, business and permission errors, retryable or not, empty results versus access failures) and task 5.3 (error propagation across agents, local recovery, partial results, coverage gaps). Expect the guide's field names, such as an error category and an isRetryable flag, in the answers.
More in all articles.
Sources: Handle tool calls; Parallel tool use; Strict tool use; MCP specification 2026-07-28: Tools, error handling; Writing effective tools for agents; Claude Certified Architect Foundations exam guide v1.0, task statements 2.2 and 5.3.
FAQ
What is isError in MCP?
A flag in an MCP tool result that marks a tool execution error, such as an API failure, invalid input or a broken business rule. The error message goes in the result's content so the model can read it and self-correct.
How do I return a tool error to Claude?
Send a tool_result with is_error set to true and the error message as its content. Make the message instructive: what went wrong and what Claude should try next, instead of a generic 'failed'.
What is the difference between an MCP protocol error and a tool error?
Protocol errors, such as an unknown tool or a malformed request, come back as JSON-RPC errors and are hard for the model to fix. Tool execution errors come back inside the result with isError: true, so the model can act on them.
Should an empty search result be an error?
No. A query that ran and found nothing is a successful result with no matches. A query that couldn't run, because of a timeout or missing access, is an error. Keep them apart, so the agent never reports 'no booking found' when it actually couldn't check.