From 66d337dfd29c6cdc3c4b7f0ddb6255f791b4e717 Mon Sep 17 00:00:00 2001 From: Maksym Stavytskyi Date: Wed, 24 Jun 2026 13:43:09 +0200 Subject: [PATCH] fix(tui): discover profile MCP before desktop agent snapshot --- tests/tui_gateway/test_make_agent_provider.py | 90 +++++++++++++++++++ tui_gateway/server.py | 21 +++++ 2 files changed, 111 insertions(+) diff --git a/tests/tui_gateway/test_make_agent_provider.py b/tests/tui_gateway/test_make_agent_provider.py index 94b606dbd385f..a42571c74e196 100644 --- a/tests/tui_gateway/test_make_agent_provider.py +++ b/tests/tui_gateway/test_make_agent_provider.py @@ -264,6 +264,96 @@ def test_probe_config_health_flags_null_personalities_with_active_personality(): assert "agent.personalities" in msg +def test_make_agent_discovers_profile_mcp_before_tool_snapshot(): + """Dashboard profile sessions must discover that profile's MCP tools before + constructing AIAgent. + + The dashboard process can be launched from one HERMES_HOME while the user + opens a Desktop chat under another profile. Startup MCP discovery only covers + the launch profile; without this per-session discovery, AIAgent snapshots an + empty/non-MCP registry for the selected profile and the model never receives + those MCP tool schemas. + """ + + fake_runtime = { + "provider": "openrouter", + "base_url": "https://api.synthetic.new/v1", + "api_key": "sk-test", + "api_mode": "chat_completions", + "command": None, + "args": None, + "credential_pool": None, + } + fake_cfg = { + "agent": {"system_prompt": ""}, + "model": {"default": "glm-5"}, + "mcp_servers": {"profile-mcp": {"enabled": True, "url": "http://localhost/mcp"}}, + } + + call_order = [] + + def record_discover(): + call_order.append("discover") + return ["mcp_profile_mcp_ping"] + + def record_agent(*args, **kwargs): + call_order.append("agent") + return MagicMock() + + with ( + patch("tui_gateway.server._load_cfg", return_value=fake_cfg), + patch("tui_gateway.server._get_db", return_value=MagicMock()), + patch("tui_gateway.server._load_reasoning_config", return_value=None), + patch("tui_gateway.server._load_service_tier", return_value=None), + patch("tui_gateway.server._load_enabled_toolsets", return_value=["profile-mcp"]), + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value=fake_runtime, + ), + patch("tools.mcp_tool.discover_mcp_tools", side_effect=record_discover) as mock_discover, + patch("run_agent.AIAgent", side_effect=record_agent) as mock_agent, + ): + from tui_gateway.server import _make_agent + + _make_agent("sid-profile-mcp", "key-profile-mcp") + + mock_discover.assert_called_once() + assert call_order == ["discover", "agent"] + assert mock_agent.call_args.kwargs["enabled_toolsets"] == ["profile-mcp"] + + +def test_make_agent_skips_profile_mcp_discovery_when_unconfigured(): + fake_runtime = { + "provider": "openrouter", + "base_url": "https://api.synthetic.new/v1", + "api_key": "sk-test", + "api_mode": "chat_completions", + "command": None, + "args": None, + "credential_pool": None, + } + fake_cfg = {"agent": {"system_prompt": ""}, "model": {"default": "glm-5"}} + + with ( + patch("tui_gateway.server._load_cfg", return_value=fake_cfg), + patch("tui_gateway.server._get_db", return_value=MagicMock()), + patch("tui_gateway.server._load_reasoning_config", return_value=None), + patch("tui_gateway.server._load_service_tier", return_value=None), + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value=fake_runtime, + ), + patch("tools.mcp_tool.discover_mcp_tools") as mock_discover, + patch("run_agent.AIAgent") as mock_agent, + ): + from tui_gateway.server import _make_agent + + _make_agent("sid-no-mcp", "key-no-mcp") + + mock_discover.assert_not_called() + assert mock_agent.called + + def test_make_agent_tolerates_null_config_sections(): """Bare `agent:` / `display:` keys in ~/.hermes/config.yaml parse as None. cfg.get("agent", {}) returns None (default only fires on missing diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 93ad41a2dd4c0..3fa77b3989587 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3853,6 +3853,27 @@ def _make_agent( pass cfg = _load_cfg() + + # Desktop dashboard sessions can be created for a profile that is different + # from the dashboard process' launch HERMES_HOME. The dashboard startup MCP + # discovery thread only sees the launch profile, so a newly-created profile + # session (for example `epam`) can reach AIAgent construction before that + # profile's MCP servers have ever been discovered in this process. AIAgent + # snapshots tools once in __init__, so waiting for the launch-profile thread + # is not sufficient: discover the *currently bound* profile's MCP servers + # here, while the caller's profile HERMES_HOME override is active, before the + # tool snapshot is built. + try: + raw_mcp = cfg.get("mcp_servers") if isinstance(cfg, dict) else None + if isinstance(raw_mcp, dict) and raw_mcp: + from tools.mcp_tool import discover_mcp_tools + + discover_mcp_tools() + except Exception: + logger.warning( + "Profile MCP tool discovery failed before agent snapshot", + exc_info=True, + ) agent_cfg = cfg.get("agent") or {} system_prompt = _prompt_text(agent_cfg.get("system_prompt", "")) startup_skills = _parse_tui_skills_env()