|
3 | 3 | from pydantic import AnyUrl |
4 | 4 |
|
5 | 5 | from mcp.server.fastmcp import FastMCP |
6 | | -from mcp.shared.memory import ( |
7 | | - create_connected_server_and_client_session as create_session, |
8 | | -) |
9 | | - |
10 | | -_sleep_time_seconds = 0.01 |
11 | | -_resource_name = "slow://slow_resource" |
| 6 | +from mcp.shared.memory import create_connected_server_and_client_session as create_session |
12 | 7 |
|
13 | 8 |
|
14 | 9 | @pytest.mark.anyio |
15 | | -async def test_messages_are_executed_concurrently(): |
| 10 | +async def test_messages_are_executed_concurrently_tools(): |
16 | 11 | server = FastMCP("test") |
17 | | - call_timestamps = [] |
| 12 | + event = anyio.Event() |
| 13 | + tool_started = anyio.Event() |
| 14 | + call_order = [] |
18 | 15 |
|
19 | 16 | @server.tool("sleep") |
20 | 17 | async def sleep_tool(): |
21 | | - call_timestamps.append(("tool_start_time", anyio.current_time())) |
22 | | - await anyio.sleep(_sleep_time_seconds) |
23 | | - call_timestamps.append(("tool_end_time", anyio.current_time())) |
| 18 | + call_order.append("waiting_for_event") |
| 19 | + tool_started.set() |
| 20 | + await event.wait() |
| 21 | + call_order.append("tool_end") |
24 | 22 | return "done" |
25 | 23 |
|
26 | | - @server.resource(_resource_name) |
27 | | - async def slow_resource(): |
28 | | - call_timestamps.append(("resource_start_time", anyio.current_time())) |
29 | | - await anyio.sleep(_sleep_time_seconds) |
30 | | - call_timestamps.append(("resource_end_time", anyio.current_time())) |
| 24 | + @server.tool("trigger") |
| 25 | + async def trigger(): |
| 26 | + # Wait for tool to start before setting the event |
| 27 | + await tool_started.wait() |
| 28 | + call_order.append("trigger_started") |
| 29 | + event.set() |
| 30 | + call_order.append("trigger_end") |
31 | 31 | return "slow" |
32 | 32 |
|
33 | 33 | async with create_session(server._mcp_server) as client_session: |
| 34 | + # First tool will wait on event, second will set it |
34 | 35 | async with anyio.create_task_group() as tg: |
35 | | - for _ in range(10): |
36 | | - tg.start_soon(client_session.call_tool, "sleep") |
37 | | - tg.start_soon(client_session.read_resource, AnyUrl(_resource_name)) |
| 36 | + # Start the tool first (it will wait on event) |
| 37 | + tg.start_soon(client_session.call_tool, "sleep") |
| 38 | + # Then the trigger tool will set the event to allow the first tool to continue |
| 39 | + await client_session.call_tool("trigger") |
38 | 40 |
|
39 | | - active_calls = 0 |
40 | | - max_concurrent_calls = 0 |
41 | | - for call_type, _ in sorted(call_timestamps, key=lambda x: x[1]): |
42 | | - if "start" in call_type: |
43 | | - active_calls += 1 |
44 | | - max_concurrent_calls = max(max_concurrent_calls, active_calls) |
45 | | - else: |
46 | | - active_calls -= 1 |
47 | | - print(f"Max concurrent calls: {max_concurrent_calls}") |
48 | | - assert max_concurrent_calls > 1, "No concurrent calls were executed" |
| 41 | + # Verify that both ran concurrently |
| 42 | + assert call_order == [ |
| 43 | + "waiting_for_event", |
| 44 | + "trigger_started", |
| 45 | + "trigger_end", |
| 46 | + "tool_end", |
| 47 | + ], f"Expected concurrent execution, but got: {call_order}" |
49 | 48 |
|
50 | 49 |
|
51 | | -def main(): |
52 | | - anyio.run(test_messages_are_executed_concurrently) |
| 50 | +@pytest.mark.anyio |
| 51 | +async def test_messages_are_executed_concurrently_tools_and_resources(): |
| 52 | + server = FastMCP("test") |
| 53 | + event = anyio.Event() |
| 54 | + tool_started = anyio.Event() |
| 55 | + call_order = [] |
53 | 56 |
|
| 57 | + @server.tool("sleep") |
| 58 | + async def sleep_tool(): |
| 59 | + call_order.append("waiting_for_event") |
| 60 | + tool_started.set() |
| 61 | + await event.wait() |
| 62 | + call_order.append("tool_end") |
| 63 | + return "done" |
54 | 64 |
|
55 | | -if __name__ == "__main__": |
56 | | - import logging |
| 65 | + @server.resource("slow://slow_resource") |
| 66 | + async def slow_resource(): |
| 67 | + # Wait for tool to start before setting the event |
| 68 | + await tool_started.wait() |
| 69 | + event.set() |
| 70 | + call_order.append("resource_end") |
| 71 | + return "slow" |
57 | 72 |
|
58 | | - logging.basicConfig(level=logging.DEBUG) |
| 73 | + async with create_session(server._mcp_server) as client_session: |
| 74 | + # First tool will wait on event, second will set it |
| 75 | + async with anyio.create_task_group() as tg: |
| 76 | + # Start the tool first (it will wait on event) |
| 77 | + tg.start_soon(client_session.call_tool, "sleep") |
| 78 | + # Then the resource (it will set the event) |
| 79 | + tg.start_soon(client_session.read_resource, AnyUrl("slow://slow_resource")) |
59 | 80 |
|
60 | | - main() |
| 81 | + # Verify that both ran concurrently |
| 82 | + assert call_order == [ |
| 83 | + "waiting_for_event", |
| 84 | + "resource_end", |
| 85 | + "tool_end", |
| 86 | + ], f"Expected concurrent execution, but got: {call_order}" |
0 commit comments