Skip to content

Remote MCP (OAuth): expired access token forces interactive re-auth instead of silent refresh_token grant #4203

Open

Description

@ulugbekna

Summary

When a remote OAuth-protected MCP server's cached access token is expired at session start, the Copilot CLI treats the server as needing a fresh interactive OAuth login and drops its tools — even though a valid refresh token is cached. It never attempts an RFC 6749 §6 refresh_token grant. In non-interactive/automation sessions the interactive flow can't complete, so the server's tools silently never load.

Environment

  • GitHub Copilot CLI 1.0.71, running inside VS Code
  • macOS (Darwin); credential store = Apple Keychain (copilot-mcp-oauth)
  • MCP transport: rmcp streamable HTTP (rmcp::transport::worker / ::streamable), reqwest 0.13.4

Configuration

  • Remote HTTP MCP server in ~/.copilot/mcp-config.json:
    "notion": { "type": "http", "url": "https://mcp.notion.com/mcp" }
  • Cached grant in Keychain (service copilot-mcp-oauth, account <hash>) and ~/.copilot/mcp-oauth-config/<hash>.tokens.json — contains accessToken, expiresAt, refreshToken.
  • Server authorization-server metadata (RFC 8414) advertises refresh support:
    token_endpoint: https://mcp.notion.com/token, grant_types_supported: ["authorization_code", "refresh_token"]

Steps to reproduce

  1. Authenticate a remote OAuth MCP server (e.g. Notion) so a valid access+refresh grant is cached.
  2. Let the access token expire while the refresh token stays valid (Notion access tokens last ~8h — e.g. leave it overnight).
  3. Start a new Copilot CLI session (especially a non-interactive/scheduled one).
  4. Observe: the server's tools are absent from the session.

Observed behavior

From ~/.copilot/logs/process-<...>.log (redacted):

[ERROR] Starting remote MCP client for notion with url: https://mcp.notion.com/mcp
[ERROR] Connecting MCP client for notion...
        -> connects to mcp.notion.com:443 using the cached (expired) access token
[ERROR] [rust:rmcp::transport::worker] worker quit with fatal: Transport channel closed,
        when Client(OAuthChallenge { www_authenticate_header:
        "...OAuth, resource_metadata=\".../.well-known/oauth-protected-resource/mcp\",
        error=\"invalid_token\", error_description=\"Mis...\"" })
[ERROR] Server notion requires authentication, initiating OAuth flow
[ERROR] OAuth authentication required for notion

The runtime then reads the cached OAuth entry from Keychain but still ends at "OAuth authentication required."

Evidence that no refresh was attempted

Grep of the entire session-start process log:

POSTs to mcp.notion.com/token : 0

Zero refresh_token grants were sent, despite a valid refresh token being cached.

Expected behavior

On a 401 / WWW-Authenticate: … error="invalid_token" for a server that has a cached refresh token:

  1. Silently POST grant_type=refresh_token (+ client_id) to the discovered token_endpoint.
  2. Persist the rotated access/refresh tokens.
  3. Reconnect and load the server's tools.
  4. Only fall back to the interactive OAuth flow if the refresh itself fails (invalid_grant, expired/revoked).

Impact

  • Any remote OAuth MCP server silently loses its tools once the access token expires, until a manual re-auth — even though the refresh token is valid and the whole point of storing it is to avoid exactly this.
  • Breaks unattended/scheduled sessions (e.g. a daily automation), where the interactive prompt can never be completed.

Likely root cause / where to look

  • The rmcp streamable transport surfaces the OAuth challenge as a fatal transport error ("worker quit with fatal … OAuthChallenge"), and the CLI's handler maps "auth required" directly to the interactive flow without first attempting a token refresh.
  • Secondary: the session's tool list appears to be snapshotted at startup, so even a later successful (re)auth doesn't surface the tools into an already-running session.

Suggested fix

  • Insert a refresh step in the auth-challenge path: if a refresh_token is cached for the server, perform the refresh grant against the discovered token_endpoint, persist rotated tokens, and reconnect before considering interactive auth.
  • Optionally refresh proactively when expiresAt is at/near expiry before the first connect.
  • Consider re-syncing tools into active sessions after a late auth success.

Workaround

  • Manually perform the refresh_token grant against the token_endpoint, write the rotated tokens back into the cache; or re-run the CLI's interactive auth for the server before starting the session.

cc @connor4312 (VS Code team)

Filed with assistance from GitHub Copilot CLI.

Activity

  1. added
    area:authenticationLogin, OAuth, device auth, token management, and keychain integration
    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry
    and removed on Jul 26, 2026
  2. pinpikokun commented on Sep 2, 2026

    @pinpikokun

    I'm seeing the exact same behavior, but with the Microsoft 365 MCP servers (mail, Teams, calendar) rather than Notion, so this doesn't look like it's specific to a single MCP server implementation — it looks like a bug in Copilot CLI's OAuth handling itself.

    Setup

    • 3 remote MCP servers configured in ~/.copilot/mcp-config.json (m365-mail, m365-teams, m365-calendar), each OAuth-protected

    • Signed in interactively once (~11:04), all 3 servers connected successfully

    Observed

    All 3 servers independently transitioned to "Sign in required" within 70–90 minutes of the initial sign-in, well before I'd expect a session to need re-auth:

    Server Token expired at Time since sign-in
    Calendar 12:10 ~66 min
    Mail 12:24 ~80 min
    Teams 12:33 ~89 min

    Confirmed the refresh token is cached but unused

    Checked the token cache files under ~/.copilot/mcp-oauth-config/ directly — each one still contains a refreshToken field alongside expiresAt. The expiresAt timestamp passes with no attempt to refresh; the server just flips to needs-auth. The process log logs something like:

    Plain text

    OAuth required for <server> with no cached tokens; marking as needs-auth
    

    which is misleading — the token file clearly has a refreshToken in it at that point, so it's not actually "no cached tokens," it's "cached refresh token not used."

    Ruled out client ID / app registration as the cause

    To make sure this wasn't a client-ID-specific permission issue, I reconfigured auth.oauthClientId in mcp-config.json to use VS Code's own client ID (aebc6443-996d-45c2-90f0-388ff96faa56) and signed in again. Same result — all 3 servers expired at the sa

  3. princejonn commented on Oct 20, 2026

    @princejonn

    Same behaviour with the Atlassian Rovo MCP server ( https://mcp.atlassian.com/v1/mcp/authv2 ), Copilot CLI 1.0.100, macOS, Keychain store.

    • The access token is an 8 h JWT ( exp - iat = 28800 ).  offline_access  is granted and a  refreshToken  is stored.
    • Day 1: interactive login at 09:47Z. The token expired at 17:47Z. At 18:39Z the log shows  sse stream error: MCP OAuth challenge required (HTTP 401), max retry times reached . No refresh was attempted.
    • Day 2 at startup, within one second:
      07:16:25.768Z worker quit ... OAuthChallenge ... error="invalid_token"
      07:16:25.970Z Refreshing authentication for atlassian-mcp-server...
      07:16:26.470Z Successfully authenticated with atlassian-mcp-server
      07:17:03.799Z worker quit ... OAuthChallenge ... error="invalid_token"
      It reports success but the next request gets a 401 again. An interactive browser login follows.
    • Other clients (Codex, ChatGPT) on the same Atlassian account stay authenticated for weeks, so the Atlassian refresh tokens themselves are valid.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:authenticationLogin, OAuth, device auth, token management, and keychain integrationarea:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions