GitLab MCP

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.

Category
Visit Server

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_id other than the configured one is refused — the id, the full path and the URL-encoded path are all compared, so 42, group/project and group%2Fproject count as the same project;
  • group-wide and instance-wide operations (listing groups, group members, group search) are refused;
  • gitlab_api_request only accepts paths under /projects/<your project>/ plus the harmless /user, /version, /markdown, /todos;
  • gitlab_download_url and gitlab_read_attachment only fetch URLs that belong to the project;
  • gitlab_list_projects returns exactly one project and an unscoped gitlab_search searches 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_file with as_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

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured