OAuth Authentication
GitLab MCP Server supports OAuth 2.1 authentication for use as a Claude Custom Connector. This enables secure per-user authentication without sharing GitLab tokens.
When to Use OAuth
| Scenario | Recommended Mode |
|---|---|
| Personal/local use | Static Token (GITLAB_TOKEN) |
| Team access via Claude Web/Desktop | OAuth Mode |
| Private LAN GitLab with public MCP server | OAuth Mode |
| CI/CD or automated pipelines | Static Token |
Prerequisites
- GitLab 16.0+; the Device Flow needs GitLab 17.3+ (17.2 with the
oauth2_device_grant_flowfeature flag) - HTTPS endpoint for gitlab-mcp (required for OAuth)
- GitLab OAuth Application configured
Setup Guide
Step 1: Create GitLab OAuth Application
- In GitLab, navigate to User Settings > Applications (or Admin > Applications for instance-wide)
- Create a new application:
- Name:
GitLab MCP Server - Redirect URI:
https://your-mcp-server.com/oauth/callback - Confidential:
No(PKCE provides security without client secret) - Scopes: Select
apiandread_user
- Name:
- Save and copy the Application ID
TIP
The redirect URI is used by Claude.ai Custom Connectors (Authorization Code Flow). CLI clients use Device Flow which doesn't require redirect URI.
OAUTH_ISSUER
OAUTH_ISSUER is the public URL clients connect to (for example https://your-mcp-server.com). It is the issuer of every token and the base of the redirect URI above (<OAUTH_ISSUER>/oauth/callback). It is configuration, not derived from request headers.
The issuer may include a path when a reverse proxy serves the server under a prefix (for example https://example.com/gitlab) and strips that prefix before forwarding. Discovery documents of such an issuer live on the origin with the path after the well-known segment (/.well-known/oauth-authorization-server/gitlab, /.well-known/oauth-protected-resource/gitlab/mcp); route these URLs to the server too.
Step 2: Configure Server
# Required for OAuth mode
OAUTH_ENABLED=true
OAUTH_ISSUER=https://your-mcp-server.com # Public URL clients connect to
OAUTH_SESSION_SECRET=your-minimum-32-character-secret-key
OAUTH_CLIENT_ID=your-gitlab-application-id
GITLAB_API_URL=https://your-gitlab-instance.com
# Server configuration
PORT=3000
HOST=0.0.0.0
# Optional OAuth settings
OAUTH_CLIENT_SECRET=your-secret # Required only if GitLab app is confidential
OAUTH_SCOPES=api,read_user # Default scopes
OAUTH_TOKEN_TTL=3600 # Token lifetime (seconds)
OAUTH_REFRESH_TOKEN_TTL=604800 # Refresh token lifetime (seconds)
OAUTH_DEVICE_POLL_INTERVAL=5 # Minimum device flow poll interval (seconds)
OAUTH_DEVICE_TIMEOUT=300 # Maximum device flow lifetime (seconds)Step 3: Deploy with HTTPS
OAuth requires HTTPS. Example with Docker:
docker run -d \
--name gitlab-mcp \
-e OAUTH_ENABLED=true \
-e OAUTH_ISSUER=https://your-mcp-server.com \
-e OAUTH_SESSION_SECRET="$(openssl rand -base64 32)" \
-e OAUTH_CLIENT_ID=your-app-id \
-e GITLAB_API_URL=https://gitlab.example.com \
-e PORT=3000 \
-p 3000:3000 \
ghcr.io/structured-world/gitlab-mcp:latestUse a reverse proxy (nginx, Caddy, Traefik) to add HTTPS. See TLS/HTTPS Configuration.
Claude Web Setup
- Go to claude.ai and sign in
- Navigate to Customize > Connectors
- Click Add custom connector
- Enter your MCP endpoint:
https://mcp.example.com/mcp - Click Add
- When prompted, complete authentication:
- Follow the browser redirect to your GitLab instance
- Sign in and approve the authorization request
- Return to Claude when authorization completes
- The connector is now active
Claude Desktop Setup
For a remote OAuth server, add a custom connector using the same connector UI as Claude Web. These connections originate from Anthropic's cloud, including when configured in Claude Desktop, so the MCP endpoint must be publicly reachable. See Anthropic's remote connector instructions.
For local access, use a desktop extension or stdio configuration instead. See Claude Desktop integration. An HTTP url entry is not a local stdio configuration in claude_desktop_config.json.
Private LAN GitLab Architecture
For GitLab instances on private networks (not internet-accessible):
+-------------------+ +-------------------+ +-------------------+
| Claude Cloud | HTTPS | gitlab-mcp | HTTP | GitLab Server |
| or Desktop |-------->| (Public IP) |-------->| (Private LAN) |
+-------------------+ +-------------------+ +-------------------+
|
| Device code displayed
v
+-------------------+
| User (on VPN) |
| visits GitLab |
| enters code |
+-------------------+How it works:
- gitlab-mcp server has network access to GitLab (same network or VPN)
- User connects to gitlab-mcp via Claude (public internet)
- gitlab-mcp initiates device authorization with GitLab
- User receives a code and visits GitLab directly (requires VPN/internal access)
- User authenticates in GitLab and enters the code
- gitlab-mcp receives the token and issues an MCP session token
- All subsequent requests use the user's GitLab identity
Requirements:
- gitlab-mcp must reach GitLab API (deploy on same network or use VPN)
- Users must be able to access GitLab web UI (typically via VPN)
- gitlab-mcp must be accessible from internet (for Claude to connect)
OAuth vs Static Token
| Feature | Static Token | OAuth Mode |
|---|---|---|
| Setup complexity | Simple | Moderate |
| Per-user identity | No (shared token) | Yes |
| Token management | Manual | Automatic |
| Audit trail | Single identity | Per-user actions |
| Security | Token in config | No tokens in config |
| Best for | Personal use, CI/CD | Teams, shared access |
OAuth Flows
The server supports two OAuth flows automatically:
| Flow | Trigger | Used By | How It Works |
|---|---|---|---|
| Authorization Code | redirect_uri present | Claude.ai Custom Connectors | Redirects to GitLab OAuth, then back |
| Device Flow | No redirect_uri | CLI clients, Claude Desktop | Shows device code for manual entry |
The flow is selected automatically based on the presence of redirect_uri in the authorization request.
The Device Flow needs GitLab 17.3 or later (17.2 with the oauth2_device_grant_flow feature flag). On older instances a request without redirect_uri is refused with invalid_request; clients there use the Authorization Code flow.
Request rate limits
MCP requests are authenticated before the server selects their rate-limit identity. A valid OAuth session does not consume the anonymous IP quota, including during initialization and when its MCP transport session was lost after a restart. Authenticated requests are unlimited by default. With RATE_LIMIT_SESSION_ENABLED=true, each OAuth session has its own quota, shared by its transport connections. Separate OAuth sessions have separate quotas even when their users share one source IP.
Anonymous requests and rejected credentials share the source address's IP quota. Supplying a transport session ID does not prove OAuth authentication. Users behind the same NAT still share this anonymous quota: TRUST_PROXY can recover the address forwarded by a trusted proxy, but cannot distinguish users of one public IP. Trust only the actual proxy addresses or network, rather than arbitrary forwarded headers.
| Variable | Default | Effect |
|---|---|---|
RATE_LIMIT_IP_ENABLED | true | Enable the anonymous source-address quota |
RATE_LIMIT_IP_WINDOW_MS | 60000 | Anonymous quota window in milliseconds |
RATE_LIMIT_IP_MAX_REQUESTS | 100 | Requests per anonymous source address per window |
RATE_LIMIT_SESSION_ENABLED | false | Enable the optional authenticated-session quota |
RATE_LIMIT_SESSION_WINDOW_MS | 60000 | Authenticated quota window in milliseconds |
RATE_LIMIT_SESSION_MAX_REQUESTS | 300 | Requests per OAuth session per window |
Counters belong to each server process and reset on restart; they are not a shared quota across replicas. Exceeded quotas return 429 with Retry-After. These limits are separate from GitLab's API limits and the client-registration limit below. Disabling RATE_LIMIT_IP_ENABLED does not disable OAuth or change GitLab permissions.
The root endpoint serves the public dashboard for ordinary browser or JSON requests. Streaming GET requests (Accept: text/event-stream) and requests naming an MCP session go through the authenticated MCP transport instead. An expired transport session returns 404, so the client can initialize a new session.
Token binding
- Client and redirect: the Authorization Code flow requires a client registered through
/register, andredirect_urimust be one of its registered URIs; otherwise the request fails without a redirect. The Device Flow does not use a redirect and needs no registration. - Client authentication: public clients (
token_endpoint_auth_method: none) identify themselves withclient_idand PKCE. A client registered withclient_secret_basicorclient_secret_postmust present its secret at/tokenand/revoke, in theAuthorization: Basicheader or the form (never both); a wrong or missing secret gets401 invalid_client. Other methods are refused at registration. - Registrations: a registration that never completes an authorization expires after 24 hours; a client that obtained tokens keeps its registration. One source address may register at most 100 clients per hour: further requests get
429withRetry-After, and no existing registration is removed. Behind a reverse proxy setTRUST_PROXYso the address is the client's, not the proxy's; otherwise all clients share one limit. - Resource: clients may send
resource(RFC 8707) naming<OAUTH_ISSUER>or<OAUTH_ISSUER>/mcp; any other value fails withinvalid_target. Access tokens carry that resource asaud(<OAUTH_ISSUER>/mcpwhen none was requested) andOAUTH_ISSUERasiss; both are checked on every request. - Scope: supported scopes are
mcp:toolsandmcp:resources; unknown values are ignored and no recognised value grants both. Tool calls requiremcp:tools. A refresh may narrow the scope, never widen it. - Transports: every MCP transport requires the access token in OAuth mode, including the legacy SSE endpoints (
/sse,/messages). - Codes and refresh: an authorization code is consumed on first use, and both grants require the
client_idthe code or refresh token was issued to. - Issuer identification: every redirect back to the client carries
iss(RFC 9207).
Choosing a GitLab instance
Users sign in to an operator-configured instance only: the default GITLAB_API_URL with OAUTH_CLIENT_ID, plus every instance in GITLAB_INSTANCES / GITLAB_INSTANCES_FILE that has its own oauth application. When more than one is available, /authorize shows a page to choose one; a client may also pass instance=<instance URL>. A URL that is not one of the configured instances is rejected, so credentials are never sent to an arbitrary host.
The chosen instance and its application are used for the whole account: authorization, code exchange, user lookup and every token refresh. Each instance's GitLab application registers the same redirect URI, <OAUTH_ISSUER>/oauth/callback. Instances with a base path (for example https://git.example.com/gitlab) are supported. If an instance is removed from the configuration, its accounts must sign in again; they are never moved to another instance.
Disconnecting and recovery
- Disconnect: a client revokes its access or refresh token at
/revoke(RFC 7009,client_idrequired). The session ends on every replica and the linked GitLab token is revoked as well. - Denied consent: the client receives
error=access_denied(withstateandiss) and can start again. - Expired or revoked GitLab authorization: requests get
401 invalid_tokenand tool results carry a reconnect challenge. - Missing GitLab scope: a GitLab
403 insufficient_scopeproduces a tool result challenge witherror="insufficient_scope". - Instance removed from the configuration: its accounts must sign in again; they are never moved to another instance.
- Storage outage: requests answer
503, so clients retry instead of discarding valid credentials.
Access and refresh tokens, GitLab tokens and application secrets never appear in logs, tool results or diagnostics.
Rejected GitLab OAuth HTTP responses report only the operation, HTTP status and a recognized OAuth error code. Provider response bodies, descriptions and status text are omitted because a provider or proxy may echo credentials in them. An invalid_grant during refresh requires reconnecting; application errors, rate limits and service outages keep the account linked so the operation can be retried. Authorization callback errors likewise use only recognized codes and a local description, including when linking another account. Malformed JSON in an otherwise successful response reports only the operation and HTTP status, without parser messages that could quote the provider's response.
Account profile
The read-only get_profile tool takes no input and returns the account behind the connection: an opaque id, the GitLab display name and email when GitLab provides them, and a nickname of the form <username> @ <instance>. The id is derived from the instance and the GitLab user id, so it stays the same across token refresh and reconnects and differs for the same user id on different instances. The tool is marked with _meta["openai/profile"] so hosts can label each connection.
Reconnecting an account
In OAuth mode every tool descriptor declares securitySchemes: [{ "type": "oauth2", "scopes": ["mcp:tools"] }] (mirrored in _meta). When GitLab rejects the account's credentials (HTTP 401), the tool result is an error carrying _meta["mcp/www_authenticate"] with a Bearer challenge (error="invalid_token" and an error_description) that points at the protected resource metadata, so the client starts its own reconnect flow. HTTP 401 responses carry the same error parameters for rejected tokens.
Endpoints
When OAuth is enabled:
| Endpoint | Method | Description |
|---|---|---|
/.well-known/oauth-authorization-server | GET | OAuth metadata discovery |
/.well-known/oauth-protected-resource | GET | Protected resource metadata (RFC 9470) |
/.well-known/oauth-protected-resource/mcp | GET | Protected resource metadata of the /mcp endpoint (RFC 9728) |
/authorize | GET | Start authorization (auto-selects flow) |
/oauth/callback | GET | GitLab callback (Auth Code Flow only) |
/oauth/poll | GET | Poll for completion (Device Flow only) |
/token | POST | Exchange code for tokens |
/register | POST | Dynamic Client Registration (RFC 7591) |
/revoke | POST | Token revocation (RFC 7009): disconnects the account |
/health | GET | Health check |
Troubleshooting
"OAuth not configured" error
- Ensure
OAUTH_ENABLED=trueis set - Verify
OAUTH_SESSION_SECRETis at least 32 characters - Check
OAUTH_CLIENT_IDis correct
Device code not accepted
- Verify GitLab version is 17.3 or later (17.2 with the
oauth2_device_grant_flowfeature flag) - Check OAuth application scopes include
api - Ensure the application is not set as "Confidential"
"Failed to refresh token" error
- GitLab refresh token may have expired
- Re-authenticate through Claude connector settings
Cannot reach GitLab for authentication
- For private LAN GitLab, connect to VPN first
- Verify you can access GitLab web UI in your browser
