4155ea97e8
`cmd_dashboard` started the background MCP discovery thread before importing `hermes_cli.web_server`. The thread's first act is the ~350ms `mcp` SDK import, which holds the GIL against the main thread's own web_server import, so the HERMES_BACKEND_READY sentinel — and every renderer paint behind it — moved ~300ms later on every Desktop cold start with any MCP server configured. Desktop `serve` (headless + HERMES_DESKTOP=1) now arms discovery one second after the sentinel instead. Starting it AT the bind was measured to give back most of the gain (the renderer's WebSocket connect + first hydration reads contend on the same loop). An agent build inside that window pulls the deferred start forward itself via `wait_for_mcp_discovery`, so the bounded join and the late-binding tool refresh behave exactly as before. Dashboard and non-Desktop `serve` keep the eager pre-import ordering. Minimal reimplementation of the MCP-deferral slice of #96751 by @helix4u; the plugin-route deferral / 503 middleware / cron-after-bind slices were measured at ~0-10ms each and are not taken. Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""Desktop `serve` starts background MCP discovery only after the socket binds.
|
|
|
|
The MCP SDK import (~350ms) used to run on a thread started BEFORE
|
|
web_server was imported, holding the GIL against the main thread's own
|
|
import path and delaying the READY sentinel the Desktop waits on.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import threading
|
|
|
|
import hermes_cli.mcp_startup as mcp_startup
|
|
import hermes_cli.web_server as web_server
|
|
from tests.hermes_cli.test_dashboard_auth_gate import _stub_uvicorn_run
|
|
|
|
|
|
def _reset_discovery_state(monkeypatch):
|
|
monkeypatch.setattr(mcp_startup, "_mcp_discovery_started", False)
|
|
monkeypatch.setattr(mcp_startup, "_mcp_discovery_thread", None)
|
|
monkeypatch.setattr(mcp_startup, "_mcp_discovery_deferred", None)
|
|
|
|
|
|
def test_desktop_serve_arms_mcp_discovery_only_after_ready_sentinel(monkeypatch):
|
|
_reset_discovery_state(monkeypatch)
|
|
order: list[str] = []
|
|
monkeypatch.setattr(
|
|
mcp_startup,
|
|
"start_background_mcp_discovery",
|
|
lambda *, logger, thread_name: order.append("discovery:" + thread_name),
|
|
)
|
|
monkeypatch.setattr(web_server, "_write_machine_sentinel_line", lambda line: order.append("sentinel"))
|
|
_stub_uvicorn_run(monkeypatch)
|
|
|
|
web_server.start_server(
|
|
host="127.0.0.1", port=0, open_browser=False, headless=True,
|
|
start_mcp_discovery_after_bind=True,
|
|
)
|
|
timer = mcp_startup._mcp_discovery_deferred
|
|
assert order == ["sentinel"] and isinstance(timer, threading.Timer)
|
|
timer.cancel()
|
|
# An agent build inside the delay window pulls discovery forward itself.
|
|
mcp_startup.wait_for_mcp_discovery(timeout=0)
|
|
assert order == ["sentinel", "discovery:dashboard-mcp-discovery"]
|
|
assert mcp_startup._mcp_discovery_deferred is None
|
|
|
|
# Without the flag (dashboard / non-Desktop serve) start_server does not
|
|
# start discovery itself — cmd_dashboard's pre-import path still owns it.
|
|
order.clear()
|
|
_reset_discovery_state(monkeypatch)
|
|
web_server.start_server(host="127.0.0.1", port=0, open_browser=False, headless=True)
|
|
assert order == ["sentinel"] and mcp_startup._mcp_discovery_deferred is None
|
|
|
|
|
|
def test_deferred_discovery_fires_once_and_is_idempotent(monkeypatch):
|
|
_reset_discovery_state(monkeypatch)
|
|
calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
mcp_startup,
|
|
"start_background_mcp_discovery",
|
|
lambda *, logger, thread_name: calls.append(thread_name),
|
|
)
|
|
log = logging.getLogger("test")
|
|
mcp_startup.defer_background_mcp_discovery(logger=log, thread_name="t", delay=60)
|
|
mcp_startup.defer_background_mcp_discovery(logger=log, thread_name="t", delay=60) # second arm is a no-op
|
|
first = mcp_startup._mcp_discovery_deferred
|
|
mcp_startup._start_deferred_mcp_discovery_now()
|
|
mcp_startup._start_deferred_mcp_discovery_now()
|
|
assert calls == ["t"]
|
|
assert first is not None and mcp_startup._mcp_discovery_deferred is None
|