Skip to content

Commit 014094b

Browse files
Nayana-Parameswarappastruckoff
authored andcommitted
Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider (google-gemini#20121)
1 parent c11502b commit 014094b

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi } from 'vitest';
8+
import {
9+
MCPOAuthClientProvider,
10+
type OAuthAuthorizationResponse,
11+
} from './mcp-oauth-provider.js';
12+
import type {
13+
OAuthClientInformation,
14+
OAuthClientMetadata,
15+
OAuthTokens,
16+
} from '@modelcontextprotocol/sdk/shared/auth.js';
17+
18+
describe('MCPOAuthClientProvider', () => {
19+
const mockRedirectUrl = 'http://localhost:8090/callback';
20+
const mockClientMetadata: OAuthClientMetadata = {
21+
client_name: 'Test Client',
22+
redirect_uris: [mockRedirectUrl],
23+
grant_types: ['authorization_code', 'refresh_token'],
24+
response_types: ['code'],
25+
token_endpoint_auth_method: 'client_secret_post',
26+
scope: 'test-scope',
27+
};
28+
const mockState = 'test-state-123';
29+
30+
describe('oauth flow', () => {
31+
it('should support full OAuth flow', async () => {
32+
const onRedirectMock = vi.fn();
33+
const provider = new MCPOAuthClientProvider(
34+
mockRedirectUrl,
35+
mockClientMetadata,
36+
mockState,
37+
onRedirectMock,
38+
);
39+
40+
// Step 1: Save client information
41+
const clientInfo: OAuthClientInformation = {
42+
client_id: 'my-client-id',
43+
client_secret: 'my-client-secret',
44+
};
45+
provider.saveClientInformation(clientInfo);
46+
47+
// Step 2: Save code verifier
48+
provider.saveCodeVerifier('my-code-verifier');
49+
50+
// Step 3: Set up callback server
51+
const mockAuthResponse: OAuthAuthorizationResponse = {
52+
code: 'authorization-code',
53+
state: mockState,
54+
};
55+
const mockServer = {
56+
port: Promise.resolve(8090),
57+
waitForResponse: vi.fn().mockResolvedValue(mockAuthResponse),
58+
close: vi.fn().mockResolvedValue(undefined),
59+
};
60+
provider.saveCallbackServer(mockServer);
61+
62+
// Step 4: Redirect to authorization
63+
const authUrl = new URL('http://auth.example.com/authorize');
64+
await provider.redirectToAuthorization(authUrl);
65+
66+
// Step 5: Save tokens after exchange
67+
const tokens: OAuthTokens = {
68+
access_token: 'final-access-token',
69+
token_type: 'Bearer',
70+
expires_in: 3600,
71+
refresh_token: 'final-refresh-token',
72+
};
73+
provider.saveTokens(tokens);
74+
75+
// Verify all data is stored correctly
76+
expect(provider.clientInformation()).toEqual(clientInfo);
77+
expect(provider.codeVerifier()).toBe('my-code-verifier');
78+
expect(provider.state()).toBe(mockState);
79+
expect(provider.tokens()).toEqual(tokens);
80+
expect(onRedirectMock).toHaveBeenCalledWith(authUrl);
81+
expect(provider.getSavedCallbackServer()).toBe(mockServer);
82+
});
83+
});
84+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
8+
import type {
9+
OAuthClientInformation,
10+
OAuthClientMetadata,
11+
OAuthTokens,
12+
} from '@modelcontextprotocol/sdk/shared/auth.js';
13+
import { debugLogger } from '../utils/debugLogger.js';
14+
15+
/**
16+
* OAuth authorization response.
17+
*/
18+
export interface OAuthAuthorizationResponse {
19+
code: string;
20+
state: string;
21+
}
22+
23+
type CallbackServer = {
24+
port: Promise<number>;
25+
waitForResponse: () => Promise<OAuthAuthorizationResponse>;
26+
close: () => Promise<void>;
27+
};
28+
29+
export class MCPOAuthClientProvider implements OAuthClientProvider {
30+
private _clientInformation?: OAuthClientInformation;
31+
private _tokens?: OAuthTokens;
32+
private _codeVerifier?: string;
33+
private _cbServer?: CallbackServer;
34+
35+
constructor(
36+
private readonly _redirectUrl: string | URL,
37+
private readonly _clientMetadata: OAuthClientMetadata,
38+
private readonly _state?: string | undefined,
39+
private readonly _onRedirect: (url: URL) => void = (url) => {
40+
debugLogger.log(`Redirect to: ${url.toString()}`);
41+
},
42+
) {}
43+
44+
get redirectUrl(): string | URL {
45+
return this._redirectUrl;
46+
}
47+
48+
get clientMetadata(): OAuthClientMetadata {
49+
return this._clientMetadata;
50+
}
51+
52+
saveCallbackServer(server: CallbackServer): void {
53+
this._cbServer = server;
54+
}
55+
56+
getSavedCallbackServer(): CallbackServer | undefined {
57+
return this._cbServer;
58+
}
59+
60+
clientInformation(): OAuthClientInformation | undefined {
61+
return this._clientInformation;
62+
}
63+
64+
saveClientInformation(clientInformation: OAuthClientInformation): void {
65+
this._clientInformation = clientInformation;
66+
}
67+
68+
tokens(): OAuthTokens | undefined {
69+
return this._tokens;
70+
}
71+
72+
saveTokens(tokens: OAuthTokens): void {
73+
this._tokens = tokens;
74+
}
75+
76+
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
77+
this._onRedirect(authorizationUrl);
78+
}
79+
80+
saveCodeVerifier(codeVerifier: string): void {
81+
this._codeVerifier = codeVerifier;
82+
}
83+
84+
codeVerifier(): string {
85+
if (!this._codeVerifier) {
86+
throw new Error('No code verifier saved');
87+
}
88+
return this._codeVerifier;
89+
}
90+
91+
state(): string {
92+
if (!this._state) {
93+
throw new Error('No code state saved');
94+
}
95+
return this._state;
96+
}
97+
}

0 commit comments

Comments
 (0)