Repository navigation
Description
Description
I'm using a remote MCP server protected by OAuth 2.0 / OIDC. The initial authentication flow succeeds, and OpenCode stores both the access token and the refresh token.
However, when the short-lived access token expires, OpenCode does not appear to perform a refresh-token grant. Instead, the MCP transitions to "Requires Authentication" and all MCP calls fail until I authenticate again.
Environment
- OpenCode 1.17.11
- Remote MCP server
- OAuth 2.0 / OIDC
- Identity Provider: Keycloak
- Access token lifetime: 5 minutes
- Refresh token: long-lived
Expected behavior
When the access token expires, OpenCode should use the stored refresh token (grant_type=refresh_token) to obtain a new access token and continue communicating with the MCP server without requiring user interaction.
Actual behavior
After the access token expires:
- MCP requests fail.
opencode mcp listreports the server as Requires Authentication.- The stored refresh token does not appear to be used.
- Running
opencode mcp auth <server>restores functionality until the next access-token expiration.
opencode mcp debug output (redacted)
$ opencode mcp debug quartermaster
┌ MCP OAuth Debug
│
● Server: quartermaster
│
● URL: https://<redacted>/mcp
│
● Auth status: ✓ authenticated
│
● Access token: eyJhbGciOiJSUzI1NiIs...
│
● Expires: 2026-06-30T15:13:10.634Z
│
● Refresh token: present
│
◇ HTTP response: 401 Unauthorized
│
● WWW-Authenticate:
│ Bearer realm="Instructions MCP",
│ resource_metadata="https://<redacted>/.well-known/oauth-protected-resource"
│
▲ Server returned 401 Unauthorized
│
● Testing OAuth flow (without completing authorization)...
│
◆ Connection successful (already authenticated)
│
└ Debug complete
Additional information
The OAuth server is Keycloak, and refresh-token flow has been verified to work correctly with other OAuth clients. The authorization server supports grant_type=refresh_token, and OpenCode's debug output reports:
Refresh token: present
This suggests that the refresh token is successfully stored, but is either never used or the refresh flow fails before retrying the original request.
Plugins
No response
OpenCode version
1.17.11
Steps to reproduce
-
Configure a remote MCP server protected by OAuth/OIDC.
-
Authenticate using:
opencode mcp auth <server>
-
Confirm authentication succeeds.
-
Verify with:
opencode mcp debug <server>
that both an access token and refresh token are present.
-
Wait for the 5-minute access token to expire.
-
Invoke any MCP tool or run:
opencode mcp list
Screenshot and/or share link
No response
Operating System
Ubuntu 24.04
Terminal
tmux
Activity
github-actions commentedon Jun 30, 2026
This issue might be a duplicate of existing issues. Please check:
needs_authstateexhuma commentedon Jun 30, 2026
Ben-Home commentedon Jun 30, 2026
I've dealt with this exact pattern across dozens of MCP server integrations. The root cause is usually one of these three things:
1. The refresh is never triggered because the 401 isn't caught
Many MCP clients check the
expires_infrom the initial token response once, then never re-evaluate. The access token expires silently and the nexttools/callgets a 401 — but if the client doesn't have a retry-with-refresh interceptor, it just surfaces "Requires Authentication."2. The refresh token is stored but the grant call isn't wired
Storing the refresh token in the OAuth state is step 1. Step 2 is: before every
tools/call, check(expires_at - now) < 60sand proactively call the token endpoint withgrant_type=refresh_token&refresh_token=<stored>. The MCP Authorization header gets the new access token. The stored refresh token gets updated if the response includes a rotated one (Keycloak often rotates).3. The token endpoint discovery is one-shot
OAuth metadata (
/.well-known/oauth-authorization-server) is fetched once during initial auth. If the client doesn't cache the token endpoint URL, the refresh call goes to the wrong place or nowhere at all.Quick debugging check: before the next
tools/call, manually POST to your Keycloak token endpoint with the stored refresh token. If that returns 200 with a new access token, the problem is #1 or #2 (client-side wiring). If it returns 400invalid_grant, the refresh token was already consumed/revoked — you need a new auth flow.We handle this in our MCP server (36 business API connectors at corpusiq.io) with a pre-request interceptor that checks
expires_aton every call and refreshes proactively. The key design choice: refresh before expiry, never reactively on 401. Reactive refresh creates a failure-then-recovery cycle that looks like flakiness to the user. Proactive refresh makes it invisible.lukeyeager commentedon Jul 29, 2026
Reproduced on OpenCode 1.18.6 with Microsoft Entra ID. OpenCode reports an expired access token and a present refresh token, but does not refresh it. It instead enters the OAuth authorization test, which fails with AADSTS90009.
Evanthel commentedon Aug 12, 2026
I am seeing what appears to be the same refresh/persistence failure with the Rize remote MCP on OpenCode 1.18.16 on macOS.
Configuration
{ "mcp": { "rize": { "type": "remote", "url": "https://mcp.rize.io/mcp", "enabled": true } } }Observed behavior
opencode mcp auth rizecompletes successfully in the browser.opencode mcp listthen reports Rize as requiring authentication.This happened repeatedly across many separate days rather than as a one-off failure. Another remote MCP configured in the same OpenCode installation remains authenticated.
Redacted local evidence
The current redacted shape of
~/.local/share/opencode/mcp-auth.jsonis:No token values, client IDs, user identifiers, or account data are included here.
The Rize authorization-server metadata advertises both grants:
{ "grant_types_supported": ["authorization_code", "refresh_token"] }Current redacted debug result:
Historical OpenCode logs repeatedly show
status=needs_authfor Rize after prior successful reauthorization. I did not find a logged successful refresh-token grant. The public symptom is close to #34582, while repeated full authorization may also interact with the persisted-client problem described in #35625.Important diagnostic caveat
I inspected the redacted credential shape after running
opencode mcp debug rize. That command starts an OAuth test without completing authorization and may create or update pending OAuth state such ascodeVerifier. Therefore, the pendingcodeVerifieris not by itself evidence that tokens were deleted. The reproducible evidence is the repeated transition from successful MCP calls after authorization toNot authenticated/needs_authin later runs.Expected behavior
OpenCode should persist the OAuth credentials, use the advertised refresh-token grant when the access token expires, and reconnect without requiring a daily interactive authorization.
Questions
McpOAuthPendingProviderbe replacing persisted token state when auth/debug is started?the-dream-machine commentedon Aug 17, 2026
Run into this with TinyFish MCP (https://agent.tinyfish.ai/mcp, Clerk OAuth, single-use rotating refresh tokens) on opencode 0.0.0-next-17236.
After the access token expires, every session start reports needs_auth and forces manual re-auth. I verified the persisted refresh token is still valid. The token endpoint returns HTTP 200 with fresh tokens, yet opencode won't use it. Post-refresh, Clerk invalidates the old refresh token via
invalid_grant, so any stale attempt leaves the creds permanently broken until a full re-auth. A single successful refresh consumes the rotation before opencode can persist it, which matches #34074/#34523.rekram1-node commentedon Sep 22, 2026
Fixed on V2. Expired access tokens are refreshed with the stored refresh token before the connection is attempted, and the refreshed token pair is persisted back to the credential. Concurrent refreshes of the same token within a process share a single request so rotating refresh tokens don't invalidate each other. Covered by "refreshes tokens loaded from a persisted credential" and "shares concurrent refreshes for the same token" in
packages/core/test/mcp-oauth.test.ts.