Skip to content

Commit 46be5ca

Browse files
committed
fix(cli): handle missing Windows npx without crashing
Catch FileNotFoundError when probing npx on Windows, add return annotations for CLI helpers, and cover the missing-binary path in unit tests. Fixes #3673.
1 parent 4b5fe5a commit 46be5ca

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

‎src/mcp/cli/cli.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import subprocess
77
import sys
88
from pathlib import Path
9-
from typing import Annotated, Any
9+
from typing import Annotated
1010

1111
from mcp.server import MCPServer
1212
from mcp.server import Server as LowLevelServer
@@ -39,15 +39,16 @@
3939
)
4040

4141

42-
def _get_npx_command():
42+
def _get_npx_command() -> str | None:
4343
"""Get the correct npx command for the current platform."""
4444
if sys.platform == "win32":
4545
# Try both npx.cmd and npx.exe on Windows
4646
for cmd in ["npx.cmd", "npx.exe", "npx"]:
4747
try:
4848
subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)
4949
return cmd
50-
except subprocess.CalledProcessError:
50+
except (subprocess.CalledProcessError, FileNotFoundError):
51+
# Missing executables raise FileNotFoundError before CalledProcessError.
5152
continue
5253
return None
5354
return "npx" # On Unix-like systems, just use npx
@@ -116,7 +117,7 @@ def _parse_file_path(file_spec: str) -> tuple[Path, str | None]:
116117
return file_path, server_object
117118

118119

119-
def _import_server(file: Path, server_object: str | None = None): # pragma: no cover
120+
def _import_server(file: Path, server_object: str | None = None) -> MCPServer: # pragma: no cover
120121
"""Import an MCP server from a file.
121122
122123
Args:
@@ -140,7 +141,7 @@ def _import_server(file: Path, server_object: str | None = None): # pragma: no
140141
module = importlib.util.module_from_spec(spec)
141142
spec.loader.exec_module(module)
142143

143-
def _check_server_object(server_object: Any, object_name: str):
144+
def _check_server_object(server_object: object, object_name: str) -> bool:
144145
"""Helper function to check that the server object is supported
145146
146147
Args:

‎tests/cli/test_utils.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,14 @@ def always_fail(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]
118118

119119
monkeypatch.setattr(subprocess, "run", always_fail)
120120
assert _get_npx_command() is None
121+
122+
123+
def test_get_npx_returns_none_when_windows_binary_missing(monkeypatch: pytest.MonkeyPatch):
124+
"""Missing executables raise FileNotFoundError before CalledProcessError on Windows."""
125+
monkeypatch.setattr(sys, "platform", "win32", raising=False)
126+
127+
def missing_binary(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]:
128+
raise FileNotFoundError(args[0])
129+
130+
monkeypatch.setattr(subprocess, "run", missing_binary)
131+
assert _get_npx_command() is None

0 commit comments

Comments
 (0)