fix: add MCP tool name collision protection (#3077)
- Registry now warns when a tool name is overwritten by a different toolset (silent dict overwrite was the previous behavior) - MCP tool registration checks for collisions with non-MCP (built-in) tools before registering. If an MCP tool's prefixed name matches an existing built-in, the MCP tool is skipped and a warning is logged. MCP-to-MCP collisions are allowed (last server wins). - Both regular MCP tools and utility tools (resources/prompts) are guarded. - Adds 5 tests covering: registry overwrite warning, same-toolset re-registration silence, built-in collision skip, normal registration, and MCP-to-MCP collision pass-through. Reported by k_sze (KONG) — MiniMax MCP server's web_search tool could theoretically shadow Hermes's built-in web_search if prefixing failed.
This commit is contained in:
@@ -1532,6 +1532,16 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]:
|
||||
schema = _convert_mcp_schema(name, mcp_tool)
|
||||
tool_name_prefixed = schema["name"]
|
||||
|
||||
# Guard against collisions with built-in (non-MCP) tools.
|
||||
existing_toolset = registry.get_toolset_for_tool(tool_name_prefixed)
|
||||
if existing_toolset and not existing_toolset.startswith("mcp-"):
|
||||
logger.warning(
|
||||
"MCP server '%s': tool '%s' (→ '%s') collides with built-in "
|
||||
"tool in toolset '%s' — skipping to preserve built-in",
|
||||
name, mcp_tool.name, tool_name_prefixed, existing_toolset,
|
||||
)
|
||||
continue
|
||||
|
||||
registry.register(
|
||||
name=tool_name_prefixed,
|
||||
toolset=toolset_name,
|
||||
@@ -1556,9 +1566,20 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]:
|
||||
schema = entry["schema"]
|
||||
handler_key = entry["handler_key"]
|
||||
handler = _handler_factories[handler_key](name, server.tool_timeout)
|
||||
util_name = schema["name"]
|
||||
|
||||
# Same collision guard for utility tools.
|
||||
existing_toolset = registry.get_toolset_for_tool(util_name)
|
||||
if existing_toolset and not existing_toolset.startswith("mcp-"):
|
||||
logger.warning(
|
||||
"MCP server '%s': utility tool '%s' collides with built-in "
|
||||
"tool in toolset '%s' — skipping to preserve built-in",
|
||||
name, util_name, existing_toolset,
|
||||
)
|
||||
continue
|
||||
|
||||
registry.register(
|
||||
name=schema["name"],
|
||||
name=util_name,
|
||||
toolset=toolset_name,
|
||||
schema=schema,
|
||||
handler=handler,
|
||||
@@ -1566,7 +1587,7 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]:
|
||||
is_async=False,
|
||||
description=schema["description"],
|
||||
)
|
||||
registered_names.append(schema["name"])
|
||||
registered_names.append(util_name)
|
||||
|
||||
server._registered_tool_names = list(registered_names)
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ class ToolRegistry:
|
||||
emoji: str = "",
|
||||
):
|
||||
"""Register a tool. Called at module-import time by each tool file."""
|
||||
existing = self._tools.get(name)
|
||||
if existing and existing.toolset != toolset:
|
||||
logger.warning(
|
||||
"Tool name collision: '%s' (toolset '%s') is being "
|
||||
"overwritten by toolset '%s'",
|
||||
name, existing.toolset, toolset,
|
||||
)
|
||||
self._tools[name] = ToolEntry(
|
||||
name=name,
|
||||
toolset=toolset,
|
||||
|
||||
Reference in New Issue
Block a user