From 2355058f73b2e8f6542c33b94913ffd52449d14c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 23 Feb 2026 18:28:22 +0000 Subject: [PATCH 1/3] docs: add server-side tool error handling documentation Add an 'Error Handling' subsection to the Tools section of docs/server.md covering three approaches: raising ToolError, unhandled exception auto-conversion, and returning CallToolResult(isError=True) directly. Includes a runnable example at examples/snippets/servers/tool_errors.py. --- docs/server.md | 64 ++++++++++++++++++++++++ examples/snippets/servers/tool_errors.py | 49 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 examples/snippets/servers/tool_errors.py diff --git a/docs/server.md b/docs/server.md index 0eddffa50..ecb26b55b 100644 --- a/docs/server.md +++ b/docs/server.md @@ -225,6 +225,70 @@ def get_weather(city: str, unit: str = "celsius") -> str: _Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/basic_tool.py)_ +#### Error Handling + +When a tool encounters an error, it should signal this to the client rather than returning a normal result. The MCP protocol uses the `isError` flag on `CallToolResult` to distinguish error responses from successful ones. There are three ways to handle errors: + + +```python +"""Example showing how to handle and return errors from tools.""" + +from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp.exceptions import ToolError +from mcp.types import CallToolResult, TextContent + +mcp = FastMCP("Tool Error Handling Example") + + +# Option 1: Raise ToolError for expected error conditions. +# The error message is returned to the client with isError=True. +@mcp.tool() +def divide(a: float, b: float) -> float: + """Divide two numbers.""" + if b == 0: + raise ToolError("Cannot divide by zero") + return a / b + + +# Option 2: Unhandled exceptions are automatically caught and +# converted to error responses with isError=True. +@mcp.tool() +def read_config(path: str) -> str: + """Read a configuration file.""" + # If this raises FileNotFoundError, the client receives an + # error response like "Error executing tool read_config: ..." + with open(path) as f: + return f.read() + + +# Option 3: Return CallToolResult directly for full control +# over error responses, including custom content. +@mcp.tool() +def validate_input(data: str) -> CallToolResult: + """Validate input data.""" + errors = [] + if len(data) < 3: + errors.append("Input must be at least 3 characters") + if not data.isascii(): + errors.append("Input must be ASCII only") + + if errors: + return CallToolResult( + content=[TextContent(type="text", text="\n".join(errors))], + isError=True, + ) + return CallToolResult( + content=[TextContent(type="text", text="Validation passed")], + ) +``` + +_Full example: [examples/snippets/servers/tool_errors.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/tool_errors.py)_ + + +- **`ToolError`** is the preferred approach for most cases — raise it with a descriptive message and the framework handles the rest. +- **Unhandled exceptions** are caught automatically, so tools won't crash the server. The exception message is forwarded to the client as an error response. +- **`CallToolResult`** with `isError=True` gives full control when you need to customize the error content or include multiple content items. + Tools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the FastMCP framework and provides access to MCP capabilities: diff --git a/examples/snippets/servers/tool_errors.py b/examples/snippets/servers/tool_errors.py new file mode 100644 index 000000000..a27b6bd62 --- /dev/null +++ b/examples/snippets/servers/tool_errors.py @@ -0,0 +1,49 @@ +"""Example showing how to handle and return errors from tools.""" + +from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp.exceptions import ToolError +from mcp.types import CallToolResult, TextContent + +mcp = FastMCP("Tool Error Handling Example") + + +# Option 1: Raise ToolError for expected error conditions. +# The error message is returned to the client with isError=True. +@mcp.tool() +def divide(a: float, b: float) -> float: + """Divide two numbers.""" + if b == 0: + raise ToolError("Cannot divide by zero") + return a / b + + +# Option 2: Unhandled exceptions are automatically caught and +# converted to error responses with isError=True. +@mcp.tool() +def read_config(path: str) -> str: + """Read a configuration file.""" + # If this raises FileNotFoundError, the client receives an + # error response like "Error executing tool read_config: ..." + with open(path) as f: + return f.read() + + +# Option 3: Return CallToolResult directly for full control +# over error responses, including custom content. +@mcp.tool() +def validate_input(data: str) -> CallToolResult: + """Validate input data.""" + errors = [] + if len(data) < 3: + errors.append("Input must be at least 3 characters") + if not data.isascii(): + errors.append("Input must be ASCII only") + + if errors: + return CallToolResult( + content=[TextContent(type="text", text="\n".join(errors))], + isError=True, + ) + return CallToolResult( + content=[TextContent(type="text", text="Validation passed")], + ) From dccc95363dcbfc61ee38cf06a5bc6da1a113674c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 23 Feb 2026 22:28:19 +0000 Subject: [PATCH 2/3] fix: add type annotation to fix pyright error --- examples/snippets/servers/tool_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/snippets/servers/tool_errors.py b/examples/snippets/servers/tool_errors.py index a27b6bd62..42c8b0159 100644 --- a/examples/snippets/servers/tool_errors.py +++ b/examples/snippets/servers/tool_errors.py @@ -33,7 +33,7 @@ def read_config(path: str) -> str: @mcp.tool() def validate_input(data: str) -> CallToolResult: """Validate input data.""" - errors = [] + errors: list[str] = [] if len(data) < 3: errors.append("Input must be at least 3 characters") if not data.isascii(): From 40aa8429f7786027518ca9460fd8be7f6c5cb2c7 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 23 Feb 2026 22:41:38 +0000 Subject: [PATCH 3/3] fix: sync doc snippets for server.md --- docs/server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server.md b/docs/server.md index ecb26b55b..6340687c3 100644 --- a/docs/server.md +++ b/docs/server.md @@ -266,7 +266,7 @@ def read_config(path: str) -> str: @mcp.tool() def validate_input(data: str) -> CallToolResult: """Validate input data.""" - errors = [] + errors: list[str] = [] if len(data) < 3: errors.append("Input must be at least 3 characters") if not data.isascii():