get_pipeline MCP tool
<!-- mcp-tool-guidance-callout -->
> [!note]
> **Before picking up this work:** this issue adds an MCP tool. Please follow the [Adding a new tool](https://docs.gitlab.com/development/duo_agent_platform/mcp/#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.
## Problem Statement / Use Case
Agents debugging CI need a pipeline's status plus its jobs, downstream (child) pipelines, and bridge jobs. Today these are ~4 discrete DAP reads (`get_pipeline` + `list_pipeline_jobs`/`get_downstream_pipelines`/`get_failing_bridge_jobs`, with a separate failing-jobs tool). `get_pipeline` folds them into one facet reader via `include`, and replaces the separate "failing jobs" tool with a `job_status` filter on the `jobs` facet.
## Scope and Non-Goals
- **In scope:** single-pipeline metadata; facets `jobs`, `downstream_pipelines`, `bridge_jobs`; `job_status` filter on `jobs`.
- **Non-goals:** listing pipelines (`list_pipelines`), lifecycle actions (`save_pipeline`), job logs (`get_job`), CI validation (`validate_ci_file`), a `url` resource-identification parameter (deferred — additive, can be added later without a breaking change).
- **Follow-ups:** `exclude_allow_failure` was cut from v1 (see Implementation notes below) — file a follow-up to add it back via a `Resolvers::Ci::JobsResolver` argument if it's needed.
## Data Shape and Context Engineering
`include` bounds what's fetched. The `job_status` filter (e.g. `failed`) replaces a redundant `failing_jobs` facet — filtering is preferred over naming a new facet for a subset. Applies only to the `jobs` facet.
- Input schema (as shipped):
```json
{
"tool": "get_pipeline",
"description": "Get a pipeline and optionally its jobs, downstream pipelines, or bridge jobs.",
"parameters": {
"id": "string (required) - project ID or URL-encoded path",
"pipeline_id": "integer (required)",
"include": "array, max 1 item (optional; one of: jobs, downstream_pipelines, bridge_jobs)",
"job_status": "enum (optional; e.g. failed — filters the jobs facet)",
"first": "integer (optional; page size for the selected include facet, default 20, max 100)",
"after": "string (optional; cursor for the selected include facet, from the previous response's page_info.end_cursor)"
}
}
```
- Output schema (JSON example):
```json
{
"id": 987654,
"status": "failed",
"ref": "main",
"sha": "9a1b2c",
"source": "push",
"web_url": "https://gitlab.example.com/group/project/-/pipelines/987654",
"jobs": [
{ "id": 111, "name": "rspec", "status": "failed", "stage": "test", "allow_failure": false, "web_url": "..." }
],
"page_info": { "has_next_page": false, "end_cursor": null }
}
```
`downstream_pipelines` and `bridge_jobs` follow the same shape when requested via `include`; only the requested facet's key is present in the response.
## Backward compatibility
Consolidates several shipped read tools into this one facet reader: the base pipeline read, the pipeline-jobs read, plus `get_pipeline_failing_jobs`, `get_downstream_pipelines`, and `get_failing_bridge_jobs` (Python `duo_workflow_service/tools/pipeline.py`). The base `get_pipeline` name is unchanged, but those sub-reads become `include` facets (`jobs`/`downstream_pipelines`/`bridge_jobs`) plus a `job_status` filter, so their call shapes change — name aliases alone cannot reproduce them. Keep the superseded tools available during migration and document the facet mapping. Follow the tool-renaming guidance in `doc/development/duo_agent_platform/mcp/_index.md`.
### Resources (already implemented similar tools etc.)
- Single pipeline: `Query.project(fullPath:) { pipeline(id:) }` (`Resolvers::Ci::ProjectPipelineResolver`) → `Types::Ci::PipelineType`. The `id` argument is a GID whose numeric part is the same global pipeline id REST uses — build it from `pipeline_id` as `"gid://gitlab/Ci::Pipeline/#{pipeline_id}"` (`id`/`iid`/`sha` are mutually exclusive; use `id`, not `iid`).
- `jobs` facet: `PipelineType.jobs` (`Resolvers::Ci::JobsResolver`) — filterable by `statuses: [CiJobStatus]` (→ this tool's `job_status`) and `job_kind: CiJobKind`. `JobType` exposes `id`, `name`, `status`, `stage`, `allowFailure`. **Note:** this field is a real GraphQL connection (`JobType.connection_type`), not a bounded inline list — implemented with native `first`/`after` cursor pagination, not the `page`/`per_page` used elsewhere in this domain.
- `bridge_jobs` facet: same `jobs` resolver with `job_kind: BRIDGE` (`Types::Ci::JobKindEnum` has a `BRIDGE` value = `::Ci::Bridge`) — no separate bridge field needed.
- `downstream_pipelines` facet: `PipelineType.downstream` (a `PipelineType` connection, `method: :triggered_pipelines_with_preloads`) — direct match for child pipelines.
- **`exclude_allow_failure` — cut from v1.** `JobsResolver`'s full argument set is `job_kind`, `retried`, `security_report_types`, `statuses`, `when_executed` — no `allow_failure` filter. Because `jobs` is a real paginated connection, filtering it out client-side (Option A below) would silently return fewer than `first` results per page, which breaks the pagination contract. Two ways to bring it back if needed:
- **Option A (client-side reject):** only safe if the `jobs` facet stops being independently paginated (for example, always fetching everything and paginating after filtering).
- **Option B (resolver argument, recommended):** add an `exclude_allow_failure:` (or `allow_failure:`) argument to `Resolvers::Ci::JobsResolver`, filtering server-side. File as a separate blocking issue in this sub-epic before re-adding the parameter here.
- Reference shape: `list_merge_requests` (`app/services/mcp/tools/merge_requests/list_merge_requests_{tool,service}.rb`) — GraphQL-backed reader resolving a project + selecting nested connections.
- MCP dev guidelines: `doc/development/duo_agent_platform/mcp/_index.md`; `gitlab-mcp-tool-builder` skill's build recipe — verify a GraphQL field exists before writing a custom one.
### Implementation Plan
1. Two classes: `Mcp::Tools::Pipelines::GetPipelineTool < Mcp::Tools::Base::GraphqlTool` and `Mcp::Tools::Pipelines::GetPipelineService < Base::GraphqlService`. **Shipped as planned.**
2. Operation file: `app/graphql/queries/mcp/pipelines/get_pipeline.query.graphql`, calling `project(fullPath:) { pipeline(id:) { … } }`. Selects base fields (`id status ref sha source path`) always; selects `jobs(jobKind: BUILD, statuses:, first:, after:)`, `downstream(first:, after:)`, and `jobs(jobKind: BRIDGE, first:, after:) as bridgeJobs`, each behind an `@include(if:)` directive driven by `include`.
3. `id` (project path) → `projectPath` via `find_project!`; `pipeline_id` → `id: "gid://gitlab/Ci::Pipeline/#{pipeline_id}"`. `url` support deferred (see Scope and Non-Goals).
4. `job_status` → `statuses` (upcased to match the `CiJobStatus` enum's external representation); `bridge_jobs` facet → `jobs(jobKind: BRIDGE)`; `downstream_pipelines` → `downstream`. All `id` GIDs are unwrapped to plain integers in `process_result`, and `status` enum values are downcased back to match the rest of the pipeline-domain tools (`list_pipelines`, `manage_pipeline`).
5. **`exclude_allow_failure` — cut from v1.** See the "Resources" section above for the two options to bring it back.
6. Only one project identifier (`id`) is supported for now; no cross-validation needed since `url` was deferred.
7. Registered in `GRAPHQL_TOOLS` in `app/services/mcp/tools/manager.rb`; the superseded DAP tools remain available during migration and the facet mapping is documented in `doc/user/model_context_protocol/mcp_server_tools.md`.
8. Specs: `spec/graphql/all_queries_spec.rb` coverage comes free from the committed `.graphql` file. Added Tool and Service specs for the base read, each `include` facet, `job_status` filtering, pagination (`first`/`after` and `page_info`), and missing/inaccessible pipeline — including a request-spec case for a caller without `read_pipeline`, and cases proving `Types::Ci::PipelineType`'s object-level `authorize :read_pipeline` automatically omits a `downstream_pipelines`/`bridge_jobs` entry the caller can't read (no hand-rolled authorization needed).
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