Skip to content

[BUG]: Duplicate tool names still possible outside the update_tool path fixed in #6129 #6189

Description

@msureshkumar88

🐞 Bug Summary

#6057 / #6129 fixed duplicate custom_name values slipping through the tool rename path (update_tool, edit modal). While reviewing that fix, several other paths that create or modify tools were checked, and duplicate custom_name values within the same visibility scope are still possible through all of them. Per the MCP spec, each tool exposed by a server must be uniquely identified by name — tools/call has no other disambiguator — so a duplicate custom_name within one visibility scope is a protocol-correctness issue, not just cosmetic.


🧩 Affected Component

  • mcpgateway - API
  • mcpgateway - UI (admin panel)
  • Federation or Transports

Gaps found (with evidence)

1. Federation bulk-import performs no conflict check at all
mcpgateway/services/gateway_service.py:1739 builds DbTool(custom_name=tool.name, ...) in a loop over the upstream server's discovered tools and appends directly to db_tools — it never calls any conflict-check helper. Two different upstream gateways both exposing a tool named e.g. search, federated into the same visibility scope (e.g. public), land as two DbTool rows with the same custom_name. Nothing blocks it.

2. register_tool's creation-time check has a slug/case mismatch that lets duplicates through
mcpgateway/services/tool_service.py:2061-2072 (in register_tool) checks:

existing_tool = db.execute(select(DbTool).where(DbTool.name == tool.name, DbTool.visibility == "public")).scalar_one_or_none()

This compares the raw incoming tool.name against the stored, already-slugified DbTool.name column. DbTool.name is only normalized to slugify(custom_name) in the before_insert event listener (mcpgateway/db.py:6779,6805), which runs after this check. So an existing public tool stored as name="my-tool" and a new tool submitted with tool.name="My Tool" (different case/spacing, same slug) do not match in the check ("My Tool" != "my-tool"), and the second tool is created — producing two tools that resolve to an identical exposed slug.

3. No conflict check at all for private visibility at creation time
Same location (mcpgateway/services/tool_service.py:2061-2072) — only if visibility == "public" and elif visibility == "team" branches exist. There is no private branch, so creating two private tools with the same custom_name under the same owner is never checked.

4. No DB-level backstop for the scope the new update_tool check enforces
mcpgateway/db.py:3432-3433 — the only unique constraint on tools is UniqueConstraint("team_id", "owner_email", "name"), keyed on the computed, gateway-prefixed name column and requiring a matching owner_email. It does not back custom_name uniqueness across different owners in the same team or across public tools from different owners — which is exactly the scope _check_tool_name_conflict (added in #6129) checks in application code. get_for_update (mcpgateway/db.py:6017) only locks existing matching rows; if no row matches yet, there's nothing to lock, so two concurrent requests renaming/creating tools to the same not-yet-used custom_name can both pass the check and both commit.


🔁 Steps to Reproduce

Example for gap 1 (federation, no check):

  1. Register two separate MCP gateways, each exposing an upstream tool named search.
  2. Federate both gateways with visibility=public (or any shared scope).
  3. Observe two Tool rows created with custom_name="search", same visibility — no error raised.

Example for gap 2 (slug mismatch):

  1. Create a public tool via POST /tools (or admin UI) with name="My Tool".
  2. Create a second public tool with name="my-tool".
  3. Both succeed — DbTool.name == tool.name never matches because one side is pre-slugify and the other is post-slugify.

🤔 Expected Behavior

register_tool and federation import should enforce the same custom_name uniqueness semantics (per visibility scope, using the normalized/slugified value) that update_tool now enforces after #6129, and that scope should be backed by a DB-level unique constraint (not just an application-level locked-read check) to close the concurrent-request race.


🧠 Environment Info

Key Value
Version or commit main (as of PR #6129)
Runtime Python 3.11
Platform / OS n/a — code-level finding
Container n/a

🧩 Additional Context

Found while reviewing #6129, which fixes the duplicate-custom_name-on-rename bug described in #6057. That PR only touches update_tool; the gaps above are in register_tool, gateway_service.py's federation sync, and the DB schema, all of which are outside that PR's diff.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecurityImproves securitytriageIssues / Features awaiting triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions