MCP tool errors, and tools that models use well

The tool bug I see most often is an error the model can’t read: a tool fails on bad input, and the model learns nothing it can act on. What surprised me in Python is what the official software development kit (SDK) does with an exception you didn’t plan for: the model gets an error result, but not your exception’s text, so it still can’t tell what to fix.
This is part 9 of my series on what a Model Context Protocol (MCP) server does under the 2026-07-28 specification, after the tool definition and its result in part 8. The facts are as I read them in October 2026.
In brief
- A tool reports a failure in one of two ways. A protocol error is for a request that is itself invalid, such as a call to an unknown tool; a result with
isError: trueis for everything else, such as bad input or a broken business rule. - Clients SHOULD pass the second kind to the model, and only MAY pass the first.
- In the Python SDK, raise
ToolErrorwith a message that says what a valid value looks like. An unexpected exception also becomes anisErrorresult, but the model sees only the tool’s name. - Servers MUST validate inputs, control access, rate-limit calls and sanitize outputs.
- My advice: build few, sharp, well-described tools, with small outputs, idempotent writes, and reads kept apart from writes.
Two kinds of errors
The tools page of the specification gives a tool two ways to report a failure, chosen by whether the request itself is invalid:
A protocol error is a JSON-RPC 2.0 error object in place of a result. A malformed request is one that doesn’t fit the shape of a tools/call at all; arguments that fail the tool’s input schema, a broken business rule or a failed call to an upstream application programming interface (API) are tool execution errors, as my host rulebook also records. It comes back as an ordinary result:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "Invalid departure date: must be in the future. Current date is 2026-10-08." }],
"isError": true
}
}
This matters because clients SHOULD pass tool execution errors to the model so it can correct itself, and MAY pass protocol errors, which are less likely to lead to a successful recovery. An expired or unknown handle is a tool execution error too; part 5 shows one.
The spec already makes validating every tool input a server MUST (see the security duties below). My advice is about what the model sees when a check fails: return isError: true with a message that says what was wrong and what a valid value looks like.
In Python: ToolError and MCPError
With the official Python SDK, mcp 2.3.0, each mechanism has its exception. The SDK’s page on handling errors draws the same line: raise ToolError when what the tool tried didn’t work, an upstream API timeout included, and MCPError when the request itself should be rejected. A bad date is the model’s to fix; a server without its key can’t serve anyone:
import os
from datetime import date
from mcp import MCPError
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import INTERNAL_ERROR
mcp = MCPServer("flights")
@mcp.tool()
def search_flights(departure_date: str) -> str:
"""Search flights for one day. departure_date is a future date as YYYY-MM-DD."""
if "FARES_API_KEY" not in os.environ:
# No model can fix this: the server itself isn't set up.
raise MCPError(
code=INTERNAL_ERROR,
message="Flight search is not configured on this server.",
)
try:
day = date.fromisoformat(departure_date)
except ValueError:
day = None
if day is None or day <= date.today():
raise ToolError(
f"Invalid departure date {departure_date!r}: "
f"use a future date as YYYY-MM-DD. Today is {date.today()}."
)
return f"No flights found on {day}."
With FARES_API_KEY set, the SDK’s in-memory Client (which speaks 2026-07-28) got “next Friday” back as a result with is_error set. Its text is my message, after a prefix the SDK adds:
Error executing tool search_flights: Invalid departure date 'next Friday': use a future date as YYYY-MM-DD. Today is 2026-10-09.
Without FARES_API_KEY, the client got no result: it raised the protocol error as an MCPError with code -32603. Here is what MCPServer returned for each failure I tried:
| In the tool or the call | What the client gets |
|---|---|
raise ToolError(...) | An isError result: Error executing tool <name>: and your message |
An unexpected exception, such as a ValueError | An isError result with only Error executing tool <name>; the traceback goes to the server’s log |
raise MCPError(...) | A JSON-RPC error with your code and message |
| Arguments that fail the input schema | An isError result with Pydantic’s validation message; your function never runs |
| A tool name the server doesn’t have | An isError result: Unknown tool: <name> |
The second row is the trap: the model learns that the call failed, not why. The Claude connector directory’s review criteria, as I recorded them in what AI hosts require of an MCP server, rule out generic errors. The last row differs from the spec, which answers an unknown tool with a protocol error, for example -32602. The SDK’s low-level Server differs again: any exception other than MCPError in a handler reaches the client as a -32603 protocol error, Internal server error.
Security duties on both sides
The tools page closes with duties for both sides of a call. Servers MUST:
- validate all tool inputs;
- implement proper access controls;
- rate-limit tool invocations;
- sanitize tool outputs.
Clients SHOULD, among other things:
- prompt for confirmation on sensitive operations;
- show tool inputs to the user before calling the server, to prevent data exfiltration;
- validate results before passing them to the model;
- enforce timeouts;
- log tool usage for audit.
Designing tools that models use well
The specification stops at the wire; these lessons are mine:
- Fewer, sharper tools. Ten well-described tools beat fifty thin wrappers around a web API’s endpoints. Every tool definition costs context, even with tool search.
- Descriptions are prompts. Say when to use the tool, when not to, and what comes back, with the most important sentence first, because some hosts cut long descriptions (part 4 gives Claude Code’s limit). My post on what AI hosts require of an MCP server has more on how descriptions are judged.
- Control output size. As of October 2026, Claude Code warns when a tool’s output exceeds 10,000 tokens and limits it to 25,000 tokens by default. It also saves a successful text result over 50,000 characters to a file, whatever its token count, unless the tool raises that threshold with
anthropic/maxResultSizeChars. Return summaries plusresource_links rather than whole documents. - Make writes idempotent. Accept a client-supplied idempotency key on tools that create things: streams drop and requests get re-issued, as part 7 explains.
- Separate reads from writes. Read-only tools are easier for hosts to auto-approve, and a tool that mixes a read and a write forces a confirmation prompt every time. As of October 2026, the Claude connector directory asks for reads and writes in separate tools; part 8 covers the annotations that mark which is which.
- Use vendor
_metakeys carefully. Claude Code honours keys such asanthropic/maxResultSizeChars(raises the 50,000-character threshold for one tool),anthropic/alwaysLoad(loads the tool up front, not through tool search) andanthropic/requiresUserInteraction(asks for approval on every call). Other hosts ignore them; a later part covers vendor keys.
When a call can’t finish in one go
A tools/call doesn’t always return a final result straight away. Each of these gets a part of its own later in this series:
| The tool needs… | What the server uses |
|---|---|
| A confirmation or a missing detail before it can run | resultType: "input_required", a Multi Round-Trip Request (MRTR) |
| Minutes or hours of work | resultType: "task", from the Tasks extension, only if the request opted in |
| To report progress while it runs | notifications/progress on the response stream |
| To render an interactive user interface (UI) | _meta.ui.resourceUri on the tool definition, from MCP Apps |
Method and caveats
- Built from the second half of my guide’s tools chapter, written against the 2026-07-28 specification.
- Claude Code’s limits and
_metakeys are as its MCP documentation gave them on 9 October 2026. - The Python sample ran against
mcp2.3.0 on 9 October 2026. The table and the low-levelServerresult come from the same run, which added a tool without its own check, an unknown tool name and a low-levelServer. - Where the Python SDK differs from my notes (what an exception in a tool becomes), this article describes
mcp2.3.0.
Series What an MCP server actually does in 2026 Part 9 of 35
- What an MCP server does in 2026: much more than a list of tools
- MCP goes stateless: what changed in the 2026-07-28 specification
- server/discover: the one method every MCP server must implement
- Server instructions: the paragraph every model reads first
- Stateless MCP requests: _meta, resultType and explicit handles
- Caching hints and pagination in MCP
- MCP transports in 2026: stdio and Streamable HTTP
- Designing an MCP tool: the definition and the result
- MCP tool errors, and tools that models use well
- MCP resources: context the application decides to use
- MCP prompts: slash commands your server ships
- Tool, resource or prompt? Choosing the right MCP primitive