CI/CD Pipelines · 2026-07-11 · CI/CD integrations arc
MCP Tools for CI/CD Pipelines: The Async Run Lifecycle, Log Retrieval Architecture, and Runner Health Separation Across GitHub Actions, GitLab CI, Bitbucket Pipelines, Tekton, Jenkins, and Azure DevOps
When you build your first MCP tool that triggers a CI/CD pipeline, three problems appear in a predictable sequence. You call the trigger endpoint, the API returns immediately, and you report success — not realizing the returned handle is a run ID for polling, not a result. You poll the run status until it reports completion and then try to return the failure details from the status response — discovering that logs and artifacts are at a different endpoint entirely, addressed by a different ID scheme. You build a health check that confirms your MCP server can reach the CI system's web UI — and later find that all builds are queuing indefinitely because every registered runner is offline. By the second CI/CD platform you recognize all three problems wearing a different platform's uniform. This synthesis covers six CI/CD platforms — GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Tekton, Jenkins, and Azure DevOps — through the three patterns they all share, so you can identify and implement them correctly before they surface as production failures.
TL;DR
Six different CI/CD platforms, three shared patterns. (1) The async run lifecycle: every CI/CD trigger endpoint returns a run ID, not a result; polling until terminal state is required on every platform; terminal state sets differ substantially — GitHub Actions uses status === 'completed' (with conclusion as the actual result), GitLab CI uses success/failed/canceled/skipped (but not manual — that is an in-progress state), Bitbucket Pipelines uses a two-field state.name === 'COMPLETE' then state.result.name check, Tekton checks status.conditions[type=Succeeded].status !== 'Unknown' on the PipelineRun CRD, Jenkins checks build.building === false (not build.result !== null), and Azure DevOps checks build.status === 'completed' then build.result; treating any single platform's terminal state model as universal produces silent polling loops or premature result reporting. (2) The log/artifact retrieval architecture: pipeline run status responses report outcome metadata only; logs and artifacts always require a separate API call with a different addressing scheme; GitHub Actions logs require a ZIP download keyed by artifact ID (two hops for artifacts); GitLab CI logs are per-job traces keyed by job_id (a separate entity from the pipeline); Bitbucket Pipelines logs are per-step keyed by step_uuid (which must be discovered from the steps list); Tekton logs are Kubernetes pod logs accessed via /api/v1/namespaces/:ns/pods/:pod/log?container=:step (pod name discovered from TaskRun status); Jenkins uses progressive log polling at /logText/progressiveText?start=N until X-More-Data: false; Azure DevOps provides structured log entry IDs via a timeline API enabling selective stage/job/task log retrieval. (3) The runner/executor health vs CI server health separation: a CI server that accepts trigger calls and returns 200 may have zero agents available to execute queued builds; GitHub Actions self-hosted runners: GET /repos/.../actions/runners (check status === 'online' && !busy); GitLab: GET /projects/:id/runners (active && online && !paused); Bitbucket: Atlassian-managed (no runner health API for cloud runners); Tekton: Tekton Pipelines controller Deployment health + kubectl get pods -l tekton.dev/task; Jenkins: GET /computer/api/json (ratio of idle executors to queued items is the key metric); Azure DevOps: GET /_apis/distributedtask/pools/:poolId/agents (status === 'online' && enabled). Wire AliveMCP to a composite health endpoint that checks CI server reachability, credential validity, and the ratio of available runners to queued builds — not just the web UI liveness.
Pattern 1: The async pipeline run lifecycle
Every CI/CD platform exposes pipeline triggering as a fire-and-forget API call that returns control immediately. The response contains a run identifier — a build number, a UUID, a Kubernetes resource name — but never the build result. The result is not known yet; the build has just been queued or started. An MCP tool that reports "build triggered successfully" and returns is giving the calling agent half of the information it needs. The correct design is a two-step tool or a single tool that polls internally: trigger the build, capture the run ID, poll the status endpoint at an interval appropriate for the expected build duration (typically 15–30 seconds per poll to avoid rate limits), and return the terminal result including outcome, duration, and a reference to the log endpoint. Each platform has its own terminal state model, and treating one platform's model as universal is a reliable source of bugs.
GitHub Actions: status plus conclusion
GitHub Actions workflows are triggered via POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches for manual dispatch, or via push/pull_request events via the webhook. The dispatch endpoint returns 204 No Content — there is no run ID in the response. To obtain the run ID, poll GET /repos/{owner}/{repo}/actions/runs?event=workflow_dispatch&branch={branch}&created={timestamp_range} immediately after triggering and capture the most recently created run matching your trigger. This two-step pattern (trigger → list runs to find your run) is specific to Actions and distinguishes it from every other platform.
Once you have the run ID, poll GET /repos/{owner}/{repo}/actions/runs/{run_id}. The status field progresses through queued → in_progress → completed. Terminal state is only status === 'completed'. The conclusion field holds the actual result: success, failure, cancelled, timed_out, action_required, neutral, or skipped. The conclusion field is null while the run is not yet completed — reading conclusion before checking status === 'completed' produces silent null returns that look like successful builds. A helper: const isTerminal = run => run.status === 'completed'; const succeeded = run => run.conclusion === 'success'.
GitLab CI: five states, two terminal sets
GitLab pipelines are triggered via POST /projects/{id}/pipeline (manual trigger), POST /projects/{id}/trigger/pipeline (trigger token), or via push events. The trigger response includes the pipeline object with an id field — unlike GitHub Actions, the run ID is in the trigger response. Poll GET /projects/{id}/pipelines/{pipeline_id} and inspect the status field.
GitLab CI status values form two sets: in-progress (created, waiting_for_resource, preparing, pending, running, manual, scheduled) and terminal (success, failed, canceled, skipped). The critical trap is manual: a pipeline with a stage that has a manual-trigger job sits in manual state waiting for a human to click "run" in the GitLab UI. From the API's perspective, this pipeline is not terminal — it is suspended waiting for user interaction. If your MCP tool treats manual as a non-running, non-terminal state and continues polling, it will poll forever unless the pipeline times out. The correct handling is to detect manual, surface it to the agent as "pipeline paused waiting for manual approval," and stop polling — the agent cannot unblock a manual gate via the pipeline status API (it can trigger specific jobs via POST /projects/{id}/jobs/{job_id}/play, but that requires knowing the job ID and having the appropriate authorization). The scheduled status has a similar semantics: the pipeline is waiting for its scheduled trigger time.
Bitbucket Pipelines: two-field state structure
Bitbucket Pipelines are triggered via POST /repositories/{workspace}/{repo_slug}/pipelines/. The response includes a uuid field for the pipeline run. Poll GET /repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}. Bitbucket uses a two-field state structure distinct from all other platforms: the state.name field reports the broad phase (PENDING, IN_PROGRESS, PAUSED, COMPLETE), while the state.result.name field reports the outcome and is only populated when state.name === 'COMPLETE'.
Terminal state check: if (pipeline.state.name === 'COMPLETE') { const result = pipeline.state.result.name; }. Result values: SUCCESSFUL, FAILED, ERROR, STOPPED. FAILED means one or more steps failed; ERROR means the pipeline itself had a configuration or infrastructure error (e.g., the Docker image could not be pulled, or the pipeline YAML was invalid); STOPPED means it was manually stopped. A pipeline in PAUSED state has hit a manual approval gate — similar to GitLab's manual state, it will not advance without user action. Do not read state.result before confirming state.name === 'COMPLETE'; state.result is absent from the response object (not null, but entirely absent) for in-progress pipelines, causing property access errors in strict JavaScript.
Tekton: Kubernetes conditions with tri-value status
Tekton has no HTTP trigger API — pipeline runs are created by applying a PipelineRun CRD object (kubectl apply -f pipelinerun.yaml) or via the Tekton Triggers EventListener webhook relay. From an MCP tool's perspective, creating a PipelineRun means calling the Kubernetes API: POST /apis/tekton.dev/v1/namespaces/{ns}/pipelineruns with the PipelineRun manifest. The response includes the resource's metadata.name, which is the polling handle.
Poll GET /apis/tekton.dev/v1/namespaces/{ns}/pipelineruns/{name} and inspect status.conditions. Tekton uses the Kubernetes condition pattern: an array of condition objects each with type, status (True, False, Unknown), reason, and message. The relevant condition is the one with type: 'Succeeded'. Terminal state is when status !== 'Unknown': True means succeeded; False means failed or cancelled. The reason field on a False condition distinguishes failure sub-types: Failed (a task failed), PipelineRunCancelled, PipelineRunStopped, PipelineRunCouldntCancel, PipelineRunTimeout. A PipelineRun with no conditions at all (conditions array is empty or absent) has just been created and is not yet reconciled by the Tekton controller — treat this as Unknown.
Helper: function tektonTerminal(run) { const s = run.status?.conditions?.find(c => c.type === 'Succeeded'); return s ? s.status !== 'Unknown' : false; }. The status.completionTime field is a reliable terminal indicator when present, but prefer checking conditions explicitly since completionTime can theoretically be set on failed runs where conditions were cleared by a reconciler bug.
Jenkins: building flag over result field
Jenkins builds are triggered via POST /job/{job_name}/build (no parameters) or POST /job/{job_name}/buildWithParameters. The response is a 201 Created with a Location header pointing to the queue item: Location: http://jenkins/queue/item/N/. The queue item transitions to an actual build only once an executor picks it up. Poll GET /queue/item/{N}/api/json until executable is non-null in the response — executable.url and executable.number give you the build URL and build number.
Once you have the build number, poll GET /job/{job_name}/{build_number}/api/json. The idiomatic terminal state check is build.building === false — not build.result !== null. The result field is set once the build finishes and can hold SUCCESS, FAILURE, UNSTABLE, ABORTED, or NOT_BUILT. The trap: in some Jenkins versions and plugin combinations, result may be set to FAILURE while building is still true during pipeline stages that fail-fast — reading result as the primary terminal indicator can produce premature completion detection. Always check building === false first.
UNSTABLE is a Jenkins-specific state with no equivalent on other platforms: it means the build ran to completion but one or more quality checks (typically test reports or static analysis) reported below-threshold results. A Jenkins build author can designate specific failures as "unstable" rather than "failed" to allow downstream stages to still run. MCP tools should surface UNSTABLE as a distinct outcome state, not collapse it into FAILURE.
Azure DevOps: completed status plus result field
Azure DevOps builds are triggered via POST /{org}/{project}/_apis/build/builds?api-version=7.1 with a request body specifying the definition.id (pipeline definition ID) and optionally sourceBranch and parameters. The response includes the build object with an id field. Poll GET /{org}/{project}/_apis/build/builds/{buildId}?api-version=7.1.
Azure DevOps uses the same two-field pattern as Bitbucket Pipelines: status (notStarted, inProgress, cancelling, completed, postponed, none) and result (null until completed, then succeeded, failed, canceled, partiallySucceeded). Terminal state: build.status === 'completed'. The partiallySucceeded result is Azure DevOps-specific: it indicates that some stages were skipped or failed but the pipeline was configured with continueOnError: true at the stage level and reached the end. Do not treat partiallySucceeded as equivalent to succeeded in MCP tool responses — surface it explicitly, as the agent may need to investigate which stages failed.
Cross-platform terminal state reference
A single lookup table for the six platforms covered, covering the polling endpoint, the terminal state condition, and the result field semantics:
| Platform | Config file | Trigger response ID | Status poll endpoint | Terminal state check | Result values |
|---|---|---|---|---|---|
| GitHub Actions | .github/workflows/*.yml |
Must list runs after dispatch (204 response) | GET /repos/.../actions/runs/:run_id |
run.status === 'completed' |
success, failure, cancelled, timed_out, action_required, neutral, skipped |
| GitLab CI | .gitlab-ci.yml |
pipeline.id in trigger response |
GET /projects/:id/pipelines/:pipeline_id |
['success','failed','canceled','skipped'].includes(pipeline.status) |
success, failed, canceled, skipped; manual/scheduled are suspended (not terminal) |
| Bitbucket Pipelines | bitbucket-pipelines.yml |
pipeline.uuid in trigger response |
GET /repositories/.../pipelines/:uuid |
pipeline.state.name === 'COMPLETE' |
state.result.name: SUCCESSFUL, FAILED, ERROR, STOPPED |
| Tekton | Kubernetes CRDs | metadata.name of PipelineRun resource |
GET /apis/tekton.dev/v1/namespaces/:ns/pipelineruns/:name |
conditions.find(c => c.type==='Succeeded')?.status !== 'Unknown' |
condition status: True (success) or False (failed/cancelled); reason field gives sub-type |
| Jenkins | Jenkinsfile |
Queue item in Location header; must poll queue to get build number |
GET /job/:job_name/:build_number/api/json |
build.building === false |
result field: SUCCESS, FAILURE, UNSTABLE, ABORTED, NOT_BUILT |
| Azure DevOps | azure-pipelines.yml |
build.id in trigger response |
GET /:org/:proj/_apis/build/builds/:id |
build.status === 'completed' |
result field: succeeded, failed, canceled, partiallySucceeded |
The confirm guard pattern applies to pipeline triggers on every platform. Triggering a CI/CD pipeline is not easily reversible — cancelling requires a separate API call, and some deployments within the pipeline (Kubernetes apply, SSH-based deploys) may have already executed by the time a cancel arrives. For pipelines that target production environments, MCP tools should implement a preview/confirm pair: the preview tool shows which pipeline will be triggered, which branch, and what environment variables or parameters, and the confirm tool performs the actual trigger. This is especially important when the agent context includes ambiguous branch references or when the pipeline targets production via a protected environment gate.
Pattern 2: The log and artifact retrieval architecture
The run status endpoint answers one question: is the build done, and did it succeed? It does not answer the questions that follow: what failed, why, which step printed the error, and what artifacts were produced? Build logs and artifacts are separate resources on every CI/CD platform, addressed by a different identifier scheme and sometimes requiring multiple API hops to reach. An MCP tool that only polls run status and returns the top-level outcome is operationally incomplete — the agent cannot diagnose a build failure or locate a produced artifact without the secondary retrieval. The addressing model differs enough across platforms that per-platform implementation is required; there is no shared abstraction.
GitHub Actions: ZIP downloads and artifact ID hops
GitHub Actions separates logs from artifacts. Workflow run logs are downloadable as a ZIP archive via GET /repos/{owner}/{repo}/actions/runs/{run_id}/logs — the response is a redirect to a time-limited signed URL for the ZIP. Inside the ZIP, each job and step has its own log file named {step_number}_{step_name}.txt. There is no streaming, line-by-line log API; all logs for a run must be fetched as a batch ZIP and parsed client-side. For an MCP tool, this means downloading the ZIP to a temporary buffer, extracting the relevant step log, and returning the extracted text — not streaming the ZIP directly to the agent.
Artifacts are addressed by a two-hop system. First, list artifacts for the run: GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts — returns an array with artifact id, name, size_in_bytes, and expired. Then download a specific artifact by ID: GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip — again a redirect to a signed ZIP URL. The artifact name (e.g., test-results) is the human-readable identifier; the artifact id is the numeric API identifier. MCP tools should surface both in their response so the agent can reference the artifact name while the tool uses the ID internally for retrieval.
GitHub Actions logs expire after 90 days and artifacts expire after the repository's configured retention period (default 90 days for public repos, configurable). An MCP health check that validates log retrieval should check whether the test run's logs are still accessible before reporting a retrieval failure as an API error versus an expiry.
GitLab CI: per-job traces addressed by job_id
GitLab CI log retrieval is fundamentally at the job level, not the pipeline level. A GitLab pipeline is a collection of jobs grouped into stages; the pipeline status is the aggregate of all job statuses. Log output ("trace" in GitLab's terminology) is per-job at GET /projects/{id}/jobs/{job_id}/trace — raw text, no redirect, streamed directly in the response body.
The pipeline status API does not include job IDs — you must first list pipeline jobs: GET /projects/{id}/pipelines/{pipeline_id}/jobs. The response is an array of job objects each with id, name, stage, status, and web_url. For a failing pipeline, filter for jobs with status: 'failed' to find the job whose trace contains the error. In pipelines with parallel jobs in the same stage, multiple jobs can fail simultaneously — log retrieval must handle multiple failing job IDs.
GitLab CI also supports artifacts via GET /projects/{id}/jobs/{job_id}/artifacts (downloads the entire artifact ZIP) or GET /projects/{id}/jobs/{job_id}/artifacts/{artifact_path} (downloads a specific file within the artifact). The key difference from GitHub Actions: GitLab artifact paths are relative paths within the artifact ZIP rather than named artifact bundles with IDs. To list files within an artifact ZIP, use GET /projects/{id}/jobs/{job_id}/artifacts?file_type=archive — this is a page-level API that returns file paths within the archive.
Bitbucket Pipelines: per-step logs addressed by step UUID
Bitbucket Pipelines logs are per-step (each item in the steps: list in bitbucket-pipelines.yml is a step with its own log). Step log retrieval: GET /repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}/log — raw text, streamed directly. The step UUID is not returned in the pipeline trigger response — you must first list pipeline steps: GET /repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/, which returns an array of step objects each with uuid, name, and state.
Bitbucket Pipelines does not have a general artifact API equivalent to GitHub Actions or GitLab CI in the REST API as of mid-2026. Artifacts produced by a step must be deployed to an external storage provider (e.g., an S3 bucket via the atlassian/aws-s3-deploy pipe, or Bitbucket Deployments for deployment targets) to be retrievable programmatically. MCP tools targeting Bitbucket Pipelines should not attempt to retrieve build artifacts via the Bitbucket API — instead, retrieve them from whichever external store the pipeline writes to, guided by the pipeline definition.
Tekton: Kubernetes pod logs accessed by pod name
Tekton logs are Kubernetes pod logs. Each TaskRun creates a Pod; the Pod's containers correspond to the Task's steps. Log retrieval for a TaskRun: read status.podName from the TaskRun object, then fetch the pod log via the Kubernetes API: GET /api/v1/namespaces/{ns}/pods/{pod_name}/log?container={step_container_name}. Step containers are named step-{step_name} by convention; the actual container names are listed in the TaskRun's status.steps[].container field.
For a PipelineRun, collect logs from each TaskRun individually. List TaskRuns associated with the PipelineRun: GET /apis/tekton.dev/v1/namespaces/{ns}/taskruns?labelSelector=tekton.dev/pipelineRun={pipelinerun_name}. For each TaskRun, retrieve its pod name from status.podName and fetch per-step logs. This is the most multi-hop log retrieval in the arc: PipelineRun → list TaskRuns → per TaskRun get pod name → per pod get per-step log. The Tekton Dashboard provides a single-page view of all pipeline logs, but the programmatic path through the Kubernetes API requires all these hops.
Kubernetes pod logs have a time-to-live determined by the cluster's log rotation configuration and whether the pods still exist. Tekton's PipelineRun and TaskRun resources have a configurable retention policy via Tekton's config-leader-election ConfigMap; after the retention period, completed TaskRun pods are garbage collected and their logs are no longer accessible via the Kubernetes API. Production Tekton setups typically route logs to an external log aggregator (Elasticsearch, Loki, Cloud Logging) — health check design should account for log storage backend connectivity as a separate health dimension.
Jenkins: progressive text polling with byte offset
Jenkins console output is unique among the six platforms for offering native progressive (streaming) log retrieval. The endpoint is GET /job/{job_name}/{build_number}/logText/progressiveText?start={byte_offset}. The response body contains the log text starting at the requested byte offset, and two response headers control continuation: X-Text-Size (total bytes written so far) and X-More-Data (true if the build is still running; false when the build completes and the full log is available).
Progressive polling pattern: start with start=0, append the response body to a buffer, read the new X-Text-Size as the next start value, and repeat until X-More-Data: false. This is different from polling the build status endpoint — the log stream itself signals completion via the X-More-Data header. For MCP tools that need to tail build output in near-real-time (useful for deployments with observable progress), this is the cleanest approach across all six platforms: no ZIP extraction, no secondary ID discovery, just progressive byte-range fetching.
Jenkins artifacts are listed in the build JSON under the artifacts array: each entry has fileName, relativePath, and displayPath. Download via GET /job/{job_name}/{build_number}/artifact/{relativePath}. No secondary ID hop is required — the relative path from the build JSON is the download path directly. Jenkins stores artifacts on the Jenkins controller's file system (or a cloud artifact store if the Artifact Manager plugin is configured); artifact retention is controlled by the job's build discarder settings.
Azure DevOps: structured timeline with selective log retrieval
Azure DevOps provides the most structured log retrieval model of the six platforms. First, fetch the build timeline: GET /{org}/{project}/_apis/build/builds/{buildId}/timeline?api-version=7.1. The timeline returns a flat list of records — stages, phases, jobs, tasks — each with a numeric log.id, a state, a result, and a name. This enables selective log retrieval: find the records with result === 'failed', read their log.id values, and fetch only the logs for failed tasks.
Fetch a specific log by ID: GET /{org}/{project}/_apis/build/builds/{buildId}/logs/{logId}?api-version=7.1 — returns plain text. To list all logs without the timeline: GET /{org}/{project}/_apis/build/builds/{buildId}/logs?api-version=7.1 returns a log collection with IDs, types, and download URLs. The timeline-first approach is preferred for MCP tools because it maps log IDs to named stages and tasks — the agent can be told "the Deploy to AKS task in the production stage failed" rather than having to inspect all log IDs to determine which one is relevant.
Azure DevOps pipeline artifacts are published via the PublishPipelineArtifact@1 task and retrieved via GET /{org}/{project}/_apis/build/builds/{buildId}/artifacts?api-version=7.1 — returns artifact names and download URLs. Signed artifact download URLs are embedded in the response and expire after a short window; re-fetch the artifacts API to get fresh URLs rather than caching the signed URL across sessions.
Pattern 3: Runner/executor health vs CI server health
The most dangerous invisible failure in CI/CD MCP integrations is the state where the CI server is reachable and accepting build triggers, all health checks return 200, but zero agents are available to execute the queued builds. Builds queue indefinitely. There are no errors in the API responses — the queue accumulates silently. An MCP tool that checks only CI server liveness will report healthy while every triggered build waits in an empty executor queue. The fix is always the same: separate the CI server health check from the runner availability check, and wire both into the composite health probe that external monitoring — like AliveMCP — polls.
GitHub Actions: online and non-busy self-hosted runners
For GitHub-hosted runners (the default), runner availability is managed by GitHub and not exposed via the API — capacity issues surface only as increased queue wait times. For self-hosted runners, use GET /repos/{owner}/{repo}/actions/runners (repository-scoped) or GET /orgs/{org}/actions/runners (organization-scoped). Each runner object has status (online or offline) and busy (boolean indicating whether it is currently executing a job). Available runner count: runners.filter(r => r.status === 'online' && !r.busy).length.
Runners are registered per label. A workflow's runs-on key specifies a label; if no online, non-busy runner has that label, the job queues. The runners API does not expose per-label capacity vs demand — that correlation requires comparing available runners per label against queued jobs per workflow. For health monitoring, the practical check is total available (online && !busy) vs zero: if available runners drops to zero and there are recent build triggers, alert before queuing time reaches user-visible thresholds.
GitLab CI: per-project vs group vs instance runners
GitLab has three runner scopes: instance runners (managed by the GitLab admin, available to all projects), group runners (available to all projects in a group), and project runners (registered specifically for one project). The runner API reflects this hierarchy. For an MCP tool scoped to a specific project, use GET /projects/{id}/runners?status=online — this returns all runners accessible to the project across all scopes. Each runner has active (whether the runner is set to pick up jobs), paused (whether it has been administratively paused), and the status field (online/offline based on heartbeat).
Healthy runner check per runner object: runner.active && !runner.paused && runner.status === 'online'. The most common misconfiguration on GitLab is a runner that is registered and appears in the API response but has been paused by an administrator — paused runners do not pick up new jobs, and the pause state is only visible in the paused field, not in the status field. A runner with status: 'online' and paused: true looks healthy in a status-only health check but picks up no work.
Tekton: controller deployment health as the CI server
Tekton's "CI server" is the tekton-pipelines-controller Deployment in the tekton-pipelines namespace. The controller is the reconciliation loop that transitions PipelineRuns and TaskRuns from pending to running state. If the controller is degraded — OOMKilled, crashlooping, or stuck in a reconciliation error — PipelineRuns sit in pending state indefinitely with no error response from the API. The Kubernetes API accepts the PipelineRun creation successfully; the failure is in the controller loop, not the API server.
Health check components for a Tekton-based MCP integration: (1) Kubernetes API connectivity — GET /healthz on the cluster API server; (2) Controller Deployment health — GET /apis/apps/v1/namespaces/tekton-pipelines/deployments/tekton-pipelines-controller, check status.availableReplicas > 0; (3) If Tekton Triggers is in use, the EventListener Service health — verify the EventListener Pod is running and the Service endpoint is reachable. A PipelineRun in Pending state for more than 2–3 minutes with a healthy controller is a signal of a resource starvation issue (no Kubernetes nodes with sufficient CPU/memory), not a Tekton issue — health check design should distinguish between controller health and cluster capacity.
Jenkins: executor availability in the computer API
Jenkins provides the most detailed runner health API of all six platforms. GET /computer/api/json?tree=computer[displayName,offline,temporarilyOffline,idle,numExecutors,executors[idle,likely_stuck]] returns a list of all nodes and agents with per-executor state. Key fields: offline (node is disconnected or not yet connected), temporarilyOffline (node was manually taken offline by an administrator but will be brought back), idle (no jobs currently running on this node), and numExecutors (total executor count configured on this node).
The derived metric that matters for capacity health: available executors = sum over online nodes of numExecutors where !offline && !temporarilyOffline minus busy executors = sum of executors where !executor.idle. Queue depth is at GET /queue/api/json — the items array length is the number of builds waiting for an executor. The dangerous state: queue depth > 0 with available executors = 0. Jenkins itself does not alert on this condition — it is visible only via the queue + computer APIs in combination.
Jenkins' own health check endpoint (GET /login, which returns 200 when the web UI is accessible) is entirely agnostic to executor availability. So is the classic health check of querying the Jenkins API root (GET /api/json) — it returns 200 whether the queue has 0 or 500 waiting builds. A Jenkins health probe that does not check executor availability is providing false assurance.
Azure DevOps: agent pool capacity via distributedtask API
Azure DevOps pipeline agents belong to agent pools. Pool list: GET /{org}/_apis/distributedtask/pools?api-version=7.1. Agents within a pool: GET /{org}/_apis/distributedtask/pools/{poolId}/agents?includeCapabilities=true&includeAssignedRequest=true&api-version=7.1. Each agent object has status (online or offline), enabled (boolean — disabled agents do not pick up jobs even if online), and assignedRequest (non-null if the agent is currently running a job).
Available agent count: agents.filter(a => a.status === 'online' && a.enabled && !a.assignedRequest).length. For Microsoft-hosted agent pools (the default pools like Azure Pipelines), the API still returns agent entries but their online/offline state is managed by Microsoft and reflects the agent VM lifecycle rather than a persistent state — Microsoft-hosted agents are created and destroyed per job. For Microsoft-hosted pools, the relevant health signal is queue wait time, not individual agent status. For self-hosted pools, individual agent status is the right monitoring target.
The enabled field on Azure DevOps agents is the equivalent of GitLab's paused field — an agent can be online (connected and heartbeating) but disabled (not picking up new jobs) by an administrator action. Health checks that only check status === 'online' will count disabled agents as available. Always check both status === 'online' && enabled === true.
Composite health endpoint design for CI/CD MCP integrations
A CI/CD MCP server's /health endpoint should aggregate three independent checks: (1) API credential validity, (2) CI server reachability, and (3) runner availability. These three can fail independently — a credential can expire while the CI server is healthy, a runner pool can be emptied while credentials are valid, and a CI server can be down while credentials are syntactically correct. Combining them in a single 200/503 response masks which layer failed.
The pattern used in composable health probes across this arc:
async function checkCIHealth() {
const checks = await Promise.allSettled([
checkCredentials(), // validates API token/credentials
checkServerReach(), // validates CI server API is responding
checkRunnerPool(), // validates at least one runner is available
]);
return {
status: checks.every(c => c.status === 'fulfilled' && c.value.ok)
? 'healthy'
: 'degraded',
credentials: checks[0].status === 'fulfilled' ? checks[0].value : { ok: false, error: checks[0].reason?.message },
server: checks[1].status === 'fulfilled' ? checks[1].value : { ok: false, error: checks[1].reason?.message },
runners: checks[2].status === 'fulfilled' ? checks[2].value : { ok: false, error: checks[2].reason?.message },
};
}
Register the /health/cicd URL with AliveMCP rather than the CI server's own status page. The CI server's status page reports platform availability — it does not report whether your API token is valid, whether your self-hosted runner pool is drained, or whether your project-scoped runner was paused after a quarterly infrastructure audit. Those are the failure modes that affect your builds specifically, and only your composite health endpoint can surface them.
MCP tool design: confirm guards for pipeline triggers
Triggering a CI/CD pipeline — especially one that deploys to production — is a write operation that can have real downstream effects before it can be cancelled. The design pattern that applies across all six platforms is the preview/confirm tool pair: a preview_pipeline_trigger tool that returns the full trigger intent (which pipeline, which branch, which environment, which parameters, estimated duration from recent runs) without executing it, and a trigger_pipeline tool that performs the actual trigger after the agent has confirmed the intent with the user or with an automated policy check.
| Pipeline operation | Platform | Effect | Confirm guard required? |
|---|---|---|---|
| Trigger build on main branch | All | Deploys to production if pipeline targets prod | Yes — show branch, pipeline name, and target environment |
| Cancel in-progress build | All | Halts pipeline mid-deployment; may leave infrastructure in partial state | Yes — show current stage and what has already deployed |
| Re-run failed build | GitHub Actions, GitLab, Azure DevOps | Re-executes deployment steps; may duplicate infrastructure changes | Yes — show which steps will re-run and their deployment scope |
| Trigger manual stage | GitLab CI, Azure DevOps | Unblocks a deployment gate; may be one-way | Yes — show the stage being unblocked and its deployment target |
| List recent builds | All | Read-only | No |
| Fetch build status | All | Read-only | No |
| Retrieve build logs | All | Read-only | No |
An additional MCP-specific consideration is duplicate run prevention. Agents can be invoked multiple times on the same conversational context — for example, if the user asks "trigger the deploy pipeline and wait for it to finish" and the session is interrupted and resumed, the agent may attempt to trigger the pipeline again. The guard is a run ID check: before triggering, call list_recent_runs and check whether a run triggered from the same commit SHA or branch within the last N minutes is still in progress. If so, return the existing run ID rather than creating a duplicate. This pattern is most important for Jenkins (where queued builds don't expose a run ID until an executor picks them up) and GitHub Actions (where the trigger response is 204 with no run ID, so a lost run ID requires list-then-filter to recover).
MCP tool design: pagination for listing runs
All six platforms paginate their run listing APIs, and the pagination models are not interchangeable. Returning only the first page from any of these APIs is a common defect in MCP tool implementations — the most recent run may not be on the first page if a high volume of runs has been triggered since the last poll.
GitHub Actions GET /repos/.../actions/runs uses cursor-based pagination via the Link header (RFC 5988). Each response includes a Link: <url>; rel="next" header when there are additional pages. Page size is controlled by the per_page parameter (max 100). GitLab CI GET /projects/{id}/pipelines uses offset pagination via page and per_page query parameters (max 100), with a X-Total-Pages response header. Bitbucket Pipelines uses cursor-based pagination via a next field in the response body. Tekton uses Kubernetes API pagination via the continue token in the metadata.continue field of the list response. Jenkins GET /job/{job_name}/api/json?tree=builds[*]{N,M} uses range syntax ({start,end}) rather than page/cursor parameters. Azure DevOps uses $skip and $top OData-style parameters with a x-ms-continuationtoken header for additional pages.
For MCP tools that need to find a specific recent run (e.g., the run triggered from a specific commit SHA), the correct pattern is to filter at the API level using available query parameters — not to retrieve all pages and filter client-side. GitHub Actions supports ?head_sha={sha} for SHA-based run lookup; GitLab supports ?sha={sha}; Azure DevOps supports ?reasonFilter=manual&repositoryId=...&repositoryType=TfsGit&branchName=.... Using server-side filters reduces pagination depth and API quota consumption significantly when pipelines run frequently.
Summary: the three patterns as an implementation checklist
For every CI/CD MCP integration:
1. The async run lifecycle: Never return a trigger response as a build result. Capture the run ID from the trigger response (or discover it via list-after-trigger for GitHub Actions). Poll the status endpoint until the platform-specific terminal state is reached — status === 'completed' for GitHub Actions and Azure DevOps, the terminal status set for GitLab CI (remembering that manual is suspended, not terminal), state.name === 'COMPLETE' for Bitbucket, conditions[type=Succeeded].status !== 'Unknown' for Tekton, and building === false for Jenkins. Surface platform-specific result nuances (UNSTABLE for Jenkins, partiallySucceeded for Azure DevOps, manual/scheduled for GitLab) as distinct states — do not collapse them into binary success/failure.
2. The log/artifact retrieval architecture: Run status is never the same as run output. Implement separate log retrieval tools for each platform with the correct addressing model: by artifact ID after listing for GitHub Actions; by job_id (discovered from pipeline jobs list) for GitLab CI; by step UUID (discovered from steps list) for Bitbucket Pipelines; by pod name (from TaskRun status.podName) plus step container name for Tekton; by progressiveText with byte offset for Jenkins; by log ID (from timeline API) for Azure DevOps. Always surface the log endpoint URL or log ID in your run status response so the agent knows where to retrieve output when needed.
3. The runner/executor health vs CI server health: CI server availability and runner availability are independent failure modes. Wire a composite health endpoint that checks both. For GitHub Actions self-hosted: filter runners API for online && !busy. For GitLab: filter runners for active && !paused && online. For Tekton: check the tekton-pipelines-controller Deployment's status.availableReplicas. For Jenkins: check the computer API for non-offline nodes with at least one idle executor vs queue depth. For Azure DevOps: filter agents for status=online && enabled=true && no assignedRequest; apply this only to self-hosted pools (Microsoft-hosted pool capacity is not exposed). Register the composite health URL with AliveMCP — not the CI platform's own status page, which tells you about platform availability but not about your specific credentials, runner pool, or project-scoped runner configuration.
The common thread across all three patterns: CI/CD platforms are designed for human-in-the-loop interactions where a developer triggers a build, waits on a web dashboard, and opens log pages manually. MCP tools must make all three of those human-interactive steps programmable — the async-to-synchronous bridge (polling), the log retrieval (API-to-text), and the health signal (beyond process liveness). See also the DevOps Tooling guide for singleton client, credential lifecycle, and health transparency patterns that apply across all DevOps platform integrations, and the IaC and GitOps guide for deployment pipeline patterns downstream of CI/CD triggers.
Further reading
- MCP Servers with GitHub Actions — build, test, deploy, and verify the MCP protocol
- MCP Servers with GitLab CI/CD — pipeline stages, container registry, and protocol verification
- MCP Servers with Bitbucket Pipelines — build, deploy, and verify the MCP protocol
- MCP Servers with Tekton Pipelines — cloud-native Kubernetes CI/CD and protocol verification
- MCP Servers with Jenkins — declarative pipeline, Docker builds, and protocol verification
- MCP Servers with Azure DevOps Pipelines — build, deploy to Azure, and verify the MCP protocol
- MCP Servers with ArgoCD — GitOps deployment tools and application health monitoring
- MCP Server CI/CD Integration — patterns for build triggering, status polling, and deployment verification
- Building MCP Tools for DevOps Platforms — singleton client, credential lifecycle, and health transparency patterns
- MCP Tools for IaC and GitOps — Terraform, Pulumi, ArgoCD, and Flux deployment pipeline patterns