From 7b3c6d126c97b9d886a779527b85a36ebf248e1c Mon Sep 17 00:00:00 2001 From: RYABOV Evgeny Date: Tue, 30 Sep 2025 18:15:24 +0300 Subject: [PATCH] feat: add Qwen CLI client manager support --- QWEN.md | 1 + src/mcpm/clients/client_registry.py | 2 + src/mcpm/clients/managers/__init__.py | 2 + src/mcpm/clients/managers/qwen_cli.py | 61 ++++++++++++++++++ tests/test_clients/test_qwen_cli.py | 91 +++++++++++++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 QWEN.md create mode 100644 src/mcpm/clients/managers/qwen_cli.py create mode 100644 tests/test_clients/test_qwen_cli.py diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 0000000..9c70040 --- /dev/null +++ b/QWEN.md @@ -0,0 +1 @@ +Reference `CLAUDE.md` for project conventions. \ No newline at end of file diff --git a/src/mcpm/clients/client_registry.py b/src/mcpm/clients/client_registry.py index b583331..48bcd2d 100644 --- a/src/mcpm/clients/client_registry.py +++ b/src/mcpm/clients/client_registry.py @@ -19,6 +19,7 @@ from mcpm.clients.managers.fiveire import FiveireManager from mcpm.clients.managers.gemini_cli import GeminiCliManager from mcpm.clients.managers.goose import GooseClientManager +from mcpm.clients.managers.qwen_cli import QwenCliManager from mcpm.clients.managers.trae import TraeManager from mcpm.clients.managers.vscode import VSCodeManager from mcpm.clients.managers.windsurf import WindsurfManager @@ -50,6 +51,7 @@ class ClientRegistry: "vscode": VSCodeManager, "gemini-cli": GeminiCliManager, "codex-cli": CodexCliManager, + "qwen-cli": QwenCliManager, } @classmethod diff --git a/src/mcpm/clients/managers/__init__.py b/src/mcpm/clients/managers/__init__.py index 789081c..5968fcb 100644 --- a/src/mcpm/clients/managers/__init__.py +++ b/src/mcpm/clients/managers/__init__.py @@ -13,6 +13,7 @@ from mcpm.clients.managers.fiveire import FiveireManager from mcpm.clients.managers.gemini_cli import GeminiCliManager from mcpm.clients.managers.goose import GooseClientManager +from mcpm.clients.managers.qwen_cli import QwenCliManager from mcpm.clients.managers.trae import TraeManager from mcpm.clients.managers.vscode import VSCodeManager from mcpm.clients.managers.windsurf import WindsurfManager @@ -26,6 +27,7 @@ "ContinueManager", "FiveireManager", "GooseClientManager", + "QwenCliManager", "TraeManager", "VSCodeManager", "GeminiCliManager", diff --git a/src/mcpm/clients/managers/qwen_cli.py b/src/mcpm/clients/managers/qwen_cli.py new file mode 100644 index 0000000..64c0974 --- /dev/null +++ b/src/mcpm/clients/managers/qwen_cli.py @@ -0,0 +1,61 @@ +""" +Qwen CLI integration utilities for MCP +""" + +import logging +import os +import shutil +from typing import Any, Dict + +from mcpm.clients.base import JSONClientManager + +logger = logging.getLogger(__name__) + + +class QwenCliManager(JSONClientManager): + """Manages Qwen CLI MCP server configurations""" + + # Client information + client_key = "qwen-cli" + display_name = "Qwen CLI" + download_url = "https://github.com/QwenLM/qwen-code" + + def __init__(self, config_path_override: str | None = None): + """Initialize the Qwen CLI client manager + + Args: + config_path_override: Optional path to override the default config file location + """ + # Qwen CLI stores its settings in ~/.qwen/settings.json + self.config_path = os.path.expanduser("~/.qwen/settings.json") + super().__init__(config_path_override=config_path_override) + + def _get_empty_config(self) -> Dict[str, Any]: + """Get empty config structure for Qwen CLI""" + return { + "mcpServers": {}, + # Include other default settings that Qwen CLI expects + "theme": "Qwen Dark", + "selectedAuthType": "openai", + } + + def is_client_installed(self) -> bool: + """Check if Qwen CLI is installed + Returns: + bool: True if qwen command is available, False otherwise + """ + qwen_executable = "qwen.exe" if self._system == "Windows" else "qwen" + return shutil.which(qwen_executable) is not None + + def get_client_info(self) -> Dict[str, str]: + """Get information about this client + + Returns: + Dict: Information about the client including display name, download URL, and config path + """ + return { + "name": self.display_name, + "download_url": self.download_url, + "config_file": self.config_path, + "description": "Alibaba's Qwen CLI tool", + } diff --git a/tests/test_clients/test_qwen_cli.py b/tests/test_clients/test_qwen_cli.py new file mode 100644 index 0000000..9d2c4ca --- /dev/null +++ b/tests/test_clients/test_qwen_cli.py @@ -0,0 +1,91 @@ +""" +Test for Qwen CLI manager +""" + +import os +import tempfile +from unittest.mock import patch + +from mcpm.clients.managers.qwen_cli import QwenCliManager + + +def test_qwen_cli_manager_initialization(): + """Test QwenCliManager initialization""" + # Test with default config path + manager = QwenCliManager() + assert manager.client_key == "qwen-cli" + assert manager.display_name == "Qwen CLI" + assert manager.download_url == "https://github.com/QwenLM/qwen-code" + assert manager.config_path == os.path.expanduser("~/.qwen/settings.json") + + # Test with custom config path + custom_path = "/tmp/custom_settings.json" + manager = QwenCliManager(config_path_override=custom_path) + assert manager.config_path == custom_path + + +def test_qwen_cli_manager_get_empty_config(): + """Test QwenCliManager _get_empty_config method""" + manager = QwenCliManager() + config = manager._get_empty_config() + assert "mcpServers" in config + assert "theme" in config + assert "selectedAuthType" in config + assert config["mcpServers"] == {} + + +def test_qwen_cli_manager_is_client_installed(): + """Test QwenCliManager is_client_installed method""" + manager = QwenCliManager() + + # Mock shutil.which to return a path (simulating installed client) + with patch("shutil.which", return_value="/usr/local/bin/qwen") as mock_which: + assert manager.is_client_installed() is True + mock_which.assert_called_with("qwen") + + # Mock shutil.which to return None (simulating uninstalled client) + with patch("shutil.which", return_value=None) as mock_which: + assert manager.is_client_installed() is False + mock_which.assert_called_with("qwen") + + +def test_qwen_cli_manager_is_client_installed_windows(): + """Test QwenCliManager is_client_installed method on Windows""" + manager = QwenCliManager() + + with patch.object(manager, "_system", "Windows"): + # Mock shutil.which to return a path (simulating installed client) + with patch("shutil.which", return_value="C:\\Program Files\\qwen\\qwen.exe") as mock_which: + assert manager.is_client_installed() is True + mock_which.assert_called_with("qwen.exe") + + # Mock shutil.which to return None (simulating uninstalled client) + with patch("shutil.which", return_value=None) as mock_which: + assert manager.is_client_installed() is False + mock_which.assert_called_with("qwen.exe") + + +def test_qwen_cli_manager_get_empty_config_structure(): + """Test QwenCliManager _get_empty_config method returns expected structure""" + manager = QwenCliManager() + config = manager._get_empty_config() + + # Check that required keys are present + assert "mcpServers" in config + assert "theme" in config + assert "selectedAuthType" in config + + # Check default values + assert config["mcpServers"] == {} + assert config["theme"] == "Qwen Dark" + assert config["selectedAuthType"] == "openai" + + +def test_qwen_cli_manager_get_client_info(): + """Test QwenCliManager get_client_info method""" + manager = QwenCliManager() + info = manager.get_client_info() + assert info["name"] == "Qwen CLI" + assert info["download_url"] == "https://github.com/QwenLM/qwen-code" + assert info["config_file"] == os.path.expanduser("~/.qwen/settings.json") + assert info["description"] == "Alibaba's Qwen CLI tool" \ No newline at end of file