Skip to content

Commit 627704a

Browse files
committed
Address PR comments
1 parent 725dc61 commit 627704a

File tree

3 files changed

+177
-148
lines changed

3 files changed

+177
-148
lines changed
Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import os
2-
import shutil
3-
import subprocess
42
import sys
53
from contextlib import asynccontextmanager
64
from pathlib import Path
@@ -14,6 +12,8 @@
1412

1513
import mcp.types as types
1614

15+
from .win32 import create_windows_process, get_windows_executable_command
16+
1717
# Environment variables to inherit by default
1818
DEFAULT_INHERITED_ENV_VARS = (
1919
[
@@ -115,7 +115,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
115115
else get_default_environment()
116116
),
117117
errlog=errlog,
118-
cwd=server.cwd
118+
cwd=server.cwd,
119119
)
120120

121121
async def stdout_reader():
@@ -172,9 +172,9 @@ async def stdin_writer():
172172
try:
173173
process.terminate()
174174
if sys.platform == "win32":
175-
# On Windows, terminating a process with process.terminate() doesn't
176-
# always guarantee immediate process termination.
177-
# So we give it 2s to exit, or we call process.kill()
175+
# On Windows, terminating a process with process.terminate() doesn't
176+
# always guarantee immediate process termination.
177+
# So we give it 2s to exit, or we call process.kill()
178178
# which sends a SIGKILL equivalent signal.
179179
try:
180180
with anyio.fail_after(2.0):
@@ -196,28 +196,9 @@ def _get_executable_command(command: str) -> str:
196196
Returns:
197197
str: Platform-appropriate command
198198
"""
199-
200-
try:
201-
if sys.platform != "win32":
202-
return command
203-
else:
204-
# For Windows, we need more sophisticated path resolution
205-
# First check if command exists in PATH as-is
206-
if command_path:= shutil.which(command):
207-
return command_path
208-
209-
# Check for Windows-specific extensions
210-
for ext in [".cmd", ".bat", ".exe", ".ps1"]:
211-
ext_version = f"{command}{ext}"
212-
if ext_path:= shutil.which(ext_version):
213-
return ext_path
214-
215-
# For regular commands or if we couldn't find special versions
216-
return command
217-
except Exception:
218-
# If anything goes wrong, just return the original command
219-
# shutil.which() could raise exceptions if there are permission
220-
# issues accessing directories in PATH
199+
if sys.platform == "win32":
200+
return get_windows_executable_command(command)
201+
else:
221202
return command
222203

223204

@@ -232,28 +213,8 @@ async def _create_platform_compatible_process(
232213
Creates a subprocess in a platform-compatible way.
233214
Returns a process handle.
234215
"""
235-
236-
process = None
237-
238216
if sys.platform == "win32":
239-
try:
240-
process = await anyio.open_process(
241-
[command, *args],
242-
env=env,
243-
# Ensure we don't create console windows for each process
244-
creationflags=subprocess.CREATE_NO_WINDOW # type: ignore
245-
if hasattr(subprocess, "CREATE_NO_WINDOW")
246-
else 0,
247-
stderr=errlog,
248-
cwd=cwd,
249-
)
250-
251-
return process
252-
except Exception:
253-
# Don't raise, let's try to create the process without creation flags
254-
process = await anyio.open_process(
255-
[command, *args], env=env, stderr=errlog, cwd=cwd
256-
)
217+
process = await create_windows_process(command, args, env, errlog, cwd)
257218
else:
258219
process = await anyio.open_process(
259220
[command, *args], env=env, stderr=errlog, cwd=cwd

src/mcp/client/stdio/win32.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Windows-specific functionality for stdio client operations.
3+
"""
4+
5+
import shutil
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
from typing import TextIO
10+
11+
import anyio
12+
13+
14+
def get_windows_executable_command(command: str) -> str:
15+
"""
16+
Get the correct executable command normalized for Windows.
17+
18+
On Windows, commands might exist with specific extensions (.exe, .cmd, etc.)
19+
that need to be located for proper execution.
20+
21+
Args:
22+
command: Base command (e.g., 'uvx', 'npx')
23+
24+
Returns:
25+
str: Windows-appropriate command path
26+
"""
27+
try:
28+
# First check if command exists in PATH as-is
29+
if command_path := shutil.which(command):
30+
return command_path
31+
32+
# Check for Windows-specific extensions
33+
for ext in [".cmd", ".bat", ".exe", ".ps1"]:
34+
ext_version = f"{command}{ext}"
35+
if ext_path := shutil.which(ext_version):
36+
return ext_path
37+
38+
# For regular commands or if we couldn't find special versions
39+
return command
40+
except OSError:
41+
# Handle file system errors during path resolution
42+
# (permissions, broken symlinks, etc.)
43+
return command
44+
45+
46+
async def create_windows_process(
47+
command: str,
48+
args: list[str],
49+
env: dict[str, str] | None = None,
50+
errlog: TextIO = sys.stderr,
51+
cwd: Path | str | None = None,
52+
):
53+
"""
54+
Creates a subprocess in a Windows-compatible way.
55+
56+
Windows processes need special handling for console windows and
57+
process creation flags.
58+
59+
Args:
60+
command: The command to execute
61+
args: Command line arguments
62+
env: Environment variables
63+
errlog: Where to send stderr output
64+
cwd: Working directory for the process
65+
66+
Returns:
67+
A process handle
68+
"""
69+
try:
70+
# Try with Windows-specific flags to hide console window
71+
process = await anyio.open_process(
72+
[command, *args],
73+
env=env,
74+
# Ensure we don't create console windows for each process
75+
creationflags=subprocess.CREATE_NO_WINDOW # type: ignore
76+
if hasattr(subprocess, "CREATE_NO_WINDOW")
77+
else 0,
78+
stderr=errlog,
79+
cwd=cwd,
80+
)
81+
return process
82+
except Exception:
83+
# Don't raise, let's try to create the process without creation flags
84+
process = await anyio.open_process(
85+
[command, *args], env=env, stderr=errlog, cwd=cwd
86+
)
87+
return process

0 commit comments

Comments
 (0)