[BUG] Remote MCP OAuth requests every scope from oauth-authorization-server.scopes_supported, causing invalid_scope on GitLab self-hosted
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When Claude Code connects to a remote MCP server via the native HTTP transport, the OAuth 2.1 + PKCE authorize request includes every scope advertised in the authorization server's scopes_supported rather than only the scopes required by the protected MCP resource. On GitLab self-hosted instances this list contains admin-only and dynamic-template scopes that a normal user cannot be granted, so the entire OAuth flow aborts with invalid_scope before the MCP handshake even begins.
The community mcp-remote stdio↔HTTP bridge authenticates against the same endpoint, same user, same OAuth server without issue. This isolates the defect to Claude Code's native HTTP transport OAuth scope-selection logic.
What Should Happen?
Per RFC 9728 (OAuth 2.0 Protected Resource Metadata) and the MCP authorization specification, the client should request only the scopes required by the protected resource, discovered via either:
- The WWW-Authenticate: Bearer ... scope="..." header on the MCP endpoint's 401 response, or
- scopes_supported in https://<gitlab-host>/.well-known/oauth-protected-resource[/api/v4/mcp]
For GitLab MCP this resolves to mcp (optionally augmented with openid read_user for identity).
scopes_supported from the authorization server metadata represents what the AS can issue across all clients — not what the resource requires. Treating the two as equivalent is the root cause here.
Confirming the bug is client-side, not server-side
Replacing the native HTTP registration with the community mcp-remote stdio bridge — pointing at the same URL, same OAuth server, same user account — succeeds:
claude mcp remove GitLab
claude mcp add GitLab -- npx -y mcp-remote https://<gitlab-host>/api/v4/mcp
mcp-remote requests scope=mcp only, and GitLab issues the token. Claude Desktop also relies on mcp-remote for this same endpoint and works for the same reason. This rules out:
- GitLab not supporting HTTP transport for MCP (it does — mcp-remote speaks HTTP to it)
- GitLab not supporting OAuth (it does — full RFC-compliant metadata)
- GitLab not issuing the mcp scope (it does — to mcp-remote)
The OAuth failure happens before the MCP transport handshake, so transport-level differences cannot explain it. The defect is in Claude Code's OAuth scope-selection step.
Error Messages/Logs
Steps to Reproduce
- Register the GitLab MCP endpoint with the native HTTP transport: claude mcp add --transport http GitLab https://<gitlab-host>/api/v4/mcp
- Run /mcp in Claude Code and trigger GitLab authentication.
- The browser opens an authorize URL (captured below).
- GitLab responds with: The requested scope is invalid, unknown, or malformed.
Actual authorize URL produced by Claude Code
https://<gitlab-host>/oauth/authorize
?response_type=code
&client_id=<dcr-client-id>
&code_challenge=<...>
&code_challenge_method=S256
&redirect_uri=http%3A%2F%2Flocalhost%3A59251%2Fcallback
&state=<...>
&scope=api+read_api+read_user+create_runner+manage_runner+k8s_proxy
+self_rotate+mcp+read_repository+write_repository
+read_virtual_registry+write_virtual_registry
+read_observability+write_observability+ai_features
+sudo+admin_mode+read_service_ping
+openid+profile+email+ai_workflows+user%3A*
The 23-element scope= parameter is byte-identical to scopes_supported from https://<gitlab-host>/.well-known/oauth-authorization-server:
[
"api","read_api","read_user","create_runner","manage_runner","k8s_proxy",
"self_rotate","mcp","read_repository","write_repository",
"read_virtual_registry","write_virtual_registry",
"read_observability","write_observability","ai_features",
"sudo","admin_mode","read_service_ping",
"openid","profile","email","ai_workflows","user:*"
]
Scopes that cause the failure:
- sudo, admin_mode — admin-only, not grantable to regular users
- user:* — dynamic scope template (user:<id>), not a literal value
- Any one of these triggers invalid_scope and aborts the entire flow
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.126 (Claude Code)
Platform
Other
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Environment
- Claude Code version: 2.1.126 (Claude Code)
- OS: macOS 15 (Darwin 25.4.0)
- MCP server: GitLab Community Edition 18.7.0, self-hosted
- MCP endpoint: https://<gitlab-host>/api/v4/mcp
- Transport tested: --transport http (native, fails) vs. mcp-remote stdio bridge (works)
Workaround
Two options that work today:
- Manual scope override — keep the original client_id, code_challenge, state, redirect_uri, replace scope=... with scope=mcp (or scope=mcp+openid+read_user), open in a browser tab while Claude Code is still listening on the callback port. Token exchange completes normally.
- Use mcp-remote instead of native HTTP (recommended):
claude mcp add GitLab -- npx -y mcp-remote https://<gitlab-host>/api/v4/mcp
Suggested fix
When initiating OAuth for a remote MCP server, derive the requested scope set in this order:
- The scope parameter from WWW-Authenticate: Bearer on the MCP endpoint's 401 response.
- scopes_supported from oauth-protected-resource metadata (RFC 9728).
- A minimal default of mcp — never the full scopes_supported from the authorization server metadata.
Impact / scope of the bug
This will affect any OAuth server whose oauth-authorization-server.scopes_supported advertises scopes the user cannot be granted — extremely common in enterprise IdPs (GitLab, Keycloak, Okta with custom scopes, etc.). The reproduction here is GitLab self-hosted, but the fix benefits all native-HTTP MCP integrations.
References
- RFC 9728 — OAuth 2.0 Protected Resource Metadata
- MCP Authorization specification — https://modelcontextprotocol.io/specification/draft/basic/authorization
- GitLab MCP server documentation — https://docs.gitlab.com/user/gitlab_duo/model_context_protocol/mcp_server/
- mcp-remote (working reference implementation) — https://www.npmjs.com/package/mcp-remote
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Same here on our end
Confirming this also reproduces on gitlab.com SaaS (not just self-hosted) via the
gitlab@claude-plugins-officialplugin, which registers an HTTP-transport MCP athttps://gitlab.com/api/v4/mcp.gitlab@claude-plugins-official(Anthropic marketplace), installed 2026-05-26.mcp.json:{"gitlab": {"type": "http", "url": "https://gitlab.com/api/v4/mcp"}}The authorize URL produced by
mcp__plugin_gitlab_gitlab__authenticaterequests 25 scopes — a superset of the 23 listed in the original report. Decodedscope=:New compared to the OP:
mcp_orbit,read_registry,write_registry. Still includessudo,admin_mode, and theuser:*dynamic-template scope.On gitlab.com,
sudo/admin_modeare silently dropped for non-admin users so the flow doesn't fail withinvalid_scope— but it still issues a token withapi(full read+write to all repos/MRs/snippets/packages/runners) plus every other broad scope advertised. There's no in-flow way for the user to narrow the grant; it's accept-all or deny.This is a meaningful security regression vs. the github-pat / mcp-remote pattern where scopes are chosen explicitly. The fix proposed in the OP (prefer
WWW-Authenticatescope →oauth-protected-resource.scopes_supported→ minimalmcpdefault) would fix both the self-hostedinvalid_scopefailure and the SaaS over-scoping issue with the same code change.Suggest removing the
stalelabel — this is still a current, security-relevant defect.hitting the same error on gitlab.com (SaaS), not just self-hosted.
Claude Code: 2.1.165
OS: macOS Darwin 25.5.0