Menu

AI

Lesson 20: Handle Errors Well

Learn the two kinds of MCP errors, when to use each in Python and TypeScript, and how to write error messages an AI model can recover from.

By: Irina ShvayaOctober 28, 2026

Lesson 20: Handle Errors Well

This lesson is part of Learn MCP, our free course on the Model Context Protocol, within The MCP Guide.

In this lesson you will learn the two kinds of MCP errors and when to use each. You will raise a fixable error, a request-level error and an unexpected crash in Python. Then you will compare TypeScript and rewrite error messages so a model can recover.

Good errors matter more in MCP than in a normal API. The caller is often an AI model, and the error message is the only clue it gets. A clear message lets it fix its own mistake. A vague one makes it give up or guess.

The code adapts the Python error-handling guide and the TypeScript Errors guide. It ran on 28 October 2026 with mcp 2.3.0 and @modelcontextprotocol/server 2.4.0.

Key Takeaways

  • The spec defines two error paths: tool execution errors (isError: true, which the model reads) and protocol errors (JSON-RPC errors, which the host handles).
  • Use a tool error when a smarter call could succeed; use a protocol error when no retry from the model can fix it.
  • Python hides the text of unexpected exceptions from the model; TypeScript sends a thrown error's message to the model.
  • Resource and prompt failures are always protocol errors, because no model reads them.
  • Put the fix in the message: what was wrong, what is valid, and what to call next.

Step 1: Learn the two paths from the spec

The tools spec says tools use two error reporting mechanisms:

Tool execution errorProtocol error
What it isA normal result with isError: trueA JSON-RPC error response
Who reads itThe modelThe host application
Spec examplesAPI failures, input validation errors, business logic errorsUnknown tool, malformed request, server errors
Can the model retry?Yes, that is the pointRarely

The spec adds that clients should provide tool execution errors to language models so they can self-correct. Clients may pass protocol errors along, but those "are less likely to result in successful recovery."

The Python guide reduces the choice to one question: could a smarter model have avoided this? If yes, use a tool error. If no, use a protocol error.

Step 2: Raise the right error in Python

from mcp import MCPError
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import INVALID_REQUEST

mcp = MCPServer("Bookshop")

CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
MAINTENANCE = False


@mcp.tool()
def get_author(title: str) -> str:
    """Look up the author of a book. Use an exact title."""
    if MAINTENANCE:
        # Nothing the model can do about this: reject the request itself.
        raise MCPError(code=INVALID_REQUEST, message="Catalog is in maintenance. Try again later.")
    if title not in CATALOG:
        # The model can fix this: tell it how.
        known = ", ".join(sorted(CATALOG))
        raise ToolError(f"No book titled {title!r}. Known titles: {known}.")
    return CATALOG[title]


@mcp.tool()
def get_author_unsafe(title: str) -> str:
    """Same lookup, with no check (shows what a crash looks like)."""
    return CATALOG[title]

Calling each from an in-memory test client gave this on 28 October 2026:

ToolError -> True Error executing tool get_author: No book titled 'Dun'. Known titles: Dune, Neuromancer.
KeyError  -> True Error executing tool get_author_unsafe
MCPError  -> -32600 Catalog is in maintenance. Try again later.

What happened, matching the Python guide:

  • ToolError returned a result with is_error=True and your message. The model can read "Known titles" and retry.
  • An unexpected KeyError also returned is_error=True, but only the text "Error executing tool get_author_unsafe." The guide explains that exception text "may describe your server's internals, so it never leaves the server." The full traceback went to the server log at ERROR level.
  • MCPError was not caught. The whole request failed with a JSON-RPC error, and the client raised an exception. The model saw nothing.

One more rule from the guide: never return an error message as a normal string. A returned string has is_error=False, so the model and the host both think the tool worked.

Step 3: Compare TypeScript

The TypeScript SDK draws the line differently, so read this section even if you only write Python.

server.registerTool(
    'get_author',
    { description: 'Look up the author of a book. Use an exact title.', inputSchema: z.object({ title: z.string() }) },
    async ({ title }) => {
        const author = CATALOG[title];
        if (!author) {
            return {
                content: [{ type: 'text', text: `No book titled "${title}". Known titles: ${Object.keys(CATALOG).join(', ')}.` }],
                isError: true
            };
        }
        return { content: [{ type: 'text', text: author }] };
    }
);

Three variants ran against an in-process client:

Handler did thisResult the client got
Returned isError: true with a messageisError: true, your message
Threw new Error('No book titled "Dun"')isError: true, text "No book titled \"Dun\""
Threw new ProtocolError(InvalidRequest, 'Catalog is in maintenance')isError: true, text "Catalog is in maintenance"

The TypeScript guide confirms this: a tool handler produces only tool errors. The SDK converts every exception it throws, including a ProtocolError, into an isError: true result. The one documented exception is UrlElicitationRequiredError.

Two practical consequences:

  1. In TypeScript, a thrown error's message reaches the model. Do not throw errors that contain SQL, file paths, tokens or stack details. Catch them and return a clean message.
  2. In TypeScript, a tool cannot fail the request with a protocol error. If you need that, reject the request before it reaches the tool. In resources and prompts, the guide says to throw ProtocolError.

Step 4: Handle resource and prompt failures

Resources and prompts are read by the application, not chosen by the model, so they have no isError channel. Both SDKs use protocol errors there:

  • Python: raise ResourceNotFoundError. Lesson 16 showed it produced -32602 with the URI in data.
  • TypeScript: throw ResourceNotFoundError(uri) or ProtocolError(code, message).

The resources spec requires -32602 for a resource that does not exist, and says servers must not return an empty contents array instead.

Step 5: Write messages a model can use

A good tool error answers three questions: what went wrong, what is valid, and what to do next. Compare:

Weak messageBetter message
"Not found""No book titled 'Dun'. Known titles: Dune, Neuromancer."
"Invalid date""Invalid departure date: must be in the future. Current date is 08/08/2025." (the spec's own example)
"Error 500""The inventory service timed out. Try again in a minute, or call get_stock for one title."
"Bad input""order_id must look like ORD-004211. Call list_orders to find the ID."

Some habits:

  • Name the next tool. Models follow concrete suggestions well.
  • Include valid values when the list is short. For long lists, point to the tool that lists them.
  • Keep secrets out. No tokens, internal hostnames or customer data from other accounts.
  • Do not re-validate types. Both SDKs reject bad arguments against the schema before your code runs. Spend your effort on business rules instead.

Step 6: Log for yourself, message for the model

The model's message and your log serve different readers. Python already splits them: ToolError logs one INFO line, and a crash logs a full traceback at ERROR. The guide notes that a production log at WARNING stays quiet through tool errors and speaks up when something actually breaks.

In TypeScript, log the real error with console.error() before returning a clean message. On stdio, console.log() would corrupt the protocol stream.

Try it

  1. Add an order_book tool that fails when stock is zero. Make it a tool error that names a tool for checking restock dates.
  2. Add a check that rejects orders while MAINTENANCE is true. Choose the error type deliberately in Python, and note how TypeScript would differ.
  3. Make get_author_unsafe call a function that raises an exception with a fake database password in its message. Confirm Python hides it from the client.
  4. Do the same in TypeScript. You will see the password in the result. Fix it.

Recap

  • Tool errors (isError: true) are for the model; protocol errors are for the host.
  • Python: ToolError for fixable problems, MCPError to reject the request, and crashes are hidden from the model.
  • TypeScript: every thrown error in a tool becomes isError: true with its message, so sanitize.
  • Resources and prompts fail with protocol errors such as -32602.

Next step

Lesson 21 takes your server off your laptop. For the spec's view of errors, read how MCP servers report tool errors and MCP JSON-RPC messages. The Learn MCP course page lists every lesson.

Course navigation

Previous lesson: Lesson 19: Test With the MCP Inspector.

Next lesson: Lesson 21: Deploy a Remote Server.

Back to the Learn MCP course outline.

Frequently asked questions

Is a validation error a tool error or a protocol error?

For tool arguments, it is a tool error. The spec lists input validation errors under tool execution errors, and both SDKs return them as isError: true so the model can retry. For prompt arguments, it is a protocol error.

What error code should I use for a protocol error?

Use the standard JSON-RPC codes. -32602 is invalid params and -32603 is internal error. The Python SDK exports these as constants in mcp.types, and the TypeScript SDK has ProtocolErrorCode.

Should I retry failed upstream calls inside the tool?

Often, yes, for short transient failures. Keep retries brief so the tool call does not hang. If retries fail, return a tool error that tells the model what happened and whether trying later may help.

Why did my Python tool's exception text disappear?

Because unexpected exceptions are treated as crashes. The SDK shows the model only "Error executing tool <name>" and logs the full details on the server. Raise ToolError with a message when you want the model to see it.

Do hosts show tool errors to the user?

It depends on the host. Many show the tool call and its result. Write every error message as if a customer might read it.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.