Include duo_agent_platform MCP toolset in defaults when Duo Agent Platform is enabled
## Problem
The DAP MCP tools (`list_duo_agents_and_flows`, `start_duo_session`, `get_duo_session`, `list_duo_sessions`, `send_duo_session_input`) are in the opt-in `duo_agent_platform` toolset. `tools/list` only returns them when the client sends `X-Gitlab-Enabled-Mcp-Server-Toolsets`. Header-less OAuth clients, such as Claude Web/Desktop "Connect", never see them. This is a risk for MCP GA.
The toolset should be on by default, but only for users who have DAP. We get there in two phases: turn it on for everyone first, then filter it to users who have DAP.
## Implementation plan
**Phase 1: on by default for everyone**
1. **Move `duo_agent_platform` into the default toolset.** Toolsets stay static constants. They describe what a client asks for, not what the user can use.
1. **Update docs** to list `duo_agent_platform` as a default toolset.
After phase 1, clients on instances or namespaces without DAP also see the five tools. Calls there are rejected by the existing permission checks in each tool. Open question 1 also applies from phase 1 onward.
**Phase 2: only for users with DAP**
1. **Pass the user into `available?`.** Change the signature to `available?(current_user)` and stop setting the user on shared tool instances. This must come before step 2: today a per-user check can run as the wrong user under concurrent requests (see Background).
1. **Add a DAP availability check to the five DAP tools.** Each tool's `available?` returns true only when DAP is available for the user (conditions below). `tools/list` already drops unavailable tools on every path.
1. **Update docs** with the conditions below.
### When the DAP tools are listed (phase 2)
| Deployment | Listed when |
|------------|-------------|
| GitLab.com | The user belongs to a top-level group on a supported plan with DAP enabled, or has DAP through GitLab credits. |
| Self-Managed / Dedicated | GitLab Duo is on for the instance, **and** the instance has the `ai_workflows` license or GitLab credits, **and** DAP is enabled for the user's organization, **and** the user is allowed to use DAP. |
If GitLab Duo is off for a Self-Managed or Dedicated instance ("Off by default" or "Always off"), the DAP tools are never listed. With "Off by default", this holds even for groups that turn Duo on, because the check is instance-level.
Listing is not authorization. Each tool still enforces project and namespace permissions when called.
<details><summary>Implementation details</summary>
**Phase 1, step 1**
- Move `duo_agent_platform` from `OPT_IN` to `DEFAULT` in `app/services/mcp/tools/toolsets.rb`.
**Phase 2, step 1**
- `Mcp::Tools::Base::BaseService#available?` becomes `available?(_current_user)` and still returns `true`.
- `API::Mcp::Handlers::ListTools#tool_available?` (`lib/api/mcp/handlers/list_tools.rb`) calls `tool.available?(current_user)` and drops the `set_cred` call.
- No tool overrides `available?` today, so there are no other call sites.
**Phase 2, step 2**
Put this in one concern included by the five DAP tool services under `ee/app/services/mcp/tools/duo_workflows/`:
```ruby
def available?(current_user)
return false unless current_user
if ::Gitlab::Saas.feature_available?(:gitlab_com_subscriptions)
::Ai::Catalog.duo_agent_platform_available_for_user?(current_user)
else
::Gitlab::CurrentSettings.duo_features_enabled? &&
(::License.feature_available?(:ai_workflows) || ::GitlabSubscriptions::Duo.active_self_managed_gitlab_credits?) &&
::Ai::Catalog.duo_agent_platform_available_for_user?(current_user) &&
current_user.allowed_to_use?(:duo_agent_platform)
end
end
```
- Make `Ai::Catalog.duo_agent_platform_available_for_user?` (`ee/lib/ai/catalog.rb`) public.
- On GitLab.com it calls `cached_duo_agent_platform_available?`: a top-level group on a supported plan with `duo_agent_platform_enabled`, or `duo_agent_platform_enabled_via_credits?`. Cached for 30 minutes per user.
- On Self-Managed it reads `Ai::Setting#duo_agent_platform_enabled` for the user's organization. That setting defaults to `true` (`ee/app/models/ai/setting.rb:23`), which is why the license, credits, Duo switch, and user checks are added around it.
- The Self-Managed branch mirrors the instance-wide parts of `Ai::DuoAgentPlatform::AccessPolicy#duo_workflow_available` and `Gitlab::Llm::StageCheck`. `duo_workflow` is GA in `AiFeaturesCatalogue`, so no stage check is needed.
- Memoize the result per request with `Gitlab::SafeRequestStore`, since `tools/list` runs it once per DAP tool.
**Docs (both phases)**
- `doc/user/model_context_protocol/mcp_server_tools.md` and `mcp_server.md`: `duo_agent_platform` is a default toolset, listed only when DAP is available, with the conditions per deployment.
- `doc/user/gitlab_duo/model_context_protocol/mcp_clients.md`: drop the header guidance for DAP tools.
- `doc/development/duo_agent_platform/mcp/_index.md` (phase 2): per-user checks go in `available?(current_user)`; toolsets stay static.
</details>
## Open questions
1. **Should Duo Agentic Chat get the DAP tools?** DAP's own `tools/list` call sends no toolset header, so once the toolset is a default, every agentic chat session gets all five tools. The three read-only ones are auto-preapproved, and nothing stops chat from starting a DAP session from inside one. Options: accept it, or have `McpConfigService` send an explicit toolset header for agentic chat. Needs a product decision before phase 1 merges.
<details><summary>How DAP sessions get MCP tools</summary>
- Workhorse is the MCP client for DAP sessions (`workhorse/internal/ai_assist/duoworkflow/mcp.go`). It sends `Authorization` (from `McpConfigService#gitlab_mcp_server`), `User-Agent`, and `X-Duo-Workflow-Session-Id`. Nothing in the repo sends `X-Gitlab-Enabled-Mcp-Server-Toolsets` or `X-Gitlab-Enabled-Mcp-Server-Tools`.
- For agentic chat (`chat`, `agentic_chat/v1`) with no agent-selected tools, `Tools` is nil and Workhorse passes every listed tool to the agent.
- `McpConfigService#preapproved_tools_from_annotations` preapproves every tool with `readOnlyHint: true`, which covers `get_duo_session`, `list_duo_sessions`, and `list_duo_agents_and_flows`. `start_duo_session` and `send_duo_session_input` still need approval.
- Custom agents that set `Tools` get the intersection of their list and `tools/list`. Today an agent that selected a DAP tool doesn't get it. After this change it will.
</details>
1. **Does GitLab.com need a seat check?** The GitLab.com branch doesn't call `allowed_to_use?(:duo_agent_platform)`. Without a `root_namespace`, that call falls back to the request's project or group context, which a `tools/list` request doesn't have, so adding it might hide the tools for users who should see them. Leaving it out means a user in a DAP-enabled group without a seat still sees the tools. Needs confirmation from someone who knows the GitLab.com seat model.
## Acceptance criteria
**Phase 1**
- [ ] `duo_agent_platform` is in `Toolsets::DEFAULT`, and `tools/list` with no headers returns the DAP tools.
- [ ] Duo Agentic Chat behavior matches the decision on open question 1, with a spec in `ee/spec/services/ai/duo_workflows/mcp_config_service_spec.rb`.
- [ ] The default-toolset spec (`spec/requests/api/mcp/handlers/list_tools_spec.rb:529-543`) passes with the DAP tools in the defaults.
- [ ] Docs list `duo_agent_platform` as a default toolset and drop the header guidance for DAP tools.
**Phase 2**
- [ ] `available?` takes `current_user`, and `ListTools` no longer calls `set_cred`. `spec/services/mcp/tools/base/base_service_spec.rb:97` is updated.
- [ ] The DAP tools implement the availability check above, and `Ai::Catalog.duo_agent_platform_available_for_user?` is public, with unit specs.
- [ ] With no headers, `tools/list` includes the DAP tools when DAP is available for the user and excludes them when it isn't, on GitLab.com and Self-Managed.
- [ ] An explicit toolset header with `duo_agent_platform` or `all` returns the DAP tools only when DAP is available.
- [ ] Self-Managed: the tools are not listed when Duo is off, when there's neither the license nor credits, when the DAP setting is off, or when the user isn't allowed to use DAP.
- [ ] Self-Managed with credits and no `ai_workflows` license: the tools are listed.
- [ ] The default-toolset spec and the EE `all` header case (`ee/spec/requests/api/mcp/handlers/list_tools_spec.rb`) use a test user with DAP available.
- [ ] Docs describe the conditions per deployment, and the development docs say per-user checks go in `available?(current_user)`.
## Out of scope
- Checking `available?` in `tools/call`. Tools already authorize per resource.
- `notifications/tools/list_changed`. Availability changes are picked up on the next `tools/list`.
- The header-less discovery meta-tool, https://gitlab.com/gitlab-org/gitlab/-/work_items/630992.
## Background
<details><summary>Why available? needs the user passed in (thread safety)</summary>
- `Mcp::Tools::Manager` is created once per process (`namespace_setting :mcp_manager, ::Mcp::Tools::Manager.new`, `lib/api/mcp/base.rb:166`), and `Manager#tools` memoizes tool instances. Every Puma thread shares the same tool objects.
- `ListTools#tool_available?` calls `set_cred(current_user:)` on those shared objects, then `available?`. Two concurrent `tools/list` calls can interleave, so user A's check can run as user B.
- `set_cred` was added there in `e4d1b8d2384a` (Oct 2025) for a per-user feature flag check on `semantic_code_search`. That check later became `current_user.present?`, and the tool was removed in `33bb9dcaac29` (June 2026). No tool overrides `available?` today, so the call has no effect and the race is harmless. It stops being harmless as soon as a tool checks the user.
- `set_cred` there only runs for `CustomService`. The DAP tools are `GraphqlService` and `AggregatedService`, so `current_user` would be nil in their `available?` anyway.
- `tools/call` is not affected. `Manager#get_tool` builds a fresh instance per call.
</details>
<details><summary>Impact on other code that reads the tool list</summary>
- `tools/list` is the only caller of `available?` and the only reader of `Toolsets.defaults`. `tools/call` checks neither.
- `Ai::Catalog::McpTool.fixed_items` (AI Catalog tool picker) and `Ai::ToolRules::GovernedMcpTools` (tool governance) build their own `Manager` and read `list_tools` directly. Unaffected.
- `Ai::DuoWorkflows::McpConfigService` reads `list_tools` for preapproval and agent tool lists. Unaffected by the code change, but see open question 1.
- The `MCP tools list` log and the `list_mcp_tools` internal event use the request's `current_user`, not tool state, so removing `set_cred` doesn't change logging or tracking.
- For DAP composite-identity tokens, `Gitlab::Auth::AuthFinders#resolve_composite_identity_user` returns the human user as `current_user`, so the check runs against the right user.
</details>
<details><summary>Why not the other existing DAP checks</summary>
- `Ai::Catalog.available?` also requires `allowed_to_use_through_namespace?(:ai_catalog)`, which is about the AI Catalog.
- `Ai::DuoWorkflow.duo_agent_platform_available?(container)` and `Ai::DuoAgentPlatform.available?(user:, container:)` need a project or group. A `tools/list` request has neither.
- `Ai::DuoWorkflow.enabled?` requires the `ai_workflows` license, so it would hide the tools on Self-Managed instances that use GitLab credits.
- Self-hosted DAP configuration (`Ai::DuoAgentPlatform.configured?`) also needs a container, so it isn't checked here.
</details>
<details><summary>Operational risks</summary>
- The `tool_count` field on the `MCP tools list` log goes up by 5 for users with DAP. Any dashboard or alert on that field will shift.
- On GitLab.com each `tools/list` runs the check once per DAP tool: five `Rails.cache` reads on a warm cache, and a namespace query on a miss. Memoize per request with `Gitlab::SafeRequestStore` to make it one.
- Call volume for the DAP tools will rise as header-less clients see them, including some calls that fail per-resource authorization.
</details>
## Links
- Header-less discovery: https://gitlab.com/gitlab-org/gitlab/-/work_items/630992
- MCP user journeys: https://gitlab.com/groups/gitlab-org/-/work_items/21395
- Discussion: https://gitlab.com/groups/gitlab-org/-/work_items/21395#note_3928788161
- The toolset feature flag was recently removed, so this needs to move quickly for GA.
issue
GitLab AI Context
Project: gitlab-org/gitlab
Instance: https://gitlab.com
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/CONTRIBUTING.md — contribution guidelines
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/README.md — project overview and setup
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/AGENTS.md — AI agent instructions
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/CLAUDE.md — Claude Code instructions
Repository: https://gitlab.com/gitlab-org/gitlab
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD