fix(toolsets): merge MCP tools when alias collides with static toolset

If an MCP server registers itself under the same name as a built-in
toolset (e.g. an MCP "homeassistant" running alongside the built-in
`homeassistant` toolset), `get_toolset()` returned the static definition
early and the MCP tools were silently shadowed — they got registered
into `mcp-homeassistant` but never surfaced when the agent looked up
`homeassistant`.

Detect the collision via the registry alias (`alias_target` starting
with `mcp-`) and merge the two tool lists. The description is annotated
so the source is visible in `/toolsets info`.
This commit is contained in:
Muno
2026-05-04 14:25:49 +02:00
committed by Teknium
parent 96ed0e71ea
commit 3bc1780f2d
2 changed files with 26 additions and 2 deletions

View File

@@ -56,6 +56,24 @@ class TestGetToolset:
assert set(ts["tools"]) == {"web_search", "web_extract", "web_search_plus"}
def test_static_and_mcp_alias_with_same_name_are_merged(self, monkeypatch):
# An MCP server named like a built-in toolset registers a bare alias to its
# `mcp-<name>` toolset; the static entry must union those tools in (and keep
# its own includes) instead of shadowing the server.
TOOLSETS["_mergetest"] = {"description": "static", "tools": ["builtin_tool_a"], "includes": ["web"]}
try:
reg = ToolRegistry()
reg.register(name="mcp__mergetest_call", toolset="mcp-_mergetest",
schema=_make_schema("mcp__mergetest_call", "Call"), handler=_dummy_handler)
reg.register_toolset_alias("_mergetest", "mcp-_mergetest")
monkeypatch.setattr("tools.registry.registry", reg)
ts = get_toolset("_mergetest")
assert {"builtin_tool_a", "mcp__mergetest_call"} <= set(ts["tools"])
assert ts["includes"] == ["web"]
finally:
del TOOLSETS["_mergetest"]
class TestResolveToolset:
def test_leaf_toolset(self):

View File

@@ -281,8 +281,14 @@ def get_toolset(name: str, *, include_registry: bool = True) -> Optional[Dict[st
return toolset if toolset else None
if toolset:
merged_tools = sorted(set(toolset.get("tools", [])) | set(registry.get_tool_names_for_toolset(name)))
return {**toolset, "tools": merged_tools}
merged_tools = set(toolset.get("tools", [])) | set(registry.get_tool_names_for_toolset(name))
# An MCP server named like a built-in toolset ("homeassistant", "browser") registers a bare
# alias to its `mcp-<name>` toolset; without this union the static entry shadows it and the
# server's tools never reach the model even though discovery registered them.
alias_target = registry.get_toolset_alias_target(name)
if alias_target and alias_target != name:
merged_tools |= set(registry.get_tool_names_for_toolset(alias_target))
return {**toolset, "tools": sorted(merged_tools)}
if name in _get_plugin_toolset_names():
# Plugin toolset; shown as its MCP server alias when one exists.