fix(mcp): autowire on direct hermes CLI path (closes #84)#85
Merged
Conversation
#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.
This was referenced May 24, 2026
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.
This was referenced May 24, 2026
Closed
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.
Closes #84.
Bug
After #83 merged, fresh
hermes --provider X --model Yinvocations leaveconfig.yamlwithoutmcp_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 intui_gateway/entry.py. That file is only on the path when the TUI gateway subprocess spawns (interactive TUI shell active). Directhermes …CLI invocations land inhermes_cli/main.py:mainper[project.scripts] hermes = "hermes_cli.main:main"— never touchtui_gateway/entry.py— so the autowire never fires.Fix
Adds the autowire call inside
hermes_cli/main.py's CLI startup block, betweendiscover_plugins()anddiscover_mcp_tools()(around line 12756). The block is gated onargs.command in {None, "chat", "acp", "rl"}— covers bare invocation, chat, acp, rl. Order matters: autowire must run beforediscover_mcp_toolsso the freshly-written entry is visible on the same boot (theload_configmtime-cache auto-invalidates onsave_config).The #83
tui_gateway/entry.pycall 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.pycontainsensure_internal_mcp_server()at allfrom tools.mcp_tool import discover_mcp_toolsline (ordering regression catcher)_AGENT_COMMANDS-gated block (not outside, where it would fire forhermes 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 pastdiscover_mcp_tools" — both caught at the source level.Test plan
hermes --provider X --model Y(no operator-sidemcp_serversconfig)~/.hermes/profiles/<profile>/config.yamlcontainsmcp_servers.hermes-internalgrafted_context_fetchwithout operator wiringHERMES_DISABLE_INTERNAL_MCP=1, verify NO entry is written🤖 Generated with Claude Code