Skip to content

Commit e222671

Browse files
authored
Merge pull request #901 from jlowin/output-schema
MCP 6/18/25: Add output schema to tools
2 parents dd4ca95 + b10bab9 commit e222671

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4608
-474
lines changed

docs/clients/tools.mdx

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ Execute a tool using `call_tool()` with the tool name and arguments:
3737
async with client:
3838
# Simple tool call
3939
result = await client.call_tool("add", {"a": 5, "b": 3})
40-
# result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...]
40+
# result -> CallToolResult with structured and unstructured data
4141

42-
# Access the result content
43-
print(result[0].text) # Assuming TextContent, e.g., '8'
42+
# Access structured data (automatically deserialized)
43+
print(result.data) # 8 (int) or {"result": 8} for primitive types
44+
45+
# Access traditional content blocks
46+
print(result.content[0].text) # "8" (TextContent)
4447
```
4548

4649
### Advanced Execution Options
@@ -72,21 +75,97 @@ async with client:
7275

7376
## Handling Results
7477

75-
Tool execution returns a list of content objects. The most common types are:
78+
<VersionBadge version="2.10.0" />
79+
80+
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.
81+
82+
### CallToolResult Properties
83+
84+
<Card icon="code" title="CallToolResult Properties">
85+
<ResponseField name=".data" type="Any">
86+
**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.
87+
</ResponseField>
88+
89+
<ResponseField name=".content" type="list[mcp.types.ContentBlock]">
90+
Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.) available from all MCP servers.
91+
</ResponseField>
92+
93+
<ResponseField name=".structured_content" type="dict[str, Any] | None">
94+
Standard MCP structured JSON data as sent by the server, available from all MCP servers that support structured outputs.
95+
</ResponseField>
7696

77-
- **`TextContent`**: Text-based results with a `.text` attribute
78-
- **`ImageContent`**: Image data with image-specific attributes
79-
- **`BlobContent`**: Binary data content
97+
<ResponseField name=".is_error" type="bool">
98+
Boolean indicating if the tool execution failed.
99+
</ResponseField>
100+
</Card>
101+
102+
### Structured Data Access
103+
104+
FastMCP's `.data` property provides fully hydrated Python objects, not just JSON dictionaries. This includes complex type reconstruction:
80105

81106
```python
107+
from datetime import datetime
108+
from uuid import UUID
109+
82110
async with client:
83111
result = await client.call_tool("get_weather", {"city": "London"})
84112

85-
for content in result:
86-
if hasattr(content, 'text'):
87-
print(f"Text result: {content.text}")
88-
elif hasattr(content, 'data'):
89-
print(f"Binary data: {len(content.data)} bytes")
113+
# FastMCP reconstructs complete Python objects from the server's output schema
114+
weather = result.data # Server-defined WeatherReport object
115+
print(f"Temperature: {weather.temperature}°C at {weather.timestamp}")
116+
print(f"Station: {weather.station_id}")
117+
print(f"Humidity: {weather.humidity}%")
118+
119+
# The timestamp is a real datetime object, not a string!
120+
assert isinstance(weather.timestamp, datetime)
121+
assert isinstance(weather.station_id, UUID)
122+
123+
# Compare with raw structured JSON (standard MCP)
124+
print(f"Raw JSON: {result.structured_content}")
125+
# {"temperature": 20, "timestamp": "2024-01-15T14:30:00Z", "station_id": "123e4567-..."}
126+
127+
# Traditional content blocks (standard MCP)
128+
print(f"Text content: {result.content[0].text}")
129+
```
130+
131+
### Fallback Behavior
132+
133+
For tools without output schemas or when deserialization fails, `.data` will be `None`:
134+
135+
```python
136+
async with client:
137+
result = await client.call_tool("legacy_tool", {"param": "value"})
138+
139+
if result.data is not None:
140+
# Structured output available and successfully deserialized
141+
print(f"Structured: {result.data}")
142+
else:
143+
# No structured output or deserialization failed - use content blocks
144+
for content in result.content:
145+
if hasattr(content, 'text'):
146+
print(f"Text result: {content.text}")
147+
elif hasattr(content, 'data'):
148+
print(f"Binary data: {len(content.data)} bytes")
149+
```
150+
151+
### Primitive Type Unwrapping
152+
153+
<Tip>
154+
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.
155+
</Tip>
156+
157+
```python
158+
async with client:
159+
result = await client.call_tool("calculate_sum", {"a": 5, "b": 3})
160+
161+
# FastMCP client automatically unwraps for convenience
162+
print(result.data) # 8 (int) - the original value
163+
164+
# Raw structured content shows the server-side wrapping
165+
print(result.structured_content) # {"result": 8}
166+
167+
# Other MCP clients would need to manually access ["result"]
168+
# value = result.structured_content["result"] # Not needed with FastMCP!
90169
```
91170

92171
## Error Handling
@@ -101,14 +180,32 @@ from fastmcp.exceptions import ToolError
101180
async with client:
102181
try:
103182
result = await client.call_tool("potentially_failing_tool", {"param": "value"})
104-
print("Tool succeeded:", result)
183+
print("Tool succeeded:", result.data)
105184
except ToolError as e:
106185
print(f"Tool failed: {e}")
107186
```
108187

109188
### Manual Error Checking
110189

111-
For more granular control, use `call_tool_mcp()` which returns the raw MCP protocol object with an `isError` flag:
190+
You can disable automatic error raising and manually check the result:
191+
192+
```python
193+
async with client:
194+
result = await client.call_tool(
195+
"potentially_failing_tool",
196+
{"param": "value"},
197+
raise_on_error=False
198+
)
199+
200+
if result.is_error:
201+
print(f"Tool failed: {result.content[0].text}")
202+
else:
203+
print(f"Tool succeeded: {result.data}")
204+
```
205+
206+
### Raw MCP Protocol Access
207+
208+
For complete control, use `call_tool_mcp()` which returns the raw MCP protocol object:
112209

113210
```python
114211
async with client:
@@ -119,6 +216,7 @@ async with client:
119216
print(f"Tool failed: {result.content}")
120217
else:
121218
print(f"Tool succeeded: {result.content}")
219+
# Note: No automatic deserialization with call_tool_mcp()
122220
```
123221

124222
## Argument Handling

docs/patterns/tool-transformation.mdx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ The `Tool.from_tool()` class method is the primary way to create a transformed t
8989
- `description`: An optional description for the new tool.
9090
- `transform_args`: A dictionary of `ArgTransform` objects, one for each argument you want to modify.
9191
- `transform_fn`: An optional function that will be called instead of the parent tool's logic.
92+
- `output_schema`: Control output schema and structured outputs (see [Output Schema Control](#output-schema-control)).
9293
- `tags`: An optional set of tags for the new tool.
9394
- `annotations`: An optional set of `ToolAnnotations` for the new tool.
9495
- `serializer`: An optional function that will be called to serialize the result of the new tool.
@@ -439,7 +440,44 @@ mcp.add_tool(new_tool)
439440

440441
<Tip>
441442
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()`.
442-
</Tip>
443+
</Tip>
444+
445+
## Output Schema Control
446+
447+
<VersionBadge version="2.10.0" />
448+
449+
Transformed tools inherit output schemas from their parent by default, but you can control this behavior:
450+
451+
**Inherit from Parent (Default)**
452+
```python
453+
Tool.from_tool(parent_tool, name="renamed_tool")
454+
```
455+
The transformed tool automatically uses the parent tool's output schema and structured output behavior.
456+
457+
**Custom Output Schema**
458+
```python
459+
Tool.from_tool(parent_tool, output_schema={
460+
"type": "object",
461+
"properties": {"status": {"type": "string"}}
462+
})
463+
```
464+
Provide your own schema that differs from the parent. The tool must return data matching this schema.
465+
466+
**Remove Output Schema**
467+
```python
468+
Tool.from_tool(parent_tool, output_schema=False)
469+
```
470+
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.
471+
472+
**Full Control with Transform Functions**
473+
```python
474+
async def custom_output(**kwargs) -> ToolResult:
475+
result = await forward(**kwargs)
476+
return ToolResult(content=[...], structured_content={...})
477+
478+
Tool.from_tool(parent_tool, transform_fn=custom_output)
479+
```
480+
Use a transform function returning `ToolResult` for complete control over both content blocks and structured outputs.
443481

444482
## Common Patterns
445483

0 commit comments

Comments
 (0)