shareAI-lab/learn-claude-code · error · ValueError
MCP tool name collision after normalization: {prefixed!r} ma
Error message
MCP tool name collision after normalization: {prefixed!r} maps both {origins[prefixed]} and {origin} What it means
Raised when two different tools normalize to the same mcp__<server>__<tool> name. normalize_mcp_name collapses every character outside [a-zA-Z0-9_-] into '_', so 'doc.search' and 'doc/search' on one server — or identical server/tool pairs registered under server names that normalize together — produce identical prefixed names. The harness keeps an origins map and refuses ambiguous wiring rather than silently shadowing a tool.
Source
Thrown at s15_integrated_harness/code.py:2557
global mcp_tool_policies
tools = list(BUILTIN_TOOLS)
handlers = dict(BUILTIN_HANDLERS)
policies: dict[str, str] = {}
origins = {tool["name"]: f"built-in tool {tool['name']!r}"
for tool in tools}
for server_name, mcp_client in mcp_clients.items():
safe_server = normalize_mcp_name(server_name)
for tool_def in mcp_client.tools:
raw_name = tool_def["name"]
safe_tool = normalize_mcp_name(raw_name)
prefixed = f"mcp__{safe_server}__{safe_tool}"
if len(prefixed) > 64:
raise ValueError(
f"MCP tool name is longer than 64 characters: {prefixed}"
)
origin = f"MCP tool {server_name!r}/{raw_name!r}"
if prefixed in origins:
raise ValueError(
"MCP tool name collision after normalization: "
f"{prefixed!r} maps both {origins[prefixed]} and {origin}"
)
schema = tool_def.get("inputSchema", {})
if not isinstance(schema, dict) or schema.get("type", "object") != "object":
raise ValueError(f"Invalid input schema for {origin}")
origins[prefixed] = origin
tools.append({
"name": prefixed,
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=mcp_client, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
policies[prefixed] = MCP_HOST_POLICY.get(
(server_name, raw_name), "confirm"View on GitHub (pinned to 985456f4ad)
Solutions
- Rename the colliding tool on the MCP server so post-normalization names differ
- Give each server a distinct, already-normalized alias (only [a-zA-Z0-9_-]) as the mcp_clients dict key
- The error names both origins — use them to identify exactly which pair collided before renaming
Example fix
# before # server 'docs' exposes 'get.version' and 'get_version' # both normalize to mcp__docs__get_version -> collision # after # rename on the server: 'get.version' -> 'get_api_version' # now mcp__docs__get_api_version is unique
Defensive patterns
Strategy: validation
Validate before calling
import re
def prefixed_names(mcp_clients):
disallowed = re.compile(r"[^a-zA-Z0-9_-]")
seen = {}
for server, client in mcp_clients.items():
s = disallowed.sub("_", server)
for t in client.tools:
n = f"mcp__{s}__{disallowed.sub('_', t['name'])}"
if n in seen:
raise ValueError(f"collision: {n} <- {seen[n]} and {server}/{t['name']}")
seen[n] = f"{server}/{t['name']}"
return seen
prefixed_names(mcp_clients) # run before wiring the harness Type guard
def names_are_unique_after_normalization(server: str, tool: str, taken: set[str]) -> bool:
import re
n = f"mcp__{re.sub(r'[^a-zA-Z0-9_-]', '_', server)}__{re.sub(r'[^a-zA-Z0-9_-]', '_', tool)}"
return n not in taken Try / catch
try:
harness = build_harness(mcp_clients)
except ValueError as exc:
if "name collision after normalization" in str(exc):
# message names both origins; rename one tool or server alias and rebuild
raise SystemExit(str(exc)) from exc
raise Prevention
- Use already-normalized, unique server aliases
- Avoid punctuation variants of existing tool names ('.', '/') on one server
- Add a collision pre-check in integration tests
When it happens
Trigger: Registering one MCP server exposing both 'get-version' and 'get_version' (or 'get.version'). Registering two servers named 'docs' and 'docs!' whose tools share names. A collision between an MCP-prefixed name and a built-in tool is also possible in principle since built-ins are seeded into the same origins map.
Common situations: Merging MCP servers from different vendors that each define generically named tools. Version skew: a server upgrade renames 'search.v2' which normalizes onto an existing 'search_v2'.
Related errors
- MCP names cannot normalize to an empty string
- MCP tool name is longer than 64 characters: {prefixed}
- Every MCP tool needs a non-empty name
- Duplicate MCP tool name on server {self.name!r}
- Missing MCP handlers: {', '.join(missing)}
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/a9ddc90f8c23d1af.
Report an issue: GitHub.