Skip to content

Commit fb09415

Browse files
authored
Merge branch 'main' into modelcontextprotocol#552
2 parents bd6cc71 + c2f8730 commit fb09415

File tree

14 files changed

+314
-19
lines changed

14 files changed

+314
-19
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,9 @@ mcp = FastMCP("StatefulServer")
402402
# Stateless server (no session persistence)
403403
mcp = FastMCP("StatelessServer", stateless_http=True)
404404

405+
# Stateless server (no session persistence, no sse stream with supported client)
406+
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
407+
405408
# Run server with streamable_http transport
406409
mcp.run(transport="streamable-http")
407410
```
@@ -434,15 +437,22 @@ def add_two(n: int) -> int:
434437

435438
```python
436439
# main.py
440+
import contextlib
437441
from fastapi import FastAPI
438442
from mcp.echo import echo
439443
from mcp.math import math
440444

441445

442-
app = FastAPI()
446+
# Create a combined lifespan to manage both session managers
447+
@contextlib.asynccontextmanager
448+
async def lifespan(app: FastAPI):
449+
async with contextlib.AsyncExitStack() as stack:
450+
await stack.enter_async_context(echo.mcp.session_manager.run())
451+
await stack.enter_async_context(math.mcp.session_manager.run())
452+
yield
453+
443454

444-
# Use the session manager's lifespan
445-
app = FastAPI(lifespan=lambda app: echo.mcp.session_manager.run())
455+
app = FastAPI(lifespan=lifespan)
446456
app.mount("/echo", echo.mcp.streamable_http_app())
447457
app.mount("/math", math.mcp.streamable_http_app())
448458
```

examples/servers/simple-auth/mcp_simple_auth/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
from mcp_simple_auth.server import main
66

7-
sys.exit(main())
7+
sys.exit(main()) # type: ignore[call-arg]

examples/servers/simple-prompt/mcp_simple_prompt/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .server import main
44

5-
sys.exit(main())
5+
sys.exit(main()) # type: ignore[call-arg]

examples/servers/simple-resource/mcp_simple_resource/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .server import main
44

5-
sys.exit(main())
5+
sys.exit(main()) # type: ignore[call-arg]

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import click
33
import mcp.types as types
44
from mcp.server.lowlevel import Server
5-
from pydantic import FileUrl
5+
from pydantic import AnyUrl
66

77
SAMPLE_RESOURCES = {
88
"greeting": "Hello! This is a sample text resource.",
@@ -26,7 +26,7 @@ def main(port: int, transport: str) -> int:
2626
async def list_resources() -> list[types.Resource]:
2727
return [
2828
types.Resource(
29-
uri=FileUrl(f"file:///{name}.txt"),
29+
uri=AnyUrl(f"file:///{name}.txt"),
3030
name=name,
3131
description=f"A sample text resource named {name}",
3232
mimeType="text/plain",
@@ -35,7 +35,9 @@ async def list_resources() -> list[types.Resource]:
3535
]
3636

3737
@app.read_resource()
38-
async def read_resource(uri: FileUrl) -> str | bytes:
38+
async def read_resource(uri: AnyUrl) -> str | bytes:
39+
if uri.path is None:
40+
raise ValueError(f"Invalid resource path: {uri}")
3941
name = uri.path.replace(".txt", "").lstrip("/")
4042

4143
if name not in SAMPLE_RESOURCES:
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from .server import main
22

33
if __name__ == "__main__":
4-
main()
4+
# Click will handle CLI arguments
5+
import sys
6+
7+
sys.exit(main()) # type: ignore[call-arg]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .server import main
22

33
if __name__ == "__main__":
4-
main()
4+
main() # type: ignore[call-arg]

examples/servers/simple-tool/mcp_simple_tool/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .server import main
44

5-
sys.exit(main())
5+
sys.exit(main()) # type: ignore[call-arg]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Issues = "https://github.com/modelcontextprotocol/python-sdk/issues"
8585
packages = ["src/mcp"]
8686

8787
[tool.pyright]
88-
include = ["src/mcp", "tests"]
88+
include = ["src/mcp", "tests", "examples/servers"]
8989
venvPath = "."
9090
venv = ".venv"
9191
strict = ["src/mcp/**/*.py"]

src/mcp/client/session.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,23 +201,29 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
201201
types.EmptyResult,
202202
)
203203

204-
async def list_resources(self) -> types.ListResourcesResult:
204+
async def list_resources(
205+
self, cursor: str | None = None
206+
) -> types.ListResourcesResult:
205207
"""Send a resources/list request."""
206208
return await self.send_request(
207209
types.ClientRequest(
208210
types.ListResourcesRequest(
209211
method="resources/list",
212+
cursor=cursor,
210213
)
211214
),
212215
types.ListResourcesResult,
213216
)
214217

215-
async def list_resource_templates(self) -> types.ListResourceTemplatesResult:
218+
async def list_resource_templates(
219+
self, cursor: str | None = None
220+
) -> types.ListResourceTemplatesResult:
216221
"""Send a resources/templates/list request."""
217222
return await self.send_request(
218223
types.ClientRequest(
219224
types.ListResourceTemplatesRequest(
220225
method="resources/templates/list",
226+
cursor=cursor,
221227
)
222228
),
223229
types.ListResourceTemplatesResult,
@@ -278,12 +284,13 @@ async def call_tool(
278284
request_read_timeout_seconds=read_timeout_seconds,
279285
)
280286

281-
async def list_prompts(self) -> types.ListPromptsResult:
287+
async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResult:
282288
"""Send a prompts/list request."""
283289
return await self.send_request(
284290
types.ClientRequest(
285291
types.ListPromptsRequest(
286292
method="prompts/list",
293+
cursor=cursor,
287294
)
288295
),
289296
types.ListPromptsResult,
@@ -322,12 +329,13 @@ async def complete(
322329
types.CompleteResult,
323330
)
324331

325-
async def list_tools(self) -> types.ListToolsResult:
332+
async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
326333
"""Send a tools/list request."""
327334
return await self.send_request(
328335
types.ClientRequest(
329336
types.ListToolsRequest(
330337
method="tools/list",
338+
cursor=cursor,
331339
)
332340
),
333341
types.ListToolsResult,

src/mcp/shared/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from mcp.types import LATEST_PROTOCOL_VERSION
22

3-
SUPPORTED_PROTOCOL_VERSIONS: tuple[int, str] = (1, LATEST_PROTOCOL_VERSION)
3+
SUPPORTED_PROTOCOL_VERSIONS: list[str] = ["2024-11-05", LATEST_PROTOCOL_VERSION]

src/mcp/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
not separate types in the schema.
3030
"""
3131

32-
LATEST_PROTOCOL_VERSION = "2024-11-05"
32+
LATEST_PROTOCOL_VERSION = "2025-03-26"
3333

3434
ProgressToken = str | int
3535
Cursor = str
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import pytest
2+
3+
from mcp.server.fastmcp import FastMCP
4+
from mcp.shared.memory import (
5+
create_connected_server_and_client_session as create_session,
6+
)
7+
8+
# Mark the whole module for async tests
9+
pytestmark = pytest.mark.anyio
10+
11+
12+
async def test_list_tools_cursor_parameter():
13+
"""Test that the cursor parameter is accepted for list_tools.
14+
15+
Note: FastMCP doesn't currently implement pagination, so this test
16+
only verifies that the cursor parameter is accepted by the client.
17+
"""
18+
server = FastMCP("test")
19+
20+
# Create a couple of test tools
21+
@server.tool(name="test_tool_1")
22+
async def test_tool_1() -> str:
23+
"""First test tool"""
24+
return "Result 1"
25+
26+
@server.tool(name="test_tool_2")
27+
async def test_tool_2() -> str:
28+
"""Second test tool"""
29+
return "Result 2"
30+
31+
async with create_session(server._mcp_server) as client_session:
32+
# Test without cursor parameter (omitted)
33+
result1 = await client_session.list_tools()
34+
assert len(result1.tools) == 2
35+
36+
# Test with cursor=None
37+
result2 = await client_session.list_tools(cursor=None)
38+
assert len(result2.tools) == 2
39+
40+
# Test with cursor as string
41+
result3 = await client_session.list_tools(cursor="some_cursor_value")
42+
assert len(result3.tools) == 2
43+
44+
# Test with empty string cursor
45+
result4 = await client_session.list_tools(cursor="")
46+
assert len(result4.tools) == 2
47+
48+
49+
async def test_list_resources_cursor_parameter():
50+
"""Test that the cursor parameter is accepted for list_resources.
51+
52+
Note: FastMCP doesn't currently implement pagination, so this test
53+
only verifies that the cursor parameter is accepted by the client.
54+
"""
55+
server = FastMCP("test")
56+
57+
# Create a test resource
58+
@server.resource("resource://test/data")
59+
async def test_resource() -> str:
60+
"""Test resource"""
61+
return "Test data"
62+
63+
async with create_session(server._mcp_server) as client_session:
64+
# Test without cursor parameter (omitted)
65+
result1 = await client_session.list_resources()
66+
assert len(result1.resources) >= 1
67+
68+
# Test with cursor=None
69+
result2 = await client_session.list_resources(cursor=None)
70+
assert len(result2.resources) >= 1
71+
72+
# Test with cursor as string
73+
result3 = await client_session.list_resources(cursor="some_cursor")
74+
assert len(result3.resources) >= 1
75+
76+
# Test with empty string cursor
77+
result4 = await client_session.list_resources(cursor="")
78+
assert len(result4.resources) >= 1
79+
80+
81+
async def test_list_prompts_cursor_parameter():
82+
"""Test that the cursor parameter is accepted for list_prompts.
83+
84+
Note: FastMCP doesn't currently implement pagination, so this test
85+
only verifies that the cursor parameter is accepted by the client.
86+
"""
87+
server = FastMCP("test")
88+
89+
# Create a test prompt
90+
@server.prompt()
91+
async def test_prompt(name: str) -> str:
92+
"""Test prompt"""
93+
return f"Hello, {name}!"
94+
95+
async with create_session(server._mcp_server) as client_session:
96+
# Test without cursor parameter (omitted)
97+
result1 = await client_session.list_prompts()
98+
assert len(result1.prompts) >= 1
99+
100+
# Test with cursor=None
101+
result2 = await client_session.list_prompts(cursor=None)
102+
assert len(result2.prompts) >= 1
103+
104+
# Test with cursor as string
105+
result3 = await client_session.list_prompts(cursor="some_cursor")
106+
assert len(result3.prompts) >= 1
107+
108+
# Test with empty string cursor
109+
result4 = await client_session.list_prompts(cursor="")
110+
assert len(result4.prompts) >= 1
111+
112+
113+
async def test_list_resource_templates_cursor_parameter():
114+
"""Test that the cursor parameter is accepted for list_resource_templates.
115+
116+
Note: FastMCP doesn't currently implement pagination, so this test
117+
only verifies that the cursor parameter is accepted by the client.
118+
"""
119+
server = FastMCP("test")
120+
121+
# Create a test resource template
122+
@server.resource("resource://test/{name}")
123+
async def test_template(name: str) -> str:
124+
"""Test resource template"""
125+
return f"Data for {name}"
126+
127+
async with create_session(server._mcp_server) as client_session:
128+
# Test without cursor parameter (omitted)
129+
result1 = await client_session.list_resource_templates()
130+
assert len(result1.resourceTemplates) >= 1
131+
132+
# Test with cursor=None
133+
result2 = await client_session.list_resource_templates(cursor=None)
134+
assert len(result2.resourceTemplates) >= 1
135+
136+
# Test with cursor as string
137+
result3 = await client_session.list_resource_templates(cursor="some_cursor")
138+
assert len(result3.resourceTemplates) >= 1
139+
140+
# Test with empty string cursor
141+
result4 = await client_session.list_resource_templates(cursor="")
142+
assert len(result4.resourceTemplates) >= 1

0 commit comments

Comments
 (0)