Skip to content

MCP 6/18/25: Add output schema to tools #901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 28, 2025
Merged

MCP 6/18/25: Add output schema to tools #901

merged 17 commits into from
Jun 28, 2025

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="alice@example.com")

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": "alice@example.com"}')]

# After: automatic deserialization
result = await client.call_tool("get_user_profile", {"user_id": "123"})  
# result.data: UserProfile(name="Alice", age=30, email="alice@example.com")
# 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 Copilot AI review requested due to automatic review settings June 20, 2025 22:26
@github-actions github-actions bot added documentation Improvements or additions to documentation tests component: openapi component: client 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 label Jun 20, 2025
Copy link
Contributor

@Copilot 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 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 <noreply@anthropic.com>
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Annotated for mcp.tool return value type hints
1 participant