diff --git a/tests/tools/test_mcp_loop_windows_selector.py b/tests/tools/test_mcp_loop_windows_selector.py new file mode 100644 index 000000000000..19f423a65225 --- /dev/null +++ b/tests/tools/test_mcp_loop_windows_selector.py @@ -0,0 +1,25 @@ +"""Regression: the MCP event loop must not be a ProactorEventLoop on Windows. + +The MCP stdio client spawns servers as subprocesses and reads their stdout +pipe. Windows' ProactorEventLoop hangs those subprocess-pipe reads during the +`initialize` handshake, so discovery times out and no MCP tools register. +`_ensure_mcp_loop` therefore builds an explicit SelectorEventLoop on win32, +matching the win32 SelectorEventLoop handling in cli.py and web_server.py. +""" +import asyncio +import sys + + +def test_mcp_loop_is_selector_on_windows(): + import tools.mcp_tool as mcp_tool + + mcp_tool._ensure_mcp_loop() + try: + loop = mcp_tool._mcp_loop + assert loop is not None + if sys.platform == "win32": + proactor = getattr(asyncio, "ProactorEventLoop", None) + assert proactor is None or not isinstance(loop, proactor) + assert isinstance(loop, asyncio.SelectorEventLoop) + finally: + mcp_tool._stop_mcp_loop() diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index cd999f2712f7..19523e9b0062 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -3570,7 +3570,17 @@ def _ensure_mcp_loop(): with _lock: if _mcp_loop is not None and _mcp_loop.is_running(): return - _mcp_loop = asyncio.new_event_loop() + # On Windows the default event loop is the ProactorEventLoop, whose + # subprocess-pipe reads hang the MCP stdio_client handshake (the + # `initialize` request is sent but its response is never surfaced, + # so discovery times out and no MCP tools register). The + # SelectorEventLoop reads stdio subprocess pipes correctly. Use it + # explicitly for the dedicated MCP loop thread on Windows, matching + # the win32 SelectorEventLoop handling in cli.py and web_server.py. + if sys.platform == "win32": + _mcp_loop = asyncio.SelectorEventLoop() + else: + _mcp_loop = asyncio.new_event_loop() _mcp_loop.set_exception_handler(_mcp_loop_exception_handler) _mcp_thread = threading.Thread( target=_mcp_loop.run_forever,