Blog / Tool results and errors

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

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:

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

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).

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
StateMeaningExample
fix_argsYour arguments are wrong; here's what's allowedBad enum, malformed ID, wrong field name
retryDo this first, then call againStale version, transient upstream failure
wrong_stateNot possible yet; this tool moves it forwardStep called out of order
needs_userA person must decideSpending, sending, deleting, ambiguous intent
stopDon't retry; nothing the agent can doPermission 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. How size adds up across a whole run is in the 12,689-token typo (coming soon).

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.

Go deeper: 5 ways to optimise your MCP tools.

How we measured

Sources

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