Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions tests/tools/test_mcp_loop_windows_selector.py
Original file line number Diff line number Diff line change
@@ -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()
12 changes: 11 additions & 1 deletion tools/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down