feat: auto-refresh MCP tools for slow servers via async late-binding - #42802
Closed
397239396 wants to merge 8 commits into
Closed
feat: auto-refresh MCP tools for slow servers via async late-binding#42802397239396 wants to merge 8 commits into
397239396 wants to merge 8 commits into
Conversation
Add HERMES_MCP_DISCOVERY_TIMEOUT environment variable to allow users with slow MCP servers (e.g. lark ~8s, redis ~10s, ssh ~4s) to increase the discovery timeout. Default remains 0.75s for backward compatibility. Both tui_gateway/entry.py and hermes_cli/mcp_startup.py are updated. Resolution order: 1. Explicit timeout argument (callers can override) 2. HERMES_MCP_DISCOVERY_TIMEOUT env var 3. Default 0.75s
…ound thread Instead of blocking startup for 15s waiting for all MCP servers to connect, use a two-phase approach: Phase 1 (startup): Brief 0.75s wait for fast servers, then build agent Phase 2 (background): Spawn a daemon thread that waits for MCP discovery to complete, then auto-refreshes the agent's tools in-place This means: - Startup is instant (~0.75s wait, not 15s) - Agent starts with whatever tools were ready at build time - Slow servers (lark ~8s, redis ~10s, ssh ~4s) auto-merge their tools once they connect, without user intervention - TUI emits updated session.info so the frontend reflects new tools - No need for HERMES_MCP_DISCOVERY_TIMEOUT env var workaround
With the late-binding async refresh mechanism, the env var is no longer needed. Phase 1 always waits 0.75s for fast servers; slow servers are handled by spawn_late_mcp_refresh in the background.
- entry.py: publish discovery thread to mcp_startup module so spawn_late_mcp_refresh can see it - mcp_startup.py: when discovery thread is already finished, do inline refresh check instead of skipping Without these fixes, slow MCP servers (lark/redis/ssh) would never have their tools loaded into the agent.
tui_gateway/server.py had CRLF line endings causing merge conflicts with main. Convert back to LF.
P0 fixes: - Remove dead 'import os' from mcp_startup.py - Add _agent_tools_lock for thread-safe agent.tools updates - Normalize all line endings to LF (was CRLF in 3 files) - Add 6 unit tests for spawn_late_mcp_refresh P1 fixes: - Add on_refreshed callback for CLI path (user feedback) - Guard _mcp_discovery_started = True with is_alive() check P2 fixes: - Reduce discovery wait timeout from 120s to 30s (_LATE_REFRESH_DISCOVERY_TIMEOUT_S) - Move _mcp_late_refresh_thread assignment inside lock - Extract _update_agent_tools helper for consistent tool swap
Author
|
Superseded by #48431 — resolved merge conflicts with latest main and squashed into a clean single commit. |
…efresh - Replaced main's local _schedule_mcp_late_refresh with shared spawn_late_mcp_refresh - Removed duplicate function, using unified mcp_startup.spawn_late_mcp_refresh - All TUI and CLI paths now use the same late-binding mechanism
alt-glitch
added a commit
that referenced
this pull request
Jun 19, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (#48403/#41630/#42802) don't close it.
teknium1
pushed a commit
that referenced
this pull request
Jun 19, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (#48403/#41630/#42802) don't close it.
gnalvesteffer
pushed a commit
to gnalvesteffer/hermes-agent
that referenced
this pull request
Jun 19, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
Contributor
|
Thanks for the MCP late-binding work. An automated hermes-sweeper review found that the requested behavior is already implemented on current
The PR's own |
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…binding) A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting after the agent's one-time tool snapshot was uncallable for the rest of the session. The merged pre-first-turn late-refresh only helps during the dead air before the user's first keystroke; once a turn starts it bails to protect the prompt cache, so a user who types before the server connects never gets the tools without a manual /reload-mcp. Refresh the snapshot in the per-turn prologue (build_turn_context), before this turn's first API call assembles tools=. This is cache-safe by construction: the refresh only ever extends a fresh request prefix at a turn boundary, never mutates the cached prefix of an in-flight turn. So late tools become callable on the user's NEXT turn automatically, with no /reload-mcp and no cache cost. - tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with no MCP servers (the common case) skip the rebuild entirely. - agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the top of the prologue when MCP servers are registered. - tests: 3 contract tests through the real build_turn_context (adds late tool; skipped when no servers; no snapshot churn when unchanged). .hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When the TUI or CLI starts, it waits only 0.75 seconds for all MCP servers to connect. Slow servers like
lark(~8s),redis(~10s), andssh(~4s) miss this window, so their tools never get registered with the agent.Users had to manually
/reload-mcpafter startup — a poor experience.main branch has a partial fix:
_schedule_mcp_late_refresh()intui_gateway/server.py. But it has several problems:Problems with main's
_schedule_mcp_late_refreshTUI-only — lives inside
server.py, CLI has no equivalent. CLI users with slow MCP servers are still stuck.No thread safety — directly mutates
agent.toolsandagent.valid_tool_nameswithout any lock. The main thread reads these during tool iteration; a concurrent write mid-iteration can produce inconsistent state.No thread deduplication — spawns a new daemon thread per session. If multiple sessions are created rapidly, multiple background threads race to update the same agent.
Fragile tool-change detection — compares tool counts (
len(new_defs) == len(agent.tools)), not tool names. If a slow server connects with 5 tools and the agent already has 5 from other servers, the refresh is silently skipped.TUI thread not synced to mcp_startup —
entry.pycreates its own_mcp_discovery_threadbut never publishes it tohermes_cli.mcp_startup._mcp_discovery_thread. Any shared module trying to check "is MCP discovery still running?" getsNone, even though TUI discovery is active. This is a latent bug that blocks any future shared MCP infrastructure.Solution
Shared two-phase async MCP discovery in
hermes_cli/mcp_startup.py:Changes
hermes_cli/mcp_startup.pyspawn_late_mcp_refresh()— shared background thread that waits for_mcp_discovery_event, then merges tools from newly-connected servers into the agent. Thread-safe (_agent_tools_lock), deduplicates threads, detects changes by tool name not count.hermes_cli/cli_agent_setup_mixin.pyspawn_late_mcp_refresh(agent)after CLI agent creation — CLI now also gets late-bound tools.tui_gateway/server.py_schedule_mcp_late_refreshwith sharedspawn_late_mcp_refresh+on_refreshedcallback that emitssession.info. Removes ~60 lines of duplicate TUI-only logic.tui_gateway/entry.py_mcp_discovery_threadtomcp_startup._mcp_discovery_threadso shared code can see TUI discovery state. Without this,spawn_late_mcp_refreshwould skip the background path and only do an inline check.tests/hermes_cli/test_mcp_startup.pytests/test_tui_mcp_late_refresh.pyWhy this is better than main's approach
_schedule_mcp_late_refresh)spawn_late_mcp_refresh)agent.tools_agent_tools_lockguards mutations_mcp_discovery_lock_emiton_refreshedcallbackmcp_startupWhat this replaces
— replaced by shared_schedule_mcp_late_refreshin server.pyspawn_late_mcp_refresh— no longer neededHERMES_MCP_DISCOVERY_TIMEOUTenvironment variableBenefits