diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index fd19eefa47..a10c7f4361 100644 --- a/tests/tools/test_mcp_tool.py +++ b/tests/tools/test_mcp_tool.py @@ -547,6 +547,43 @@ class TestRunOnMCPLoopInterrupts: mcp_mod._mcp_loop = old_loop mcp_mod._mcp_thread = old_thread + def test_timeout_reports_elapsed_and_configured_timeout(self): + import tools.mcp_tool as mcp_mod + + loop = asyncio.new_event_loop() + thread = threading.Thread(target=loop.run_forever, daemon=True) + thread.start() + + cancelled = threading.Event() + + async def _slow_call(): + try: + await asyncio.sleep(5) + return "done" + except asyncio.CancelledError: + cancelled.set() + raise + + old_loop = mcp_mod._mcp_loop + old_thread = mcp_mod._mcp_thread + mcp_mod._mcp_loop = loop + mcp_mod._mcp_thread = thread + + try: + with pytest.raises(TimeoutError, match=r"MCP call timed out after .*configured timeout: 0.2s"): + mcp_mod._run_on_mcp_loop(_slow_call(), timeout=0.2) + + deadline = time.time() + 2 + while time.time() < deadline and not cancelled.is_set(): + time.sleep(0.05) + assert cancelled.is_set() + finally: + loop.call_soon_threadsafe(loop.stop) + thread.join(timeout=2) + loop.close() + mcp_mod._mcp_loop = old_loop + mcp_mod._mcp_thread = old_thread + # --------------------------------------------------------------------------- # Tool registration (discovery + register) diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index d5c6fc6a45..b2e0ae802c 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -1942,7 +1942,8 @@ def _run_on_mcp_loop(coro, timeout: float = 30): if loop is None or not loop.is_running(): raise RuntimeError("MCP event loop is not running") future = asyncio.run_coroutine_threadsafe(coro, loop) - deadline = None if timeout is None else time.monotonic() + timeout + start_time = time.monotonic() + deadline = None if timeout is None else start_time + timeout while True: if is_interrupted(): @@ -1953,7 +1954,12 @@ def _run_on_mcp_loop(coro, timeout: float = 30): if deadline is not None: remaining = deadline - time.monotonic() if remaining <= 0: - return future.result(timeout=0) + future.cancel() + elapsed = time.monotonic() - start_time + raise TimeoutError( + f"MCP call timed out after {elapsed:.1f}s " + f"(configured timeout: {float(timeout):.1f}s)" + ) wait_timeout = min(wait_timeout, remaining) try: