Hmbown/CodeWhale · error

stored MCP OAuth credential for server

Error message

stored MCP OAuth credential for server {} was rejected by the provider ({reason}) and removed; the server requires OAuth login again

What it means

Raised in McpOAuth refresh_and_persist when the provider rejects a stored refresh/access token (rejection reason defaults to "unauthorized"). The stored credential is removed as a side effect and the user is told to log in again, because an invalid grant cannot be recovered programmatically.

Solutions

  1. Run the MCP OAuth login flow again for that server (e.g. /mcp login <server> or `codewhale mcp login <server>`); the stale token has already been deleted
  2. Check the {reason} in the message: "unauthorized"/"invalid_grant" confirms the token is dead and re-login is the only fix
  3. If tokens were rotated on another machine, that is expected — re-login here
  4. If re-login immediately fails with the same rejection, verify the server's OAuth client registration (client_id/redirect URL) with the provider

Example fix

// no code fix; re-authenticate
/mcp login my-server
// or
codewhale mcp login my-server
Defensive patterns

Strategy: try-catch

Try / catch

match client.refresh_if_needed().await {
    Err(e) if e.to_string().contains("was rejected by the provider") => {
        eprintln!("stored MCP token is dead; starting interactive login...");
        perform_oauth_login_for_server(&server).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling refresh_if_needed or force_refresh when the OAuth provider answers a token refresh with a rejection (e.g. HTTP 400 invalid_grant / 401) for a server's stored tokens.

Common situations: Tokens revoked by the user from the provider's dashboard; refresh token expired or rotated elsewhere (the same account logged in from another machine); provider-side session/consent revocation; server or provider policy changed invalidating old grants.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/be8fb1353e2b3d46. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/oauth.rs:729

    }

    async fn refresh_and_persist(&self) -> Result<()> {
        // A credential the provider definitively rejected is never replayed:
        // the `AuthorizationManager` still holds it, but every later refresh
        // with that grant is a guaranteed `invalid_grant`. The only way back
        // is a credential another process stored since (a completed login),
        // so adopt that when present and otherwise report auth-required
        // without touching the token endpoint.
        if self.is_invalidated().await {
            if !self.adopt_rotated_on_disk_tokens().await? {
                let reason = self
                    .inner
                    .rejection
                    .lock()
                    .await
                    .clone()
                    .unwrap_or_else(|| "unauthorized".to_string());
                bail!(
                    "stored MCP OAuth credential for server {} was rejected by the provider ({reason}) and removed; the server requires OAuth login again",
                    self.inner.server_name
                );
            }
            let adopted_needs_refresh = {
                let last = self.inner.last_tokens.lock().await;
                token_needs_refresh(last.as_ref().and_then(|tokens| tokens.expires_at))
            };
            if !adopted_needs_refresh {
                return Ok(());
            }
        }
        // Only this refresh's answer may explain this refresh's failure.
        self.inner.http_client.take_token_endpoint_receipt();
        let mut err = match self.try_refresh_and_persist().await {
            Ok(()) => return Ok(()),
            Err(err) => err,
        };

View on GitHub (pinned to 73e0f67d83)