Skip to content

Commit 67f64e0

Browse files
Kirill Turanskiyclaude
andcommitted
fix: correct Windows CLI detection for npm-installed tools
The CLI managers for Claude Code, Gemini CLI, Codex CLI, and Qwen CLI were incorrectly searching for `.exe` files on Windows. These tools are installed via npm which creates `.cmd` wrapper scripts, not `.exe` files. The fix removes the explicit `.exe` extension check because `shutil.which()` automatically handles Windows PATHEXT environment variable, which includes `.CMD`, `.BAT`, `.EXE`, and other executable extensions. Before: `shutil.which("claude.exe")` -> None (not found) After: `shutil.which("claude")` -> finds claude.cmd via PATHEXT Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0eb2bbd commit 67f64e0

5 files changed

Lines changed: 24 additions & 20 deletions

File tree

src/mcpm/clients/managers/claude_code.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def is_client_installed(self) -> bool:
4242
Returns:
4343
bool: True if claude command is available, False otherwise
4444
"""
45-
claude_executable = "claude.exe" if self._system == "Windows" else "claude"
46-
return shutil.which(claude_executable) is not None
45+
# shutil.which() handles Windows PATHEXT automatically (.cmd, .bat, .exe, etc.)
46+
return shutil.which("claude") is not None
4747

4848
def get_client_info(self) -> Dict[str, str]:
4949
"""Get information about this client

src/mcpm/clients/managers/codex_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ def is_client_installed(self) -> bool:
4747
Returns:
4848
bool: True if codex command is available, False otherwise
4949
"""
50-
codex_executable = "codex.exe" if self._system == "Windows" else "codex"
51-
return shutil.which(codex_executable) is not None
50+
# shutil.which() handles Windows PATHEXT automatically (.cmd, .bat, .exe, etc.)
51+
return shutil.which("codex") is not None
5252

5353
def get_client_info(self) -> Dict[str, str]:
5454
"""Get information about this client

src/mcpm/clients/managers/gemini_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def is_client_installed(self) -> bool:
4949
Returns:
5050
bool: True if gemini command is available, False otherwise
5151
"""
52-
gemini_executable = "gemini.exe" if self._system == "Windows" else "gemini"
53-
return shutil.which(gemini_executable) is not None
52+
# shutil.which() handles Windows PATHEXT automatically (.cmd, .bat, .exe, etc.)
53+
return shutil.which("gemini") is not None
5454

5555
def get_client_info(self) -> Dict[str, str]:
5656
"""Get information about this client

src/mcpm/clients/managers/qwen_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def is_client_installed(self) -> bool:
4444
Returns:
4545
bool: True if qwen command is available, False otherwise
4646
"""
47-
qwen_executable = "qwen.exe" if self._system == "Windows" else "qwen"
48-
return shutil.which(qwen_executable) is not None
47+
# shutil.which() handles Windows PATHEXT automatically (.cmd, .bat, .exe, etc.)
48+
return shutil.which("qwen") is not None
4949

5050
def get_client_info(self) -> Dict[str, str]:
5151
"""Get information about this client

tests/test_clients/test_qwen_cli.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,44 +37,48 @@ def test_qwen_cli_manager_get_empty_config():
3737
def test_qwen_cli_manager_is_client_installed():
3838
"""Test QwenCliManager is_client_installed method"""
3939
manager = QwenCliManager()
40-
40+
4141
# Mock shutil.which to return a path (simulating installed client)
4242
with patch("shutil.which", return_value="/usr/local/bin/qwen") as mock_which:
4343
assert manager.is_client_installed() is True
4444
mock_which.assert_called_with("qwen")
45-
45+
4646
# Mock shutil.which to return None (simulating uninstalled client)
4747
with patch("shutil.which", return_value=None) as mock_which:
4848
assert manager.is_client_installed() is False
4949
mock_which.assert_called_with("qwen")
5050

5151

5252
def test_qwen_cli_manager_is_client_installed_windows():
53-
"""Test QwenCliManager is_client_installed method on Windows"""
53+
"""Test QwenCliManager is_client_installed method on Windows
54+
55+
Note: shutil.which() handles Windows PATHEXT automatically, so we always
56+
search for "qwen" without extension. This finds qwen.cmd, qwen.ps1, qwen.exe, etc.
57+
"""
5458
manager = QwenCliManager()
55-
59+
5660
with patch.object(manager, "_system", "Windows"):
57-
# Mock shutil.which to return a path (simulating installed client)
58-
with patch("shutil.which", return_value="C:\\Program Files\\qwen\\qwen.exe") as mock_which:
61+
# Mock shutil.which to return a path (simulating installed client via npm .cmd)
62+
with patch("shutil.which", return_value="C:\\Users\\user\\AppData\\Roaming\\npm\\qwen.cmd") as mock_which:
5963
assert manager.is_client_installed() is True
60-
mock_which.assert_called_with("qwen.exe")
61-
64+
mock_which.assert_called_with("qwen") # No .exe - shutil.which handles PATHEXT
65+
6266
# Mock shutil.which to return None (simulating uninstalled client)
6367
with patch("shutil.which", return_value=None) as mock_which:
6468
assert manager.is_client_installed() is False
65-
mock_which.assert_called_with("qwen.exe")
69+
mock_which.assert_called_with("qwen")
6670

6771

6872
def test_qwen_cli_manager_get_empty_config_structure():
6973
"""Test QwenCliManager _get_empty_config method returns expected structure"""
7074
manager = QwenCliManager()
7175
config = manager._get_empty_config()
72-
76+
7377
# Check that required keys are present
7478
assert "mcpServers" in config
7579
assert "theme" in config
7680
assert "selectedAuthType" in config
77-
81+
7882
# Check default values
7983
assert config["mcpServers"] == {}
8084
assert config["theme"] == "Qwen Dark"
@@ -88,4 +92,4 @@ def test_qwen_cli_manager_get_client_info():
8892
assert info["name"] == "Qwen CLI"
8993
assert info["download_url"] == "https://github.com/QwenLM/qwen-code"
9094
assert info["config_file"] == os.path.expanduser("~/.qwen/settings.json")
91-
assert info["description"] == "Alibaba's Qwen CLI tool"
95+
assert info["description"] == "Alibaba's Qwen CLI tool"

0 commit comments

Comments
 (0)