fix(desktop): discover MCP tools for dashboard /api/ws backends (#44512)

The desktop chat surface talks to the dashboard's in-process /api/ws
gateway, which builds agents through tui_gateway.server._make_agent. That
path only snapshots the existing tool registry — MCP discovery is started
by tui_gateway/entry.py (the stdio TUI), which the dashboard process never
runs. So a profile's configured MCP servers never connect under the
desktop app and sessions show no MCP tools.

Start a shared background MCP discovery thread at dashboard startup (via
hermes_cli.mcp_startup, bounded so a slow/dead server can't block boot),
and have _make_agent briefly join that thread in addition to the existing
entry-owned TUI thread before snapshotting tools.

Carved out of #44478.

Co-authored-by: AJ <yspdev@gmail.com>
This commit is contained in:
brooklyn! 2026-06-11 17:45:45 -05:00 committed by GitHub
parent 2ee69d0579
commit 73969771a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 91 additions and 5 deletions

View file

@ -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

View file

@ -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",
}
]

View file

@ -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}

View file

@ -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