From 83699ac8b8eee06adfad70527f576eabf597323c Mon Sep 17 00:00:00 2001 From: theailanguage Date: Mon, 28 Apr 2025 17:31:04 +0530 Subject: [PATCH 1/4] Fix Windows subprocess compatibility for STDIO mode with async streams --- src/mcp/client/stdio/win32.py | 98 ++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 825a0477..8ca5df92 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -10,6 +10,10 @@ import anyio from anyio.abc import Process +from anyio.streams.file import FileReadStream, FileWriteStream + +from typing import Optional, TextIO, Union +from pathlib import Path def get_windows_executable_command(command: str) -> str: @@ -43,49 +47,95 @@ def get_windows_executable_command(command: str) -> str: # (permissions, broken symlinks, etc.) return command +class DummyProcess: + """ + A fallback process wrapper for Windows to handle async I/O + when using subprocess.Popen, which provides sync-only FileIO objects. + + This wraps stdin and stdout into async-compatible streams (FileReadStream, FileWriteStream), + so that MCP clients expecting async streams can work properly. + """ + def __init__(self, popen_obj: subprocess.Popen): + self.popen = popen_obj + self.stdin_raw = popen_obj.stdin + self.stdout_raw = popen_obj.stdout + self.stderr = popen_obj.stderr + + # Wrap into async-compatible AnyIO streams + self.stdin = FileWriteStream(self.stdin_raw) + self.stdout = FileReadStream(self.stdout_raw) + + async def __aenter__(self): + """Support async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Terminate and wait on process exit inside a thread.""" + self.popen.terminate() + await anyio.to_thread.run_sync(self.popen.wait) + + async def wait(self): + """Async wait for process completion.""" + return await anyio.to_thread.run_sync(self.popen.wait) + + def terminate(self): + """Terminate the subprocess immediately.""" + return self.popen.terminate() + +# ------------------------ +# Updated function +# ------------------------ async def create_windows_process( command: str, args: list[str], - env: dict[str, str] | None = None, - errlog: TextIO = sys.stderr, - cwd: Path | str | None = None, + env: Optional[dict[str, str]] = None, + errlog: Optional[TextIO] = sys.stderr, + cwd: Union[Path, str, None] = None, ): """ Creates a subprocess in a Windows-compatible way. - - Windows processes need special handling for console windows and - process creation flags. + + On Windows, asyncio.create_subprocess_exec has incomplete support + (NotImplementedError when trying to open subprocesses). + Therefore, we fallback to subprocess.Popen and wrap it for async usage. Args: - command: The command to execute - args: Command line arguments - env: Environment variables - errlog: Where to send stderr output - cwd: Working directory for the process + command (str): The executable to run + args (list[str]): List of command line arguments + env (dict[str, str] | None): Environment variables + errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr) + cwd (Path | str | None): Working directory for the subprocess Returns: - A process handle + DummyProcess: Async-compatible subprocess with stdin and stdout streams """ try: - # Try with Windows-specific flags to hide console window - process = await anyio.open_process( + # Try launching with creationflags to avoid opening a new console window + popen_obj = subprocess.Popen( [command, *args], - env=env, - # Ensure we don't create console windows for each process - creationflags=subprocess.CREATE_NO_WINDOW # type: ignore - if hasattr(subprocess, "CREATE_NO_WINDOW") - else 0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=errlog, + env=env, cwd=cwd, + bufsize=0, # Unbuffered output + creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0, ) - return process + return DummyProcess(popen_obj) + except Exception: - # Don't raise, let's try to create the process without creation flags - process = await anyio.open_process( - [command, *args], env=env, stderr=errlog, cwd=cwd + # If creationflags failed, fallback without them + popen_obj = subprocess.Popen( + [command, *args], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=errlog, + env=env, + cwd=cwd, + bufsize=0, ) - return process + return DummyProcess(popen_obj) async def terminate_windows_process(process: Process): From f1bc421be39a2e5903344dc79e0e5c787ca39f78 Mon Sep 17 00:00:00 2001 From: theailanguage Date: Mon, 28 Apr 2025 18:47:21 +0530 Subject: [PATCH 2/4] Fix: Windows stdio subprocess compatibility with type hints and fallback to subprocess.Popen --- src/mcp/client/stdio/win32.py | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 8ca5df92..2b34181b 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -6,16 +6,13 @@ import subprocess import sys from pathlib import Path -from typing import TextIO +from typing import IO, TextIO import anyio +from anyio import to_thread from anyio.abc import Process from anyio.streams.file import FileReadStream, FileWriteStream -from typing import Optional, TextIO, Union -from pathlib import Path - - def get_windows_executable_command(command: str) -> str: """ Get the correct executable command normalized for Windows. @@ -52,18 +49,18 @@ class DummyProcess: A fallback process wrapper for Windows to handle async I/O when using subprocess.Popen, which provides sync-only FileIO objects. - This wraps stdin and stdout into async-compatible streams (FileReadStream, FileWriteStream), + This wraps stdin and stdout into async-compatible + streams (FileReadStream, FileWriteStream), so that MCP clients expecting async streams can work properly. """ - def __init__(self, popen_obj: subprocess.Popen): - self.popen = popen_obj - self.stdin_raw = popen_obj.stdin - self.stdout_raw = popen_obj.stdout - self.stderr = popen_obj.stderr + def __init__(self, popen_obj: subprocess.Popen[bytes]): + self.popen: subprocess.Popen[bytes] = popen_obj + self.stdin_raw: IO[bytes] | None = popen_obj.stdin + self.stdout_raw: IO[bytes] | None = popen_obj.stdout + self.stderr: IO[bytes] | None = popen_obj.stderr - # Wrap into async-compatible AnyIO streams - self.stdin = FileWriteStream(self.stdin_raw) - self.stdout = FileReadStream(self.stdout_raw) + self.stdin = FileWriteStream(self.stdin_raw) if self.stdin_raw else None + self.stdout = FileReadStream(self.stdout_raw) if self.stdout_raw else None async def __aenter__(self): """Support async context manager entry.""" @@ -72,11 +69,11 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_val, exc_tb): """Terminate and wait on process exit inside a thread.""" self.popen.terminate() - await anyio.to_thread.run_sync(self.popen.wait) + await to_thread.run_sync(self.popen.wait) async def wait(self): """Async wait for process completion.""" - return await anyio.to_thread.run_sync(self.popen.wait) + return await to_thread.run_sync(self.popen.wait) def terminate(self): """Terminate the subprocess immediately.""" @@ -89,10 +86,10 @@ def terminate(self): async def create_windows_process( command: str, args: list[str], - env: Optional[dict[str, str]] = None, - errlog: Optional[TextIO] = sys.stderr, - cwd: Union[Path, str, None] = None, -): + env: dict[str, str] | None = None, + errlog: TextIO | None = sys.stderr, + cwd: Path | str | None = None, +) -> DummyProcess: """ Creates a subprocess in a Windows-compatible way. @@ -120,7 +117,11 @@ async def create_windows_process( env=env, cwd=cwd, bufsize=0, # Unbuffered output - creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0, + creationflags=( + subprocess.CREATE_NO_WINDOW + if hasattr(subprocess, "CREATE_NO_WINDOW") + else 0 + ), ) return DummyProcess(popen_obj) From a4c6500abde52a9857cdedb7008a19158796004b Mon Sep 17 00:00:00 2001 From: theailanguage Date: Mon, 28 Apr 2025 19:00:49 +0530 Subject: [PATCH 3/4] style(win32): fix import sorting and formatting issues --- src/mcp/client/stdio/win32.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 2b34181b..56be0c3f 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -13,6 +13,7 @@ from anyio.abc import Process from anyio.streams.file import FileReadStream, FileWriteStream + def get_windows_executable_command(command: str) -> str: """ Get the correct executable command normalized for Windows. @@ -44,15 +45,17 @@ def get_windows_executable_command(command: str) -> str: # (permissions, broken symlinks, etc.) return command + class DummyProcess: """ - A fallback process wrapper for Windows to handle async I/O + A fallback process wrapper for Windows to handle async I/O when using subprocess.Popen, which provides sync-only FileIO objects. - - This wraps stdin and stdout into async-compatible + + This wraps stdin and stdout into async-compatible streams (FileReadStream, FileWriteStream), so that MCP clients expecting async streams can work properly. """ + def __init__(self, popen_obj: subprocess.Popen[bytes]): self.popen: subprocess.Popen[bytes] = popen_obj self.stdin_raw: IO[bytes] | None = popen_obj.stdin @@ -79,10 +82,12 @@ def terminate(self): """Terminate the subprocess immediately.""" return self.popen.terminate() + # ------------------------ # Updated function # ------------------------ + async def create_windows_process( command: str, args: list[str], @@ -92,9 +97,9 @@ async def create_windows_process( ) -> DummyProcess: """ Creates a subprocess in a Windows-compatible way. - - On Windows, asyncio.create_subprocess_exec has incomplete support - (NotImplementedError when trying to open subprocesses). + + On Windows, asyncio.create_subprocess_exec has incomplete support + (NotImplementedError when trying to open subprocesses). Therefore, we fallback to subprocess.Popen and wrap it for async usage. Args: @@ -118,8 +123,8 @@ async def create_windows_process( cwd=cwd, bufsize=0, # Unbuffered output creationflags=( - subprocess.CREATE_NO_WINDOW - if hasattr(subprocess, "CREATE_NO_WINDOW") + subprocess.CREATE_NO_WINDOW + if hasattr(subprocess, "CREATE_NO_WINDOW") else 0 ), ) From 25596ab9cffee1025829f9f1e7252547f0a10fc3 Mon Sep 17 00:00:00 2001 From: theailanguage Date: Mon, 28 Apr 2025 19:25:52 +0530 Subject: [PATCH 4/4] style(stdio): format imports and wrap long lines for ruff compliance --- src/mcp/client/stdio/win32.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 56be0c3f..7b9c79e2 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -6,7 +6,7 @@ import subprocess import sys from pathlib import Path -from typing import IO, TextIO +from typing import BinaryIO, TextIO, cast import anyio from anyio import to_thread @@ -58,18 +58,27 @@ class DummyProcess: def __init__(self, popen_obj: subprocess.Popen[bytes]): self.popen: subprocess.Popen[bytes] = popen_obj - self.stdin_raw: IO[bytes] | None = popen_obj.stdin - self.stdout_raw: IO[bytes] | None = popen_obj.stdout - self.stderr: IO[bytes] | None = popen_obj.stderr + self.stdin_raw = popen_obj.stdin # type: ignore[assignment] + self.stdout_raw = popen_obj.stdout # type: ignore[assignment] + self.stderr = popen_obj.stderr # type: ignore[assignment] - self.stdin = FileWriteStream(self.stdin_raw) if self.stdin_raw else None - self.stdout = FileReadStream(self.stdout_raw) if self.stdout_raw else None + self.stdin = ( + FileWriteStream(cast(BinaryIO, self.stdin_raw)) if self.stdin_raw else None + ) + self.stdout = ( + FileReadStream(cast(BinaryIO, self.stdout_raw)) if self.stdout_raw else None + ) async def __aenter__(self): """Support async context manager entry.""" return self - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__( + self, + exc_type: BaseException | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: """Terminate and wait on process exit inside a thread.""" self.popen.terminate() await to_thread.run_sync(self.popen.wait) @@ -122,11 +131,7 @@ async def create_windows_process( env=env, cwd=cwd, bufsize=0, # Unbuffered output - creationflags=( - subprocess.CREATE_NO_WINDOW - if hasattr(subprocess, "CREATE_NO_WINDOW") - else 0 - ), + creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0), ) return DummyProcess(popen_obj)