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
- In GDK, call
tools/listwithX-Gitlab-Enabled-Mcp-Server-Toolsets: all. - Confirm every tool has
_meta["com.gitlab/toolset"],annotationsholds only spec keys, andget_mcp_server_versionhas noannotations. - Set one tool to Always ask on the group's Tool management tab (with
duo_mcp_server_governance_rule_enforcementandduo_workflow_local_tool_governanceon), calltools/listagain, and confirm its_metahas bothcom.gitlab/toolsetandanthropic/requiresUserInteraction. - Connect Claude Code and MCP Inspector, and confirm the tools list loads.
Edited by 🤖 GitLab Bot 🤖