MCP: move the toolset key from annotations into _meta

Problem

API::Mcp::Handlers::ListTools#build_tool_data merges a custom toolset key into each tool's annotations. The MCP spec defines five annotation keys, and _meta is the field it reserves for extra metadata. The custom key still validates, but clients may not expect it there.

Proposal

Emit the toolset as _meta: { "com.gitlab/toolset": "<name>" }, which follows the spec's reverse-DNS key convention. Check whether any consumer (DAP, governance, E2E tests) reads annotations.toolset, and keep it for one release if one does.

Context

Raised by an external review of the MCP tool catalog.

Implementation plan

Consumers of annotations.toolset (checked 2026-10-09)

Consumer Reads annotations.toolset? Notes
Governance (Ai::ToolRules::GovernedMcpTools) No Reads tool.annotations on the tool object, not the tools/list payload
Duo Workflow Service No contract.proto ToolAnnotations has only the five spec fields, so toolset is already dropped
gitlab-lsp, gitlab-org/cli No Their toolset hits are flow toolsets, a different concept
In-repo specs Yes CE and EE list_tools_spec.rb
User docs No Never documented. On by default since the mcp_toolsets flag was removed on 2026-10-01

No consumer found, so move the key without keeping it in both places for a release. Add a changelog line for any external client that parses it.

Changes

File Change
lib/api/mcp/handlers/list_tools.rb#build_tool_data Emit tool.annotations only, and omit annotations when it's empty (same as Base::BaseService#to_h). Add _meta: { 'com.gitlab/toolset' => tool.toolset.to_s }
spec/requests/api/mcp/handlers/list_tools_spec.rb Drop 'toolset' from every expected_annotations entry. Rewrite "includes toolset annotation on every tool" to assert _meta. The get_mcp_server_version example expects no annotations key
ee/spec/requests/api/mcp/handlers/list_tools_spec.rb Drop 'toolset' from every expected_annotations entry. Change the Ask-tier _meta expectation (line 238) to include both com.gitlab/toolset and anthropic/requiresUserInteraction
ee/lib/ee/api/mcp/handlers/list_tools.rb#governed_tool_data super.merge(_meta: ASK_META) → super.deep_merge(_meta: ASK_META), so an Ask-tier tool keeps its toolset key
ee/spec/lib/ee/api/mcp/handlers/list_tools_spec.rb Change the Ask-tier _meta expectation (line 95) to include both keys
doc/development/duo_agent_platform/mcp/_index.md One sentence in "Declaring a toolset": tools/list sends the toolset as _meta["com.gitlab/toolset"]
.claude/skills/gitlab-mcp-tool-builder/SKILL.md No change. The step 9 example already leaves out toolset

Coordination

Issue Overlap Action
#623330 / !258209 (merged) (merged 2026-10-16) The EE ListTools#governed_tool_data override adds _meta["anthropic/requiresUserInteraction"] to Ask-tier tools with super.merge(_meta: ASK_META), which would replace the toolset key This MR switches it to deep_merge (see Changes)

Validation

  1. In GDK, call tools/list with X-Gitlab-Enabled-Mcp-Server-Toolsets: all.
  2. Confirm every tool has _meta["com.gitlab/toolset"], annotations holds only spec keys, and get_mcp_server_version has no annotations.
  3. Set one tool to Always ask on the group's Tool management tab (with duo_mcp_server_governance_rule_enforcement and duo_workflow_local_tool_governance on), call tools/list again, and confirm its _meta has both com.gitlab/toolset and anthropic/requiresUserInteraction.
  4. Connect Claude Code and MCP Inspector, and confirm the tools list loads.
Edited by 🤖 GitLab Bot 🤖