Authentication Guide

July 11, 2026 · View on GitHub

gitlab-mcp supports multiple authentication methods. Token behavior depends on whether remote authorization is enabled. REMOTE_AUTHORIZATION=true and GITLAB_MCP_OAUTH=true are alternative per-request HTTP authentication modes and cannot be enabled together.

Token Resolution

Default Mode (REMOTE_AUTHORIZATION=false)

Static PAT (GITLAB_PERSONAL_ACCESS_TOKEN)
  └─> CI job token (GITLAB_JOB_TOKEN)
      └─> OAuth 2.0 PKCE
          └─> External token script
              └─> Token file

Remote Authorization Mode (REMOTE_AUTHORIZATION=true, HTTP)

Per-request auth only (required)

In remote authorization mode, each request must include Authorization: Bearer <token> or Private-Token: <token>, or Job-Token: <token>. If ENABLE_DYNAMIC_API_URL=true, each request must also include X-GitLab-API-URL.

Cookie-based auth (GITLAB_AUTH_COOKIE_PATH) is applied independently through a cookie jar and is not part of the token chain.


1. Personal Access Token (PAT)

The simplest method. Create a token at GitLab > Settings > Access Tokens with the api scope.

GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx

This token is the default request token in stdio and HTTP modes when REMOTE_AUTHORIZATION=false. When REMOTE_AUTHORIZATION=true, request headers are required and PAT fallback is disabled.


2. CI Job Token

For GitLab CI contexts, set GITLAB_JOB_TOKEN when no personal access token is configured. Requests use GitLab's JOB-TOKEN header.

GITLAB_JOB_TOKEN="$CI_JOB_TOKEN"

If both GITLAB_PERSONAL_ACCESS_TOKEN and GITLAB_JOB_TOKEN are set, the personal access token takes precedence.


3. OAuth 2.0 PKCE

Browser-based OAuth flow for interactive use. The server launches a local callback server and opens the browser for authorization.

Setup

  1. Register an OAuth application in GitLab (Settings > Applications or admin area):

    • Redirect URI: http://127.0.0.1:8765/callback
    • Scopes: api
    • Confidential: No (for PKCE public clients)
    • Note the Application ID
  2. Configure environment variables:

GITLAB_USE_OAUTH=true
GITLAB_OAUTH_CLIENT_ID=your-application-id
GITLAB_OAUTH_REDIRECT_URI=http://127.0.0.1:8765/callback
GITLAB_OAUTH_SCOPES=api

If GITLAB_OAUTH_SCOPES is omitted, gitlab-mcp defaults to api, or read_api when the effective GITLAB_PERMISSION_MODE is readonly. The legacy GITLAB_READ_ONLY_MODE=true switch also forces this readonly behavior.

Optional Settings

# For confidential applications
GITLAB_OAUTH_CLIENT_SECRET=your-client-secret

# Custom GitLab URL (derived from GITLAB_API_URL if not set)
GITLAB_OAUTH_GITLAB_URL=https://gitlab.example.com

# Token storage location (default: ~/.gitlab-mcp-oauth-token.json)
GITLAB_OAUTH_TOKEN_PATH=~/.gitlab-mcp-oauth-token.json

# Disable auto-opening the browser
GITLAB_OAUTH_AUTO_OPEN_BROWSER=false

OAuth Group Access Control

To restrict OAuth users by GitLab membership, configure group full paths:

GITLAB_OAUTH_ALLOWED_GROUPS=my-org,my-org-security

Membership is checked with the user's OAuth token against GET /api/v4/groups using at least Guest access. Matching is case-insensitive and a configured parent path includes subgroups. The same fail-closed authorizer protects local PKCE OAuth tokens and HTTP MCP OAuth bearer tokens, including initial token issuance and refresh. Successful and rejected decisions are cached by token digest for 60 seconds by default; the cache is bounded by GITLAB_OAUTH_GROUP_CACHE_MAX_ENTRIES. The OAuth scope must permit the Groups API (api or read_api); API errors, malformed pagination, and insufficient scope deny access. In MCP OAuth mode, configuring this allowlist also disables the direct Private-Token and Job-Token bypass because job tokens cannot prove a user-group membership.

How It Works

  1. On first request, the server checks for a stored token at GITLAB_OAUTH_TOKEN_PATH
  2. If no valid token exists, it generates a PKCE challenge and opens the browser
  3. The user authorizes the application in GitLab
  4. GitLab redirects back to the local callback server with an authorization code
  5. The server exchanges the code for an access token (with PKCE verifier)
  6. The token is persisted to disk (chmod 600) for future sessions
  7. On subsequent requests, the stored token is reused until it expires
  8. Expired tokens are automatically refreshed using the refresh token

Notes

  • The callback server listens for up to 3 minutes before timing out
  • Token files are stored with 0600 permissions
  • If refresh fails, the server falls back to interactive authorization

4. External Token Script

Execute a shell command to obtain a token dynamically. Useful for integration with secret managers, vault systems, or custom token providers.

GITLAB_TOKEN_SCRIPT=/path/to/get-token.sh
GITLAB_TOKEN_SCRIPT_TIMEOUT_MS=10000   # 500ms–120s (default: 10s)
GITLAB_TOKEN_CACHE_SECONDS=300         # 0–86400s (default: 5min)

Script Output Format

The script must output one of:

  1. Raw token string (plain text on stdout):

    glpat-xxxxxxxxxxxxxxxxxxxx
    
  2. JSON object with any of these keys:

    { "access_token": "glpat-xxxxxxxxxxxxxxxxxxxx" }
    
    { "token": "glpat-xxxxxxxxxxxxxxxxxxxx" }
    
    { "private_token": "glpat-xxxxxxxxxxxxxxxxxxxx" }
    

Example Script

#!/usr/bin/env bash
set -euo pipefail

# Example: read from environment or secret manager
if [[ -n "${GITLAB_OAUTH_ACCESS_TOKEN:-}" ]]; then
  printf '{"access_token":"%s"}\n' "${GITLAB_OAUTH_ACCESS_TOKEN}"
  exit 0
fi

echo "Token not available" >&2
exit 1

The resolved token is cached for GITLAB_TOKEN_CACHE_SECONDS to avoid repeated script executions.


5. Token File

Read a token from a file on disk. The file should contain a raw token string or JSON (same format as the token script output).

GITLAB_TOKEN_FILE=~/.gitlab-token

Security

By default, the server enforces strict file permissions — the token file must be readable only by the owner (chmod 600). If the file has group or other permissions, the server rejects it.

To override this check:

GITLAB_ALLOW_INSECURE_TOKEN_FILE=true

The token is cached for GITLAB_TOKEN_CACHE_SECONDS (default: 300s).


Use browser cookies from a Netscape-format cookie file. This is useful when working with GitLab instances that use SSO or other browser-based authentication.

GITLAB_AUTH_COOKIE_PATH=~/.gitlab-cookies.txt

How It Works

  1. The server reads cookies from the file in Netscape cookie format
  2. A cookie jar is created and attached to all API requests via fetch-cookie
  3. Before the first API call to each GitLab instance, a warmup request is sent to establish the session
  4. If the cookie file changes on disk, it is automatically reloaded

Warmup Path

The warmup request hits a lightweight endpoint to establish the session:

GITLAB_COOKIE_WARMUP_PATH=/user   # default

Standard Netscape cookie format (tab-separated):

# Netscape HTTP Cookie File
.gitlab.example.com	TRUE	/	TRUE	0	_gitlab_session	abc123...

Lines starting with #HttpOnly_ are parsed as HttpOnly cookies.


7. Remote Authorization (HTTP Mode)

In HTTP transport mode, this enables strict per-request credentials. This is the recommended approach for shared/multi-user deployments.

REMOTE_AUTHORIZATION=true

REMOTE_AUTHORIZATION=true enforces per-request credentials. If a request does not include a token header, the request is rejected.

Before creating or updating an MCP session, the server validates the credential against the selected canonical GitLab API (GET /user; job tokens fall back to GET /job). Both successful and rejected checks are cached for 30 seconds by default using a SHA-256 token digest as the cache key. Validation fails closed on timeout or upstream errors.

Client Headers

The server accepts tokens via:

  • Authorization: Bearer <token> — Standard bearer token
  • Private-Token: <token> — GitLab private token header
  • Job-Token: <token> — GitLab CI job token header

Dynamic API URL

When serving multiple GitLab instances, enable dynamic API URL per request:

REMOTE_AUTHORIZATION=true
ENABLE_DYNAMIC_API_URL=true
GITLAB_ALLOWED_HOSTS=other-gitlab.example.com

Clients can then send:

X-GitLab-API-URL: https://other-gitlab.example.com/api/v4

The header host and port must match GITLAB_API_URL or GITLAB_ALLOWED_HOSTS. It acts only as a selector: the server forwards requests to the registered canonical API base, not to an arbitrary scheme or path supplied by the client.

Authentication Flow (HTTP Mode)

  1. Client sends a request with auth headers
  2. The server extracts the token and required API URL (when dynamic API URLs are enabled) from headers
  3. Auth context is stored in AsyncLocalStorage for the duration of the request
  4. All GitLab API calls within that request use the per-session credentials
  5. Requests missing required headers are rejected before tool execution

MCP OAuth Discovery

For clients that support MCP OAuth, enable GitLab-backed discovery/proxy endpoints:

GITLAB_MCP_OAUTH=true
MCP_SERVER_URL=https://mcp.example.com
GITLAB_OAUTH_APP_ID=<pre-registered-gitlab-application-id>
GITLAB_MCP_OAUTH_STATE_SECRET=<base64-encoded-32-byte-secret>

Public MCP OAuth issuers must use HTTPS. Plain HTTP is accepted only for loopback development URLs using localhost or 127.0.0.1. The MCP SDK does not accept [::1] as a plain HTTP issuer; an IPv6-bound local server should advertise a localhost issuer URL. MCP_HTTP_AUTH_TOKEN cannot be enabled with MCP OAuth because both authenticate through Authorization: Bearer; use one of these modes.

Create a normal GitLab OAuth application before starting the server. Register the single fixed callback <MCP_SERVER_URL without trailing slash>/callback (for a prefixed issuer, for example https://mcp.example.com/gitlab-mcp/callback) and permit the configured GITLAB_OAUTH_SCOPES. Set GITLAB_OAUTH_APP_SECRET only for a confidential application. The proxy intentionally does not call GitLab /oauth/register: GitLab dynamically registered applications cannot obtain the api or read_api scope needed by this server.

The HTTP server exposes OAuth metadata plus local authorize/token/register/revoke and fixed callback endpoints. DCR returns an encrypted virtual client ID. The proxy uses its pre-registered GitLab app and a separate PKCE challenge, then binds the resulting proxy code to the virtual client, exact redirect URI, and client PKCE verifier. The encrypted proxy code contains the still-single-use GitLab authorization code, so GitLab enforces replay rejection when /token performs the exchange. Returned refresh tokens are encrypted and bound to the same virtual client.

/mcp accepts only tokens whose GitLab token-info response identifies the configured application and contains every configured scope. Private-Token and Job-Token headers remain supported as validated direct bypass headers only when GITLAB_OAUTH_ALLOWED_GROUPS is empty.

The direct Private-Token / Job-Token OAuth bypass is allowed only after the same upstream GitLab validation succeeds; malformed, expired, or cross-instance credentials receive HTTP 401 before MCP request handling.

GITLAB_MCP_OAUTH_STATE_SECRET is required even for one replica. It makes DCR and in-flight OAuth operations survive process restarts and allows any replica with the same key to handle the next step. Multi-replica rotation must use two rollouts: first set old=current, new=previous on every replica; then set new=current, old=previous on every replica. This overlap lets old and new pods read each other's values during the second rollout. Wait a full virtual-client TTL after that rollout completes before removing the old key. All replicas must use identical steady-state key settings. No shared writable OAuth store is used.

Stateless HTTP Mode

Set OAUTH_STATELESS_MODE=true for multi-replica HTTP deployments where MCP session affinity is not available. The server creates a fresh Streamable HTTP transport for each request and does not store MCP sessions in memory. In REMOTE_AUTHORIZATION or GITLAB_MCP_OAUTH mode, clients must send the auth header on every request. MCP OAuth DCR/callback state is independently stateless whenever MCP OAuth is enabled; the shared state secret is mandatory, so the server never silently falls back to a per-process OAuth client cache.


Cloudflare Bypass

If your GitLab instance is behind Cloudflare, enable browser-like headers:

GITLAB_CLOUDFLARE_BYPASS=true

This adds:

  • A Chrome-like User-Agent header
  • Accept-Language: en-US,en;q=0.9
  • Cache-Control: no-cache
  • Pragma: no-cache

You can also set a custom User-Agent:

GITLAB_USER_AGENT="MyApp/1.0"

TLS & Proxy Configuration

Custom CA Certificate

For self-signed or internal CA certificates:

GITLAB_CA_CERT_PATH=/path/to/ca-bundle.pem

HTTP Proxy

HTTP_PROXY=http://proxy.example.com:8080
HTTPS_PROXY=http://proxy.example.com:8080

To disable TLS certificate verification, you must explicitly acknowledge the risk:

NODE_TLS_REJECT_UNAUTHORIZED=0
GITLAB_ALLOW_INSECURE_TLS=true

Both settings are required — setting only NODE_TLS_REJECT_UNAUTHORIZED=0 without the acknowledgment flag will cause a startup error.