Add Google credentials provider for authenticating with MCP servers (#4748)
This commit is contained in:
parent
3dd6e431df
commit
d254d4ce00
|
@ -179,6 +179,28 @@ OAuth tokens are automatically:
|
|||
- **Validated** before each connection attempt
|
||||
- **Cleaned up** when invalid or expired
|
||||
|
||||
#### Authentication Provider Type
|
||||
|
||||
You can specify the authentication provider type using the `authProviderType` property:
|
||||
|
||||
- **`authProviderType`** (string): Specifies the authentication provider. Can be one of the following:
|
||||
- **`dynamic_discovery`** (default): The CLI will automatically discover the OAuth configuration from the server.
|
||||
- **`google_credentials`**: The CLI will use the Google Application Default Credentials (ADC) to authenticate with the server. When using this provider, you must specify the required scopes.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"googleCloudServer": {
|
||||
"httpUrl": "https://my-gcp-service.run.app/mcp",
|
||||
"authProviderType": "google_credentials",
|
||||
"oauth": {
|
||||
"scopes": ["https://www.googleapis.com/auth/userinfo.email"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Configurations
|
||||
|
||||
#### Python MCP Server (Stdio)
|
||||
|
|
|
@ -118,9 +118,15 @@ export class MCPServerConfig {
|
|||
readonly extensionName?: string,
|
||||
// OAuth configuration
|
||||
readonly oauth?: MCPOAuthConfig,
|
||||
readonly authProviderType?: AuthProviderType,
|
||||
) {}
|
||||
}
|
||||
|
||||
export enum AuthProviderType {
|
||||
DYNAMIC_DISCOVERY = 'dynamic_discovery',
|
||||
GOOGLE_CREDENTIALS = 'google_credentials',
|
||||
}
|
||||
|
||||
export interface SandboxConfig {
|
||||
command: 'docker' | 'podman' | 'sandbox-exec';
|
||||
image: string;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
import { GoogleCredentialProvider } from './google-auth-provider.js';
|
||||
import { vi, describe, beforeEach, it, expect, Mock } from 'vitest';
|
||||
import { MCPServerConfig } from '../config/config.js';
|
||||
|
||||
vi.mock('google-auth-library');
|
||||
|
||||
describe('GoogleCredentialProvider', () => {
|
||||
it('should throw an error if no scopes are provided', () => {
|
||||
expect(() => new GoogleCredentialProvider()).toThrow(
|
||||
'Scopes must be provided in the oauth config for Google Credentials provider',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use scopes from the config if provided', () => {
|
||||
const config = {
|
||||
oauth: {
|
||||
scopes: ['scope1', 'scope2'],
|
||||
},
|
||||
} as MCPServerConfig;
|
||||
new GoogleCredentialProvider(config);
|
||||
expect(GoogleAuth).toHaveBeenCalledWith({
|
||||
scopes: ['scope1', 'scope2'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('with provider instance', () => {
|
||||
let provider: GoogleCredentialProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
const config = {
|
||||
oauth: {
|
||||
scopes: ['scope1', 'scope2'],
|
||||
},
|
||||
} as MCPServerConfig;
|
||||
provider = new GoogleCredentialProvider(config);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return credentials', async () => {
|
||||
const mockClient = {
|
||||
getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
|
||||
};
|
||||
(GoogleAuth.prototype.getClient as Mock).mockResolvedValue(mockClient);
|
||||
|
||||
const credentials = await provider.tokens();
|
||||
|
||||
expect(credentials?.access_token).toBe('test-token');
|
||||
});
|
||||
|
||||
it('should return undefined if access token is not available', async () => {
|
||||
const mockClient = {
|
||||
getAccessToken: vi.fn().mockResolvedValue({ token: null }),
|
||||
};
|
||||
(GoogleAuth.prototype.getClient as Mock).mockResolvedValue(mockClient);
|
||||
|
||||
const credentials = await provider.tokens();
|
||||
expect(credentials).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
||||
import {
|
||||
OAuthClientInformation,
|
||||
OAuthClientInformationFull,
|
||||
OAuthClientMetadata,
|
||||
OAuthTokens,
|
||||
} from '@modelcontextprotocol/sdk/shared/auth.js';
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
import { MCPServerConfig } from '../config/config.js';
|
||||
|
||||
export class GoogleCredentialProvider implements OAuthClientProvider {
|
||||
private readonly auth: GoogleAuth;
|
||||
|
||||
// Properties required by OAuthClientProvider, with no-op values
|
||||
readonly redirectUrl = '';
|
||||
readonly clientMetadata: OAuthClientMetadata = {
|
||||
client_name: 'Gemini CLI (Google ADC)',
|
||||
redirect_uris: [],
|
||||
grant_types: [],
|
||||
response_types: [],
|
||||
token_endpoint_auth_method: 'none',
|
||||
};
|
||||
private _clientInformation?: OAuthClientInformationFull;
|
||||
|
||||
constructor(private readonly config?: MCPServerConfig) {
|
||||
const scopes = this.config?.oauth?.scopes;
|
||||
if (!scopes || scopes.length === 0) {
|
||||
throw new Error(
|
||||
'Scopes must be provided in the oauth config for Google Credentials provider',
|
||||
);
|
||||
}
|
||||
this.auth = new GoogleAuth({
|
||||
scopes,
|
||||
});
|
||||
}
|
||||
|
||||
clientInformation(): OAuthClientInformation | undefined {
|
||||
return this._clientInformation;
|
||||
}
|
||||
|
||||
saveClientInformation(clientInformation: OAuthClientInformationFull): void {
|
||||
this._clientInformation = clientInformation;
|
||||
}
|
||||
|
||||
async tokens(): Promise<OAuthTokens | undefined> {
|
||||
const client = await this.auth.getClient();
|
||||
const accessTokenResponse = await client.getAccessToken();
|
||||
|
||||
if (!accessTokenResponse.token) {
|
||||
console.error('Failed to get access token from Google ADC');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tokens: OAuthTokens = {
|
||||
access_token: accessTokenResponse.token,
|
||||
token_type: 'Bearer',
|
||||
};
|
||||
return tokens;
|
||||
}
|
||||
|
||||
saveTokens(_tokens: OAuthTokens): void {
|
||||
// No-op, ADC manages tokens.
|
||||
}
|
||||
|
||||
redirectToAuthorization(_authorizationUrl: URL): void {
|
||||
// No-op
|
||||
}
|
||||
|
||||
saveCodeVerifier(_codeVerifier: string): void {
|
||||
// No-op
|
||||
}
|
||||
|
||||
codeVerifier(): string {
|
||||
// No-op
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|||
import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import * as ClientLib from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import * as GenAiLib from '@google/genai';
|
||||
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
||||
import { AuthProviderType } from '../config/config.js';
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/client/stdio.js');
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js');
|
||||
|
@ -173,6 +175,63 @@ describe('mcp-client', () => {
|
|||
stderr: 'pipe',
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGoogleCredentialProvider', () => {
|
||||
it('should use GoogleCredentialProvider when specified', async () => {
|
||||
const transport = await createTransport(
|
||||
'test-server',
|
||||
{
|
||||
httpUrl: 'http://test-server',
|
||||
authProviderType: AuthProviderType.GOOGLE_CREDENTIALS,
|
||||
oauth: {
|
||||
scopes: ['scope1'],
|
||||
},
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const authProvider = (transport as any)._authProvider;
|
||||
expect(authProvider).toBeInstanceOf(GoogleCredentialProvider);
|
||||
});
|
||||
|
||||
it('should use GoogleCredentialProvider with SSE transport', async () => {
|
||||
const transport = await createTransport(
|
||||
'test-server',
|
||||
{
|
||||
url: 'http://test-server',
|
||||
authProviderType: AuthProviderType.GOOGLE_CREDENTIALS,
|
||||
oauth: {
|
||||
scopes: ['scope1'],
|
||||
},
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
expect(transport).toBeInstanceOf(SSEClientTransport);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const authProvider = (transport as any)._authProvider;
|
||||
expect(authProvider).toBeInstanceOf(GoogleCredentialProvider);
|
||||
});
|
||||
|
||||
it('should throw an error if no URL is provided with GoogleCredentialProvider', async () => {
|
||||
await expect(
|
||||
createTransport(
|
||||
'test-server',
|
||||
{
|
||||
authProviderType: AuthProviderType.GOOGLE_CREDENTIALS,
|
||||
oauth: {
|
||||
scopes: ['scope1'],
|
||||
},
|
||||
},
|
||||
false,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'No URL configured for Google Credentials MCP server',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('isEnabled', () => {
|
||||
const funcDecl = { name: 'myTool' };
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
StreamableHTTPClientTransportOptions,
|
||||
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { parse } from 'shell-quote';
|
||||
import { MCPServerConfig } from '../config/config.js';
|
||||
import { AuthProviderType, MCPServerConfig } from '../config/config.js';
|
||||
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
||||
import { DiscoveredMCPTool } from './mcp-tool.js';
|
||||
import { FunctionDeclaration, mcpToTool } from '@google/genai';
|
||||
import { ToolRegistry } from './tool-registry.js';
|
||||
|
@ -852,6 +853,29 @@ export async function createTransport(
|
|||
mcpServerConfig: MCPServerConfig,
|
||||
debugMode: boolean,
|
||||
): Promise<Transport> {
|
||||
if (
|
||||
mcpServerConfig.authProviderType === AuthProviderType.GOOGLE_CREDENTIALS
|
||||
) {
|
||||
const provider = new GoogleCredentialProvider(mcpServerConfig);
|
||||
const transportOptions:
|
||||
| StreamableHTTPClientTransportOptions
|
||||
| SSEClientTransportOptions = {
|
||||
authProvider: provider,
|
||||
};
|
||||
if (mcpServerConfig.httpUrl) {
|
||||
return new StreamableHTTPClientTransport(
|
||||
new URL(mcpServerConfig.httpUrl),
|
||||
transportOptions,
|
||||
);
|
||||
} else if (mcpServerConfig.url) {
|
||||
return new SSEClientTransport(
|
||||
new URL(mcpServerConfig.url),
|
||||
transportOptions,
|
||||
);
|
||||
}
|
||||
throw new Error('No URL configured for Google Credentials MCP server');
|
||||
}
|
||||
|
||||
// Check if we have OAuth configuration or stored tokens
|
||||
let accessToken: string | null = null;
|
||||
let hasOAuthConfig = mcpServerConfig.oauth?.enabled;
|
||||
|
|
Loading…
Reference in New Issue