gitlab-ci-mcp
# gitlab-ci-mcp
<!-- mcp-name: io.github.mshegolev/gitlab-ci-mcp -->
[](https://pypi.org/project/gitlab-ci-mcp/)
[](https://pypi.org/project/gitlab-ci-mcp/)
[](LICENSE)
[](https://pepy.tech/project/gitlab-ci-mcp)
MCP server for GitLab CI/CD. Lets an LLM agent (Claude Code, Cursor, OpenCode, DevX Agent, etc.) work with pipelines, jobs, schedules, branches, tags, merge requests and repository files.
Python and [FastMCP](https://github.com/modelcontextprotocol/python-sdk). Local stdio remains the default; opt-in Streamable HTTP supports shared deployments with one GitLab PAT per caller.
Works with any GitLab — SaaS `gitlab.com` or self-hosted / on-prem. Designed with corporate networks in mind: configurable `NO_PROXY` handling, optional SSL-verify toggle, per-project scoping via env vars.
## Design highlights
- **Tool annotations** — every tool carries `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` so MCP clients can classify operations (e.g. ask for confirmation only on destructive ones like `gitlab_merge_mr`, `gitlab_delete_schedule`).
- **Structured output on every tool** — each tool declares a TypedDict return type, so FastMCP auto-generates an `outputSchema` and every result carries `structuredContent` alongside a pre-rendered markdown text block. Clients that can render structured data use it; agents that prefer compact text get the markdown. No `response_format` parameter needed.
- **Structured errors** — authentication, 404, 403, 429 (rate-limit), 5xx, missing-env errors are converted to actionable `ToolError` messages (e.g. _"GitLab authentication failed… verify GITLAB_TOKEN has `api` scope"_) and surfaced as `isError=True` results.
- **Pydantic input validation** — every argument has typed constraints (ranges, lengths, literals) auto-exposed as JSON Schema.
- **Explicit project scoping** — every GitLab tool requires `project_path` by default. Set `GITLAB_ALLOW_DEFAULT_PROJECT=true` to opt into the legacy environment fallback.
- **Pagination** — list tools return a `pagination` block with `page`, `total`, `has_more`, `next_page` and a next-page hint in the markdown footer.
- **Per-user HTTP credentials** — in Streamable HTTP mode every request must carry `X-GitLab-Token`; GitLab API calls and cached clients are isolated by PAT. Stdio remains single-user with `GITLAB_TOKEN`.
- **MCP Resources** — `gitlab://project/info` and `gitlab://project/ci-config` mirror common lookups for clients that prefer the Resource model over tools.
- **Lifespan management** — `python-gitlab` HTTP sessions are closed cleanly on server shutdown via an `asynccontextmanager` lifespan hook.
- **Log grep** — `gitlab_get_job_log` accepts `grep_pattern` + `grep_context` (surrounding lines) for regex-filtering megabyte-scale CI logs without pulling the whole trace into agent context.
### Threading model
FastMCP automatically runs synchronous tools in a worker thread
(`anyio.to_thread.run_sync`), so they do not block the asyncio event loop —
`python-gitlab` is a synchronous library and wrapping every call in
`asyncio.to_thread` ourselves would be ceremony. Tools that benefit from the
MCP Context (progress, info logs) are written as `async def` and explicitly
wrap `python-gitlab` calls with `asyncio.to_thread`.
## Features
24 tools covering the everyday CI/CD surface:
**Pipelines**
`gitlab_list_pipelines` · `gitlab_get_pipeline` · `gitlab_get_pipeline_jobs` · `gitlab_get_job_log` · `gitlab_trigger_pipeline` · `gitlab_retry_pipeline` · `gitlab_cancel_pipeline` · `gitlab_pipeline_health`
**Schedules**
`gitlab_list_schedules` · `gitlab_create_schedule` · `gitlab_update_schedule` · `gitlab_delete_schedule`
**Branches & tags**
`gitlab_list_branches` · `gitlab_list_tags` · `gitlab_compare_branches`
**Merge requests**
`gitlab_list_merge_requests` · `gitlab_get_merge_request` · `gitlab_get_merge_request_changes` · `gitlab_create_merge_request` · `gitlab_merge_mr`
**Repository & project**
`gitlab_get_file` · `gitlab_list_repository_tree` · `gitlab_project_info`
**Package**
`gitlab_ci_mcp_check_updates`
### Pipeline health report
`gitlab_pipeline_health` returns a ready-to-read summary over 7/30 days:
```
Last 7d: 96.4% up | 27/28 success
Last 30d: 92.1% | 105/114 success
Last 10: success success success failed success ...
```
Handy for on-call / triage: `покажи health master за последние 7 дней`.
## Installation
Requires Python 3.10+.
```bash
# via uvx (recommended)
uvx --from gitlab-ci-mcp gitlab-ci-mcp
# or via pip/pipx
pipx install gitlab-ci-mcp
```
## Configuration
All config is via environment variables:
| Variable | Required | Description |
| --- | --- | --- |
| `GITLAB_URL` | **yes** | Base URL, e.g. `https://gitlab.example.com` |
| `GITLAB_TOKEN` | stdio only | Personal Access Token with `api` scope. In HTTP mode, each client sends its own `X-GitLab-Token` header. |
| `MCP_TRANSPORT` | no | `stdio` (default) or `streamable-http` |
| `MCP_HTTP_HOST` | no | HTTP bind host; defaults to `127.0.0.1` |
| `MCP_HTTP_PORT` | no | HTTP bind port; defaults to `8000` |
| `GITLAB_PROJECT_PATH` | no | Legacy fallback project; used only with `GITLAB_ALLOW_DEFAULT_PROJECT=true` |
| `GITLAB_ALLOW_DEFAULT_PROJECT` | no | Set to `true` to opt into the legacy `GITLAB_PROJECT_PATH` fallback |
| `MCP_GITLAB_UPDATE_CHECK_ENABLED` | no | `false` by default; set to `true` to allow the explicit update-check tool to query PyPI |
| `GITLAB_SSL_VERIFY` | no | `true` (default) / `false` |
| `GITLAB_NO_PROXY_DOMAINS` | no | Comma-separated domains to add to `NO_PROXY` (useful in corp networks behind a local HTTP proxy — e.g. `.corp.example.com,gitlab.internal`) |
Every tool accepts a `project_path` arg. It is required by default so a call
cannot silently target an environment-wide project. For legacy deployments,
set `GITLAB_ALLOW_DEFAULT_PROJECT=true` together with `GITLAB_PROJECT_PATH` to
opt into the old fallback behavior.
Update checking is disabled by default. Set `MCP_GITLAB_UPDATE_CHECK_ENABLED=true`
to enable the explicit `gitlab_ci_mcp_check_updates` tool. When enabled, it
makes one bounded request to fixed PyPI JSON, caches a result for five minutes,
reports failures and unsupported versions, and never installs anything. No
startup or other tool call performs this request. If called while disabled,
the tool reports `disabled` without making a network request.
Pipeline health success rate uses completed pipelines only: `success` and
`failed` are in the denominator; `running`, `pending`, `manual`, `canceled`,
`skipped`, and other non-completed statuses are excluded. A missing comparison
period or a sample without completed pipelines is reported as insufficient
data, not as a zero rate or a flat trend.
## Shared Streamable HTTP deployment
For a shared server, run with `MCP_TRANSPORT=streamable-http`. Each caller
must send their own GitLab PAT in `X-GitLab-Token`; the server never falls back
to the process `GITLAB_TOKEN` in this mode. GitLab applies that PAT's identity
and permissions to each API operation. The header is a custom credential
mechanism, not MCP OAuth; clients need support for configured custom headers.
The server binds to loopback (`127.0.0.1:8000`) by default. Expose it through
an HTTPS reverse proxy, restrict network access, and redact the PAT header from
proxy, tracing, and error logs. Initialize and tool discovery only check that
the header is non-empty; GitLab validates the PAT when an API call is made.
Example for Claude Code (provide `GITLAB_PAT` through a secret manager; do not
put a literal token in shell history):
```bash
claude mcp add --transport http \
--header "X-GitLab-Token: ${GITLAB_PAT}" \
gitlab https://mcp.example.com/mcp
```
The client must attach the header to every request. Each GitLab tool still
requires an explicit `project_path`. Fixed resources
`gitlab://project/info` and `gitlab://project/ci-config` have no project
argument; they require the explicit compatibility setting
`GITLAB_ALLOW_DEFAULT_PROJECT=true` and `GITLAB_PROJECT_PATH`.
Full credential details and limitations: [docs/per-user-auth.md](docs/per-user-auth.md).
## Claude Code
**Full walkthrough:** [docs/claude-code.md](docs/claude-code.md) — prerequisites, two install paths, multi-project setup, self-hosted GitLab behind a corp proxy, troubleshooting, uninstall.
Short version:
```bash
claude mcp add --transport stdio \
--env GITLAB_URL=https://gitlab.example.com \
--env GITLAB_TOKEN="$GITLAB_PAT" \
gitlab -- uvx --from gitlab-ci-mcp gitlab-ci-mcp
```
Each GitLab tool call must include `project_path`, for example
`project_path="my-org/my-repo"`. For legacy default-project behavior, also set
`GITLAB_PROJECT_PATH` and `GITLAB_ALLOW_DEFAULT_PROJECT=true`.
Or in `~/.claude.json` / project `.mcp.json`:
```json
{
"mcpServers": {
"gitlab": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "gitlab-ci-mcp", "gitlab-ci-mcp"],
"env": {
"GITLAB_URL": "https://gitlab.example.com",
"GITLAB_TOKEN": "${GITLAB_PAT}",
"GITLAB_PROJECT_PATH": "my-org/my-repo",
"GITLAB_ALLOW_DEFAULT_PROJECT": "true",
"GITLAB_SSL_VERIFY": "true"
}
}
}
}
```
Check:
```bash
claude mcp list
# gitlab: uvx --from gitlab-ci-mcp gitlab-ci-mcp - ✓ Connected
```
## Cursor / OpenCode / DevX Agent
Same idea — point the MCP config to `uvx --from gitlab-ci-mcp gitlab-ci-mcp` with the env vars above. See each tool's own MCP config syntax.
## Example prompts
```
что сломалось в последнем pipeline master
```
```
покажи health master за 7 дней для проекта my-org/other-repo
```
```
создай MR из feature/foo в master с title "feat: foo"
```
```
покажи содержимое .gitlab-ci.yml из master
```
## Rate limits & connection reuse
GitLab enforces a per-user rate limit (typically 2000 req/h for the REST API,
configurable by the admin — see your instance's `/admin/application_settings/network`).
- The server caches a bounded `python-gitlab` HTTP session per GitLab URL, project, and PAT fingerprint. Different users never reuse one another's cached client.
- List tools default to `per_page=20` to keep a single call within a small
number of API requests.
- If you hit a `429 Too Many Requests`, the error handler returns an actionable
message — wait and try again with larger `per_page` or fewer calls.
## Self-hosted GitLab behind a corporate proxy
When your laptop has a local HTTP proxy (e.g. `http://127.0.0.1:3128` for corp web access) but GitLab is on the intranet, the proxy intercepts and kills internal requests. Two options:
1. Set `GITLAB_NO_PROXY_DOMAINS` — the server will add them to `NO_PROXY` at startup **and clear `HTTP_PROXY`/`HTTPS_PROXY` from its own process** so they don't affect GitLab traffic.
2. Pass explicitly empty `HTTP_PROXY=""` etc. in the MCP `env` section.
## Development
```bash
git clone https://github.com/mshegolev/gitlab-ci-mcp
cd gitlab-ci-mcp
python -m venv .venv && . .venv/bin/activate
pip install -e '.[dev]'
pytest
```
Run the server directly (stdio transport is the default and waits on stdin for MCP messages):
```bash
GITLAB_URL=... GITLAB_TOKEN=... gitlab-ci-mcp
```
## License
MIT — see [LICENSE](LICENSE).
## Acknowledgements
Built on [python-gitlab](https://github.com/python-gitlab/python-gitlab) and the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).
TDQS
Scored across 24 tools
Tools are clearly separated by resource and action: pipeline tools split into list/get/jobs/log/trigger/retry/cancel/health, MR tools into list/get/changes/create/merge, and schedule CRUD is explicit. The only mild overlap is among pipeline-read tools, but the descriptions make boundaries clear.
Most tools follow a predictable gitlab_verb_noun pattern such as gitlab_get_pipeline, gitlab_list_branches, and gitlab_create_schedule. Exceptions include noun-phrase names like gitlab_pipeline_health and gitlab_project_info, the abbreviated gitlab_merge_mr, and the meta gitlab_ci_mcp_check_updates.
At 24 tools the set is on the heavy side, but it covers several distinct areas—pipeline operations, job logs, merge requests, repository browsing, and schedules—and most tools have a clear role. The lone meta update-check tool is slightly out of place but does not dominate the surface.
Core CI lifecycle coverage is strong: pipelines can be listed, inspected, triggered, retried, cancelled, and health-checked, and schedules have full CRUD. However, notable gaps remain: merge requests cannot be updated, closed, commented on, or approved; branches and tags are list-only; and repository file operations are read-only.