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
This commit is contained in:
chengyongru 2026-05-06 13:28:48 +08:00 committed by Xubin Ren
parent 99209a806d
commit 4fad19dc17

View File

@ -436,8 +436,8 @@ async def connect_mcp_servers(
"""Connect to configured MCP servers and register their tools, resources, prompts. """Connect to configured MCP servers and register their tools, resources, prompts.
Returns a dict mapping server name -> its dedicated AsyncExitStack. Returns a dict mapping server name -> its dedicated AsyncExitStack.
Each server gets its own stack and runs in its own task to prevent Each server gets its own stack to prevent cancel scope conflicts
cancel scope conflicts when multiple MCP servers are configured. when multiple MCP servers are configured.
""" """
from mcp import ClientSession, StdioServerParameters from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client from mcp.client.sse import sse_client
@ -612,19 +612,13 @@ async def connect_mcp_servers(
server_stacks: dict[str, AsyncExitStack] = {} server_stacks: dict[str, AsyncExitStack] = {}
tasks: list[asyncio.Task] = []
for name, cfg in mcp_servers.items(): for name, cfg in mcp_servers.items():
task = asyncio.create_task(connect_single_server(name, cfg)) try:
tasks.append(task) result = await connect_single_server(name, cfg)
except Exception as e:
results = await asyncio.gather(*tasks, return_exceptions=True) logger.error("MCP server '{}' connection failed: {}", name, e)
continue
for i, result in enumerate(results): if result is not None and result[1] is not None:
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:
server_stacks[result[0]] = result[1] server_stacks[result[0]] = result[1]
return server_stacks return server_stacks