Guide · MCP CI/CD Integration
MCP Server CircleCI — trigger pipelines, get workflow status, and stream job logs
CircleCI's v2 API gives agents the ability to trigger pipelines, inspect workflow and job status, retrieve job logs, and cancel running workflows. This guide covers building TypeScript MCP tools around the CircleCI v2 REST API: authentication with a personal API token, project slug format, the pipeline → workflow → job hierarchy, artifact retrieval, and a /health/circleci endpoint so AliveMCP can detect token expiry before pipeline trigger calls silently fail.
TL;DR
Authenticate with Circle-Token: <token> header — not Basic auth. The project slug format is gh/org/repo or bb/org/repo. Triggering a pipeline returns a pipeline ID; fetch workflows via /pipeline/:id/workflow, then jobs via /workflow/:id/job. Job logs aren't served directly from the API — download them from the artifact or step output URLs returned by /project/:slug/job/:number/artifacts. Rate limit is 1,000 requests per minute per token. Wire AliveMCP to /health/circleci — a 401 response reveals a revoked token before your deployment workflows break.
Authentication and HTTP client setup
CircleCI's v2 API authenticates with a personal API token sent in the Circle-Token HTTP header. Generate a token at https://app.circleci.com/settings/user/tokens. The token has the same permissions as your CircleCI user — scope it to a machine account for production MCP servers.
import axios, { AxiosInstance } from 'axios';
const CIRCLECI_TOKEN = process.env.CIRCLECI_TOKEN!;
const CIRCLECI_BASE = 'https://circleci.com/api/v2';
// Singleton HTTP client — Circle-Token header pre-wired on every request
const cci: AxiosInstance = axios.create({
baseURL: CIRCLECI_BASE,
headers: {
'Circle-Token': CIRCLECI_TOKEN,
'Content-Type': 'application/json'
},
timeout: 15_000
});
// Project slug format: 'gh/myorg/myrepo' or 'bb/myorg/myrepo'
// gh = GitHub, bb = Bitbucket
// Encode as URL component when used in path segments: encodeURIComponent(slug)
The CircleCI v2 API base URL is https://circleci.com/api/v2 for cloud. For CircleCI Server (self-hosted), replace with your server's domain: https://circleci.yourdomain.com/api/v2. The token format and API shape are identical across cloud and server.
trigger_pipeline tool
The CircleCI pipeline is the top-level unit triggered by a commit or API call. Triggering via the API creates a pipeline that runs the project's .circleci/config.yml workflow. You can pass pipeline parameters — named values defined in the config — to customise the run.
import { z } from 'zod';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
server.tool(
'trigger_circleci_pipeline',
{
project_slug: z.string().regex(/^(gh|bb)\/.+\/.+$/, 'Must be gh/org/repo or bb/org/repo'),
branch: z.string().optional(), // omit to use the project's default branch
tag: z.string().optional(), // mutually exclusive with branch
parameters: z.record(
z.union([z.string(), z.boolean(), z.number()])
).optional()
},
async ({ project_slug, branch, tag, parameters }) => {
if (branch && tag) {
throw new McpError(ErrorCode.InvalidParams, 'Specify branch OR tag, not both');
}
const body: Record = {};
if (branch) body.branch = branch;
if (tag) body.tag = tag;
if (parameters && Object.keys(parameters).length > 0) body.parameters = parameters;
const res = await cci.post(
`/project/${encodeURIComponent(project_slug)}/pipeline`,
body
);
return {
content: [{
type: 'text',
text: JSON.stringify({
pipeline_id: res.data.id,
pipeline_number: res.data.number,
state: res.data.state,
project_slug,
branch: branch ?? null,
tag: tag ?? null
}, null, 2)
}]
};
}
);
Pipeline parameters must be declared in the .circleci/config.yml under the top-level parameters key before they can be passed via the API. Passing an undeclared parameter returns a 400 error with a clear message about which parameter name was unexpected.
get_pipeline_workflows and get_workflow_jobs tools
A pipeline runs one or more workflows. Each workflow contains one or more jobs. The CircleCI object hierarchy is: Pipeline → Workflow → Job → Step. Status queries follow this hierarchy — you need the pipeline ID to get workflows, and the workflow ID to get jobs.
server.tool(
'get_circleci_pipeline_status',
{
pipeline_id: z.string().uuid()
},
async ({ pipeline_id }) => {
// Get pipeline metadata
const pipeRes = await cci.get(`/pipeline/${pipeline_id}`);
const pipeline = pipeRes.data;
// Get workflows for this pipeline
const wfRes = await cci.get(`/pipeline/${pipeline_id}/workflow`);
const workflows = (wfRes.data.items ?? []).map((wf: Record) => ({
id: wf.id,
name: wf.name,
status: wf.status, // 'running', 'success', 'failed', 'canceled', 'on_hold'
started_at: wf.started_at,
stopped_at: wf.stopped_at ?? null
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
pipeline_id,
pipeline_number: pipeline.number,
pipeline_state: pipeline.state,
vcs: {
branch: pipeline.vcs?.branch,
revision: pipeline.vcs?.revision,
commit_message: pipeline.vcs?.commit?.subject
},
workflows
}, null, 2)
}]
};
}
);
server.tool(
'get_circleci_workflow_jobs',
{
workflow_id: z.string().uuid()
},
async ({ workflow_id }) => {
const res = await cci.get(`/workflow/${workflow_id}/job`);
const jobs = (res.data.items ?? []).map((job: Record) => ({
id: job.id,
job_number: job.job_number,
name: job.name,
status: job.status, // 'success', 'failed', 'running', 'queued', 'blocked', 'not_run'
type: job.type, // 'build', 'approval'
started_at: job.started_at,
stopped_at: job.stopped_at ?? null,
duration_sec: job.duration ?? null
}));
return {
content: [{
type: 'text',
text: JSON.stringify({ workflow_id, jobs, count: jobs.length }, null, 2)
}]
};
}
);
| Workflow status | Meaning |
|---|---|
running |
At least one job is still executing |
success |
All jobs passed |
failed |
At least one job failed (others may have been canceled) |
on_hold |
Waiting for manual approval job to be approved |
canceled |
Workflow was canceled before completion |
unauthorized |
Context or environment variable permission denied |
get_job_artifacts and cancel_workflow tools
server.tool(
'get_circleci_job_artifacts',
{
project_slug: z.string(),
job_number: z.number().int().positive()
},
async ({ project_slug, job_number }) => {
const res = await cci.get(
`/project/${encodeURIComponent(project_slug)}/${job_number}/artifacts`
);
const artifacts = (res.data.items ?? []).map((a: Record) => ({
path: a.path,
node_index: a.node_index,
url: a.url // pre-authenticated download URL (valid for 7 days)
}));
return {
content: [{
type: 'text',
text: JSON.stringify({ job_number, artifacts, count: artifacts.length }, null, 2)
}]
};
}
);
server.tool(
'cancel_circleci_workflow',
{
workflow_id: z.string().uuid(),
confirm: z.literal(true)
},
async ({ workflow_id }) => {
await cci.post(`/workflow/${workflow_id}/cancel`);
return {
content: [{
type: 'text',
text: JSON.stringify({ cancelled: true, workflow_id })
}]
};
}
);
Artifact download URLs returned by the artifacts API are pre-authenticated and valid for 7 days. They include a temporary token in the query string — do not log or expose them to end users as they grant read access to your build artifacts without further authentication.
Health endpoint: /health/circleci
import express from 'express';
const app = express();
app.get('/health/circleci', async (_req, res) => {
const start = Date.now();
try {
// GET /me verifies the token and returns the authenticated user
const me = await cci.get('/me');
res.status(200).json({
status: 'ok',
authenticated_as: me.data.login,
latency_ms: Date.now() - start
});
} catch (err) {
const axErr = err as { response?: { status?: number; data?: unknown }; message?: string };
res.status(503).json({
status: 'error',
http_status: axErr.response?.status,
error: axErr.message,
latency_ms: Date.now() - start
});
}
});
app.listen(3001);
The /me endpoint returns the authenticated user's ID and login — a fast, low-cost verification that the token is valid and CircleCI is reachable. A 401 means the token is invalid or revoked. A 429 means you've hit the rate limit (1,000 req/min per token) — AliveMCP should back off and not count this as a downtime event.
Frequently asked questions
How do I wait for a pipeline to complete in a single tool call?
Poll the workflow status on a 3-second interval inside the tool handler, up to a maximum timeout. Fetch workflows with GET /pipeline/:id/workflow, then check if all workflows have a terminal status (success, failed, canceled). Return the final status when all workflows are done, or return the intermediate state with a message if the timeout is reached. Keep the MCP tool timeout well above your poll window — 90–120 seconds is reasonable for most pipeline durations. Return partial results if the pipeline runs longer than the timeout so the caller can decide whether to re-poll.
How do I get the console output (step logs) for a job?
CircleCI v2 doesn't expose step-level console output directly via the REST API. Two options: (1) configure the job to persist its log as an artifact with store_artifacts, then retrieve it via the artifacts API; (2) use the GET /project/:slug/:job-number/tests endpoint for test metadata. For raw console output, download the pre-authenticated artifact URL. Many teams pipe build output to a file with tee in the CI config and store it as an artifact specifically so MCP tools and other automations can access it.
What is the difference between a pipeline parameter and an environment variable?
Pipeline parameters are typed, declared in .circleci/config.yml, and available in the config YAML for conditional logic (e.g., when: << pipeline.parameters.run-integration >>). They affect which jobs and workflows are generated. Environment variables set in the CircleCI web UI or contexts are available inside job steps as shell environment variables but cannot be used for conditional workflow generation. For triggering different workflow branches from an MCP tool, use pipeline parameters — they're the only way to make that decision at the config level.
Does triggering via the API count against plan limits?
Yes. API-triggered pipelines consume the same credits as VCS-triggered builds. There is no separate API quota. Monitor your credit consumption if you trigger CircleCI pipelines frequently from MCP tools — a tool that re-triggers on every agent invocation can exhaust monthly credits quickly. Cache the most recent pipeline result and only trigger a new run if the cached result is stale or the caller explicitly requests a fresh run.