Skip to content

Commit a74d00f

Browse files
authored
Merge branch 'modelcontextprotocol:main' into main
2 parents df6b41a + 3a9f118 commit a74d00f

45 files changed

Lines changed: 1120 additions & 264 deletions

Some content is hidden

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

.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: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
[protocol-badge]: https://img.shields.io/badge/protocol-modelcontextprotocol.io-blue.svg
8080
[protocol-url]: https://modelcontextprotocol.io
8181
[spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg
82-
[spec-url]: https://spec.modelcontextprotocol.io
82+
[spec-url]: https://modelcontextprotocol.io/specification/latest
8383

8484
## Overview
8585

@@ -383,6 +383,61 @@ causes the tool to be classified as structured _and this is undesirable_,
383383
the classification can be suppressed by passing `structured_output=False`
384384
to the `@tool` decorator.
385385

386+
##### Advanced: Direct CallToolResult
387+
388+
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:
389+
390+
<!-- snippet-source examples/snippets/servers/direct_call_tool_result.py -->
391+
```python
392+
"""Example showing direct CallToolResult return for advanced control."""
393+
394+
from typing import Annotated
395+
396+
from pydantic import BaseModel
397+
398+
from mcp.server.fastmcp import FastMCP
399+
from mcp.types import CallToolResult, TextContent
400+
401+
mcp = FastMCP("CallToolResult Example")
402+
403+
404+
class ValidationModel(BaseModel):
405+
"""Model for validating structured output."""
406+
407+
status: str
408+
data: dict[str, int]
409+
410+
411+
@mcp.tool()
412+
def advanced_tool() -> CallToolResult:
413+
"""Return CallToolResult directly for full control including _meta field."""
414+
return CallToolResult(
415+
content=[TextContent(type="text", text="Response visible to the model")],
416+
_meta={"hidden": "data for client applications only"},
417+
)
418+
419+
420+
@mcp.tool()
421+
def validated_tool() -> Annotated[CallToolResult, ValidationModel]:
422+
"""Return CallToolResult with structured output validation."""
423+
return CallToolResult(
424+
content=[TextContent(type="text", text="Validated response")],
425+
structuredContent={"status": "success", "data": {"result": 42}},
426+
_meta={"internal": "metadata"},
427+
)
428+
429+
430+
@mcp.tool()
431+
def empty_result_tool() -> CallToolResult:
432+
"""For empty results, return CallToolResult with empty content."""
433+
return CallToolResult(content=[])
434+
```
435+
436+
_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)_
437+
<!-- /snippet-source -->
438+
439+
**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`.
440+
386441
<!-- snippet-source examples/snippets/servers/structured_output.py -->
387442
```python
388443
"""Example showing structured output with tools."""
@@ -1769,14 +1824,93 @@ if __name__ == "__main__":
17691824
_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_
17701825
<!-- /snippet-source -->
17711826

1772-
Tools can return data in three ways:
1827+
Tools can return data in four ways:
17731828

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

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

1836+
##### Returning CallToolResult Directly
1837+
1838+
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:
1839+
1840+
<!-- snippet-source examples/snippets/servers/lowlevel/direct_call_tool_result.py -->
1841+
```python
1842+
"""
1843+
Run from the repository root:
1844+
uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py
1845+
"""
1846+
1847+
import asyncio
1848+
from typing import Any
1849+
1850+
import mcp.server.stdio
1851+
import mcp.types as types
1852+
from mcp.server.lowlevel import NotificationOptions, Server
1853+
from mcp.server.models import InitializationOptions
1854+
1855+
server = Server("example-server")
1856+
1857+
1858+
@server.list_tools()
1859+
async def list_tools() -> list[types.Tool]:
1860+
"""List available tools."""
1861+
return [
1862+
types.Tool(
1863+
name="advanced_tool",
1864+
description="Tool with full control including _meta field",
1865+
inputSchema={
1866+
"type": "object",
1867+
"properties": {"message": {"type": "string"}},
1868+
"required": ["message"],
1869+
},
1870+
)
1871+
]
1872+
1873+
1874+
@server.call_tool()
1875+
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:
1876+
"""Handle tool calls by returning CallToolResult directly."""
1877+
if name == "advanced_tool":
1878+
message = str(arguments.get("message", ""))
1879+
return types.CallToolResult(
1880+
content=[types.TextContent(type="text", text=f"Processed: {message}")],
1881+
structuredContent={"result": "success", "message": message},
1882+
_meta={"hidden": "data for client applications only"},
1883+
)
1884+
1885+
raise ValueError(f"Unknown tool: {name}")
1886+
1887+
1888+
async def run():
1889+
"""Run the server."""
1890+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
1891+
await server.run(
1892+
read_stream,
1893+
write_stream,
1894+
InitializationOptions(
1895+
server_name="example",
1896+
server_version="0.1.0",
1897+
capabilities=server.get_capabilities(
1898+
notification_options=NotificationOptions(),
1899+
experimental_capabilities={},
1900+
),
1901+
),
1902+
)
1903+
1904+
1905+
if __name__ == "__main__":
1906+
asyncio.run(run())
1907+
```
1908+
1909+
_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)_
1910+
<!-- /snippet-source -->
1911+
1912+
**Note:** When returning `CallToolResult`, you bypass the automatic content/structured conversion. You must construct the complete response yourself.
1913+
17801914
### Pagination (Advanced)
17811915

17821916
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.
@@ -2299,7 +2433,7 @@ MCP servers declare capabilities during initialization:
22992433

23002434
- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/)
23012435
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
2302-
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
2436+
- [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest)
23032437
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)
23042438

23052439
## Contributing

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
187187

188188
# Create OAuth authentication handler using the new interface
189189
oauth_auth = OAuthClientProvider(
190-
server_url=self.server_url.replace("/mcp", ""),
190+
server_url=self.server_url,
191191
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
192192
storage=InMemoryTokenStorage(),
193193
redirect_handler=_default_redirect_handler,

examples/clients/simple-auth-client/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ ignore = []
3939
line-length = 120
4040
target-version = "py310"
4141

42-
[tool.uv]
43-
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
42+
[dependency-groups]
43+
dev = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]

examples/clients/simple-chatbot/pyproject.toml

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

47-
[tool.uv]
48-
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
47+
[dependency-groups]
48+
dev = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
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/mcp_simple_auth/simple_auth_provider.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
7373

7474
async def register_client(self, client_info: OAuthClientInformationFull):
7575
"""Register a new OAuth client."""
76+
if not client_info.client_id:
77+
raise ValueError("No client_id provided")
7678
self.clients[client_info.client_id] = client_info
7779

7880
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
@@ -209,6 +211,8 @@ async def exchange_authorization_code(
209211
"""Exchange authorization code for tokens."""
210212
if authorization_code.code not in self.auth_codes:
211213
raise ValueError("Invalid authorization code")
214+
if not client.client_id:
215+
raise ValueError("No client_id provided")
212216

213217
# Generate MCP access token
214218
mcp_token = f"mcp_{secrets.token_hex(32)}"

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"]

0 commit comments

Comments
 (0)