diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 4911de19a7c..c1d6d01f89f 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -10431,6 +10431,26 @@ def cmd_dashboard(args): # the missing-provider state if it matters. print(f"⚠ Plugin discovery failed: {exc}", file=sys.stderr) + # Desktop chat uses the dashboard's in-process /api/ws gateway, which builds + # agents via tui_gateway.server._make_agent. That path only snapshots the + # tool registry — it never starts MCP discovery (the stdio TUI does that in + # tui_gateway/entry.py, which the dashboard process doesn't run). Without + # this, a profile's configured MCP servers never connect, so desktop + # sessions show no MCP tools. Spawn discovery in the background here so a + # slow/dead server can't block dashboard startup. + try: + from hermes_cli.mcp_startup import start_background_mcp_discovery + + start_background_mcp_discovery( + logger=logger, + thread_name="dashboard-mcp-discovery", + ) + except Exception: + logger.debug( + "Background MCP tool discovery failed at dashboard startup", + exc_info=True, + ) + from hermes_cli.web_server import start_server # The in-browser Chat tab (the embedded TUI over PTY/WebSocket) is always diff --git a/tests/hermes_cli/test_dashboard_unified_launch.py b/tests/hermes_cli/test_dashboard_unified_launch.py index 232d7a4a394..3a14894316d 100644 --- a/tests/hermes_cli/test_dashboard_unified_launch.py +++ b/tests/hermes_cli/test_dashboard_unified_launch.py @@ -128,3 +128,45 @@ class TestUnifiedDashboardRouting: with pytest.raises((SystemExit, AttributeError, ImportError, TypeError)): main_mod.cmd_dashboard(_args(open_profile="worker_x")) assert execs == [] + + def test_dashboard_starts_mcp_discovery_for_ws_backend(self, main_mod, monkeypatch): + """The dashboard process serves the /api/ws gateway but never runs + tui_gateway/entry.py, so it must kick off MCP discovery itself or + desktop sessions never see a profile's MCP tools.""" + monkeypatch.setattr( + "hermes_cli.profiles.get_active_profile_name", lambda: "default" + ) + monkeypatch.delenv("HERMES_WEB_DIST", raising=False) + monkeypatch.setattr(main_mod, "_sync_bundled_skills_quietly", lambda: None) + monkeypatch.setattr(main_mod, "_build_web_ui", lambda *_a, **_k: True) + monkeypatch.setitem(sys.modules, "fastapi", types.SimpleNamespace()) + monkeypatch.setitem(sys.modules, "uvicorn", types.SimpleNamespace()) + monkeypatch.setitem( + sys.modules, + "hermes_logging", + types.SimpleNamespace(setup_logging=lambda **_k: None), + ) + monkeypatch.setitem( + sys.modules, + "hermes_cli.plugins", + types.SimpleNamespace(discover_plugins=lambda: None), + ) + calls = [] + monkeypatch.setattr( + "hermes_cli.mcp_startup.start_background_mcp_discovery", + lambda **kwargs: calls.append(kwargs), + ) + monkeypatch.setitem( + sys.modules, + "hermes_cli.web_server", + types.SimpleNamespace(start_server=lambda **_kwargs: None), + ) + + main_mod.cmd_dashboard(_args()) + + assert calls == [ + { + "logger": main_mod.logger, + "thread_name": "dashboard-mcp-discovery", + } + ] diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index c510c4ef230..ea5d20aac4a 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -6096,6 +6096,24 @@ def test_make_agent_reads_nested_max_turns(monkeypatch): assert mock_agent.call_args.kwargs["max_iterations"] == 200 +def test_make_agent_waits_for_shared_mcp_discovery(monkeypatch): + _setup_make_agent_mocks(monkeypatch, {}) + waited = [] + + from hermes_cli import mcp_startup + + monkeypatch.setattr( + mcp_startup, + "wait_for_mcp_discovery", + lambda timeout=0.75: waited.append(timeout), + ) + + with patch("run_agent.AIAgent"): + server._make_agent("sid1", "key1") + + assert waited == [0.75] + + def test_make_agent_nested_max_turns_takes_priority(monkeypatch): _setup_make_agent_mocks( monkeypatch, {"agent": {"max_turns": 500}, "max_turns": 100} diff --git a/tui_gateway/server.py b/tui_gateway/server.py index d932e98510f..e4eb010d425 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3072,11 +3072,17 @@ def _make_agent( from hermes_cli.runtime_provider import resolve_runtime_provider # MCP tool discovery runs in a background daemon thread at startup so a - # dead server can't freeze the shell (see tui_gateway/entry.py). The agent - # snapshots its tool list once here and never re-reads it, so briefly wait - # for in-flight discovery to land before building — bounded, so a slow/dead - # server still can't block. No-op once discovery has finished (every build - # after the first during a slow startup). + # dead server can't freeze the shell. The agent snapshots its tool list + # once here and never re-reads it, so briefly wait for in-flight discovery + # to land before building — bounded, so a slow/dead server still can't + # block. Dashboard /api/ws uses hermes_cli.mcp_startup; TUI stdio keeps + # its existing tui_gateway.entry-owned thread. + try: + from hermes_cli.mcp_startup import wait_for_mcp_discovery + + wait_for_mcp_discovery() + except Exception: + pass try: from tui_gateway.entry import wait_for_mcp_discovery