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_metadata URL from the WWW-Authenticate challenge (including path + query string forms) before/instead of the origin-root lookup.
- Feature (escape hatch): an explicit override in the
oauth config 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.
Summary
For remote MCP servers, opencode ignores the
resource_metadataURL announced in theWWW-Authenticateheader 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:
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
Reproduction / evidence
Unauthenticated POST to the MCP endpoint — the server correctly announces where its metadata lives:
That metadata URL resolves fine and names the real authorization server:
The domain-root lookup (the only one opencode appears to try) 404s:
What opencode then does (
opencode mcp auth <name>) — authorize URL built on the resource host, not the IdP: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-Authenticateheader carriesresource_metadata="<url>", the client should fetch that exact URL and use itsauthorization_serversentry for AS metadata discovery — not derive a well-known URL from the origin, and never assume the resource host is the AS.Asks
resource_metadataURL from theWWW-Authenticatechallenge (including path + query string forms) before/instead of the origin-root lookup.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