alexsee commited on
Commit
c6f834f
·
1 Parent(s): af91f4b

fix: missing related_request_id for report_progress

Browse files
src/fastmcp/server/context.py CHANGED
@@ -122,6 +122,7 @@ class Context:
122
  progress=progress,
123
  total=total,
124
  message=message,
 
125
  )
126
 
127
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
 
122
  progress=progress,
123
  total=total,
124
  message=message,
125
+ related_request_id=self.request_id,
126
  )
127
 
128
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
tests/client/test_streamable_http.py CHANGED
@@ -6,9 +6,11 @@ from collections.abc import AsyncGenerator
6
  import pytest
7
  import uvicorn
8
  from mcp import McpError
 
9
  from starlette.applications import Starlette
10
  from starlette.routing import Mount
11
 
 
12
  from fastmcp.client import Client
13
  from fastmcp.client.transports import StreamableHttpTransport
14
  from fastmcp.server.dependencies import get_http_request
@@ -22,7 +24,7 @@ def fastmcp_server():
22
 
23
  # Add a tool
24
  @server.tool
25
- def greet(name: str) -> str:
26
  """Greet someone by name."""
27
  return f"Hello, {name}!"
28
 
@@ -38,6 +40,12 @@ def fastmcp_server():
38
  await asyncio.sleep(seconds)
39
  return f"Slept for {seconds} seconds"
40
 
 
 
 
 
 
 
41
  # Add a resource
42
  @server.resource(uri="data://users")
43
  async def get_users():
@@ -95,6 +103,14 @@ async def streamable_http_server() -> AsyncGenerator[str, None]:
95
  yield f"{url}/mcp"
96
 
97
 
 
 
 
 
 
 
 
 
98
  async def test_ping(streamable_http_server: str):
99
  """Test pinging the server."""
100
  async with Client(
@@ -117,6 +133,22 @@ async def test_http_headers(streamable_http_server: str):
117
  assert json_result["x-demo-header"] == "ABC"
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  async def test_nested_streamable_http_server_resolves_correctly():
121
  # tests patch for
122
  # https://github.com/modelcontextprotocol/python-sdk/pull/659
 
6
  import pytest
7
  import uvicorn
8
  from mcp import McpError
9
+ from mcp.types import TextContent
10
  from starlette.applications import Starlette
11
  from starlette.routing import Mount
12
 
13
+ from fastmcp import Context
14
  from fastmcp.client import Client
15
  from fastmcp.client.transports import StreamableHttpTransport
16
  from fastmcp.server.dependencies import get_http_request
 
24
 
25
  # Add a tool
26
  @server.tool
27
+ async def greet(name: str) -> str:
28
  """Greet someone by name."""
29
  return f"Hello, {name}!"
30
 
 
40
  await asyncio.sleep(seconds)
41
  return f"Slept for {seconds} seconds"
42
 
43
+ @server.tool
44
+ async def greet_with_progress(name: str, ctx: Context) -> str:
45
+ """Report progress for a greeting."""
46
+ await ctx.report_progress(0.5, 1.0, "Greeting in progress")
47
+ return f"Hello, {name}!"
48
+
49
  # Add a resource
50
  @server.resource(uri="data://users")
51
  async def get_users():
 
103
  yield f"{url}/mcp"
104
 
105
 
106
+ PROGRESS_MESSAGES = []
107
+
108
+ async def progress_handler(
109
+ progress: float, total: float | None, message: str | None
110
+ ) -> None:
111
+ PROGRESS_MESSAGES.append(dict(progress=progress, total=total, message=message))
112
+
113
+
114
  async def test_ping(streamable_http_server: str):
115
  """Test pinging the server."""
116
  async with Client(
 
133
  assert json_result["x-demo-header"] == "ABC"
134
 
135
 
136
+ async def test_greet_with_progress_tool(streamable_http_server: str):
137
+ """Test calling the greet tool."""
138
+ async with Client(
139
+ transport=StreamableHttpTransport(streamable_http_server), progress_handler=progress_handler
140
+ ) as client:
141
+ result = await client.call_tool("greet_with_progress", {"name": "Alice"})
142
+
143
+ assert isinstance(result, list)
144
+ assert isinstance(result[0], TextContent)
145
+ assert result[0].text == "Hello, Alice!"
146
+
147
+ assert PROGRESS_MESSAGES == [
148
+ dict(progress=0.5, total=1.0, message="Greeting in progress"),
149
+ ]
150
+
151
+
152
  async def test_nested_streamable_http_server_resolves_correctly():
153
  # tests patch for
154
  # https://github.com/modelcontextprotocol/python-sdk/pull/659
tests/client/test_streamable_http_stateless.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import sys
4
+ from collections.abc import AsyncGenerator
5
+
6
+ import pytest
7
+ import uvicorn
8
+ from mcp import McpError
9
+ from mcp.types import TextContent
10
+ from starlette.applications import Starlette
11
+ from starlette.routing import Mount
12
+
13
+ from fastmcp import Context
14
+ from fastmcp.client import Client
15
+ from fastmcp.client.transports import StreamableHttpTransport
16
+ from fastmcp.server.dependencies import get_http_request
17
+ from fastmcp.server.server import FastMCP
18
+ from fastmcp.utilities.tests import run_server_in_process
19
+
20
+
21
+ def fastmcp_server():
22
+ """Fixture that creates a FastMCP server with tools, resources, and prompts."""
23
+ server = FastMCP("TestServer")
24
+ server.settings.stateless_http = True
25
+
26
+ # Add a tool
27
+ @server.tool
28
+ async def greet(name: str) -> str:
29
+ """Greet someone by name."""
30
+ return f"Hello, {name}!"
31
+
32
+ # Add a second tool
33
+ @server.tool
34
+ def add(a: int, b: int) -> int:
35
+ """Add two numbers together."""
36
+ return a + b
37
+
38
+ @server.tool
39
+ async def sleep(seconds: float) -> str:
40
+ """Sleep for a given number of seconds."""
41
+ await asyncio.sleep(seconds)
42
+ return f"Slept for {seconds} seconds"
43
+
44
+ @server.tool
45
+ async def greet_with_progress(name: str, ctx: Context) -> str:
46
+ """Report progress for a greeting."""
47
+ await ctx.report_progress(0.5, 1.0, "Greeting in progress")
48
+ return f"Hello, {name}!"
49
+
50
+ # Add a resource
51
+ @server.resource(uri="data://users")
52
+ async def get_users():
53
+ return ["Alice", "Bob", "Charlie"]
54
+
55
+ # Add a resource template
56
+ @server.resource(uri="data://user/{user_id}")
57
+ async def get_user(user_id: str):
58
+ return {"id": user_id, "name": f"User {user_id}", "active": True}
59
+
60
+ @server.resource(uri="request://headers")
61
+ async def get_headers() -> dict[str, str]:
62
+ request = get_http_request()
63
+
64
+ return dict(request.headers)
65
+
66
+ # Add a prompt
67
+ @server.prompt
68
+ def welcome(name: str) -> str:
69
+ """Example greeting prompt."""
70
+ return f"Welcome to FastMCP, {name}!"
71
+
72
+ return server
73
+
74
+
75
+ def run_server(host: str, port: int, **kwargs) -> None:
76
+ fastmcp_server().run(host=host, port=port, **kwargs)
77
+
78
+
79
+ def run_nested_server(host: str, port: int) -> None:
80
+ mcp_app = fastmcp_server().http_app(path="/final/mcp")
81
+
82
+ mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
83
+ mount2 = Starlette(
84
+ routes=[Mount("/nest-outer", app=mount)],
85
+ lifespan=mcp_app.lifespan,
86
+ )
87
+ server = uvicorn.Server(
88
+ config=uvicorn.Config(
89
+ app=mount2,
90
+ host=host,
91
+ port=port,
92
+ log_level="error",
93
+ lifespan="on",
94
+ )
95
+ )
96
+ server.run()
97
+
98
+
99
+ @pytest.fixture()
100
+ async def streamable_http_server() -> AsyncGenerator[str, None]:
101
+ with run_server_in_process(run_server, transport="streamable-http") as url:
102
+ async with Client(transport=StreamableHttpTransport(f"{url}/mcp")) as client:
103
+ assert await client.ping()
104
+ yield f"{url}/mcp"
105
+
106
+
107
+ PROGRESS_MESSAGES = []
108
+
109
+ async def progress_handler(
110
+ progress: float, total: float | None, message: str | None
111
+ ) -> None:
112
+ PROGRESS_MESSAGES.append(dict(progress=progress, total=total, message=message))
113
+
114
+
115
+ async def test_ping(streamable_http_server: str):
116
+ """Test pinging the server."""
117
+ async with Client(
118
+ transport=StreamableHttpTransport(streamable_http_server)
119
+ ) as client:
120
+ result = await client.ping()
121
+ assert result is True
122
+
123
+
124
+ async def test_http_headers(streamable_http_server: str):
125
+ """Test getting HTTP headers from the server."""
126
+ async with Client(
127
+ transport=StreamableHttpTransport(
128
+ streamable_http_server, headers={"X-DEMO-HEADER": "ABC"}
129
+ )
130
+ ) as client:
131
+ raw_result = await client.read_resource("request://headers")
132
+ json_result = json.loads(raw_result[0].text) # type: ignore[attr-defined]
133
+ assert "x-demo-header" in json_result
134
+ assert json_result["x-demo-header"] == "ABC"
135
+
136
+
137
+ async def test_greet_with_progress_tool(streamable_http_server: str):
138
+ """Test calling the greet tool."""
139
+ async with Client(
140
+ transport=StreamableHttpTransport(streamable_http_server), progress_handler=progress_handler
141
+ ) as client:
142
+ result = await client.call_tool("greet_with_progress", {"name": "Alice"})
143
+
144
+ assert isinstance(result, list)
145
+ assert isinstance(result[0], TextContent)
146
+ assert result[0].text == "Hello, Alice!"
147
+
148
+ assert PROGRESS_MESSAGES == [
149
+ dict(progress=0.5, total=1.0, message="Greeting in progress"),
150
+ ]
151
+
152
+
153
+ async def test_nested_streamable_http_server_resolves_correctly():
154
+ # tests patch for
155
+ # https://github.com/modelcontextprotocol/python-sdk/pull/659
156
+
157
+ with run_server_in_process(run_nested_server) as url:
158
+ async with Client(
159
+ transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp")
160
+ ) as client:
161
+ result = await client.ping()
162
+ assert result is True
163
+
164
+
165
+ @pytest.mark.skipif(
166
+ sys.platform == "win32",
167
+ reason="Timeout tests are flaky on Windows. Timeouts *are* supported but the tests are unreliable.",
168
+ )
169
+ class TestTimeout:
170
+ async def test_timeout(self, streamable_http_server: str):
171
+ # note this transport behaves differently than others and raises
172
+ # McpError from the *client* context
173
+ with pytest.raises(McpError, match="Timed out"):
174
+ async with Client(
175
+ transport=StreamableHttpTransport(streamable_http_server),
176
+ timeout=0.1,
177
+ ) as client:
178
+ await client.call_tool("sleep", {"seconds": 0.2})
179
+
180
+ async def test_timeout_tool_call(self, streamable_http_server: str):
181
+ async with Client(
182
+ transport=StreamableHttpTransport(streamable_http_server),
183
+ ) as client:
184
+ with pytest.raises(McpError):
185
+ await client.call_tool("sleep", {"seconds": 0.2}, timeout=0.1)
186
+
187
+ async def test_timeout_tool_call_overrides_client_timeout(
188
+ self, streamable_http_server: str
189
+ ):
190
+ async with Client(
191
+ transport=StreamableHttpTransport(streamable_http_server),
192
+ timeout=2,
193
+ ) as client:
194
+ with pytest.raises(McpError):
195
+ await client.call_tool("sleep", {"seconds": 0.2}, timeout=0.1)