Skip to main content
Glama

MCP GitLab

MCP server for sandboxed agents that forwards every request to the official MCP server of a GitLab instance and adds the GitLab token on the server side.

GitLab ships its own MCP server at <instance>/api/v4/mcp, in the Free tier and in the Community Edition. An agent in a sandbox should use it without ever holding a GitLab token. mwaeckerlin/mcp-gitlab stands between the two: the sandbox talks to it exactly as it talks to mwaeckerlin/mcp-github and mwaeckerlin/mcp-gitea, and the server passes each request to GitLab with the token it reads from a Docker secret. The tools are the ones GitLab offers, so they follow the GitLab version without a release of this server.

The numbered feature list is in FEATURES.md, the tests in TESTS.md, the architecture in ARCHITECTURE.md.

Security Model

  1. The sandboxed client calls this server, at MCP_GITLAB_URL.

  2. The server, outside the sandbox, reads the GitLab token from the file GITLAB_TOKEN_FILE names and adds it as Authorization: Bearer to the request it passes to <GITLAB_URL>/api/v4/mcp.

  3. GitLab decides with that token what the call may do.

  • No GitLab token in the sandbox, in a tool argument or in the environment of the server.

  • Only the one address <GITLAB_URL>/api/v4/mcp is asked; the path and the query of a request never reach GitLab.

  • Only the headers of the MCP transport and the tool selection of GitLab pass; the Authorization header of the sandbox, which carries MCP_AUTH_TOKEN, cookies and every other header stay on the server.

  • The OAuth challenge of GitLab stays on the server, so the agent does not start a login of its own; a redirect of GitLab is not followed with the token.

  • No token appears in an error message of the server.

Related MCP server: GitLab MCP

Configuration

Server

Every variable is optional; an empty value counts as unset.

Variable

Default

Effect

GITLAB_URL

empty: nothing is forwarded, MCP requests are answered 503 gitlab_not_configured

address of the GitLab instance, such as https://gitlab.example.com, with its sub path where GitLab lives under one; the server forwards to <GITLAB_URL>/api/v4/mcp. An address without http:// or https://, or with a query, a fragment or credentials, stops the start

GITLAB_TOKEN_FILE

empty: no token, GitLab answers 401

file with the fine-grained personal access token of GitLab, such as the Docker secret /run/secrets/gitlab_token; the line breaks at its end fall away; a named file that does not exist, is a directory, cannot be read or is empty, or a line break or another control character inside the token, stops the start

MCP_AUTH_TOKEN_FILE

empty

file with the shared token that the sandbox presents, such as the Docker secret /run/secrets/mcp_auth_token

MCP_AUTH_TOKEN

empty: every request is accepted

the shared token itself, as in mwaeckerlin/mcp-github; set it or MCP_AUTH_TOKEN_FILE, both together stop the start

GITLAB_TIMEOUT

120

seconds the server waits for the answer of GitLab before it answers 502 gitlab_unreachable; 1 to 86400

MCP_GITLAB_HOST

0.0.0.0

address the server listens on

MCP_GITLAB_PORT

4000

port the server listens on; 1 to 65535

A GitLab with a certificate of a private authority needs that authority in NODE_EXTRA_CA_CERTS, the file variable of Node.js.

The GitLab token is read from a file only. A token in the environment shows in docker inspect and in the environment of every process of the container; a Docker secret keeps it in a file under /run/secrets.

Security Trade-offs

  • Without MCP_AUTH_TOKEN the server accepts every request and acts with the token of the server: every client that reaches port 4000 can do in GitLab what the owner of the token may do. Publish no port where the sandbox reaches the server over a Docker network, set BIND_ADDRESS=127.0.0.1 for the sample stack, or set MCP_AUTH_TOKEN_FILE.

  • An http:// address in GITLAB_URL sends the token unencrypted and belongs on a closed network only, like the test stack.

  • GITLAB_TIMEOUT bounds the wait for the first answer of GitLab; an event stream that has begun has no bound, because GitLab sends it at its own pace.

  • The token query parameter can land in the log of a proxy in front of the server; the Authorization: Bearer header does not.

Sandbox

Variable

Effect

MCP_GITLAB_URL

where the MCP client of the sandbox reaches the server, such as http://mcp-gitlab:4000

MCP_AUTH_TOKEN

the shared token, sent as Authorization: Bearer <token> or as ?token=<token>, where the server requires one

GitLab Token

GitLab accepts two kinds of token at its MCP server: an OAuth access token with the scope mcp, and a fine-grained personal access token. For a server without a person in front of it, the fine-grained token is the one that fits:

  1. Sign in to GitLab as the user the agent acts as; the agent can do exactly what this user may do.

  2. Avatar › Edit profile › Access › Personal access tokens › Generate token › Fine-grained token.

  3. Name and expiry date; under Add resource permissions the tab User, the resource MCP tool, the permission Execute.

  4. Store the token in a file, hand it to the server as a Docker secret, and point GITLAB_TOKEN_FILE at it.

A legacy personal access token is refused with 403, also with the scope api: GitLab CE 19.5.3 answers {"message":"403 Forbidden"}, its documentation names the error insufficient_scope and the same refusal for project and group access tokens. The administrator of a self-managed instance allows the MCP server with the instance setting «Allow access to the MCP server», which is on in a fresh GitLab CE 19.5.3; while it is off, GitLab answers 403 MCP server disabled for this instance.

Ports

The server listens on port 4000. docker-compose.yml publishes it on every address of the host, IPv4 and IPv6, as long as BIND_ADDRESS is unset or empty. BIND_ADDRESS limits it to one address of the host: 127.0.0.1 serves the local machine alone, an IPv6 address stands in brackets, such as [::1]. 0.0.0.0 publishes on every IPv4 address and on no IPv6 address. Where the sandbox reaches the server over a Docker network, publish no port at all.

Sample Stack

docker-compose.yml reads the token from the file gitlab_token beside it, or from the file GITLAB_TOKEN_SECRET names, and needs GITLAB_URL:

GITLAB_URL=https://gitlab.example.com npm start

Health

  • GET /healthz answers 200 while the process runs, without MCP_AUTH_TOKEN.

  • With GITLAB_URL and a token: { "ok": true, "status": "ready", "gitlabUrlConfigured": true, "gitlabTokenConfigured": true }.

  • Otherwise status is degraded, and message names the missing variable.

  • The image carries a HEALTHCHECK that asks /healthz and prints its answer; it runs /usr/bin/node directly, because the image has no shell. The container is healthy in both states, so a compose file or a Swarm stack needs no healthcheck of its own.

Usage through MCP

The sandbox connects with the streamable HTTP transport of MCP to MCP_GITLAB_URL. GitLab answers every POST with JSON and offers no event stream: GET returns its 405. tools/list returns the tools of the GitLab version, with their argument schemas. Three request headers of GitLab choose the tools: X-Gitlab-Enabled-Mcp-Server-Toolsets names toolsets, such as ci; X-Gitlab-Enabled-Mcp-Server-Tools names single tools; X-Gitlab-Mcp-Server-Tool-Name-Prefix puts a prefix before every tool name, such as gitlab_.

Example call, read a project:

{ "name": "get_project", "arguments": { "project_id": "mwaeckerlin/mcp-gitlab" } }

SKILL.md carries the instructions for the agent; the image ships it at /app/skills/mcp-gitlab/SKILL.md.

Troubleshooting

Answer

Cause

Action

401 unauthorized of mcp-gitlab

the sandbox sent no or a wrong MCP_AUTH_TOKEN

send Authorization: Bearer <MCP_AUTH_TOKEN>

401 of GitLab

no token on the server, or GitLab does not know it

set GITLAB_TOKEN_FILE to a valid token

403 {"message":"403 Forbidden"} of GitLab

legacy, project or group token

use a fine-grained token with «MCP tool: Execute»

403 MCP server disabled for this instance

the MCP server is switched off in the instance settings

the administrator allows «Allow access to the MCP server»

502 gitlab_unreachable

GitLab did not answer within GITLAB_TIMEOUT or not at all

check GITLAB_URL and the network; the message names the cause

503 gitlab_not_configured

GITLAB_URL not set

set GITLAB_URL

start refused: GITLAB_TOKEN_FILE names <path>, which cannot be read: …

the file does not exist, is a directory, or the server's user may not read it

mount the secret at that path, or correct GITLAB_TOKEN_FILE

start refused: GITLAB_TOKEN_FILE names <path>, which is empty

the secret holds no token

recreate the secret with the token

start refused: GITLAB_TOKEN_FILE names <path>, which holds a line break inside the token, MCP_AUTH_TOKEN holds a control character inside the token

the token has a line break or another control character inside, often two tokens or a text pasted into the secret; no such token can stand in an HTTP header

recreate the secret, or set the variable, with the token alone

start refused: MCP_AUTH_TOKEN and MCP_AUTH_TOKEN_FILE are both set

both ways of giving the shared token are configured

remove one of them

Development

npm install
npm run build:ts
npm run build
npm test

npm run dev starts the server from the sources. npm test runs the groups of TESTS.md in one regression run: the feature register check, the compose contract, the type check and the unit tests, the dependency audit, the image contract and the end-to-end suite; a failed group does not stop the others, an interrupted run resumes where it was cut off, and npm test -- --from <ID> --to <ID> runs a range. The end-to-end suite (tests/e2e.sh, test/docker-compose.yml) starts a real GitLab CE from gitlab/gitlab-ce, in which test/gitlab/setup.rb creates a user, a private project, a fine-grained token and a legacy token; four servers with different tokens and a test client run beside it on internal networks, and the stack is removed with its volumes afterwards. npm run test:e2e:up starts and sets up that GitLab once and leaves it standing, so that every following npm run test:e2e and npm test builds and starts only the servers and the test client against it; npm run test:e2e:down removes it. GitLab CE 19.5.3 needed about four minutes until its /-/readiness answered and held 2.8GiB of memory; the health check of its image reports healthy before Rails can start, so the stack waits for /-/readiness, and the root password of the stack contains no common words, because GitLab refuses such a password and its database setup then fails. The image is built, tested and published for linux/amd64 and linux/arm64 by the reusable workflow of mwaeckerlin/scratch; npm run deploy pushes it.

Related MCP Connectors

Related MCP Servers