Skip to content
Merged
84 changes: 84 additions & 0 deletions packages/core/src/mcp/mcp-oauth-provider.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 97 additions & 0 deletions packages/core/src/mcp/mcp-oauth-provider.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading