Skip to content

fix(mcp): autowire on direct hermes CLI path (closes #84)#85

Merged
PowerCreek merged 1 commit into
mainfrom
issue-82-mcp-autowire-cli-path
May 24, 2026
Merged

fix(mcp): autowire on direct hermes CLI path (closes #84)#85
PowerCreek merged 1 commit into
mainfrom
issue-82-mcp-autowire-cli-path

Conversation

@PowerCreek

Copy link
Copy Markdown

Closes #84.

Bug

After #83 merged, fresh hermes --provider X --model Y invocations leave config.yaml without mcp_servers.hermes-internal. Worker boots with zero G2/G3/G4 MCP tools — the same symptom #82 was supposed to fix.

Root cause

#83 placed the ensure_internal_mcp_server() call in tui_gateway/entry.py. That file is only on the path when the TUI gateway subprocess spawns (interactive TUI shell active). Direct hermes … CLI invocations land in hermes_cli/main.py:main per [project.scripts] hermes = "hermes_cli.main:main" — never touch tui_gateway/entry.py — so the autowire never fires.

Fix

Adds the autowire call inside hermes_cli/main.py's CLI startup block, between discover_plugins() and discover_mcp_tools() (around line 12756). The block is gated on args.command in {None, "chat", "acp", "rl"} — covers bare invocation, chat, acp, rl. Order matters: autowire must run before discover_mcp_tools so the freshly-written entry is visible on the same boot (the load_config mtime-cache auto-invalidates on save_config).

The #83 tui_gateway/entry.py call stays — correct for the TUI-gateway subprocess path. Idempotent: calling both is safe.

Tests (tests/test_autowire_cli_path.py — 3 cases, all pass)

  • hermes_cli/main.py contains ensure_internal_mcp_server() at all
  • The call appears BEFORE the from tools.mcp_tool import discover_mcp_tools line (ordering regression catcher)
  • The call sits INSIDE the _AGENT_COMMANDS-gated block (not outside, where it would fire for hermes mcp add / hermes hooks list / introspection commands too)

Total: 23 autowire tests pass (3 new + 20 from #83 still green).

Why source-level tests

Exercising the full CLI main() requires mocking argparse, provider auth, model resolution — orders of magnitude more setup than the bug warrants. The regression risk is "someone deletes the call" or "someone reorders past discover_mcp_tools" — both caught at the source level.

Test plan

  • After merge, rebuild worker container
  • Run fresh hermes --provider X --model Y (no operator-side mcp_servers config)
  • Verify ~/.hermes/profiles/<profile>/config.yaml contains mcp_servers.hermes-internal
  • Verify worker can invoke grafted_context_fetch without operator wiring
  • With HERMES_DISABLE_INTERNAL_MCP=1, verify NO entry is written

🤖 Generated with Claude Code

#83 placed ensure_internal_mcp_server() in tui_gateway/entry.py — but
that file is only on the path for the TUI-gateway subprocess. Direct
`hermes --provider X --model Y` invocations land in hermes_cli/main.py
(per [project.scripts] hermes = "hermes_cli.main:main") and skip the
autowire entirely, leaving config.yaml without hermes-internal and
workers without G2/G3/G4 MCP tools.

This adds the same autowire call inside hermes_cli/main.py's CLI
startup block, between discover_plugins() and discover_mcp_tools().
The block is gated on args.command in {None, chat, acp, rl} so it
fires for the bare invocation and all agent-running subcommands but
NOT for management commands (`hermes mcp add`, `hermes hooks list`,
etc. — those don't need the autowire).

Ordering: autowire must run BEFORE discover_mcp_tools so the freshly-
written hermes-internal entry is visible in the same boot. The
load_config mtime-cache in hermes_cli/config.py auto-invalidates on
save_config, so the second read sees the new entry.

The #83 tui_gateway/entry.py call stays — it's correct for the TUI
path. Idempotent: calling both is safe.

Tests (tests/test_autowire_cli_path.py — 3 cases, all pass):
- hermes_cli/main.py contains the call at all
- Call appears BEFORE the discover_mcp_tools import (ordering)
- Call is INSIDE the _AGENT_COMMANDS-gated block (no fire for
  management commands)

The existing 20-case suite from #83 still passes — 23 total green.
@PowerCreek PowerCreek merged commit 7064ce8 into main May 24, 2026
@PowerCreek PowerCreek deleted the issue-82-mcp-autowire-cli-path branch May 24, 2026 07:53
PowerCreek added a commit that referenced this pull request May 24, 2026
…loses #86) (#87)

Post-#82/#83/#85 the autowire surfaces ~36 MCP tools from
hermes-internal, pushing fresh workers to 52 total tools and into the
tool-paralysis ceiling (text responses with affirmation pattern, no
tool_call emission for verticals that should be one-shot).

#75 (HERMES_TOOLS_SUBSET) was supposed to let operators narrow the
surface per worker, but it had three gaps for MCP tools:

1. Subset filtering only ran in agent_init.py:838 (built-in tool path)
2. cli.py:9790 (/reload-mcp + auto-reload on config change) re-assigns
   agent.tools without re-applying the filter — regression risk where
   any post-init MCP server reload nukes the subset
3. Even when filtering at agent.tools, the registry still carried the
   unwanted MCP tools, costing schema-conversion + collision-check
   work per tool per boot

Fix: apply the allow-list at MCP tool registration in
tools/mcp_tool.py::_register_server_tools. Excluded tools never enter
the registry, so both initial discovery AND /reload-mcp paths honor
the subset uniformly + the registry stays clean.

Three edits:

1. hermes_cli/tool_subset.py (new) — shared helpers `get_subset_allow()`
   + `is_tool_allowed()`. Single source of truth for env parsing +
   allow-list semantics so the two call sites (agent_init + mcp_tool)
   can't drift in casing/whitespace/empty-vs-missing handling.

2. tools/mcp_tool.py::_register_server_tools — read the subset once
   per server registration (O(1) check per tool, not O(N) env parse),
   apply inside both the per-tool loop AND the utility-tools loop
   (list_resources/read_resource/list_prompts/get_prompt).

3. agent/agent_init.py — refactored the inline #75 filter to use the
   shared helper. Behavior unchanged; dedupes parsing logic.

Subset compares against the **prefixed** MCP name
(`mcp_<server>_<tool>`) — exact match, predictable behavior. Fuzzy /
unprefixed matching is a separate feature request.

Tests (tests/test_mcp_subset_filter.py — 14 cases, all pass):
- get_subset_allow: unset/empty/whitespace-only/single/multi/padded/
  MCP-prefixed parse cases
- is_tool_allowed: None passes through, exact match, exclusion, no
  substring matching, MCP prefixed vs unprefixed (contract test)
- Source-level: _register_server_tools imports the shared helper +
  calls it inside main loop
- Source-level: same call inside the utility-tools loop (regression
  catcher for "fix the main loop, forget utility tools")
- Source-level: agent_init.py uses shared helper, no stale inline
  parser

37 total green (14 new + 20 from #83 + 3 from #85).

Final layer of the post-#67 cascade. After this lands + container
rebuild, workers can be configured with HERMES_TOOLS_SUBSET to a 5-tool
surface that includes their specific MCP needs (e.g.
"doc_view,mcp_hermes-internal_grafted_context_fetch,..."), out of
tool-paralysis range.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Autowire from #83 doesn't fire on direct hermes CLI invocation

1 participant