Skip to content

Conversation

@jlowin
Copy link
Owner

@jlowin jlowin commented Jun 20, 2025

Summary

This PR introduces two complementary features for FastMCP tools: structured outputs (JSON tool results) and output schemas (validation/declaration of result format), plus automatic client-side deserialization. FastMCP clients now receive richer, automatically deserialized tool outputs instead of raw content blocks.

Closes #743
Implements a clean DX for modelcontextprotocol/modelcontextprotocol#371

Key Features

Automatic Output Schema Generation + Structured Outputs

@dataclass
class UserProfile:
    name: str
    age: int
    email: str

@mcp.tool
def get_user_profile(user_id: str) -> UserProfile:
    """Get a user's profile information."""
    return UserProfile(name="Alice", age=30, email="[email protected]")

FastMCP automatically:

  1. Generates output schemas from return type annotations for validation/declaration
  2. Converts results to structured outputs (JSON) instead of just text content
  3. Enables client-side deserialization back to Python objects

Manual Control Options

# Custom output schema
@mcp.tool(output_schema={"type": "object", "properties": {...}})
def custom_schema_tool() -> dict: ...

# Full control over structured output
@mcp.tool  
def manual_control_tool() -> ToolResult:
    return ToolResult(
        content=[TextContent(text="Human readable")],
        structured_content={"key": "value"}
    )

Client-Side Deserialization (Breaking Change)

# Before: raw content blocks
result = await client.call_tool("get_user_profile", {"user_id": "123"})
# result: [TextContent(text='{"name": "Alice", "age": 30, "email": "[email protected]"}')]

# After: automatic deserialization
result = await client.call_tool("get_user_profile", {"user_id": "123"})  
# result.data: UserProfile(name="Alice", age=30, email="[email protected]")
# result.content: [TextContent(...)]  # still available

Breaking Change

FastMCP clients now receive CallToolResult objects from call_tool() instead of raw list[ContentBlock]. This provides automatic deserialization of structured outputs while maintaining access to original content through .content.

Technical Implementation

  • Structured Outputs: Tools can return JSON data alongside traditional content blocks
  • Output Schemas: Automatic schema generation from type annotations with manual override support
  • JSON Schema Type Conversion: New json_schema_to_type() utility for client-side validation
  • ToolResult Control: Direct control over structured/unstructured output via ToolResult objects
  • Schema Wrapping: Primitive types automatically wrapped under "result" key
  • FastMCP Type Handling: Special support for Image, Audio, File types

Other Changes

  • Removed experimental component manager functionality
  • Updated all tests for structured output patterns
  • Enhanced documentation with examples
  • Comprehensive test coverage for return types and edge cases

This maintains natural, Pythonic DX while enabling richer tool interactions and full user control when needed.

Copilot AI review requested due to automatic review settings June 20, 2025 22:26
@github-actions github-actions bot added documentation Updates to docs, examples, or guides. Primary change is documentation-related. tests openapi Related to OpenAPI integration, parsing, or code generation features. client Related to the FastMCP client SDK or client-side functionality. labels Jun 20, 2025
@jlowin jlowin changed the base branch from main to protocol-update June 20, 2025 22:26
@jlowin jlowin added the feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. label Jun 20, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds output schema support to FastMCP tools by automatically generating a JSON schema from tool return type annotations, updating tool registration, test cases, and related type conversions. Key changes include:

  • Adding an “output_schema” field to the Tool class and propagating it through tool registration.
  • Implementing schema generation for various return types (native, Pydantic models, dataclasses, and FastMCP special types).
  • Updating tests and documentation to validate and demonstrate the new output schema functionality.

Reviewed Changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/utilities/test_types.py Added tests for the new replace_type function.
tests/tools/test_tool.py Updated tool tests to check for the new output_schema field.
tests/server/test_server_interactions.py Added tests verifying output schema on tool registration via Client.
tests/server/openapi/test_openapi.py Updated OpenAPI schema tests to include output schema fields.
src/fastmcp/utilities/types.py Introduced and implemented the replace_type helper.
src/fastmcp/tools/tool_transform.py Updated schema merging to use input_schema instead of parameters.
src/fastmcp/tools/tool_manager.py Updated return type from MCPContent to ContentBlock.
src/fastmcp/tools/tool.py Integrated output_schema in tool conversion and from_function.
src/fastmcp/server/*.py Updated type hints for content types from MCPContent to ContentBlock
docs/servers/tools.mdx Documented output schema generation and provided JSON example.
Comments suppressed due to low confidence (1)

src/fastmcp/tools/tool.py:305

  • Using the union operator (list | tuple) in isinstance may cause a runtime error. Replace it with a tuple of types: isinstance(result, (list, tuple)).
    """Convert a result to a sequence of content objects."""

@jlowin jlowin added this to the MCP 6/18/25 milestone Jun 20, 2025
@jlowin jlowin changed the title Add output schema to tools MCP 6/18/25: Add output schema to tools Jun 20, 2025
@jlowin
Copy link
Owner Author

jlowin commented Jun 20, 2025

modelcontextprotocol/python-sdk#993 seems like a blocker to get this into the low-level server

@jlowin jlowin removed openapi Related to OpenAPI integration, parsing, or code generation features. client Related to the FastMCP client SDK or client-side functionality. labels Jun 23, 2025
Base automatically changed from protocol-update to main June 26, 2025 22:45
jlowin and others added 9 commits June 26, 2025 19:30
- Tools can return StructuredOutput() for explicit structured data
- Tools with output_schema automatically return structured data
- Client hydrates structured responses into typed objects
- Maintains backward compatibility with unstructured-only tools
- TransformedTool.run() now returns ToolResult instead of list[ContentBlock]
- forward() and forward_raw() return ToolResult from parent tools
- Transform functions can return ToolResult for full control or any value for auto-wrapping
- Maintains backward compatibility with existing transform functions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@github-actions github-actions bot added openapi Related to OpenAPI integration, parsing, or code generation features. client Related to the FastMCP client SDK or client-side functionality. labels Jun 28, 2025
@jlowin jlowin merged commit e222671 into main Jun 28, 2025
8 checks passed
@jlowin jlowin deleted the output-schema branch June 28, 2025 12:16
jordicore pushed a commit to jordicore/fastmcp that referenced this pull request Jul 2, 2025
MCP 6/18/25: Add output schema to tools
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Related to the FastMCP client SDK or client-side functionality. documentation Updates to docs, examples, or guides. Primary change is documentation-related. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. openapi Related to OpenAPI integration, parsing, or code generation features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Annotated for mcp.tool return value type hints

2 participants