Skip to content

Commit fc775e9

Browse files
Fix Windows subprocess NotImplementedError for STDIO clients
On Windows, asyncio.create_subprocess_exec raises NotImplementedError due to incomplete subprocess transport support in ProactorEventLoop. This commit implements a fallback using subprocess.Popen directly with async wrappers. The fix introduces a DummyProcess class that wraps subprocess.Popen and provides async-compatible stdin/stdout streams using anyio's FileReadStream and FileWriteStream. This allows MCP STDIO clients to work on Windows without encountering the NotImplementedError. Based on PR #596 by @theailanguage Github-Issue: #596 Reported-by: theailanguage
1 parent 679b229 commit fc775e9

File tree

1 file changed

+81
-22
lines changed

1 file changed

+81
-22
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import subprocess
77
import sys
88
from pathlib import Path
9-
from typing import TextIO
9+
from typing import BinaryIO, TextIO, cast
1010

1111
import anyio
12+
from anyio import to_thread
1213
from anyio.abc import Process
14+
from anyio.streams.file import FileReadStream, FileWriteStream
1315

1416

1517
def get_windows_executable_command(command: str) -> str:
@@ -44,46 +46,103 @@ def get_windows_executable_command(command: str) -> str:
4446
return command
4547

4648

49+
class DummyProcess:
50+
"""
51+
A fallback process wrapper for Windows to handle async I/O
52+
when using subprocess.Popen, which provides sync-only FileIO objects.
53+
54+
This wraps stdin and stdout into async-compatible
55+
streams (FileReadStream, FileWriteStream),
56+
so that MCP clients expecting async streams can work properly.
57+
"""
58+
59+
def __init__(self, popen_obj: subprocess.Popen[bytes]):
60+
self.popen: subprocess.Popen[bytes] = popen_obj
61+
self.stdin_raw = popen_obj.stdin # type: ignore[assignment]
62+
self.stdout_raw = popen_obj.stdout # type: ignore[assignment]
63+
self.stderr = popen_obj.stderr # type: ignore[assignment]
64+
65+
self.stdin = FileWriteStream(cast(BinaryIO, self.stdin_raw)) if self.stdin_raw else None
66+
self.stdout = FileReadStream(cast(BinaryIO, self.stdout_raw)) if self.stdout_raw else None
67+
68+
async def __aenter__(self):
69+
"""Support async context manager entry."""
70+
return self
71+
72+
async def __aexit__(
73+
self,
74+
exc_type: BaseException | None,
75+
exc_val: BaseException | None,
76+
exc_tb: object | None,
77+
) -> None:
78+
"""Terminate and wait on process exit inside a thread."""
79+
self.popen.terminate()
80+
await to_thread.run_sync(self.popen.wait)
81+
82+
async def wait(self):
83+
"""Async wait for process completion."""
84+
return await to_thread.run_sync(self.popen.wait)
85+
86+
def terminate(self):
87+
"""Terminate the subprocess immediately."""
88+
return self.popen.terminate()
89+
90+
91+
# ------------------------
92+
# Updated function
93+
# ------------------------
94+
95+
4796
async def create_windows_process(
4897
command: str,
4998
args: list[str],
5099
env: dict[str, str] | None = None,
51-
errlog: TextIO = sys.stderr,
100+
errlog: TextIO | None = sys.stderr,
52101
cwd: Path | str | None = None,
53-
):
102+
) -> DummyProcess:
54103
"""
55104
Creates a subprocess in a Windows-compatible way.
56105
57-
Windows processes need special handling for console windows and
58-
process creation flags.
106+
On Windows, asyncio.create_subprocess_exec has incomplete support
107+
(NotImplementedError when trying to open subprocesses).
108+
Therefore, we fallback to subprocess.Popen and wrap it for async usage.
59109
60110
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
111+
command (str): The executable to run
112+
args (list[str]): List of command line arguments
113+
env (dict[str, str] | None): Environment variables
114+
errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr)
115+
cwd (Path | str | None): Working directory for the subprocess
66116
67117
Returns:
68-
A process handle
118+
DummyProcess: Async-compatible subprocess with stdin and stdout streams
69119
"""
70120
try:
71-
# Try with Windows-specific flags to hide console window
72-
process = await anyio.open_process(
121+
# Try launching with creationflags to avoid opening a new console window
122+
popen_obj = subprocess.Popen(
73123
[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,
124+
stdin=subprocess.PIPE,
125+
stdout=subprocess.PIPE,
79126
stderr=errlog,
127+
env=env,
80128
cwd=cwd,
129+
bufsize=0, # Unbuffered output
130+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
81131
)
82-
return process
132+
return DummyProcess(popen_obj)
133+
83134
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
135+
# If creationflags failed, fallback without them
136+
popen_obj = subprocess.Popen(
137+
[command, *args],
138+
stdin=subprocess.PIPE,
139+
stdout=subprocess.PIPE,
140+
stderr=errlog,
141+
env=env,
142+
cwd=cwd,
143+
bufsize=0,
144+
)
145+
return DummyProcess(popen_obj)
87146

88147

89148
async def terminate_windows_process(process: Process):

0 commit comments

Comments
 (0)