fix(core): refresh MCP OAuth token usage after re-auth (#26312)

Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
Sahil Kirad
2026-05-14 00:31:27 +05:30
committed by GitHub
parent fc4054446f
commit fd01cc03bf
5 changed files with 430 additions and 47 deletions
+70
View File
@@ -616,4 +616,74 @@ ${authUrl}
return null;
}
async getValidTokenWithMetadata(
serverName: string,
config: MCPOAuthConfig,
): Promise<{
accessToken: string;
tokenType: string;
expiresAt?: number;
scope?: string;
refreshToken?: string;
} | null> {
const credentials = await this.tokenStorage.getCredentials(serverName);
if (!credentials) return null;
let current = credentials.token;
if (this.tokenStorage.isTokenExpired(current)) {
const clientId = config.clientId ?? credentials.clientId;
if (current.refreshToken && clientId && credentials.tokenUrl) {
try {
const newTokenResponse = await this.refreshAccessToken(
config,
current.refreshToken,
credentials.tokenUrl,
credentials.mcpServerUrl,
);
const refreshed: OAuthToken = {
accessToken: newTokenResponse.access_token,
tokenType: newTokenResponse.token_type,
refreshToken:
newTokenResponse.refresh_token || current.refreshToken,
scope: newTokenResponse.scope || current.scope,
};
if (newTokenResponse.expires_in) {
refreshed.expiresAt =
Date.now() + newTokenResponse.expires_in * 1000;
}
await this.tokenStorage.saveToken(
serverName,
refreshed,
clientId,
credentials.tokenUrl,
credentials.mcpServerUrl,
);
current = refreshed;
} catch (error) {
coreEvents.emitFeedback(
'error',
'Failed to refresh auth token.',
error,
);
await this.tokenStorage.deleteCredentials(serverName);
return null;
}
} else {
return null;
}
}
return {
accessToken: current.accessToken,
tokenType: current.tokenType || 'Bearer',
expiresAt: current.expiresAt,
scope: current.scope,
refreshToken: current.refreshToken,
};
}
}
@@ -0,0 +1,101 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import type {
OAuthClientInformation,
OAuthClientMetadata,
OAuthTokens,
} from '@modelcontextprotocol/sdk/shared/auth.js';
import type { MCPServerConfig } from '../config/config.js';
import { MCPOAuthProvider } from './oauth-provider.js';
import { FIVE_MIN_BUFFER_MS } from './oauth-utils.js';
export class DynamicStoredOAuthProvider implements OAuthClientProvider {
readonly redirectUrl = '';
readonly clientMetadata: OAuthClientMetadata = {
client_name: 'Gemini CLI (Stored OAuth)',
redirect_uris: [],
grant_types: [],
response_types: [],
token_endpoint_auth_method: 'none',
};
private clientInfo?: OAuthClientInformation;
private readonly oauthProvider = new MCPOAuthProvider();
private cachedToken?: OAuthTokens;
private tokenExpiryTime?: number;
constructor(
private readonly serverName: string,
private readonly serverConfig: MCPServerConfig,
) {}
clientInformation(): OAuthClientInformation | undefined {
return this.clientInfo;
}
saveClientInformation(clientInformation: OAuthClientInformation): void {
this.clientInfo = clientInformation;
}
private isCachedTokenValid(): boolean {
return !!(
this.cachedToken?.access_token &&
this.tokenExpiryTime &&
Date.now() < this.tokenExpiryTime - FIVE_MIN_BUFFER_MS
);
}
async tokens(): Promise<OAuthTokens | undefined> {
if (this.isCachedTokenValid()) {
return this.cachedToken;
}
const oauthConfig =
this.serverConfig.oauth?.enabled && this.serverConfig.oauth
? this.serverConfig.oauth
: {};
const tokenMeta = await this.oauthProvider.getValidTokenWithMetadata(
this.serverName,
oauthConfig,
);
if (!tokenMeta?.accessToken) {
this.cachedToken = undefined;
this.tokenExpiryTime = undefined;
return undefined;
}
const freshTokens: OAuthTokens = {
access_token: tokenMeta.accessToken,
token_type: tokenMeta.tokenType || 'Bearer',
expires_in: tokenMeta.expiresAt
? Math.max(0, Math.floor((tokenMeta.expiresAt - Date.now()) / 1000))
: undefined,
scope: tokenMeta.scope,
refresh_token: tokenMeta.refreshToken,
};
if (freshTokens.expires_in !== undefined) {
this.cachedToken = freshTokens;
this.tokenExpiryTime = Date.now() + freshTokens.expires_in * 1000;
return this.cachedToken;
}
this.cachedToken = undefined;
this.tokenExpiryTime = undefined;
return freshTokens;
}
saveTokens(_tokens: OAuthTokens): void {}
redirectToAuthorization(_authorizationUrl: URL): void {}
saveCodeVerifier(_codeVerifier: string): void {}
codeVerifier(): string {
return '';
}
}