feat: add MCP OAuth mode (GITLAB_MCP_OAUTH=true)#359
Open
titouanmathis wants to merge 7 commits intozereight:mainfrom
Open
feat: add MCP OAuth mode (GITLAB_MCP_OAUTH=true)#359titouanmathis wants to merge 7 commits intozereight:mainfrom
titouanmathis wants to merge 7 commits intozereight:mainfrom
Conversation
Adds server-side GitLab OAuth proxy support via the MCP spec OAuth flow. When GITLAB_MCP_OAUTH=true, Claude.ai (and any MCP-spec client) can authenticate users against any GitLab instance via browser-based OAuth with per-session token isolation — no manual PAT management required. How it works: - ProxyOAuthServerProvider (oauth-proxy.ts) delegates all OAuth operations (authorize, token exchange, refresh, revocation, DCR) to GitLab - GitLab's open Dynamic Client Registration (/oauth/register) means no pre-registered OAuth app is needed on the GitLab side - mcpAuthRouter mounts discovery + DCR endpoints on the MCP server - requireBearerAuth validates each /mcp request; token stored per session in authBySession for reuse by buildAuthHeaders() via AsyncLocalStorage - All existing auth modes (PAT, cookie, REMOTE_AUTHORIZATION, USE_OAUTH) are completely unchanged New files: - oauth-proxy.ts: createGitLabOAuthProvider() factory - test/mcp-oauth-tests.ts: unit + integration tests (9 tests, all passing) Changed files: - index.ts: imports, GITLAB_MCP_OAUTH/MCP_SERVER_URL constants, auth router mount, requireBearerAuth middleware on /mcp, validateConfiguration() extension, startup guard, session lifecycle (onclose, shutdown, DELETE) - test/utils/server-launcher.ts: skip GITLAB_TOKEN check for MCP OAuth mode - test/utils/mock-gitlab-server.ts: add addRootHandler() + rootRouter for OAuth endpoints mounted outside /api/v4 - package.json: add test:mcp-oauth script; include mcp-oauth-tests in test:mock - .env.example, README.md: document new env vars and setup New env vars: - GITLAB_MCP_OAUTH=true enable this mode - MCP_SERVER_URL public HTTPS URL of the MCP server (required) - MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL=true local HTTP dev only
The SDK's authorize handler calls clientsStore.getClient(clientId) to validate redirect_uri before calling provider.authorize(). With the original stub (redirect_uris: []), Claude.ai's redirect_uri was always rejected as 'Unregistered redirect_uri' before the proxy could forward the authorization request to GitLab. Fix: subclass ProxyOAuthServerProvider and override the clientsStore getter to wrap registerClient with an in-memory cache. After DCR, the full GitLab response (including redirect_uris) is cached per client_id. Subsequent getClient() calls return the cached entry with real redirect_uris, allowing the authorize handler to proceed. Added test: 'clientsStore caches DCR response so getClient returns real redirect_uris after registration' (10/10 tests passing).
The unbounded Map could grow without limit if POST /register is called repeatedly (e.g. by a misconfigured or abusive client). Each entry is ~500 bytes so 1000 entries = ~500 KB max — negligible for legitimate use (typically < 10 distinct MCP client apps), but capped against abuse. Introduced BoundedClientCache: a Map-backed LRU cache using JS insertion- order semantics. get() refreshes an entry to the tail; set() evicts the least-recently-used head when the cap is reached. O(1) for both ops, no external dependencies. Tests added: - LRU: most-recently-used client survives when oldest is evicted - cache: re-registration updates the stored entry (12/12 tests passing)
Append ' via <resourceName>' to the client_name forwarded to GitLab during DCR so the OAuth consent screen reads: [Unverified Dynamic Application] Claude via GitLab MCP Server is requesting access to your account instead of just 'Claude', giving users context about which server is requesting access on their behalf. The resourceName defaults to 'GitLab MCP Server' and is passed through createGitLabOAuthProvider(gitlabBaseUrl, resourceName). GitLabProxyOAuthServerProvider now takes resourceName as a second constructor argument so the clientsStore getter can reference it.
buildAuthHeaders() only checked REMOTE_AUTHORIZATION to read from AsyncLocalStorage, causing all GitLab API calls to be sent without auth headers when GITLAB_MCP_OAUTH was enabled instead. Also update the stored token on every request so that refreshed OAuth tokens are picked up instead of reusing the expired one. Co-authored-by: Claude <claude@anthropic.com>
Some MCP clients (e.g. Claude.ai) send an empty or insufficient scope (like ai_workflows) when initiating the OAuth flow. Without at least the 'api' scope, every GitLab API call returns 403 insufficient_scope. Override authorize() to inject the required scopes when the client does not request them, ensuring the resulting token can actually call the GitLab API. Co-authored-by: Claude <claude@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When deploying this MCP server for a team using a self-hosted GitLab instance, there is no way for multiple users to authenticate with their own GitLab accounts without each one manually generating a Personal Access Token and configuring it in their MCP client. This is friction-heavy, hard to manage at scale, and breaks when tokens expire.
This PR adds a
GITLAB_MCP_OAUTH=truemode that enables the MCP spec OAuth flow: users authenticate directly with GitLab through their MCP client's browser flow, tokens are managed per session on the server, and no pre-registered GitLab OAuth application or manual token management is required.How it works
/.well-known/oauth-authorization-serverPOST /register) — proxied to GitLab's open DCR endpoint; no pre-registration neededAuthorization: Bearer <token>on every MCP request/oauth/token/infoand stores it per sessionAll existing auth modes (
REMOTE_AUTHORIZATION,GITLAB_USE_OAUTH, PAT, cookie) are completely unchanged.Configuration
Claude.ai config — no
headersneeded:{ "mcpServers": { "GitLab": { "url": "https://your-mcp-server.example.com/mcp" } } }GITLAB_MCP_OAUTHtrueto enableMCP_SERVER_URLGITLAB_API_URLSTREAMABLE_HTTPtrue(SSE not supported)MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URLtruefor local HTTP dev only