Error Handling and Tool Distribution Choices
How to design structured tool error responses and how to choose between a built-in tool, an MCP server, and a skill.
This lesson covers two related decisions that show up together on the exam: how a tool should report failure, and which mechanism — built-in tool, MCP server, or skill/slash command — should carry a given piece of functionality in the first place.
Structured error responses
When a tool call fails for any reason — invalid input that passed schema validation but failed at runtime, a downstream service being unavailable, a permission denial, a not-found condition — the response sent back to the model should always be a tool_result with is_error: true and a message describing what went wrong in plain, actionable language. This is what lets the model self-correct on the next turn: it can see that the call failed, understand roughly why, and either retry with adjusted input, try a different tool, or surface the problem to the user instead of confabulating a fake success.
What that error message should never contain is a raw stack trace, a database error code with no explanation, or an internal exception string. Those are meaningless to the model as guidance and often leak implementation details (table names, internal paths, library versions) that shouldn't be exposed. The fix is to catch the underlying error in your tool implementation and translate it into a short, human-readable sentence describing what went wrong and, where possible, what a valid retry would look like — for example, "no customer found with id cus_004, check the id format" rather than "KeyError: 'cus_004' at db/lookup.py:52."
Choosing how to expose a capability
Given some piece of functionality you want an agent to have, there are three common ways to expose it, and the exam expects you to pick the right one for a given scenario rather than defaulting to MCP for everything:
- Built-in tool: if a built-in tool already does what's needed — Bash, the text editor tool, web search, web fetch — use it rather than building a custom equivalent. Reimplementing "run a shell command" or "fetch a URL" as a custom MCP tool adds maintenance burden and duplicated surface area for no benefit over the tool Anthropic already ships and maintains.
- MCP server: use this for integrations with an external system — a SaaS API, a database, a ticketing system — especially when that integration needs to be reused across multiple projects or multiple host applications. The value of MCP is standardizing that integration once so it's portable; if you'll only ever use it in one project and it involves no external system, MCP is more infrastructure than the problem needs.
- Skill or slash command: use this for a project-local workflow that doesn't talk to any external system — packaging up a sequence of steps, a house style, or a repeatable procedure that's specific to one codebase or one team's way of working. There's no external system to integrate with, so standing up a server is unnecessary; a skill or slash command captures the workflow directly where it's used.
Scenario: a team wants their agent to (1) run repo-local lint-and-format checks before committing, (2) query their production Postgres database, and (3) fetch arbitrary web pages for research. The right distribution is: (1) a skill or slash command, since it's a local workflow with no external system; (2) an MCP server, since Postgres is an external system that multiple team members and projects will want to query the same way; (3) the built-in web fetch tool, since Claude already ships one and there's no reason to rebuild it.
Try it
Take a tool you've built or are considering building. Write out its failure modes (bad input, downstream timeout, not-found, permission denied) and draft the exact is_error:true message text for each — not a category, the literal sentence the model would receive. Then separately classify the tool itself: should it actually be a custom tool at all, or does it duplicate a built-in, belong in an MCP server, or belong in a skill instead?