MCP server toolset selection support

Problem

Not every usage scenario needs all the tools our MCP server can expose and even with gitlab-org#20222 (closed), there is likely going to be 40+ tools over time. This can create a strain on the context of an LLM especially if multiple mcp servers are used.

In addition to this, we might not want to automatically expose all newly added tools per default via the MCP server. Even when using MCP we might have tools that only make sense to expose per default if a full set is available (e.g. to work with packages) or that only make sense to expose to DAP but not to other MCP clients.

As analyzed in gitlab-org#20222 (comment 3514519006) many MCP servers already provide a way to opt into specific sets of their tools.

Desired Outcome

  • The ability to assign individual tools to a specific toolset (e.g. work_items) (see gitlab-org#20222 (comment 3514825550)
  • The ability for users to configure which toolsets to use
  • A way to designate a "default" toolset that spans across the specific toolsets. New tools should not automatically be added to this.

Implementation Plan

v1 scope: Rails layer only. Additive toolset filtering on top of the existing per-name allow-list in the /api/v4/mcp endpoint. External consumers (Claude Desktop, VS Code, custom integrations) can select toolsets via a new request header. Workhorse/DAP (McpConfigService) changes are deferred to a follow-up — they already compute their own tool lists.

Toolset definitions

Toolsets group tools by domain. Each tool belongs to exactly one toolset.

"Default" is a set of toolsets, not a set of tools. No header → the server returns tools from ALWAYS_ON_TOOLSETS + DEFAULT_TOOLSETS. A header value of work_items,ci → the server returns tools from ALWAYS_ON_TOOLSETS + [work_items, ci]. No toolset is named "default".

Toolset names and tool assignments are derived from the GA must-have tools epic and the consolidated toolset proposal.

GA toolsets

Toolset Behavior Tools Count
meta always-on authenticate, complete_authentication, get_mcp_server_version 3
core default search, get_project, save_project, save_note, get_user, list_projects, list_groups 7
merge_requests default list_merge_requests, get_merge_request, save_merge_request, save_merge_request_review 4
work_items default list_work_items, get_work_item, save_work_item 3
repository default get_repository_file, list_repository_tree, save_commit, list_commits, get_commit 5
ci default get_job, get_pipeline, list_pipelines, save_pipeline 4
agent_platform opt-in list_flows, start_flow, list_duo_sessions, get_duo_session 4

30 GA tools total. 23 in default toolsets, 4 opt-in, 3 always-on.

Post-GA toolsets

Toolset Behavior Tools Count
wikis opt-in list_wiki_pages, get_wiki_page, save_wiki_page 3
code_security opt-in list_vulnerabilities, get_vulnerability, save_vulnerability 3
audit opt-in list_audit_events 1

Post-GA additions to existing toolsets:

  • core: get_current_user, get_users, list_todos, save_todo, semantic_search
  • ci: list_ci_artifacts, get_ci_artifacts, save_ci_artifacts, validate_ci_file, lint_ci_config

Constants

VALID_TOOLSETS = %i[meta core merge_requests work_items repository ci agent_platform wikis code_security audit].freeze
DEFAULT_TOOLSETS = %i[core merge_requests work_items repository ci].freeze
ALWAYS_ON_TOOLSETS = %i[meta].freeze

Current tool → toolset mapping (v1)

The v1 implementation tags each existing tool with its toolset. Many of these tools will later be consolidated into the target tool names above (tracked separately in #613080). Until consolidation happens, the current names carry their toolset assignment.

Toolset Current tools on master
meta get_mcp_server_version
core search, semantic_code_search (EE), search_labels, get_work_item_types, save_note, list_project_members
merge_requests get_merge_request, save_merge_request, list_merge_requests, get_merge_request_commits, get_merge_request_diffs, get_merge_request_pipelines, get_merge_request_notes, get_merge_request_conflicts, save_merge_request_review, accept_merge_request
work_items get_issue, create_issue, save_work_item, get_workitem_notes, get_saved_view_work_items, link_work_items
repository add_branch, get_commit, get_repository_file, list_repository_tree, add_commit, list_commits, fork_repository
ci get_pipeline, list_pipelines, get_pipeline_jobs, get_job, manage_pipeline, save_pipeline
agent_platform list_duo_sessions (EE)
wikis list_wiki_pages
code_security attach_scan_profile (EE)

Backwards compatibility

  • No header = default toolsets (at time of creation of toolset feature) returned (preserves every existing consumer).
  • New toolsets must be evaluated as to whether they will become part of the "no header" toolset groups or not.

1. Declare a toolset per tool (required, no default)

toolset is a required declaration on every MCP tool. No default — a tool without a toolset declaration fails at registration/boot, not at runtime.

  • BaseService#toolset raises NotImplementedError (same pattern as perform).
  • Versionable tools: toolset read from register_version metadata hash; raises at class load if missing.
  • ApiTool: read from route_setting :mcp, toolset: :work_items, ...; discover_api_tools raises if missing.
  • Valid toolsets: VALID_TOOLSETS = %i[meta core merge_requests work_items repository ci agent_platform wikis code_security audit].freeze validated at build time.
  • A spec iterates all registered tools and asserts each declares a valid toolset.

Tool assignments per the toolset definitions above.

2. Manager grouping helpers

  • Manager#toolsets → { toolset_sym => [canonical_name, ...] }, built from list_tools.
  • Manager#tools_in_toolsets(ids) → flat array of canonical names, always including meta.

3. Rails server-side filter in tools/list

  • New header X-Gitlab-Enabled-Mcp-Server-Toolsets (comma-separated toolset ids). Parsed in lib/api/mcp/base.rb, returns nil when absent.
  • all pseudo-value: X-Gitlab-Enabled-Mcp-Server-Toolsets: all returns every tool across every toolset, including opt-in. Useful for consumers that want the full surface without listing every toolset name.
  • Passed as allowed_toolsets: to ListTools#invoke.
  • Filter in list_tools.rb: skip if allowed_toolsets is present, not all, tool is not meta, and toolset is not in the list.
  • Invalid toolset names in the header return a 400 Bad Request with a message listing the unrecognized names and the valid set. This surfaces misconfiguration immediately rather than hiding typos.
  • Combination rule: when both per-name and per-toolset headers are present, behavior per open question #2.
  • Toolset annotation: each tool's tools/list entry includes toolset in its annotations (e.g., { "toolset": "work_items", ... }). This lets agents see which toolsets are loaded without reading docs.
  • Gated behind feature flag mcp_toolsets. When disabled, the header is ignored.

4. Docs

  • User-facing: document the new header + toolset list in doc/user/model_context_protocol/mcp_server.md.
  • Developer-facing: toolset declaration guide in doc/development/duo_agent_platform/mcp/_index.md.

Open questions

  1. Combination rule — when both X-Gitlab-Enabled-Mcp-Server-Tools (per-name) and X-Gitlab-Enabled-Mcp-Server-Toolsets are present, should the result be a union (tool passes if it matches either header) or an intersection (tool must match both)? Union is recommended — toolsets for broad selection, explicit names for surgical additions.

Out of scope / follow-ups

  • Workhorse/DAP integration (McpServerConfig.Toolsets, McpConfigService toolset-based configs)
  • Read-only connection mode (X-Gitlab-Mcp-Server-Read-Only)
  • Catalog UI grouping by toolset
  • #609451 deprecation lifecycle and stored-name migration
  • Exclude header (X-Gitlab-Exclude-Mcp-Server-Tools) — a way to opt out of specific tools while keeping the rest of a toolset (e.g., "give me everything except agent_platform tools")
  • context toolset (per #613080) for project/user/group lookup tools (get_project, get_user, list_projects, list_groups). Added to VALID_TOOLSETS when those tools are built.

Verification

  1. Unit specs for toolset reader on each tool flavor + Manager#toolsets / #tools_in_toolsets.
  2. Handler spec: toolset filtering, meta always present, absent header = all tools.
  3. Request spec: POST /api/v4/mcp tools/list with X-Gitlab-Enabled-Mcp-Server-Toolsets: work_items returns only work_items + meta tools.
  4. All-tools coverage spec: every registered tool declares a valid toolset.
  5. Manual GDK validation with curl.

Rollout

Feature flag mcp_toolsets gates filtering. Toolset declarations always present (metadata). Inherently backward compatible: no header = all tools.

Edited by Terri Chu