From c09e0f50bea67e8bfe00bf947647322b0914b842 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 12 Mar 2026 15:33:37 -0400 Subject: [PATCH] feat(app_controller): Integrate MCP configuration loading and add tests --- src/app_controller.py | 13 ++++ tests/test_app_controller_mcp.py | 106 +++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 tests/test_app_controller_mcp.py diff --git a/src/app_controller.py b/src/app_controller.py index 6915f5e..0201e11 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -197,6 +197,7 @@ class AppController: self._pending_dialog_open: bool = False self._pending_actions: Dict[str, ConfirmDialog] = {} self._pending_ask_dialog: bool = False + self.mcp_config: models.MCPConfiguration = models.MCPConfiguration() # AI settings state self._current_provider: str = "gemini" self._current_model: str = "gemini-2.5-flash-lite" @@ -894,6 +895,18 @@ class AppController: self.tool_presets = self.tool_preset_manager.load_all_presets() self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles() + mcp_path = self.project.get('project', {}).get('mcp_config_path') or self.config.get('ai', {}).get('mcp_config_path') + if mcp_path: + mcp_p = Path(mcp_path) + if not mcp_p.is_absolute() and self.active_project_path: + mcp_p = Path(self.active_project_path).parent / mcp_path + if mcp_p.exists(): + self.mcp_config = models.load_mcp_config(str(mcp_p)) + else: + self.mcp_config = models.MCPConfiguration() + else: + self.mcp_config = models.MCPConfiguration() + from src.personas import PersonaManager self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None) self.personas = self.persona_manager.load_all() diff --git a/tests/test_app_controller_mcp.py b/tests/test_app_controller_mcp.py new file mode 100644 index 0000000..1651bd9 --- /dev/null +++ b/tests/test_app_controller_mcp.py @@ -0,0 +1,106 @@ +import os +import json +import pytest +from pathlib import Path +from src.app_controller import AppController +from src import models + +@pytest.fixture +def controller(tmp_path): + # Setup mock config and project files + config_path = tmp_path / "config.toml" + project_path = tmp_path / "project.toml" + mcp_config_path = tmp_path / "mcp_config.json" + + config_data = { + "ai": { + "mcp_config_path": str(mcp_config_path) + }, + "projects": { + "paths": [str(project_path)], + "active": str(project_path) + } + } + + project_data = { + "project": { + "name": "test-project", + "mcp_config_path": "project_mcp.json" # Relative path + } + } + + mcp_data = { + "mcpServers": { + "global-server": {"command": "echo"} + } + } + + project_mcp_data = { + "mcpServers": { + "project-server": {"command": "echo"} + } + } + + # We can't easily use models.save_config because it uses a hardcoded path + # But AppController.init_state calls models.load_config() which uses CONFIG_PATH + + return AppController() + +def test_app_controller_mcp_loading(tmp_path, monkeypatch): + # Mock CONFIG_PATH to point to our temp config + config_file = tmp_path / "config.toml" + monkeypatch.setattr(models, "CONFIG_PATH", str(config_file)) + + mcp_global_file = tmp_path / "mcp_global.json" + mcp_global_file.write_text(json.dumps({"mcpServers": {"global": {"command": "echo"}}})) + + config_content = f""" +[ai] +mcp_config_path = "{mcp_global_file.as_posix()}" +[projects] +paths = [] +active = "" +""" + config_file.write_text(config_content) + + ctrl = AppController() + # Mock _load_active_project to not do anything for now + monkeypatch.setattr(ctrl, "_load_active_project", lambda: None) + ctrl.project = {} + + ctrl.init_state() + + assert "global" in ctrl.mcp_config.mcpServers + assert ctrl.mcp_config.mcpServers["global"].command == "echo" + +def test_app_controller_mcp_project_override(tmp_path, monkeypatch): + config_file = tmp_path / "config.toml" + monkeypatch.setattr(models, "CONFIG_PATH", str(config_file)) + + project_file = tmp_path / "project.toml" + mcp_project_file = tmp_path / "mcp_project.json" + mcp_project_file.write_text(json.dumps({"mcpServers": {"project": {"command": "echo"}}})) + + config_content = f""" +[ai] +mcp_config_path = "non-existent.json" +[projects] +paths = ["{project_file.as_posix()}"] +active = "{project_file.as_posix()}" +""" + config_file.write_text(config_content) + + ctrl = AppController() + ctrl.active_project_path = str(project_file) + ctrl.project = { + "project": { + "mcp_config_path": "mcp_project.json" + } + } + # Mock _load_active_project to keep our manual project dict + monkeypatch.setattr(ctrl, "_load_active_project", lambda: None) + + ctrl.init_state() + + assert "project" in ctrl.mcp_config.mcpServers + assert "non-existent" not in ctrl.mcp_config.mcpServers