Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 112 additions & 14 deletions docs/clients/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ Execute a tool using `call_tool()` with the tool name and arguments:
async with client:
# Simple tool call
result = await client.call_tool("add", {"a": 5, "b": 3})
# result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...]
# result -> CallToolResult with structured and unstructured data

# Access the result content
print(result[0].text) # Assuming TextContent, e.g., '8'
# Access structured data (automatically deserialized)
print(result.data) # 8 (int) or {"result": 8} for primitive types

# Access traditional content blocks
print(result.content[0].text) # "8" (TextContent)
```

### Advanced Execution Options
Expand Down Expand Up @@ -72,21 +75,97 @@ async with client:

## Handling Results

Tool execution returns a list of content objects. The most common types are:
<VersionBadge version="2.10.0" />

Tool execution returns a `CallToolResult` object with both structured and traditional content. FastMCP's standout feature is the `.data` property, which doesn't just provide raw JSON but actually hydrates complete Python objects including complex types like datetimes, UUIDs, and custom classes.

### CallToolResult Properties

<Card icon="code" title="CallToolResult Properties">
<ResponseField name=".data" type="Any">
**FastMCP exclusive**: Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). Goes beyond JSON to provide complete object reconstruction from output schemas.
</ResponseField>

<ResponseField name=".content" type="list[mcp.types.ContentBlock]">
Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.) available from all MCP servers.
</ResponseField>

<ResponseField name=".structured_content" type="dict[str, Any] | None">
Standard MCP structured JSON data as sent by the server, available from all MCP servers that support structured outputs.
</ResponseField>

- **`TextContent`**: Text-based results with a `.text` attribute
- **`ImageContent`**: Image data with image-specific attributes
- **`BlobContent`**: Binary data content
<ResponseField name=".is_error" type="bool">
Boolean indicating if the tool execution failed.
</ResponseField>
</Card>

### Structured Data Access

FastMCP's `.data` property provides fully hydrated Python objects, not just JSON dictionaries. This includes complex type reconstruction:

```python
from datetime import datetime
from uuid import UUID

async with client:
result = await client.call_tool("get_weather", {"city": "London"})

for content in result:
if hasattr(content, 'text'):
print(f"Text result: {content.text}")
elif hasattr(content, 'data'):
print(f"Binary data: {len(content.data)} bytes")
# FastMCP reconstructs complete Python objects from the server's output schema
weather = result.data # Server-defined WeatherReport object
print(f"Temperature: {weather.temperature}°C at {weather.timestamp}")
print(f"Station: {weather.station_id}")
print(f"Humidity: {weather.humidity}%")

# The timestamp is a real datetime object, not a string!
assert isinstance(weather.timestamp, datetime)
assert isinstance(weather.station_id, UUID)

# Compare with raw structured JSON (standard MCP)
print(f"Raw JSON: {result.structured_content}")
# {"temperature": 20, "timestamp": "2024-01-15T14:30:00Z", "station_id": "123e4567-..."}

# Traditional content blocks (standard MCP)
print(f"Text content: {result.content[0].text}")
```

### Fallback Behavior

For tools without output schemas or when deserialization fails, `.data` will be `None`:

```python
async with client:
result = await client.call_tool("legacy_tool", {"param": "value"})

if result.data is not None:
# Structured output available and successfully deserialized
print(f"Structured: {result.data}")
else:
# No structured output or deserialization failed - use content blocks
for content in result.content:
if hasattr(content, 'text'):
print(f"Text result: {content.text}")
elif hasattr(content, 'data'):
print(f"Binary data: {len(content.data)} bytes")
```

### Primitive Type Unwrapping

<Tip>
FastMCP servers automatically wrap non-object results (like `int`, `str`, `bool`) in a `{"result": value}` structure to create valid structured outputs. FastMCP clients understand this convention and automatically unwrap the value in `.data` for convenience, so you get the original primitive value instead of a wrapper object.
</Tip>

```python
async with client:
result = await client.call_tool("calculate_sum", {"a": 5, "b": 3})

# FastMCP client automatically unwraps for convenience
print(result.data) # 8 (int) - the original value

# Raw structured content shows the server-side wrapping
print(result.structured_content) # {"result": 8}

# Other MCP clients would need to manually access ["result"]
# value = result.structured_content["result"] # Not needed with FastMCP!
```

## Error Handling
Expand All @@ -101,14 +180,32 @@ from fastmcp.exceptions import ToolError
async with client:
try:
result = await client.call_tool("potentially_failing_tool", {"param": "value"})
print("Tool succeeded:", result)
print("Tool succeeded:", result.data)
except ToolError as e:
print(f"Tool failed: {e}")
```

### Manual Error Checking

For more granular control, use `call_tool_mcp()` which returns the raw MCP protocol object with an `isError` flag:
You can disable automatic error raising and manually check the result:

```python
async with client:
result = await client.call_tool(
"potentially_failing_tool",
{"param": "value"},
raise_on_error=False
)

if result.is_error:
print(f"Tool failed: {result.content[0].text}")
else:
print(f"Tool succeeded: {result.data}")
```

### Raw MCP Protocol Access

For complete control, use `call_tool_mcp()` which returns the raw MCP protocol object:

```python
async with client:
Expand All @@ -119,6 +216,7 @@ async with client:
print(f"Tool failed: {result.content}")
else:
print(f"Tool succeeded: {result.content}")
# Note: No automatic deserialization with call_tool_mcp()
```

## Argument Handling
Expand Down
40 changes: 39 additions & 1 deletion docs/patterns/tool-transformation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ The `Tool.from_tool()` class method is the primary way to create a transformed t
- `description`: An optional description for the new tool.
- `transform_args`: A dictionary of `ArgTransform` objects, one for each argument you want to modify.
- `transform_fn`: An optional function that will be called instead of the parent tool's logic.
- `output_schema`: Control output schema and structured outputs (see [Output Schema Control](#output-schema-control)).
- `tags`: An optional set of tags for the new tool.
- `annotations`: An optional set of `ToolAnnotations` for the new tool.
- `serializer`: An optional function that will be called to serialize the result of the new tool.
Expand Down Expand Up @@ -439,7 +440,44 @@ mcp.add_tool(new_tool)

<Tip>
In the above example, `**kwargs` receives the renamed argument `b`, not the original argument `y`. It is therefore recommended to use with `forward()`, not `forward_raw()`.
</Tip>
</Tip>

## Output Schema Control

<VersionBadge version="2.10.0" />

Transformed tools inherit output schemas from their parent by default, but you can control this behavior:

**Inherit from Parent (Default)**
```python
Tool.from_tool(parent_tool, name="renamed_tool")
```
The transformed tool automatically uses the parent tool's output schema and structured output behavior.

**Custom Output Schema**
```python
Tool.from_tool(parent_tool, output_schema={
"type": "object",
"properties": {"status": {"type": "string"}}
})
```
Provide your own schema that differs from the parent. The tool must return data matching this schema.

**Remove Output Schema**
```python
Tool.from_tool(parent_tool, output_schema=False)
```
Removes the output schema declaration. Automatic structured content still works for object-like returns (dict, dataclass, Pydantic models) but primitive types won't be structured.

**Full Control with Transform Functions**
```python
async def custom_output(**kwargs) -> ToolResult:
result = await forward(**kwargs)
return ToolResult(content=[...], structured_content={...})

Tool.from_tool(parent_tool, transform_fn=custom_output)
```
Use a transform function returning `ToolResult` for complete control over both content blocks and structured outputs.

## Common Patterns

Expand Down
Loading