Skip to content

Commit 7af9e65

Browse files
Try anyio.open_process first before Windows fallback
This re-establishes behavior before #596 in the default case. - Attempt to use anyio's native open_process function on Windows - Fall back to subprocess.Popen only if NotImplementedError is raised - This improves compatibility with event loops that support async subprocesses - Extract fallback logic into separate function for clarity
1 parent 2be6f09 commit 7af9e65

File tree

1 file changed

+46
-4
lines changed

1 file changed

+46
-4
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from pathlib import Path
99
from typing import BinaryIO, TextIO, cast
1010

11+
import anyio
1112
from anyio import to_thread
13+
from anyio.abc import Process
1214
from anyio.streams.file import FileReadStream, FileWriteStream
1315

1416

@@ -113,13 +115,14 @@ async def create_windows_process(
113115
env: dict[str, str] | None = None,
114116
errlog: TextIO | None = sys.stderr,
115117
cwd: Path | str | None = None,
116-
) -> FallbackProcess:
118+
) -> Process | FallbackProcess:
117119
"""
118120
Creates a subprocess in a Windows-compatible way.
119121
120-
On Windows, asyncio.create_subprocess_exec has incomplete support
121-
(NotImplementedError when trying to open subprocesses).
122-
Therefore, we fallback to subprocess.Popen and wrap it for async usage.
122+
Attempt to use anyio's open_process for async subprocess creation.
123+
In some cases this will throw NotImplementedError on Windows, e.g.
124+
when using the SelectorEventLoop which does not support async subprocesses.
125+
In that case, we fall back to using subprocess.Popen.
123126
124127
Args:
125128
command (str): The executable to run
@@ -131,6 +134,45 @@ async def create_windows_process(
131134
Returns:
132135
FallbackProcess: Async-compatible subprocess with stdin and stdout streams
133136
"""
137+
try:
138+
# First try using anyio with Windows-specific flags to hide console window
139+
process = await anyio.open_process(
140+
[command, *args],
141+
env=env,
142+
# Ensure we don't create console windows for each process
143+
creationflags=subprocess.CREATE_NO_WINDOW # type: ignore
144+
if hasattr(subprocess, "CREATE_NO_WINDOW")
145+
else 0,
146+
stderr=errlog,
147+
cwd=cwd,
148+
)
149+
return process
150+
except NotImplementedError:
151+
# Windows often doesn't support async subprocess creation, use fallback
152+
return await _create_windows_fallback_process(command, args, env, errlog, cwd)
153+
except Exception:
154+
# Try again without creation flags
155+
process = await anyio.open_process(
156+
[command, *args],
157+
env=env,
158+
stderr=errlog,
159+
cwd=cwd,
160+
)
161+
return process
162+
163+
164+
async def _create_windows_fallback_process(
165+
command: str,
166+
args: list[str],
167+
env: dict[str, str] | None = None,
168+
errlog: TextIO | None = sys.stderr,
169+
cwd: Path | str | None = None,
170+
) -> FallbackProcess:
171+
"""
172+
Create a subprocess using subprocess.Popen as a fallback when anyio fails.
173+
174+
This function wraps the sync subprocess.Popen in an async-compatible interface.
175+
"""
134176
try:
135177
# Try launching with creationflags to avoid opening a new console window
136178
popen_obj = subprocess.Popen(

0 commit comments

Comments
 (0)