Skip to content

Commit 306b28d

Browse files
committed
Fix: Windows stdio subprocess compatibility with type hints and fallback to subprocess.Popen
1 parent 9f5eecd commit 306b28d

File tree

1 file changed

+22
-21
lines changed

1 file changed

+22
-21
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@
66
import subprocess
77
import sys
88
from pathlib import Path
9-
from typing import TextIO
9+
from typing import IO, TextIO
1010

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

15-
from typing import Optional, TextIO, Union
16-
from pathlib import Path
17-
18-
1916
def get_windows_executable_command(command: str) -> str:
2017
"""
2118
Get the correct executable command normalized for Windows.
@@ -52,18 +49,18 @@ class DummyProcess:
5249
A fallback process wrapper for Windows to handle async I/O
5350
when using subprocess.Popen, which provides sync-only FileIO objects.
5451
55-
This wraps stdin and stdout into async-compatible streams (FileReadStream, FileWriteStream),
52+
This wraps stdin and stdout into async-compatible
53+
streams (FileReadStream, FileWriteStream),
5654
so that MCP clients expecting async streams can work properly.
5755
"""
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
56+
def __init__(self, popen_obj: subprocess.Popen[bytes]):
57+
self.popen: subprocess.Popen[bytes] = popen_obj
58+
self.stdin_raw: IO[bytes] | None = popen_obj.stdin
59+
self.stdout_raw: IO[bytes] | None = popen_obj.stdout
60+
self.stderr: IO[bytes] | None = popen_obj.stderr
6361

64-
# Wrap into async-compatible AnyIO streams
65-
self.stdin = FileWriteStream(self.stdin_raw)
66-
self.stdout = FileReadStream(self.stdout_raw)
62+
self.stdin = FileWriteStream(self.stdin_raw) if self.stdin_raw else None
63+
self.stdout = FileReadStream(self.stdout_raw) if self.stdout_raw else None
6764

6865
async def __aenter__(self):
6966
"""Support async context manager entry."""
@@ -72,11 +69,11 @@ async def __aenter__(self):
7269
async def __aexit__(self, exc_type, exc_val, exc_tb):
7370
"""Terminate and wait on process exit inside a thread."""
7471
self.popen.terminate()
75-
await anyio.to_thread.run_sync(self.popen.wait)
72+
await to_thread.run_sync(self.popen.wait)
7673

7774
async def wait(self):
7875
"""Async wait for process completion."""
79-
return await anyio.to_thread.run_sync(self.popen.wait)
76+
return await to_thread.run_sync(self.popen.wait)
8077

8178
def terminate(self):
8279
"""Terminate the subprocess immediately."""
@@ -89,10 +86,10 @@ def terminate(self):
8986
async def create_windows_process(
9087
command: str,
9188
args: list[str],
92-
env: Optional[dict[str, str]] = None,
93-
errlog: Optional[TextIO] = sys.stderr,
94-
cwd: Union[Path, str, None] = None,
95-
):
89+
env: dict[str, str] | None = None,
90+
errlog: TextIO | None = sys.stderr,
91+
cwd: Path | str | None = None,
92+
) -> DummyProcess:
9693
"""
9794
Creates a subprocess in a Windows-compatible way.
9895
@@ -120,7 +117,11 @@ async def create_windows_process(
120117
env=env,
121118
cwd=cwd,
122119
bufsize=0, # Unbuffered output
123-
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
120+
creationflags=(
121+
subprocess.CREATE_NO_WINDOW
122+
if hasattr(subprocess, "CREATE_NO_WINDOW")
123+
else 0
124+
),
124125
)
125126
return DummyProcess(popen_obj)
126127

0 commit comments

Comments
 (0)