Skip to content

bug(mcp): MCP OAuth fails to obtain refresh_token and drops client_secret during background token refresh #29577

Description

@arsenyspb

What happened?

When configuring and using remote MCP servers that utilize OAuth 2.0 with Google endpoints (such as Google Workspace APIs like Docs, Sheets, Slides, Drive) or other confidential OAuth providers requiring a client_secret for token refresh:

  1. No Refresh Token Issued: On initial interactive authentication via /mcp auth <server>, Google's authorization server only issues a 1-hour access_token and no refresh_token. This occurs because buildAuthorizationUrl (packages/core/src/utils/oauth-flow.ts) constructs a generic RFC 6749 authorization URL without requesting access_type=offline and prompt=consent for Google endpoints (unlike the CLI's internal login flow in packages/core/src/code_assist/oauth2.ts).
  2. client_secret Discarded During Refresh: When a refresh token is present, background token refresh fails against Google's token endpoint (https://oauth2.googleapis.com/token) because Google strictly requires client_secret for OAuth clients registered with a secret. The CLI completely drops clientSecret:
    • packages/core/src/tools/mcp-client.ts: getStoredOAuthToken() only passes { clientId: credentials.clientId } to authProvider.getValidToken().
    • packages/core/src/mcp/token-storage/types.ts: OAuthCredentials does not store clientSecret.
      As a result, refreshAccessToken() posts without client_secret, and Google rejects the request:
      {"error": "invalid_request", "error_description": "client_secret is missing."}
  3. Destructive Wipe on Refresh Failure: In packages/core/src/mcp/oauth-provider.ts (getValidToken), any failure during token refresh immediately triggers:
    await this.tokenStorage.deleteCredentials(serverName);
    This deletes the stored credentials and traps the user in a continuous re-authentication loop where they are forced to log in via browser every single hour.

What did you expect to happen?

  1. For Google authorization endpoints, buildAuthorizationUrl should request access_type: "offline" and prompt: "consent" so that a long-lived refresh_token is returned on initial authentication.
  2. clientSecret provided in MCP configuration should be preserved in OAuthCredentials and forwarded by getStoredOAuthToken to getValidToken, allowing refreshAccessToken to successfully authenticate against endpoints that enforce client secret validation.
  3. Refresh operations should rotate tokens silently in the background without wiping credentials on recoverable or configuration-related errors (also related to bug: concurrent MCP OAuth refreshes race, and any refresh failure deletes valid credentials (forced re-auth) #29048).

Client information

Client Information

• CLI Version: 0.62.0
• Operating System: macOS Darwin arm64
• Node Version: v26.8.1

Login information

Google Account & Remote MCP Servers with OAuth 2.0 (Google Workspace APIs / Google Cloud OAuth 2.0 Web & Desktop Client IDs)

Anything else we need to know?

We have audited the relevant code paths in packages/core:

  • packages/core/src/utils/oauth-flow.ts:441 (buildAuthorizationUrl)
  • packages/core/src/tools/mcp-client.ts:1656 (getStoredOAuthToken)
  • packages/core/src/mcp/token-storage/types.ts (OAuthCredentials)
  • packages/core/src/mcp/oauth-token-storage.ts (MCPOAuthTokenStorage.saveToken)
  • packages/core/src/mcp/oauth-provider.ts:620 (MCPOAuthProvider.getValidToken)

A pull request addressing this with tests will be linked shortly.

Activity

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

    status/need-triageIssues that need to be triaged by the triage automation.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions