fix(mcp): show "needs auth" instead of "failed" for OAuth servers#15986
fix(mcp): show "needs auth" instead of "failed" for OAuth servers#15986mattzcarey wants to merge 1 commit intoanomalyco:devfrom
Conversation
When connecting to an OAuth-protected MCP server for the first time, McpOAuthProvider.state() threw a generic Error because no OAuth state had been saved yet. This error bypassed the UnauthorizedError check in the connection handler, causing the UI to show "failed" instead of "Needs authentication (run: opencode mcp auth <server>)". Fix state() to generate and persist a new 32-byte random hex state (matching the existing pattern in startAuth) instead of throwing. This allows the MCP SDK's auth flow to complete normally, surface UnauthorizedError, and trigger the correct needs_auth status with a helpful toast notification.
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
|
The following comment was made by an LLM, it may be inaccurate: Based on my search, I found one potentially related PR: Related PR:
The other results (#11477, #11925) are older feature PRs for adding OAuth authentication UI and are less likely to be direct duplicates of this specific bug fix. |
|
Closing as duplicate of #15547 which addresses the same root cause (McpOAuthProvider.state() throwing instead of generating a state on first connect). |
Summary
McpOAuthProvider.state()throws a genericError("No OAuth state saved...")because no state has been saved yetUnauthorizedError, so the connection handler classifies the server as"failed"instead of"needs_auth"state()to generate and persist a new 32-byte random hex state (matching the existing pattern instartAuth()) instead of throwingRoot cause trace
StreamableHTTPClientTransport.send()gets 401 → callsauth(provider, ...)authInternal()callsprovider.state()to check for existing statestate()throws genericError→ propagates back to connection handlererror instanceof UnauthorizedErrorisfalse→ falls through tostatus: "failed"With this fix
state()generates a new random state → auth flow proceeds normally'REDIRECT'→ transport throwsUnauthorizedErrorUnauthorizedError→ setsstatus: "needs_auth"Test plan
state()generates and persists a 64-char hex string when no state existsstate()returns existing saved state when one existsneeds_authstatus (notfailed)