MCP server: renaming/aliasing a tool silently drops it from stored agent definitions

Summary

When an MCP tool is renamed (or consolidated) and the old name is preserved as an alias for backward compatibility, stored agent definitions that reference the old tool name silently lose that tool. The agent keeps running without the capability, and no error or warning is surfaced.

This is reproducible today. Two aliases already ship:

  • gitlab_searchsearch (app/services/mcp/tools/search/search_service.rb:25)
  • gitlab_merge_request_searchlist_merge_requests (app/services/mcp/tools/merge_requests/list_merge_requests_service.rb:83)

Any agent definition saved before those renames has been missing that tool ever since.

Root cause

1. Stored alias names are dropped

Ai::DuoWorkflows::McpConfigService#mcp_tools_for_agent builds an agent's tool set by intersecting the agent's stored tool names against the set of currently-available tools:

available = all_mcp_tools            # = Mcp::Tools::Manager.new.list_tools.keys
((built_in_names + Array(agent_mcp_tools)) & available).uniq

list_tools returns canonical names only. Aliases live in a separate alias_map (Manager#build_alias_map) that is consulted only on the tools/call path via resolve_alias. So an alias resolves correctly when a tool is called, but a stored mcp_tools: ["old_name"] entry is dropped by & available.

2. API-backed tools cannot declare aliases

Mcp::Tools::Base::ApiTool is a single class instantiated once per route (Manager#discover_api_tools). A class-level self.tool_aliases would therefore be shared by every API tool, so it is structurally unusable for them — aliases must be per-instance, read from route_setting :mcp.

Note this is not simply a matter of ApiTool not inheriting Base::BaseService; making it inherit would not fix the per-route problem.

doc/development/duo_agent_platform/mcp/_index.md:519 currently claims "Aliases work for all tool types: custom, GraphQL, API, and aggregated tools", which is false for API tools.

Current behavior

Renaming or aliasing a tool causes stored agent definitions naming the old tool to silently lose it, with no error. API-backed tools cannot declare an alias at all.

Expected behavior

A tool preserved via an alias remains resolvable for stored agent definitions, and API-backed tools can declare aliases.

Proposed fix

Resolve aliases to canonical names on the way into the intersection. Do not add aliases to list_tools.

list_tools must stay canonical because it is shared by consumers that would break:

  • the MCP tools/list response — aliases would appear to every client as duplicate, fully-schema'd tools and inflate the tool budget, defeating the point of consolidation
  • Ai::Catalog::McpTool.fixed_items — aliases would become newly selectable in the agent builder, the opposite of deprecating a name
  • _index.md:518 documents list_tools as canonical-only, and manager_spec.rb guards it

Canonical output is required rather than merely preferred: gitlab_enabled_tools becomes the x-gitlab-enabled-mcp-server-tools header, which returns to the MCP server as allowed_tools, and ListTools#invoke filters that against canonical keys while logging unknown names. Emitting an alias would drop the tool one layer later and log a warning.

MR A — stop dropping alias-named tools

  • McpConfigService#mcp_tools_for_agent: map stored agent_mcp_tools through Manager#resolve_alias before & all_mcp_tools
  • Make Manager#resolve_alias public
  • Do not resolve built_in_names. Built-in tool names are a separate namespace, and an MCP alias colliding with a built-in name would silently rewrite the built-in

Verifiable against the two aliases that already exist, so it carries no dependency on MR B.

MR B — let API tools declare aliases

  • Base::ApiTool#tool_aliases, sourced from settings[:tool_aliases]
  • BaseService#tool_aliases instance-level delegator to the existing class method
  • Manager#build_alias_map: use the instance method rather than tool.class
  • Correct _index.md:519, and note that tool_aliases: on a route carrying aggregators: is dead config

Declaring an actual production alias belongs in the consolidation MR, not here. Once a readOnlyHint route gains an alias, the pre-approved tool list grows and the hardcoded expectations in workflows_spec.rb and mcp_config_service_spec.rb will fail by design — those lists are deliberate change-detectors.

Out of scope

  • notifications/tools/list_changed — tracked separately in #582750
  • Alias-with-default-args (old name → new tool plus a forced argument) — a different mechanism; it changes tools/call argument handling rather than name resolution, and needs a decision on merge/override semantics against caller-supplied arguments. Should be its own issue, blocked on the consolidation design
  • ListTools#invoke does not alias-resolve allowed_tools — same bug class one layer up. During a rolling deploy, a session whose header was built by a pre-rename node has the tool filtered out by a post-rename node. - tracked in #618876

Why it matters

This blocks backward-compatible tool consolidation. The get_merge_request consolidation needs stored agent definitions referencing old names (for example get_merge_request_commits) to keep working.

References

Edited by Terri Chu