Skip to content

Commit 0953fcb

Browse files
authored
Merge branch 'main' into main
2 parents 6c7819a + 6c26d08 commit 0953fcb

File tree

21 files changed

+487
-45
lines changed

21 files changed

+487
-45
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
uses: astral-sh/setup-uv@v3
2020
with:
2121
enable-cache: true
22-
version: 0.7.2
22+
version: 0.9.5
2323

2424
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
2525
- uses: actions/cache@v4

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
uses: astral-sh/setup-uv@v3
1717
with:
1818
enable-cache: true
19-
version: 0.7.2
19+
version: 0.9.5
2020

2121
- name: Set up Python 3.12
2222
run: uv python install 3.12
@@ -68,7 +68,7 @@ jobs:
6868
uses: astral-sh/setup-uv@v3
6969
with:
7070
enable-cache: true
71-
version: 0.7.2
71+
version: 0.9.5
7272

7373
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
7474
- uses: actions/cache@v4

.github/workflows/shared.yml

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,56 +13,62 @@ jobs:
1313
pre-commit:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@v5
1717

18-
- uses: astral-sh/setup-uv@v5
18+
- uses: astral-sh/setup-uv@v7
1919
with:
2020
enable-cache: true
21-
version: 0.7.2
22-
21+
version: 0.9.5
2322
- name: Install dependencies
2423
run: uv sync --frozen --all-extras --python 3.10
2524

26-
- uses: pre-commit/action@v3.0.0
25+
- uses: pre-commit/action@v3.0.1
2726
with:
2827
extra_args: --all-files --verbose
2928
env:
3029
SKIP: no-commit-to-branch
3130

3231
test:
32+
name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }})
3333
runs-on: ${{ matrix.os }}
3434
timeout-minutes: 10
3535
continue-on-error: true
3636
strategy:
3737
matrix:
3838
python-version: ["3.10", "3.11", "3.12", "3.13"]
39-
dep-resolution: ["lowest-direct", "highest"]
39+
dep-resolution:
40+
- name: lowest-direct
41+
install-flags: "--resolution lowest-direct"
42+
- name: highest
43+
install-flags: "--frozen"
4044
os: [ubuntu-latest, windows-latest]
4145

4246
steps:
43-
- uses: actions/checkout@v4
47+
- uses: actions/checkout@v5
4448

4549
- name: Install uv
46-
uses: astral-sh/setup-uv@v3
50+
uses: astral-sh/setup-uv@v7
4751
with:
4852
enable-cache: true
49-
version: 0.7.2
53+
version: 0.9.5
5054

5155
- name: Install the project
52-
run: uv sync --frozen --all-extras --python ${{ matrix.python-version }} --resolution ${{ matrix.dep-resolution }}
56+
run: uv sync ${{ matrix.dep-resolution.install-flags }} --all-extras --python ${{ matrix.python-version }}
5357

5458
- name: Run pytest
55-
run: uv run --frozen --no-sync pytest
59+
run: uv run ${{ matrix.dep-resolution.install-flags }} --no-sync pytest
60+
env:
61+
UV_RESOLUTION: ${{ matrix.dep-resolution.name == 'lowest-direct' && 'lowest-direct' || 'highest' }}
5662

5763
readme-snippets:
5864
runs-on: ubuntu-latest
5965
steps:
60-
- uses: actions/checkout@v4
66+
- uses: actions/checkout@v5
6167

62-
- uses: astral-sh/setup-uv@v5
68+
- uses: astral-sh/setup-uv@v7
6369
with:
6470
enable-cache: true
65-
version: 0.7.2
71+
version: 0.9.5
6672

6773
- name: Install dependencies
6874
run: uv sync --frozen --all-extras --python 3.10

README.md

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,61 @@ causes the tool to be classified as structured _and this is undesirable_,
497497
the classification can be suppressed by passing `structured_output=False`
498498
to the `@tool` decorator.
499499

500+
##### Advanced: Direct CallToolResult
501+
502+
For full control over tool responses including the `_meta` field (for passing data to client applications without exposing it to the model), you can return `CallToolResult` directly:
503+
504+
<!-- snippet-source examples/snippets/servers/direct_call_tool_result.py -->
505+
```python
506+
"""Example showing direct CallToolResult return for advanced control."""
507+
508+
from typing import Annotated
509+
510+
from pydantic import BaseModel
511+
512+
from mcp.server.fastmcp import FastMCP
513+
from mcp.types import CallToolResult, TextContent
514+
515+
mcp = FastMCP("CallToolResult Example")
516+
517+
518+
class ValidationModel(BaseModel):
519+
"""Model for validating structured output."""
520+
521+
status: str
522+
data: dict[str, int]
523+
524+
525+
@mcp.tool()
526+
def advanced_tool() -> CallToolResult:
527+
"""Return CallToolResult directly for full control including _meta field."""
528+
return CallToolResult(
529+
content=[TextContent(type="text", text="Response visible to the model")],
530+
_meta={"hidden": "data for client applications only"},
531+
)
532+
533+
534+
@mcp.tool()
535+
def validated_tool() -> Annotated[CallToolResult, ValidationModel]:
536+
"""Return CallToolResult with structured output validation."""
537+
return CallToolResult(
538+
content=[TextContent(type="text", text="Validated response")],
539+
structuredContent={"status": "success", "data": {"result": 42}},
540+
_meta={"internal": "metadata"},
541+
)
542+
543+
544+
@mcp.tool()
545+
def empty_result_tool() -> CallToolResult:
546+
"""For empty results, return CallToolResult with empty content."""
547+
return CallToolResult(content=[])
548+
```
549+
550+
_Full example: [examples/snippets/servers/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_call_tool_result.py)_
551+
<!-- /snippet-source -->
552+
553+
**Important:** `CallToolResult` must always be returned (no `Optional` or `Union`). For empty results, use `CallToolResult(content=[])`. For optional simple types, use `str | None` without `CallToolResult`.
554+
500555
<!-- snippet-source examples/snippets/servers/structured_output.py -->
501556
```python
502557
"""Example showing structured output with tools."""
@@ -1883,14 +1938,93 @@ if __name__ == "__main__":
18831938
_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_
18841939
<!-- /snippet-source -->
18851940

1886-
Tools can return data in three ways:
1941+
Tools can return data in four ways:
18871942

18881943
1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18)
18891944
2. **Structured data only**: Return a dictionary that will be serialized to JSON (Introduced in spec revision 2025-06-18)
18901945
3. **Both**: Return a tuple of (content, structured_data) preferred option to use for backwards compatibility
1946+
4. **Direct CallToolResult**: Return `CallToolResult` directly for full control (including `_meta` field)
18911947

18921948
When an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early.
18931949

1950+
##### Returning CallToolResult Directly
1951+
1952+
For full control over the response including the `_meta` field (for passing data to client applications without exposing it to the model), return `CallToolResult` directly:
1953+
1954+
<!-- snippet-source examples/snippets/servers/lowlevel/direct_call_tool_result.py -->
1955+
```python
1956+
"""
1957+
Run from the repository root:
1958+
uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py
1959+
"""
1960+
1961+
import asyncio
1962+
from typing import Any
1963+
1964+
import mcp.server.stdio
1965+
import mcp.types as types
1966+
from mcp.server.lowlevel import NotificationOptions, Server
1967+
from mcp.server.models import InitializationOptions
1968+
1969+
server = Server("example-server")
1970+
1971+
1972+
@server.list_tools()
1973+
async def list_tools() -> list[types.Tool]:
1974+
"""List available tools."""
1975+
return [
1976+
types.Tool(
1977+
name="advanced_tool",
1978+
description="Tool with full control including _meta field",
1979+
inputSchema={
1980+
"type": "object",
1981+
"properties": {"message": {"type": "string"}},
1982+
"required": ["message"],
1983+
},
1984+
)
1985+
]
1986+
1987+
1988+
@server.call_tool()
1989+
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:
1990+
"""Handle tool calls by returning CallToolResult directly."""
1991+
if name == "advanced_tool":
1992+
message = str(arguments.get("message", ""))
1993+
return types.CallToolResult(
1994+
content=[types.TextContent(type="text", text=f"Processed: {message}")],
1995+
structuredContent={"result": "success", "message": message},
1996+
_meta={"hidden": "data for client applications only"},
1997+
)
1998+
1999+
raise ValueError(f"Unknown tool: {name}")
2000+
2001+
2002+
async def run():
2003+
"""Run the server."""
2004+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
2005+
await server.run(
2006+
read_stream,
2007+
write_stream,
2008+
InitializationOptions(
2009+
server_name="example",
2010+
server_version="0.1.0",
2011+
capabilities=server.get_capabilities(
2012+
notification_options=NotificationOptions(),
2013+
experimental_capabilities={},
2014+
),
2015+
),
2016+
)
2017+
2018+
2019+
if __name__ == "__main__":
2020+
asyncio.run(run())
2021+
```
2022+
2023+
_Full example: [examples/snippets/servers/lowlevel/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/direct_call_tool_result.py)_
2024+
<!-- /snippet-source -->
2025+
2026+
**Note:** When returning `CallToolResult`, you bypass the automatic content/structured conversion. You must construct the complete response yourself.
2027+
18942028
### Pagination (Advanced)
18952029

18962030
For servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
FastMCP Echo Server with direct CallToolResult return
3+
"""
4+
5+
from typing import Annotated
6+
7+
from pydantic import BaseModel
8+
9+
from mcp.server.fastmcp import FastMCP
10+
from mcp.types import CallToolResult, TextContent
11+
12+
mcp = FastMCP("Echo Server")
13+
14+
15+
class EchoResponse(BaseModel):
16+
text: str
17+
18+
19+
@mcp.tool()
20+
def echo(text: str) -> Annotated[CallToolResult, EchoResponse]:
21+
"""Echo the input text with structure and metadata"""
22+
return CallToolResult(
23+
content=[TextContent(type="text", text=text)], structuredContent={"text": text}, _meta={"some": "metadata"}
24+
)

examples/servers/simple-auth/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ build-backend = "hatchling.build"
2929
[tool.hatch.build.targets.wheel]
3030
packages = ["mcp_simple_auth"]
3131

32-
[tool.uv]
33-
dev-dependencies = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
32+
[dependency-groups]
33+
dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]

examples/servers/simple-pagination/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ ignore = []
4343
line-length = 120
4444
target-version = "py310"
4545

46-
[tool.uv]
47-
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
46+
[dependency-groups]
47+
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]

examples/servers/simple-prompt/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ ignore = []
4343
line-length = 120
4444
target-version = "py310"
4545

46-
[tool.uv]
47-
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
46+
[dependency-groups]
47+
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]

examples/servers/simple-resource/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ ignore = []
4343
line-length = 120
4444
target-version = "py310"
4545

46-
[tool.uv]
47-
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
46+
[dependency-groups]
47+
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]

examples/servers/simple-streamablehttp-stateless/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ ignore = []
3232
line-length = 120
3333
target-version = "py310"
3434

35-
[tool.uv]
36-
dev-dependencies = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]
35+
[dependency-groups]
36+
dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"]

0 commit comments

Comments
 (0)