You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Remote MCP OAuth: access token is not refreshed despite refresh token being present #34582
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 list reports 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.
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.
Not a duplicate of MCP OAuth: opencode mcp auth doesn't refresh token in active sessions #21702: that issue is unrelated to refresh-tokens. It discusses a failure mode where authentication stops working after calling "opencode mcp auth" a second time (new token is not used and opencode keeps using the old one).
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_in from the initial token response once, then never re-evaluate. The access token expires silently and the next tools/call gets 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) < 60s and proactively call the token endpoint with grant_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 400 invalid_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_at on 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.
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.
opencode mcp auth rize completes successfully in the browser.
Rize MCP tool calls work immediately afterward, including in the same collector session.
On a later OpenCode run, usually the next day, the first identity call fails with:
AUTHENTICATION_ERROR: Not authenticated
opencode mcp list then reports Rize as requiring authentication.
Repeating the full OAuth flow restores access, but the failure recurs.
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.json is:
Server: rize
Auth status: not authenticated
HTTP response: 401 Unauthorized
OAuth required: yes
Historical OpenCode logs repeatedly show status=needs_auth for 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 as codeVerifier. Therefore, the pending codeVerifier is not by itself evidence that tokens were deleted. The reproducible evidence is the repeated transition from successful MCP calls after authorization to Not authenticated / needs_auth in 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
Does OpenCode 1.18.16 still have a known path where a failed refresh causes persisted tokens to be removed or ignored?
Could McpOAuthPendingProvider be replacing persisted token state when auth/debug is started?
Is there any additional redacted logging I can collect to distinguish a server-side refresh-token rejection from a client-side persistence/refresh problem?
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.
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.
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.