Skip to content

fix(agent): repair MCP tool names with dropped mcp__server__ prefix - #61336

Closed
yingliang-zhang wants to merge 1 commit into
NousResearch:mainfrom
yingliang-zhang:fix/mcp-tool-name-prefix-repair
Closed

fix(agent): repair MCP tool names with dropped mcp__server__ prefix#61336
yingliang-zhang wants to merge 1 commit into
NousResearch:mainfrom
yingliang-zhang:fix/mcp-tool-name-prefix-repair

Conversation

@yingliang-zhang

Copy link
Copy Markdown
Contributor

What

Models (notably GLM) sometimes emit MCP tool names without the full mcp__<server>__ prefix — e.g. list_memory_projects instead of mcp__basic_memory__list_memory_projects. The existing _repair_tool_call pipeline (case-folding, CamelCase, suffix stripping, fuzzy match at 0.7 cutoff) cannot map the bare suffix back to the full prefixed name. The call fails with "Unknown tool", the agent burns 3 retries, then aborts with Model generated invalid tool call: list_memory_projects.

Root cause

After #52750 (merged 2026-07-05) adopted the mcp__server__tool double-underscore naming convention, the repair pipeline has no strategy to bridge a bare tool suffix to its full mcp__server__tool registered name. The fuzzy matcher scores list_memory_projects vs mcp__basic_memory__list_memory_projects well below the 0.7 cutoff.

Prior PRs #21696, #37100, #33359 all target the old single-underscore mcp_ format and do not apply to the new convention. #21696 also modifies run_agent.py (the pre-refactor location of _repair_tool_call), which would conflict with the current agent/agent_runtime_helpers.py location.

Fix

Add two repair paths in repair_tool_call() before the fuzzy-match fallback:

Case 1 — bare suffix (no __ in emitted name): scan registered mcp__ tools and match on the tool-suffix portion (parts[2] after splitting on __). Skip when ambiguous (multiple servers expose the same suffix) to avoid non-deterministic routing.

Case 2 — partial prefix (__ in emitted name but missing mcp__): prepend mcp__ and check for an exact match. Handles basic_memory__list_memory_projectsmcp__basic_memory__list_memory_projects.

Both paths only resolve on exact matches — they cannot mis-route non-MCP tools.

How to reproduce

  1. Configure any MCP server (e.g. basic_memory) with tools registered as mcp__basic_memory__list_memory_projects
  2. Use a model that drops the prefix (observed with GLM-5.2-heavy, also reported with other models in fix(agent): repair mcp_ prefix drop in _repair_tool_call #21696)
  3. Model calls list_memory_projectsTool 'list_memory_projects' does not exist → 3 retries → session aborts

Tests

9 new tests in TestMcpPrefixRepair:

Test Covers
test_bare_suffix_matches_mcp_tool Case 1: list_memory_projects → full name
test_bare_suffix_different_server Case 1 with different server
test_bare_suffix_with_underscores_in_name Case 1 with multi-word suffix
test_non_mcp_tool_unaffected Non-MCP tools still work
test_unknown_bare_name_returns_none Unknown bare name → None
test_partial_prefix_prepends_mcp Case 2: server__toolmcp__server__tool
test_partial_prefix_different_server Case 2 with different server
test_partial_prefix_unknown_returns_none Unknown partial prefix → None
test_ambiguous_bare_suffix_skips_repair Two servers, same suffix → None (no guess)

All 38 tests in test_repair_tool_call_name.py pass.

scripts/run_tests.sh tests/run_agent/test_repair_tool_call_name.py

Related

Platform

Tested on macOS 26.5 / Python 3.11.

@yingliang-zhang
yingliang-zhang requested a review from a team July 9, 2026 07:11
Models (notably GLM) sometimes emit MCP tool names without the full
mcp__<server>__ prefix — e.g. list_memory_projects instead of
mcp__basic_memory__list_memory_projects. The existing repair pipeline
(case-folding, CamelCase, suffix stripping, fuzzy match at 0.7 cutoff)
cannot map the bare suffix back to the full prefixed name, so the call
fails with "Unknown tool" and the agent burns 3 retries before aborting
with "Model generated invalid tool call".

Add two repair paths in repair_tool_call() before the fuzzy-match
fallback:

1. Bare suffix (no __ in emitted name): scan registered mcp__ tools
   and match on the tool-suffix portion. Skip when ambiguous (multiple
   servers expose the same suffix) to avoid non-deterministic routing.

2. Partial prefix (__ in emitted name but missing mcp__): prepend
   mcp__ and check for an exact match.

The mcp__server__tool naming convention was introduced in NousResearch#52750
(merged 2026-07-05). Prior PRs NousResearch#21696, NousResearch#37100, NousResearch#33359 target the old
single-underscore mcp_ format and do not apply to the new convention.

9 new tests in TestMcpPrefixRepair covering both cases, ambiguity
guard, and non-MCP tool passthrough. All 38 tests in
test_repair_tool_call_name.py pass.
@yingliang-zhang
yingliang-zhang force-pushed the fix/mcp-tool-name-prefix-repair branch from 9849848 to c464de1 Compare July 9, 2026 07:18
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/mcp MCP client and OAuth P3 Low — cosmetic, nice to have labels Jul 9, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused investigation and for covering the ambiguity case.

This automated hermes-sweeper review is closing this under the standing model-output-repair policy: Hermes does not add new reconstruction passes for incorrect model output. This PR's two new branches infer a registered MCP name from a bare or partial model-emitted tool name.

  • tools/mcp_tool.py:4466-4484 defines the fully qualified mcp__<server>__<tool> registry contract.
  • agent/conversation_loop.py:4431-4448 already validates unknown names and returns the error to the model for correction.
  • The PR diff at c464de11730b07358768b630a5c966d05098119c adds the prohibited name-reconstruction behavior before that correction path.

Please re-scope any follow-up to the model/provider or MCP schema/prompt source that emits the incorrect name rather than adding another core output-repair path.


Closed as not-planned per standing maintainer policy (model-output-repair). This is a design-direction decision, not a code-quality judgment — see the Contribution Rubric in AGENTS.md for what the project is looking for. If you believe this policy was misapplied to your change, comment here and a maintainer will take a look.

@teknium1 teknium1 closed this Jul 10, 2026
@teknium1 teknium1 added the sweeper:not-planned Sweeper: closed per standing maintainer policy (design direction) label Jul 10, 2026
@yingliang-zhang

Copy link
Copy Markdown
Contributor Author

Hi @teknium1, I'd like to request a second look on this. I believe the model-output-repair policy was misapplied here — this PR extends an existing repair function, not adds a new reconstruction pass.

The existing repair_tool_call() already does name reconstruction. It lives in agent/agent_runtime_helpers.py:2330 and already performs:

  1. Case-folding (lowercase direct match)
  2. Hyphen/space → underscore normalization
  3. CamelCase → snake_case conversion
  4. Trailing _tool / -tool / tool suffix stripping (e.g. TodoTool_tooltodo)
  5. VolcEngine XML fragment trimming (issue [Bug] VolcEngine api/plan endpoint pollutes tool_use.name with XML attribute fragments, breaking all tool calls #33007)
  6. Fuzzy match at 0.7 cutoff (difflib)

All of these are model-output-repair by the same definition. They all normalize/reconstruct incorrect model emissions before the error-retry path in conversation_loop.py:4431-4448. The sweeper says "adds the prohibited name-reconstruction behavior before that correction path" — but steps 1-6 above all run before that correction path, by design. The repair function exists specifically to avoid burning 3 API retries on trivially fixable name variants.

This PR adds two patterns to the same function, same mechanism:

  • Bare suffix: list_memory_projectsmcp__basic_memory__list_memory_projects (model dropped entire mcp__server__ prefix)
  • Partial prefix: basic_memory__list_memory_projectsmcp__basic_memory__list_memory_projects (model dropped mcp__ only)

These are structurally identical to steps 3-4 (CamelCase conversion, suffix stripping) — they normalize a known model emission pattern back to the registered name. No new function, no new code path, no new architectural surface. Just two additional if blocks inside the existing repair pipeline, with an ambiguity guard that returns None when multiple MCP servers expose the same bare name.

Why the model/provider-level fix is insufficient here: The root cause is that models like GLM-5.2 sometimes drop the mcp__ prefix when emitting tool calls — likely because the prefix is long (mcp__basic_memory__list_memory_projects = 42 chars) and the model truncates. This is a model behavior that cannot be fixed at the MCP schema/prompt level: the schema already sends the full mcp__<server>__<tool> name in the tool definition, but the model still emits the truncated form. The error-retry path (sending "Unknown tool" back to the model) sometimes works, but costs 3 wasted API calls + retries. The repair function exists precisely for this class of problem.

Summary: This PR is a natural extension of the existing repair_tool_call() repair pipeline — same function, same mechanism, same architectural position. It does not create a new reconstruction pass or a new correction path. If the existing steps 1-6 are acceptable (and they are — they shipped in the original repair_tool_call), then steps 7-8 (MCP prefix repair) follow the same precedent. I believe the sweeper correctly identified the code pattern but misclassified it as a new pass rather than an extension of an existing one.

Happy to re-scope or adjust if there are concerns I'm missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:not-planned Sweeper: closed per standing maintainer policy (design direction) 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.

3 participants