Skip to content

Bare-name tool alias silently re-registers on a 3rd+ collision, only the 2nd logs a warning #515

Description

@AmirF194

Background

ToolUniverse.load_tools() (src/mcphub/__init__.py) registers a bare
(unqualified) alias for each discovered tool name, so callers can do
tu.run("status") instead of tu.run("iot.status"). When a second server
exposes the same bare name, the collision is detected, a warning is logged,
and the alias is removed (self._alias.pop(t.name, None)) so the ambiguous
name stops resolving.

Removing the alias also erases all memory that the name was ever ambiguous.
If a third (or any later) server also happens to expose that same bare
name, the registration check sees the name is absent from self._alias again
and silently re-adds it, pointing at whichever server loaded last. No warning
fires on this second-and-later collision, only on the very first one. A
caller invoking the bare name afterward gets routed to one of 3+ legitimate
servers with no indication anything is ambiguous.

With the 6 MCP servers currently shipped (iot, utilities, fmsr, wo, tsfm,
vibration) I did not find a live 3-way name collision, so today this is a
latent defect rather than an observed production failure, but it would bite
silently the moment a third server (including a user-added one) reuses a
bare name already used by two others.

Steps to Reproduce

Ran against a real ToolUniverse instance and the real load_tools() code
path; only _connect and _worker.list are stubbed out so the repro doesn't
need three live stdio MCP server processes.

import logging, sys
sys.path.insert(0, "src")
from mcphub import ToolUniverse

logging.basicConfig(level=logging.WARNING)

class FakeTool:
    def __init__(self, name):
        self.name = name
        self.description = ""
        self.inputSchema = {}

class FakeListResult:
    def __init__(self, tools):
        self.tools = tools

FAKE_SERVERS = {
    "server1": [FakeTool("status")],
    "server2": [FakeTool("status")],
    "server3": [FakeTool("status")],
}

tu = ToolUniverse(servers={k: [] for k in FAKE_SERVERS})
tu._connect = lambda name: None                       # skip real stdio connect
tu._worker.list = lambda name: FakeListResult(FAKE_SERVERS[name])

count = tu.load_tools(servers=list(FAKE_SERVERS))
print("tools registered:", count)
print("alias table:", tu._alias)
tu.close()

Run with:

python -m pip install mcp
python repro.py

Observed

WARNING:mcphub:Ambiguous tool name 'status'; use 'server2.status'.
tools registered: 3
alias table: {'status': 'server3.status'}

Only one warning is logged (for the 2nd collision, server2 vs server1). The
3rd collision (server3) silently re-registers the bare alias with no warning
at all, and status now resolves to server3.status as if it had never been
ambiguous.

Expected

Either no bare alias for status after any collision, or a warning on every
collision (2nd, 3rd, ...), not just the first.

Environment

  • Repo HEAD: 248204c4cce9f9e3c3f3234e466f6694a080f6f7
  • Python 3.12.14, python:3.12-slim Docker image
  • mcp client package (latest from PyPI at repro time)

Reproduced end to end in a clean container against current main. Happy to
send a PR if useful; the fix looks like replacing the pop-then-forget pattern
with a separate set of names known to be ambiguous that's never cleared, and
warning on every collision.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions