{"record":{"id":"a0523f511bb217c2","repo":"shareAI-lab/learn-claude-code","slug":"mcp-tool-name-collision-after-normalization-pref","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":"s14_mcp_plugin/code.py","lineNumber":334,"sourceCode":"    tools = list(BUILTIN_TOOLS)\n    handlers = dict(BUILTIN_HANDLERS)\n    policies: dict[str, str] = {}\n    origins = {\n        tool[\"name\"]: f\"built-in tool {tool['name']!r}\"\n        for tool in tools\n    }\n\n    for server_name, server in mcp_clients.items():\n        safe_server = normalize_mcp_name(server_name)\n        for tool_def in server.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(f\"MCP tool name is longer than 64 characters: {prefixed}\")\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=server, 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":316,"sourceCodeEnd":352,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s14_mcp_plugin/code.py#L316-L352","documentation":"Normalization (disallowed chars → '_') plus the mcp__server__tool prefix can make two distinct tools collide: e.g. 'my-tool' and 'my_tool' on the same server both become mcp__srv__my_tool, or a server/tool pair that prefixes to a built-in tool name. The catalog build detects the collision against the origins map (which pre-seeds built-in tools) and raises with both conflicting origins.","triggerScenarios":"Server 'my.server' with tool 'search' vs server 'my_server' with tool 'search' → both 'mcp__my_server__search'; tools 'a-b' and 'a_b' on one server; an MCP tool that normalizes to a name already used by a built-in tool.","commonSituations":"Registering multiple MCP servers whose names differ only in punctuation; tools whose raw names differ only by case/punctuation; mirroring a tool that collides with built-ins.","solutions":["Rename one of the colliding servers or tools so their normalized forms differ (change letters, not just punctuation).","Check the error message: it names both origins — rename the less important one.","If mirroring built-ins, drop or prefix the MCP variant (e.g. tool name 'ext_search')."],"exampleFix":"// before\nservers = {'my.server': srv_a, 'my_server': srv_b}  // both -> mcp__my_server__*\n\n// after\nservers = {'myserver_docs': srv_a, 'myserver_ops': srv_b}","handlingStrategy":"validation","validationCode":"def catalog_names_unique(existing: set[str], server: str, tools: list[str]) -> list[str]:\n    import re\n    norm = lambda s: re.sub(r'[^A-Za-z0-9_-]', '_', s)\n    clashes = []\n    for t in tools:\n        p = f'mcp__{norm(server)}__{norm(t)}'\n        if p in existing:\n            clashes.append(p)\n        existing.add(p)\n    return clashes  # empty means safe to register","typeGuard":null,"tryCatchPattern":"try:\n    build_catalog(tools, mcp_clients)\nexcept ValueError as exc:\n    if 'collision after normalization' in str(exc):\n        # error names both origins; rename the less critical one and rebuild\n        logging.error('MCP name collision: %s', exc)\n        raise SystemExit(2) from exc\n    raise","preventionTips":["Make server names differ by letters, not punctuation (normalization collapses punctuation to '_').","Avoid tool names that differ only in '-' vs '_' within a server.","In multi-server setups, namespace tool names per server to avoid built-in collisions."],"tags":["mcp","name-collision","normalization","tool-registration"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}