From 4fad19dc174768938e53e4bd7b2ea8c4d1a27f8b Mon Sep 17 00:00:00 2001 From: chengyongru Date: Wed, 6 May 2026 13:28:48 +0800 Subject: [PATCH] fix: use sequential MCP server connections to prevent CPU spin asyncio.create_task in connect_mcp_servers creates child tasks for each MCP server, but close_mcp calls stack.aclose() from the main task. anyio CancelScope requires enter/exit in the same task, so the cross-task exit raises RuntimeError which gets silently caught. The orphaned cancel scope keeps retrying via call_soon on every event loop tick, consuming 100% CPU. Fix: remove create_task/gather and connect servers sequentially in the caller task. MCP servers are typically 1-2, so parallel connection provides negligible benefit while introducing the cancel scope hazard. Closes #3638 --- nanobot/agent/tools/mcp.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/nanobot/agent/tools/mcp.py b/nanobot/agent/tools/mcp.py index 04b88386f..6d4e7d6cd 100644 --- a/nanobot/agent/tools/mcp.py +++ b/nanobot/agent/tools/mcp.py @@ -436,8 +436,8 @@ async def connect_mcp_servers( """Connect to configured MCP servers and register their tools, resources, prompts. Returns a dict mapping server name -> its dedicated AsyncExitStack. - Each server gets its own stack and runs in its own task to prevent - cancel scope conflicts when multiple MCP servers are configured. + Each server gets its own stack to prevent cancel scope conflicts + when multiple MCP servers are configured. """ from mcp import ClientSession, StdioServerParameters from mcp.client.sse import sse_client @@ -612,19 +612,13 @@ async def connect_mcp_servers( server_stacks: dict[str, AsyncExitStack] = {} - tasks: list[asyncio.Task] = [] for name, cfg in mcp_servers.items(): - task = asyncio.create_task(connect_single_server(name, cfg)) - tasks.append(task) - - results = await asyncio.gather(*tasks, return_exceptions=True) - - for i, result in enumerate(results): - name = list(mcp_servers.keys())[i] - if isinstance(result, BaseException): - if not isinstance(result, asyncio.CancelledError): - logger.error("MCP server '{}' connection task failed: {}", name, result) - elif result is not None and result[1] is not None: + try: + result = await connect_single_server(name, cfg) + except Exception as e: + logger.error("MCP server '{}' connection failed: {}", name, e) + continue + if result is not None and result[1] is not None: server_stacks[result[0]] = result[1] return server_stacks