Skip to content

Remote MCP OAuth: access token is not refreshed despite refresh token being present #34582

Closed

Description

@exhuma

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 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.

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

  1. Configure a remote MCP server protected by OAuth/OIDC.

  2. Authenticate using:

    opencode mcp auth <server>
  3. Confirm authentication succeeds.

  4. Verify with:

    opencode mcp debug <server>

    that both an access token and refresh token are present.

  5. Wait for the 5-minute access token to expire.

  6. Invoke any MCP tool or run:

    opencode mcp list

Screenshot and/or share link

No response

Operating System

Ubuntu 24.04

Terminal

tmux

Activity

  1. github-actions commented on Jun 30, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. exhuma commented on Jun 30, 2026

    @exhuma
    Author
  3. Ben-Home commented on Jun 30, 2026

    @Ben-Home

    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.

  4. lukeyeager commented on Jul 29, 2026

    @lukeyeager

    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.

  5. Evanthel commented on Aug 12, 2026

    @Evanthel

    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

    1. opencode mcp auth rize completes successfully in the browser.
    2. Rize MCP tool calls work immediately afterward, including in the same collector session.
    3. On a later OpenCode run, usually the next day, the first identity call fails with:
    AUTHENTICATION_ERROR: Not authenticated
    
    1. opencode mcp list then reports Rize as requiring authentication.
    2. 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:

    other remote MCP:
      tokens: accessToken, refreshToken, expiresAt, scope
      clientInfo: present
    
    rize:
      tokens: absent
      clientInfo: present
      codeVerifier: present
      serverUrl: present
    

    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:

    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?
  6. the-dream-machine commented on Aug 17, 2026

    @the-dream-machine

    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.

  7. rekram1-node commented on Sep 22, 2026

    @rekram1-node
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions