Skip to content

MCP tools with identical names across multiple servers silently overwrite each other — only the last-loaded server wins #575

@masa0703

Description

@masa0703

What version of Kimi Code is running?

1.47.0

Which open platform/subscription were you using?

kimi for coding

Which model were you using?

kimi-k2.6

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

Summary

When multiple MCP servers expose tools with the same name (e.g., read_node, search_nodes), Kimi CLI registers them into a single flat dictionary keyed only by tool.name. This causes later-loaded servers to silently overwrite earlier ones. The user has no way to know which server’s tool is actually being invoked, leading to unpredictable behavior and access-denied errors.

Environment

  • kimi-cli version: 1.47.0
  • Python version: 3.13
  • OS: macOS
  • Open platform/subscription: kimi for coding
  • Model: kimi-for-coding
  • MCP servers tested: zeeta_masa, zeeta_tp (both HTTP, same endpoint URL with different tokens)

Steps to Reproduce

  1. Configure two MCP servers that expose tools with identical names:

    {
      "mcpServers": {
        "zeeta_masa": {
          "type": "http",
          "url": "https://zeetaweb.jp/api/mcp?token=<token_a>"
        },
        "zeeta_tp": {
          "type": "http",
          "url": "https://zeetaweb.jp/api/mcp?token=<token_b>"
        }
      }
    }

    Both servers expose tools named: read_node, search_nodes, create_node, etc.

  2. Start a session and verify both servers appear as "connected":

    kimi mcp test zeeta_masa
    kimi mcp test zeeta_tp

    Both show ✓ Connected with 15 tools each.

  3. Attempt to read a node that exists on zeeta_masa:

    read_node(node_id="8524")
    

    Success (returns node data)

  4. Attempt to read a node that exists on zeeta_tp (and you have access to via Web UI):

    read_node(node_id="8522")
    

    Access denied to node

Expected Behavior

Both servers’ tools should be usable simultaneously. Either:

  • Tools should be namespaced: zeeta_masa__read_node, zeeta_tp__read_node
  • OR the runtime should route tool calls to the correct server internally.

Actual Behavior

Only one server’s tools are effective. Which server "wins" is non-deterministic because _connect_server runs in parallel via asyncio.gather, and the last call to self.add(tool) overwrites the previous one.

Root Cause Analysis

In kimi_cli/soul/toolset.py, tools are stored in a flat dictionary keyed only by name:

# Line ~198-199
class KimiToolset:
    def add(self, tool: ToolType) -> None:
        self._tool_dict[tool.name] = tool   # <-- key is ONLY tool.name

During MCP loading (load_mcp_tools, line ~629), each server is connected in parallel:

# Line ~270-273 in _connect()
tasks = [
    asyncio.create_task(_connect_server(server_name, server_info))
    for server_name, server_info in self._mcp_servers.items()
    if server_info.status == "pending"
]
results = await asyncio.gather(*tasks) if tasks else []

Inside _connect_server:

# Line ~255-257
for tool in server_info.tools:
    self.add(tool)   # <-- silently overwrites any previous tool with same name

Because multiple zeeta_* servers all expose read_node, whichever server finishes last determines which read_node is actually callable. The user cannot target a specific server.

Impact

Severity Description
Functional Impossible to use multiple MCP servers with overlapping tool names
Security Risk of invoking the wrong server’s tool (e.g., write_file targeting unintended filesystem)
Debugging Users cannot determine which server’s tool was actually executed
UX kimi mcp test shows all servers as "connected", creating false confidence

This is not specific to Zeeta — any two MCP servers exposing common names like read_file, query, search, save_data will collide.

Suggested Fix

Option A: Prefix tool names with server name (recommended)

Change tool registration so that MCP tools are keyed as {server_name}__{tool_name}:

# In _connect_server or add()
prefixed_name = f"{server_name}__{tool.name}"
self._tool_dict[prefixed_name] = tool

The LLM prompt should include both the prefixed name and the original server context.

Option B: Use composite key internally

Keep the LLM-facing name as read_node but internally route using (server_name, tool_name). Requires the LLM to also specify which server to target, or for the runtime to intelligently route.

Option C: Detect collisions and warn/fail

At minimum, if tool.name already exists in _tool_dict, log a clear warning or raise an error instead of silently overwriting:

def add(self, tool: ToolType) -> None:
    if tool.name in self._tool_dict:
        logger.warning(f"Tool '{tool.name}' from server X overwrites tool from server Y")
    self._tool_dict[tool.name] = tool

Additional Context

  • The zeeta_* servers use the same endpoint URL (https://zeetaweb.jp/api/mcp) but different token query parameters to scope to different workspaces. Even if Kimi CLI currently deduplicates by URL, the tool-level collision is the deeper issue.
  • I observed a CI branch named mcp-tool-name-collision (author: salmandeniz), suggesting the team may already be aware of this. However, no open issue currently tracks it.
  • This problem is fundamental to MCP adoption at scale — as more servers are published, name collisions will become inevitable.

What steps can reproduce the bug?

  1. Configure two MCP servers that expose tools with identical names (e.g.
    , zeeta_masa and zeeta_tp)
  2. Start a session and verify both servers appear as "connected" via kim i mcp test
  3. Call read_node(node_id="8524") → success (node exists on zeeta_masa)
  4. Call read_node(node_id="8522") → Access denied (node exists on zeeta_t
    p but the wrong server's tool is invoked)

What is the expected behavior?

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions