mirror of
https://github.com/aljazceru/mcp-python-sdk.git
synced 2025-12-20 07:14:24 +01:00
Fixes to stdio_client to support Windows more robustly (#372)
This commit is contained in:
@@ -12,6 +12,12 @@ from pydantic import BaseModel, Field
|
||||
|
||||
import mcp.types as types
|
||||
|
||||
from .win32 import (
|
||||
create_windows_process,
|
||||
get_windows_executable_command,
|
||||
terminate_windows_process,
|
||||
)
|
||||
|
||||
# Environment variables to inherit by default
|
||||
DEFAULT_INHERITED_ENV_VARS = (
|
||||
[
|
||||
@@ -101,14 +107,18 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
|
||||
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
|
||||
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
|
||||
|
||||
process = await anyio.open_process(
|
||||
[server.command, *server.args],
|
||||
command = _get_executable_command(server.command)
|
||||
|
||||
# Open process with stderr piped for capture
|
||||
process = await _create_platform_compatible_process(
|
||||
command=command,
|
||||
args=server.args,
|
||||
env=(
|
||||
{**get_default_environment(), **server.env}
|
||||
if server.env is not None
|
||||
else get_default_environment()
|
||||
),
|
||||
stderr=errlog,
|
||||
errlog=errlog,
|
||||
cwd=server.cwd,
|
||||
)
|
||||
|
||||
@@ -159,4 +169,48 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
|
||||
):
|
||||
tg.start_soon(stdout_reader)
|
||||
tg.start_soon(stdin_writer)
|
||||
try:
|
||||
yield read_stream, write_stream
|
||||
finally:
|
||||
# Clean up process to prevent any dangling orphaned processes
|
||||
if sys.platform == "win32":
|
||||
await terminate_windows_process(process)
|
||||
else:
|
||||
process.terminate()
|
||||
|
||||
|
||||
def _get_executable_command(command: str) -> str:
|
||||
"""
|
||||
Get the correct executable command normalized for the current platform.
|
||||
|
||||
Args:
|
||||
command: Base command (e.g., 'uvx', 'npx')
|
||||
|
||||
Returns:
|
||||
str: Platform-appropriate command
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
return get_windows_executable_command(command)
|
||||
else:
|
||||
return command
|
||||
|
||||
|
||||
async def _create_platform_compatible_process(
|
||||
command: str,
|
||||
args: list[str],
|
||||
env: dict[str, str] | None = None,
|
||||
errlog: TextIO = sys.stderr,
|
||||
cwd: Path | str | None = None,
|
||||
):
|
||||
"""
|
||||
Creates a subprocess in a platform-compatible way.
|
||||
Returns a process handle.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
process = await create_windows_process(command, args, env, errlog, cwd)
|
||||
else:
|
||||
process = await anyio.open_process(
|
||||
[command, *args], env=env, stderr=errlog, cwd=cwd
|
||||
)
|
||||
|
||||
return process
|
||||
109
src/mcp/client/stdio/win32.py
Normal file
109
src/mcp/client/stdio/win32.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Windows-specific functionality for stdio client operations.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
import anyio
|
||||
from anyio.abc import Process
|
||||
|
||||
|
||||
def get_windows_executable_command(command: str) -> str:
|
||||
"""
|
||||
Get the correct executable command normalized for Windows.
|
||||
|
||||
On Windows, commands might exist with specific extensions (.exe, .cmd, etc.)
|
||||
that need to be located for proper execution.
|
||||
|
||||
Args:
|
||||
command: Base command (e.g., 'uvx', 'npx')
|
||||
|
||||
Returns:
|
||||
str: Windows-appropriate command path
|
||||
"""
|
||||
try:
|
||||
# First check if command exists in PATH as-is
|
||||
if command_path := shutil.which(command):
|
||||
return command_path
|
||||
|
||||
# Check for Windows-specific extensions
|
||||
for ext in [".cmd", ".bat", ".exe", ".ps1"]:
|
||||
ext_version = f"{command}{ext}"
|
||||
if ext_path := shutil.which(ext_version):
|
||||
return ext_path
|
||||
|
||||
# For regular commands or if we couldn't find special versions
|
||||
return command
|
||||
except OSError:
|
||||
# Handle file system errors during path resolution
|
||||
# (permissions, broken symlinks, etc.)
|
||||
return command
|
||||
|
||||
|
||||
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,
|
||||
):
|
||||
"""
|
||||
Creates a subprocess in a Windows-compatible way.
|
||||
|
||||
Windows processes need special handling for console windows and
|
||||
process creation flags.
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
A process handle
|
||||
"""
|
||||
try:
|
||||
# Try with Windows-specific flags to hide console window
|
||||
process = await anyio.open_process(
|
||||
[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,
|
||||
stderr=errlog,
|
||||
cwd=cwd,
|
||||
)
|
||||
return process
|
||||
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
|
||||
)
|
||||
return process
|
||||
|
||||
|
||||
async def terminate_windows_process(process: Process):
|
||||
"""
|
||||
Terminate a Windows process.
|
||||
|
||||
Note: On Windows, terminating a process with process.terminate() doesn't
|
||||
always guarantee immediate process termination.
|
||||
So we give it 2s to exit, or we call process.kill()
|
||||
which sends a SIGKILL equivalent signal.
|
||||
|
||||
Args:
|
||||
process: The process to terminate
|
||||
"""
|
||||
try:
|
||||
process.terminate()
|
||||
with anyio.fail_after(2.0):
|
||||
await process.wait()
|
||||
except TimeoutError:
|
||||
# Force kill if it doesn't terminate
|
||||
process.kill()
|
||||
Reference in New Issue
Block a user