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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。