MCP tool errors, and tools that models use well

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

  1. 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: true is for everything else, such as bad input or a broken business rule.
  2. Clients SHOULD pass the second kind to the model, and only MAY pass the first.
  3. In the Python SDK, raise ToolError with a message that says what a valid value looks like. An unexpected exception also becomes an isError result, but the model sees only the tool’s name.
  4. Servers MUST validate inputs, control access, rate-limit calls and sanitize outputs.
  5. 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:

Something went wrong → Is the request itself invalid? unknown tool, malformed request, unexpected server failure; Is the request itself invalid? unknown tool, malformed request, unexpected server failure → Protocol error JSON-RPC error object e.g. -32602 Unknown tool: yes; Is the request itself invalid? unknown tool, malformed request, unexpected server failure → Tool execution error result with isError true + actionable message bad input, business rule, API failure: noSomething went wrongIs the request itself invalid?unknown tool, malformed request,unexpected server failureProtocol errorJSON-RPC error objecte.g. -32602 Unknown toolTool execution errorresult with isError true+ actionable messagebad input, business rule, API failureyesno

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 callWhat the client gets
raise ToolError(...)An isError result: Error executing tool <name>: and your message
An unexpected exception, such as a ValueErrorAn 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 schemaAn isError result with Pydantic’s validation message; your function never runs
A tool name the server doesn’t haveAn 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 plus resource_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 _meta keys carefully. Claude Code honours keys such as anthropic/maxResultSizeChars (raises the 50,000-character threshold for one tool), anthropic/alwaysLoad (loads the tool up front, not through tool search) and anthropic/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 runresultType: "input_required", a Multi Round-Trip Request (MRTR)
Minutes or hours of workresultType: "task", from the Tasks extension, only if the request opted in
To report progress while it runsnotifications/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 _meta keys are as its MCP documentation gave them on 9 October 2026.
  • The Python sample ran against mcp 2.3.0 on 9 October 2026. The table and the low-level Server result come from the same run, which added a tool without its own check, an unknown tool name and a low-level Server.
  • Where the Python SDK differs from my notes (what an exception in a tool becomes), this article describes mcp 2.3.0.
Series What an MCP server actually does in 2026 Part 9 of 35
  1. What an MCP server does in 2026: much more than a list of tools
  2. MCP goes stateless: what changed in the 2026-07-28 specification
  3. server/discover: the one method every MCP server must implement
  4. Server instructions: the paragraph every model reads first
  5. Stateless MCP requests: _meta, resultType and explicit handles
  6. Caching hints and pagination in MCP
  7. MCP transports in 2026: stdio and Streamable HTTP
  8. Designing an MCP tool: the definition and the result
  9. MCP tool errors, and tools that models use well
  10. MCP resources: context the application decides to use
  11. MCP prompts: slash commands your server ships
  12. Tool, resource or prompt? Choosing the right MCP primitive

I’m Amir Pournasserian. I build AI and platform systems for a living, maintain FluentCMS and YeSvelte, and write here about what I find along the way.