What you will be able to do
- Tell apart an MCP protocol error from a tool execution error, and say which one the model can see
- Return a tool failure with isError: true and a message the agent can act on, not a raw exception
- Sort a failure into transient, validation, business or permission, and say what recovery each one allows
- Explain why a generic failure message causes wasted retries, and say what retry information an error should carry so the agent stops or retries correctly
Key concept
Two-tier MCP error model — MCP reports errors in two separate ways. Problems with the request itself (an unknown tool, a malformed call) come back as JSON-RPC protocol errors. Failures that happen while the tool runs come back inside a normal tool result with isError: true, so the model can read them and decide how to recover.
1.Two channels for failure: protocol errors and tool execution errors
An MCP tool can fail in two different ways, and the specification gives each one its own reporting channel. The first is the protocol error: something is wrong with the request before any tool logic runs. The spec names three cases: unknown tools, malformed requests (requests that fail to satisfy the CallToolRequest schema), and server errors. These come back as standard JSON-RPC errors, with an error object in place of a result. The spec says these are problems that models are less likely to be able to fix.
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Unknown tool: invalid_tool_name"
}
}The second is the tool execution error: the call reached the tool and the tool's own work failed. The spec lists three kinds: API failures, input validation errors (a date in the wrong format, a value out of range) and business logic errors. These are returned as an ordinary result with isError: true, and the content array carries text describing what went wrong. The spec describes this channel as carrying actionable feedback that models can use to self-correct and retry with adjusted parameters.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "Invalid departure date: must be in the future. Current date is 08/08/2025."
}
],
"isError": true
}
}This split matters for the agent because of what the model gets to see. The schema reference says tool-originated errors SHOULD go inside the result with isError set, because otherwise the LLM would not be able to see that an error occurred and self-correct. The flag also has a default: if isError is not set, the call is treated as successful. A server that reports a tool-level failure as a JSON-RPC error hides it from the model's reasoning. A server that returns a failure without setting the flag tells the model the call worked.
Checkpoint 1 of 5· Check yourself
A server developer wraps every tool exception, including a downstream API returning HTTP 500, in a JSON-RPC error response. Per the MCP schema reference, what is the problem?
Errors that originate in the tool belong in the result object with isError: true. If they go out as protocol-level errors, the model never sees them as something it can react to.
“Otherwise, the LLM would not be able to see that an error occurred and self-correct.”Source: modelcontextprotocol.io
Checkpoint 2 of 5· Exam question
A subagent responsible for enriching customer records calls an internal `lookup_address` tool for 50 customers. For 47 the lookup succeeds; for 3 the tool returns `isError: true` with `errorCategory: "permission"` because the subagent's credentials lack access to those records' region. The subagent cannot obtain broader credentials itself. What should it send back to the coordinator?
Correct answer: A — The 47 enriched records as partial results, plus a report naming the 3 unresolved customers, the permission error encountered, and what was attempted
- A. Correct. Errors the subagent cannot resolve locally, like a credential-scope gap, should propagate to the coordinator along with the partial results already obtained and a summary of what was tried, so the coordinator can decide the next step.
- B. Incorrect. Silently dropping the failures hides a real, unresolved permission problem from the coordinator, which then has no way to know 3 records were never enriched.
- C. Incorrect. Discarding 47 successful lookups because 3 others failed for an unrelated reason throws away valid completed work that the coordinator could otherwise use immediately.
- D. Incorrect. Retrying with the same insufficient credentials will keep failing identically; a permission error of this kind cannot be resolved by repetition and should be escalated instead.
2.Writing the error the agent reads
Setting isError: true tells the agent that something failed. The text in content tells it what to do next. The Claude Agent SDK documentation lists this as a deliberate design decision: to control the error message Claude reads, return isError: true and write the message yourself instead of letting the raw exception surface. The SDK's error-handling example catches non-200 responses and exceptions, then returns them as tool results. Its inline comments give the reasons: return the failure as a tool result so Claude can react to it, and set the flag so the result reads as a failed call rather than odd-looking data. The same example notes that an uncaught exception would reach Claude as the raw exception string, with no context.
The spec's own example shows what a useful message looks like. "Invalid departure date: must be in the future. Current date is 08/08/2025." says which field is wrong, which rule it broke, and the fact the model needs to fix it (today's date). Compare a bare "Operation failed": the flag is set, but nothing tells the agent whether to change an argument, wait and try again, or stop and explain. The spec has a related rule for stateful tools. A call against an expired or unknown handle should return an execution error that says so, so the model can recover by creating a new handle.
The Messages API works the same way on the client side. The tool-using-agent tutorial catches the exception and appends a tool_result carrying the exception text and the error flag, instead of letting the loop crash:
Checkpoint 3 of 5· Fill the gap
Which key completes this tutorial code so Claude knows the tool call failed?
except Exception as exc:
# Signal failure so Claude can retry or ask for clarification.
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(exc),
" ? ": True,
}
)In the Messages API, a tool_result block marks failure with is_error: true. MCP's CallToolResult spells the same idea isError. The tutorial says the flag is the only difference from a successful result.
Source: platform.claude.com3.Transient, validation, business and permission errors
For recovery, the useful question is not just whether a call failed but what kind of failure it was. Each kind allows a different next step. The MCP spec names three tool execution error kinds: API failures, input validation errors and business logic errors. The exam adds a fourth, permission errors. The spec does not name that as an error category. It only requires that servers implement proper access controls. A denied access still originates in the tool, so under the schema rule it belongs in the result with isError: true, like the other three. The table maps each kind to the recovery it allows.
| Kind | Example | How MCP reports it | Sensible agent response |
|---|---|---|---|
| Transient | API failure: upstream service unavailable or timed out | Tool execution error, isError: true | Retrying may succeed. If it keeps failing, tell the user to try later |
| Validation | Date in wrong format, value out of range | Tool execution error, isError: true | Correct the arguments and call again |
| Business | Business logic error, e.g. a policy limit is exceeded | Tool execution error, isError: true | Do not resend the same request. Explain the rule to the user |
| Permission | Request refused by the server's access controls | Not a named category in the spec. It originates in the tool, so isError: true | Do not retry. Ask for access or escalate |
| Protocol | Unknown tool, malformed CallToolRequest | JSON-RPC error, e.g. code -32602 | Fix the request itself. This is not a tool result the model can reason about |
Two of these four kinds can be cured by trying again: a transient failure by waiting, and a validation failure by changing the input. The other two can't. Resending a request that breaks a business rule gives the same answer every time, and so does resending a request the caller isn't authorised to make. That distinction between retryable and non-retryable errors is what the next lesson builds on. The tool has to make it visible, because the agent can't infer it from a generic message.
Checkpoint 4 of 5· Match them up
Match each failure to its category
Tap a term, then the definition that fits it.
The MCP spec lists API failures, input validation errors and business logic errors as tool execution errors. Permission failures come from the server's access controls. Each category allows a different next step.
“Input validation errors (e.g., date in wrong format, value out of range) Business logic errors”Source: modelcontextprotocol.io
Sources2
4.Retryable vs non-retryable errors: metadata that prevents wasted retries
An agent that gets a failed result has to decide whether to try again. If the result does not say, the agent guesses, and a wrong guess costs turns. The Claude docs show how readily the model retries: when a tool request is invalid or missing parameters, Claude will retry 2-3 times with corrections before apologizing to the user. That is the right behavior for a validation error whose message says what to fix. It is wasted effort for a business-rule violation or a permission denial, where the same request fails identically every time. A bare "Operation failed" gives the agent no way to tell these cases apart, so it may retry a request that can never succeed, or give up on one that a short wait would have fixed.
The remedy is to put the retry decision in the error itself. Anthropic's tool-writing guidance says error responses should communicate specific and actionable improvements, rather than opaque error codes or tracebacks. Applied here, the message you compose should state three things: the category (transient, validation, business or permission), whether retrying the same call can ever work, and what to do instead. For a transient failure, say that the service was unavailable and that a later retry may succeed. For a validation failure, name the field and the rule so the agent can correct the input. For a business or permission failure, say plainly that retrying will not help and give a customer-friendly explanation the agent can pass on, or name who to escalate to.
The SDK gives you two places to carry this. The text in content is what Claude reads. A result may also include structuredContent, a JSON object holding the result as machine-readable data, returned alongside content. A field such as an error category or a retryable flag is a convention you define in your own tool design, not a field the MCP spec requires, so keep the wording in content clear enough that it works even if the structured copy is ignored. Used consistently, the metadata lets the agent or its coordinator retry only the failures that can succeed, and stop immediately on the rest.
Checkpoint 5 of 5· Check yourself
A refund tool rejects a request because it exceeds the policy limit and returns only "Operation failed" with isError: true. What is the most likely cost?
A generic message hides the category and whether retrying can help. A specific, actionable message lets the agent stop retrying and explain the limit instead.
“clearly communicate specific and actionable improvements, rather than opaque error codes or tracebacks”Source: www.anthropic.com
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Any tool failure, such as a downstream API outage, should be returned as a JSON-RPC protocol error.Why is that wrong?
Errors that originate in the tool go inside the result object with isError: true. Protocol errors are for unknown tools, malformed requests and server errors.
Covered in Two channels for failure: protocol errors and tool execution errors
2.Letting the exception propagate out of the handler is fine, because Claude will see the error either way.Why is that wrong?
An uncaught exception reaches Claude as raw text with no context. Return isError: true with a message you wrote so the agent knows what to do next.
Covered in Writing the error the agent reads
3.Setting isError: true is enough for the agent to choose the right recovery, so the message text does not matter.Why is that wrong?
The flag only says the call failed. The agent can retry invalid requests several times, so the message must say what to fix or that retrying will not help.
Covered in Retryable vs non-retryable errors: metadata that prevents wasted retries
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Protocol Errors indicate issues with the request structure itself that models are less likely to be able to fix”
↩︎ Two channels for failure: protocol errors and tool execution errors“Tool Execution Errors contain actionable feedback that language models can use to self-correct and retry with adjusted parameters”
↩︎ Two channels for failure: protocol errors and tool execution errors“should return a tool execution error that says so, so the model can recover by creating a new one”
↩︎ Writing the error the agent reads - 2.
“Malformed requests (requests that fail to satisfy CallToolRequest schema) Server errors”
↩︎ Two channels for failure: protocol errors and tool execution errors“Implement proper access controls”
↩︎ Transient, validation, business and permission errors“Reported in tool results with isError: true: API failures Input validation errors”
↩︎ Transient, validation, business and permission errors“Input validation errors (e.g., date in wrong format, value out of range) Business logic errors”
↩︎ Prediction“Input validation errors (e.g., date in wrong format, value out of range) Business logic errors”
↩︎ Checkpoint - 3.
“If not set, this is assumed to be false (the call was successful).”
↩︎ Two channels for failure: protocol errors and tool execution errors“Any errors that originate from the tool SHOULD be reported inside the result object, with isError set to true”
↩︎ Key concept“Any errors that originate from the tool SHOULD be reported inside the result object, with isError set to true”
↩︎ Exam trap 1“Otherwise, the LLM would not be able to see that an error occurred and self-correct.”
↩︎ Checkpoint - 4.
“Return isError: true to compose the message instead of surfacing the raw exception.”
↩︎ Writing the error the agent reads“Return the failure as a tool result so Claude can react to it.”
↩︎ Writing the error the agent reads“is_error marks this as a failed call rather than odd-looking data.”
↩︎ Writing the error the agent reads“a JSON object holding the result as machine-readable data, returned alongside content”
↩︎ Retryable vs non-retryable errors: metadata that prevents wasted retries“Return isError: true to compose the message instead of surfacing the raw exception.”
↩︎ Exam trap 2 - 5.
“Claude will retry 2-3 times with corrections before apologizing to the user.”
↩︎ Retryable vs non-retryable errors: metadata that prevents wasted retries“Claude will retry 2-3 times with corrections before apologizing to the user.”
↩︎ Exam trap 3 - 6.https://www.anthropic.com/engineering/writing-tools-for-agentsSecondary source
“clearly communicate specific and actionable improvements, rather than opaque error codes or tracebacks”
↩︎ Retryable vs non-retryable errors: metadata that prevents wasted retries