Skip to content

Commit 04669d8

Browse files
authored
fix: Fix mcp timeout issue (#922)
1 parent 439653d commit 04669d8

File tree

2 files changed

+40
-1
lines changed

2 files changed

+40
-1
lines changed

src/strands/tools/mcp/mcp_client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from types import TracebackType
1919
from typing import Any, Callable, Coroutine, Dict, Optional, TypeVar, Union, cast
2020

21+
import anyio
2122
from mcp import ClientSession, ListToolsResult
2223
from mcp.types import CallToolResult as MCPCallToolResult
2324
from mcp.types import GetPromptResult, ListPromptsResult
@@ -378,6 +379,13 @@ def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolRes
378379

379380
return result
380381

382+
# Raise an exception if the underlying client raises an exception in a message
383+
# This happens when the underlying client has an http timeout error
384+
async def _handle_error_message(self, message: Exception | Any) -> None:
385+
if isinstance(message, Exception):
386+
raise message
387+
await anyio.lowlevel.checkpoint()
388+
381389
async def _async_background_thread(self) -> None:
382390
"""Asynchronous method that runs in the background thread to manage the MCP connection.
383391
@@ -388,7 +396,9 @@ async def _async_background_thread(self) -> None:
388396
try:
389397
async with self._transport_callable() as (read_stream, write_stream, *_):
390398
self._log_debug_with_thread("transport connection established")
391-
async with ClientSession(read_stream, write_stream) as session:
399+
async with ClientSession(
400+
read_stream, write_stream, message_handler=self._handle_error_message
401+
) as session:
392402
self._log_debug_with_thread("initializing MCP session")
393403
await session.initialize()
394404

tests_integ/mcp/test_mcp_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def start_comprehensive_mcp_server(transport: Literal["sse", "streamable-http"],
3131

3232
mcp = FastMCP("Comprehensive MCP Server", port=port)
3333

34+
@mcp.tool(description="Tool that will timeout")
35+
def timeout_tool() -> str:
36+
time.sleep(10)
37+
return "This tool has timed out"
38+
3439
@mcp.tool(description="Calculator tool which performs calculations")
3540
def calculator(x: int, y: int) -> int:
3641
return x + y
@@ -297,3 +302,27 @@ def slow_transport():
297302
with client:
298303
tools = client.list_tools_sync()
299304
assert len(tools) >= 0 # Should work now
305+
306+
307+
@pytest.mark.skipif(
308+
condition=os.environ.get("GITHUB_ACTIONS") == "true",
309+
reason="streamable transport is failing in GitHub actions, debugging if linux compatibility issue",
310+
)
311+
@pytest.mark.asyncio
312+
async def test_streamable_http_mcp_client_times_out_before_tool():
313+
"""Test an mcp server that timesout before the tool is able to respond."""
314+
server_thread = threading.Thread(
315+
target=start_comprehensive_mcp_server, kwargs={"transport": "streamable-http", "port": 8001}, daemon=True
316+
)
317+
server_thread.start()
318+
time.sleep(2) # wait for server to startup completely
319+
320+
def transport_callback() -> MCPTransport:
321+
return streamablehttp_client(sse_read_timeout=2, url="http://127.0.0.1:8001/mcp")
322+
323+
streamable_http_client = MCPClient(transport_callback)
324+
with streamable_http_client:
325+
# Test tools
326+
result = await streamable_http_client.call_tool_async(tool_use_id="123", name="timeout_tool")
327+
assert result["status"] == "error"
328+
assert result["content"][0]["text"] == "Tool execution failed: Connection closed"

0 commit comments

Comments
 (0)