{"record":{"id":"a9ddc90f8c23d1af","repo":"shareAI-lab/learn-claude-code","slug":"mcp-tool-name-collision-after-normalization-pref-a9ddc9","errorCode":null,"errorMessage":"MCP tool name collision after normalization: {prefixed!r} maps both {origins[prefixed]} and {origin}","messagePattern":"MCP tool name collision after normalization: (.+?) maps both (.+?) and (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s15_integrated_harness/code.py","lineNumber":2557,"sourceCode":"    global mcp_tool_policies\n    tools = list(BUILTIN_TOOLS)\n    handlers = dict(BUILTIN_HANDLERS)\n    policies: dict[str, str] = {}\n    origins = {tool[\"name\"]: f\"built-in tool {tool['name']!r}\"\n               for tool in tools}\n    for server_name, mcp_client in mcp_clients.items():\n        safe_server = normalize_mcp_name(server_name)\n        for tool_def in mcp_client.tools:\n            raw_name = tool_def[\"name\"]\n            safe_tool = normalize_mcp_name(raw_name)\n            prefixed = f\"mcp__{safe_server}__{safe_tool}\"\n            if len(prefixed) > 64:\n                raise ValueError(\n                    f\"MCP tool name is longer than 64 characters: {prefixed}\"\n                )\n            origin = f\"MCP tool {server_name!r}/{raw_name!r}\"\n            if prefixed in origins:\n                raise ValueError(\n                    \"MCP tool name collision after normalization: \"\n                    f\"{prefixed!r} maps both {origins[prefixed]} and {origin}\"\n                )\n            schema = tool_def.get(\"inputSchema\", {})\n            if not isinstance(schema, dict) or schema.get(\"type\", \"object\") != \"object\":\n                raise ValueError(f\"Invalid input schema for {origin}\")\n            origins[prefixed] = origin\n            tools.append({\n                \"name\": prefixed,\n                \"description\": tool_def.get(\"description\", \"\"),\n                \"input_schema\": schema,\n            })\n            handlers[prefixed] = (\n                lambda *, client=mcp_client, tool=raw_name, **kwargs:\n                client.call_tool(tool, kwargs)\n            )\n            policies[prefixed] = MCP_HOST_POLICY.get(\n                (server_name, raw_name), \"confirm\"","sourceCodeStart":2539,"sourceCodeEnd":2575,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s15_integrated_harness/code.py#L2539-L2575","documentation":"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.","triggerScenarios":"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.","commonSituations":"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'.","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"],"exampleFix":"# before\n# server 'docs' exposes 'get.version' and 'get_version'\n# both normalize to mcp__docs__get_version -> collision\n\n# after\n# rename on the server: 'get.version' -> 'get_api_version'\n# now mcp__docs__get_api_version is unique","handlingStrategy":"validation","validationCode":"import re\n\ndef prefixed_names(mcp_clients):\n    disallowed = re.compile(r\"[^a-zA-Z0-9_-]\")\n    seen = {}\n    for server, client in mcp_clients.items():\n        s = disallowed.sub(\"_\", server)\n        for t in client.tools:\n            n = f\"mcp__{s}__{disallowed.sub('_', t['name'])}\"\n            if n in seen:\n                raise ValueError(f\"collision: {n} <- {seen[n]} and {server}/{t['name']}\")\n            seen[n] = f\"{server}/{t['name']}\"\n    return seen\n\nprefixed_names(mcp_clients)  # run before wiring the harness","typeGuard":"def names_are_unique_after_normalization(server: str, tool: str, taken: set[str]) -> bool:\n    import re\n    n = f\"mcp__{re.sub(r'[^a-zA-Z0-9_-]', '_', server)}__{re.sub(r'[^a-zA-Z0-9_-]', '_', tool)}\"\n    return n not in taken","tryCatchPattern":"try:\n    harness = build_harness(mcp_clients)\nexcept ValueError as exc:\n    if \"name collision after normalization\" in str(exc):\n        # message names both origins; rename one tool or server alias and rebuild\n        raise SystemExit(str(exc)) from exc\n    raise","preventionTips":["Use already-normalized, unique server aliases","Avoid punctuation variants of existing tool names ('.', '/') on one server","Add a collision pre-check in integration tests"],"tags":["mcp","validation","naming","collision"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}