@wanadev/mcp-gitlab
# @wanadev/mcp-gitlab
A Model Context Protocol (MCP) server that gives project managers **and developers** full control over GitLab **epics**, **issues**, **milestones**, **iterations**, **merge requests**, **code review**, **pipelines**, **branches**, **repository content**, **labels**, **boards**, **members**, and **governance** from Claude Desktop, Claude Code, or any MCP-compatible client.
## Why this MCP server?
Existing tools like **glab** are developer-oriented and CLI-only. `@wanadev/mcp-gitlab` covers both lenses -- planning *and* code -- through a conversational interface:
- **Epics, milestones & iterations** -- create, update, close, reopen, and link issues to epics. Track sprints with iterations (CRUD). Set health status (on track / needs attention / at risk) and view progress via Work Items API. Attach issues under epics via the modern hierarchy widget.
- **Full MR lifecycle** -- create, update, merge, rebase, approve, diff, and comment on merge requests.
- **Code review** -- create line-positioned discussions on MR diffs and resolve them.
- **CI/CD** -- list and inspect pipelines, fetch job logs, retry or cancel runs.
- **Repository content** -- list/create branches, browse the tree, read files, list commits, *and* write files (create / update / delete / atomic multi-file commits).
- **Search** -- full-text search across issues, MRs, and code blobs at project or group scope.
- **Members management** -- add, update, and remove group/project members with access levels.
- **Governance** -- list and manage protected branches and MR approval rules.
- **Uploads** -- upload images/binaries to embed in issues, MRs, and epics.
- **Labels & users** -- CRUD on labels, search users to assign work.
- **Cross-group visibility** -- query multiple GitLab groups in the same conversation (no hardcoded group ID).
- **Time tracking** -- see estimated vs. spent time on issues at a glance.
- **Comments (notes)** -- read, add, edit, and delete notes on issues, epics, and MRs.
- **Dry-run by default** -- every write operation previews what it will do before touching GitLab.
## Quick setup
### Prerequisites
- **Node.js >= 20**
- A GitLab **Personal Access Token** (PAT) with the `api` scope (or `read_api` for read-only access)
- **GitLab Premium/Ultimate** for epics and iterations (issues, milestones, MRs, labels, and boards work with all editions)
### 1. Generate a GitLab token
1. Go to **GitLab > Settings > Access Tokens**
2. Create a token with the `api` scope
3. Copy the token
### 2. Install
#### Claude Code (plugin)
```
/plugin marketplace add wanadev/gitlab-mcp
/plugin install wanadev-gitlab@wanadev-gitlab
```
Set `GITLAB_TOKEN` in your environment (`.bashrc`, `.zshrc`, or system variable). The plugin will guide you on first launch if it's missing.
#### Claude Desktop (manual config)
Add the following to your `claude_desktop_config.json`:
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"gitlab": {
"command": "npx",
"args": ["-y", "@wanadev/mcp-gitlab"],
"env": {
"GITLAB_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx",
"GITLAB_BASE_URL": "https://gitlab.com",
"GITLAB_READ_ONLY": "false"
}
}
}
}
```
### 3. Restart and test
The MCP server will be available immediately. Test with: *"List my GitLab groups"*
## Environment variables
| Variable | Required | Description |
|----------|:--------:|-------------|
| `GITLAB_TOKEN` | Yes | GitLab Personal Access Token |
| `GITLAB_BASE_URL` | No | GitLab instance URL (default: `https://gitlab.com`) |
| `GITLAB_READ_ONLY` | No | Set to `true` to block all write operations |
> **Note:** There is no `GITLAB_GROUP_ID` environment variable. Every group-scoped tool takes a `group_id` parameter. Use `list_groups` to discover accessible groups -- the LLM does this automatically.
## Dry-run safety
All write tools (`create_*`, `update_*`, `close_*`, `set_*`, `add_*`) include a `dry_run` parameter that defaults to **`true`**.
| Mode | Behavior |
|------|----------|
| `dry_run: true` (default) | Returns a summary of the planned action **without** executing anything on GitLab. |
| `dry_run: false` | Executes the action for real, after the user confirms. |
This prevents accidental changes: the LLM always shows what it intends to do first and only proceeds after your approval.
## All 100 tools
### Epics (12 tools -- requires GitLab Premium/Ultimate)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_epics` | List epics (filter by state, search, labels) | group | -- |
| `get_epic` | Get epic details by number | group | -- |
| `create_epic` | Create an epic | group | dry_run |
| `update_epic` | Update an epic (title, description, labels, dates) | group | dry_run |
| `close_epic` | Close an epic | group | dry_run |
| `reopen_epic` | Reopen a closed epic | group | dry_run |
| `list_epic_issues` | List issues linked to an epic | group | -- |
| `add_issue_to_epic` | Link an issue to an epic | group | dry_run |
| `list_epic_notes` | List comments on an epic | group | -- |
| `add_epic_note` | Add a comment to an epic | group | dry_run |
| `update_epic_note` | Edit an existing epic comment | group | dry_run |
| `delete_epic_note` | Delete an epic comment | group | dry_run |
### Work Items (9 tools -- requires GitLab Premium/Ultimate)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `get_epic_widgets` | Get epic widgets: health status, progress, milestone, iteration, linked items | group | -- |
| `set_epic_milestone` | Associate a milestone with an epic | group | dry_run |
| `set_epic_health_status` | Set health status on an epic (onTrack / needsAttention / atRisk) | group | dry_run |
| `set_issue_health_status` | Set health status on an issue (onTrack / needsAttention / atRisk) | project | dry_run |
| `set_epic_iteration` | Associate an iteration (sprint) with an epic | group | dry_run |
| `add_linked_item` | Link work items (RELATED / BLOCKS / BLOCKED_BY) | group/project | dry_run |
| `get_work_item_id` | Resolve the WorkItem global ID for an epic or issue (required by link/unlink) | group/project | -- |
| `link_work_items` | Attach children (issues/epics) under a parent epic via hierarchy widget | group/project | dry_run |
| `unlink_work_items` | Detach children from their current parent | group/project | dry_run |
### Issues (11 tools)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_issues` | List issues for a group (filter by state, labels, milestone, assignee, iteration) | group | -- |
| `list_project_issues` | List issues for a single project (with iteration filter) | project | -- |
| `get_issue` | Get issue details (with time tracking) | project | -- |
| `create_issue` | Create an issue | project | dry_run |
| `update_issue` | Update an issue | project | dry_run |
| `close_issue` | Close an issue | project | dry_run |
| `reopen_issue` | Reopen a closed issue | project | dry_run |
| `list_issue_notes` | List comments on an issue | project | -- |
| `add_issue_note` | Add a comment to an issue | project | dry_run |
| `update_issue_note` | Edit an existing issue comment | project | dry_run |
| `delete_issue_note` | Delete an issue comment | project | dry_run |
### Milestones (10 tools)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_milestones` | List milestones for a group | group | -- |
| `get_milestone` | Get milestone details (group) | group | -- |
| `create_milestone` | Create a group milestone | group | dry_run |
| `update_milestone` | Update a group milestone | group | dry_run |
| `close_milestone` | Close a group milestone | group | dry_run |
| `list_project_milestones` | List milestones at the project level (the only way on GitLab CE/Free) | project | -- |
| `get_project_milestone` | Get a project milestone | project | -- |
| `create_project_milestone` | Create a project milestone | project | dry_run |
| `update_project_milestone` | Update a project milestone | project | dry_run |
| `close_project_milestone` | Close a project milestone | project | dry_run |
### Merge Requests (18 tools)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_merge_requests` | List merge requests for a group | group | -- |
| `get_merge_request` | Get merge request details | project | -- |
| `create_merge_request` | Open a new MR (source/target branch, title, description) | project | dry_run |
| `update_merge_request` | Update an MR (title, description, labels, assignees, reviewers) | project | dry_run |
| `merge_merge_request` | Merge an MR (optionally squash or delete source branch) | project | dry_run |
| `approve_merge_request` | Approve an MR | project | dry_run |
| `rebase_merge_request` | Asynchronously rebase the MR's source branch on top of target | project | dry_run |
| `get_mr_diff` | Get the diff/changes for an MR (also returns diff_refs for line-positioned comments) | project | -- |
| `list_mr_notes` | List comments on an MR | project | -- |
| `add_mr_note` | Add a comment to an MR | project | dry_run |
| `update_mr_note` | Edit an existing MR comment | project | dry_run |
| `delete_mr_note` | Delete an MR comment | project | dry_run |
| `create_mr_discussion` | Code-review comment on a specific diff line (requires diff_refs from get_mr_diff) | project | dry_run |
| `resolve_mr_discussion` | Mark an MR discussion as resolved or unresolved | project | dry_run |
| `list_mr_approval_rules` | List project-level MR approval rules | project | -- |
| `create_mr_approval_rule` | Create a project-level approval rule (eligible users/groups, branches) | project | dry_run |
| `update_mr_approval_rule` | Update an existing approval rule | project | dry_run |
| `delete_mr_approval_rule` | Delete an approval rule | project | dry_run |
### CI/CD (5 tools)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_pipelines` | List pipelines for a project (filter by ref, status, etc.) | project | -- |
| `get_pipeline` | Get pipeline details and jobs | project | -- |
| `get_job_log` | Fetch the trace/log of a CI job | project | -- |
| `retry_pipeline` | Retry a failed pipeline | project | dry_run |
| `cancel_pipeline` | Cancel a running pipeline | project | dry_run |
### Branches & Repository (12 tools)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_branches` | List branches in a project | project | -- |
| `create_branch` | Create a branch from a ref | project | dry_run |
| `list_repository_tree` | List the files/folders in a repo path | project | -- |
| `get_file` | Read a file's contents at a given ref | project | -- |
| `list_commits` | List commits (filter by ref, author, date) | project | -- |
| `create_file` | Create a new file via the API (commit included) | project | dry_run |
| `update_file` | Update a file (optimistic concurrency via last_commit_id) | project | dry_run |
| `delete_file` | Delete a file in a single commit | project | dry_run |
| `commit_files` | Atomic multi-file commit (create/update/delete/move/chmod) | project | dry_run |
| `list_protected_branches` | List protected branches with their access levels | project | -- |
| `protect_branch` | Protect a branch with push/merge/unprotect access levels | project | dry_run |
| `unprotect_branch` | Remove branch protection | project | dry_run |
### Iterations (3 tools -- requires GitLab Premium/Ultimate)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_iterations` | List iterations/sprints (filter by state: upcoming, current, closed) | group | -- |
| `create_iteration` | Create an iteration | group | dry_run |
| `update_iteration` | Update an iteration (title, dates, state) | group | dry_run |
### Utilities (20 tools)
| Tool | Description | Scope | Write |
|------|-------------|:-----:|:-----:|
| `list_groups` | Discover accessible groups | -- | -- |
| `list_projects` | List projects in a group | group | -- |
| `list_group_members` | List members of a group | group | -- |
| `add_group_member` | Add a user to a group with an access level | group | dry_run |
| `add_project_member` | Add a user to a project with an access level | project | dry_run |
| `update_member_access_level` | Change a member's access level (group or project) | group/project | dry_run |
| `remove_member` | Remove a user from a group or project | group/project | dry_run |
| `list_labels` | List labels for a group | group | -- |
| `create_label` | Create a label | group | dry_run |
| `update_label` | Update a label (name, color, description) | group | dry_run |
| `delete_label` | Delete a label | group | dry_run |
| `list_boards` | List issue boards for a group | group | -- |
| `list_workitem_statuses` | List available work item statuses (for health filtering) | group | -- |
| `get_work_item_type_id` | Resolve the global ID of a Work Item type (EPIC, ISSUE, TASK, …) for a namespace | group/project | -- |
| `upload_file` | Upload a file to a project and return the Markdown snippet to embed | project | dry_run |
| `search_issues` | Full-text search issues at project or group scope | group/project | -- |
| `search_merge_requests` | Full-text search MRs at project or group scope | group/project | -- |
| `search_blobs` | Full-text search code/file content at project or group scope | group/project | -- |
| `search_users` | Search GitLab users by name or username | -- | -- |
| `get_current_user` | Check connection (current user info) | -- | -- |
> A `gitlab_setup` helper tool is also registered automatically when `GITLAB_TOKEN` is missing or `GITLAB_BASE_URL` is invalid — it guides the user through configuration and is not counted in the 100 above.
## Example prompts
- *"List my GitLab groups"*
- *"Show open epics in group 42"*
- *"What's the health status and progress of epic #5?"*
- *"Set epic #12 health status to 'needs attention'"*
- *"Link epic #3 as blocking issue #45"*
- *"List issues labeled `bug` in the wanadev group"*
- *"Create an epic called 'Homepage Redesign' in group 42 with a deadline of April 30"*
- *"Which issues are linked to epic #5 in the wanadev group?"*
- *"Close issue #15 in project 789"*
- *"Who are the members of the wanadev group?"*
- *"Show me all open merge requests in group 42"*
- *"How much time has been spent on issue #23 in project 456?"*
- *"Add a comment on epic #3 in group 42: Specs validated, ready for dev"*
- *"What milestones are coming up in the wanadev group?"*
- *"List the boards for group 42"*
- *"Open an MR from `feat/new-login` to `main` in project 789, then request review from @alice"*
- *"Show me the diff of MR !42 and summarize the changes"*
- *"List the last 5 failed pipelines on `main` in project 789 and fetch the log of the failing job"*
- *"Retry pipeline 12345 in project 789"*
- *"Create a `release/2026.05` branch from `main` in project 789"*
- *"Read the contents of `package.json` on branch `main` in project 789"*
- *"Find a user named 'Jean Dupont' and assign them issue #15"*
- *"Rebase MR !42 on top of main"*
- *"Comment on line 73 of `src/auth.ts` in MR !42 — this should be wrapped in a try/catch"*
- *"Search the wanadev group for issues mentioning 'CVE-2024'"*
- *"Show me where `processPayment` is used across project 789"*
- *"Add @alice as Developer (level 30) to the kp1/api project"*
- *"Protect `main` on project 789 — only maintainers can merge, no direct pushes"*
- *"Set up a 2-approval rule named 'security' on project 789, applies to all protected branches"*
- *"Attach issues #12, #14, and #18 as children of epic #5 in the wanadev group"*
- *"Upload `screenshot.png` to project 789 and embed it in issue #15"*
## Use cases for project managers
### Sprint planning
> *"List the active milestones in the wanadev group. For each one, show me the open issues and how many are unassigned."*
Claude calls `list_milestones` → `list_issues` per milestone → summarizes the gaps. You see at a glance what's on track and what needs attention.
### Daily standup prep
> *"Show me all open MRs in the [project] group that have been waiting for review for more than 3 days. Also list any issues that were closed yesterday."*
Claude calls `list_merge_requests` (state: opened) → `list_issues` (state: closed, sort: updated_at) → gives you a ready-made standup brief.
### Epic progress review
> *"Give me a status report on epic #12 in group wanadev: how many issues are done vs. open, what's the total time spent, and list the latest comments."*
Claude chains `get_epic` → `list_epic_issues` → reads `time_stats` from each issue → `list_epic_notes` → returns a structured progress report.
### Cross-group dashboard
> *"Compare the open issue count across my three groups: wanadev, [project], and infra. Which group has the most overdue issues?"*
Claude calls `list_groups` → `list_issues` for each group with due date filtering → builds a comparison table.
### Quick issue triage
> *"In the [project] group, find all issues labeled 'urgent' with no assignee. Assign them to @jean and add a comment saying 'Triaged in weekly review'."*
Claude calls `list_issues` (labels: urgent) → for each unassigned issue, dry-runs `update_issue` (assignee) + `add_issue_note` → shows you the plan → you confirm → done.
### Milestone closure
> *"Close milestone 'Sprint 14' in group wanadev. Before that, show me any issues still open in it."*
Claude calls `list_issues` (milestone: Sprint 14, state: opened) → warns you about remaining items → dry-runs `close_milestone` → you confirm.
## Development
```bash
git clone https://github.com/wanadev/gitlab-mcp.git
cd gitlab-mcp
npm install
npm run build # Build ESM (tsc)
npm run typecheck # TypeScript type checking
npm run dev # Build in watch mode
```
## Notes
- **`issue_id` vs `issue_iid`** -- `add_issue_to_epic` requires the global issue ID (not the `#iid` displayed in the project). Both `list_issues` and `list_epic_issues` return both values.
- **Read-only mode** -- With `GITLAB_READ_ONLY=true`, any create/update/close attempt returns a clear error.
- **403 on epics** -- Epic endpoints require a GitLab Premium or Ultimate license.
- **Multi-group workflow** -- You can work across several groups in a single conversation. The LLM will call `list_groups` to discover them, then pass the right `group_id` to each tool.
- **GitLab CE / Free support** -- The server introspects your GitLab instance at startup and detects whether `Issue.weight` / `Issue.epic` are available (Premium/Ultimate-only fields). On Free/CE, those fields are silently omitted from queries and mutations, so all issue tools work — only epic-specific features remain Premium. The detected tier is logged at startup: `[..., Premium/Ultimate]` or `[..., Free/CE]`.
TDQS
Scored across 100 tools
Most tools follow a clear resource+action pattern, so group vs project variants and note/diff/discussion tools are generally distinguishable. However, add_issue_to_epic and link_work_items explicitly overlap (the latter is described as the replacement), and list_issues/list_project_issues plus several similar work-item helpers add some ambiguity.
The vast majority use consistent snake_case verb_noun names (list_*, get_*, create_*, update_*, close_*, delete_*). Minor inconsistencies: `mr` abbreviations vs `merge_request`, and `workitem` vs `work_item` in list_workitem_statuses, prevent a perfect score.
100 tools is an extreme count for an MCP surface, far beyond typical scoped servers, and includes redundant group/project variants plus legacy/new Work Item linking (add_issue_to_epic vs link_work_items). Even though GitLab is broad, this bloated surface is expensive for agents to navigate and many tools feel duplicative rather than earning their place.
Core lifecycles for epics, issues, milestones, MRs, notes, labels, members, branches/files, and pipelines are well covered. Notable gaps remain: no delete issue/epic/milestone, no close/reopen/delete MR, no project member listing, no branch deletion, and no project/group creation, so agents will hit dead ends in some administrative workflows.