Skip to content

Commit 140d6e4

Browse files
authored
fix(router): honor MCP aliases in excluded tools (#1822) (#1863)
## Description Normalize MCP tool-name aliases in the shared exclusion matcher so Anthropic/custom-agent names like `mcp_Server_tool` match the documented `mcp__*` glob and bare tool exclusions such as `headroom_retrieve`. Closes #1822 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added MCP alias matching for `mcp__server__tool`, `mcp_Server_tool`, and the bare wrapped tool name. - Added Anthropic `tool_use` / `tool_result` regressions for custom-agent MCP names and bare `headroom_retrieve` exclusions. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/python -m pytest tests/test_transforms/test_content_router.py -q 57 passed, 1 warning in 0.95s $ .venv/bin/python -m ruff check . All checks passed! $ .venv/bin/python -m ruff format --check . 1058 files already formatted $ .venv/bin/python -m mypy headroom --ignore-missing-imports Success: no issues found in 407 source files ``` ## Real Behavior Proof - Environment: macOS, Python 3.13.5 local venv with editable headroom build. - Exact command / steps: Added #1822 regressions, ran the focused tests before the fix, then reran after adding MCP aliases. - Observed result: Before the fix, custom-agent MCP tool results were compressed instead of excluded; after the fix, the full content-router test file passes and excluded MCP results stay on the lossless excluded path. - Not tested: Full repository test suite locally; GitHub CI passed the full PR matrix. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review - [x] Principal engineer agent approved - [x] Senior developer agent approved ## Checklist - [x] My code follows the project style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes Review agents approved the scoped MCP exclusion-alias fix. One non-blocking review note: #1822 also mentions TOIN/prefix-cache symptoms, while this PR specifically fixes the custom-agent MCP exclusion name-resolution path.
1 parent 2ccd831 commit 140d6e4

2 files changed

Lines changed: 122 additions & 4 deletions

File tree

‎headroom/config.py‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,27 @@ class AnchorConfig:
229229
)
230230

231231

232+
def _tool_name_aliases(name: str) -> tuple[str, ...]:
233+
"""Return equivalent spellings for tool exclusion matching."""
234+
aliases = [name]
235+
lname = name.lower()
236+
237+
if lname.startswith("mcp__"):
238+
# OpenAI-style MCP wrappers use mcp__server__tool. Custom agents that
239+
# speak Anthropic sometimes emit the same wrapper as mcp_Server_tool.
240+
parts = name.split("__", 2)
241+
if len(parts) == 3 and parts[1] and parts[2]:
242+
aliases.append(f"mcp_{parts[1]}_{parts[2]}")
243+
aliases.append(parts[2])
244+
elif lname.startswith("mcp_"):
245+
parts = name.split("_", 2)
246+
if len(parts) == 3 and parts[1] and parts[2]:
247+
aliases.append(f"mcp__{parts[1]}__{parts[2]}")
248+
aliases.append(parts[2])
249+
250+
return tuple(dict.fromkeys(aliases))
251+
252+
232253
def is_tool_excluded(name: str, exclude_tools: Iterable[str]) -> bool:
233254
"""Return True if ``name`` matches the tool-exclusion set.
234255
@@ -237,15 +258,28 @@ def is_tool_excluded(name: str, exclude_tools: Iterable[str]) -> bool:
237258
``[``) are matched with :func:`fnmatch.fnmatchcase`, letting a single pattern
238259
such as ``mcp__*`` cover every tool an MCP server exposes without listing
239260
each name (issue #870).
261+
262+
MCP tool wrappers are also matched through their common aliases. For example,
263+
``mcp__Headroom__headroom_retrieve`` and
264+
``mcp_Headroom_headroom_retrieve`` both match ``mcp__*`` and the bare
265+
``headroom_retrieve`` entry.
240266
"""
241267
if not exclude_tools:
242268
return False
243-
if name in exclude_tools or name.lower() in exclude_tools:
269+
270+
patterns = tuple(exclude_tools)
271+
if not patterns:
272+
return False
273+
aliases = _tool_name_aliases(name)
274+
exact_patterns = set(patterns)
275+
lower_exact_patterns = {pat.lower() for pat in exact_patterns}
276+
if any(alias in exact_patterns or alias.lower() in lower_exact_patterns for alias in aliases):
244277
return True
245-
lname = name.lower()
278+
246279
return any(
247-
fnmatch.fnmatchcase(lname, pat.lower())
248-
for pat in exclude_tools
280+
fnmatch.fnmatchcase(alias.lower(), pat.lower())
281+
for alias in aliases
282+
for pat in patterns
249283
if "*" in pat or "?" in pat or "[" in pat
250284
)
251285

‎tests/test_transforms/test_content_router.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,16 +796,100 @@ def test_glob_exclude_tools(self, tokenizer):
796796
assert json.loads(result.messages[1]["content"]) == json.loads(messages[1]["content"])
797797
assert "router:excluded:lossless_json" in result.transforms_applied
798798

799+
def test_anthropic_mcp_alias_exclude_tools(self, tokenizer):
800+
"""Single-underscore MCP names from custom agents honor documented MCP globs."""
801+
config = ContentRouterConfig(
802+
min_section_tokens=10,
803+
exclude_tools={"mcp__*"},
804+
)
805+
router = ContentRouter(config)
806+
807+
messages = [
808+
{
809+
"role": "assistant",
810+
"content": [
811+
{
812+
"type": "tool_use",
813+
"id": "toolu_mcp_1",
814+
"name": "mcp_CursorTaskRegistry_cursor_list_tasks",
815+
"input": {"project": "headroom"},
816+
}
817+
],
818+
},
819+
{
820+
"role": "user",
821+
"content": [
822+
{
823+
"type": "tool_result",
824+
"tool_use_id": "toolu_mcp_1",
825+
"content": generate_json_data(50),
826+
}
827+
],
828+
},
829+
]
830+
831+
result = router.apply(messages, tokenizer)
832+
833+
tool_result_block = result.messages[1]["content"][0]
834+
assert json.loads(tool_result_block["content"]) == json.loads(
835+
messages[1]["content"][0]["content"]
836+
)
837+
assert "router:excluded:lossless_json" in result.transforms_applied
838+
839+
def test_anthropic_mcp_bare_tool_alias_exclude_tools(self, tokenizer):
840+
"""Bare tool exclusions match custom-agent MCP wrappers (#1822)."""
841+
config = ContentRouterConfig(
842+
min_section_tokens=10,
843+
exclude_tools={"headroom_retrieve"},
844+
)
845+
router = ContentRouter(config)
846+
847+
messages = [
848+
{
849+
"role": "assistant",
850+
"content": [
851+
{
852+
"type": "tool_use",
853+
"id": "toolu_retrieve_1",
854+
"name": "mcp_HeadroomZai_headroom_retrieve",
855+
"input": {"key": "abc123"},
856+
}
857+
],
858+
},
859+
{
860+
"role": "user",
861+
"content": [
862+
{
863+
"type": "tool_result",
864+
"tool_use_id": "toolu_retrieve_1",
865+
"content": generate_json_data(50),
866+
}
867+
],
868+
},
869+
]
870+
871+
result = router.apply(messages, tokenizer)
872+
873+
tool_result_block = result.messages[1]["content"][0]
874+
assert json.loads(tool_result_block["content"]) == json.loads(
875+
messages[1]["content"][0]["content"]
876+
)
877+
assert "router:excluded:lossless_json" in result.transforms_applied
878+
799879
def test_is_tool_excluded_helper(self):
800880
"""is_tool_excluded: exact (case-insensitive) and glob matching."""
801881
from headroom.config import is_tool_excluded
802882

803883
# Glob entry covers a whole MCP server; unrelated tools are untouched.
804884
assert is_tool_excluded("mcp__build123d__measure", {"mcp__*"})
885+
assert is_tool_excluded("mcp_CursorTaskRegistry_cursor_list_tasks", {"mcp__*"})
805886
assert not is_tool_excluded("Bash", {"mcp__*"})
806887
# Plain entries keep exact, case-insensitive membership.
807888
assert is_tool_excluded("Read", {"read"})
808889
assert is_tool_excluded("MCP__X", {"mcp__*"})
890+
# MCP wrapper aliases can still be excluded by their bare tool name.
891+
assert is_tool_excluded("mcp_HeadroomZai_headroom_retrieve", {"headroom_retrieve"})
892+
assert is_tool_excluded("mcp__Headroom__headroom_retrieve", {"headroom_retrieve"})
809893
# Empty set never excludes.
810894
assert not is_tool_excluded("Read", set())
811895

0 commit comments

Comments
 (0)