Repository navigation
MCP OAuth: config scope is ignored when the resource server advertises scopes offline_access can't be requested #34034
Description
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, noopenid:
…/oauth2/authorize?…&scope=<resource-scopes>&resource=https%3A%2F%2Fmcp.example.com%2F
matching the server'sWWW-Authenticate: Bearer … scope="<resource-scopes>". - A direct
authorization_code+ PKCE flow against the same AS / client / resource withoffline_accessadded returns arefresh_token— the AS supports it; OpenCode just doesn't ask when the server advertises scopes. - Stored creds (
mcp-auth.json) contain onlyaccessToken+expiresAt, never arefreshToken.
References
- fix(mcp): scope config field has no effect on OAuth authorization request #28811, MCP OAuth scope from config is ignored for pre-registered remote servers #28895 (closed — config scope ignored), fixed by fix(mcp): include scope in clientMetadata and add callbackPort option #28810 (fallback-only)
- fix(mcp): refresh expired oauth tokens #32135 (open — refresh expired tokens; depends on a refresh token existing)
- MCP OAuth: opencode mcp auth doesn't refresh token in active sessions #21702 (open — active session keeps stale token after re-auth)
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.)
- Use a remote OAuth MCP server that satisfies the precondition above (advertises resource scopes), with a pre-registered OAuth client.
- Configure it in
opencode.jsonwith anoauth.scopethat includesoffline_accesson 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>" } }
- Run
opencode mcp logout my-mcpthenopencode mcp auth my-mcp; complete the browser login. - Inspect the authorization URL OpenCode opened (browser address bar before login, or run auth with
--print-logs --log-level DEBUG). - Inspect the stored credentials at
~/.local/share/opencode/mcp-auth.json(macOS/Linux). - (Optional — isolates the bug to the client) Run the same
authorization_code+ PKCE flow manually against the same AS / client / resource, but withoffline_accessin the requested scope, and inspect the token-endpoint response. - 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=includesoffline_access(and the other configured scopes). - Step 5:
tokenscontains arefreshToken. - 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 scopes —offline_accessandopenidfrom config are absent, e.g.:
…/oauth2/authorize?…&scope=<resource-scopes>&resource=https%3A%2F%2Fmcp.example.com%2F
(matches the server'sWWW-Authenticate: Bearer … scope="<resource-scopes>"). - Step 5:
tokenshas onlyaccessToken+expiresAt— norefreshToken. - Step 6: the token response does contain a
refresh_token→ the AS supports it; OpenCode simply never requestsoffline_accesswhen the server advertises scopes. - Step 7: the tool call fails with
401; the only recovery is to manuallyopencode mcp logout/authagain.
Screenshot and/or share link
No response
Operating System
macOS
Terminal
Ghosty
Activity