Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0

This commit is contained in:
mingholy.lmh
2025-09-10 21:01:40 +08:00
583 changed files with 30160 additions and 10770 deletions

View File

@@ -6,8 +6,9 @@
import { GoogleAuth } from 'google-auth-library';
import { GoogleCredentialProvider } from './google-auth-provider.js';
import { vi, describe, beforeEach, it, expect, Mock } from 'vitest';
import { MCPServerConfig } from '../config/config.js';
import type { Mock } from 'vitest';
import { vi, describe, beforeEach, it, expect } from 'vitest';
import type { MCPServerConfig } from '../config/config.js';
vi.mock('google-auth-library');

View File

@@ -4,15 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import {
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import type {
OAuthClientInformation,
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthTokens,
} from '@modelcontextprotocol/sdk/shared/auth.js';
import { GoogleAuth } from 'google-auth-library';
import { MCPServerConfig } from '../config/config.js';
import type { MCPServerConfig } from '../config/config.js';
const ALLOWED_HOSTS = [/^.+\.googleapis\.com$/, /^(.*\.)?luci\.app$/];

View File

@@ -17,13 +17,14 @@ vi.mock('./oauth-token-storage.js');
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as http from 'node:http';
import * as crypto from 'node:crypto';
import {
MCPOAuthProvider,
import type {
MCPOAuthConfig,
OAuthTokenResponse,
OAuthClientRegistrationResponse,
} from './oauth-provider.js';
import { MCPOAuthTokenStorage, MCPOAuthToken } from './oauth-token-storage.js';
import { MCPOAuthProvider } from './oauth-provider.js';
import type { OAuthToken } from './token-storage/types.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
// Mock fetch globally
const mockFetch = vi.fn();
@@ -100,7 +101,7 @@ describe('MCPOAuthProvider', () => {
audiences: ['https://api.example.com'],
};
const mockToken: MCPOAuthToken = {
const mockToken: OAuthToken = {
accessToken: 'access_token_123',
refreshToken: 'refresh_token_456',
tokenType: 'Bearer',

View File

@@ -8,7 +8,8 @@ import * as http from 'node:http';
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
import { MCPOAuthToken, MCPOAuthTokenStorage } from './oauth-token-storage.js';
import type { OAuthToken } from './token-storage/types.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
import { getErrorMessage } from '../utils/errors.js';
import { OAuthUtils } from './oauth-utils.js';
@@ -582,7 +583,7 @@ export class MCPOAuthProvider {
serverName: string,
config: MCPOAuthConfig,
mcpServerUrl?: string,
): Promise<MCPOAuthToken> {
): Promise<OAuthToken> {
// If no authorization URL is provided, try to discover OAuth configuration
if (!config.authorizationUrl && mcpServerUrl) {
console.log(
@@ -776,7 +777,7 @@ export class MCPOAuthProvider {
throw new Error('No access token received from token endpoint');
}
const token: MCPOAuthToken = {
const token: OAuthToken = {
accessToken: tokenResponse.access_token,
tokenType: tokenResponse.token_type || 'Bearer',
refreshToken: tokenResponse.refresh_token,
@@ -862,7 +863,7 @@ export class MCPOAuthProvider {
);
// Update stored token
const newToken: MCPOAuthToken = {
const newToken: OAuthToken = {
accessToken: newTokenResponse.access_token,
tokenType: newTokenResponse.token_type,
refreshToken: newTokenResponse.refresh_token || token.refreshToken,

View File

@@ -7,11 +7,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import {
MCPOAuthTokenStorage,
MCPOAuthToken,
MCPOAuthCredentials,
} from './oauth-token-storage.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
import type { OAuthToken, OAuthCredentials } from './token-storage/types.js';
// Mock file system operations
vi.mock('node:fs', () => ({
@@ -21,6 +18,7 @@ vi.mock('node:fs', () => ({
mkdir: vi.fn(),
unlink: vi.fn(),
},
mkdirSync: vi.fn(),
}));
vi.mock('node:os', () => ({
@@ -28,7 +26,7 @@ vi.mock('node:os', () => ({
}));
describe('MCPOAuthTokenStorage', () => {
const mockToken: MCPOAuthToken = {
const mockToken: OAuthToken = {
accessToken: 'access_token_123',
refreshToken: 'refresh_token_456',
tokenType: 'Bearer',
@@ -36,7 +34,7 @@ describe('MCPOAuthTokenStorage', () => {
expiresAt: Date.now() + 3600000, // 1 hour from now
};
const mockCredentials: MCPOAuthCredentials = {
const mockCredentials: OAuthCredentials = {
serverName: 'test-server',
token: mockToken,
clientId: 'test-client-id',
@@ -72,7 +70,7 @@ describe('MCPOAuthTokenStorage', () => {
expect(tokens.size).toBe(1);
expect(tokens.get('test-server')).toEqual(mockCredentials);
expect(fs.readFile).toHaveBeenCalledWith(
path.join('/mock/home', '.gemini', 'mcp-oauth-tokens.json'),
path.join('/mock/home', '.qwen', 'mcp-oauth-tokens.json'),
'utf-8',
);
});
@@ -114,12 +112,11 @@ describe('MCPOAuthTokenStorage', () => {
'https://token.url',
);
expect(fs.mkdir).toHaveBeenCalledWith(
path.join('/mock/home', '.gemini'),
{ recursive: true },
);
expect(fs.mkdir).toHaveBeenCalledWith(path.join('/mock/home', '.qwen'), {
recursive: true,
});
expect(fs.writeFile).toHaveBeenCalledWith(
path.join('/mock/home', '.gemini', 'mcp-oauth-tokens.json'),
path.join('/mock/home', '.qwen', 'mcp-oauth-tokens.json'),
expect.stringContaining('test-server'),
{ mode: 0o600 },
);
@@ -219,7 +216,7 @@ describe('MCPOAuthTokenStorage', () => {
await MCPOAuthTokenStorage.removeToken('test-server');
expect(fs.unlink).toHaveBeenCalledWith(
path.join('/mock/home', '.gemini', 'mcp-oauth-tokens.json'),
path.join('/mock/home', '.qwen', 'mcp-oauth-tokens.json'),
);
expect(fs.writeFile).not.toHaveBeenCalled();
});
@@ -300,7 +297,7 @@ describe('MCPOAuthTokenStorage', () => {
await MCPOAuthTokenStorage.clearAllTokens();
expect(fs.unlink).toHaveBeenCalledWith(
path.join('/mock/home', '.gemini', 'mcp-oauth-tokens.json'),
path.join('/mock/home', '.qwen', 'mcp-oauth-tokens.json'),
);
});

View File

@@ -6,47 +6,21 @@
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { Storage } from '../config/storage.js';
import { getErrorMessage } from '../utils/errors.js';
/**
* Interface for MCP OAuth tokens.
*/
export interface MCPOAuthToken {
accessToken: string;
refreshToken?: string;
expiresAt?: number;
tokenType: string;
scope?: string;
}
/**
* Interface for stored MCP OAuth credentials.
*/
export interface MCPOAuthCredentials {
serverName: string;
token: MCPOAuthToken;
clientId?: string;
tokenUrl?: string;
mcpServerUrl?: string;
updatedAt: number;
}
import type { OAuthToken, OAuthCredentials } from './token-storage/types.js';
/**
* Class for managing MCP OAuth token storage and retrieval.
*/
export class MCPOAuthTokenStorage {
private static readonly TOKEN_FILE = 'mcp-oauth-tokens.json';
private static readonly CONFIG_DIR = '.gemini';
/**
* Get the path to the token storage file.
*
* @returns The full path to the token storage file
*/
private static getTokenFilePath(): string {
const homeDir = os.homedir();
return path.join(homeDir, this.CONFIG_DIR, this.TOKEN_FILE);
return Storage.getMcpOAuthTokensPath();
}
/**
@@ -62,13 +36,13 @@ export class MCPOAuthTokenStorage {
*
* @returns A map of server names to credentials
*/
static async loadTokens(): Promise<Map<string, MCPOAuthCredentials>> {
const tokenMap = new Map<string, MCPOAuthCredentials>();
static async loadTokens(): Promise<Map<string, OAuthCredentials>> {
const tokenMap = new Map<string, OAuthCredentials>();
try {
const tokenFile = this.getTokenFilePath();
const data = await fs.readFile(tokenFile, 'utf-8');
const tokens = JSON.parse(data) as MCPOAuthCredentials[];
const tokens = JSON.parse(data) as OAuthCredentials[];
for (const credential of tokens) {
tokenMap.set(credential.serverName, credential);
@@ -96,7 +70,7 @@ export class MCPOAuthTokenStorage {
*/
static async saveToken(
serverName: string,
token: MCPOAuthToken,
token: OAuthToken,
clientId?: string,
tokenUrl?: string,
mcpServerUrl?: string,
@@ -105,7 +79,7 @@ export class MCPOAuthTokenStorage {
const tokens = await this.loadTokens();
const credential: MCPOAuthCredentials = {
const credential: OAuthCredentials = {
serverName,
token,
clientId,
@@ -139,9 +113,7 @@ export class MCPOAuthTokenStorage {
* @param serverName The name of the MCP server
* @returns The stored credentials or null if not found
*/
static async getToken(
serverName: string,
): Promise<MCPOAuthCredentials | null> {
static async getToken(serverName: string): Promise<OAuthCredentials | null> {
const tokens = await this.loadTokens();
return tokens.get(serverName) || null;
}
@@ -181,7 +153,7 @@ export class MCPOAuthTokenStorage {
* @param token The token to check
* @returns True if the token is expired
*/
static isTokenExpired(token: MCPOAuthToken): boolean {
static isTokenExpired(token: OAuthToken): boolean {
if (!token.expiresAt) {
return false; // No expiry, assume valid
}

View File

@@ -5,11 +5,11 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
OAuthUtils,
import type {
OAuthAuthorizationServerMetadata,
OAuthProtectedResourceMetadata,
} from './oauth-utils.js';
import { OAuthUtils } from './oauth-utils.js';
// Mock fetch globally
const mockFetch = vi.fn();
@@ -142,6 +142,74 @@ describe('OAuthUtils', () => {
});
});
describe('discoverAuthorizationServerMetadata', () => {
const mockAuthServerMetadata: OAuthAuthorizationServerMetadata = {
issuer: 'https://auth.example.com',
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
scopes_supported: ['read', 'write'],
};
it('should handle URLs without path components correctly', async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthServerMetadata),
});
const result = await OAuthUtils.discoverAuthorizationServerMetadata(
'https://auth.example.com/',
);
expect(result).toEqual(mockAuthServerMetadata);
expect(mockFetch).nthCalledWith(
1,
'https://auth.example.com/.well-known/oauth-authorization-server',
);
expect(mockFetch).nthCalledWith(
2,
'https://auth.example.com/.well-known/openid-configuration',
);
});
it('should handle URLs with path components correctly', async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
})
.mockResolvedValueOnce({
ok: false,
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthServerMetadata),
});
const result = await OAuthUtils.discoverAuthorizationServerMetadata(
'https://auth.example.com/mcp',
);
expect(result).toEqual(mockAuthServerMetadata);
expect(mockFetch).nthCalledWith(
1,
'https://auth.example.com/.well-known/oauth-authorization-server/mcp',
);
expect(mockFetch).nthCalledWith(
2,
'https://auth.example.com/.well-known/openid-configuration/mcp',
);
expect(mockFetch).nthCalledWith(
3,
'https://auth.example.com/mcp/.well-known/openid-configuration',
);
});
});
describe('metadataToOAuthConfig', () => {
it('should convert metadata to OAuth config', () => {
const metadata: OAuthAuthorizationServerMetadata = {

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { MCPOAuthConfig } from './oauth-provider.js';
import type { MCPOAuthConfig } from './oauth-provider.js';
import { getErrorMessage } from '../utils/errors.js';
/**
@@ -140,6 +140,76 @@ export class OAuthUtils {
};
}
/**
* Discover Oauth Authorization server metadata given an Auth server URL, by
* trying the standard well-known endpoints.
*
* @param authServerUrl The authorization server URL
* @returns The authorization server metadata or null if not found
*/
static async discoverAuthorizationServerMetadata(
authServerUrl: string,
): Promise<OAuthAuthorizationServerMetadata | null> {
const authServerUrlObj = new URL(authServerUrl);
const base = `${authServerUrlObj.protocol}//${authServerUrlObj.host}`;
const endpointsToTry: string[] = [];
// With issuer URLs with path components, try the following well-known
// endpoints in order:
if (authServerUrlObj.pathname !== '/') {
// 1. OAuth 2.0 Authorization Server Metadata with path insertion
endpointsToTry.push(
new URL(
`/.well-known/oauth-authorization-server${authServerUrlObj.pathname}`,
base,
).toString(),
);
// 2. OpenID Connect Discovery 1.0 with path insertion
endpointsToTry.push(
new URL(
`/.well-known/openid-configuration${authServerUrlObj.pathname}`,
base,
).toString(),
);
// 3. OpenID Connect Discovery 1.0 with path appending
endpointsToTry.push(
new URL(
`${authServerUrlObj.pathname}/.well-known/openid-configuration`,
base,
).toString(),
);
}
// With issuer URLs without path components, and those that failed previous
// discoveries, try the following well-known endpoints in order:
// 1. OAuth 2.0 Authorization Server Metadata
endpointsToTry.push(
new URL('/.well-known/oauth-authorization-server', base).toString(),
);
// 2. OpenID Connect Discovery 1.0
endpointsToTry.push(
new URL('/.well-known/openid-configuration', base).toString(),
);
for (const endpoint of endpointsToTry) {
const authServerMetadata =
await this.fetchAuthorizationServerMetadata(endpoint);
if (authServerMetadata) {
return authServerMetadata;
}
}
console.debug(
`Metadata discovery failed for authorization server ${authServerUrl}`,
);
return null;
}
/**
* Discover OAuth configuration using the standard well-known endpoints.
*
@@ -172,33 +242,8 @@ export class OAuthUtils {
if (resourceMetadata?.authorization_servers?.length) {
// Use the first authorization server
const authServerUrl = resourceMetadata.authorization_servers[0];
// The authorization server URL may include a path (e.g., https://github.com/login/oauth)
// We need to preserve this path when constructing the metadata URL
const authServerUrlObj = new URL(authServerUrl);
const authServerPath =
authServerUrlObj.pathname === '/' ? '' : authServerUrlObj.pathname;
// Try with the authorization server's path first
let authServerMetadataUrl = new URL(
`/.well-known/oauth-authorization-server${authServerPath}`,
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
).toString();
let authServerMetadata = await this.fetchAuthorizationServerMetadata(
authServerMetadataUrl,
);
// If that fails, try root as fallback
if (!authServerMetadata && authServerPath) {
authServerMetadataUrl = new URL(
'/.well-known/oauth-authorization-server',
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
).toString();
authServerMetadata = await this.fetchAuthorizationServerMetadata(
authServerMetadataUrl,
);
}
const authServerMetadata =
await this.discoverAuthorizationServerMetadata(authServerUrl);
if (authServerMetadata) {
const config = this.metadataToOAuthConfig(authServerMetadata);
@@ -212,13 +257,10 @@ export class OAuthUtils {
}
}
// Fallback: try /.well-known/oauth-authorization-server at the base URL
console.debug(
`Trying OAuth discovery fallback at ${wellKnownUrls.authorizationServer}`,
);
const authServerMetadata = await this.fetchAuthorizationServerMetadata(
wellKnownUrls.authorizationServer,
);
// Fallback: try well-known endpoints at the base URL
console.debug(`Trying OAuth discovery fallback at ${serverUrl}`);
const authServerMetadata =
await this.discoverAuthorizationServerMetadata(serverUrl);
if (authServerMetadata) {
const config = this.metadataToOAuthConfig(authServerMetadata);
@@ -277,34 +319,8 @@ export class OAuthUtils {
}
const authServerUrl = resourceMetadata.authorization_servers[0];
// The authorization server URL may include a path (e.g., https://github.com/login/oauth)
// We need to preserve this path when constructing the metadata URL
const authServerUrlObj = new URL(authServerUrl);
const authServerPath =
authServerUrlObj.pathname === '/' ? '' : authServerUrlObj.pathname;
// Build auth server metadata URL with the authorization server's path
const authServerMetadataUrl = new URL(
`/.well-known/oauth-authorization-server${authServerPath}`,
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
).toString();
let authServerMetadata = await this.fetchAuthorizationServerMetadata(
authServerMetadataUrl,
);
// If that fails and we have a path, also try the root path as a fallback
if (!authServerMetadata && authServerPath) {
const rootAuthServerMetadataUrl = new URL(
'/.well-known/oauth-authorization-server',
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
).toString();
authServerMetadata = await this.fetchAuthorizationServerMetadata(
rootAuthServerMetadataUrl,
);
}
const authServerMetadata =
await this.discoverAuthorizationServerMetadata(authServerUrl);
if (authServerMetadata) {
return this.metadataToOAuthConfig(authServerMetadata);

View File

@@ -0,0 +1,208 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { BaseTokenStorage } from './base-token-storage.js';
import type { OAuthCredentials, OAuthToken } from './types.js';
class TestTokenStorage extends BaseTokenStorage {
private storage = new Map<string, OAuthCredentials>();
async getCredentials(serverName: string): Promise<OAuthCredentials | null> {
return this.storage.get(serverName) || null;
}
async setCredentials(credentials: OAuthCredentials): Promise<void> {
this.validateCredentials(credentials);
this.storage.set(credentials.serverName, credentials);
}
async deleteCredentials(serverName: string): Promise<void> {
this.storage.delete(serverName);
}
async listServers(): Promise<string[]> {
return Array.from(this.storage.keys());
}
async getAllCredentials(): Promise<Map<string, OAuthCredentials>> {
return new Map(this.storage);
}
async clearAll(): Promise<void> {
this.storage.clear();
}
override validateCredentials(credentials: OAuthCredentials): void {
super.validateCredentials(credentials);
}
override isTokenExpired(credentials: OAuthCredentials): boolean {
return super.isTokenExpired(credentials);
}
override sanitizeServerName(serverName: string): string {
return super.sanitizeServerName(serverName);
}
}
describe('BaseTokenStorage', () => {
let storage: TestTokenStorage;
beforeEach(() => {
storage = new TestTokenStorage();
});
describe('validateCredentials', () => {
it('should validate valid credentials', () => {
const credentials: OAuthCredentials = {
serverName: 'test-server',
token: {
accessToken: 'access-token',
tokenType: 'Bearer',
},
updatedAt: Date.now(),
};
expect(() => storage.validateCredentials(credentials)).not.toThrow();
});
it('should throw for missing server name', () => {
const credentials = {
serverName: '',
token: {
accessToken: 'access-token',
tokenType: 'Bearer',
},
updatedAt: Date.now(),
} as OAuthCredentials;
expect(() => storage.validateCredentials(credentials)).toThrow(
'Server name is required',
);
});
it('should throw for missing token', () => {
const credentials = {
serverName: 'test-server',
token: null as unknown as OAuthToken,
updatedAt: Date.now(),
} as OAuthCredentials;
expect(() => storage.validateCredentials(credentials)).toThrow(
'Token is required',
);
});
it('should throw for missing access token', () => {
const credentials = {
serverName: 'test-server',
token: {
accessToken: '',
tokenType: 'Bearer',
},
updatedAt: Date.now(),
} as OAuthCredentials;
expect(() => storage.validateCredentials(credentials)).toThrow(
'Access token is required',
);
});
it('should throw for missing token type', () => {
const credentials = {
serverName: 'test-server',
token: {
accessToken: 'access-token',
tokenType: '',
},
updatedAt: Date.now(),
} as OAuthCredentials;
expect(() => storage.validateCredentials(credentials)).toThrow(
'Token type is required',
);
});
});
describe('isTokenExpired', () => {
it('should return false for tokens without expiry', () => {
const credentials: OAuthCredentials = {
serverName: 'test-server',
token: {
accessToken: 'access-token',
tokenType: 'Bearer',
},
updatedAt: Date.now(),
};
expect(storage.isTokenExpired(credentials)).toBe(false);
});
it('should return false for valid tokens', () => {
const credentials: OAuthCredentials = {
serverName: 'test-server',
token: {
accessToken: 'access-token',
tokenType: 'Bearer',
expiresAt: Date.now() + 3600000,
},
updatedAt: Date.now(),
};
expect(storage.isTokenExpired(credentials)).toBe(false);
});
it('should return true for expired tokens', () => {
const credentials: OAuthCredentials = {
serverName: 'test-server',
token: {
accessToken: 'access-token',
tokenType: 'Bearer',
expiresAt: Date.now() - 3600000,
},
updatedAt: Date.now(),
};
expect(storage.isTokenExpired(credentials)).toBe(true);
});
it('should apply 5-minute buffer for expiry check', () => {
const fourMinutesFromNow = Date.now() + 4 * 60 * 1000;
const credentials: OAuthCredentials = {
serverName: 'test-server',
token: {
accessToken: 'access-token',
tokenType: 'Bearer',
expiresAt: fourMinutesFromNow,
},
updatedAt: Date.now(),
};
expect(storage.isTokenExpired(credentials)).toBe(true);
});
});
describe('sanitizeServerName', () => {
it('should keep valid characters', () => {
expect(storage.sanitizeServerName('test-server.example_123')).toBe(
'test-server.example_123',
);
});
it('should replace invalid characters with underscore', () => {
expect(storage.sanitizeServerName('test@server#example')).toBe(
'test_server_example',
);
});
it('should handle special characters', () => {
expect(storage.sanitizeServerName('test server/example:123')).toBe(
'test_server_example_123',
);
});
});
});

View File

@@ -0,0 +1,49 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { TokenStorage, OAuthCredentials } from './types.js';
export abstract class BaseTokenStorage implements TokenStorage {
protected readonly serviceName: string;
constructor(serviceName: string = 'gemini-cli-mcp-oauth') {
this.serviceName = serviceName;
}
abstract getCredentials(serverName: string): Promise<OAuthCredentials | null>;
abstract setCredentials(credentials: OAuthCredentials): Promise<void>;
abstract deleteCredentials(serverName: string): Promise<void>;
abstract listServers(): Promise<string[]>;
abstract getAllCredentials(): Promise<Map<string, OAuthCredentials>>;
abstract clearAll(): Promise<void>;
protected validateCredentials(credentials: OAuthCredentials): void {
if (!credentials.serverName) {
throw new Error('Server name is required');
}
if (!credentials.token) {
throw new Error('Token is required');
}
if (!credentials.token.accessToken) {
throw new Error('Access token is required');
}
if (!credentials.token.tokenType) {
throw new Error('Token type is required');
}
}
protected isTokenExpired(credentials: OAuthCredentials): boolean {
if (!credentials.token.expiresAt) {
return false;
}
const bufferMs = 5 * 60 * 1000;
return Date.now() > credentials.token.expiresAt - bufferMs;
}
protected sanitizeServerName(serverName: string): string {
return serverName.replace(/[^a-zA-Z0-9-_.]/g, '_');
}
}

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Interface for OAuth tokens.
*/
export interface OAuthToken {
accessToken: string;
refreshToken?: string;
expiresAt?: number;
tokenType: string;
scope?: string;
}
/**
* Interface for stored OAuth credentials.
*/
export interface OAuthCredentials {
serverName: string;
token: OAuthToken;
clientId?: string;
tokenUrl?: string;
mcpServerUrl?: string;
updatedAt: number;
}
export interface TokenStorage {
getCredentials(serverName: string): Promise<OAuthCredentials | null>;
setCredentials(credentials: OAuthCredentials): Promise<void>;
deleteCredentials(serverName: string): Promise<void>;
listServers(): Promise<string[]>;
getAllCredentials(): Promise<Map<string, OAuthCredentials>>;
clearAll(): Promise<void>;
}