diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index f12a4b39722..4946a7c8fa6 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -354,9 +354,24 @@ def _get_platform_tools(config: dict, platform: str) -> Set[str]: def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[str]): - """Save the selected toolset keys for a platform to config.""" + """Save the selected toolset keys for a platform to config. + + Preserve dynamic MCP toolsets (``mcp-``) that were already set for + the platform. These entries are not exposed in the tools configurator UI, + but they should survive when the user changes the configurable toolsets. + """ config.setdefault("platform_toolsets", {}) - config["platform_toolsets"][platform] = sorted(enabled_toolset_keys) + + existing_toolsets = config.get("platform_toolsets", {}).get(platform, []) + if not isinstance(existing_toolsets, list): + existing_toolsets = [] + + preserved_entries = { + entry for entry in existing_toolsets + if isinstance(entry, str) and entry.startswith("mcp-") + } + + config["platform_toolsets"][platform] = sorted(enabled_toolset_keys | preserved_entries) save_config(config) diff --git a/tests/hermes_cli/test_tools_config.py b/tests/hermes_cli/test_tools_config.py index 92e1e60c30c..441181c14c7 100644 --- a/tests/hermes_cli/test_tools_config.py +++ b/tests/hermes_cli/test_tools_config.py @@ -1,6 +1,13 @@ """Tests for hermes_cli.tools_config platform tool persistence.""" -from hermes_cli.tools_config import _get_platform_tools, _platform_toolset_summary, _toolset_has_keys +from unittest.mock import patch + +from hermes_cli.tools_config import ( + _get_platform_tools, + _platform_toolset_summary, + _save_platform_tools, + _toolset_has_keys, +) def test_get_platform_tools_uses_default_when_platform_not_configured(): @@ -31,7 +38,7 @@ def test_platform_toolset_summary_uses_explicit_platform_list(): def test_toolset_has_keys_for_vision_accepts_codex_auth(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) (tmp_path / "auth.json").write_text( - '{"active_provider":"openai-codex","providers":{"openai-codex":{"tokens":{"access_token":"codex-access-token","refresh_token":"codex-refresh-token"}}}}' + '{"active_provider":"openai-codex","providers":{"openai-codex":{"tokens":{"access_token": "codex-...oken","refresh_token": "codex-...oken"}}}}' ) monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) monkeypatch.delenv("OPENAI_BASE_URL", raising=False) @@ -40,3 +47,70 @@ def test_toolset_has_keys_for_vision_accepts_codex_auth(tmp_path, monkeypatch): monkeypatch.delenv("CONTEXT_VISION_PROVIDER", raising=False) assert _toolset_has_keys("vision") is True + + +def test_save_platform_tools_preserves_dynamic_mcp_toolsets(): + """Dynamic MCP toolsets should survive platform tool reconfiguration.""" + config = { + "platform_toolsets": { + "cli": ["web", "terminal", "mcp-time", "mcp-github", "mcp-custom-server"] + } + } + + new_selection = {"web", "browser"} + + with patch("hermes_cli.tools_config.save_config"): + _save_platform_tools(config, "cli", new_selection) + + saved_toolsets = config["platform_toolsets"]["cli"] + + assert "mcp-time" in saved_toolsets + assert "mcp-github" in saved_toolsets + assert "mcp-custom-server" in saved_toolsets + assert "web" in saved_toolsets + assert "browser" in saved_toolsets + assert "terminal" not in saved_toolsets + + +def test_save_platform_tools_does_not_preserve_platform_presets(): + config = { + "platform_toolsets": { + "cli": ["hermes-cli", "mcp-time"] + } + } + + with patch("hermes_cli.tools_config.save_config"): + _save_platform_tools(config, "cli", {"web"}) + + saved_toolsets = config["platform_toolsets"]["cli"] + + assert "web" in saved_toolsets + assert "mcp-time" in saved_toolsets + assert "hermes-cli" not in saved_toolsets + + +def test_save_platform_tools_handles_empty_existing_config(): + config = {} + + with patch("hermes_cli.tools_config.save_config"): + _save_platform_tools(config, "telegram", {"web", "terminal"}) + + saved_toolsets = config["platform_toolsets"]["telegram"] + + assert "web" in saved_toolsets + assert "terminal" in saved_toolsets + + +def test_save_platform_tools_handles_invalid_existing_config(): + config = { + "platform_toolsets": { + "cli": "invalid-string-value" + } + } + + with patch("hermes_cli.tools_config.save_config"): + _save_platform_tools(config, "cli", {"web"}) + + saved_toolsets = config["platform_toolsets"]["cli"] + + assert saved_toolsets == ["web"]