Rubric
Contents — domains, guide and mocks

Structured errors for MCP tools

CCAR-F 2.211 min read · checked 21 September 2026

Task statementImplement structured error responses for MCP tools

Every failure needs a different recovery

What kind of outcome is this?
  • Transient: timeout, rate limit
    Retry after a waitisRetryable: true
  • Validation: bad input
    Fix input, try againsay what was wrong
  • Business rule or permission
    Don’t retry — explainor escalate to a human
  • Query ran, nothing matched
    Not an errorsuccess with zero results
The agent can only take the right branch if the tool says which branch it is on. A bare “Operation failed” collapses all four into a guess.

Two kinds of error in MCP

The MCP specification separates two reporting channels. Protocol errors are standard JSON-RPC errors: an unknown tool name, a request that does not fit the tools/call schema, a server fault. Tool execution errors are reported inside a normal tool result with isError: true: an API that failed, input that broke a rule (a date in the past, a value out of range), a business-logic refusal. The spec is explicit about why the split matters: execution errors carry actionable feedback a model can use to self-correct, so clients should pass them to the model; protocol errors are less likely to lead to recovery.

Where a failure is reported

Protocol error

  • JSON-RPC error: unknown tool, bad request
  • The call never really ran
  • The model may never see it, or can do little with it

Tool execution error

  • isError: true: the tool ran and failed
  • Returned in the result content, where the model reads it
  • Worded so the model can retry, adapt or explain

The same idea exists in the Messages API: a tool_result block takes an optional is_error: true. Anthropic’s docs advise writing instructive messages — not “failed”, but what went wrong and what to try next, such as “Rate limit exceeded. Retry after 60 seconds.” They also note that when a call is invalid, for example missing a required parameter, Claude will usually retry two or three times with corrections before apologising to the user. That is useful for validation errors and wasteful for anything that can never succeed.

What a structured error contains

isError says only that something went wrong. To choose a recovery, the agent needs to know which kind of wrong. That is what “structured” in this task statement points to: return metadata alongside the flag — a category, whether retrying could help, and a human-readable explanation. For business-rule failures, add wording the agent can pass on to a customer, so it explains instead of improvising.

The same failure, reported two ways

Uniform error

isError: true
content[0].text:

"Operation failed"

Structured error

isError: true
content[0].text:

{
  "errorCategory": "business",
  "isRetryable": false,
  "reason": "Refund exceeds the $500
             agent approval limit",
  "customerMessage": "This refund
     needs a manager's approval.
     I've passed it on."
}
Both results set isError: true. Only the one on the right tells the agent not to retry and gives it the words to use with the customer.
CategoryTypical causeRetryable?What the agent should do
transientTimeout, rate limit, service briefly downYesWait and retry a bounded number of times
validationWrong format, missing field, out-of-range valueAfter fixing the inputCorrect the input from the message, then call again
businessPolicy forbids it: over a limit, outside a windowNoExplain to the user, or escalate
permissionCaller not allowed to see or change thisNoStop; escalate or ask for access — do not probe
An Agent SDK tool that reports errors by categorypython
import json
from claude_agent_sdk import tool

def fail(category, retryable, reason, customer_message=None):
    body = {"errorCategory": category, "isRetryable": retryable,
            "reason": reason, "customerMessage": customer_message}
    return {"content": [{"type": "text", "text": json.dumps(body)}],
            "is_error": True}              # Python key; TypeScript uses isError

@tool("billing_issue_refund", "Issue an approved refund on an order. ...",
      {"order_number": str, "amount": float})
async def billing_issue_refund(args):
    if args["amount"] <= 0:
        return fail("validation", True, "amount must be greater than 0")
    if args["amount"] > 500:
        return fail("business", False, "Over the $500 agent limit",
                    "This refund needs a manager's approval.")
    try:
        ref = await payments.refund(        # your payments client
            args["order_number"], args["amount"])
    except TimeoutError:
        return fail("transient", True, "Payments timed out; retry in 30s")
    return {"content": [{"type": "text", "text": f"Refunded. Reference {ref}"}]}

Catching the exception yourself matters. The Agent SDK docs note that an uncaught exception in a handler does not stop the loop: the in-process MCP server turns it into an error result carrying the raw exception text. The agent then sees something like a bare TimeoutError with no guidance. Anthropic’s engineering team makes the same point about any tool: send specific, actionable messages rather than opaque error codes or tracebacks.

An empty result is not an error

The most expensive confusion in this area runs the other way. A search that ran successfully and found nothing is a valid answer — return it as a normal result that says so. A search that could not run at all (the index was offline, access was denied) is a failure and must be flagged. If both come back as an empty list, the agent will tell a customer “you have no open orders” when in fact the order system was down.

Recover locally, escalate what’s left

In a multi-agent system the same structure decides who handles a failure. Anthropic’s research system found that telling an agent a tool is failing and letting it adapt works surprisingly well, backed by retries and checkpoints so work resumes rather than restarts. A sound pattern follows from that: a subagent handles transient failures itself, and only escalates what it cannot resolve — with the partial results it did obtain and a note of what it tried, so the coordinator can decide without repeating the work.

A subagent absorbs what it can

Coordinator
Search subagent
MCP tool
Step 1: Coordinator to Search subagent: Find filings for three companies
Step 2: Search subagent to MCP tool: Search company A
Step 3: MCP tool to Search subagent: transient, retryable
Step 4: Search subagent to MCP tool: Retry company A
Step 5: MCP tool to Search subagent: Results for A
Step 6: Search subagent to MCP tool: Search companies B and C
Step 7: MCP tool to Search subagent: B results · C permission error
Step 8: Search subagent to Coordinator: A, B found; C blocked, not retried
The coordinator never hears about the timeout that a retry fixed. It hears about the permission failure, together with what was already found.

Traps the wrong answers are built from

Tempting but wrongDo this instead
Returning “Operation failed” for every problemReturn a category, isRetryable and a specific explanation with isError: true.
Catching errors and returning an empty resultReturn empty only when the query ran and matched nothing; flag failures as errors.
Letting raw exceptions or tracebacks reach the modelCatch them in the handler and compose an actionable message.
Retrying business-rule or permission failuresMark them non-retryable and give the agent wording to explain or escalate.
Escalating every error to the coordinatorRecover transient errors in the subagent; escalate the rest with partial results and what was tried.

You should now be able to

  • Distinguish MCP protocol errors from tool execution errors reported with isError.
  • Return structured error metadata: category, retryable flag and a human-readable reason.
  • Give business-rule failures a customer-safe explanation and a non-retryable flag.
  • Keep valid empty results distinct from access and availability failures.
  • Design subagents to recover transient errors locally and escalate the rest with context.

Practice questions

Original questions written for this lesson, in the exam’s style. Answer first, then open the reasoning — every option is explained, including why the wrong ones are tempting.

  1. Question 1

    An airline rebooking agent calls rebook_flight. When a fare rule forbids a change, the tool returns isError: true with the text “Operation failed”. The agent retries the same call four times, then tells the passenger the system is down.

    What change best fixes this behaviour?

    1. ATell the agent in its system prompt never to retry rebook_flight calls at all.
    2. BFlag it as a non-retryable business error with a passenger-safe reason.
    3. CStop setting isError, so the agent treats the text as an ordinary result.
    4. DReturn a JSON-RPC protocol error instead of a normal tool result.
    Show answer and reasoning
    1. AIncorrect. That also stops retries of genuine timeouts, which should be retried.
    2. BCorrect. The agent learns this failure cannot succeed on retry and gets accurate wording to explain the fare rule.
    3. CIncorrect. Hiding the failure makes it worse: the agent may tell the passenger the change went through.
    4. DIncorrect. Protocol errors are for malformed requests; the model is less able to see or act on them.
  2. Question 2

    A CRM lookup tool wraps every call in a try/except that returns an empty list on any exception. During a network outage, a sales agent tells several account managers that their key accounts have no open opportunities.

    What is the core design flaw?

    1. AThe tool should return more fields per opportunity.
    2. BThe agent should double-check every empty result by calling again.
    3. CThe tool needs a longer timeout so the outage is less likely to matter.
    4. DAn access failure and a valid empty result look the same to the agent.
    Show answer and reasoning
    1. AIncorrect. Field count is irrelevant; the tool returned nothing at all because it hid the failure.
    2. BIncorrect. Blind double-calls add cost and still cannot tell an outage from a real empty result.
    3. CIncorrect. Timeouts will still happen, and the failure will still look like a valid empty answer.
    4. DCorrect. Empty should mean “the query ran and found nothing”. A failure must be flagged so the agent retries or reports it.
  3. Question 3

    In a research system, a document subagent hits a rate limit on one source and a permission error on another. It currently returns “Unable to complete task” to the coordinator and discards the eight documents it had already analysed.

    How should the subagent handle these failures?

    1. ARetry the rate limit locally; escalate the permission error with the eight results.
    2. BKeep retrying both calls until they succeed, however long that takes.
    3. CSend both errors straight to the coordinator and let it do the retrying.
    4. DLeave out the failed sources and report success with the eight results.
    Show answer and reasoning
    1. ACorrect. Transient failures are recovered locally; the unresolvable one goes up with partial results and what was tried.
    2. BIncorrect. A permission error will not succeed on retry, and unbounded retries waste time and money.
    3. CIncorrect. The coordinator is further from the failure; a simple rate-limit retry belongs in the subagent.
    4. DIncorrect. Silently dropping sources hides a gap the coordinator needs to know about.

Sources

Drafted with AI assistance and checked against the sources above; expert review is in progress. Spotted an error? Tell us and it gets fixed, dated and listed on how this is written.