From dd9ccc980780109f6c2229351afaa304120ec2ca Mon Sep 17 00:00:00 2001 From: Nayana Parameswarappa <138813846+Nayana-Parameswarappa@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:37:44 -0800 Subject: [PATCH] Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider (#20121) --- .../core/src/mcp/mcp-oauth-provider.test.ts | 84 ++++++++++++++++ packages/core/src/mcp/mcp-oauth-provider.ts | 97 +++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 packages/core/src/mcp/mcp-oauth-provider.test.ts create mode 100644 packages/core/src/mcp/mcp-oauth-provider.ts diff --git a/packages/core/src/mcp/mcp-oauth-provider.test.ts b/packages/core/src/mcp/mcp-oauth-provider.test.ts new file mode 100644 index 0000000000..a7891f035b --- /dev/null +++ b/packages/core/src/mcp/mcp-oauth-provider.test.ts @@ -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); + }); + }); +}); diff --git a/packages/core/src/mcp/mcp-oauth-provider.ts b/packages/core/src/mcp/mcp-oauth-provider.ts new file mode 100644 index 0000000000..daf977438c --- /dev/null +++ b/packages/core/src/mcp/mcp-oauth-provider.ts @@ -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; + waitForResponse: () => Promise; + close: () => Promise; +}; + +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 { + 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; + } +}