Skip to content

Commit ef3c201

Browse files
matthicksjclaude
andcommitted
style: fix pre-commit formatting issues
- Remove trailing whitespace - Consolidate multi-line Tool() constructors - Remove unused imports from anyio.streams.memory - Add type annotations for test interceptor functions - Apply ruff formatting rules 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 009fa8b commit ef3c201

File tree

2 files changed

+58
-73
lines changed

2 files changed

+58
-73
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ async def handler(req: types.UnsubscribeRequest):
385385
def list_tools(self):
386386
def decorator(func: Callable[[], Awaitable[list[types.Tool]]]):
387387
logger.debug("Registering handler for ListToolsRequest")
388-
388+
389389
# Store direct reference to the function for cache refresh.
390390
# This avoids nested handler invocation which can disrupt
391391
# async execution flow in streaming contexts.

tests/server/test_tool_cache_refresh_bug.py

Lines changed: 57 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
which can disrupt async execution in streaming contexts.
55
"""
66

7-
from typing import Any
7+
from typing import Any, cast
88

99
import anyio
1010
import pytest
@@ -17,192 +17,177 @@
1717
@pytest.mark.anyio
1818
async def test_no_nested_handler_invocation_on_cache_refresh():
1919
"""Verify that cache refresh doesn't use nested handler invocation.
20-
20+
2121
Issue #1298: Tool handlers can fail when cache refresh triggers
2222
nested handler invocation via self.request_handlers[ListToolsRequest](None),
2323
which disrupts async execution flow in streaming contexts.
24-
24+
2525
This test verifies the fix by detecting whether nested handler
2626
invocation occurs during cache refresh.
2727
"""
2828
server = Server("test-server")
29-
29+
3030
# Track handler invocations
3131
handler_invocations = []
32-
32+
3333
@server.list_tools()
3434
async def list_tools():
3535
# Normal tool listing
3636
await anyio.sleep(0.001)
37-
return [
38-
Tool(
39-
name="test_tool",
40-
description="Test tool",
41-
inputSchema={"type": "object", "properties": {}}
42-
)
43-
]
44-
37+
return [Tool(name="test_tool", description="Test tool", inputSchema={"type": "object", "properties": {}})]
38+
4539
@server.call_tool()
4640
async def call_tool(name: str, arguments: dict[str, Any]):
4741
# Simple tool implementation
4842
return [TextContent(type="text", text="Tool result")]
49-
43+
5044
# Intercept the ListToolsRequest handler to detect nested invocation
5145
original_handler = None
52-
46+
5347
def setup_handler_interceptor():
5448
nonlocal original_handler
5549
original_handler = server.request_handlers.get(ListToolsRequest)
56-
57-
async def interceptor(req):
50+
51+
async def interceptor(req: Any) -> Any:
5852
# Track the invocation
5953
# req is None for nested invocations (the problematic pattern)
6054
# req is a proper request object for normal invocations
6155
if req is None:
6256
handler_invocations.append("nested")
6357
else:
6458
handler_invocations.append("normal")
65-
59+
6660
# Call the original handler
6761
if original_handler:
6862
return await original_handler(req)
6963
return None
70-
71-
server.request_handlers[ListToolsRequest] = interceptor
72-
64+
65+
server.request_handlers[ListToolsRequest] = cast(Any, interceptor)
66+
7367
# Set up the interceptor after decorators have run
7468
setup_handler_interceptor()
75-
69+
7670
# Setup communication channels
77-
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
7871
from mcp.shared.message import SessionMessage
79-
72+
8073
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)
8174
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)
82-
75+
8376
async def run_server():
84-
await server.run(
85-
client_to_server_receive,
86-
server_to_client_send,
87-
server.create_initialization_options()
88-
)
89-
77+
await server.run(client_to_server_receive, server_to_client_send, server.create_initialization_options())
78+
9079
async with anyio.create_task_group() as tg:
9180
tg.start_soon(run_server)
92-
81+
9382
async with ClientSession(server_to_client_receive, client_to_server_send) as session:
9483
await session.initialize()
95-
84+
9685
# Clear the cache to force a refresh on next tool call
9786
server._tool_cache.clear()
98-
87+
9988
# Make a tool call - this should trigger cache refresh
10089
result = await session.call_tool("test_tool", {})
101-
90+
10291
# Verify the tool call succeeded
10392
assert result is not None
10493
assert not result.isError
105-
assert result.content[0].text == "Tool result"
106-
94+
content = result.content[0]
95+
assert isinstance(content, TextContent)
96+
assert content.text == "Tool result"
97+
10798
# Check if nested handler invocation occurred
10899
has_nested_invocation = "nested" in handler_invocations
109-
100+
110101
# The bug is present if nested handler invocation occurs
111102
assert not has_nested_invocation, (
112103
"Nested handler invocation detected during cache refresh. "
113104
"This pattern (calling request_handlers[ListToolsRequest](None)) "
114105
"can disrupt async execution in streaming contexts (issue #1298)."
115106
)
116-
107+
117108
tg.cancel_scope.cancel()
118109

119110

120111
@pytest.mark.anyio
121112
async def test_concurrent_cache_refresh_safety():
122113
"""Verify that concurrent tool calls with cache refresh work correctly.
123-
114+
124115
Multiple concurrent tool calls that all trigger cache refresh should
125116
not cause issues or result in nested handler invocations.
126117
"""
127118
server = Server("test-server")
128-
119+
129120
# Track concurrent handler invocations
130121
nested_invocations = 0
131-
122+
132123
@server.list_tools()
133124
async def list_tools():
134125
await anyio.sleep(0.01) # Simulate some async work
135126
return [
136-
Tool(
137-
name=f"tool_{i}",
138-
description=f"Tool {i}",
139-
inputSchema={"type": "object", "properties": {}}
140-
)
127+
Tool(name=f"tool_{i}", description=f"Tool {i}", inputSchema={"type": "object", "properties": {}})
141128
for i in range(3)
142129
]
143-
130+
144131
@server.call_tool()
145132
async def call_tool(name: str, arguments: dict[str, Any]):
146133
await anyio.sleep(0.001)
147134
return [TextContent(type="text", text=f"Result from {name}")]
148-
135+
149136
# Intercept handler to detect nested invocations
150137
original_handler = server.request_handlers.get(ListToolsRequest)
151-
152-
async def interceptor(req):
138+
139+
async def interceptor(req: Any) -> Any:
153140
nonlocal nested_invocations
154141
if req is None:
155142
nested_invocations += 1
156143
if original_handler:
157144
return await original_handler(req)
158145
return None
159-
146+
160147
if original_handler:
161-
server.request_handlers[ListToolsRequest] = interceptor
162-
148+
server.request_handlers[ListToolsRequest] = cast(Any, interceptor)
149+
163150
# Setup communication
164-
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
165151
from mcp.shared.message import SessionMessage
166-
152+
167153
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)
168154
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)
169-
155+
170156
async def run_server():
171-
await server.run(
172-
client_to_server_receive,
173-
server_to_client_send,
174-
server.create_initialization_options()
175-
)
176-
157+
await server.run(client_to_server_receive, server_to_client_send, server.create_initialization_options())
158+
177159
async with anyio.create_task_group() as tg:
178160
tg.start_soon(run_server)
179-
161+
180162
async with ClientSession(server_to_client_receive, client_to_server_send) as session:
181163
await session.initialize()
182-
164+
183165
# Clear cache to force refresh
184166
server._tool_cache.clear()
185-
167+
186168
# Make concurrent tool calls
187169
import asyncio
170+
188171
results = await asyncio.gather(
189172
session.call_tool("tool_0", {}),
190173
session.call_tool("tool_1", {}),
191174
session.call_tool("tool_2", {}),
192-
return_exceptions=True
175+
return_exceptions=True,
193176
)
194-
177+
195178
# Verify all calls succeeded
196179
for i, result in enumerate(results):
197180
assert not isinstance(result, Exception), f"Tool {i} failed: {result}"
198181
assert not result.isError
199-
assert f"tool_{i}" in result.content[0].text
200-
182+
content = result.content[0]
183+
assert isinstance(content, TextContent)
184+
assert f"tool_{i}" in content.text
185+
201186
# Verify no nested invocations occurred
202187
assert nested_invocations == 0, (
203188
f"Detected {nested_invocations} nested handler invocations "
204189
"during concurrent cache refresh. This indicates the bug from "
205190
"issue #1298 is present."
206191
)
207-
208-
tg.cancel_scope.cancel()
192+
193+
tg.cancel_scope.cancel()

0 commit comments

Comments
 (0)