From 46be5ca18290ca87a91e94c1b9ec95b8a06e8e9d Mon Sep 17 00:00:00 2001 From: Sak3th2004 Date: Mon, 12 Oct 2026 14:18:08 +0530 Subject: [PATCH] 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. --- src/mcp/cli/cli.py | 11 ++++++----- tests/cli/test_utils.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 203034c6ef..b45b90974a 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -6,7 +6,7 @@ import subprocess import sys from pathlib import Path -from typing import Annotated, Any +from typing import Annotated from mcp.server import MCPServer from mcp.server import Server as LowLevelServer @@ -39,7 +39,7 @@ ) -def _get_npx_command(): +def _get_npx_command() -> str | None: """Get the correct npx command for the current platform.""" if sys.platform == "win32": # Try both npx.cmd and npx.exe on Windows @@ -47,7 +47,8 @@ def _get_npx_command(): try: subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True) return cmd - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): + # Missing executables raise FileNotFoundError before CalledProcessError. continue return None return "npx" # On Unix-like systems, just use npx @@ -116,7 +117,7 @@ def _parse_file_path(file_spec: str) -> tuple[Path, str | None]: return file_path, server_object -def _import_server(file: Path, server_object: str | None = None): # pragma: no cover +def _import_server(file: Path, server_object: str | None = None) -> MCPServer: # pragma: no cover """Import an MCP server from a file. Args: @@ -140,7 +141,7 @@ def _import_server(file: Path, server_object: str | None = None): # pragma: no module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - def _check_server_object(server_object: Any, object_name: str): + def _check_server_object(server_object: object, object_name: str) -> bool: """Helper function to check that the server object is supported Args: diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 309e5035cd..167640b089 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -118,3 +118,14 @@ def always_fail(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes] monkeypatch.setattr(subprocess, "run", always_fail) assert _get_npx_command() is None + + +def test_get_npx_returns_none_when_windows_binary_missing(monkeypatch: pytest.MonkeyPatch): + """Missing executables raise FileNotFoundError before CalledProcessError on Windows.""" + monkeypatch.setattr(sys, "platform", "win32", raising=False) + + def missing_binary(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]: + raise FileNotFoundError(args[0]) + + monkeypatch.setattr(subprocess, "run", missing_binary) + assert _get_npx_command() is None