Skip to content

MCP OAuth: config scope is ignored when the resource server advertises scopes offline_access can't be requested #34034

Description

@mroffmix

Description

#28810 (merged) made config scope work by adding it to McpOAuthProvider.clientMetadata. But the MCP TypeScript SDK uses clientMetadata.scope only as a last-resort fallback — when neither the WWW-Authenticate header nor Protected Resource Metadata advertise any scopes (the AWS Bedrock AgentCore case in #28810).

Remaining gap

When a resource server does advertise its scopes (the common, spec-correct case per RFC 9728 / RFC 6750), the SDK uses those and ignores clientMetadata.scope entirely. So config scope still has no effect for any properly-configured OAuth MCP server.

This blocks offline_access specifically (the exact scope requested in #28811's "Expected"). offline_access is a client↔authorization-server scope — it requests a refresh token — not a resource scope, so a resource server correctly does not advertise it. With clientMetadata.scope used only as a fallback, there's no way to add offline_access on top of the advertised resource scopes. Net effect: OpenCode never requests offline_access, the AS issues no refresh token, short-lived access tokens (e.g. ~15 min) expire, and the user must manually opencode mcp logout/auth each time. (PR #32135 — refresh expired tokens — can't help: it requires a refresh token to already exist.)

  • Config:
    "my-mcp": {
      "type": "remote",
      "url": "https://mcp.example.com/mcp",
      "oauth": { "clientId": "my-mcp", "scope": "openid offline_access <resource-scopes>" }
    }
  • The authorization URL OpenCode generates contains only the server's advertised resource scopes — no offline_access, no openid:
    …/oauth2/authorize?…&scope=<resource-scopes>&resource=https%3A%2F%2Fmcp.example.com%2F
    matching the server's WWW-Authenticate: Bearer … scope="<resource-scopes>".
  • A direct authorization_code + PKCE flow against the same AS / client / resource with offline_access added returns a refresh_token — the AS supports it; OpenCode just doesn't ask when the server advertises scopes.
  • Stored creds (mcp-auth.json) contain only accessToken + expiresAt, never a refreshToken.

References

Plugins

No response

OpenCode version

1.17.11

Steps to reproduce

Precondition (important)

The resource server must advertise its scopes — i.e. its 401 WWW-Authenticate: Bearer … challenge (and/or /.well-known/oauth-protected-resource) returns a non-empty scope / scopes_supported. (If it advertises none, #28810's fallback applies and the bug is hidden.)

  1. Use a remote OAuth MCP server that satisfies the precondition above (advertises resource scopes), with a pre-registered OAuth client.
  2. Configure it in opencode.json with an oauth.scope that includes offline_access on top of the resource scopes:
    "my-mcp": {
      "type": "remote",
      "url": "https://mcp.example.com/mcp",
      "oauth": { "clientId": "my-mcp", "scope": "openid offline_access <resource-scopes>" }
    }
  3. Run opencode mcp logout my-mcp then opencode mcp auth my-mcp; complete the browser login.
  4. Inspect the authorization URL OpenCode opened (browser address bar before login, or run auth with --print-logs --log-level DEBUG).
  5. Inspect the stored credentials at ~/.local/share/opencode/mcp-auth.json (macOS/Linux).
  6. (Optional — isolates the bug to the client) Run the same authorization_code + PKCE flow manually against the same AS / client / resource, but with offline_access in the requested scope, and inspect the token-endpoint response.
  7. Wait for the access token to expire (e.g. ~15 min) and make any MCP tool call.

Expected

  • Step 4: the authorization URL's scope= includes offline_access (and the other configured scopes).
  • Step 5: tokens contains a refreshToken.
  • Step 7: OpenCode silently refreshes the access token (grant_type=refresh_token); the tool call succeeds without re-auth.

Actual

  • Step 4: scope= contains only the server-advertised resource scopesoffline_access and openid from config are absent, e.g.:
    …/oauth2/authorize?…&scope=<resource-scopes>&resource=https%3A%2F%2Fmcp.example.com%2F
    (matches the server's WWW-Authenticate: Bearer … scope="<resource-scopes>").
  • Step 5: tokens has only accessToken + expiresAtno refreshToken.
  • Step 6: the token response does contain a refresh_token → the AS supports it; OpenCode simply never requests offline_access when the server advertises scopes.
  • Step 7: the tool call fails with 401; the only recovery is to manually opencode mcp logout/auth again.

Screenshot and/or share link

No response

Operating System

macOS

Terminal

Ghosty

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions