MCP OAuth: no scope param sent in authorize request -> Cognito issues an unexchangeable code (invalid_grant), even after working around #67258's DCR issue

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 0 comments · opened Aug 27, 2026

MCP OAuth: no scope param sent in authorize request → Cognito issues an unexchangeable code (invalid_grant), even after working around #67258's DCR issue

Summary

Filed as a related report to #67258 (DCR fallback) and #12077 (scope omitted even when the protected resource advertises one) — this adds a fully isolated, byte-for-byte proof of the mechanism, plus one new data point: the authorization server's own discovery metadata often has usable scopes even when the protected resource's metadata doesn't, and Claude Code doesn't fall back to it.

Environment: Claude Code 2.1.246, native Windows (PowerShell + Git Bash), Windows 11 Pro. MCP server: an AWS Bedrock AgentCore Gateway with a CUSTOM_JWT authorizer backed by Amazon Cognito.

What's wrong

  1. Adding this Gateway with claude mcp add --transport http fails immediately with Incompatible auth server: does not support dynamic client registration (#67258 — Cognito has no RFC 7591 DCR endpoint).
  2. Working around that with --client-id <pre-registered-client-id> --callback-port <port> gets past DCR — the browser reaches Cognito's real hosted-UI login, "succeeds" there — but the flow then fails with invalid_grant at the token-exchange step. ~/.claude/.credentials.json shows the same empty-token signature reported elsewhere in this bug family: "accessToken": "", "expiresAt": 0.

Root cause, proven (not inferred)

Debug log (--debug-file) shows Claude Code never determines a scope to request at all:

MCP server "<name>": Fetched OAuth metadata with scope: NONE
...
MCP server "<name>": Authorization URL: https://<cognito-domain>/oauth2/authorize?response_type=code&client_id=<id>&code_challenge=...&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fcallback&state=...&resource=<gateway-mcp-url>
MCP server "<name>": Scopes in URL: NOT FOUND
MCP server "<name>": No scopes available from URL or metadata

No scope parameter anywhere in the authorize URL. Captured via an intercepting proxy, the Gateway's own .well-known/oauth-protected-resource response is:

{
    "authorization_servers": ["https://cognito-idp.<region>.amazonaws.com/<pool-id>"],
    "resource": "<gateway-mcp-url>",
    "scopes_supported": []
}

Empty. So on its own terms Claude Code has nothing to put in scope=. But — critically — the identity provider's own discovery document does have usable scopes, which Claude Code never consults:

$ curl -s https://cognito-idp.<region>.amazonaws.com/<pool-id>/.well-known/openid-configuration | jq .scopes_supported
["openid", "email", "phone", "profile"]

The subsequent token exchange (captured via proxy, same request Claude Code sent, reproduced twice — an apparent internal retry, both attempts byte-identical and both immediately rejected):

POST https://<cognito-domain>/oauth2/token
grant_type=authorization_code
code=<redacted>
code_verifier=<redacted>
redirect_uri=http://localhost:<port>/callback
resource=<gateway-mcp-url>
client_id=<client-id>

← 400 {"error":"invalid_grant"}

Isolated proof this is specifically the missing scope, not resource or anything else: I replayed this exact flow manually, entirely outside Claude Code, three ways, using a fresh authorization code each time (single-use codes, so each needed its own real browser login):

| Variant | Result |
|---|---|
| Same request Claude Code sends (no scope, has resource) | 400 invalid_grant |
| Same request with resource stripped (proxy-rewritten in flight), no scope added | 400 invalid_grant — rules out resource as the cause |
| Same request with scope=openid email profile added at the authorize step (nothing else changed) | 200 OK, real access_token/id_token/refresh_token returned |

Adding scope at authorize time — and nowhere else — is the only variable that flips this from failure to success.

What should happen

When the protected resource's scopes_supported is empty or absent, Claude Code's MCP OAuth client should fall back to requesting at least openid (or whatever the authorization server's own discovery document lists, if consulted) rather than sending no scope parameter at all. This is the same underlying defect #12077 reports (scope omitted from the authorize URL) — that report shows it happening even when the protected resource does advertise a non-empty scopes_supported, which suggests the client isn't wiring discovered scopes into the authorize request at all, regardless of source. This report adds: (a) a controlled, isolated proof of exactly which parameter fixes it, and (b) the authorization-server-metadata fallback as a concrete, always-available second source when the protected resource gives nothing.

Related

  • #67258 — DCR fallback with a pre-configured client id (this Gateway hits that too; had to work around it first to even reach the scope bug above).
  • #12077 — closed/locked; same missing-scope symptom, different metadata shape.
  • #35846 / #17274 — an earlier, unrelated theory for this project's own auth failures (Cognito not advertising code_challenge_methods_supported); directly disproven by the debug log above, since PKCE (code_challenge/S256) is sent correctly. Noted here only so this doesn't get merged with that theory by mistake.

Steps to reproduce

  1. Stand up (or use) an MCP server behind an OAuth-protected resource whose .well-known/oauth-protected-resource has an empty scopes_supported, backed by an authorization server that requires an explicit scope at /authorize to issue an exchangeable code (Cognito does).
  2. claude mcp add --transport http --client-id <id> --callback-port <port> <name> <url>
  3. claude --debug-file <path> mcp login <name>
  4. Complete the browser login (succeeds).
  5. Observe invalid_grant on completion; debug log shows Scopes in URL: NOT FOUND / No scopes available from URL or metadata.

View original on GitHub ↗