Skip to content

CI/CD Usage

gitlab-mcp-server can run inside CI/CD jobs just like any other CLI tool. Two usage modes are available:

ModeLLM RequiredUse CaseDeterminism
Deterministic (JSON-RPC)NoScripted operations: list issues, post comments, create releases✅ Fully deterministic
LLM-driven (headless MCP client)YesIntelligent workflows: code review, issue triage, MR analysis❌ Non-deterministic

Both modes authenticate with a Personal Access Token (PAT) or Project Access Token. Enterprise/Premium deployments using a token with api scope have access to the full tool surface. GitLab.com deployments have access to the core tool set plus additional Orbit-specific tools.

GitLab APIMCP Server (stdio)CI/CD JobGitLab APIMCP Server (stdio)CI/CD Jobinitialize (JSON-RPC via stdin)capabilities (via stdout)notifications/initializedtools/call {tool, arguments}REST API v4 / GraphQLJSON responseCallToolResult (via stdout)Parse result with jq
  1. Download the binary from GitHub Releases:

    Terminal window
    curl -sSL "https://github.com/jmrplens/gitlab-mcp-server/releases/latest/download/gitlab-mcp-server-linux-amd64" \
    -o gitlab-mcp-server
    chmod +x gitlab-mcp-server
  2. Create a Project Access Token with api scope (recommended over personal PATs for CI).

  3. Store the token as a masked CI/CD variable named MCP_PAT.

Send JSON-RPC messages directly to the server via stdio. Fully deterministic — no LLM or external API needed.

The server communicates via the MCP protocol over stdin/stdout using JSON-RPC 2.0. Each interaction requires an initialize handshake, an initialized notification, then one or more tools/call requests.

Which tool names a job may call depends on the active tool surface. The default is dynamic: it registers exactly two tools — gitlab_find_action and gitlab_execute_action — so a script calls gitlab_execute_action with a canonical domain.action ID and a params object. Naming an individual tool on the default surface is answered with {"code":-32602,"message":"unknown tool ..."}; because that is a JSON-RPC error and not a tool result, jq -s '.[1].result.content[0].text' prints null and the job succeeds with no output. If you prefer one tool per operation, set GITLAB_MCP_TOOL_SURFACE=individual in the job’s variables: — individual names are domain-first, not verb-first (gitlab_issue_list, gitlab_mr_list, gitlab_project_get); see the Tools Overview.

.gitlab-ci.yml
mcp-list-issues:
stage: test
image: debian:stable-slim
variables:
GITLAB_URL: ${CI_SERVER_URL}
GITLAB_TOKEN: ${MCP_PAT}
before_script:
- apt-get update && apt-get install -y --no-install-recommends ca-certificates curl jq
- curl -sSL "https://github.com/jmrplens/gitlab-mcp-server/releases/latest/download/gitlab-mcp-server-linux-amd64"
-o gitlab-mcp-server
- chmod +x gitlab-mcp-server
script:
- |
RESULT=$({
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}},"id":1}'
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"issue.list","params":{"project_id":"'"${CI_PROJECT_ID}"'","state":"opened","per_page":5}}},"id":2}'
} | ./gitlab-mcp-server 2>/dev/null | jq -s '.[1]')
# jq -e so a JSON-RPC error or a tool error fails the job instead of
# printing "null" and exiting 0.
- jq -e 'has("error") | not' >/dev/null <<<"${RESULT}"
- jq -e '.result.isError != true' >/dev/null <<<"${RESULT}"
- jq -r '.result.content[0].text' <<<"${RESULT}"

For pipelines with many tool calls, wrap the protocol in a reusable function:

Terminal window
mcp_call() {
local action="$1"
local args="$2"
local response
response=$({
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}},"id":1}'
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"'"${action}"'","params":'"${args}"'}},"id":2}'
} | ./gitlab-mcp-server 2>/dev/null | jq -s '.[1]')
# A JSON-RPC error and a tool error are different failures, and neither
# reaches the exit status of the pipeline above: without these checks the
# function prints "null" and the job succeeds.
if jq -e 'has("error")' >/dev/null <<<"${response}"; then
echo "MCP ${action} failed: $(jq -r '.error.message' <<<"${response}")" >&2
return 1
fi
if jq -e '.result.isError == true' >/dev/null <<<"${response}"; then
echo "MCP ${action} returned a tool error: $(jq -r '.result.content[0].text' <<<"${response}")" >&2
return 1
fi
jq -r '.result.content[0].text' <<<"${response}"
}
# Usage
ISSUES=$(mcp_call "issue.list" '{"project_id":"'"${CI_PROJECT_ID}"'","state":"opened"}')

Use a headless MCP client to let an LLM drive tool selection and orchestration. Ideal for intelligent workflows like code review, issue triage, and release notes generation.

IBM mcp-cli supports command mode for scriptable LLM-driven workflows, with OpenAI, Anthropic, Azure, Gemini, Groq, and local Ollama providers.

.gitlab-ci.yml
auto-review:
stage: review
image: python:3.12-slim
variables:
GITLAB_URL: ${CI_SERVER_URL}
GITLAB_TOKEN: ${MCP_PAT}
OPENAI_API_KEY: ${OPENAI_KEY}
before_script:
- apt-get update && apt-get install -y curl
- curl -sSL "https://github.com/jmrplens/gitlab-mcp-server/releases/latest/download/gitlab-mcp-server-linux-amd64"
-o gitlab-mcp-server
- chmod +x gitlab-mcp-server
- pip install --quiet mcp-cli
script:
- |
cat > server_config.json << 'EOF'
{
"mcpServers": {
"gitlab": {
"command": "./gitlab-mcp-server",
"env": {
"GITLAB_URL": "${GITLAB_URL}",
"GITLAB_TOKEN": "${GITLAB_TOKEN}"
}
}
}
}
EOF
- |
mcp-cli cmd \
--config-file server_config.json \
--server gitlab \
--provider openai \
--model gpt-4o \
--prompt "Review merge request !${CI_MERGE_REQUEST_IID} in project ${CI_PROJECT_ID}. Check for code quality, security issues, and missing tests. Post your review as a note on the MR." \
--raw
rules:
- if: $CI_MERGE_REQUEST_IID

For pipelines that cannot use external LLM APIs, run Ollama as a CI service:

local-llm-review:
stage: review
image: python:3.12-slim
services:
- name: ollama/ollama:latest
alias: ollama
variables:
GITLAB_URL: ${CI_SERVER_URL}
GITLAB_TOKEN: ${MCP_PAT}
OLLAMA_HOST: http://ollama:11434
before_script:
- apt-get update && apt-get install -y --no-install-recommends curl
- curl -sSL "https://github.com/jmrplens/gitlab-mcp-server/releases/latest/download/gitlab-mcp-server-linux-amd64"
-o gitlab-mcp-server
- chmod +x gitlab-mcp-server
- pip install --quiet mcp-cli
- curl -s "${OLLAMA_HOST}/api/pull" -d '{"name":"qwen2.5-coder:7b"}'
script:
- |
cat > server_config.json << 'EOF'
{
"mcpServers": {
"gitlab": {
"command": "./gitlab-mcp-server",
"env": {
"GITLAB_URL": "${GITLAB_URL}",
"GITLAB_TOKEN": "${GITLAB_TOKEN}"
}
}
}
}
EOF
- |
mcp-cli cmd \
--config-file server_config.json \
--server gitlab \
--provider ollama \
--model qwen2.5-coder:7b \
--prompt "Summarize the latest 5 merge requests in project ${CI_PROJECT_ID}." \
--raw

For pipelines that make many tool calls, the HTTP transport avoids per-call process startup overhead:

http-mode-pipeline:
script:
# Start HTTP server in background
- ./gitlab-mcp-server --http --gitlab-url="${CI_SERVER_URL}" --http-addr=127.0.0.1:8080 &
- sleep 2
# Call tools via HTTP
- |
curl -s -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-H "PRIVATE-TOKEN: ${MCP_PAT}" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"issue.list","params":{"project_id":"'"${CI_PROJECT_ID}"'","state":"opened"}}},"id":2}' \
| jq '.result.content[0].text'

See HTTP Server Mode for full details.

Beyond running the server in pipelines, GitLab MCP Server provides comprehensive CI/CD management tools that AI assistants can use interactively. These are available through the default dynamic find/execute surface and through explicit meta-tools with GITLAB_MCP_TOOL_SURFACE=meta.

The pipeline domain manages the full pipeline lifecycle. Its actions are pipeline.<action> IDs for gitlab_execute_action on the default dynamic surface (the schedule actions carry pipeline_schedule. instead, as noted below) and the action values of the gitlab_pipeline meta-tool with GITLAB_MCP_TOOL_SURFACE=meta:

ActionDescription
listList pipelines with filtering by status, ref
getGet pipeline details and status
createTrigger a new pipeline with variables
cancelCancel a running pipeline
retryRetry a failed pipeline
deleteDelete a pipeline
variablesList pipeline variables
test_reportGet test report for a pipeline
waitWait for pipeline completion with polling
latestGet the latest pipeline for a ref
test_report_summaryGet the test report summary for a pipeline
update_metadataUpdate a pipeline’s metadata (name)
trigger_*Pipeline trigger token management (list, get, create, update, delete, run)
schedule_*Pipeline schedule CRUD (list, get, create, update, delete, run, take ownership, list triggered pipelines); pipeline_schedule. prefix on the dynamic surface
schedule_*_variableSchedule variable CRUD (create, edit, delete); same prefix on the dynamic surface

The schedule rows belong to the pipeline_schedule domain, so on the dynamic surface their IDs read pipeline_schedule.schedule_list, pipeline_schedule.schedule_run, pipeline_schedule.schedule_take_ownership, pipeline_schedule.schedule_list_triggered_pipelines, pipeline_schedule.schedule_create_variable, pipeline_schedule.schedule_edit_variable and so on; on the meta surface they are action values of gitlab_pipeline like the rest of the table.

The job domain provides complete job management. Its actions are job.<action> IDs for gitlab_execute_action on the default dynamic surface and the action values of the gitlab_job meta-tool with GITLAB_MCP_TOOL_SURFACE=meta:

ActionDescription
listList jobs for a pipeline
getGet job details
playTrigger a manual job
cancelCancel a running job
retryRetry a failed job
traceGet job log output
artifactsList job artifacts
download_artifactsDownload a job’s artifacts archive
download_single_*Download one file from a job’s artifacts, by job ID or by ref and job name
keep_artifactsKeep artifacts past their expiry
delete_artifactsDelete a job’s artifacts (a project-wide variant deletes every job’s)
eraseErase a job’s log and artifacts
list_projectList jobs across a project
list_bridgesList a pipeline’s trigger (bridge) jobs
waitWait for job completion with polling

The two single-file downloads are job.download_single_artifact (by job ID) and job.download_single_artifact_by_ref (by ref and job name); the project-wide delete is job.delete_project_artifacts.

Domain (meta-tool)ActionsDescription
template (gitlab_template)lint, lint_projectValidate .gitlab-ci.yml syntax
ci_variable (gitlab_ci_variable)list, get, create, update, deleteManage CI/CD variables
environment (gitlab_environment)list, get, create, update, delete, stop, deployment_*Manage environments and deployments

Trigger a pipeline with custom variables on the default dynamic surface:

{
"tool": "gitlab_execute_action",
"arguments": {
"action": "pipeline.create",
"params": {
"project_id": "my-group/my-project",
"ref": "main",
"variables": [
{ "key": "DEPLOY_ENV", "value": "staging", "variable_type": "env_var" },
{ "key": "CONFIG", "value": "...", "variable_type": "file" }
]
}
}
}

With GITLAB_MCP_TOOL_SURFACE=meta the same call is gitlab_pipeline with { "action": "create", "params": { ... } }; meta-tools accept only action and params at the top level, so the parameters stay nested under params on both surfaces.

For the complete tool reference, see Tools Overview.

PracticeRecommendation
Token typeProject Access Token — scoped to a single project, auditable
Scopeapi for full access, read_api for read-only workflows
Expiration90 days maximum, rotate before expiry
StorageMasked CI/CD variable — never commit to repository
Multi-projectUse Group Access Tokens for cross-project workflows
ErrorSolution
not found / permission deniedVerify binary downloaded for correct platform, run chmod +x
401 UnauthorizedCheck MCP_PAT variable is set and token not expired
x509: certificate signed by unknown authoritySet GITLAB_MCP_SKIP_TLS_VERIFY=true
Timeout on large responsesAdd per_page argument to limit results
mcp-cli provider errorsVerify API key variables, check pip install --upgrade mcp-cli

Frequently asked questions

Can I use gitlab-mcp-server in CI/CD without an LLM?

Yes. The deterministic mode sends JSON-RPC 2.0 messages directly to the server over stdio, with no LLM or external API involved. Each interaction performs an initialize handshake, sends a notifications/initialized message, then issues one or more tools/call requests, and you parse the result with jq. This mode is fully deterministic, making it ideal for scripted operations such as listing issues, posting comments, or creating releases.

Which GitLab token type should I use in CI/CD pipelines?

Use a Project Access Token scoped to a single project and store it as a masked CI/CD variable, never committed to the repository. Choose the api scope for full access or read_api for read-only workflows, set a maximum 90-day expiration, and rotate the token before it expires. For workflows that span several projects, use a Group Access Token instead.

How do I block a CI script until a GitLab pipeline finishes?

Run the pipeline wait action: gitlab_execute_action with action: 'pipeline.wait' on the default dynamic surface, or the gitlab_pipeline meta-tool with action: 'wait' when GITLAB_MCP_TOOL_SURFACE=meta. It polls the pipeline until it reaches a terminal state (success, failed, or canceled), which lets a CI script block until a triggered pipeline completes. The job.wait action (gitlab_job with wait on the meta surface) does the same for individual jobs.

Should I use stdio or HTTP transport in CI?

Use stdio for occasional tool calls, because each call spawns and tears down a server process. For pipelines that make many tool calls, start the server once in HTTP mode with --http and call it over http://127.0.0.1:8080/mcp to avoid per-call process startup overhead. Both transports authenticate with a Personal Access Token or Project Access Token.