Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ Alternatively, for projects using pip for dependencies:
pip install "mcp[cli]"
```

On Windows, add the `stdio` extra if you want `pywin32`-backed Job Object cleanup for subprocesses spawned through `mcp.client.stdio`:

```bash
uv add "mcp[cli,stdio]"
pip install "mcp[cli,stdio]"
```

### Running the standalone MCP development tools

To run the mcp command with uv:
Expand Down
4 changes: 3 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ The following dependencies are automatically installed:
- [`pydantic-settings`](https://pypi.org/project/pydantic-settings/): Settings management used in MCPServer.
- [`uvicorn`](https://pypi.org/project/uvicorn/): ASGI server used to run the HTTP transport endpoints.
- [`jsonschema`](https://pypi.org/project/jsonschema/): JSON schema validation.
- [`pywin32`](https://pypi.org/project/pywin32/): Windows specific dependencies for the CLI tools.

This package has the following optional groups:

- `cli`: Installs `typer` and `python-dotenv` for the MCP CLI tools.
- `stdio`: Installs [`pywin32`](https://pypi.org/project/pywin32/) on Windows for Job Object based subprocess cleanup in `mcp.client.stdio`.

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.
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ dependencies = [
"pydantic-settings>=2.5.2",
"uvicorn>=0.31.1; sys_platform != 'emscripten'",
"jsonschema>=4.20.0",
"pywin32>=311; sys_platform == 'win32'",
"pyjwt[crypto]>=2.10.1",
"typing-extensions>=4.13.0",
"typing-inspection>=0.4.1",
Expand All @@ -45,6 +44,7 @@ dependencies = [
[project.optional-dependencies]
rich = ["rich>=13.9.4"]
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
stdio = ["pywin32>=311; sys_platform == 'win32'"]
ws = ["websockets>=15.0.1"]

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

[dependency-groups]
dev = [
# We add mcp[cli,ws] so `uv sync` considers the extras.
"mcp[cli,ws]",
# We add mcp[cli,stdio,ws] so `uv sync` considers the extras.
"mcp[cli,stdio,ws]",
"pyright>=1.1.400",
"pytest>=8.3.4",
"ruff>=0.8.5",
Expand Down
25 changes: 23 additions & 2 deletions src/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from importlib import import_module
from typing import TYPE_CHECKING, Any

from .client.client import Client
from .client.session import ClientSession
from .client.session_group import ClientSessionGroup
from .client.stdio import StdioServerParameters, stdio_client
from .server.session import ServerSession
from .server.stdio import stdio_server
from .shared.exceptions import MCPError, UrlElicitationRequiredError
Expand Down Expand Up @@ -65,6 +66,10 @@
)
from .types import Role as SamplingRole

if TYPE_CHECKING:
from .client.session_group import ClientSessionGroup
from .client.stdio import StdioServerParameters, stdio_client

__all__ = [
"CallToolRequest",
"Client",
Expand Down Expand Up @@ -133,3 +138,19 @@
"stdio_client",
"stdio_server",
]

_LAZY_EXPORTS = {
"ClientSessionGroup": (".client.session_group", "ClientSessionGroup"),
"StdioServerParameters": (".client.stdio", "StdioServerParameters"),
"stdio_client": (".client.stdio", "stdio_client"),
}


def __getattr__(name: str) -> Any:
if name not in _LAZY_EXPORTS:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

module_name, export_name = _LAZY_EXPORTS[name]
value = getattr(import_module(module_name, __name__), export_name)
globals()[name] = value
return value
14 changes: 10 additions & 4 deletions src/mcp/os/win32/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@

# Windows-specific imports for Job Objects
if sys.platform == "win32":
import pywintypes
import win32api
import win32con
import win32job
try:
import pywintypes
import win32api
import win32con
import win32job
except ImportError:
win32api = None
win32con = None
win32job = None
pywintypes = None
else:
# Type stubs for non-Windows platforms
win32api = None
Expand Down
64 changes: 64 additions & 0 deletions tests/issues/test_2233_windows_stdio_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Regression tests for issue #2233: stdio imports should stay optional on Windows."""

import os
import subprocess
import sys
import textwrap
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]


def _run_repo_python(script: str) -> subprocess.CompletedProcess[str]:
env = os.environ.copy()
src_path = str(REPO_ROOT / "src")
env["PYTHONPATH"] = src_path if "PYTHONPATH" not in env else os.pathsep.join([src_path, env["PYTHONPATH"]])
return subprocess.run(
[sys.executable, "-c", script],
cwd=REPO_ROOT,
env=env,
capture_output=True,
text=True,
check=False,
)


def test_server_stdio_import_does_not_load_client_stdio():
script = textwrap.dedent("""
import importlib
import sys

importlib.import_module("mcp.server.stdio")
assert "mcp.client.stdio" not in sys.modules
""")

result = _run_repo_python(script)

assert result.returncode == 0, result.stderr


def test_root_stdio_exports_handle_missing_pywin32():
script = textwrap.dedent("""
import builtins

real_import = builtins.__import__
blocked_modules = {"pywintypes", "win32api", "win32con", "win32job"}

def guarded_import(name, globals=None, locals=None, fromlist=(), level=0):
if name.split(".", 1)[0] in blocked_modules:
raise ImportError(f"blocked import: {name}")
return real_import(name, globals, locals, fromlist, level)

builtins.__import__ = guarded_import
try:
from mcp import StdioServerParameters, stdio_client

assert StdioServerParameters.__name__ == "StdioServerParameters"
assert callable(stdio_client)
finally:
builtins.__import__ = real_import
""")

result = _run_repo_python(script)

assert result.returncode == 0, result.stderr
12 changes: 7 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading