diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index c7adc16d6b..e326258d2b 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -453,62 +453,89 @@ The 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/speci This automatic behavior enables clients to receive machine-readable data alongside human-readable content without requiring explicit output schemas for object-like returns. -#### Object-like Results (Automatic Structured Content) +#### Dictionaries and Objects + +When your tool returns a dictionary, dataclass, or Pydantic model, FastMCP automatically creates structured content from it. The structured content contains the actual object data, making it easy for clients to deserialize back to native objects. -```python Dict Return (No Schema Needed) +```python Tool Definition @mcp.tool def get_user_data(user_id: str) -> dict: - """Get user data without type annotation.""" + """Get user data.""" return {"name": "Alice", "age": 30, "active": True} ``` -```json Traditional Content -"{\n \"name\": \"Alice\",\n \"age\": 30,\n \"active\": true\n}" -``` - -```json Structured Content (Automatic) +```json MCP Result { - "name": "Alice", - "age": 30, - "active": true + "content": [ + { + "type": "text", + "text": "{\n \"name\": \"Alice\",\n \"age\": 30,\n \"active\": true\n}" + } + ], + "structuredContent": { + "name": "Alice", + "age": 30, + "active": true + } } ``` -#### Non-object Results (Schema Required) +#### Primitives and Collections + +When your tool returns a primitive type (int, str, bool) or a collection (list, set), FastMCP needs a return type annotation to generate structured content. The annotation tells FastMCP how to validate and serialize the result. + +Without a type annotation, the tool only produces `content`: -```python Integer Return (No Schema) -@mcp.tool +```python Tool Definition +@mcp.tool def calculate_sum(a: int, b: int): """Calculate sum without return annotation.""" return a + b # Returns 8 ``` -```json Traditional Content Only -"8" +```json MCP Result +{ + "content": [ + { + "type": "text", + "text": "8" + } + ] +} ``` + + +When you add a return annotation, such as `-> int`, FastMCP generates `structuredContent` by wrapping the primitive value in a `{"result": ...}` object, since JSON schemas require object-type roots for structured output: -```python Integer Return (With Schema) + +```python Tool Definition @mcp.tool def calculate_sum(a: int, b: int) -> int: - """Calculate sum with return annotation.""" + """Calculate sum with return annotation.""" return a + b # Returns 8 ``` -```json Traditional Content -"8" -``` - -```json Structured Content (From Schema) +```json MCP Result { - "result": 8 + "content": [ + { + "type": "text", + "text": "8" + } + ], + "structuredContent": { + "result": 8 + } } ``` -#### Complex Type Example +#### Typed Models + +Return type annotations work with any type that can be converted to a JSON schema. Dataclasses and Pydantic models are particularly useful because FastMCP extracts their field definitions to create detailed schemas. ```python Tool Definition @@ -526,14 +553,18 @@ class Person: @mcp.tool def get_user_profile(user_id: str) -> Person: """Get a user's profile information.""" - return Person(name="Alice", age=30, email="alice@example.com") + return Person( + name="Alice", + age=30, + email="alice@example.com", + ) ``` ```json Generated Output Schema { "properties": { "name": {"title": "Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": {"title": "Age", "type": "integer"}, "email": {"title": "Email", "type": "string"} }, "required": ["name", "age", "email"], @@ -542,15 +573,25 @@ def get_user_profile(user_id: str) -> Person: } ``` -```json Structured Output +```json MCP Result { - "name": "Alice", - "age": 30, - "email": "alice@example.com" + "content": [ + { + "type": "text", + "text": "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}" + } + ], + "structuredContent": { + "name": "Alice", + "age": 30, + "email": "alice@example.com" + } } ``` +The `Person` dataclass becomes an output schema (second tab) that describes the expected format. When executed, clients receive the result (third tab) with both `content` and `structuredContent` fields. + ### Output Schemas @@ -614,12 +655,13 @@ Schema generation works for most common types including basic types, collections - However, you can provide structured output without an output schema (using `ToolResult`) -### Full Control with ToolResult +### ToolResult and Metadata -For complete control over traditional content, structured output, and metadata, return a `ToolResult` object: +For complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata. ```python from fastmcp.tools.tool import ToolResult +from mcp.types import TextContent @mcp.tool def advanced_tool() -> ToolResult: @@ -627,19 +669,56 @@ def advanced_tool() -> ToolResult: return ToolResult( content=[TextContent(type="text", text="Human-readable summary")], structured_content={"data": "value", "count": 42}, - meta={"some": "metadata"} + meta={"execution_time_ms": 145} ) ``` -When returning `ToolResult`: -- You control exactly what content, structured data, and metadata is sent -- Output schemas are optional - structured content can be provided without a schema -- Clients receive traditional content blocks, structured data, and metadata +`ToolResult` accepts three fields: + +**`content`** - The traditional MCP content blocks that clients display to users. Can be a string (automatically converted to `TextContent`), a list of MCP content blocks, or any serializable value (converted to JSON string). At least one of `content` or `structured_content` must be provided. + +```python +# Simple string +ToolResult(content="Hello, world!") + +# List of content blocks +ToolResult(content=[ + TextContent(type="text", text="Result: 42"), + ImageContent(type="image", data="base64...", mimeType="image/png") +]) +``` + +**`structured_content`** - A dictionary containing structured data that matches your tool's output schema. This enables clients to programmatically process the results. If you provide `structured_content`, it must be a dictionary or `None`. If only `structured_content` is provided, it will also be used as `content` (converted to JSON string). + +```python +ToolResult( + content="Found 3 users", + structured_content={"users": [{"name": "Alice"}, {"name": "Bob"}]} +) +``` + +**`meta`** + +Runtime metadata about the tool execution. Use this for performance metrics, debugging information, or any client-specific data that doesn't belong in the content or structured output. + +```python +ToolResult( + content="Analysis complete", + structured_content={"result": "positive"}, + meta={ + "execution_time_ms": 145, + "model_version": "2.1", + "confidence": 0.95 + } +) +``` -If your return type annotation cannot be converted to a JSON schema (e.g., complex custom classes without Pydantic support), the output schema will be omitted but the tool will still function normally with traditional content. +The `meta` field in `ToolResult` is for runtime metadata about tool execution (e.g., execution time, performance metrics). This is separate from the `meta` parameter in `@mcp.tool(meta={...})`, which provides static metadata about the tool definition itself. +When returning `ToolResult`, you have full control - FastMCP won't automatically wrap or transform your data. `ToolResult` can be returned with or without an output schema. + ## Error Handling diff --git a/examples/tool_result_echo.py b/examples/tool_result_echo.py index 5e542b4e24..bd1185f29e 100644 --- a/examples/tool_result_echo.py +++ b/examples/tool_result_echo.py @@ -1,7 +1,12 @@ """ -FastMCP Echo Server +FastMCP Echo Server with Metadata + +Demonstrates how to return metadata alongside content and structured data. +The meta field can include execution details, versioning, or other information +that clients may find useful. """ +import time from dataclasses import dataclass from fastmcp import FastMCP @@ -13,10 +18,24 @@ @dataclass class EchoData: data: str + length: int @mcp.tool def echo(text: str) -> ToolResult: + """Echo text back with metadata about the operation.""" + start = time.perf_counter() + + result = EchoData(data=text, length=len(text)) + + execution_time = (time.perf_counter() - start) * 1000 + return ToolResult( - content=text, structured_content=EchoData(data=text), meta={"some": "metadata"} + content=f"Echoed: {text}", + structured_content=result, + meta={ + "execution_time_ms": round(execution_time, 2), + "character_count": len(text), + "word_count": len(text.split()), + }, )