Skip to main content
Glama

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"

Or write the configuration yourself. The same object goes into .mcp.json in a repository, ~/.claude.json, claude_desktop_config.json or .cursor/mcp.json — fill in the blanks:

{
  "mcpServers": {
    "gitlab": {
      "type": "http",
      "url": "https://gitlab-mcp.webapace.ink/mcp",
      "headers": {
        "X-GitLab-Url": "",
        "X-GitLab-Token": "",
        "X-GitLab-Project-Id": ""
      }
    }
  }
}

Header

Fill in with

If left empty

X-GitLab-Url

the address as typed in the browser, https://gitlab.example.com

https://gitlab.com is assumed

X-GitLab-Token

the glpat-… access token

the request is refused — this one is required

X-GitLab-Project-Id

default project: team/backend (group included) or a numeric id

tools ask for a project each time

X-GitLab-Project-Lock and X-GitLab-Read-Only can be added the same way; see Hosted mode for the full header list.

Keep the file out of version control, or write "X-GitLab-Token": "${GITLAB_TOKEN}" — Claude Code and Cursor substitute environment variables, so the secret stays in your shell.

Two things worth knowing when a change appears to do nothing:

  • In Claude Code an entry added to the local scope (kept in ~/.claude.json) takes precedence over the project's .mcp.json. claude mcp list shows what is actually in use, and claude mcp remove <name> -s local drops a stale one.

  • Client configuration is read at startup, so restart the app after editing the file.

The rest of this document is for running your own copy in either mode.


Related MCP server: gitlab-mcp

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:

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

GET /robots.txt, /sitemap.xml

generated per request from the Host header, so a self-hosted copy advertises its own address; /mcp and /health are excluded from crawling

GET /favicon.ico, /favicon.svg, /apple-touch-icon.png, /icon-192.png, /icon-512.png, /og-image.png, /site.webmanifest

static files from assets/ (or ASSETS_PATH), cached for a week

GET /index.html redirects to / so the page has a single canonical address.

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 /

ASSETS_PATH

./assets

directory holding the icons, the manifest and the preview image

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.

Landing page assets

assets/ holds everything the page references: favicon.svg (the source of every raster icon), favicon.ico, the touch and PWA icons, site.webmanifest, and og-image.png — the 1200×630 preview used by link unfurlers, rendered from assets/og-card.html.

The PNG and ICO files are committed, so a normal build needs nothing extra. Regenerate them only after editing favicon.svg or og-card.html:

npm run assets      # headless Chrome does the rasterising; set CHROME=… if it is not found

The page carries a description, canonical URL, Open Graph and Twitter cards, and JSON-LD (SoftwareApplication, WebSite, FAQPage). Those absolute URLs point at gitlab-mcp.webapace.ink; a self-hosted copy that should be indexed under its own name needs them replaced in landing.htmlrobots.txt and sitemap.xml already follow the request host on their own.

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.

Install Server
A
license - permissive license
C
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • A MCP server built for developers enabling Git based project management with project and personal…

  • GitLab Public MCP — wraps the GitLab REST API v4 (public endpoints, no auth)

  • The MCP server for Azure DevOps, bringing the power of Azure DevOps directly to your agents.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/amalychev/gitlab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server