Skip to content

[BUG][API]: Tool update conflict checks have pre-existing gaps in name resolution and scope validation #3528

@crivetimihai

Description

@crivetimihai

Bug Summary

During review of #3503 (fix for #3468), three pre-existing gaps were identified in the tool update conflict checking logic. These are not regressions from that PR — they existed before and remain unchanged. Documenting them here for follow-up.


Gap 1: custom_name-only updates bypass uniqueness checks

Affected code: mcpgateway/services/tool_service.pyupdate_tool()

update_tool() only triggers conflict validation when tool_update.name changes. However, the before_update event listener set_custom_name_and_slug (mcpgateway/db.py:6387-6441) recomputes the stored name column from custom_name on every commit. A custom_name-only update can therefore produce a slug collision without any application-level conflict check running.

The composite unique constraint (team_id, owner_email, name) on the tools table (db.py:3097) provides partial DB-level protection, but NULL values in team_id/owner_email weaken it for public and some private tools.

Reference: PromptService.update_prompt() (prompt_service.py:1736-1744) correctly handles this by building a candidate name from either name or custom_name before checking conflicts.


Gap 2: Conflict detection compares raw custom_name instead of computed slugified name

Affected code: _check_tool_name_conflict() queries DbTool.custom_name == custom_name

Tool uniqueness is enforced on the stored name column, which is derived from slugify(custom_name) plus the gateway prefix. Slug-equivalent names like foo_bar, Foo Bar, and foo-bar all normalize to the same stored name, but the conflict check compares raw custom_name values and would miss these collisions.

Reference: PromptService correctly checks against computed_name (the slugified form) via _compute_prompt_name() (prompt_service.py:1743-1747).


Gap 3: Visibility changes to team/private proceed without required scope fields

Affected code: _check_tool_name_conflict() and update_tool()

When changing visibility to team or private, if the DB record lacks the required scope field (team_id or owner_email), the conflict check logs a warning and skips, but update_tool() still writes the new visibility. This can create an inconsistent record (e.g., visibility="team" with team_id=NULL).

Since ToolUpdate does not expose team_id/owner_email (by design — ownership is derived from the DB), there is no way for the caller to provide the missing scope. Consider returning HTTP 400 instead of warning-and-proceeding.

Note: PR #3503 improved observability here by adding the warning log. The old code would crash with AttributeError on tool_update.team_id (which doesn't exist on the schema), so the current behavior is strictly better than before.


Suggested Fix

Rework update_tool() conflict checking to mirror PromptService:

  1. Build a candidate persisted name from either name or custom_name using the same slugification logic as the before_update hook
  2. Check conflicts against the stored name column, not raw custom_name
  3. Replace warn-and-proceed with an explicit 400 when a team/private visibility change lacks required scope fields

Related


Gap 4: name-only rename silently fails for most tool names

Affected code: mcpgateway/services/tool_service.pyupdate_tool() lines 4242 and 4247

The auto-tracking condition that syncs custom_name when only name is provided compares tool.name == tool.custom_name. But tool.name is the slugified stored value (e.g., my-cool-tool) while tool.custom_name is the raw value (e.g., my_cool_tool). These are never equal for names containing underscores, mixed case, or special characters — which is the majority of tools.

Consequence: When a user sends {"name": "new_name"} without custom_name, the auto-tracking condition evaluates to False, custom_name is not updated, and the before_update hook recomputes name from the unchanged custom_name, effectively making the rename a silent no-op.

Verified behavior:

  • custom_name="my-slug-safe"name="my-slug-safe"equal → auto-tracking works
  • custom_name="my_cool_tool"name="my-cool-tool"not equal → auto-tracking broken

Fix: Replace the comparison with slug-aware logic:

# Before (broken):
if tool_update.custom_name is None and tool.name == tool.custom_name:

# After (fixed):
if tool_update.custom_name is None and tool.custom_name_slug == tool.name:

This applies to both occurrences (lines 4242 and 4247). The same fix should be applied to the custom_name_ref resolution logic at line 4242.

Metadata

Metadata

Assignees

Labels

SHOULDP2: Important but not vital; high-value items that are not crucial for the immediate releaseapiREST API Related itembugSomething isn't workingneeds-verificationCode fix appears present but could not be verified against running instance

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions