Skip to content

feat: Add output schema generation for tools and update documentation #757

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 147 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The Model Context Protocol allows applications to provide context for LLMs in a

### Adding MCP to your python project

We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.

If you haven't created a uv-managed project yet, create one:

Expand All @@ -89,6 +89,7 @@ If you haven't created a uv-managed project yet, create one:
```

Alternatively, for projects using pip for dependencies:

```bash
pip install "mcp[cli]"
```
Expand Down Expand Up @@ -128,11 +129,13 @@ def get_greeting(name: str) -> str:
```

You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running:

```bash
mcp install server.py
```

Alternatively, you can test it with the MCP Inspector:

```bash
mcp dev server.py
```
Expand Down Expand Up @@ -245,6 +248,144 @@ async def fetch_weather(city: str) -> str:
return response.text
```

#### Output Schemas

Tools automatically generate JSON Schema definitions for their return types, helping LLMs understand the structure of the data they'll receive. FastMCP also enhances these schemas with semantic metadata that enables intelligent UI rendering and data formatting.

##### Basic Schema Generation

```python
from pydantic import BaseModel
from mcp.server.fastmcp import FastMCP

# Create server
mcp = FastMCP("Output Schema Demo")


# Tools with primitive return types
@mcp.tool()
def get_temperature(city: str) -> float:
"""Get the current temperature for a city"""
# In a real implementation, this would fetch actual weather data
return 72.5


# Tools with dictionary return types
@mcp.tool()
def get_user(user_id: int) -> dict:
"""Get user information by ID"""
return {"id": user_id, "name": "John Doe", "email": "john@example.com"}


# Using Pydantic models for structured output
class WeatherData(BaseModel):
temperature: float
humidity: float
conditions: str


@mcp.tool()
def get_weather_data(city: str) -> WeatherData:
"""Get structured weather data for a city"""
# In a real implementation, this would fetch actual weather data
return WeatherData(
temperature=72.5,
humidity=65.0,
conditions="Partly cloudy",
)


# Complex nested models
class Location(BaseModel):
city: str
country: str
coordinates: tuple[float, float]


class WeatherForecast(BaseModel):
current: WeatherData
location: Location
forecast: list[WeatherData]


@mcp.tool()
def get_weather_forecast(city: str) -> WeatherForecast:
"""Get detailed weather forecast for a city"""
# In a real implementation, this would fetch actual weather data
return WeatherForecast(
current=WeatherData(
temperature=72.5,
humidity=65.0,
conditions="Partly cloudy",
),
location=Location(city=city, country="USA", coordinates=(37.7749, -122.4194)),
forecast=[
WeatherData(temperature=75.0, humidity=62.0, conditions="Sunny"),
WeatherData(temperature=68.0, humidity=80.0, conditions="Rainy"),
],
)
```

##### Semantic Metadata Enhancement

FastMCP automatically enhances output schemas with semantic metadata by analyzing field names and types. This helps client applications provide intelligent UI rendering and formatting:

```python
from pydantic import BaseModel
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Enhanced Schema Demo")


class UserProfile(BaseModel):
email: str # Automatically detected as semantic_type: "email"
profile_url: str # Automatically detected as semantic_type: "url"
avatar_image: str # Automatically detected as semantic_type: "image_url"
created_date: str # Automatically detected as semantic_type: "datetime"
account_balance: float # Automatically detected as semantic_type: "currency"
completion_percentage: (
float # Automatically detected as semantic_type: "percentage"
)
primary_color: str # Automatically detected as semantic_type: "color"


@mcp.tool()
def get_user_profile(user_id: str) -> UserProfile:
"""Get user profile with semantic field types"""
return UserProfile(
email="user@example.com",
profile_url="https://example.com/users/12345",
avatar_image="https://example.com/avatars/user.jpg",
created_date="2023-06-15T10:30:00Z",
account_balance=150.75,
completion_percentage=85.5,
primary_color="#3498db",
)
```

**Supported Semantic Types:**

- `email` - Email addresses
- `url`, `link` - Web URLs and links
- `image_url`, `audio_url`, `video_url` - Media URLs
- `datetime` - Date and time fields (with subtypes like `date`, `time`, `timestamp`)
- `currency`, `money` - Monetary values
- `percentage` - Percentage values
- `color` - Color codes and values
- `phone` - Phone numbers
- `status` - Status indicators
- `media_format` - File format detection for audio/video/image files

**Benefits for Client Applications:**

- Email fields can display mail icons and validation
- URLs become clickable links with preview capabilities
- Date fields get appropriate date pickers and formatting
- Currency fields show proper monetary formatting
- Media URLs can display thumbnails or players
- Status fields can show colored indicators
- Percentage fields can render as progress bars

### Prompts

Prompts are reusable templates that help LLMs interact with your server effectively:
Expand Down Expand Up @@ -381,6 +522,7 @@ if __name__ == "__main__":
```

Run it with:

```bash
python server.py
# or
Expand Down Expand Up @@ -458,18 +600,17 @@ app.mount("/math", math.mcp.streamable_http_app())
```

For low level server with Streamable HTTP implementations, see:

- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)



The streamable HTTP transport supports:

- Stateful and stateless operation modes
- Resumability with event stores
- JSON or SSE response formats
- JSON or SSE response formats
- Better scalability for multi-node deployments


### Mounting to an Existing ASGI Server

> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
Expand Down Expand Up @@ -637,6 +778,7 @@ async def query_db(name: str, arguments: dict) -> list:
```

The lifespan API provides:

- A way to initialize resources when the server starts and clean them up when it stops
- Access to initialized resources through the request context in handlers
- Type-safe context passing between lifespan and request handlers
Expand Down Expand Up @@ -849,7 +991,6 @@ async def main():

For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).


### MCP Primitives

The MCP protocol defines three core primitives that servers can implement:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ async def list_tools() -> list[types.Tool]:
},
},
},
outputSchema={
"type": "array",
"items": {
"type": "object",
"description": "TextContent with notification information",
},
"description": "List of text content with notification results",
},
)
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ async def list_tools() -> list[types.Tool]:
},
},
},
outputSchema={
"type": "array",
"items": {
"type": "object",
"description": "TextContent with notification information",
},
"description": "List of text content with notification results",
},
)
]

Expand Down
11 changes: 11 additions & 0 deletions examples/servers/simple-tool/mcp_simple_tool/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ async def list_tools() -> list[types.Tool]:
}
},
},
outputSchema={
"type": "array",
"items": {
"anyOf": [
{"type": "object", "description": "TextContent"},
{"type": "object", "description": "ImageContent"},
{"type": "object", "description": "EmbeddedResource"},
]
},
"description": "List of content from the fetched website",
},
)
]

Expand Down
1 change: 0 additions & 1 deletion src/mcp/client/session_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ async def __aexit__(
for exit_stack in self._session_exit_stacks.values():
tg.start_soon(exit_stack.aclose)


@property
def sessions(self) -> list[mcp.ClientSession]:
"""Returns the list of sessions being managed."""
Expand Down
1 change: 1 addition & 0 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ async def list_tools(self) -> list[MCPTool]:
name=info.name,
description=info.description,
inputSchema=info.parameters,
outputSchema=info.output_schema,
annotations=info.annotations,
)
for info in tools
Expand Down
47 changes: 44 additions & 3 deletions src/mcp/server/fastmcp/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
from mcp.server.fastmcp.utilities.schema import enhance_output_schema
from mcp.types import ToolAnnotations

if TYPE_CHECKING:
Expand All @@ -24,6 +25,9 @@ class Tool(BaseModel):
name: str = Field(description="Name of the tool")
description: str = Field(description="Description of what the tool does")
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
output_schema: dict[str, Any] | None = Field(
None, description="JSON schema for tool output format"
)
fn_metadata: FuncMetadata = Field(
description="Metadata about the function including a pydantic model for tool"
" arguments"
Expand All @@ -46,6 +50,8 @@ def from_function(
annotations: ToolAnnotations | None = None,
) -> Tool:
"""Create a Tool from a function."""
from pydantic import TypeAdapter

from mcp.server.fastmcp.server import Context

func_name = name or fn.__name__
Expand All @@ -71,6 +77,38 @@ def from_function(
)
parameters = func_arg_metadata.arg_model.model_json_schema()

# Generate output schema from return type annotation if possible
output_schema = None
sig = inspect.signature(fn)
if sig.return_annotation != inspect.Signature.empty:
try:
# Handle common return types that don't need schema
if sig.return_annotation is str:
output_schema = {"type": "string"}
elif sig.return_annotation is int:
output_schema = {"type": "integer"}
elif sig.return_annotation is float:
output_schema = {"type": "number"}
elif sig.return_annotation is bool:
output_schema = {"type": "boolean"}
elif sig.return_annotation is dict:
output_schema = {"type": "object"}
elif sig.return_annotation is list:
output_schema = {"type": "array"}
else:
# Try to generate schema using TypeAdapter
return_type_adapter = TypeAdapter(sig.return_annotation)
output_schema = return_type_adapter.json_schema()

# Enhance the schema with detailed field information
if output_schema:
output_schema = enhance_output_schema(
output_schema, sig.return_annotation
)
except Exception:
# If we can't generate a schema, we'll leave it as None
pass

return cls(
fn=fn,
name=func_name,
Expand All @@ -80,6 +118,7 @@ def from_function(
is_async=is_async,
context_kwarg=context_kwarg,
annotations=annotations,
output_schema=output_schema,
)

async def run(
Expand All @@ -93,9 +132,11 @@ async def run(
self.fn,
self.is_async,
arguments,
{self.context_kwarg: context}
if self.context_kwarg is not None
else None,
(
{self.context_kwarg: context}
if self.context_kwarg is not None
else None
),
)
except Exception as e:
raise ToolError(f"Error executing tool {self.name}: {e}") from e
Expand Down
Loading
Loading