Skip to content

Commit a49b287

Browse files
authored
Merge branch 'main' into feature/run-with-port-option
2 parents 796e67e + 4b65963 commit a49b287

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+5318
-1659
lines changed

.github/workflows/publish-docs-manually.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ jobs:
3030
mkdocs-material-
3131
3232
- run: uv sync --frozen --group docs
33-
- run: uv run --no-sync mkdocs gh-deploy --force
33+
- run: uv run --frozen --no-sync mkdocs gh-deploy --force

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
run: uv python install 3.12
2323

2424
- name: Build
25-
run: uv build
25+
run: uv build --frozen
2626

2727
- name: Upload artifacts
2828
uses: actions/upload-artifact@v4
@@ -79,4 +79,4 @@ jobs:
7979
mkdocs-material-
8080
8181
- run: uv sync --frozen --group docs
82-
- run: uv run --no-sync mkdocs gh-deploy --force
82+
- run: uv run --frozen --no-sync mkdocs gh-deploy --force

.github/workflows/shared.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ jobs:
4444
run: uv sync --frozen --all-extras --python ${{ matrix.python-version }}
4545

4646
- name: Run pytest
47-
run: uv run --no-sync pytest
47+
run: uv run --frozen --no-sync pytest
4848
continue-on-error: true

README.md

Lines changed: 205 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
- [Server](#server)
2828
- [Resources](#resources)
2929
- [Tools](#tools)
30+
- [Structured Output](#structured-output)
3031
- [Prompts](#prompts)
3132
- [Images](#images)
3233
- [Context](#context)
@@ -249,6 +250,127 @@ async def fetch_weather(city: str) -> str:
249250
return response.text
250251
```
251252

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+
252374
### Prompts
253375

254376
Prompts are reusable templates that help LLMs interact with your server effectively:
@@ -423,43 +545,42 @@ The `elicit()` method returns an `ElicitationResult` with:
423545

424546
Authentication can be used by servers that want to expose tools accessing protected resources.
425547

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:
428551

429552
```python
430553
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
437556

438557

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+
...
442563

443564

444565
mcp = FastMCP(
445566
"My App",
446-
auth_server_provider=MyOAuthServerProvider(),
567+
token_verifier=MyTokenVerifier(),
447568
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"],
458572
),
459573
)
460574
```
461575

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.
463584

464585
## Running Your Server
465586

@@ -844,6 +965,67 @@ if __name__ == "__main__":
844965

845966
Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server.
846967

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+
8471029
### Writing MCP Clients
8481030

8491031
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):

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ async def connect(self):
160160
print(f"🔗 Attempting to connect to {self.server_url}...")
161161

162162
try:
163-
# Set up callback server
164-
callback_server = CallbackServer(port=3000)
163+
callback_server = CallbackServer(port=3030)
165164
callback_server.start()
166165

167166
async def callback_handler() -> tuple[str, str | None]:
@@ -175,7 +174,7 @@ async def callback_handler() -> tuple[str, str | None]:
175174

176175
client_metadata_dict = {
177176
"client_name": "Simple Auth Client",
178-
"redirect_uris": ["http://localhost:3000/callback"],
177+
"redirect_uris": ["http://localhost:3030/callback"],
179178
"grant_types": ["authorization_code", "refresh_token"],
180179
"response_types": ["code"],
181180
"token_endpoint_auth_method": "client_secret_post",

0 commit comments

Comments
 (0)