Skip to content
Closed
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
19 changes: 19 additions & 0 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ async def _lifespan(app: "FastAPI"):
app.state.event_channels = {} # dict[str, set]
app.state.event_lock = asyncio.Lock()

# Dashboard-hosted chat sessions run on this process's in-memory
# tui_gateway (the chat PTY attaches via HERMES_TUI_GATEWAY_URL instead
# of spawning its own gateway), so tui_gateway.entry's startup MCP
# discovery never runs here. Without this, configured ``mcp_servers``
# contribute zero tools to dashboard/desktop sessions until a manual
# ``/reload-mcp`` — the enabled-toolset list includes the server names,
# but the registry has nothing to hand out, so they're silently dropped.
# Backgrounded + config-gated by the shared helper, so dashboards with
# no MCP servers configured pay neither the import nor a thread.
try:
from hermes_cli.mcp_startup import start_background_mcp_discovery

start_background_mcp_discovery(
logger=_log,
thread_name="dashboard-mcp-discovery",
)
except Exception:
_log.debug("MCP tool discovery failed at dashboard startup", exc_info=True)

# Desktop-spawned backends (HERMES_DESKTOP=1) fire cron jobs themselves,
# since the app has no gateway running the scheduler. Server `hermes
# dashboard` is unaffected — it relies on its own gateway.
Expand Down
53 changes: 53 additions & 0 deletions tests/hermes_cli/test_mcp_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,59 @@ def test_cli_get_tool_definitions_briefly_waits_for_fast_mcp_thread(monkeypatch)
assert not thread.is_alive()


def test_web_server_lifespan_starts_background_mcp_discovery(monkeypatch):
"""Dashboard startup must kick off MCP discovery for in-memory gateway
sessions — without it, configured MCP servers contribute zero tools to
dashboard/desktop chats until a manual /reload-mcp."""
discovered = threading.Event()

monkeypatch.setattr(
mcp_startup,
"_has_configured_mcp_servers",
lambda: True,
)
monkeypatch.setitem(
sys.modules,
"tools.mcp_tool",
types.SimpleNamespace(discover_mcp_tools=discovered.set),
)

from fastapi.testclient import TestClient

from hermes_cli import web_server

with TestClient(web_server.app):
assert discovered.wait(timeout=2.0)
assert mcp_startup._mcp_discovery_started is True


def test_web_server_lifespan_skips_mcp_discovery_without_servers(monkeypatch):
calls = {"mcp": 0}

monkeypatch.setattr(
mcp_startup,
"_has_configured_mcp_servers",
lambda: False,
)
monkeypatch.setitem(
sys.modules,
"tools.mcp_tool",
types.SimpleNamespace(
discover_mcp_tools=lambda: calls.__setitem__("mcp", calls["mcp"] + 1)
),
)

from fastapi.testclient import TestClient

from hermes_cli import web_server

with TestClient(web_server.app):
pass

assert calls["mcp"] == 0
assert mcp_startup._mcp_discovery_thread is None


def test_init_agent_waits_for_mcp_discovery_before_agent_build(monkeypatch):
waited = {"done": False}

Expand Down
13 changes: 13 additions & 0 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2825,12 +2825,25 @@ def _make_agent(
# 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).
#
# Two possible owners for that thread: the standalone PTY gateway starts
# it in tui_gateway.entry; a dashboard-hosted in-memory gateway starts it
# via hermes_cli.mcp_startup in the web server's lifespan. Join whichever
# exists — each wait is a no-op when its thread was never started.
try:
from tui_gateway.entry import wait_for_mcp_discovery

wait_for_mcp_discovery()
except Exception:
pass
try:
from hermes_cli.mcp_startup import (
wait_for_mcp_discovery as _wait_shared_mcp_discovery,
)

_wait_shared_mcp_discovery()
except Exception:
pass

cfg = _load_cfg()
agent_cfg = cfg.get("agent") or {}
Expand Down