Skip to content

perf(mcp): non-blocking startup via background MCP discovery - #32811

Closed
Prontsevich wants to merge 3 commits into
NousResearch:mainfrom
Prontsevich:perf/mcp-bg-discovery
Closed

perf(mcp): non-blocking startup via background MCP discovery#32811
Prontsevich wants to merge 3 commits into
NousResearch:mainfrom
Prontsevich:perf/mcp-bg-discovery

Conversation

@Prontsevich

Copy link
Copy Markdown
Contributor

Fire-and-forget MCP server connections on a daemon thread so the gateway / CLI / ACP process becomes interactive immediately.

Previously hermes --tui waited 2-5 s after the splash screen before rendering the UI while discover_mcp_tools() ran synchronously on the critical path.

Changes:

  • tools/mcp_tool.py: new discover_mcp_tools_background() — daemon thread wrapper
  • tui_gateway/entry.py, hermes_cli/main.py, cli.py, acp_adapter/entry.py: use background variant

Related: #29726, #29184, #19326 (closed stale)
Closes: #29726

@alt-glitch alt-glitch added type/perf Performance improvement or optimization tool/mcp MCP client and OAuth comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/acp Agent Communication Protocol adapter P2 Medium — degraded but workaround exists labels May 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related competing PRs for #29726 (MCP startup hang):

Also note #25622 (open) — fixes tool registration race on startup. Background discovery may interact with the same race window if tools aren't registered before the first user message arrives.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating the remaining ACP startup path. Current main still calls discover_mcp_tools() synchronously in acp_adapter/entry.py:255-256, so the ACP premise remains valid; CLI and TUI backgrounding have already landed separately (0c6e133c0, PR #35273).

Problems

  • The ACP change needs the race handling already used by the other surfaces. acp_adapter/session.py:645 constructs AIAgent, whose initialization snapshots tools at agent/agent_init.py:1189-1198. ACP has no wait_for_mcp_discovery at that point and no late refresh for configured MCP servers; acp_adapter/server.py:792-850 refreshes only client-supplied ACP MCP servers. A server that finishes after session creation can therefore remain absent from that session.
  • The proposed test only inspects source text/AST. It does not verify a blocked discovery is non-blocking or that delayed configured tools remain available.

Suggested changes

  • Reuse the shared startup helper with ACP-specific bounded snapshot/late-refresh handling that remains cache-safe before the first turn.
  • Add a behavioral delayed-discovery regression test.

Automated hermes-sweeper review.

Comment thread acp_adapter/entry.py
start_background_mcp_discovery(
logger=logger,
thread_name="acp-mcp-discovery",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pair this background start with ACP-side bounded snapshot/late-refresh handling. ACP constructs AIAgent in acp_adapter/session.py:645, where its tool list is snapshotted; unlike CLI/TUI, this path has no discovery wait or configured-server late refresh, so a slow server can be missing from an already-created ACP session.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
spro-work and others added 3 commits July 13, 2026 21:15
…st path

Fire-and-forget MCP server connections on a daemon thread so the
gateway / CLI / ACP process becomes interactive immediately instead
of blocking on slow remote MCP servers (HTTP timeouts, sluggish
stdio boot).  Previously `hermes --tui` waited 2-5 s after the splash
screen before rendering the UI while `discover_mcp_tools()` ran
synchronously on the critical path.

Changes:
- tools/mcp_tool.py: add `discover_mcp_tools_background()` — thin
  wrapper that spawns `discover_mcp_tools()` on a named daemon thread
- tui_gateway/entry.py: call `discover_mcp_tools_background()` before
  sending gateway.ready (replaces inline call that blocked the JSON-RPC
  pipe for the TUI Ink app)
- hermes_cli/main.py:
  - skip `\_prepare_agent_startup()` for TUI path — plugins, MCP, and
    shell hooks are only needed by the CLI agent loop; the TUI's
    gateway subprocess discovers them independently (~370 ms saved)
  - fast-path in `\_make_tui_argv()`: when `dist/entry.js` exists and
    is fresh, skip npm install / rebuild checks entirely (~350 ms saved)
- cli.py (`\_prepare_deferred_agent_startup`): same background pattern
  for deferred startup (Termux interactive CLI)
- acp_adapter/entry.py: same pattern so ACP server launches asyncio
  immediately while MCP connects in parallel

Result:
- TUI Python wrapper: ~730 ms → ~80 ms (9× faster)
- gateway.ready: ~2700 ms → ~400 ms (7× faster)
- Total TUI cold start: ~3400 ms → ~480 ms

Related: NousResearch#29726, NousResearch#29184, NousResearch#19326 (closed stale)

Closes NousResearch#29726
ACP entry.py fires MCP discovery in a background daemon thread, but
_make_agent snapshots tools once at build and never re-reads the registry.
Unlike CLI/TUI, ACP had no bounded wait before the snapshot and no
late-refresh for configured (config.yaml) MCP servers — a reachable-but-
slow server that finished after agent build was invisible for the whole
session.

Changes:
- acp_adapter/session.py (_make_agent): call wait_for_mcp_discovery()
  before AIAgent construction, bounded by mcp_discovery_timeout (default
  ~1.5s). A dead server can't block; servers that miss the bound are
  picked up by the late-refresh below.
- acp_adapter/server.py (_schedule_mcp_late_refresh): new method on
  HermesACPAgent — if discovery is still in flight after session creation,
  spawns an off-critical-path daemon that joins it (bounded 30s), then
  rebuilds the tool snapshot via the shared refresh_agent_mcp_tools helper.
  Cache-safe: only runs pre-first-turn (_user_turn_count/_api_call_count
  both 0); once the user has sent a message the snapshot is frozen, exactly
  as TUI PR NousResearch#48403 does.
- Called from new_session, load_session, resume_session.
- Mirrors the TUI pattern (tui_gateway _schedule_mcp_late_refresh, PR
  NousResearch#48403) and the CLI pattern (get_tool_definitions → wait_for_mcp_discovery).

Tests:
- Replace the AST-based test (source-text inspection) with three
  behavioral regression tests in tests/acp_adapter/test_acp_mcp_discovery.py:
  1. Blocked discovery does not block startup (non-blocking contract)
  2. Delayed discovery lands tools via late-refresh (pre-first-turn)
  3. Late-refresh is cache-safe: skips rebuild after first turn

Addresses teknium1 review on PR NousResearch#32811.
@Prontsevich
Prontsevich force-pushed the perf/mcp-bg-discovery branch from 4dfad72 to 2bbb0f7 Compare July 13, 2026 18:19
@Prontsevich

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — both points addressed in the latest push (2bbb0f703).

1. Bounded snapshot + late-refresh for configured MCP servers

acp_adapter/session.py (_make_agent): Added wait_for_mcp_discovery() before AIAgent() construction — the same bounded join CLI/TUI use, capped by mcp_discovery_timeout (config.yaml, default ~1.5s). A dead server can't block; fast servers land in the snapshot.

acp_adapter/server.py (_schedule_mcp_late_refresh): New method mirroring TUI PR #48403. If discovery is still in flight after session creation, spawns an off-critical-path daemon that:

  • Joins discovery (bounded 30s)
  • Rebuilds the tool snapshot via the shared refresh_agent_mcp_tools() helper (not a duplicate — same one CLI/TUI/gateway use)
  • Cache-safe: only runs pre-first-turn (_user_turn_count == 0 && _api_call_count == 0). Once the user has sent a message, the snapshot is frozen; late tools require explicit /reload-mcp, exactly as TUI does.

Called from new_session, load_session, resume_session.

2. Behavioral test replacing AST inspection

Removed test_acp_entry_uses_background_mcp_discovery (source-text/AST check). Added three behavioral regression tests in tests/acp_adapter/test_acp_mcp_discovery.py:

Test Verifies
test_acp_background_discovery_does_not_block_startup A blocked discovery (event-locked) returns in <0.2s — non-blocking contract
test_acp_late_refresh_adds_tools_when_discovery_lands_after_build A server that finishes after agent build lands its tools via late-refresh (pre-first-turn)
test_acp_late_refresh_skips_after_first_turn Late-refresh does NOT rebuild once _api_call_count > 0 — cache safety

All 14 tests pass (3 new + 11 existing ACP/mcp_startup).

kshitijk4poor pushed a commit to kshitijk4poor/hermes-agent that referenced this pull request Aug 1, 2026
ACP entry.py fires MCP discovery in a background daemon thread, but
_make_agent snapshots tools once at build and never re-reads the registry.
Unlike CLI/TUI, ACP had no bounded wait before the snapshot and no
late-refresh for configured (config.yaml) MCP servers — a reachable-but-
slow server that finished after agent build was invisible for the whole
session.

Changes:
- acp_adapter/session.py (_make_agent): call wait_for_mcp_discovery()
  before AIAgent construction, bounded by mcp_discovery_timeout (default
  ~1.5s). A dead server can't block; servers that miss the bound are
  picked up by the late-refresh below.
- acp_adapter/server.py (_schedule_mcp_late_refresh): new method on
  HermesACPAgent — if discovery is still in flight after session creation,
  spawns an off-critical-path daemon that joins it (bounded 30s), then
  rebuilds the tool snapshot via the shared refresh_agent_mcp_tools helper.
  Cache-safe: only runs pre-first-turn (_user_turn_count/_api_call_count
  both 0); once the user has sent a message the snapshot is frozen, exactly
  as TUI PR NousResearch#48403 does.
- Called from new_session, load_session, resume_session.
- Mirrors the TUI pattern (tui_gateway _schedule_mcp_late_refresh, PR
  NousResearch#48403) and the CLI pattern (get_tool_definitions → wait_for_mcp_discovery).

Tests:
- Replace the AST-based test (source-text inspection) with three
  behavioral regression tests in tests/acp_adapter/test_acp_mcp_discovery.py:
  1. Blocked discovery does not block startup (non-blocking contract)
  2. Delayed discovery lands tools via late-refresh (pre-first-turn)
  3. Late-refresh is cache-safe: skips rebuild after first turn

Addresses teknium1 review on PR NousResearch#32811.
kshitijk4poor added a commit to kshitijk4poor/hermes-agent that referenced this pull request Aug 1, 2026
…t agent-build wait

Review follow-ups on the NousResearch#32811 salvage:
- Hold state.runtime_lock and bail on is_running so the pre-first-turn
  guard can't race the first prompt dispatch (a refresh publishing
  mid-turn would swap tools= and break the just-created cache prefix).
  Regression test mutation-checked (guard removed -> test fails).
- In-memory-only session lookup in the daemon: get_session() falls
  through to a DB restore that builds a whole new AIAgent just to
  decide no-op (TUI equivalent also checks its in-memory dict only).
- Pass quiet_mode=True explicitly, matching the TUI/gateway callers.
- Use ensure_mcp_discovery_before_agent_build() (landed on main after
  the PR) instead of bare wait_for_mcp_discovery() so the ACP agent
  build is self-sufficient and gets the retry-after-zero-connected
  allowance, matching CLI/one-shot construction sites.
kshitijk4poor pushed a commit that referenced this pull request Aug 1, 2026
ACP entry.py fires MCP discovery in a background daemon thread, but
_make_agent snapshots tools once at build and never re-reads the registry.
Unlike CLI/TUI, ACP had no bounded wait before the snapshot and no
late-refresh for configured (config.yaml) MCP servers — a reachable-but-
slow server that finished after agent build was invisible for the whole
session.

Changes:
- acp_adapter/session.py (_make_agent): call wait_for_mcp_discovery()
  before AIAgent construction, bounded by mcp_discovery_timeout (default
  ~1.5s). A dead server can't block; servers that miss the bound are
  picked up by the late-refresh below.
- acp_adapter/server.py (_schedule_mcp_late_refresh): new method on
  HermesACPAgent — if discovery is still in flight after session creation,
  spawns an off-critical-path daemon that joins it (bounded 30s), then
  rebuilds the tool snapshot via the shared refresh_agent_mcp_tools helper.
  Cache-safe: only runs pre-first-turn (_user_turn_count/_api_call_count
  both 0); once the user has sent a message the snapshot is frozen, exactly
  as TUI PR #48403 does.
- Called from new_session, load_session, resume_session.
- Mirrors the TUI pattern (tui_gateway _schedule_mcp_late_refresh, PR
  #48403) and the CLI pattern (get_tool_definitions → wait_for_mcp_discovery).

Tests:
- Replace the AST-based test (source-text inspection) with three
  behavioral regression tests in tests/acp_adapter/test_acp_mcp_discovery.py:
  1. Blocked discovery does not block startup (non-blocking contract)
  2. Delayed discovery lands tools via late-refresh (pre-first-turn)
  3. Late-refresh is cache-safe: skips rebuild after first turn

Addresses teknium1 review on PR #32811.
kshitijk4poor added a commit that referenced this pull request Aug 1, 2026
…t agent-build wait

Review follow-ups on the #32811 salvage:
- Hold state.runtime_lock and bail on is_running so the pre-first-turn
  guard can't race the first prompt dispatch (a refresh publishing
  mid-turn would swap tools= and break the just-created cache prefix).
  Regression test mutation-checked (guard removed -> test fails).
- In-memory-only session lookup in the daemon: get_session() falls
  through to a DB restore that builds a whole new AIAgent just to
  decide no-op (TUI equivalent also checks its in-memory dict only).
- Pass quiet_mode=True explicitly, matching the TUI/gateway callers.
- Use ensure_mcp_discovery_before_agent_build() (landed on main after
  the PR) instead of bare wait_for_mcp_discovery() so the ACP agent
  build is self-sufficient and gets the retry-after-zero-connected
  allowance, matching CLI/one-shot construction sites.
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Merged via #75985 — your commits were cherry-picked onto current main with authorship preserved (rebase merge), so both land under your name on main:

  • 89f0b63 perf(mcp): non-blocking startup via background MCP discovery + TUI fast path
  • 640de65 fix(acp): add bounded wait + late-refresh for configured MCP servers

Small follow-ups we added on top during review: the late-refresh guard is now serialized with turn start under the session runtime lock (with a mutation-checked regression test), the daemon uses an in-memory-only session lookup, and the docstring was updated to reflect the between-turns prologue refresh on current main.

Thanks for the contribution — and for turning the sweeper feedback around with the bounded wait + behavioral tests.

randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
ACP entry.py fires MCP discovery in a background daemon thread, but
_make_agent snapshots tools once at build and never re-reads the registry.
Unlike CLI/TUI, ACP had no bounded wait before the snapshot and no
late-refresh for configured (config.yaml) MCP servers — a reachable-but-
slow server that finished after agent build was invisible for the whole
session.

Changes:
- acp_adapter/session.py (_make_agent): call wait_for_mcp_discovery()
  before AIAgent construction, bounded by mcp_discovery_timeout (default
  ~1.5s). A dead server can't block; servers that miss the bound are
  picked up by the late-refresh below.
- acp_adapter/server.py (_schedule_mcp_late_refresh): new method on
  HermesACPAgent — if discovery is still in flight after session creation,
  spawns an off-critical-path daemon that joins it (bounded 30s), then
  rebuilds the tool snapshot via the shared refresh_agent_mcp_tools helper.
  Cache-safe: only runs pre-first-turn (_user_turn_count/_api_call_count
  both 0); once the user has sent a message the snapshot is frozen, exactly
  as TUI PR NousResearch#48403 does.
- Called from new_session, load_session, resume_session.
- Mirrors the TUI pattern (tui_gateway _schedule_mcp_late_refresh, PR
  NousResearch#48403) and the CLI pattern (get_tool_definitions → wait_for_mcp_discovery).

Tests:
- Replace the AST-based test (source-text inspection) with three
  behavioral regression tests in tests/acp_adapter/test_acp_mcp_discovery.py:
  1. Blocked discovery does not block startup (non-blocking contract)
  2. Delayed discovery lands tools via late-refresh (pre-first-turn)
  3. Late-refresh is cache-safe: skips rebuild after first turn

Addresses teknium1 review on PR NousResearch#32811.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…t agent-build wait

Review follow-ups on the NousResearch#32811 salvage:
- Hold state.runtime_lock and bail on is_running so the pre-first-turn
  guard can't race the first prompt dispatch (a refresh publishing
  mid-turn would swap tools= and break the just-created cache prefix).
  Regression test mutation-checked (guard removed -> test fails).
- In-memory-only session lookup in the daemon: get_session() falls
  through to a DB restore that builds a whole new AIAgent just to
  decide no-op (TUI equivalent also checks its in-memory dict only).
- Pass quiet_mode=True explicitly, matching the TUI/gateway callers.
- Use ensure_mcp_discovery_before_agent_build() (landed on main after
  the PR) instead of bare wait_for_mcp_discovery() so the ACP agent
  build is self-sufficient and gets the retry-after-zero-connected
  allowance, matching CLI/one-shot construction sites.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/acp Agent Communication Protocol adapter comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/mcp MCP client and OAuth type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants