Skip to content

fix(cli): wait for MCP before first-turn agent builds - #60017

Open
chrishart0 wants to merge 1 commit into
NousResearch:mainfrom
chrishart0:fix/chat-q-mcp-first-turn
Open

fix(cli): wait for MCP before first-turn agent builds#60017
chrishart0 wants to merge 1 commit into
NousResearch:mainfrom
chrishart0:fix/chat-q-mcp-first-turn

Conversation

@chrishart0

@chrishart0 chrishart0 commented Jul 7, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes a first-turn MCP readiness race for non-interactive CLI runs. hermes chat -q and top-level hermes -z / --oneshot can construct AIAgent before configured MCP servers finish registering their dynamic tools. Because the agent snapshots its tool registry at construction time, the first and only model turn can miss native mcp__... tools even when the MCP server itself is healthy and the profile/toolset is configured correctly.

Why this matters

This is not just a missing-tool edge case. chat -q is the natural command for scripted agent runs, evals, batch tests, kanban/worker-style invocations, and other automation where there is no second turn to recover from late MCP discovery. The failure is especially confusing because out-of-band checks can pass: a configured MCP server can test successfully, but the agent still snapshots its tools too early and never sees that server's tools for the run. When MCP tools are absent from that first snapshot, the model may silently degrade into slower or less reliable workarounds such as shell/Python calls, or conclude that required native tools are unavailable. In a real evaluation workflow this caused multiple runs to be invalid, wasted hours of debugging, and burned substantial model tokens before the root cause was visible: chat -q looked like configured chat, but its first-turn tool schema did not reliably include configured MCP tools.

The expected contract is simple: if hermes chat would expose a configured MCP toolset, then hermes chat -q "..." should expose the same tools for its first and only prompt.

Fix layer

The fix adds a small shared helper that idempotently starts the existing background MCP discovery path and bounded-waits at the point of agent construction, then calls it from both CLI agent setup and oneshot. This is slightly stronger than the old wait_for_mcp_discovery() call: wait_for_mcp_discovery() only joins an already-created discovery thread, so it no-ops if a direct/single-query path reaches agent construction before MCP startup created that thread. The new helper makes the construction site self-sufficient: start discovery if needed, then wait up to the existing bound.

Putting the guarantee at the construction site keeps the fix narrow: it composes existing MCP startup primitives, is a no-op if discovery already ran, remains bounded by mcp_discovery_timeout, skips work when no MCP servers are configured, and stays fail-open on errors.

Real-world validation showed that the previous 1.5-second default was too short for an otherwise healthy remote HTTP MCP cold handshake taking roughly 2–5 seconds. The agent still snapshotted its tools before registration completed. This PR therefore raises the default bound to 15 seconds. This is a ceiling, not a fixed sleep: thread.join(timeout) returns immediately when discovery completes, so healthy servers incur only their actual handshake time, while unavailable servers remain capped at 15 seconds.

Related Issue

Fixes #38448

Also covers the hermes chat -q affected path described in the issue discussion.

Type of Change

  • 🐛 Bug fix
  • ✅ Tests

Changes Made

  • hermes_cli/mcp_startup.py: adds ensure_mcp_discovery_before_agent_build(), composing the existing background MCP discovery and bounded wait while preserving OAuth-prompt suppression and fail-open semantics.
  • hermes_cli/cli_agent_setup_mixin.py: calls the helper before CLI AIAgent(...) construction, covering hermes chat -q and the interactive first turn.
  • hermes_cli/oneshot.py: calls the same helper before oneshot AIAgent(...) construction, covering hermes -z / --oneshot.
  • hermes_cli/config.py: raises the bounded discovery default from 1.5 to 15 seconds so ordinary remote MCP cold starts can complete before the first-only tool snapshot.
  • tui_gateway/entry.py and tui_gateway/server.py: keep fallback behavior and inline documentation aligned with the shared default.
  • tests/hermes_cli/test_mcp_startup.py: adds regression coverage for the helper's real bounded discovery path, including OAuth-suppression ordering, plus ordering checks that discovery precedes agent build in both CLI and oneshot paths.
  • MCP refresh/TUI tests cover the new finite default and emergency fallback as behavior contracts rather than pinning an exact literal.

Sibling commands sharing this contract

hermes chat -q, top-level hermes -z / --oneshot, and the interactive first turn all need configured MCP tools available before the first model call. All three now route through the shared helper before AIAgent snapshots tools.

How to Test

scripts/run_tests.sh tests/hermes_cli/test_mcp_startup.py tests/hermes_cli/test_tui_resume_flow.py -q
scripts/run_tests.sh tests/hermes_cli/test_oneshot_usage_file.py tests/agent/test_oneshot.py -q
scripts/run_tests.sh tests/tools/test_refresh_agent_mcp_tools.py tests/test_tui_mcp_late_refresh.py tests/tui_gateway/test_wait_for_mcp_discovery.py tests/tui_gateway/test_mcp_late_refresh_thread_owner.py -q
python -m pytest tests/test_tui_gateway_server.py::test_make_agent_waits_for_shared_mcp_discovery -q -o 'addopts='
git diff --check
python -m compileall hermes_cli/config.py hermes_cli/mcp_startup.py hermes_cli/cli_agent_setup_mixin.py hermes_cli/oneshot.py tui_gateway/entry.py tui_gateway/server.py

Manual end-to-end smoke against a profile with a healthy remote HTTP MCP toolset:

Previous 1.5s bound: first-only run completed with the expected native MCP tool absent and zero MCP tool calls.
15s bound: discovery returned after the server's actual handshake time (roughly 2–5s), and a fresh `hermes chat -q` first turn natively executed `mcp__example__lookup`.
The executor recorded the native tool completion before the final answer.

Verification Output

=== Summary: 8 files, 105 tests passed, 0 failed (100% complete) in 1.5s ===
1 passed in 0.42s

compileall and git diff --check passed. The branch was rebased onto current upstream main before final verification.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard tool/mcp MCP client and OAuth P2 Medium — degraded but workaround exists labels Jul 7, 2026

@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 a real first-turn MCP race. Current main backgrounds CLI MCP discovery at hermes_cli/main.py:12571-12584, but oneshot builds its agent at hermes_cli/oneshot.py:393; the agent snapshots schemas at agent/agent_init.py:1187-1199.

Problems

  • hermes_cli/oneshot.py:389 always joins when any MCP server is configured, even when this invocation cannot expose MCP tools. hermes_cli/tools_config.py:1884-1900 makes no_mcp an explicit opt-out, and explicit non-MCP --toolsets have the same issue. A slow configured server can therefore delay a deliberately MCP-free one-shot.
  • hermes_cli/config.py:1351 changes the shared default to 15s. tui_gateway/server.py:4537-4546 uses that shared wait without an override, extending the change to TUI first-agent startup as well.

Suggested changes

  • Gate the one-shot wait on the effective enabled MCP server selection, including explicit toolsets, enabled: false, and no_mcp; add skip-path coverage.
  • Scope or explicitly validate the global timeout increase. The timeline-linked #61944 has an effective-toolset-gated one-shot approach worth consolidating with.
  • Add a temp-HERMES_HOME real-path regression rather than only mocked ordering tests, consistent with AGENTS.md:84-87.

Automated hermes-sweeper review.

Comment thread hermes_cli/oneshot.py

from hermes_cli.mcp_startup import ensure_mcp_discovery_before_agent_build

ensure_mcp_discovery_before_agent_build(

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.

This joins discovery based only on configured MCP servers, not on toolsets_list. A one-shot with explicit non-MCP toolsets, or a CLI config containing no_mcp, cannot expose MCP schemas but can now wait up to the shared bound. Gate this on the effective enabled MCP-server selection and add skip-path tests.

Comment thread hermes_cli/config.py
# snapshot cannot be used on that only turn. ``thread.join(timeout)`` returns
# as soon as discovery completes, so reachable servers only wait for their
# real handshake time while unavailable servers remain bounded.
"mcp_discovery_timeout": 15.0,

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.

This is a shared timeout: tui_gateway/server.py calls the same resolver before every TUI agent build. Please scope the longer wait to the intended first-only CLI path, or add coverage and rationale for the TUI latency change.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
kshitijk4poor added a commit that referenced this pull request Aug 1, 2026
…teractive sessions

Non-interactive sessions (hermes chat -q, hermes -z) snapshot the tool
registry at AIAgent construction time. If background MCP discovery hasn't
finished, MCP tools are invisible for the entire session — and unlike
interactive mode, there is no between-turns late-binding refresh to recover.

Root cause: wait_for_mcp_discovery() only joins an already-created discovery
thread, so it no-ops if a direct/single-query path reaches agent construction
before MCP startup created that thread. Oneshot._run_agent() didn't call it
at all.

Fix:
- Add ensure_mcp_discovery_before_agent_build() helper to mcp_startup.py:
  idempotently starts discovery if needed + bounded wait. Fail-open on errors.
- Add single_query parameter to _resolve_discovery_timeout/wait_for_mcp_discovery:
  uses mcp_single_query_discovery_timeout (default 15s) instead of the
  interactive mcp_discovery_timeout (1.5s) because one-shot sessions have no
  second turn to recover.
- Wire into CLI _init_agent (single_query from _single_query_mode flag set
  in cli.py's single-query path) and oneshot._run_agent (single_query=True).
- Interactive sessions unchanged: keep 1.5s bound (between-turns refresh covers).

Closes #38448, #51316, #37013, #68137
Composite salvage of #60017 (chrishart0), #51322 (Bartok9), #38620 (buptwz),
#43544 (halonke), #36882 (vanhoof).
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…teractive sessions

Non-interactive sessions (hermes chat -q, hermes -z) snapshot the tool
registry at AIAgent construction time. If background MCP discovery hasn't
finished, MCP tools are invisible for the entire session — and unlike
interactive mode, there is no between-turns late-binding refresh to recover.

Root cause: wait_for_mcp_discovery() only joins an already-created discovery
thread, so it no-ops if a direct/single-query path reaches agent construction
before MCP startup created that thread. Oneshot._run_agent() didn't call it
at all.

Fix:
- Add ensure_mcp_discovery_before_agent_build() helper to mcp_startup.py:
  idempotently starts discovery if needed + bounded wait. Fail-open on errors.
- Add single_query parameter to _resolve_discovery_timeout/wait_for_mcp_discovery:
  uses mcp_single_query_discovery_timeout (default 15s) instead of the
  interactive mcp_discovery_timeout (1.5s) because one-shot sessions have no
  second turn to recover.
- Wire into CLI _init_agent (single_query from _single_query_mode flag set
  in cli.py's single-query path) and oneshot._run_agent (single_query=True).
- Interactive sessions unchanged: keep 1.5s bound (between-turns refresh covers).

Closes NousResearch#38448, NousResearch#51316, NousResearch#37013, NousResearch#68137
Composite salvage of NousResearch#60017 (chrishart0), NousResearch#51322 (Bartok9), NousResearch#38620 (buptwz),
NousResearch#43544 (halonke), NousResearch#36882 (vanhoof).
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…teractive sessions

Non-interactive sessions (hermes chat -q, hermes -z) snapshot the tool
registry at AIAgent construction time. If background MCP discovery hasn't
finished, MCP tools are invisible for the entire session — and unlike
interactive mode, there is no between-turns late-binding refresh to recover.

Root cause: wait_for_mcp_discovery() only joins an already-created discovery
thread, so it no-ops if a direct/single-query path reaches agent construction
before MCP startup created that thread. Oneshot._run_agent() didn't call it
at all.

Fix:
- Add ensure_mcp_discovery_before_agent_build() helper to mcp_startup.py:
  idempotently starts discovery if needed + bounded wait. Fail-open on errors.
- Add single_query parameter to _resolve_discovery_timeout/wait_for_mcp_discovery:
  uses mcp_single_query_discovery_timeout (default 15s) instead of the
  interactive mcp_discovery_timeout (1.5s) because one-shot sessions have no
  second turn to recover.
- Wire into CLI _init_agent (single_query from _single_query_mode flag set
  in cli.py's single-query path) and oneshot._run_agent (single_query=True).
- Interactive sessions unchanged: keep 1.5s bound (between-turns refresh covers).

Closes NousResearch#38448, NousResearch#51316, NousResearch#37013, NousResearch#68137
Composite salvage of NousResearch#60017 (chrishart0), NousResearch#51322 (Bartok9), NousResearch#38620 (buptwz),
NousResearch#43544 (halonke), NousResearch#36882 (vanhoof).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hermes -z can miss MCP tools because oneshot snapshots tools before MCP discovery

3 participants