mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider (#20121)
This commit is contained in:
committed by
GitHub
parent
8133d63ac6
commit
dd9ccc9807
84
packages/core/src/mcp/mcp-oauth-provider.test.ts
Normal file
84
packages/core/src/mcp/mcp-oauth-provider.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
MCPOAuthClientProvider,
|
||||
type OAuthAuthorizationResponse,
|
||||
} from './mcp-oauth-provider.js';
|
||||
import type {
|
||||
OAuthClientInformation,
|
||||
OAuthClientMetadata,
|
||||
OAuthTokens,
|
||||
} from '@modelcontextprotocol/sdk/shared/auth.js';
|
||||
|
||||
describe('MCPOAuthClientProvider', () => {
|
||||
const mockRedirectUrl = 'http://localhost:8090/callback';
|
||||
const mockClientMetadata: OAuthClientMetadata = {
|
||||
client_name: 'Test Client',
|
||||
redirect_uris: [mockRedirectUrl],
|
||||
grant_types: ['authorization_code', 'refresh_token'],
|
||||
response_types: ['code'],
|
||||
token_endpoint_auth_method: 'client_secret_post',
|
||||
scope: 'test-scope',
|
||||
};
|
||||
const mockState = 'test-state-123';
|
||||
|
||||
describe('oauth flow', () => {
|
||||
it('should support full OAuth flow', async () => {
|
||||
const onRedirectMock = vi.fn();
|
||||
const provider = new MCPOAuthClientProvider(
|
||||
mockRedirectUrl,
|
||||
mockClientMetadata,
|
||||
mockState,
|
||||
onRedirectMock,
|
||||
);
|
||||
|
||||
// Step 1: Save client information
|
||||
const clientInfo: OAuthClientInformation = {
|
||||
client_id: 'my-client-id',
|
||||
client_secret: 'my-client-secret',
|
||||
};
|
||||
provider.saveClientInformation(clientInfo);
|
||||
|
||||
// Step 2: Save code verifier
|
||||
provider.saveCodeVerifier('my-code-verifier');
|
||||
|
||||
// Step 3: Set up callback server
|
||||
const mockAuthResponse: OAuthAuthorizationResponse = {
|
||||
code: 'authorization-code',
|
||||
state: mockState,
|
||||
};
|
||||
const mockServer = {
|
||||
port: Promise.resolve(8090),
|
||||
waitForResponse: vi.fn().mockResolvedValue(mockAuthResponse),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
provider.saveCallbackServer(mockServer);
|
||||
|
||||
// Step 4: Redirect to authorization
|
||||
const authUrl = new URL('http://auth.example.com/authorize');
|
||||
await provider.redirectToAuthorization(authUrl);
|
||||
|
||||
// Step 5: Save tokens after exchange
|
||||
const tokens: OAuthTokens = {
|
||||
access_token: 'final-access-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'final-refresh-token',
|
||||
};
|
||||
provider.saveTokens(tokens);
|
||||
|
||||
// Verify all data is stored correctly
|
||||
expect(provider.clientInformation()).toEqual(clientInfo);
|
||||
expect(provider.codeVerifier()).toBe('my-code-verifier');
|
||||
expect(provider.state()).toBe(mockState);
|
||||
expect(provider.tokens()).toEqual(tokens);
|
||||
expect(onRedirectMock).toHaveBeenCalledWith(authUrl);
|
||||
expect(provider.getSavedCallbackServer()).toBe(mockServer);
|
||||
});
|
||||
});
|
||||
});
|
||||
97
packages/core/src/mcp/mcp-oauth-provider.ts
Normal file
97
packages/core/src/mcp/mcp-oauth-provider.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @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 { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
/**
|
||||
* OAuth authorization response.
|
||||
*/
|
||||
export interface OAuthAuthorizationResponse {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
type CallbackServer = {
|
||||
port: Promise<number>;
|
||||
waitForResponse: () => Promise<OAuthAuthorizationResponse>;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
export class MCPOAuthClientProvider implements OAuthClientProvider {
|
||||
private _clientInformation?: OAuthClientInformation;
|
||||
private _tokens?: OAuthTokens;
|
||||
private _codeVerifier?: string;
|
||||
private _cbServer?: CallbackServer;
|
||||
|
||||
constructor(
|
||||
private readonly _redirectUrl: string | URL,
|
||||
private readonly _clientMetadata: OAuthClientMetadata,
|
||||
private readonly _state?: string | undefined,
|
||||
private readonly _onRedirect: (url: URL) => void = (url) => {
|
||||
debugLogger.log(`Redirect to: ${url.toString()}`);
|
||||
},
|
||||
) {}
|
||||
|
||||
get redirectUrl(): string | URL {
|
||||
return this._redirectUrl;
|
||||
}
|
||||
|
||||
get clientMetadata(): OAuthClientMetadata {
|
||||
return this._clientMetadata;
|
||||
}
|
||||
|
||||
saveCallbackServer(server: CallbackServer): void {
|
||||
this._cbServer = server;
|
||||
}
|
||||
|
||||
getSavedCallbackServer(): CallbackServer | undefined {
|
||||
return this._cbServer;
|
||||
}
|
||||
|
||||
clientInformation(): OAuthClientInformation | undefined {
|
||||
return this._clientInformation;
|
||||
}
|
||||
|
||||
saveClientInformation(clientInformation: OAuthClientInformation): void {
|
||||
this._clientInformation = clientInformation;
|
||||
}
|
||||
|
||||
tokens(): OAuthTokens | undefined {
|
||||
return this._tokens;
|
||||
}
|
||||
|
||||
saveTokens(tokens: OAuthTokens): void {
|
||||
this._tokens = tokens;
|
||||
}
|
||||
|
||||
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
||||
this._onRedirect(authorizationUrl);
|
||||
}
|
||||
|
||||
saveCodeVerifier(codeVerifier: string): void {
|
||||
this._codeVerifier = codeVerifier;
|
||||
}
|
||||
|
||||
codeVerifier(): string {
|
||||
if (!this._codeVerifier) {
|
||||
throw new Error('No code verifier saved');
|
||||
}
|
||||
return this._codeVerifier;
|
||||
}
|
||||
|
||||
state(): string {
|
||||
if (!this._state) {
|
||||
throw new Error('No code state saved');
|
||||
}
|
||||
return this._state;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user