start_duo_session MCP tool
Note
Before picking up this work: this issue adds an MCP tool. Please follow the Adding a new tool guidance first. That includes creating an MCP Tool Proposal, using verb_object naming (get_ / list_ / save_ / delete_), and the shared resource-identification input base classes.
Note
Shipped as route-backed, not GraphQL. !254979 (merged) implements this via route_setting :mcp on POST /ai/duo_workflows/workflows, wrapped in Mcp::Tools::DuoWorkflows::StartDuoSessionService < Base::AggregatedService — the same pattern as save_merge_request and semantic_search. The GraphQL-mutation plan below (aiDuoWorkflowCreate, TriggerDuoFlowTool < GraphqlTool, EE_GRAPHQL_TOOLS) was superseded early in development and never built; kept below for history. See the route-backed-vs-custom decision in #627559 (closed).
Problem Statement / Use Case
Agents in Duo Agentic Chat need to delegate long-running, multi-step tasks to specialist GitLab flows — such as SAST false positive detection, vulnerability resolution, or secret detection analysis — without blocking the chat session. The existing start_flow catalog tool (id: 93) already exposes this capability inside the Duo Agentic Chat agent, but it is not yet available as an MCP tool that external MCP clients (Claude Code, Cursor, etc.) can call.
start_flow maps to start_duo_session in the MCP naming convention: it starts an asynchronous CI-based Duo flow session and returns a workflow_id immediately. The caller then polls get_session (see companion issue) with that workflow_id to track progress and retrieve the final result.
Naming: the tool starts a single flow session, so it belongs to the add_/action verb class (like add_commit) rather than save_ (CRUD) or get_/list_ (reads).
Scope and Non-Goals
- In scope: start an asynchronous Duo flow (foundational or custom AI Catalog) in a project; return a
workflow_idand apoll_after_secondshint; registertrigger_duo_developeras a backward-compat alias. - Non-goals: synchronous agent turns (
ask_duo_agent), polling session status (get_session/get_duo_session_status), approving tool calls (approve_duo_agent_action), listing available agents/flows (list_duo_agents). - Follow-ups: none — the companion
get_sessionandlist_sessionsissues cover the read side.
Data Shape and Context Engineering
The tool returns immediately after creating the session; no polling happens inside the tool itself. The poll_after_seconds hint tells the caller how long to wait before calling get_session. Output is bounded to a single workflow reference. Look into MCP task spec for how to tell agent to poll again: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
- Input schema (example):
{
"tool": "start_duo_flow",
"description": "Start an asynchronous GitLab Duo agentic flow (CI-based session) to accomplish a goal in a project.",
"parameters": {
"url": "string (optional; full GitLab URL encoding project path, e.g. https://gitlab.com/group/project)",
"project_id": "int | string (optional; numeric ID or URL-encoded path such as 'gitlab-org%2Fgitlab')",
"goal": "string (required; natural-language goal, e.g. 'Fix the flaky spec in issue #1234 and open an MR')",
"flow": "string (optional; foundational flow reference, default: 'developer/v1'. Examples: 'code_review/v1', 'fix_pipeline/v1', 'security_review/v1')",
"flow_item_id": "integer (optional; AI Catalog item ID of a custom flow enabled in the project; mutually exclusive with flow)"
}
}- Output schema (JSON example):
{
"workflow_id": 42,
"flow": "developer/v1",
"status": "created",
"web_url": "https://gitlab.com/group/project/-/duo_workflows/42",
"poll_after_seconds": 20
}Backward Compatibility
The prototype MR (!246597 (closed)) already uses trigger_duo_flow as the canonical name and registers trigger_duo_developer as a tool_aliases entry. Preserve this alias so cached MCP clients continue to work. Follow the tool-renaming guidance in doc/development/duo_agent_platform/mcp/_index.md.
Resources
- Existing GraphQL mutation:
AiDuoWorkflowCreate(ee/app/graphql/mutations/ai/duo_workflows/create.rb) — acceptsprojectId/namespaceId,goal,workflowDefinition(this issue'sflow), andaiCatalogItemVersionId(see plan step 4 for theflow_item_idmapping); authorizes viaread_project/read_groupon the resolved container plusauthorize_foundational_flows!; returns aWorkflowType. - Version resolution helper:
Ai::Catalog::Item#latest_released_version_with_fallback(ee/app/models/ai/catalog/item.rb). - Mutation-tool precedent:
create_merge_request_note(app/services/mcp/tools/merge_requests/create_merge_request_note_{tool,service}.rb). - Catalog tool definition:
ee/lib/ai/catalog/built_in_tool_definitions.rb(id: 93, name:start_flow) - Companion issues:
get_sessionMCP tool (#607634 (closed)),list_sessionsMCP tool (#607635 (closed)) - MCP dev guidelines:
doc/development/duo_agent_platform/mcp/_index.md;gitlab-mcp-tool-builderskill's build recipe — verify a GraphQL field exists before writing a custom one.
Implementation Plan (superseded — see note above)
- Two classes:
Mcp::Tools::DuoWorkflows::TriggerDuoFlowTool < Mcp::Tools::Base::GraphqlToolandMcp::Tools::DuoWorkflows::TriggerDuoFlowService < Base::GraphqlService. - Operation file:
ee/app/graphql/queries/mcp/duo_workflows/trigger_duo_flow.mutation.graphql, callingaiDuoWorkflowCreate(input:), selectingworkflow { id workflowDefinition statusName project { webUrl } }anderrors.operation_name: 'aiDuoWorkflowCreate'. url/project_id→projectId: resolve viafind_parent_by_id_or_path!(:project, identifier)and passproject.to_global_id.to_s. The mutation already authorizes the container; no separateAbility.allowed?check needed.flow_item_id→aiCatalogItemVersionId: this issue'sflow_item_idis an AI Catalog item id, but the mutation takes an item version GID. Resolve withAi::Catalog::Item.find_by_id(flow_item_id)&.latest_released_version_with_fallback&.to_global_id&.to_s. Keepflow/flow_item_idmutually exclusive, validated client-side.web_url:WorkflowTypehas noweb_urlfield (onlyresource_web_url, for a linked issue/MR, not the session). Build client-side inprocess_resultfromworkflow.project.webUrl+/-/automate/agent-sessions/<numeric id>.workflow_id: unwrap the numeric id from the GraphQLidGID.poll_after_seconds: not part of the mutation response — keep as a fixed constant in the Tool/Service.process_resultchecks the mutation's in-banderrorsarray and returnsResponse.errorwhen non-empty.- Annotations:
readOnlyHint: false,destructiveHint: false(create, non-destructive). - Register in
EE_GRAPHQL_TOOLS(notEE_CUSTOM_TOOLS) inee/app/services/ee/mcp/tools/manager.rb; registertrigger_duo_developeras atool_aliasesentry. - Specs:
spec/graphql/all_queries_spec.rbcoverage comes free from the committed.graphqlfile; add Tool/Service specs covering foundational flow, custom flow (viaflow_item_id), and missing/inaccessible project. "Unknown flow" needs verification, not assumption:Ai::DuoWorkflows::Workflowhas no validation rejecting an unrecognizedworkflow_definitionstring (onlyagent_privilegesis validated) — confirm whether an unrecognizedflowvalue should actually error at the tool level (validate client-side againstAi::FoundationalChatAgent/FoundationalFlowbefore calling the mutation) rather than assuming the mutation already rejects it. - Set enum for source to MCP (enum added in https://gitlab.com/gitlab-org/gitlab/-/work_items/627560#note_3807213267)
Demo video from the R&D Summit: https://gitlab.com/groups/gitlab-org/-/work_items/22652#note_3589447033
Original Draft MR (superseded by the GraphQL-backed plan above): !246597 (closed)