Skip to content

Commit 06c1ea2

Browse files
authored
Merge branch 'v1.x' into fix-client-secret-post-for-client-credentials-auth
2 parents bb4a07d + c68e254 commit 06c1ea2

File tree

2 files changed

+113
-0
lines changed

2 files changed

+113
-0
lines changed

docs/server.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,70 @@ def get_weather(city: str, unit: str = "celsius") -> str:
225225
_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/basic_tool.py)_
226226
<!-- /snippet-source -->
227227

228+
#### Error Handling
229+
230+
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:
231+
232+
<!-- snippet-source examples/snippets/servers/tool_errors.py -->
233+
```python
234+
"""Example showing how to handle and return errors from tools."""
235+
236+
from mcp.server.fastmcp import FastMCP
237+
from mcp.server.fastmcp.exceptions import ToolError
238+
from mcp.types import CallToolResult, TextContent
239+
240+
mcp = FastMCP("Tool Error Handling Example")
241+
242+
243+
# Option 1: Raise ToolError for expected error conditions.
244+
# The error message is returned to the client with isError=True.
245+
@mcp.tool()
246+
def divide(a: float, b: float) -> float:
247+
"""Divide two numbers."""
248+
if b == 0:
249+
raise ToolError("Cannot divide by zero")
250+
return a / b
251+
252+
253+
# Option 2: Unhandled exceptions are automatically caught and
254+
# converted to error responses with isError=True.
255+
@mcp.tool()
256+
def read_config(path: str) -> str:
257+
"""Read a configuration file."""
258+
# If this raises FileNotFoundError, the client receives an
259+
# error response like "Error executing tool read_config: ..."
260+
with open(path) as f:
261+
return f.read()
262+
263+
264+
# Option 3: Return CallToolResult directly for full control
265+
# over error responses, including custom content.
266+
@mcp.tool()
267+
def validate_input(data: str) -> CallToolResult:
268+
"""Validate input data."""
269+
errors: list[str] = []
270+
if len(data) < 3:
271+
errors.append("Input must be at least 3 characters")
272+
if not data.isascii():
273+
errors.append("Input must be ASCII only")
274+
275+
if errors:
276+
return CallToolResult(
277+
content=[TextContent(type="text", text="\n".join(errors))],
278+
isError=True,
279+
)
280+
return CallToolResult(
281+
content=[TextContent(type="text", text="Validation passed")],
282+
)
283+
```
284+
285+
_Full example: [examples/snippets/servers/tool_errors.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/tool_errors.py)_
286+
<!-- /snippet-source -->
287+
288+
- **`ToolError`** is the preferred approach for most cases — raise it with a descriptive message and the framework handles the rest.
289+
- **Unhandled exceptions** are caught automatically, so tools won't crash the server. The exception message is forwarded to the client as an error response.
290+
- **`CallToolResult`** with `isError=True` gives full control when you need to customize the error content or include multiple content items.
291+
228292
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:
229293

230294
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Example showing how to handle and return errors from tools."""
2+
3+
from mcp.server.fastmcp import FastMCP
4+
from mcp.server.fastmcp.exceptions import ToolError
5+
from mcp.types import CallToolResult, TextContent
6+
7+
mcp = FastMCP("Tool Error Handling Example")
8+
9+
10+
# Option 1: Raise ToolError for expected error conditions.
11+
# The error message is returned to the client with isError=True.
12+
@mcp.tool()
13+
def divide(a: float, b: float) -> float:
14+
"""Divide two numbers."""
15+
if b == 0:
16+
raise ToolError("Cannot divide by zero")
17+
return a / b
18+
19+
20+
# Option 2: Unhandled exceptions are automatically caught and
21+
# converted to error responses with isError=True.
22+
@mcp.tool()
23+
def read_config(path: str) -> str:
24+
"""Read a configuration file."""
25+
# If this raises FileNotFoundError, the client receives an
26+
# error response like "Error executing tool read_config: ..."
27+
with open(path) as f:
28+
return f.read()
29+
30+
31+
# Option 3: Return CallToolResult directly for full control
32+
# over error responses, including custom content.
33+
@mcp.tool()
34+
def validate_input(data: str) -> CallToolResult:
35+
"""Validate input data."""
36+
errors: list[str] = []
37+
if len(data) < 3:
38+
errors.append("Input must be at least 3 characters")
39+
if not data.isascii():
40+
errors.append("Input must be ASCII only")
41+
42+
if errors:
43+
return CallToolResult(
44+
content=[TextContent(type="text", text="\n".join(errors))],
45+
isError=True,
46+
)
47+
return CallToolResult(
48+
content=[TextContent(type="text", text="Validation passed")],
49+
)

0 commit comments

Comments
 (0)