CI/CD Usage
gitlab-mcp-server can run inside CI/CD jobs just like any other CLI tool. Two usage modes are available:
| Mode | LLM Required | Use Case | Determinism |
|---|---|---|---|
| Deterministic (JSON-RPC) | No | Scripted operations: list issues, post comments, create releases | ✅ Fully deterministic |
| LLM-driven (headless MCP client) | Yes | Intelligent workflows: code review, issue triage, MR analysis | ❌ Non-deterministic |
Both modes authenticate with a Personal Access Token (PAT) or Project Access Token. A token with api scope reaches every action the instance’s tier serves, Premium and Ultimate adding their own. On GitLab.com, Premium and Ultimate also serve the Orbit actions.
This page is about running the server inside a job. For the pipeline, job, variable, schedule and trigger actions an assistant can call, see CI/CD tools available below and the CI/CD workflow examples.
Prerequisites
Section titled “Prerequisites”-
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-serverchmod +x gitlab-mcp-serverTo pin a release instead of following the newest one, replace
releases/latest/download/withreleases/download/v<version>/, as Native binary shows. -
Create a Project Access Token (recommended over personal PATs for CI) under the project’s Settings > Access Tokens. Give it
apifor every tool the job may call, orread_apifor a job that only reads (list, get and search actions): the server answers aread_apitoken with its read-only surface. Set an expiry date; 90 days at most is a sound default. -
Store the token as a masked CI/CD variable named
MCP_PAT, as Storing the token describes.
Storing the token
Section titled “Storing the token”GitLab CI: under Settings > CI/CD > Variables, add:
| Variable | Value | Properties |
|---|---|---|
MCP_PAT | glpat-xxxx... | Masked, Protected (optional) |
GitHub Actions: under Settings > Secrets and variables > Actions, add a repository secret named MCP_PAT.
Never write a token into a pipeline file, and never print it in a job log: every example on this page passes it through the CI system’s secret variable and nothing else.
Mode 1: Deterministic (No LLM)
Section titled “Mode 1: Deterministic (No LLM)”Send JSON-RPC messages directly to the server via stdio. Fully deterministic — no LLM or external API needed.
How it works
Section titled “How it works”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.
Keep stdin open until the answer has arrived. The server stops when its standard input closes, and it cancels every call still running at that moment, so a call is answered only if stdin is still open when its result is ready. A tool call also waits until the server has built its tool catalog, which follows its first requests to GitLab, so the result comes a moment after the handshake. Piping a fixed set of lines into the server ({ echo ...; } | ./gitlab-mcp-server) closes stdin as soon as the last line is written, and the job reads null instead of a result. Every example below starts the server as a bash coprocess, writes the requests to it, reads its answers until the one with the expected id arrives, and only then closes its input.
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 -r '.result.content[0].text' prints null and the job succeeds with no output unless it checks for error, as the examples below do. 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 example
Section titled “GitLab CI example”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: - | # stdin stays open until the answer to id 2 has been read: the server # cancels a call still running when its input closes. coproc MCP { ./gitlab-mcp-server 2>/dev/null; } to=${MCP[1]} from=${MCP[0]} pid=${MCP_PID} echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}},"id":1}' >&"${to}" echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' >&"${to}" 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}' >&"${to}" RESULT="" while IFS= read -r line <&"${from}"; do if jq -e '.id == 2' >/dev/null <<<"${line}"; then RESULT=${line}; break; fi done exec {to}>&- wait "${pid}" # An empty RESULT means the server exited without answering. jq -e then # fails the job on a JSON-RPC error or a tool error instead of printing # "null" and exiting 0. - test -n "${RESULT}" - jq -e 'has("error") | not' >/dev/null <<<"${RESULT}" - jq -e '.result.isError != true' >/dev/null <<<"${RESULT}" - jq -r '.result.content[0].text' <<<"${RESULT}"Standalone script
Section titled “Standalone script”The same exchange as a script a job can call, with the token taken from the masked variable:
#!/bin/bashset -euo pipefail
export GITLAB_URL="${CI_SERVER_URL}"export GITLAB_TOKEN="${MCP_PAT}"
coproc MCP { ./gitlab-mcp-server 2>/dev/null; }to=${MCP[1]} from=${MCP[0]} pid=${MCP_PID}
# 1. Initialize handshakeecho '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci-script","version":"1.0"}},"id":1}' >&"${to}"# 2. Initialized notificationecho '{"jsonrpc":"2.0","method":"notifications/initialized"}' >&"${to}"# 3. Call an action through the default dynamic surfaceecho '{"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":10}}},"id":2}' >&"${to}"
# 4. Read until the answer to id 2 arrives, and only then close stdinRESULT=""while IFS= read -r line <&"${from}"; do if jq -e '.id == 2' >/dev/null <<<"${line}"; then RESULT=${line}; break; fidoneexec {to}>&-wait "${pid}"
if [[ -z "${RESULT}" ]]; then echo "the server exited without answering" >&2 exit 1fijq '.' <<<"${RESULT}"The server answers one JSON-RPC message per line. The loop reads them in turn, skips the answer to initialize (id 1) and stops at the tool result (id 2), the moment it is safe to close the server’s input; the { echo ...; } | ./gitlab-mcp-server shape that ends stdin at once gets no result at all.
Several calls in one session
Section titled “Several calls in one session”One process can answer several calls. Give each its own id, keep stdin open until every id has been answered, and match each answer to its call by id rather than by position:
#!/bin/bashset -euo pipefail
export GITLAB_URL="${CI_SERVER_URL}"export GITLAB_TOKEN="${MCP_PAT}"
coproc MCP { ./gitlab-mcp-server 2>/dev/null; }to=${MCP[1]} from=${MCP[0]} pid=${MCP_PID}
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}},"id":1}' >&"${to}"echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' >&"${to}"
# List open merge requestsecho '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"merge_request.list","params":{"project_id":"'"${CI_PROJECT_ID}"'","state":"opened"}}},"id":2}' >&"${to}"
# Get the project's detailsecho '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"project.get","params":{"project_id":"'"${CI_PROJECT_ID}"'"}}},"id":3}' >&"${to}"
# Keep stdin open until both calls are answeredRESPONSES=()while (( ${#RESPONSES[@]} < 2 )) && IFS= read -r line <&"${from}"; do if jq -e '.id == 2 or .id == 3' >/dev/null <<<"${line}"; then RESPONSES+=("${line}"); fidoneexec {to}>&-wait "${pid}"
printf '%s\n' "${RESPONSES[@]}" | jq -s '.'Helper function
Section titled “Helper function”For pipelines with many tool calls, wrap the protocol in a reusable function. Its optional third argument sets the JSON-RPC id of the call, any number but 1, which the handshake uses:
mcp_call() { local action="$1" local args="$2" local id="${3:-2}" local response="" line to from pid coproc MCP { ./gitlab-mcp-server 2>/dev/null; } to=${MCP[1]} from=${MCP[0]} pid=${MCP_PID} echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}},"id":1}' >&"${to}" echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' >&"${to}" echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"'"${action}"'","params":'"${args}"'}},"id":'"${id}"'}' >&"${to}"
# stdin stays open until the answer arrives: the server cancels a call # still running when its input closes. while IFS= read -r line <&"${from}"; do if jq -e --argjson id "${id}" '.id == $id' >/dev/null <<<"${line}"; then response=${line} break fi done exec {to}>&- wait "${pid}"
# No answer, a JSON-RPC error and a tool error are different failures, and # none of them sets an exit status: without these checks the function # prints nothing useful and the job succeeds. if [[ -z "${response}" ]]; then echo "MCP ${action}: the server exited without answering" >&2 return 1 fi 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}"}
# UsageISSUES=$(mcp_call "issue.list" '{"project_id":"'"${CI_PROJECT_ID}"'","state":"opened"}')echo "Open issues: ${ISSUES}"
# CI_MERGE_REQUEST_IID is set only in merge request pipelinesMR_DETAILS=$(mcp_call "merge_request.get" '{"project_id":"'"${CI_PROJECT_ID}"'","merge_request_iid":"'"${CI_MERGE_REQUEST_IID}"'"}')echo "MR details: ${MR_DETAILS}"Mode 2: LLM-driven (Headless MCP client)
Section titled “Mode 2: LLM-driven (Headless MCP client)”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.
Recommended client: IBM mcp-cli
Section titled “Recommended client: IBM mcp-cli”IBM mcp-cli supports command mode for scriptable LLM-driven workflows, with OpenAI, Anthropic, Azure, Gemini, Groq, and local Ollama providers:
mcp-cli cmd --prompt "..."hands a natural-language task to the model, which picks and calls the tools.mcp-cli cmd --tool <name> --tool-args '{...}'calls one tool directly, with no model deciding anything.- Its interactive mode can also build multi-step tool plans as dependency graphs and run their independent steps in parallel.
Server configuration
Section titled “Server configuration”mcp-cli reads its servers from server_config.json:
{ "mcpServers": { "gitlab": { "command": "./gitlab-mcp-server" } }}The file names no credential on purpose. mcp-cli starts the server with the job’s own environment, so GITLAB_URL and GITLAB_TOKEN set as job variables reach it unchanged. It does not expand environment variables inside the file: an env block written as "GITLAB_TOKEN": "${GITLAB_TOKEN}" would replace the real token with that literal text. The one placeholder it resolves is a whole value of the form ${TOKEN:<namespace>:<name>}, which it reads from its own token store, not from the environment (checked against mcp-cli 0.20.1).
GitLab CI: automated MR review
Section titled “GitLab CI: automated MR review”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" } } } 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_IIDGitLab CI: scheduled issue triage
Section titled “GitLab CI: scheduled issue triage”The same setup, run from a pipeline schedule, can label the backlog. The token needs api, since adding labels is a write:
triage-issues: stage: deploy 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" } } } EOF - | mcp-cli cmd \ --config-file server_config.json \ --server gitlab \ --provider openai \ --model gpt-4o \ --prompt "List all open issues in project ${CI_PROJECT_ID} without labels. For each issue, analyze its content and add appropriate labels (bug, feature, documentation, etc.)." \ --raw rules: - if: $CI_PIPELINE_SOURCE == "schedule"Using a local LLM (Ollama)
Section titled “Using a local LLM (Ollama)”For pipelines that cannot use external LLM APIs, run Ollama as a CI service. mcp-cli’s Ollama provider (from its chuk-llm dependency) finds the service through OLLAMA_BASE_URL, which its model discovery reads as well, and never through OLLAMA_HOST:
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_BASE_URL: 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_BASE_URL}/api/pull" -d '{"name":"qwen2.5-coder:7b"}' script: - | cat > server_config.json << 'EOF' { "mcpServers": { "gitlab": { "command": "./gitlab-mcp-server" } } } 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}." \ --rawHTTP transport in CI
Section titled “HTTP transport in CI”For pipelines that make many tool calls, the HTTP transport avoids per-call process startup overhead:
http-mode-pipeline: stage: test image: debian:stable-slim 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: # Start the server in the background, then wait until /health answers. # --json-response makes each answer a plain JSON body that jq can read; # the default is a text/event-stream envelope. - | ./gitlab-mcp-server --http \ --gitlab-url="${CI_SERVER_URL}" \ --http-addr=127.0.0.1:8080 \ --json-response & for _ in $(seq 30); do curl -fsS http://127.0.0.1:8080/health >/dev/null && break sleep 1 done # The default transport is stateless: every POST stands on its own, so a # tools/call needs no initialize before it. curl reads the token's header # from a file only this user can read, so the token never reaches its argv. - | HEADER=$(mktemp) printf 'PRIVATE-TOKEN: %s\n' "${MCP_PAT}" > "${HEADER}" curl -s -X POST http://127.0.0.1:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H @"${HEADER}" \ -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' rm -f "${HEADER}"Three details decide whether that job reads an answer:
--json-response. Without it each answer is atext/event-streamenvelope, whichjqcannot parse. The flag has a cost the server states at startup: progress notifications cannot be delivered in a JSON body, so a long action such aspipeline.waitreports nothing until it ends.- The
Acceptheader. The transport answers400(Accept must contain both 'application/json' and 'text/event-stream') to a POST whoseAcceptadmits only one of the two. curl’s default*/*admits both, so name them explicitly as the protocol asks: a client with a narrower default is refused. - No handshake.
--statelessis on by default, so each POST is self-contained and the call above is the whole conversation.
In HTTP mode each credential is limited to 10 calls a second with a burst of 40 by default; a job that loops faster can raise --rate-limit-rps and --rate-limit-burst (Rate limiting). See HTTP Server Mode for every flag and the authentication options.
GitHub Actions
Section titled “GitHub Actions”The server runs the same way in a GitHub Actions job against any GitLab instance. GitHub sets no CI_SERVER_URL, so name the instance and the project as repository variables (Settings > Secrets and variables > Actions, Variables tab), next to the MCP_PAT secret. An unset or empty GITLAB_URL means https://gitlab.com, so a variable that was never created sends the token there.
Deterministic mode
Section titled “Deterministic mode”name: MCP Queryon: workflow_dispatch:
jobs: list-issues: runs-on: ubuntu-latest env: GITLAB_URL: ${{ vars.GITLAB_URL }} GITLAB_TOKEN: ${{ secrets.MCP_PAT }} GITLAB_PROJECT: ${{ vars.GITLAB_PROJECT }} steps: - name: Download gitlab-mcp-server run: | 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
- name: List open issues run: | # stdin stays open until the answer to id 2 has been read coproc MCP { ./gitlab-mcp-server 2>/dev/null; } to=${MCP[1]} from=${MCP[0]} pid=${MCP_PID} echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}},"id":1}' >&"${to}" echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' >&"${to}" echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"gitlab_execute_action","arguments":{"action":"issue.list","params":{"project_id":"'"${GITLAB_PROJECT}"'","state":"opened","per_page":5}}},"id":2}' >&"${to}" RESULT="" while IFS= read -r line <&"${from}"; do if jq -e '.id == 2' >/dev/null <<<"${line}"; then RESULT=${line}; break; fi done exec {to}>&- wait "${pid}" test -n "${RESULT}" jq -e 'has("error") | not' >/dev/null <<<"${RESULT}" jq -e '.result.isError != true' >/dev/null <<<"${RESULT}" jq -r '.result.content[0].text' <<<"${RESULT}"GITLAB_PROJECT holds the project’s numeric ID or its group/project path.
LLM-driven mode
Section titled “LLM-driven mode”name: MCP Summaryon: workflow_dispatch:
jobs: summarize: runs-on: ubuntu-latest env: GITLAB_URL: ${{ vars.GITLAB_URL }} GITLAB_TOKEN: ${{ secrets.MCP_PAT }} GITLAB_PROJECT: ${{ vars.GITLAB_PROJECT }} OPENAI_API_KEY: ${{ secrets.OPENAI_KEY }} steps: - name: Setup run: | 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 pipx install mcp-cli
- name: Summarize recent changes run: | cat > server_config.json << 'EOF' { "mcpServers": { "gitlab": { "command": "./gitlab-mcp-server" } } } EOF mcp-cli cmd \ --config-file server_config.json \ --server gitlab \ --provider openai \ --model gpt-4o \ --prompt "Summarize the merge requests merged in the last week in GitLab project ${GITLAB_PROJECT}." \ --rawAs in the GitLab CI jobs, the token reaches the server through the job’s environment and is never written into server_config.json.
CI/CD tools available
Section titled “CI/CD tools available”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.
Pipelines
Section titled “Pipelines”The pipeline domain manages the full pipeline lifecycle. Its actions are pipeline.<action> IDs for gitlab_execute_action on the default dynamic surface and the action values of the gitlab_pipeline meta-tool with GITLAB_MCP_TOOL_SURFACE=meta:
| Action | Description |
|---|---|
list | List pipelines with filtering by status, ref |
get | Get pipeline details and status |
create | Trigger a new pipeline with variables |
cancel | Cancel a running pipeline |
retry | Retry a failed pipeline |
delete | Delete a pipeline |
variables | List pipeline variables |
test_report | Get test report for a pipeline |
wait | Wait for pipeline completion with polling |
latest | Get the latest pipeline for a ref |
test_report_summary | Get the test report summary for a pipeline |
update_metadata | Update 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) |
schedule_*_variable | Schedule variable CRUD (create, edit, delete) |
The schedule rows belong to the same gitlab_pipeline group as every other row, so on the dynamic surface their IDs read pipeline.schedule_list, pipeline.schedule_run, pipeline.schedule_take_ownership, pipeline.schedule_list_triggered_pipelines, pipeline.schedule_create_variable, pipeline.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:
| Action | Description |
|---|---|
list | List jobs for a pipeline |
get | Get job details |
play | Trigger a manual job |
cancel | Cancel a running job |
retry | Retry a failed job |
trace | Get job log output |
artifacts | Download a job’s artifacts archive, or one report by file_type (19.4+) |
download_artifacts | Download the latest artifacts archive for a ref and job name |
download_single_* | Download one file from a job’s artifacts, by job ID or by ref and job name |
keep_artifacts | Keep artifacts past their expiry |
delete_artifacts | Delete a job’s artifacts (a project-wide variant deletes every job’s) |
erase | Erase a job’s log and artifacts |
list_project | List jobs across a project |
list_bridges | List a pipeline’s trigger (bridge) jobs |
wait | Wait 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.
CI/CD configuration
Section titled “CI/CD configuration”| Domain (meta-tool) | Actions | Description |
|---|---|---|
template (gitlab_template) | lint, lint_project | Validate .gitlab-ci.yml syntax |
ci_variable (gitlab_ci_variable) | list, get, create, update, delete | Manage CI/CD variables |
environment (gitlab_environment) | list, get, create, update, delete, stop, deployment_* | Manage environments and deployments |
Pipeline with variables example
Section titled “Pipeline with variables example”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.
Security best practices
Section titled “Security best practices”| Practice | Recommendation |
|---|---|
| Token type | Project Access Token, scoped to a single project, auditable |
| Scope | api for full access, read_api for read-only workflows |
| Expiration | 90 days maximum, rotate before expiry |
| Storage | Masked CI/CD variable, never committed to the repository |
| Visibility | Mark the variable Protected if only protected branches need it |
| Multi-project | Use Group Access Tokens for cross-project workflows |
Minimal scope for the workflow
Section titled “Minimal scope for the workflow”Match the token’s scope to what the job does. A read_api token is served the read-only surface, so a write the job did not need is not even listed (Token scopes):
| Workflow | Required scope |
|---|---|
| List issues, MRs, pipelines | read_api |
| Post comments, create issues | api |
| Manage releases, packages | api |
| Full MR review workflow | api |
Group Access Tokens
Section titled “Group Access Tokens”For a workflow that spans several projects, create one Group Access Token instead of a token per project:
- Go to the group’s Settings > Access Tokens.
- Create a token with the scope the workflow needs.
- Use it in any project of the group, stored the same way as
MCP_PAT.
Troubleshooting
Section titled “Troubleshooting”| Error | Solution |
|---|---|
not found / permission denied | On an Alpine (musl) image the released binary cannot start at all: use a glibc image or the container image. Otherwise check that the asset matches the runner’s platform, run chmod +x, and confirm with ./gitlab-mcp-server --version |
401 Unauthorized | Check that MCP_PAT is set, not expired, and carries api or read_api; a Project Access Token works only for the project it belongs to |
x509: certificate signed by unknown authority | Set GITLAB_MCP_SKIP_TLS_VERIFY: "true" in the job’s variables: (quoted, so YAML keeps it a string) |
| Timeout on large responses | Add per_page to the action’s params, for example {"action": "issue.list", "params": {"project_id": "123", "per_page": 20}} |
| mcp-cli provider errors | Check that the provider’s key variable is set (OPENAI_API_KEY, ANTHROPIC_API_KEY) or, for Ollama, that the service answers at OLLAMA_BASE_URL (mcp-cli does not read OLLAMA_HOST); then try 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.