Repository navigation
Description
Summary
For remote MCP servers, opencode ignores the resource_metadata URL announced in the WWW-Authenticate header of the 401 challenge and only looks for RFC 9728 protected-resource metadata at the domain root (https://<host>/.well-known/oauth-protected-resource). When the server publishes its metadata at a path-shaped URL (announced in the header), the root lookup 404s and opencode falls back to treating the resource host itself as the authorization server, building the authorize URL on the wrong host. The browser then opens a dead/error page and the whole flow fails or degrades.
This breaks a whole class of real-world servers, e.g. AWS Bedrock AgentCore runtime endpoints, whose MCP URL has the resource ARN in the path:
https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/<url-encoded-arn>/invocations?qualifier=<name>
Likely also relevant to opencode2 / V2 (unverified there, but the discovery logic decides everything downstream, and existing V2 OAuth issues like #40767 suggest the same code path).
Environment
- opencode 1.18.21, macOS (darwin/arm64)
- Server: AWS Bedrock AgentCore runtime (streamable HTTP MCP, JWT authorizer, IdP = JumpCloud/Ory Hydra, static PKCE client — the IdP has no dynamic client registration)
Reproduction / evidence
Unauthenticated POST to the MCP endpoint — the server correctly announces where its metadata lives:
$ curl -s -D - -X POST 'https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-east-1%3A<ACCOUNT_ID>%3Aruntime%2F<RUNTIME_ID>/invocations?qualifier=<NAME>' \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3A...%2F<RUNTIME_ID>/invocations/.well-known/oauth-protected-resource?qualifier=<NAME>"
That metadata URL resolves fine and names the real authorization server:
$ curl -s 'https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/.../invocations/.well-known/oauth-protected-resource?qualifier=<NAME>'
{"authorization_servers":["https://oauth.id.jumpcloud.com"],"resource":"https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/..."}
The domain-root lookup (the only one opencode appears to try) 404s:
$ curl -s 'https://bedrock-agentcore.us-east-1.amazonaws.com/.well-known/oauth-protected-resource'
Invalid api path (HTTP 404)
What opencode then does (opencode mcp auth <name>) — authorize URL built on the resource host, not the IdP:
https://bedrock-agentcore.us-east-1.amazonaws.com/authorize?response_type=code&client_id=<CLIENT_ID>&code_challenge=...&code_challenge_method=S256&redirect_uri=http%3A%2F%2F127.0.0.1%3A53712%2Fcallback&state=...
That path does not exist on the resource host ("Invalid api path"), so the user lands on an error page.
Expected behavior
Per RFC 9728 §5.1 (and the MCP authorization spec, which builds on it): when the 401 WWW-Authenticate header carries resource_metadata="<url>", the client should fetch that exact URL and use its authorization_servers entry for AS metadata discovery — not derive a well-known URL from the origin, and never assume the resource host is the AS.
Asks
- Bug fix: honor the
resource_metadataURL from theWWW-Authenticatechallenge (including path + query string forms) before/instead of the origin-root lookup. - Feature (escape hatch): an explicit override in the
oauthconfig block — e.g.authServerMetadataUrl— pointing directly at the AS metadata document, for servers whose discovery is broken or unusual. (Claude Code ships exactly this field, and it is the practical workaround there.) The current schema (clientId/clientSecret/scope/callbackPort/redirectUri) offers no way to steer discovery at all.
Notes
- The same server works in clients that follow the header (verified with mcp-remote and with Claude Code's native remote MCP support).
- No DCR on the IdP, so the automatic dynamic-registration fallback can't save the flow either — everything hinges on discovery landing on the right AS.
Activity
mhuggins7278 commentedon Aug 26, 2026
Additional evidence from OpenCode v2 beta (
0.0.0-beta-18230) using private company MCP servers:resource_metadataURL./authorizeand/tokenURLs, both of which return 404.initializeand returns tools, confirming the server and credentials are valid.HugeLetters commentedon Aug 29, 2026
I don't know much about MCP protocol but hit an issue with V2 requiring explicit scope in oauth config, while it wasn't needed in V1. My agent composed this description of the issue and it seems related to this too.
My opencode2 version is
v0.0.0-beta-18684LLM Text below
Adding another symptom of the same root cause, this time on V2 — the
scopefield fromWWW-Authenticateis also never read, not justresource_metadata.Setup
We have an MCP gateway that fronts multiple services under path-prefixed URLs (e.g.
https://mcp-gateway.example.com/service-a/mcp,https://mcp-gateway.example.com/service-b/mcp). Each service has its own OAuth scopes. A 401 from any of them returns:The gateway also serves a root PRM (
https://mcp-gateway.example.com/.well-known/oauth-protected-resource) that returns 200 — but withoutscopes_supported:The SDK's path-aware discovery (
/.well-known/oauth-protected-resource/service-a/mcp) also hits the root PRM (200, no scopes), so it accepts it and never tries the per-service URL. The scopes are only discoverable through theWWW-Authenticateheader — exactly the mechanism this issue is about.What breaks on V2
V2's
authorize(packages/core/src/mcp/oauth.ts) callsauth()directly without going through the transport:No
initialize→ no 401 → noWWW-Authenticateparsing. The SDK's scope resolution chain falls through entirely:The authorization URL to the IdP ends up with no
scopeparameter. The token is either rejected or issued without the needed permissions, and every tool call fails with 401/403.Why V1 works
V1's
startAuthcreates a realStreamableHTTPClientTransportand callsclient.connect(transport). The transport sendsinitialize, gets the 401, and the SDK'sstreamableHttp.tsextracts both fields:So on V1 the scopes are discovered automatically — no config
scopeneeded.Workaround
Adding
scopeto theoauthconfig works becauseprovider.clientMetadata.scopeis the last-resort fallback in the SDK. But it shouldn't be necessary — the server already advertises the scopes.Shared fix
The fix for this issue (honoring
resource_metadatafromWWW-Authenticate) would also fix thescopeproblem — both fields are extracted by the sameextractWWWAuthenticateParamscall. Probing the server before callingauth()and passing bothscopeandresourceMetadataUrlwould resolve both this issue and the scope-discovery problem in one change.mustafa89 commentedon Sep 6, 2026
@opencode-agent , can you please have a look.
rekram1-node commentedon Sep 15, 2026
Both asks are now in
v2:resource_metadataURL from the 401WWW-Authenticatechallenge (path and query included) landed in feat(core): rework MCP client for SDK v2 and the 2026-07-28 revision #48937.oauth.auth_server_metadata_urlon remote MCP servers. Point it at the authorization server's OAuth 2.0 or OpenID Connect metadata document and it is used for both login and token refresh instead of protected resource discovery.