rohitg00/ai-engineering-from-scratch · error · ValueError

canonical collision: {canonical_name}

Error message

canonical collision: {canonical_name}

What it means

Under prefix-on-collision, a tool name that already exists in the registry is retried as '{peer.name}/{local_name}', and even that prefixed name is taken. This happens when two tools on the same peer (or an earlier prefix) collide, or one peer's name makes its prefixed tool equal another canonical entry, so no unique name can be produced.

Source

Thrown at phases/13-tools-and-protocols/08-building-an-mcp-client/code/main.py:520

            if peer.available:
                result = self._request(peer, "tools/list", {})
                peer.tools = sorted(result.get("tools", []), key=lambda tool: tool["name"])

    def merge(self, policy: str = "prefix-on-collision") -> None:
        if policy not in {"prefix-on-collision", "reject"}:
            raise ValueError("policy must be prefix-on-collision or reject")
        self.registry.clear()
        for peer_name in sorted(self.peers):
            peer = self.peers[peer_name]
            for tool in peer.tools:
                local_name = tool["name"]
                canonical_name = local_name
                if canonical_name in self.registry:
                    if policy == "reject":
                        continue
                    canonical_name = f"{peer.name}/{local_name}"
                    if canonical_name in self.registry:
                        raise ValueError(f"canonical collision: {canonical_name}")
                self.registry[canonical_name] = MergedTool(
                    canonical_name=canonical_name,
                    peer_name=peer.name,
                    local_name=local_name,
                    description=tool.get("description", ""),
                )
        self.registry = dict(sorted(self.registry.items()))

    def call(self, canonical_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
        merged = self.registry.get(canonical_name)
        if merged is None:
            return {
                "resultType": "complete",
                "content": [{"type": "text", "text": f"Unknown tool: {canonical_name}"}],
                "isError": True,
            }
        peer = self.peers[merged.peer_name]
        if not peer.available:

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Inspect which tool names collide by dumping peer.tools per peer
  2. Rename the duplicated tools on the server side
  3. Use policy='reject' to skip collisions instead of failing
  4. Change the colliding peer's name so prefixed names are unique

Example fix

// before
client.merge("prefix-on-collision")  # raises on double collision

// after
client.merge("reject")  # colliding tools are skipped, merge completes
Defensive patterns

Strategy: fallback

Validate before calling

seen = {}
for name in sorted(p.name for p in client.peers.values()):
    for tool in tools_of(name):
        for candidate in (tool, f'{name}/{tool}'):
            if candidate in seen:
                rename_or_reject(candidate)

Type guard

null

Try / catch

try:
    client.merge()
except ValueError as e:
    if "canonical collision" in str(e):
        client.merge('reject')  # skip colliding tools and proceed

Prevention

When it happens

Trigger: policy == 'prefix-on-collision', canonical_name is already registered, f'{peer.name}/{local_name}' is also already in self.registry.

Common situations: Duplicate tool names within one peer's tools/list; a peer literally named such that peer.name/tool collides with an existing canonical name; peers exposing overlapping prefixed namespaces.

Related errors


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/bb61e882f0d94dec. Report an issue: GitHub.