diff --git a/src/mcpm/clients/managers/goose.py b/src/mcpm/clients/managers/goose.py index 65492c2b..feef6e23 100644 --- a/src/mcpm/clients/managers/goose.py +++ b/src/mcpm/clients/managers/goose.py @@ -9,7 +9,7 @@ from pydantic import TypeAdapter from mcpm.clients.base import YAMLClientManager -from mcpm.core.schema import ServerConfig, STDIOServerConfig +from mcpm.core.schema import CustomServerConfig, ServerConfig, STDIOServerConfig logger = logging.getLogger(__name__) @@ -144,6 +144,8 @@ def _normalize_server_config(self, server_config: Dict[str, Any]) -> Dict[str, A normalized = dict(server_config) # Map Goose-specific fields to standard fields + if normalized.get("type") == "builtin": + return {"config": normalized} if "cmd" in normalized: normalized["command"] = normalized.pop("cmd") if "envs" in normalized: @@ -172,6 +174,9 @@ def to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]: non_empty_env = server_config.get_filtered_env_vars(os.environ) if non_empty_env: result["envs"] = non_empty_env + elif isinstance(server_config, CustomServerConfig): + result = dict(server_config.config) + result["type"] = "builtin" else: result = server_config.to_dict() result["type"] = "sse" diff --git a/src/mcpm/commands/profile.py b/src/mcpm/commands/profile.py index 2352f66d..2d259bf4 100644 --- a/src/mcpm/commands/profile.py +++ b/src/mcpm/commands/profile.py @@ -3,7 +3,7 @@ from rich.table import Table from mcpm.clients.client_registry import ClientRegistry -from mcpm.core.schema import STDIOServerConfig +from mcpm.core.schema import CustomServerConfig, STDIOServerConfig from mcpm.profile.profile_config import ProfileConfigManager from mcpm.utils.config import ConfigManager @@ -41,6 +41,8 @@ def list(verbose=False): for config in configs: if isinstance(config, STDIOServerConfig): details.append(f"{config.name}: {config.command} {' '.join(config.args)}") + elif isinstance(config, CustomServerConfig): + details.append(f"{config.name}: Custom") else: details.append(f"{config.name}: {config.url}") row.append("\n".join(details)) diff --git a/src/mcpm/core/schema.py b/src/mcpm/core/schema.py index 0d153dbb..ae6d056b 100644 --- a/src/mcpm/core/schema.py +++ b/src/mcpm/core/schema.py @@ -75,7 +75,11 @@ def to_mcp_proxy_stdio(self) -> STDIOServerConfig: ) -ServerConfig = Union[STDIOServerConfig, RemoteServerConfig] +class CustomServerConfig(BaseServerConfig): + config: Dict[str, Any] + + +ServerConfig = Union[STDIOServerConfig, RemoteServerConfig, CustomServerConfig] class Profile(BaseModel): diff --git a/src/mcpm/router/client_connection.py b/src/mcpm/router/client_connection.py index 16fa7732..9ddfaf2a 100644 --- a/src/mcpm/router/client_connection.py +++ b/src/mcpm/router/client_connection.py @@ -49,6 +49,8 @@ def _transport_context_factory(self, server_config: ServerConfig): return _stdio_transport_context(server_config, self._errlog) elif isinstance(server_config, RemoteServerConfig): return _streamable_http_transport_context(server_config) + else: + raise ValueError(f"Custom server config {server_config.name} is not supported for router proxy") def healthy(self) -> bool: return self.session is not None and self._initialized diff --git a/src/mcpm/utils/display.py b/src/mcpm/utils/display.py index c95e9b4c..e609b718 100644 --- a/src/mcpm/utils/display.py +++ b/src/mcpm/utils/display.py @@ -2,11 +2,13 @@ Utility functions for displaying MCP server configurations """ +import json + from rich.console import Console from rich.markup import escape from rich.table import Table -from mcpm.core.schema import RemoteServerConfig, ServerConfig +from mcpm.core.schema import CustomServerConfig, RemoteServerConfig, ServerConfig from mcpm.utils.scope import CLIENT_PREFIX, PROFILE_PREFIX console = Console() @@ -34,6 +36,13 @@ def print_server_config(server_config: ServerConfig, is_stashed=False): console.print(f' [bold blue]{key}[/] = [green]"{value}"[/]') console.print(" " + "-" * 50) return + if isinstance(server_config, CustomServerConfig): + console.print(" Type: [green]Custom[/]") + console.print(" " + "-" * 50) + console.print(" Config:") + console.print(json.dumps(server_config.config, indent=2)) + console.print(" " + "-" * 50) + return command = server_config.command console.print(f" Command: [green]{command}[/]") diff --git a/tests/test_clients/test_goose.py b/tests/test_clients/test_goose.py new file mode 100644 index 00000000..262fec7c --- /dev/null +++ b/tests/test_clients/test_goose.py @@ -0,0 +1,87 @@ +import os +import tempfile +from unittest.mock import patch + +import pytest +from ruamel.yaml import YAML + +from mcpm.clients.managers.goose import GooseClientManager + + +@pytest.fixture +def temp_yml_config(): + with tempfile.NamedTemporaryFile(delete=False, suffix=".yaml") as f: + # Built in extention + config = { + "computercontroller": { + "bundled": True, + "display_name": "Computer Controller", + "enabled": False, + "name": "computercontroller", + "timeout": 300, + "type": "builtin", + } + } + YAML().dump({"extensions": config}, f) + temp_path = f.name + + yield temp_path + # Clean up + os.unlink(temp_path) + + +@pytest.fixture +def goose_manager(temp_yml_config): + return GooseClientManager(config_path=temp_yml_config) + + +def test_list_servers(goose_manager): + servers = goose_manager.list_servers() + assert "computercontroller" in servers + + +def test_get_server(goose_manager): + server = goose_manager.get_server("computercontroller") + assert server is not None + assert server.name == "computercontroller" + + +def test_server_operation(goose_manager): + # builtin extension + success = goose_manager.add_server({"name": "test-server", "type": "builtin", "enabled": True}, "test-server") + assert success + + server = goose_manager.get_server("test-server") + assert server is not None + assert server.name == "test-server" + + # stdio extension + success = goose_manager.add_server( + { + "name": "stdio-server", + "type": "stdio", + "enabled": True, + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-test"], + }, + "stdio-server", + ) + assert success + + server = goose_manager.get_server("stdio-server") + assert server is not None + assert server.name == "stdio-server" + + # remove server + success = goose_manager.remove_server("test-server") + assert success + + assert goose_manager.get_server("test-server") is None + + +def test_is_client_installed(goose_manager): + with patch("os.path.isdir", return_value=True): + assert goose_manager.is_client_installed() + + with patch("os.path.isdir", return_value=False): + assert not goose_manager.is_client_installed() diff --git a/tests/test_windsurf.py b/tests/test_clients/test_windsurf.py similarity index 100% rename from tests/test_windsurf.py rename to tests/test_clients/test_windsurf.py