Skip to main content
Tools & MCP

MCP Tool Error Handling: isError, Retries, Empty Results

MCP tool error handling for CCAR-F: the isError result shape, four error categories, when to retry, and why a failed lookup must never look like an empty one.

The CertGrove team · 7 min read

A support agent calls lookup_order, the order service times out, and the MCP tool returns "Operation failed." The agent tries the same call four more times, then tells the customer the order does not exist. Good MCP tool error handling prevents both mistakes. This post covers the error categories the CCAR-F exam expects, the isError result shape, how to tell a failed lookup from an empty one, and where recovery should happen in a multi-agent system.

The material maps to task statement 2.2 (structured error responses for MCP tools) in Domain 2, and task statement 5.3 (error propagation across multi-agent systems) in Domain 5. Expect it in Scenario 1, the customer support agent whose backend is reached through MCP tools, and in Scenario 3, the multi-agent research system.

What does isError mean in an MCP tool result?

isError: true tells the client that the tool ran but its operation failed. The MCP spec calls these tool execution errors: API failures, input validation errors and business logic errors. They come back inside a normal result, and the spec says clients SHOULD pass them to the model so it can self-correct.

The spec separates them from protocol errors: an unknown tool, a malformed request, a server error. Those come back as standard JSON-RPC errors with a code, and the model is less likely to fix them.

Here is a tool execution error for a timed-out order lookup. MCP defines content and isError. The JSON inside the text block is your own contract:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"errorCategory\": \"transient\", \"isRetryable\": true, \"description\": \"Order service timed out after 5s. A retry in a few seconds may succeed.\", \"attempted\": {\"order_id\": \"ORD-99172\"}}"
      }
    ],
    "isError": true
  }
}

⚠ The newest MCP spec revision (2026-07-28) also adds "resultType": "complete" to results. The exam guide does not mention it, and the isError pattern is the same in both versions.

When your application forwards this to the Claude Messages API, the flag becomes is_error on the tool_result block. Keep it set, or Claude reads the failure as normal output.

MCP tool error handling: transient, validation, business, permission

The exam guide lists four error types, and each needs a different next move. A timeout may clear on retry; a permission error never will.

Error typeExampleRetry the same call?Agent action
TransientOrder service timeout, 503Yes, a limited number of timesRetry locally, then report with what was tried
Validationdate: "last Tuesday-ish"No, not unchangedFix the input or ask the user for it, then call again
Business ruleRefund requested 45 days after a 30-day windowNoExplain the rule to the customer and offer allowed options
PermissionAccount cannot read the billing collectionNoStop calling; escalate or report the access gap
Valid empty resultSearch ran, zero orders matchNot an errorReport "no matches" as a finding

One detail: the guide's skills bullet lists errorCategory values as transient, validation and permission, and handles business errors with a retriable: false flag plus a customer-friendly explanation. Adding a business category is fine. The exam tests behavior, not an enum.

Why "Operation failed" is a trap

A generic message makes a timeout and a malformed date look identical, so the agent retries both or abandons both. The exam guide says uniform error responses prevent appropriate recovery decisions. Keep the response shape consistent and make the content specific.

How to return a structured error response from a tool

Return errorCategory, an isRetryable boolean and a human-readable description, which are the three fields the exam guide names. For business errors, add the explanation the agent can repeat to the customer. A small helper keeps every tool consistent:

import json

def tool_error(category, retryable, description, **details):
    payload = {"errorCategory": category, "isRetryable": retryable,
               "description": description, **details}
    return {"content": [{"type": "text", "text": json.dumps(payload)}],
            "isError": True}

refund_refused = tool_error(
    "business", False,
    "Refund window closed: order delivered 2026-09-01, policy allows 30 days.",
    customer_message="This order is outside our 30-day refund window, "
                     "but it is still eligible for store credit.",
)

isRetryable: false means "do not repeat this exact call." A new call after fixing the input is fine. The flag enforces nothing by itself; your prompt or code must honor it.

Failed lookup vs empty result: why the difference matters

A query that ran and matched nothing is a successful answer: "this customer has no orders in March." A query that never reached the database tells you nothing. If both come back as an empty list with success status, the agent will confidently report that something does not exist when nobody checked.

The exam guide raises this distinction twice, in 2.2 and again in 5.3. Return a valid empty result as success with an empty collection. Return an access failure with isError: true and a retry decision. Never catch a timeout and return [] as success. Task 5.3 names that "silent suppression" as an anti-pattern.

Local recovery in a subagent vs propagating to the coordinator

A subagent should retry transient failures itself, within a limit, and send up only the errors it cannot resolve. When it does propagate, it includes the failure type, what it attempted, any partial results and possible alternatives, so the coordinator can decide what happens next.

flowchart TD
    A[Tool returns isError true] --> B{errorCategory}
    B -- transient --> C{Retries left?}
    C -- yes --> D[Retry locally]
    C -- no --> E[Propagate: type, attempts, partial results, alternatives]
    B -- validation --> F[Fix input or ask user]
    B -- business --> G[Explain rule to customer]
    B -- permission --> E
    E --> H[Coordinator: re-delegate, use another source, or annotate the gap]

In the Agent SDK, the coordinator sees only a subagent's final report, never its intermediate tool calls. A bare "search unavailable" status hides the context it needs.

The opposite mistake, ending the whole workflow because one subagent failed, is also a 5.3 anti-pattern. Keep what succeeded, and have the synthesis mark which areas have gaps due to unavailable sources.

Try it

A research coordinator sends four subagents to search different archives. The patents subagent's source returns HTTP 503 on three attempts. The other three subagents finish. What should the patents subagent return?

A) An empty result list with success status, so the coordinator can finish the report. B) A structured error with failure type transient, the query it ran, its three attempts, any partial results and an alternative source to try. C) The message "patent search unavailable," so the coordinator knows to skip it. D) A signal to abort the run, since the report would be incomplete.

Answer: B. The subagent already tried local recovery. Now it gives the coordinator what it needs to choose the next step.

  • A is silent suppression. The report would claim no relevant patents exist when the archive was never searched.
  • C is a generic status. The coordinator cannot tell a short outage from a revoked API key, or see what was tried.
  • D discards three successful searches over one failure, the other anti-pattern task 5.3 names.

Key takeaways

  • isError: true marks a tool execution failure inside a normal result; protocol errors use JSON-RPC error instead.
  • Classify every failure as transient, validation, business or permission, and return errorCategory, isRetryable and a readable description.
  • Business refusals are non-retryable and should carry an explanation the agent can give the customer.
  • An empty result from a successful query is a finding. A failed query is an unknown. Never let them look the same.
  • Subagents retry transient errors locally and propagate the rest with attempts, partial results and alternatives.

Frequently asked questions

The CertGrove teamBilingual study notes for developers preparing for Claude Certified Architect — Foundations.