Skip to content
Merged
58 changes: 41 additions & 17 deletions packages/core/src/tools/mcp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1777,41 +1777,65 @@ describe('mcp-client', () => {

describe('createTransport', () => {
describe('should connect via httpUrl', () => {
it('without headers', async () => {
it('uses MCP SDK authProvider token() path for oauth-enabled servers', async () => {
const mockGetValidToken = vi.fn().mockResolvedValue('fresh-token');
vi.mocked(MCPOAuthProvider).mockReturnValue({
getValidToken: mockGetValidToken,
} as unknown as MCPOAuthProvider);

vi.mocked(MCPOAuthTokenStorage).mockReturnValue({
getCredentials: vi.fn().mockResolvedValue({ clientId: 'cid' }),
} as unknown as MCPOAuthTokenStorage);

const transport = await createTransport(
'test-server',
{
httpUrl: 'http://test-server',
url: 'http://test-server',
type: 'http',
oauth: { enabled: true },
},
false,
MOCK_CONTEXT,
);

expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
expect(transport).toMatchObject({
_url: new URL('http://test-server'),
_requestInit: { headers: {} },
});
const testableTransport = transport as unknown as {
_authProvider?: { token: () => Promise<string | undefined> };
};

expect(testableTransport._authProvider).toBeDefined();
const token = await testableTransport._authProvider!.token();
expect(token).toBe('fresh-token');
});

it('with headers', async () => {
it('uses dynamic authProvider when stored OAuth token exists', async () => {
const mockGetValidToken = vi
.fn()
.mockResolvedValue('stored-fresh-token');
vi.mocked(MCPOAuthProvider).mockReturnValue({
getValidToken: mockGetValidToken,
} as unknown as MCPOAuthProvider);

vi.mocked(MCPOAuthTokenStorage).mockReturnValue({
getCredentials: vi.fn().mockResolvedValue({ clientId: 'cid' }),
} as unknown as MCPOAuthTokenStorage);

const transport = await createTransport(
'test-server',
{
httpUrl: 'http://test-server',
headers: { Authorization: 'derp' },
url: 'http://test-server',
type: 'http',
},
false,
MOCK_CONTEXT,
);

expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
expect(transport).toMatchObject({
_url: new URL('http://test-server'),
_requestInit: {
headers: { Authorization: 'derp' },
},
});
const testableTransport = transport as unknown as {
_authProvider?: { token: () => Promise<string | undefined> };
};

expect(testableTransport._authProvider).toBeDefined();
const token = await testableTransport._authProvider!.token();
expect(token).toBe('stored-fresh-token');
});
});

Expand Down
92 changes: 83 additions & 9 deletions packages/core/src/tools/mcp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ import {
type EnvironmentSanitizationConfig,
} from '../services/environmentSanitization.js';
import { expandEnvVars } from '../utils/envExpansion.js';
import type {
OAuthClientInformation,
OAuthClientMetadata,
OAuthTokens,
} from '@modelcontextprotocol/sdk/shared/auth.js';

import {
GEMINI_CLI_IDENTIFICATION_ENV_VAR,
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
Expand Down Expand Up @@ -1026,6 +1032,67 @@ function createAuthProvider(
return undefined;
}

class DynamicStoredOAuthProvider implements McpAuthProvider {
readonly redirectUrl = '';
readonly clientMetadata: OAuthClientMetadata = {
client_name: 'Gemini CLI (Stored OAuth)',
redirect_uris: [],
grant_types: [],
response_types: [],
token_endpoint_auth_method: 'none',
};

private clientInfo?: OAuthClientInformation;

constructor(
private readonly serverName: string,
private readonly serverConfig: MCPServerConfig,
) {}

clientInformation(): OAuthClientInformation | undefined {
return this.clientInfo;
}

saveClientInformation(clientInformation: OAuthClientInformation): void {
this.clientInfo = clientInformation;
}

async tokens(): Promise<OAuthTokens | undefined> {
if (this.serverConfig.oauth?.enabled && this.serverConfig.oauth) {
const tokenStorage = new MCPOAuthTokenStorage();
const mcpAuthProvider = new MCPOAuthProvider(tokenStorage);
const token = await mcpAuthProvider.getValidToken(
this.serverName,
this.serverConfig.oauth,
);
if (!token) return undefined;
return { access_token: token, token_type: 'Bearer' };
}

const token = await getStoredOAuthToken(this.serverName);
if (!token) return undefined;
return { access_token: token, token_type: 'Bearer' };
}

saveTokens(_tokens: OAuthTokens): void {}
redirectToAuthorization(_authorizationUrl: URL): void {}
saveCodeVerifier(_codeVerifier: string): void {}
codeVerifier(): string {
return '';
}
}
Comment thread
sahilkirad marked this conversation as resolved.
Resolved

/**
* Creates an OAuth token provider for transports so token lookup/refresh happens
* at request/auth time instead of freezing a single token at transport creation.
*/
function createDynamicOAuthTokenProvider(
mcpServerName: string,
mcpServerConfig: MCPServerConfig,
): McpAuthProvider {
return new DynamicStoredOAuthProvider(mcpServerName, mcpServerConfig);
}

/**
* Create a transport with OAuth token for the given server configuration.
*
Expand Down Expand Up @@ -2189,17 +2256,18 @@ export async function createTransport(
}
}
if (mcpServerConfig.httpUrl || mcpServerConfig.url) {
const authProvider = createAuthProvider(mcpServerConfig);
let authProvider = createAuthProvider(mcpServerConfig);
const headers: Record<string, string> =
(await authProvider?.getRequestHeaders?.()) ?? {};

if (authProvider === undefined) {
// Check if we have OAuth configuration or stored tokens
let accessToken: string | null = null;
let shouldUseDynamicOAuthProvider = false;

if (mcpServerConfig.oauth?.enabled && mcpServerConfig.oauth) {
shouldUseDynamicOAuthProvider = true;
const tokenStorage = new MCPOAuthTokenStorage();
const mcpAuthProvider = new MCPOAuthProvider(tokenStorage);
accessToken = await mcpAuthProvider.getValidToken(
const accessToken = await mcpAuthProvider.getValidToken(
mcpServerName,
mcpServerConfig.oauth,
);
Expand All @@ -2214,16 +2282,22 @@ export async function createTransport(
);
}
} else {
// Check if we have stored OAuth tokens for this server (from previous authentication)
accessToken = await getStoredOAuthToken(mcpServerName);
if (accessToken) {
// Stored OAuth credentials imply we should use dynamic token retrieval.
const tokenStorage = new MCPOAuthTokenStorage();
const credentials = await tokenStorage.getCredentials(mcpServerName);
shouldUseDynamicOAuthProvider = !!credentials;
if (shouldUseDynamicOAuthProvider) {
debugLogger.log(
`Found stored OAuth token for server '${mcpServerName}'`,
);
}
}
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;

if (shouldUseDynamicOAuthProvider) {
authProvider = createDynamicOAuthTokenProvider(
mcpServerName,
mcpServerConfig,
);
}
}

Expand Down
Loading