---
title: How to Write Error Messages for AI Agents (MCP Examples)
description: One wrong ID cost 12,689 tokens and told the agent to ask a human. Real MCP error messages, what was wrong with them, and how to rewrite them for agents.
canonical: https://spiritroom.app/blog/error-messages-for-ai-agents/
published: 2026-10-21
updated: 2026-10-21
---

> Markdown version of https://spiritroom.app/blog/error-messages-for-ai-agents/ for agents. Index: /llms.txt

[Blog](/blog/) / [Tool results and errors](/guides/tool-results-and-errors-for-agents/)

# How to write error messages for AI agents: before and after, from a production MCP server

An agent sends a malformed ID. That's a routine mistake, and the fix is one line. Instead the server answers with its tool's entire input specification, 12,689 tokens of it, plus a note telling the agent to stop and ask a human. Here's what real production errors look like to an agent, and how to rewrite them.

Leonid Bugaev · Published 21 Oct 2026

**TL;DR**

- **Errors are read by a model, not a developer.** The agent's next move depends only on what the error says, so say the fix.
- **Keep errors small.** On one production MCP server, one malformed ID returned 12,689 tokens, identical in 3 of 3 runs. The same mistake on sibling tools cost 110 and 150 tokens.
- **Don't dump the schema.** Validation errors were multi-line type trees from a schema library, with the actual fix never stated in one line.
- **Don't tell the agent to stop on its own typo.** All three malformed-ID errors carried guidance meaning “a person must decide”, and one said “do not repeat the change” on a read.
- **A good error has four parts:** what failed, what's allowed, where to get a valid value, and whether to retry. Return it as a tool result with `isError: true`.
[Why agent errors are different](#why)[What bad errors cost](#costs)[Before and after](#before-after)[Tell the agent what kind of error it is](#states)[Checklist](#checklist)[How we measured](#how-we-measured)[FAQ](#faq)

## Why errors for agents are different

A developer who hits a bad error opens the docs, searches the code or asks a colleague. An agent can't. All it has is the text of the error, the tool descriptions and what it has seen so far. Whatever the error says shapes its next call: fix and retry, try another tool, ask the user, or give up.

The MCP spec already separates two kinds of failure. **Protocol errors** (unknown tool, malformed request) are JSON-RPC errors that the model may never see. **Tool execution errors** (bad input, missing record, upstream failure) come back as a normal result with `isError: true`, and clients should show them to the model so it can self-correct. This post is about the second kind: what to put in them.

## What bad errors cost, measured

We sent deliberately malformed arguments to a 99-tool production MCP server and counted the replies in tokens (`o200k_base`).

Tool A: malformed ID

*110 tokens*

Tool B: malformed ID

*150 tokens*

Tool C: malformed ID

*12,689 tokens*

Tool C replied with its entire input specification, identical in 3 of 3 runs. That's more than many whole tool results, and the actual cause (“this ID isn't valid; get one from the list tool”) was buried somewhere inside. Agents make argument mistakes routinely, so every such slip can add about 12.7k tokens to the conversation and to every model request after it.

Size wasn't the only problem:

- **Type dumps instead of hints.** Validation errors were multi-line trees from the server's schema library, with internals like `readonly` and branded types, and lines like `Expected "approved", actual "to_review"` repeated three times. The one-line fix was never stated.
- **Guidance that said “stop”.** Each response carried a machine-readable guidance block. For all three malformed IDs it said `state: "human_input"`, meaning a person must decide. Two of them added “Do not repeat the change automatically”, and a *read* with a wrong field name got the same text.
- **Text and guidance that disagreed.** On a stale-version conflict, the text correctly said “re-read the item, then retry”, while the guidance block still said `human_input`. An agent that follows the structured field would stop and ask the user about a routine retry.

**Find where agents fail on your tools. Measure what fixes it.**

Bring your MCP server, CLI or API and the jobs your users need done. We run real agents against them, read every call, and hand back measured gaps and fixes.

[Test your MCP →](/#contact)

## Before and after

Field and tool names below are simplified. The shape of each “before” is what the server returned.

### 1. Invalid enum value

```
// Before: 15 lines of schema-library output
Invalid arguments: { readonly status?: ReadonlyArray<"approved" | "needs_review" | ...> ... }
└─ ["status"][0]
   └─ Expected "approved", actual "to_review"
   ...

// After: one line
status[0] "to_review" is not valid. Use one of: approved, needs_review, rejected.
("To review" in the app = needs_review.)
```

The “after” also bridges the word the user sees in the app to the value the API wants, which is exactly where agents went wrong.

### 2. Wrong field name on a read

```
// Before
Invalid arguments: { readonly itemId: The item id returned by list_items. } └─ ["itemId"] └─ is missing
guidance: { "state": "human_input",
            "reason": "Review the reported refusal before choosing another action. Do not repeat the change automatically." }

// After
get_item takes itemId (the "id" field from list_items), not personId.
guidance: { "state": "fix_args" }
```

### 3. Malformed ID on an update

```
// Before: the tool's full input specification, 12,689 tokens

// After
projectId "not-a-uuid" is not a valid id. Call list_projects and pass one of its ids.
guidance: { "state": "fix_args", "next": "list_projects" }
```

### 4. Stale version

```
// Before
The item changed since you read it ... Call get_item to re-read its current version, then retry.
guidance: { "state": "human_input", "reason": "... Do not repeat the change automatically." }

// After
The item changed since you read it (your version 7, current 9). Call get_item, then retry with expectedVersion 9.
guidance: { "state": "retry", "next": "get_item" }
```

In each “after”, the recovery step names a tool the agent can call. A recent study of 3,001 error messages from 150 public MCP servers also tested recovery: naming a server tool in the step, instead of a terminal command or a bare “wait”, raised recovery on expired credentials to 84% and on rate limits to 88% ([arXiv, 2026](https://arxiv.org/html/2609.35381v2)).

## Tell the agent what kind of error it is

A small set of typed states lets the agent pick the right move without parsing prose. Use the same field on every tool, and keep it consistent with the text.

A typed state for every refusal

| State | Meaning | Example |
| --- | --- | --- |
| `fix_args` | Your arguments are wrong; here's what's allowed | Bad enum, malformed ID, wrong field name |
| `retry` | Do this first, then call again | Stale version, transient upstream failure |
| `wrong_state` | Not possible yet; this tool moves it forward | Step called out of order |
| `needs_user` | A person must decide | Spending, sending, deleting, ambiguous intent |
| `stop` | Don't retry; nothing the agent can do | Permission the user doesn't have |

A second production MCP server we looked at uses a similar set and its tool descriptions tell the agent that most refusals are answers to act on, not bugs to report. That framing is worth copying.

Results matter as much as errors: what a successful call should tell the agent next is in [tool results and errors for agents](/guides/tool-results-and-errors-for-agents/). How size adds up across a whole run is in [the 12,689-token typo](/blog/verbose-validation-errors-tokens/).

## Checklist for agent-facing errors

1. Return tool failures as results with `isError: true`, not protocol errors.
2. Name the input that failed and the value that was sent.
3. Say what's allowed: the list of values, the format, or the range.
4. Say where to get a valid value, by tool name.
5. Say whether to fix arguments, retry, wait for a state, ask the user, or stop, in the text and in one consistent structured field.
6. Reserve “ask the user” for real decisions.
7. Show only the failing path, never the whole schema. Aim for tens of tokens, not thousands.
8. Bridge app words to API values where users and the API differ.
9. Test it: send malformed arguments to every tool and count the reply in tokens. See [how to measure MCP server quality](/blog/measure-mcp-server-quality/).

**Go deeper:** [Typed refusals for agents](/blog/agent-refusal-states/), [Tool annotations agents rely on](/blog/mcp-tool-annotations-destructive-hint/), [How to optimise your MCP tools](/blog/optimise-mcp-tools-for-agents/).

## How we measured

- **Server:** a 99-tool production MCP server, 16 Oct 2026. Reads went through a read-only proxy. A few deliberately invalid write calls (malformed IDs, a stale version) were refused by the server, so nothing changed.
- **Probes:** fixed malformed calls (an ID that isn't a UUID, an invalid enum value, a wrong field name, a stale version) on several tools. Replies were recorded as bodies only and counted with `o200k_base`. The 12,689-token reply was identical in 3 of 3 runs.
- **Not measured here:** how often agents recover with the old vs rewritten errors. That needs an A/B on a mock and is planned. For recovery rates, see the arXiv study linked above.
- **Rewrites:** the “after” examples are our proposals, with field and tool names simplified.

## Sources

- Spirit Room error probes on a production MCP server, 16 Oct 2026.
- [MCP specification: tools, error handling](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
- [MCP TypeScript SDK: errors](https://ts.sdk.modelcontextprotocol.io/v2/servers/errors.html) and [Python SDK: handling errors](https://py.sdk.modelcontextprotocol.io/servers/handling-errors/).
- [MCP error messages written for developers hurt the most capable agents most](https://arxiv.org/html/2609.35381v2), arXiv, 2026.
- [RFC 9457: Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html).

## FAQ

### How should an MCP tool return an error to an AI agent?

As a normal tool result with isError: true and a short text message the model can act on, not as a JSON-RPC protocol error. The MCP spec says clients should pass tool execution errors to the model so it can self-correct; protocol errors may never reach it.

### What should an error message for an AI agent contain?

Four things: which input failed and what was sent, what is allowed, where to get a valid value (ideally a tool name), and whether to retry, fix the arguments, or ask the user. Keep it to a line or two.

### How long should an MCP error message be?

Short: tens to a few hundred tokens. On one production server the same malformed-ID mistake cost 110 tokens on one tool and 12,689 on another, because the second echoed its whole input specification. The extra length carried no extra help.

### Should an error tell the agent to ask the user?

Only when a person really has to decide, such as spending money or a conflicting instruction. A malformed ID or a stale version is the agent's to fix: say how, and let it retry.

### Do I still need RFC 9457 problem details for an MCP server?

Problem details are a good structure for HTTP APIs, and fields like type and detail map well onto agent errors. For MCP, what matters is the text the model reads in the tool result: name the fix in plain words.

**Find where agents fail on your tools. Measure what fixes it.**

Bring your MCP server, CLI or API and the jobs your users need done. We run real agents against them, read every call, and hand back measured gaps and fixes.

[Test your MCP →](/#contact)

## Keep reading

- Up[Tool results and errors for AI agents (guide)](/guides/tool-results-and-errors-for-agents/)
- Related[The 12,689-token typo: how one validation error can eat an agent's context](/blog/verbose-validation-errors-tokens/)
- Related[Retry, fix the arguments, or ask the human? Refusal states agents can act on](/blog/agent-refusal-states/)
- Related[When your UI says “To review” and your API says needs\_review: vocabulary mismatches that break agents](/blog/agent-vocabulary-mismatch/)
