Repository navigation
Description
Description
On OpenCode v2.2.0, a remote OAuth MCP can stay failed for hours because reconnect sends an expired access token, the server answers HTTP 429 (Retry later) instead of 401, and OpenCode only refreshes after 401. No operation=refresh runs even though a valid refresh token is stored in the V2 credential table.
Follow-up requested by the closer of #49773 (closed as not planned / misclassified as v1). This is a v2 report.
Expected: when the stored access token is expired/near expiry and a refresh token exists, OpenCode refreshes before (or without depending on) the first MCP POST, then connects with a fresh bearer. A connect-time 429 should back off (Retry-After if present) instead of retrying every ~hour with the same expired bearer.
Actual:
- Token lifetime ~2h (
expiresIn=7200). Refresh token remains valid. - After expiry, reconnect POSTs
/api/v4/mcpwith the expired Bearer. - When the server returns 401:
operation=refresh200 →mcp connectedwith tools. - When the server returns 429 with body
Retry later:mcp connect failed/Error POSTing to endpoint: Retry later (HTTP 429)and no refresh attempt. - ~Hourly reconnects repeat the same expired Bearer while 429 persists. The same 429 was also seen on
POST /oauth/token(exchange) and OAuth discovery GETs duringopencode mcp auth.
Notes:
- Related: MCP: refresh OAuth tokens proactively before expiry #49773 (closed), Remote MCP OAuth: access token is not refreshed despite refresh token being present #34582 (“fixed on V2” proactive refresh before connect — still reactive 401 path on 2.2.0 for this server).
- Distinct from servers that return HTTP 200 + tool-level “token expired” (Homey/LaunchDarkly in MCP: refresh OAuth tokens proactively before expiry #49773); here the transport returns 429, which also skips refresh.
- Happy to attach redacted log excerpts (401→refresh vs. immediate 429, no refresh).
MCP: remote GitLab instance MCP, type: remote, URL https://<host>/api/v4/mcp. OAuth: static client in config (clientId, redirectUri, scope=mcp); credentials in ~/.local/share/opencode/opencode.db credential table (not v1 mcp-auth.json).
Plugins
unrelated local hooks only (litellm-session-id, mistral-cache-key, strip-reasoning-content, caveman-*, rtk-rewrite)
OpenCode version
2.2.0 (@opencode/cli)
Steps to reproduce
- Configure a remote OAuth MCP (GitLab instance MCP is one example) and complete
opencode mcp auth <server>. - Wait until the access token expires (~2h) while keeping the refresh token.
- Reconnect (
opencode mcp listor open a session). - If the MCP POST returns 429 (
Retry later) instead of 401, note that the log shows nomcp oauth request … operation=refresh. - When the same setup later gets 401 on that first POST, refresh runs and the server connects.
Operating System
Linux (WSL2)
Terminal
bash
Activity
opencodejr commentedon Oct 29, 2026
Thanks for the detailed report, @sk-connectpoint. I can reproduce this on 2.2.0, and it still happens on the latest
dev(87a35fa).The script below runs a local mock OAuth MCP server. Its access tokens last 3 seconds, and once a token has expired it answers with HTTP 429
Retry laterinstead of 401. The script then:opencode mcp auth gl. The first connection works.opencode mcp list.Result:
✗ gl failed: Error POSTing to endpoint: Retry later (HTTP 429). OpenCode sendsinitializethree times with the expired bearer, and the token endpoint never gets arefresh_tokenrequest, even though the stored credential has an expiry time and a refresh token. When the mock answers the expired bearer with 401 instead, OpenCode refreshes right away and connects.So on this path, refresh only runs after a 401. Nothing refreshes an expired token before the first request. I did not test the
Retry-Afterback-off part of your report; that overlaps with #52854 (PR #52855). Related: #49773 and #34582 (same missing refresh before connecting), and PR #53275 (a 429 during the refresh itself).Reproduction script (repro.sh)
Run it:
bash repro.shVaili commentedon Oct 31, 2026
I reproduced the missing refresh on
devand I'd like to take it. I'll start with a test for an expired stored MCP credential, then refresh before the first request instead of waiting for a 401. Retry-After back-off stays with #52855.