shareAI-lab/learn-claude-code · error · ValueError

Duplicate MCP tool name on server {self.name!r}

Error message

Duplicate MCP tool name on server {self.name!r}

What it means

MCPClient.register() rejects a tool list containing the same name twice on one server. Handlers are keyed by name, so duplicates would silently bind all calls to whichever handler was inserted last; registration fails fast and names the offending server in the message.

Source

Thrown at s15_integrated_harness/code.py:2434

# -- MCP System --

# MCP is modeled as late-bound tools: connect first, then discovered server
# tools are merged into the normal tool pool with mcp__server__tool names.
class MCPClient:
    """Small in-process stand-in for MCP tools/list and tools/call."""

    def __init__(self, name: str):
        self.name = name
        self.tools: list[dict] = []
        self._handlers: dict[str, callable] = {}

    def register(self, tool_defs: list[dict],
                 handlers: dict[str, callable]):
        names = [tool.get("name") for tool in tool_defs]
        if any(not isinstance(name, str) or not name for name in names):
            raise ValueError("Every MCP tool needs a non-empty name")
        if len(set(names)) != len(names):
            raise ValueError(f"Duplicate MCP tool name on server {self.name!r}")
        missing = [name for name in names if name not in handlers]
        if missing:
            raise ValueError(f"Missing MCP handlers: {', '.join(missing)}")
        self.tools = list(tool_defs)
        self._handlers = dict(handlers)

    def call_tool(self, tool_name: str, args: dict) -> str:
        handler = self._handlers.get(tool_name)
        if not handler:
            return f"MCP error: unknown tool '{tool_name}'"
        try:
            return str(handler(**args))
        except Exception as exc:
            return f"MCP error: {type(exc).__name__}: {exc}"


mcp_clients: dict[str, MCPClient] = {}
_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_-]")

View on GitHub (pinned to 985456f4ad)

Solutions

  1. De-duplicate tool_defs by name before register() (keep one, or namespace the names).
  2. When merging servers, prefix each tool name with its source server to guarantee uniqueness.
  3. Fix the upstream server/gateway so tools/list itself has unique names.

Example fix

// before
tools = read_tools + read_tools  # duplicate names
client.register(tools, handlers)

// after
seen, unique = set(), []
for t in tools:
    if t["name"] not in seen:
        seen.add(t["name"])
        unique.append(t)
client.register(unique, handlers)
Defensive patterns

Strategy: validation

Validate before calling

names = [t["name"] for t in tool_defs if isinstance(t.get("name"), str) and t["name"]]
assert len(names) == len(set(names)), f"duplicate tool names: {names}"

# or de-duplicate, keeping first occurrence:
seen, unique = set(), []
for t in tool_defs:
    if t["name"] not in seen:
        seen.add(t["name"])
        unique.append(t)

Type guard

def names_unique(tool_defs) -> bool:
    names = [t.get("name") for t in tool_defs]
    return len(set(names)) == len(names)

Try / catch

try:
    client.register(tool_defs, handlers)
except ValueError as e:
    if "Duplicate MCP tool name" in str(e):
        # namespace by server or drop dupes, then retry once
        raise

Prevention

When it happens

Trigger: Calling register() with tool_defs where two entries share a 'name' — e.g. concatenating two tool lists with overlapping names, a copy-paste duplication while hand-building a test server, or a source server that genuinely advertises a duplicate (misconfigured gateway aggregating two plugins with the same tool name).

Common situations: Test doubles assembled by extending a list; merging tools from multiple upstream MCP servers into one client without namespacing; gateway config duplicating a plugin.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/23ffbe244cb0ac4d. Report an issue: GitHub.