Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider (#20121)

This commit is contained in:
Nayana Parameswarappa
2026-03-02 13:37:44 -08:00
committed by GitHub
parent 8133d63ac6
commit dd9ccc9807
2 changed files with 181 additions and 0 deletions

View 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);
});
});
});

View 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;
}
}