GitLab MCP
MCP server for GitLab that provides 85 tools covering issues, merge requests, repositories, CI/CD, wiki, packages, and attachments, with support for both gitlab.com and self-hosted instances. It can read attachments so models can see screenshots and other files.
README
GitLab MCP
An MCP server for GitLab — works with gitlab.com and with any self-hosted instance
(https://gitlab.velocorner.ch, https://dev.tectiers.com, anything else).
85 tools covering issues, merge requests, the repository, CI/CD, wiki, packages and
registries, plus gitlab_api_request — a generic escape hatch to any REST API v4 endpoint
that has no dedicated tool.
The headline feature: the server reads attachments. A screenshot pasted into an issue is downloaded with authentication and handed to the model as an image — not as a link it cannot open.
Two ways to run it
| Mode | Transport | Configuration comes from | Use it when |
|---|---|---|---|
| Hosted | Streamable HTTP on /mcp |
request headers, per call | clients should connect to a URL with nothing installed |
| Local | stdio | environment variables | the instance is private, or credentials must not leave the machine |
The public deployment lives at https://gitlab-mcp.webapace.ink — the landing page on /,
the MCP endpoint on /mcp. Connecting to it needs no install:
claude mcp add --transport http gitlab \
https://gitlab-mcp.webapace.ink/mcp \
--header "X-GitLab-Url: https://gitlab.example.com" \
--header "X-GitLab-Token: glpat-xxxxxxxxxxxx"
The rest of this document is for running your own copy in either mode.
Install
git clone <repo> gitlab-mcp
cd gitlab-mcp
npm install
npm run build
Requires Node.js 20 or newer.
Token
GitLab → Settings → Access Tokens. A personal, group or project access token all work.
| Scope | What it allows |
|---|---|
read_api |
everything read-only: issues, MRs, files, pipelines, attachments |
api |
the above plus writes: comments, commits, merge requests, issue edits |
A project access token is the safest choice when the work is confined to one project.
Configuration
In stdio mode everything is configured through environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
GITLAB_URL |
yes | https://gitlab.com |
Instance URL. A trailing /api/v4 is accepted and stripped |
GITLAB_TOKEN |
yes | — | Access token |
GITLAB_PROJECT_ID |
no | — | Default project: 42 or group/subgroup/project |
GITLAB_PROJECT_LOCK |
no | false |
true — hard isolation inside GITLAB_PROJECT_ID |
GITLAB_READ_ONLY |
no | false |
true — every mutating tool is refused |
GITLAB_AUTH_TYPE |
no | pat |
oauth — send the token as Authorization: Bearer |
GITLAB_TLS_REJECT_UNAUTHORIZED |
no | true |
false — skip TLS verification (private CA) |
GITLAB_TIMEOUT_MS |
no | 60000 |
Per-request timeout |
GITLAB_MAX_ATTACHMENT_BYTES |
no | 8388608 |
Size cap for downloaded attachments |
Aliases that are also read: GITLAB_API_URL / CI_SERVER_URL for the URL,
GITLAB_PERSONAL_ACCESS_TOKEN / GITLAB_ACCESS_TOKEN for the token.
Connecting from Claude Code
claude mcp add gitlab \
--env GITLAB_URL=https://gitlab.velocorner.ch \
--env GITLAB_TOKEN=glpat-xxxxxxxxxxxx \
-- node /absolute/path/gitlab-mcp/dist/index.js
Or drop a .mcp.json into the root of your working repository, so the configuration travels
with the project:
{
"mcpServers": {
"gitlab": {
"command": "node",
"args": ["/absolute/path/gitlab-mcp/dist/index.js"],
"env": {
"GITLAB_URL": "https://dev.tectiers.com",
"GITLAB_TOKEN": "glpat-xxxxxxxxxxxx",
"GITLAB_PROJECT_ID": "team/backend",
"GITLAB_PROJECT_LOCK": "true"
}
}
}
}
The same block works in Claude Desktop (claude_desktop_config.json) and Cursor (.cursor/mcp.json).
Several instances at once — just several entries with different names:
{
"mcpServers": {
"gitlab-com": { "command": "node", "args": ["…/dist/index.js"], "env": { "GITLAB_URL": "https://gitlab.com", "GITLAB_TOKEN": "glpat-…" } },
"gitlab-work": { "command": "node", "args": ["…/dist/index.js"], "env": { "GITLAB_URL": "https://gitlab.velocorner.ch", "GITLAB_TOKEN": "glpat-…" } }
}
}
To verify the connection, ask for gitlab_whoami — it returns the authenticated user, the
instance URL and the GitLab version.
Hosted mode
Start the HTTP transport with MCP_TRANSPORT=http (or --http). It serves three routes:
| Route | Purpose |
|---|---|
GET / |
the landing page (landing.html, or LANDING_PATH) |
POST /mcp |
the MCP endpoint, stateless — one server instance per request |
GET /health |
liveness probe |
Every request carries its own credentials, so one deployment serves many users and many instances without holding state:
| Header | Maps to | Notes |
|---|---|---|
X-GitLab-Url |
GITLAB_URL |
required unless the deployment sets a default |
X-GitLab-Token |
GITLAB_TOKEN |
Authorization: Bearer <token> is accepted instead |
X-GitLab-Project-Id |
GITLAB_PROJECT_ID |
default project |
X-GitLab-Project-Lock |
GITLAB_PROJECT_LOCK |
true locks the session to that project |
X-GitLab-Read-Only |
GITLAB_READ_ONLY |
true refuses every mutating tool |
X-GitLab-Auth-Type |
GITLAB_AUTH_TYPE |
oauth to send a Bearer token to GitLab |
TLS verification, timeouts and the attachment size cap are deliberately not header-controlled: they are process-wide and belong to whoever runs the deployment.
Deployment settings:
| Variable | Default | Description |
|---|---|---|
MCP_TRANSPORT |
stdio |
http to start the HTTP server |
PORT / HOST |
8080 / 0.0.0.0 |
listen address |
LANDING_PATH |
./landing.html |
page served at / |
GITLAB_ALLOWED_INSTANCES |
— | comma-separated hostnames; when set, only these instances may be targeted |
GITLAB_URL, GITLAB_TOKEN, … |
— | fallbacks used when the corresponding header is absent |
Without an allowlist the server refuses private addresses (localhost, RFC 1918 ranges,
169.254.*, *.internal, *.local) so a public deployment cannot be used to probe the network
it runs in. A GitLab on a private network therefore needs a local or internal deployment — which
is the correct answer anyway.
docker build -t gitlab-mcp .
docker run -p 8080:8080 -e GITLAB_ALLOWED_INSTANCES=gitlab.example.com gitlab-mcp
Deploying
deploy.sh does the whole cycle on the server — pull, build, swap the container, verify,
purge the CDN cache:
./deploy.sh # the usual deploy
./deploy.sh --page-only # only replace landing.html in the running container, no rebuild
./deploy.sh --no-pull # deploy the working tree as it is
./deploy.sh --logs # follow the container log afterwards
Copy deploy.env.example to deploy.env on the server and set the port, container name,
PUBLIC_URL and, if the site sits behind Cloudflare, a zone id and an API token with the
Cache Purge permission. deploy.env is git-ignored, so server-specific values stay there.
The previous image is tagged :previous before every build, and a failed health check
restores it automatically and exits non-zero, so a broken build never stays deployed.
Because the server reads landing.html from disk on every request, --page-only publishes
a page change in a second without rebuilding anything — but the file is baked into the image
by COPY landing.html, so the next full deploy is what makes it permanent.
Because the endpoint accepts tokens from callers, put it behind TLS and treat access logs accordingly. The server itself keeps nothing: no sessions, no storage, one throwaway server instance per request.
Single-project isolation
Two modes, and they stack.
Soft — a default project. Only GITLAB_PROJECT_ID is set: the project_id argument becomes
optional, but any other project is still reachable.
Hard — GITLAB_PROJECT_LOCK=true. The server resolves the project at startup and from then on:
- any
project_idother than the configured one is refused — the id, the full path and the URL-encoded path are all compared, so42,group/projectandgroup%2Fprojectcount as the same project; - group-wide and instance-wide operations (listing groups, group members, group search) are refused;
gitlab_api_requestonly accepts paths under/projects/<your project>/plus the harmless/user,/version,/markdown,/todos;gitlab_download_urlandgitlab_read_attachmentonly fetch URLs that belong to the project;gitlab_list_projectsreturns exactly one project and an unscopedgitlab_searchsearches inside it.
GITLAB_PROJECT_LOCK=true + GITLAB_READ_ONLY=true + a project access token with read_api
gives a server that can neither reach outside the project nor change anything inside it.
Screenshots and attachments
This is the part worth its own section. GitLab stores attachments at URLs like
/uploads/<hash>/screenshot.png — unreachable without authentication, and written into the issue
body as a relative link.
How it works:
1. Fetch the issue. gitlab_get_issue returns the description, every comment, and a separate
attachments block with absolute URLs already resolved:
{
"attachments": [
{
"title": "Screenshot_2026-08-04",
"url": "https://gitlab.velocorner.ch/team/web/uploads/a1b2…/Screenshot.png",
"is_image_embed": true,
"source": "description"
},
{ "title": "log", "url": "https://…/uploads/c3d4…/error.log", "is_image_embed": false, "source": "note #98123" }
]
}
2. Look at the image. gitlab_read_attachment with that url — PNG/JPEG/GIF/WebP come back as
an image the model can actually see. Text files come back as text, anything else as a base64 resource.
Relative paths work too: {"url": "/uploads/a1b2…/Screenshot.png", "project_id": "team/web"}.
Related tools:
gitlab_list_attachments— scan an issue/MR/epic and get only the attachment list, without the body;gitlab_get_filewithas_image: true— view an image committed to the repository;gitlab_get_job_artifact— pull a screenshot out of CI artifacts (Playwright, Cypress);gitlab_download_url— authenticated GET of any URL on the instance;gitlab_upload_file— upload a local file and get the markdown snippet to embed in a comment.
Downloads try several routes in order: the API first (/projects/:id/uploads/:secret/:file,
GitLab 16.6+), then the web URL with a PRIVATE-TOKEN header, then with ?private_token=. A
response that turns out to be a login page counts as a failure and the next route is tried — which
is what makes this work on older self-hosted instances.
Tools
Identifiers: a project is a numeric id or a full path group/subgroup/project; issues and merge
requests are addressed by their iid (the number shown in the UI), not the global id.
Every list tool shares the arguments page, per_page (max 100), all_pages (walk the whole
pagination) and limit.
Instance and utility
| Tool | Description |
|---|---|
gitlab_whoami |
Current user, instance URL, GitLab version — the configuration smoke test |
gitlab_search |
Search across projects, issues, MRs, code, commits, wiki, comments |
gitlab_list_todos / gitlab_mark_todo_done |
The user's todo list and marking items done |
gitlab_list_events |
Activity stream of the current user or a project |
gitlab_list_users / gitlab_get_user |
User lookup (project members only in locked mode) |
gitlab_markdown_render |
Render markdown through GitLab (expands relative links) |
gitlab_api_request |
Any REST API v4 endpoint: epics, feature flags, admin, instance specifics |
Projects and groups
| Tool | Description |
|---|---|
gitlab_list_projects / gitlab_get_project |
Project list and details |
gitlab_create_project / gitlab_update_project / gitlab_delete_project / gitlab_fork_project |
Project management |
gitlab_list_project_members / gitlab_add_project_member |
Project members |
gitlab_list_groups / gitlab_get_group / gitlab_list_group_projects / gitlab_list_group_members |
Groups |
gitlab_project_statistics |
Languages, contributors, repository size |
Issues
| Tool | Description |
|---|---|
gitlab_list_issues |
Issues of a project, a group or the whole instance, with filters |
gitlab_get_issue |
The whole issue: body, comments, attachments |
gitlab_create_issue / gitlab_update_issue / gitlab_delete_issue |
Issue CRUD, including close/reopen |
gitlab_list_notes / gitlab_create_note / gitlab_update_note |
Comments on issues, MRs, snippets, epics, commits |
gitlab_list_discussions / gitlab_reply_to_discussion |
Discussion threads |
gitlab_issue_links |
Links between issues (relates to / blocks / blocked by) |
gitlab_issue_time_tracking |
Estimate and spent time |
gitlab_issue_related_merge_requests |
Merge requests connected to the issue |
gitlab_list_labels / gitlab_create_label / gitlab_list_milestones / gitlab_list_boards |
Labels, milestones, boards |
gitlab_award_emoji |
Emoji reactions |
Merge requests
| Tool | Description |
|---|---|
gitlab_list_merge_requests / gitlab_get_merge_request |
MR list and details (with attachments) |
gitlab_get_merge_request_diff |
Per-file diff, truncated to a readable size |
gitlab_create_merge_request / gitlab_update_merge_request / gitlab_merge_merge_request |
MR lifecycle |
gitlab_merge_request_action |
approve, unapprove, rebase, cancel auto-merge, commits, participants, pipelines |
gitlab_create_merge_request_thread |
Comment on a specific line of a file under review |
gitlab_resolve_merge_request_thread |
Resolve or unresolve a thread |
Repository
| Tool | Description |
|---|---|
gitlab_list_repo_tree / gitlab_get_file |
File tree and contents (as_image for images) |
gitlab_create_or_update_file / gitlab_commit_files |
Write one file, or commit several operations at once |
gitlab_list_branches / gitlab_branch_action |
Branches: create, delete, prune merged |
gitlab_list_commits / gitlab_get_commit / gitlab_compare / gitlab_blame_file |
History, commit diff, ref comparison, blame |
gitlab_list_tags / gitlab_tag_action / gitlab_list_releases / gitlab_create_release |
Tags and releases |
gitlab_list_protected_branches |
Branch protection rules |
CI/CD
| Tool | Description |
|---|---|
gitlab_list_pipelines / gitlab_get_pipeline / gitlab_pipeline_action |
Pipelines: inspect, run, retry, cancel |
gitlab_list_jobs / gitlab_job_action |
Jobs: retry, cancel, play, erase |
gitlab_get_job_log |
Log of a failed job (tail by default) |
gitlab_get_job_artifact |
Job artifact; images come back as images |
gitlab_ci_variables |
CI/CD variables |
gitlab_lint_ci_config |
Validate .gitlab-ci.yml before committing it |
gitlab_list_environments / gitlab_list_deployments / gitlab_list_pipeline_schedules |
Environments, deployments, schedules |
Attachments
| Tool | Description |
|---|---|
gitlab_read_attachment |
Download an attachment with authentication; images arrive as images |
gitlab_list_attachments |
Every attachment of an issue/MR/epic in one list |
gitlab_upload_file |
Upload a local file, get the markdown snippet back |
gitlab_download_url |
Authenticated GET of any instance URL |
Everything else
| Tool | Description |
|---|---|
gitlab_wiki |
Wiki pages: read and edit |
gitlab_snippets |
Project snippets |
gitlab_project_hooks |
Webhooks |
gitlab_list_packages / gitlab_list_registry_repositories |
Package registry and container registry |
When no tool fits
gitlab_api_request calls any REST API v4
endpoint:
{
"method": "GET",
"path": "/groups/42/epics",
"query": { "state": "opened" },
"all_pages": true
}
Slashes inside identifiers are encoded as %2F: /projects/group%2Fproject/issues.
In read-only mode only GET passes; in locked mode only paths inside the locked project do.
Development
npm run build # compile
npm run dev # tsc --watch
npm run typecheck # types only
npm run start # stdio mode
npm run start:http # hosted mode on PORT (default 8080)
Layout:
src/
index.ts entry point, picks stdio or HTTP
server.ts builds a configured MCP server with every tool registered
http.ts hosted mode: landing page, stateless /mcp, per-request credentials
config.ts configuration from environment or headers
gitlab-client.ts HTTP client: pagination, errors, downloads, project lock
tools/
helpers.ts shared argument schemas, result formatting, guards
core.ts instance, search, todos, raw API
projects.ts projects and groups
issues.ts issues, comments, labels, milestones
merge-requests.ts merge requests, diffs, reviews
repository.ts files, branches, commits, tags, releases
ci.ts pipelines, jobs, artifacts, variables
attachments.ts attachments and screenshots
misc.ts wiki, snippets, hooks, registries
To add a tool, call defineTool in the appropriate module: error handling, the read-only guard
and the project lock are wired in automatically.
Troubleshooting
| Symptom | Cause |
|---|---|
401 Unauthorized |
The token expired, was revoked, or belongs to a different instance |
403 Forbidden |
Missing scope (read_api instead of api) or insufficient role in the project |
404 Not Found |
Wrong project id/path, or the token cannot see it. The path must be complete: group/subgroup/project |
| Attachment will not download | Check that the URL belongs to the same instance; for a private CA set GITLAB_TLS_REJECT_UNAUTHORIZED=false |
Attachment is N bytes, above… |
Raise GITLAB_MAX_ATTACHMENT_BYTES |
| Server will not start | Logs go to stderr with the [gitlab-mcp] prefix; usually GITLAB_TOKEN is missing |
A visual walkthrough with examples lives in landing.html at the repository root.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.