Skip to content

Commit 9f5eecd

Browse files
committed
Fix Windows subprocess compatibility for STDIO mode with async streams
1 parent 679b229 commit 9f5eecd

File tree

1 file changed

+75
-24
lines changed

1 file changed

+75
-24
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
import anyio
1212
from anyio.abc import Process
13+
from anyio.streams.file import FileReadStream, FileWriteStream
14+
15+
from typing import Optional, TextIO, Union
16+
from pathlib import Path
1317

1418

1519
def get_windows_executable_command(command: str) -> str:
@@ -43,48 +47,95 @@ def get_windows_executable_command(command: str) -> str:
4347
# (permissions, broken symlinks, etc.)
4448
return command
4549

50+
class DummyProcess:
51+
"""
52+
A fallback process wrapper for Windows to handle async I/O
53+
when using subprocess.Popen, which provides sync-only FileIO objects.
54+
55+
This wraps stdin and stdout into async-compatible streams (FileReadStream, FileWriteStream),
56+
so that MCP clients expecting async streams can work properly.
57+
"""
58+
def __init__(self, popen_obj: subprocess.Popen):
59+
self.popen = popen_obj
60+
self.stdin_raw = popen_obj.stdin
61+
self.stdout_raw = popen_obj.stdout
62+
self.stderr = popen_obj.stderr
63+
64+
# Wrap into async-compatible AnyIO streams
65+
self.stdin = FileWriteStream(self.stdin_raw)
66+
self.stdout = FileReadStream(self.stdout_raw)
67+
68+
async def __aenter__(self):
69+
"""Support async context manager entry."""
70+
return self
71+
72+
async def __aexit__(self, exc_type, exc_val, exc_tb):
73+
"""Terminate and wait on process exit inside a thread."""
74+
self.popen.terminate()
75+
await anyio.to_thread.run_sync(self.popen.wait)
76+
77+
async def wait(self):
78+
"""Async wait for process completion."""
79+
return await anyio.to_thread.run_sync(self.popen.wait)
80+
81+
def terminate(self):
82+
"""Terminate the subprocess immediately."""
83+
return self.popen.terminate()
84+
85+
# ------------------------
86+
# Updated function
87+
# ------------------------
4688

4789
async def create_windows_process(
4890
command: str,
4991
args: list[str],
50-
env: dict[str, str] | None = None,
51-
errlog: TextIO = sys.stderr,
52-
cwd: Path | str | None = None,
92+
env: Optional[dict[str, str]] = None,
93+
errlog: Optional[TextIO] = sys.stderr,
94+
cwd: Union[Path, str, None] = None,
5395
):
5496
"""
5597
Creates a subprocess in a Windows-compatible way.
56-
57-
Windows processes need special handling for console windows and
58-
process creation flags.
98+
99+
On Windows, asyncio.create_subprocess_exec has incomplete support
100+
(NotImplementedError when trying to open subprocesses).
101+
Therefore, we fallback to subprocess.Popen and wrap it for async usage.
59102
60103
Args:
61-
command: The command to execute
62-
args: Command line arguments
63-
env: Environment variables
64-
errlog: Where to send stderr output
65-
cwd: Working directory for the process
104+
command (str): The executable to run
105+
args (list[str]): List of command line arguments
106+
env (dict[str, str] | None): Environment variables
107+
errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr)
108+
cwd (Path | str | None): Working directory for the subprocess
66109
67110
Returns:
68-
A process handle
111+
DummyProcess: Async-compatible subprocess with stdin and stdout streams
69112
"""
70113
try:
71-
# Try with Windows-specific flags to hide console window
72-
process = await anyio.open_process(
114+
# Try launching with creationflags to avoid opening a new console window
115+
popen_obj = subprocess.Popen(
73116
[command, *args],
74-
env=env,
75-
# Ensure we don't create console windows for each process
76-
creationflags=subprocess.CREATE_NO_WINDOW # type: ignore
77-
if hasattr(subprocess, "CREATE_NO_WINDOW")
78-
else 0,
117+
stdin=subprocess.PIPE,
118+
stdout=subprocess.PIPE,
79119
stderr=errlog,
120+
env=env,
80121
cwd=cwd,
122+
bufsize=0, # Unbuffered output
123+
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
81124
)
82-
return process
83-
except Exception:
84-
# Don't raise, let's try to create the process without creation flags
85-
process = await anyio.open_process([command, *args], env=env, stderr=errlog, cwd=cwd)
86-
return process
125+
return DummyProcess(popen_obj)
87126

127+
except Exception:
128+
# If creationflags failed, fallback without them
129+
popen_obj = subprocess.Popen(
130+
[command, *args],
131+
stdin=subprocess.PIPE,
132+
stdout=subprocess.PIPE,
133+
stderr=errlog,
134+
env=env,
135+
cwd=cwd,
136+
bufsize=0,
137+
)
138+
return DummyProcess(popen_obj)
88139

89140
async def terminate_windows_process(process: Process):
90141
"""

0 commit comments

Comments
 (0)