|
27 | 27 | - [Server](#server) |
28 | 28 | - [Resources](#resources) |
29 | 29 | - [Tools](#tools) |
| 30 | + - [Structured Output](#structured-output) |
30 | 31 | - [Prompts](#prompts) |
31 | 32 | - [Images](#images) |
32 | 33 | - [Context](#context) |
@@ -249,6 +250,127 @@ async def fetch_weather(city: str) -> str: |
249 | 250 | return response.text |
250 | 251 | ``` |
251 | 252 |
|
| 253 | +#### Structured Output |
| 254 | + |
| 255 | +Tools will return structured results by default, if their return type |
| 256 | +annotation is compatible. Otherwise, they will return unstructured results. |
| 257 | + |
| 258 | +Structured output supports these return types: |
| 259 | +- Pydantic models (BaseModel subclasses) |
| 260 | +- TypedDicts |
| 261 | +- Dataclasses and other classes with type hints |
| 262 | +- `dict[str, T]` (where T is any JSON-serializable type) |
| 263 | +- Primitive types (str, int, float, bool, bytes, None) - wrapped in `{"result": value}` |
| 264 | +- Generic types (list, tuple, Union, Optional, etc.) - wrapped in `{"result": value}` |
| 265 | + |
| 266 | +Classes without type hints cannot be serialized for structured output. Only |
| 267 | +classes with properly annotated attributes will be converted to Pydantic models |
| 268 | +for schema generation and validation. |
| 269 | + |
| 270 | +Structured results are automatically validated against the output schema |
| 271 | +generated from the annotation. This ensures the tool returns well-typed, |
| 272 | +validated data that clients can easily process. |
| 273 | + |
| 274 | +**Note:** For backward compatibility, unstructured results are also |
| 275 | +returned. Unstructured results are provided for backward compatibility |
| 276 | +with previous versions of the MCP specification, and are quirks-compatible |
| 277 | +with previous versions of FastMCP in the current version of the SDK. |
| 278 | + |
| 279 | +**Note:** In cases where a tool function's return type annotation |
| 280 | +causes the tool to be classified as structured _and this is undesirable_, |
| 281 | +the classification can be suppressed by passing `structured_output=False` |
| 282 | +to the `@tool` decorator. |
| 283 | + |
| 284 | +```python |
| 285 | +from mcp.server.fastmcp import FastMCP |
| 286 | +from pydantic import BaseModel, Field |
| 287 | +from typing import TypedDict |
| 288 | + |
| 289 | +mcp = FastMCP("Weather Service") |
| 290 | + |
| 291 | + |
| 292 | +# Using Pydantic models for rich structured data |
| 293 | +class WeatherData(BaseModel): |
| 294 | + temperature: float = Field(description="Temperature in Celsius") |
| 295 | + humidity: float = Field(description="Humidity percentage") |
| 296 | + condition: str |
| 297 | + wind_speed: float |
| 298 | + |
| 299 | + |
| 300 | +@mcp.tool() |
| 301 | +def get_weather(city: str) -> WeatherData: |
| 302 | + """Get structured weather data""" |
| 303 | + return WeatherData( |
| 304 | + temperature=22.5, humidity=65.0, condition="partly cloudy", wind_speed=12.3 |
| 305 | + ) |
| 306 | + |
| 307 | + |
| 308 | +# Using TypedDict for simpler structures |
| 309 | +class LocationInfo(TypedDict): |
| 310 | + latitude: float |
| 311 | + longitude: float |
| 312 | + name: str |
| 313 | + |
| 314 | + |
| 315 | +@mcp.tool() |
| 316 | +def get_location(address: str) -> LocationInfo: |
| 317 | + """Get location coordinates""" |
| 318 | + return LocationInfo(latitude=51.5074, longitude=-0.1278, name="London, UK") |
| 319 | + |
| 320 | + |
| 321 | +# Using dict[str, Any] for flexible schemas |
| 322 | +@mcp.tool() |
| 323 | +def get_statistics(data_type: str) -> dict[str, float]: |
| 324 | + """Get various statistics""" |
| 325 | + return {"mean": 42.5, "median": 40.0, "std_dev": 5.2} |
| 326 | + |
| 327 | + |
| 328 | +# Ordinary classes with type hints work for structured output |
| 329 | +class UserProfile: |
| 330 | + name: str |
| 331 | + age: int |
| 332 | + email: str | None = None |
| 333 | + |
| 334 | + def __init__(self, name: str, age: int, email: str | None = None): |
| 335 | + self.name = name |
| 336 | + self.age = age |
| 337 | + self.email = email |
| 338 | + |
| 339 | + |
| 340 | +@mcp.tool() |
| 341 | +def get_user(user_id: str) -> UserProfile: |
| 342 | + """Get user profile - returns structured data""" |
| 343 | + return UserProfile( name="Alice", age=30, email="[email protected]") |
| 344 | + |
| 345 | + |
| 346 | +# Classes WITHOUT type hints cannot be used for structured output |
| 347 | +class UntypedConfig: |
| 348 | + def __init__(self, setting1, setting2): |
| 349 | + self.setting1 = setting1 |
| 350 | + self.setting2 = setting2 |
| 351 | + |
| 352 | + |
| 353 | +@mcp.tool() |
| 354 | +def get_config() -> UntypedConfig: |
| 355 | + """This returns unstructured output - no schema generated""" |
| 356 | + return UntypedConfig("value1", "value2") |
| 357 | + |
| 358 | + |
| 359 | +# Lists and other types are wrapped automatically |
| 360 | +@mcp.tool() |
| 361 | +def list_cities() -> list[str]: |
| 362 | + """Get a list of cities""" |
| 363 | + return ["London", "Paris", "Tokyo"] |
| 364 | + # Returns: {"result": ["London", "Paris", "Tokyo"]} |
| 365 | + |
| 366 | + |
| 367 | +@mcp.tool() |
| 368 | +def get_temperature(city: str) -> float: |
| 369 | + """Get temperature as a simple float""" |
| 370 | + return 22.5 |
| 371 | + # Returns: {"result": 22.5} |
| 372 | +``` |
| 373 | + |
252 | 374 | ### Prompts |
253 | 375 |
|
254 | 376 | Prompts are reusable templates that help LLMs interact with your server effectively: |
@@ -829,6 +951,67 @@ if __name__ == "__main__": |
829 | 951 |
|
830 | 952 | Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server. |
831 | 953 |
|
| 954 | +#### Structured Output Support |
| 955 | + |
| 956 | +The low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an `outputSchema` to validate their structured output: |
| 957 | + |
| 958 | +```python |
| 959 | +from types import Any |
| 960 | + |
| 961 | +import mcp.types as types |
| 962 | +from mcp.server.lowlevel import Server |
| 963 | + |
| 964 | +server = Server("example-server") |
| 965 | + |
| 966 | + |
| 967 | +@server.list_tools() |
| 968 | +async def list_tools() -> list[types.Tool]: |
| 969 | + return [ |
| 970 | + types.Tool( |
| 971 | + name="calculate", |
| 972 | + description="Perform mathematical calculations", |
| 973 | + inputSchema={ |
| 974 | + "type": "object", |
| 975 | + "properties": { |
| 976 | + "expression": {"type": "string", "description": "Math expression"} |
| 977 | + }, |
| 978 | + "required": ["expression"], |
| 979 | + }, |
| 980 | + outputSchema={ |
| 981 | + "type": "object", |
| 982 | + "properties": { |
| 983 | + "result": {"type": "number"}, |
| 984 | + "expression": {"type": "string"}, |
| 985 | + }, |
| 986 | + "required": ["result", "expression"], |
| 987 | + }, |
| 988 | + ) |
| 989 | + ] |
| 990 | + |
| 991 | + |
| 992 | +@server.call_tool() |
| 993 | +async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: |
| 994 | + if name == "calculate": |
| 995 | + expression = arguments["expression"] |
| 996 | + try: |
| 997 | + result = eval(expression) # Use a safe math parser |
| 998 | + structured = {"result": result, "expression": expression} |
| 999 | + |
| 1000 | + # low-level server will validate structured output against the tool's |
| 1001 | + # output schema, and automatically serialize it into a TextContent block |
| 1002 | + # for backwards compatibility with pre-2025-06-18 clients. |
| 1003 | + return structured |
| 1004 | + except Exception as e: |
| 1005 | + raise ValueError(f"Calculation error: {str(e)}") |
| 1006 | +``` |
| 1007 | + |
| 1008 | +Tools can return data in three ways: |
| 1009 | +1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18) |
| 1010 | +2. **Structured data only**: Return a dictionary that will be serialized to JSON (Introduced in spec revision 2025-06-18) |
| 1011 | +3. **Both**: Return a tuple of (content, structured_data) preferred option to use for backwards compatibility |
| 1012 | + |
| 1013 | +When an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early. |
| 1014 | + |
832 | 1015 | ### Writing MCP Clients |
833 | 1016 |
|
834 | 1017 | The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports): |
|
0 commit comments