Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions tests/hermes_cli/test_dashboard_unified_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,45 @@ def test_reexec_child_does_not_reroute(self, main_mod, monkeypatch):
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",
}
]
18 changes: 18 additions & 0 deletions tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
16 changes: 11 additions & 5 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading