Skip to content

Commit 081d463

Browse files
committed
fix: make windows stdio pywin32 optional
1 parent fb2276b commit 081d463

File tree

8 files changed

+124
-15
lines changed

8 files changed

+124
-15
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ Alternatively, for projects using pip for dependencies:
123123
pip install "mcp[cli]"
124124
```
125125

126+
On Windows, add the `stdio` extra if you want `pywin32`-backed Job Object cleanup for subprocesses spawned through `mcp.client.stdio`:
127+
128+
```bash
129+
uv add "mcp[cli,stdio]"
130+
pip install "mcp[cli,stdio]"
131+
```
132+
126133
### Running the standalone MCP development tools
127134

128135
To run the mcp command with uv:

README.v2.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ Alternatively, for projects using pip for dependencies:
124124
pip install "mcp[cli]"
125125
```
126126

127+
On Windows, add the `stdio` extra if you want `pywin32`-backed Job Object cleanup for subprocesses spawned through `mcp.client.stdio`:
128+
129+
```bash
130+
uv add "mcp[cli,stdio]"
131+
pip install "mcp[cli,stdio]"
132+
```
133+
127134
### Running the standalone MCP development tools
128135

129136
To run the mcp command with uv:

docs/installation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ The following dependencies are automatically installed:
2424
- [`pydantic-settings`](https://pypi.org/project/pydantic-settings/): Settings management used in MCPServer.
2525
- [`uvicorn`](https://pypi.org/project/uvicorn/): ASGI server used to run the HTTP transport endpoints.
2626
- [`jsonschema`](https://pypi.org/project/jsonschema/): JSON schema validation.
27-
- [`pywin32`](https://pypi.org/project/pywin32/): Windows specific dependencies for the CLI tools.
2827

2928
This package has the following optional groups:
3029

3130
- `cli`: Installs `typer` and `python-dotenv` for the MCP CLI tools.
31+
- `stdio`: Installs [`pywin32`](https://pypi.org/project/pywin32/) on Windows for Job Object based subprocess cleanup in `mcp.client.stdio`.
32+
33+
Server-only Windows installs can use the base `mcp` package (or `mcp[cli]`) without pulling in `pywin32`. Add the `stdio` extra only if you want the additional Windows stdio cleanup integration.

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ dependencies = [
3636
"pydantic-settings>=2.5.2",
3737
"uvicorn>=0.31.1; sys_platform != 'emscripten'",
3838
"jsonschema>=4.20.0",
39-
"pywin32>=311; sys_platform == 'win32'",
4039
"pyjwt[crypto]>=2.10.1",
4140
"typing-extensions>=4.13.0",
4241
"typing-inspection>=0.4.1",
@@ -45,6 +44,7 @@ dependencies = [
4544
[project.optional-dependencies]
4645
rich = ["rich>=13.9.4"]
4746
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
47+
stdio = ["pywin32>=311; sys_platform == 'win32'"]
4848
ws = ["websockets>=15.0.1"]
4949

5050
[project.scripts]
@@ -56,8 +56,8 @@ required-version = ">=0.9.5"
5656

5757
[dependency-groups]
5858
dev = [
59-
# We add mcp[cli,ws] so `uv sync` considers the extras.
60-
"mcp[cli,ws]",
59+
# We add mcp[cli,stdio,ws] so `uv sync` considers the extras.
60+
"mcp[cli,stdio,ws]",
6161
"pyright>=1.1.400",
6262
"pytest>=8.3.4",
6363
"ruff>=0.8.5",

src/mcp/__init__.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from importlib import import_module
2+
from typing import TYPE_CHECKING, Any
3+
14
from .client.client import Client
25
from .client.session import ClientSession
3-
from .client.session_group import ClientSessionGroup
4-
from .client.stdio import StdioServerParameters, stdio_client
56
from .server.session import ServerSession
67
from .server.stdio import stdio_server
78
from .shared.exceptions import MCPError, UrlElicitationRequiredError
@@ -65,6 +66,10 @@
6566
)
6667
from .types import Role as SamplingRole
6768

69+
if TYPE_CHECKING:
70+
from .client.session_group import ClientSessionGroup
71+
from .client.stdio import StdioServerParameters, stdio_client
72+
6873
__all__ = [
6974
"CallToolRequest",
7075
"Client",
@@ -133,3 +138,19 @@
133138
"stdio_client",
134139
"stdio_server",
135140
]
141+
142+
_LAZY_EXPORTS = {
143+
"ClientSessionGroup": (".client.session_group", "ClientSessionGroup"),
144+
"StdioServerParameters": (".client.stdio", "StdioServerParameters"),
145+
"stdio_client": (".client.stdio", "stdio_client"),
146+
}
147+
148+
149+
def __getattr__(name: str) -> Any:
150+
if name not in _LAZY_EXPORTS:
151+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
152+
153+
module_name, export_name = _LAZY_EXPORTS[name]
154+
value = getattr(import_module(module_name, __name__), export_name)
155+
globals()[name] = value
156+
return value

src/mcp/os/win32/utilities.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@
1717

1818
# Windows-specific imports for Job Objects
1919
if sys.platform == "win32":
20-
import pywintypes
21-
import win32api
22-
import win32con
23-
import win32job
20+
try:
21+
import pywintypes
22+
import win32api
23+
import win32con
24+
import win32job
25+
except ImportError:
26+
win32api = None
27+
win32con = None
28+
win32job = None
29+
pywintypes = None
2430
else:
2531
# Type stubs for non-Windows platforms
2632
win32api = None
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Regression tests for issue #2233: stdio imports should stay optional on Windows."""
2+
3+
import os
4+
import subprocess
5+
import sys
6+
import textwrap
7+
from pathlib import Path
8+
9+
REPO_ROOT = Path(__file__).resolve().parents[2]
10+
11+
12+
def _run_repo_python(script: str) -> subprocess.CompletedProcess[str]:
13+
env = os.environ.copy()
14+
src_path = str(REPO_ROOT / "src")
15+
env["PYTHONPATH"] = src_path if "PYTHONPATH" not in env else os.pathsep.join([src_path, env["PYTHONPATH"]])
16+
return subprocess.run(
17+
[sys.executable, "-c", script],
18+
cwd=REPO_ROOT,
19+
env=env,
20+
capture_output=True,
21+
text=True,
22+
check=False,
23+
)
24+
25+
26+
def test_server_stdio_import_does_not_load_client_stdio():
27+
script = textwrap.dedent("""
28+
import importlib
29+
import sys
30+
31+
importlib.import_module("mcp.server.stdio")
32+
assert "mcp.client.stdio" not in sys.modules
33+
""")
34+
35+
result = _run_repo_python(script)
36+
37+
assert result.returncode == 0, result.stderr
38+
39+
40+
def test_root_stdio_exports_handle_missing_pywin32():
41+
script = textwrap.dedent("""
42+
import builtins
43+
44+
real_import = builtins.__import__
45+
blocked_modules = {"pywintypes", "win32api", "win32con", "win32job"}
46+
47+
def guarded_import(name, globals=None, locals=None, fromlist=(), level=0):
48+
if name.split(".", 1)[0] in blocked_modules:
49+
raise ImportError(f"blocked import: {name}")
50+
return real_import(name, globals, locals, fromlist, level)
51+
52+
builtins.__import__ = guarded_import
53+
try:
54+
from mcp import StdioServerParameters, stdio_client
55+
56+
assert StdioServerParameters.__name__ == "StdioServerParameters"
57+
assert callable(stdio_client)
58+
finally:
59+
builtins.__import__ = real_import
60+
""")
61+
62+
result = _run_repo_python(script)
63+
64+
assert result.returncode == 0, result.stderr

uv.lock

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)