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.)
- Use a remote OAuth MCP server that satisfies the precondition above (advertises resource scopes), with a pre-registered OAuth client.
- 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>" }
}
- Run
opencode mcp logout my-mcp then opencode 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 with offline_access in 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= 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 scopes — offline_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 + expiresAt — no 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
Description
#28810 (merged) made config
scopework by adding it toMcpOAuthProvider.clientMetadata. But the MCP TypeScript SDK usesclientMetadata.scopeonly as a last-resort fallback — when neither theWWW-Authenticateheader 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.scopeentirely. So configscopestill has no effect for any properly-configured OAuth MCP server.This blocks
offline_accessspecifically (the exact scope requested in #28811's "Expected").offline_accessis a client↔authorization-server scope — it requests a refresh token — not a resource scope, so a resource server correctly does not advertise it. WithclientMetadata.scopeused only as a fallback, there's no way to addoffline_accesson top of the advertised resource scopes. Net effect: OpenCode never requestsoffline_access, the AS issues no refresh token, short-lived access tokens (e.g. ~15 min) expire, and the user must manuallyopencode mcp logout/autheach time. (PR #32135 — refresh expired tokens — can't help: it requires a refresh token to already exist.)offline_access, noopenid:…/oauth2/authorize?…&scope=<resource-scopes>&resource=https%3A%2F%2Fmcp.example.com%2Fmatching the server's
WWW-Authenticate: Bearer … scope="<resource-scopes>".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.mcp-auth.json) contain onlyaccessToken+expiresAt, never arefreshToken.References
Plugins
No response
OpenCode version
1.17.11
Steps to reproduce
Precondition (important)
The resource server must advertise its scopes — i.e. its
401WWW-Authenticate: Bearer …challenge (and/or/.well-known/oauth-protected-resource) returns a non-emptyscope/scopes_supported. (If it advertises none, #28810's fallback applies and the bug is hidden.)opencode.jsonwith anoauth.scopethat includesoffline_accesson top of the resource scopes:opencode mcp logout my-mcpthenopencode mcp auth my-mcp; complete the browser login.--print-logs --log-level DEBUG).~/.local/share/opencode/mcp-auth.json(macOS/Linux).authorization_code+ PKCE flow manually against the same AS / client / resource, but withoffline_accessin the requested scope, and inspect the token-endpoint response.Expected
scope=includesoffline_access(and the other configured scopes).tokenscontains arefreshToken.grant_type=refresh_token); the tool call succeeds without re-auth.Actual
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's
WWW-Authenticate: Bearer … scope="<resource-scopes>").tokenshas onlyaccessToken+expiresAt— norefreshToken.refresh_token→ the AS supports it; OpenCode simply never requestsoffline_accesswhen the server advertises scopes.401; the only recovery is to manuallyopencode mcp logout/authagain.Screenshot and/or share link
No response
Operating System
macOS
Terminal
Ghosty