# mcp-gitlab [![PyPI version](https://img.shields.io/pypi/v/mcp-gitlab)](https://pypi.org/project/mcp-gitlab/) [![PyPI downloads](https://img.shields.io/pypi/dm/mcp-gitlab)](https://pypi.org/project/mcp-gitlab/) [![Python](https://img.shields.io/pypi/pyversions/mcp-gitlab)](https://pypi.org/project/mcp-gitlab/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![CI](https://github.com/vish288/mcp-gitlab/actions/workflows/tests.yml/badge.svg)](https://github.com/vish288/mcp-gitlab/actions/workflows/tests.yml) [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io) **mcp-gitlab** is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for the GitLab REST API. It gives AI assistants **91 tools**, **7 resources**, and **6 prompts** to manage projects, merge requests, pipelines, CI/CD variables, approvals, issues, and code reviews. Works with Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot, and any MCP-compatible client. Runs in one of two modes: with writes, or locked read-only (`MCP_READ_ONLY=true`) so the 57 write tools are never offered to the model. Supports GitLab.com and self-hosted GitLab instances (CE/EE). No GitLab Duo or Premium required. Supports the MCP 2026-07-28 specification, often called MCP 2.0, and stays compatible with 2025-11-25 clients. Built with [FastMCP](https://github.com/jlowin/fastmcp) 4.x, [httpx](https://www.python-httpx.org/), and [Pydantic](https://docs.pydantic.dev/). **Install:** `uvx mcp-gitlab` | [PyPI](https://pypi.org/project/mcp-gitlab/) | [MCP Registry](https://registry.modelcontextprotocol.io) | [Changelog](https://github.com/vish288/mcp-gitlab/releases) ```bash uvx mcp-gitlab # with writes MCP_READ_ONLY=true uvx mcp-gitlab # locked read-only: write tools are not listed ``` ## 1-Click Installation **Claude Code plugin** — one command adds the fleet marketplace, the next installs the server, prompts for `GITLAB_URL`/`GITLAB_TOKEN` (the token is stored in the credential store, never written to a config file) with an optional `MCP_READ_ONLY` toggle, and keeps it updated: ```bash claude plugin marketplace add vish288/.github claude plugin install mcp-gitlab@vish288-mcp ``` [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://vish288.github.io/mcp-install?server=mcp-gitlab&install=cursor) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vish288.github.io/mcp-install?server=mcp-gitlab&install=vscode) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vish288.github.io/mcp-install?server=mcp-gitlab&install=vscode-insiders) > **💡 Tip:** For other AI assistants (Claude Code, Windsurf, IntelliJ, Gemini CLI), visit the **[GitLab MCP Installation Gateway](https://vish288.github.io/mcp-install?server=mcp-gitlab)**.
Manual Setup Guides (Click to expand)
> Prerequisite: Install `uv` first (required for all `uvx` install flows). [Install uv](https://docs.astral.sh/uv/getting-started/installation/). ### Claude Code ```bash # with writes claude mcp add gitlab -e GITLAB_URL=https://gitlab.example.com -e GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx -- uvx mcp-gitlab # locked read-only claude mcp add gitlab -e MCP_READ_ONLY=true -e GITLAB_URL=https://gitlab.example.com -e GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx -- uvx mcp-gitlab ``` ### Windsurf & IntelliJ **Windsurf:** Add to `~/.codeium/windsurf/mcp_config.json` **IntelliJ:** Add to `Settings | Tools | MCP Servers` > **Note:** The actual server config starts at `gitlab` inside the `mcpServers` object. Remove `MCP_READ_ONLY` to allow writes. ```json { "mcpServers": { "gitlab": { "command": "uvx", "args": ["mcp-gitlab"], "env": { "GITLAB_URL": "https://gitlab.example.com", "GITLAB_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx", "MCP_READ_ONLY": "true" } } } } ``` ### Gemini CLI ```bash # with writes gemini mcp add -s user -e GITLAB_URL=https://gitlab.example.com -e GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx gitlab uvx mcp-gitlab # locked read-only gemini mcp add -s user -e MCP_READ_ONLY=true -e GITLAB_URL=https://gitlab.example.com -e GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx gitlab uvx mcp-gitlab ``` ### VS Code Add to `.vscode/mcp.json`. Remove `MCP_READ_ONLY` to allow writes. ```json { "servers": { "gitlab": { "type": "stdio", "command": "uvx", "args": ["mcp-gitlab"], "env": { "GITLAB_URL": "https://gitlab.example.com", "GITLAB_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx", "MCP_READ_ONLY": "true" } } } } ``` ### Codex ```bash # with writes codex mcp add gitlab --env GITLAB_URL=https://gitlab.example.com --env GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx -- uvx mcp-gitlab # locked read-only codex mcp add gitlab --env MCP_READ_ONLY=true --env GITLAB_URL=https://gitlab.example.com --env GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx -- uvx mcp-gitlab ``` ### pip / uv ```bash uv pip install mcp-gitlab ``` Drop `MCP_READ_ONLY` to allow writes. On a shared or hosted server put it in the deployer's env, not the client config.
## Configuration | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `GITLAB_URL` | **Yes** | - | GitLab instance URL (e.g. `https://gitlab.example.com`) | | `GITLAB_TOKEN` | Yes, unless signed in with `mcp-gitlab auth login` | - | Authentication token (see below) | | `MCP_READ_ONLY` | No | `false` | Shared name across the mcp-* servers. Any value other than `false`/`0`/`no`/`off`/empty locks the server read-only: the 57 write tools and write-workflow prompts are not listed, and nothing can turn it back off at runtime | | `GITLAB_READ_ONLY` | No | `false` | Per-server alias for `MCP_READ_ONLY`; either one locks the server read-only. `--read-only` can only raise the lock | | `MCP_TOOLSETS` | No | all | Comma list of tool groups to expose (see Toolsets); unset or `all` lists every tool. `MCP_READ_ONLY` still applies after the selection | | `GITLAB_HTTP_BEARER_TOKEN` | No | - | Bearer token (min 16 chars) required to bind an HTTP transport to a non-loopback host; clients send `Authorization: Bearer `; token mode only | | `GITLAB_TIMEOUT` | No | `30` | Request timeout in seconds | | `GITLAB_SSL_VERIFY` | No | `true` | Set to `false` to skip SSL verification | | `GITLAB_AUTH` | No | `token` | `token` (PAT from env) or `oauth` (OAuth 2.1 proxy; see below) | ### Toolsets Clients cap how many tools they forward: Cursor warns past **40 tools across all enabled servers**. `MCP_TOOLSETS` (or `--toolsets`) shrinks `tools/list` to one or more named groups — sets of the raw domain tags the tools already carry — so a single-job selection is cheap. Unset or `all` lists every tool. Names are lowercase and comma-separated; an unknown name exits 2 with the valid list. `MCP_READ_ONLY` still applies afterward and only ever removes more tools (the read-only column). | Group | Raw tags | Tools | Read-only | |-------|----------|-------|-----------| | `code` | files, branches, commits | 12 | 9 | | `review` | merge_requests, notes, discussions, review | 29 | 9 | | `ci` | pipelines, jobs, variables | 18 | 6 | | `issues` | issues | 5 | 2 | | `release` | tags, releases | 9 | 4 | | `admin` | projects, groups, approvals | 23 | 7 | | `all` (default) | every tag | 91 | 34 | Five tools carry two domain tags, so they are listed in both of their groups: `gitlab_list_mr_commits` (`code`, `review`), `gitlab_list_mr_pipelines` (`review`, `ci`), and `gitlab_approve_mr` / `gitlab_unapprove_mr` / `gitlab_get_mr_approvals` (`review`, `admin`). Every single group stays under 40, and so does `review,issues` (34). Two pairs do not, because the shared tools are counted once each: **`review,ci` lists 46 tools** and **`code,review` lists 40**, so both reach or pass Cursor's 40-tool warning. Pick one group per job, or leave room for your other servers. ```bash MCP_TOOLSETS=review uvx mcp-gitlab # MR review: 29 tools uvx mcp-gitlab --toolsets ci # pipelines, jobs, CI/CD variables: 18 tools MCP_TOOLSETS=issues,release uvx mcp-gitlab ``` Resources and prompts are unaffected by the selection. ### Supported Token Types The server checks these environment variables in order — first match wins: 1. `GITLAB_TOKEN` 2. `GITLAB_PAT` 3. `GITLAB_PERSONAL_ACCESS_TOKEN` 4. `GITLAB_API_TOKEN` These accept any of the following token types: | Token Type | Format | Use Case | |------------|--------|----------| | Personal access token | `glpat-xxx` | User-level access with `api` scope | | OAuth2 token | `oauth-xxx` | OAuth app integrations | | CI job token | `$CI_JOB_TOKEN` | GitLab CI pipeline access | ## Sign in without a token Use this when you run the server for yourself over `stdio` and have no personal access token. `mcp-gitlab auth login` signs you in to GitLab as a public OAuth client and stores the tokens locally. The server then reads them at startup and refreshes them on its own. This is not OAuth mode; the server stays in token mode and nothing on the MCP wire changes. Three commands: ```bash mcp-gitlab auth login # device flow by default; add --web for the browser flow mcp-gitlab auth status # show the stored account, scopes, and token freshness mcp-gitlab auth logout # revoke the token at GitLab and delete the local copy ``` `auth login` prints a code and a URL. Open the URL, enter the code, and approve. The server then works with `GITLAB_URL` set and no token. ### Client ID Each sign-in needs an OAuth application ID. Resolution order: `--client-id`, then `GITLAB_OAUTH_CLIENT_ID`, then a built-in default for `https://gitlab.com`. The gitlab.com default is not registered yet. Until it is, set `GITLAB_OAUTH_CLIENT_ID` or pass `--client-id` for gitlab.com too. ### Register an app (self-managed, or gitlab.com until the default ships) Create an application under **User Settings → Applications → Add new application** with these settings: | Field | Value | |-------|-------| | Name | `mcp-gitlab` | | Redirect URI | `http://127.0.0.1/callback` (loopback IP, no port) | | Confidential | unchecked (public client) | | Scopes | `api`, `read_api` | Use `127.0.0.1`, not `localhost`. The device flow needs GitLab 17.9 or later; on older instances use `mcp-gitlab auth login --web`. ### Storage and precedence Credentials live in `~/.config/mcp-gitlab/credentials.json`, one entry per GitLab URL, mode 0600. An environment token always wins: if `GITLAB_TOKEN` is set, the server uses it and ignores the stored credentials. Pass `--scopes read_api` for a read-only session; the server then blocks write tools before any API call. ## OAuth 2.1 (remote deployments) For a server shared by several people, `--auth oauth` turns mcp-gitlab into an MCP 2026-07-28 resource server with GitLab as the upstream OAuth provider: each user signs in with their own GitLab account, every request runs with that user's token, and no personal access token is configured on the server. Use it when you host the server once and multiple users connect to it; keep the default `--auth token` for a single-user local setup. OAuth mode requires `--transport streamable-http` (OAuth applies to HTTP only; stdio keeps using the environment token). **1. Register a GitLab OAuth application** (User Settings → Applications, or a group/instance application). Set the redirect URI to `/auth/callback` (e.g. `https://mcp.example.com/auth/callback`), mark it confidential, and grant the `api` scope (or `read_api` for a read-only deployment). Copy the Application ID and Secret. **2. Configure the server:** | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `GITLAB_URL` | **Yes** | - | GitLab instance; also the upstream authorization server | | `GITLAB_OAUTH_CLIENT_ID` | **Yes** | - | Application ID (oauth mode, or local sign-in) | | `GITLAB_OAUTH_CLIENT_SECRET` | **Yes** | - | Its secret | | `GITLAB_OAUTH_BASE_URL` | **Yes** | - | Public URL of this server, no trailing slash (e.g. `https://mcp.example.com`). Resource URL is `/mcp` | | `GITLAB_OAUTH_SCOPES` | No | `api` | Space-separated GitLab scopes required on every request. For a read-only OAuth deployment set `read_api` here *and* `MCP_READ_ONLY=true`, so write tools are hidden server-wide and per-user tokens lack write scope | | `GITLAB_OAUTH_JWT_KEY` | No | derived | FastMCP JWT signing key, ≥ 32 random chars. Set it in production so client registrations survive a client-secret rotation | | `FASTMCP_HOME` | No | platform data dir | The encrypted token store lives at `$FASTMCP_HOME/oauth-proxy//` | ```bash GITLAB_URL=https://gitlab.com GITLAB_AUTH=oauth \ GITLAB_OAUTH_CLIENT_ID=... GITLAB_OAUTH_CLIENT_SECRET=... \ GITLAB_OAUTH_BASE_URL=https://mcp.example.com \ uvx mcp-gitlab --transport streamable-http ``` Point an MCP client (or the MCP Inspector, `pnpm dlx @modelcontextprotocol/inspector`) at `/mcp`; it discovers the authorization server, runs the OAuth flow, and each tool call then uses that user's GitLab token. A `read_api`-only token can read but is refused writes with an actionable hint. Full guide, client setup and troubleshooting: [docs/oauth.md](docs/oauth.md). ## Compatibility | Client | Supported | Install Method | |--------|-----------|----------------| | Claude Desktop | Yes | `claude_desktop_config.json` | | Claude Code | Yes | `claude mcp add` | | Cursor | Yes | One-click deeplink or `.cursor/mcp.json` | | VS Code Copilot | Yes | One-click deeplink or `.vscode/mcp.json` | | Windsurf | Yes | `~/.codeium/windsurf/mcp_config.json` | | Any MCP client | Yes | stdio or HTTP transport | ## Protocol support mcp-gitlab implements the Model Context Protocol. It supports the 2026-07-28 specification, often called MCP 2.0. It stays compatible with 2025-11-25 clients. - **Specification**: MCP 2026-07-28 (MCP 2.0). Verified over `stdio` and `streamable-http`. - **Legacy clients**: 2025-11-25 clients still work. - **Built on**: FastMCP 4.x and the MCP Python SDK 2.x. - **Transports**: `stdio` (default) and `streamable-http` (recommended for remote). The `sse` (HTTP+SSE) transport still works, but the 2026-07-28 specification deprecates it. The server prints a warning when you use it. - **Capabilities**: tools, resources, and prompts. The server uses no roots, sampling, logging, elicitation, or resource subscriptions, so the 2026-07-28 deprecations do not affect it. - **Server identity and cache hints**: `serverInfo` reports the installed package version and the install page as `websiteUrl`, so every response names the exact release. List results carry the 2026-07-28 cache hints `ttlMs: 300000` (5-minute TTL) and `cacheScope: private` — private because `resources/read` can vary by the per-user token. Both are inert for clients that do not read them. ### Error results An expected failure (a 404, a 401, a validation error, or a write that reaches the read-only guard) comes back as a tool result with the protocol-level `isError` flag set, so a client can detect and self-correct instead of parsing an `"error"` key out of an otherwise successful result. In locked read-only mode the write tools are hidden, so calling one returns an `Unknown tool` error; the read-only envelope is the guard's own response, returned only when a client reaches a write tool anyway (a cached listing, or a future untagged tool). The body is a JSON object with `error` and, where it helps, `status_code`, `body`, and an actionable `hint`. That object is the result's text content; the error sets no `structuredContent`, because a strict client (the TypeScript MCP SDK) validates `structuredContent` against the tool's output schema even on an `isError` result and the schema would reject the error body. An unexpected bug is also an `isError` result but carries only `TypeName: message` text, likewise with no `structuredContent`. A successful result carries the response as a JSON object in both the pretty text content and `structuredContent` (the object itself, not the old `{"result": ""}` wrapper a strict client would otherwise reject). Tools advertise no `outputSchema`: upstream GitLab fields pass through untyped, so a new API field never breaks a schema; a typed schema is opt-in per tool via `output_schema=` when a client needs one. ## Tools (91) | Category | Count | Tools | |----------|-------|-------| | **Projects** | 4 | get, create, delete, update merge settings | | **Project Approvals** | 10 | get/update config, CRUD approval rules (project + MR) | | **Groups** | 6 | list, get, share/unshare project, share/unshare group | | **Branches** | 3 | list, create, delete | | **Commits** | 4 | list, get (with diff), create, compare | | **Repository Files** | 4 | get file (at ref, line range), list tree, search code, blame | | **Merge Requests** | 15 | list, get, create, update, merge, merge-sequence, rebase, changes, approve, unapprove, get approvals, list pipelines, list commits, subscribe, unsubscribe | | **MR Notes** | 6 | list, add, delete, update, award emoji, remove emoji | | **MR Discussions** | 4 | list, create (inline + multi-line), reply, resolve | | **Draft Notes** | 4 | create (inline, auto-filled SHAs), list, publish all, delete | | **Pipelines** | 5 | list, get (with jobs), create, retry, cancel | | **Jobs** | 4 | retry, play, cancel, get log | | **Tags** | 4 | list, get, create, delete | | **Releases** | 5 | list, get, create, update, delete | | **CI/CD Variables** | 8 | CRUD for project variables, CRUD for group variables | | **Issues** | 5 | list, get, create, update, add comment |
Full tool reference (click to expand) ### Projects | Tool | Description | |------|-------------| | `gitlab_get_project` | Get project details | | `gitlab_create_project` | Create a new project | | `gitlab_delete_project` | Delete a project | | `gitlab_update_project_merge_settings` | Update merge settings | ### Project Approvals | Tool | Description | |------|-------------| | `gitlab_get_project_approvals` | Get approval config | | `gitlab_update_project_approvals` | Update approval settings | | `gitlab_list_project_approval_rules` | List approval rules | | `gitlab_create_project_approval_rule` | Create approval rule | | `gitlab_update_project_approval_rule` | Update approval rule | | `gitlab_delete_project_approval_rule` | Delete approval rule | | `gitlab_list_mr_approval_rules` | List MR approval rules | | `gitlab_create_mr_approval_rule` | Create MR approval rule | | `gitlab_update_mr_approval_rule` | Update MR approval rule | | `gitlab_delete_mr_approval_rule` | Delete MR approval rule | ### Groups | Tool | Description | |------|-------------| | `gitlab_list_groups` | List groups | | `gitlab_get_group` | Get group details | | `gitlab_share_project_with_group` | Share project with group | | `gitlab_unshare_project_with_group` | Unshare project from group | | `gitlab_share_group_with_group` | Share group with group | | `gitlab_unshare_group_with_group` | Unshare group from group | ### Branches | Tool | Description | |------|-------------| | `gitlab_list_branches` | List branches | | `gitlab_create_branch` | Create a branch | | `gitlab_delete_branch` | Delete a branch | ### Commits | Tool | Description | |------|-------------| | `gitlab_list_commits` | List commits | | `gitlab_get_commit` | Get commit (with optional diff) | | `gitlab_create_commit` | Create commit with file actions | | `gitlab_compare` | Compare branches/tags/commits | ### Repository Files | Tool | Description | |------|-------------| | `gitlab_get_file` | Get decoded file content at a ref (optional line range), with metadata | | `gitlab_list_tree` | List repository tree entries (files and directories) at a ref | | `gitlab_search_code` | Search a project's code (scope=blobs); depends on the instance's search backend | | `gitlab_get_blame` | Get git blame for a file at a ref, optionally limited to a line range | ### Merge Requests | Tool | Description | |------|-------------| | `gitlab_list_mrs` | List merge requests | | `gitlab_get_mr` | Get MR details | | `gitlab_create_mr` | Create merge request | | `gitlab_update_mr` | Update merge request | | `gitlab_merge_mr` | Merge a merge request | | `gitlab_merge_mr_sequence` | Merge multiple MRs in order | | `gitlab_rebase_mr` | Rebase a merge request | | `gitlab_mr_changes` | Get MR file changes | | `gitlab_approve_mr` | Approve a merge request | | `gitlab_unapprove_mr` | Remove approval from a merge request | | `gitlab_get_mr_approvals` | Get MR approval state | | `gitlab_list_mr_pipelines` | List MR pipelines | | `gitlab_list_mr_commits` | List MR commits | | `gitlab_subscribe_mr` | Subscribe to MR notifications | | `gitlab_unsubscribe_mr` | Unsubscribe from MR notifications | ### MR Notes | Tool | Description | |------|-------------| | `gitlab_list_mr_notes` | List MR comments | | `gitlab_add_mr_note` | Add comment to MR | | `gitlab_delete_mr_note` | Delete MR comment | | `gitlab_update_mr_note` | Update MR comment | | `gitlab_award_emoji` | Award emoji to note | | `gitlab_remove_emoji` | Remove emoji from note | ### MR Discussions | Tool | Description | |------|-------------| | `gitlab_list_mr_discussions` | List discussions | | `gitlab_create_mr_discussion` | Create discussion (inline + multi-line; old_line/new_line pair resolved from the MR diff) | | `gitlab_reply_to_discussion` | Reply to discussion | | `gitlab_resolve_discussion` | Resolve/unresolve discussion | ### Draft Notes | Tool | Description | |------|-------------| | `gitlab_create_draft_note` | Create an unpublished draft note (SHAs from the MR's latest version, paired line number from its diff) | | `gitlab_list_draft_notes` | List the current user's unpublished draft notes | | `gitlab_publish_draft_notes` | Publish ALL pending draft notes at once ("Submit review") | | `gitlab_delete_draft_note` | Delete a single unpublished draft note | ### Pipelines | Tool | Description | |------|-------------| | `gitlab_list_pipelines` | List pipelines | | `gitlab_get_pipeline` | Get pipeline (with optional jobs) | | `gitlab_create_pipeline` | Trigger pipeline | | `gitlab_retry_pipeline` | Retry failed jobs | | `gitlab_cancel_pipeline` | Cancel pipeline | ### Jobs | Tool | Description | |------|-------------| | `gitlab_retry_job` | Retry a job | | `gitlab_play_job` | Trigger manual job | | `gitlab_cancel_job` | Cancel a job | | `gitlab_get_job_log` | Get job log output (last 200 lines by default; `tail_lines=0` for all) | ### Tags | Tool | Description | |------|-------------| | `gitlab_list_tags` | List tags | | `gitlab_get_tag` | Get tag details | | `gitlab_create_tag` | Create a tag | | `gitlab_delete_tag` | Delete a tag | ### Releases | Tool | Description | |------|-------------| | `gitlab_list_releases` | List releases | | `gitlab_get_release` | Get release details | | `gitlab_create_release` | Create a release | | `gitlab_update_release` | Update a release | | `gitlab_delete_release` | Delete a release | ### CI/CD Variables | Tool | Description | |------|-------------| | `gitlab_list_variables` | List project variables | | `gitlab_create_variable` | Create project variable | | `gitlab_update_variable` | Update project variable | | `gitlab_delete_variable` | Delete project variable | | `gitlab_list_group_variables` | List group variables | | `gitlab_create_group_variable` | Create group variable | | `gitlab_update_group_variable` | Update group variable | | `gitlab_delete_group_variable` | Delete group variable | ### Issues | Tool | Description | |------|-------------| | `gitlab_list_issues` | List issues | | `gitlab_get_issue` | Get issue details | | `gitlab_create_issue` | Create an issue | | `gitlab_update_issue` | Update an issue | | `gitlab_add_issue_comment` | Add comment to issue |
## Resources (7) The server exposes curated workflow guides as [MCP resources](https://modelcontextprotocol.io/docs/concepts/resources) that clients can read on demand. | URI | Name | Description | |-----|------|-------------| | `resource://rules/gitlab-ci` | GitLab CI/CD Pipeline Patterns | Stage design, job rules, caching, artifacts, `needs` DAG, multi-project pipelines | | `resource://rules/git-workflow` | Git Workflow Standards | Branch naming, trunk-based flow, merge vs rebase, protected branches | | `resource://rules/mr-hygiene` | Merge Request Best Practices | MR size, description templates, review checklists, thread resolution | | `resource://rules/conventional-commits` | Conventional Commits Spec | Commit types, scopes, breaking changes, changelog generation | | `resource://guides/code-review` | Code Review Standards | Review priorities, inline comments, approval workflows, nit vs blocker | | `resource://guides/codeowners` | GitLab CODEOWNERS Reference | Syntax, section owners, approval rules, pattern matching | | `resource://guides/approval-workflow` | Approval Workflow Guide | Approval types, project vs MR-level rules, SHA safety, merge readiness | ## Prompts (6) The server provides [MCP prompts](https://modelcontextprotocol.io/docs/concepts/prompts) — reusable multi-tool workflow templates that clients can surface as slash commands. | Prompt | Parameters | Workflow | |--------|-----------|----------| | `review_mr` | `project_id`, `mr_iid` | Fetch MR → check pipeline → review changes → write discussion notes | | `approve_mr` | `project_id`, `mr_iid` | Check approvals → verify pipeline → review changes → approve or request changes | | `diagnose_pipeline` | `project_id`, `pipeline_id` | Fetch pipeline → identify failed jobs → get logs → suggest fix | | `prepare_release` | `project_id`, `tag_name`, `ref` | Compare commits since last tag → draft changelog → create tag + release | | `setup_branch_protection` | `project_id` | Review settings → configure merge method → set approval rules | | `triage_issues` | `project_id`, `label` | List open issues → categorize → prioritize → identify duplicates | ## Usage Examples ### Projects & Branches ``` "Get details for project my-org/api-gateway" → gitlab_get_project(project_id="my-org/api-gateway") "Create a feature branch from main" → gitlab_create_branch(project_id="123", branch_name="feat/login", ref="main") "Delete all branches merged into main" → gitlab_list_branches(project_id="123") → filter merged → gitlab_delete_branch for each ``` ### Merge Requests & Code Review ``` "Open a merge request from feat/login to main" → gitlab_create_mr(project_id="123", source_branch="feat/login", target_branch="main", title="Add login") "Review MR !42 — list changes and add inline comments" → gitlab_mr_changes(project_id="123", mr_iid=42) → gitlab_create_mr_discussion(project_id="123", mr_iid=42, body="nit: ...", new_path="src/auth.py", new_line=15) # old_line is filled from the diff when line 15 is unchanged "Merge MR !42 after resolving all threads" → gitlab_list_mr_discussions(project_id="123", mr_iid=42) → resolve unresolved → gitlab_merge_mr(project_id="123", mr_iid=42, squash=True) ``` ### Pipelines & CI/CD ``` "Show failed pipelines on main this week" → gitlab_list_pipelines(project_id="123", ref="main", status="failed") "Retry a failed pipeline" → gitlab_retry_pipeline(project_id="123", pipeline_id=456) "Get the build log for job 789" → gitlab_get_job_log(project_id="123", job_id=789, tail_lines=100) ``` ### Issues ``` "Create a bug report in project 123" → gitlab_create_issue(project_id="123", title="Login page 500 error", labels=["bug","P1"]) "Find open issues assigned to me" → gitlab_list_issues(project_id="123", state="opened", assignee_username="johndoe") ``` ## Security Considerations - **Token scope**: Use the minimum required scope. `api` scope grants full access; prefer `read_api` for read-only deployments. - **Read-only mode**: Set `MCP_READ_ONLY=true` (or `GITLAB_READ_ONLY=true`) in the launch you control and the server starts locked: the 57 write tools and the write-workflow prompts are not listed, a call to one returns "Unknown tool", and the guard refuses any write that still reaches it. Nothing lifts the lock at runtime — not a `false` from another source, not a `.env` file, not tool arguments; any value other than `false`/`0`/`no`/`off`/empty locks, and `--read-only` can only raise it. The lock is only as strong as the launch: set it where users cannot edit it (a managed MCP config such as Claude Code `managed-mcp.json`, a container, k8s or systemd env, or a hosted HTTP deployment). A user who starts the server themselves controls its environment. In OAuth mode also set `GITLAB_OAUTH_SCOPES=read_api` so each user's GitLab token lacks write scope. - **SSL verification**: `GITLAB_SSL_VERIFY=true` by default. Only disable for self-signed certificates in trusted networks. - **CI/CD variable masking**: `gitlab_list_variables` and `gitlab_list_group_variables` automatically mask values of variables marked as masked in GitLab, returning `***MASKED***` instead of the actual value. - **MCP tool annotations**: Each tool declares `readOnlyHint`, `destructiveHint`, and `idempotentHint` for client-side permission prompts. - **Approval on destructive tools**: each destructive tool carries the `anthropic/requiresUserInteraction` client-vendor meta, so Claude Code forces a permission prompt on every call — even under `acceptEdits`, `auto`, and `bypassPermissions` — with no "don't ask again". The meta is inert for clients that do not read it. - **Credential storage**: The server reads `GITLAB_TOKEN` from the environment and never writes it. `mcp-gitlab auth login` stores OAuth tokens in `~/.config/mcp-gitlab/credentials.json` (mode 0600) and refreshes them in place. ## Rate Limits & Permissions ### Rate Limits GitLab enforces per-user rate limits (default: 2000 requests/minute for authenticated users). When rate-limited, tools return a 429 error with a hint to wait before retrying. Paginated endpoints default to 20 results per page; use `per_page` (max 100) to reduce the number of API calls. ### Required Permissions | Operation | Minimum GitLab Role | |-----------|-------------------| | Read projects, MRs, pipelines, issues, files/tree/blame/code search | Reporter | | Create branches, MRs, issues, draft notes (create/publish/delete) | Developer | | Merge MRs, manage CI/CD variables | Maintainer | | Delete projects, manage approval rules | Maintainer/Owner | | Share projects/groups | Owner (or Admin) | ## CLI & Transport Options ```bash # Default: stdio transport (for MCP clients) uvx mcp-gitlab # HTTP transport — streamable-http is recommended for remote use uvx mcp-gitlab --transport streamable-http --port 9000 # SSE transport — deprecated by MCP 2026-07-28; still works, prints a warning uvx mcp-gitlab --transport sse --host 127.0.0.1 --port 8000 # OAuth 2.1 mode (per-user GitLab sign-in; streamable-http only) — see "OAuth 2.1" above uvx mcp-gitlab --auth oauth --transport streamable-http # Local sign-in for stdio without a token — see "Sign in without a token" above mcp-gitlab auth login|status|logout # CLI overrides for config GITLAB_TOKEN=glpat-xxx uvx mcp-gitlab --gitlab-url https://gitlab.example.com --read-only # Expose only one job's tools (same as MCP_TOOLSETS=review); see Toolsets above uvx mcp-gitlab --toolsets review ``` `--gitlab-token` still works but is deprecated and hidden from `--help`: it exposes the token to `ps` and shell history. Use `GITLAB_TOKEN`. `--auth {token,oauth}` selects the authentication mode (`token` by default); `oauth` requires `--transport streamable-http`. In token mode an HTTP transport serves every tool with the single `GITLAB_TOKEN` to anyone who reaches the port, so binding to a non-loopback `--host` is refused unless you set `GITLAB_HTTP_BEARER_TOKEN` (≥ 16 chars; clients send it as `Authorization: Bearer `) or switch to `--auth oauth` for per-user sign-in. The default `127.0.0.1` bind needs neither. The server loads `.env` files from the working directory automatically via `python-dotenv`. ### Observability An HTTP transport exposes `GET /health`, an unauthenticated liveness probe that returns the server name and version (no config, no secrets) for container and load-balancer checks: ```bash curl -s http://127.0.0.1:8000/health # {"status":"ok","name":"GitLab MCP Server","version":"0.23.0"} ``` FastMCP emits OpenTelemetry spans for `tools/call` and propagates trace context by default — no opt-in needed. Point it at a collector with the standard OTel environment and launch under the auto-instrumentation wrapper: ```bash OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 \ opentelemetry-instrument uvx mcp-gitlab --transport streamable-http --port 8000 ``` Set `FASTMCP_TELEMETRY_MODE=off` to disable tracing entirely (`propagation_only` keeps trace-context propagation without emitting spans). ## Docker A minimal non-root image builds straight from this repo (uv, the committed `uv.lock`). The image defaults to locked read-only (`MCP_READ_ONLY=true`); override it to allow writes. ```bash docker build -t mcp-gitlab . # locked read-only (the image default) — stdio for an MCP client docker run --rm -i \ -e GITLAB_URL=https://gitlab.example.com -e GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx \ mcp-gitlab # allow writes: override the read-only default docker run --rm -i \ -e MCP_READ_ONLY=false \ -e GITLAB_URL=https://gitlab.example.com -e GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx \ mcp-gitlab ``` For an HTTP transport, append `--transport streamable-http --host 0.0.0.0`; a non-loopback bind in token mode needs `GITLAB_HTTP_BEARER_TOKEN` (≥ 16 chars) or `GITLAB_AUTH=oauth` (see above). ## FAQ ### Does mcp-gitlab support MCP 2.0? Yes. It supports the MCP 2026-07-28 specification, often called MCP 2.0. It also stays compatible with 2025-11-25 clients. ### Does it work with self-hosted GitLab? Yes. It works with GitLab.com and self-hosted GitLab (CE and EE). It needs no GitLab Duo or Premium. ### Which transports does it support? It supports `stdio` (the default) and `streamable-http`. The `sse` transport still works, but the 2026-07-28 specification deprecates it. OAuth 2.1 mode (`--auth oauth`) is available on `streamable-http` only. ### Can I run mcp-gitlab read-only so the assistant cannot merge, push or comment? Yes. Start it with `MCP_READ_ONLY=true` (or `GITLAB_READ_ONLY=true`). The 57 write tools and the write-workflow prompts are then not listed at all, so the model never sees them; a call to one returns "Unknown tool", and nothing can turn writes back on while the process runs. Set it in a launch you control — a managed MCP config, a container or systemd env, or a hosted deployment — because a user who starts the server themselves can start it without the variable. ### What token scopes does it need? Use a token with the `api` scope for full access. Use the `read_api` scope for read-only deployments. The server reads a `GITLAB_TOKEN` from the environment and never writes it; tokens from `mcp-gitlab auth login` are stored in `~/.config/mcp-gitlab/credentials.json`. ### Which AI assistants work with it? It works with Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot, and any MCP-compatible client. ### How do I install mcp-gitlab? Run `uvx mcp-gitlab`, or add it to your MCP client configuration. See the installation section above. ## Related MCP Servers - [mcp-atlassian-extended](https://github.com/vish288/mcp-atlassian-extended) — Jira + Confluence integration (22 tools, 15 resources, 5 prompts) - [mcp-coda](https://github.com/vish288/mcp-coda) — Coda integration (53 tools, 12 resources, 5 prompts) - [mcp-argocd](https://github.com/vish288/mcp-argocd) — Argo CD integration (37 tools, 7 resources, 7 prompts) ## Development ```bash git clone https://github.com/vish288/mcp-gitlab.git cd mcp-gitlab uv sync --all-extras uv run pytest --cov uv run ruff check . uv run ruff format --check . ``` Please read the [contributing guide](https://github.com/vish288/.github/blob/main/CONTRIBUTING.md) and the [security policy](https://github.com/vish288/.github/blob/main/SECURITY.md) before opening a pull request or reporting a vulnerability. ## License MIT