|
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: |
@@ -423,43 +545,42 @@ The `elicit()` method returns an `ElicitationResult` with: |
423 | 545 |
|
424 | 546 | Authentication can be used by servers that want to expose tools accessing protected resources. |
425 | 547 |
|
426 | | -`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by |
427 | | -providing an implementation of the `OAuthAuthorizationServerProvider` protocol. |
| 548 | +`mcp.server.auth` implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and implements RFC 9728 (Protected Resource Metadata) for AS discovery. |
| 549 | + |
| 550 | +MCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol: |
428 | 551 |
|
429 | 552 | ```python |
430 | 553 | from mcp import FastMCP |
431 | | -from mcp.server.auth.provider import OAuthAuthorizationServerProvider |
432 | | -from mcp.server.auth.settings import ( |
433 | | - AuthSettings, |
434 | | - ClientRegistrationOptions, |
435 | | - RevocationOptions, |
436 | | -) |
| 554 | +from mcp.server.auth.provider import TokenVerifier, TokenInfo |
| 555 | +from mcp.server.auth.settings import AuthSettings |
437 | 556 |
|
438 | 557 |
|
439 | | -class MyOAuthServerProvider(OAuthAuthorizationServerProvider): |
440 | | - # See an example on how to implement at `examples/servers/simple-auth` |
441 | | - ... |
| 558 | +class MyTokenVerifier(TokenVerifier): |
| 559 | + # Implement token validation logic (typically via token introspection) |
| 560 | + async def verify_token(self, token: str) -> TokenInfo: |
| 561 | + # Verify with your authorization server |
| 562 | + ... |
442 | 563 |
|
443 | 564 |
|
444 | 565 | mcp = FastMCP( |
445 | 566 | "My App", |
446 | | - auth_server_provider=MyOAuthServerProvider(), |
| 567 | + token_verifier=MyTokenVerifier(), |
447 | 568 | auth=AuthSettings( |
448 | | - issuer_url="https://myapp.com", |
449 | | - revocation_options=RevocationOptions( |
450 | | - enabled=True, |
451 | | - ), |
452 | | - client_registration_options=ClientRegistrationOptions( |
453 | | - enabled=True, |
454 | | - valid_scopes=["myscope", "myotherscope"], |
455 | | - default_scopes=["myscope"], |
456 | | - ), |
457 | | - required_scopes=["myscope"], |
| 569 | + issuer_url="https://auth.example.com", |
| 570 | + resource_server_url="http://localhost:3001", |
| 571 | + required_scopes=["mcp:read", "mcp:write"], |
458 | 572 | ), |
459 | 573 | ) |
460 | 574 | ``` |
461 | 575 |
|
462 | | -See [OAuthAuthorizationServerProvider](src/mcp/server/auth/provider.py) for more details. |
| 576 | +For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/). |
| 577 | + |
| 578 | +**Architecture:** |
| 579 | +- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance |
| 580 | +- **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources |
| 581 | +- **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server |
| 582 | + |
| 583 | +See [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation. |
463 | 584 |
|
464 | 585 | ## Running Your Server |
465 | 586 |
|
@@ -844,6 +965,67 @@ if __name__ == "__main__": |
844 | 965 |
|
845 | 966 | Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server. |
846 | 967 |
|
| 968 | +#### Structured Output Support |
| 969 | + |
| 970 | +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: |
| 971 | + |
| 972 | +```python |
| 973 | +from types import Any |
| 974 | + |
| 975 | +import mcp.types as types |
| 976 | +from mcp.server.lowlevel import Server |
| 977 | + |
| 978 | +server = Server("example-server") |
| 979 | + |
| 980 | + |
| 981 | +@server.list_tools() |
| 982 | +async def list_tools() -> list[types.Tool]: |
| 983 | + return [ |
| 984 | + types.Tool( |
| 985 | + name="calculate", |
| 986 | + description="Perform mathematical calculations", |
| 987 | + inputSchema={ |
| 988 | + "type": "object", |
| 989 | + "properties": { |
| 990 | + "expression": {"type": "string", "description": "Math expression"} |
| 991 | + }, |
| 992 | + "required": ["expression"], |
| 993 | + }, |
| 994 | + outputSchema={ |
| 995 | + "type": "object", |
| 996 | + "properties": { |
| 997 | + "result": {"type": "number"}, |
| 998 | + "expression": {"type": "string"}, |
| 999 | + }, |
| 1000 | + "required": ["result", "expression"], |
| 1001 | + }, |
| 1002 | + ) |
| 1003 | + ] |
| 1004 | + |
| 1005 | + |
| 1006 | +@server.call_tool() |
| 1007 | +async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: |
| 1008 | + if name == "calculate": |
| 1009 | + expression = arguments["expression"] |
| 1010 | + try: |
| 1011 | + result = eval(expression) # Use a safe math parser |
| 1012 | + structured = {"result": result, "expression": expression} |
| 1013 | + |
| 1014 | + # low-level server will validate structured output against the tool's |
| 1015 | + # output schema, and automatically serialize it into a TextContent block |
| 1016 | + # for backwards compatibility with pre-2025-06-18 clients. |
| 1017 | + return structured |
| 1018 | + except Exception as e: |
| 1019 | + raise ValueError(f"Calculation error: {str(e)}") |
| 1020 | +``` |
| 1021 | + |
| 1022 | +Tools can return data in three ways: |
| 1023 | +1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18) |
| 1024 | +2. **Structured data only**: Return a dictionary that will be serialized to JSON (Introduced in spec revision 2025-06-18) |
| 1025 | +3. **Both**: Return a tuple of (content, structured_data) preferred option to use for backwards compatibility |
| 1026 | + |
| 1027 | +When an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early. |
| 1028 | + |
847 | 1029 | ### Writing MCP Clients |
848 | 1030 |
|
849 | 1031 | 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