diff --git a/docs/tools/mcp-server.md b/docs/tools/mcp-server.md index 685a637cf8..47f169ba38 100644 --- a/docs/tools/mcp-server.md +++ b/docs/tools/mcp-server.md @@ -150,11 +150,6 @@ Each server configuration supports the following properties: server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded. -- **`allow_unscoped_id_tokens_cloud_run`** (boolean): When `true` and the MCP - server host is a Cloud Run service (`*.run.app`), the CLI will use Google - Application Default Credentials (ADC) to generate an unscoped ID token and - send it as `Authorization: Bearer `. When using this flag, do not set - OAuth scopes; they are not needed. - **`targetAudience`** (string): The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with `authProviderType: 'service_account_impersonation'`. @@ -286,26 +281,6 @@ property: } ``` -#### Google Credential with Cloud Run ID tokens - -When connecting to a Cloud Run service endpoint (`*.run.app`), you must opt into -ID token based authentication using ADC. Note that the generated ID token is -unscoped. - -```json -{ - "mcpServers": { - "googleCloudServer": { - "url": "https://my-gcp-service.run.app/sse", - "authProviderType": "google_credentials", - "allow_unscoped_id_tokens_cloud_run": true - } - } -} -``` - -Note: Only `*.run.app` hosts are supported for this flag. - #### Service Account Impersonation To authenticate with a server using Service Account Impersonation, you must set diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 860a166f21..37f5f85641 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -189,8 +189,6 @@ export class MCPServerConfig { // OAuth configuration readonly oauth?: MCPOAuthConfig, readonly authProviderType?: AuthProviderType, - // When true, use Google ADC to fetch ID tokens for Cloud Run - readonly allow_unscoped_id_tokens_cloud_run?: boolean, // Service Account Configuration /* targetAudience format: CLIENT_ID.apps.googleusercontent.com */ readonly targetAudience?: string, diff --git a/packages/core/src/mcp/google-auth-provider.test.ts b/packages/core/src/mcp/google-auth-provider.test.ts index ce86d7a2ab..efe959ff3c 100644 --- a/packages/core/src/mcp/google-auth-provider.test.ts +++ b/packages/core/src/mcp/google-auth-provider.test.ts @@ -20,16 +20,12 @@ describe('GoogleCredentialProvider', () => { }, } as MCPServerConfig; - beforeEach(() => { - vi.clearAllMocks(); - }); - it('should throw an error if no scopes are provided', () => { const config = { url: 'https://test.googleapis.com', } as MCPServerConfig; expect(() => new GoogleCredentialProvider(config)).toThrow( - 'Scopes must be provided in the oauth config for Google Credentials provider (or enable allow_unscoped_id_tokens_for_cloud_run to use ID tokens for Cloud Run endpoints)', + 'Scopes must be provided in the oauth config for Google Credentials provider', ); }); @@ -84,19 +80,7 @@ describe('GoogleCredentialProvider', () => { ); }); - it('should not allow run.app host even when unscoped ID token flag is not present', () => { - const config = { - url: 'https://test.run.app', - oauth: { - scopes: ['scope1', 'scope2'], - }, - } as MCPServerConfig; - expect(() => new GoogleCredentialProvider(config)).toThrow( - 'To enable the Cloud Run MCP Server at https://test.run.app please set allow_unscoped_id_tokens_cloud_run:true in the MCP Server config.', - ); - }); - - describe('with provider instance (Access Tokens)', () => { + describe('with provider instance', () => { let provider: GoogleCredentialProvider; let mockGetAccessToken: Mock; let mockClient: { @@ -170,72 +154,4 @@ describe('GoogleCredentialProvider', () => { vi.useRealTimers(); }); }); - - describe('ID token flow (allow_unscoped_id_tokens_cloud_run)', () => { - let mockFetchIdToken: Mock; - let mockIdClient: { - idTokenProvider: { - fetchIdToken: Mock; - }; - }; - - beforeEach(() => { - mockFetchIdToken = vi.fn(); - mockIdClient = { - idTokenProvider: { - fetchIdToken: mockFetchIdToken, - }, - }; - (GoogleAuth.prototype.getIdTokenClient as Mock).mockResolvedValue( - mockIdClient, - ); - }); - - it('should return ID token when flag is enabled and derive audience from hostname', async () => { - const config = { - url: 'https://test.run.app/path', - allow_unscoped_id_tokens_cloud_run: true, - } as MCPServerConfig; - const payload = { exp: Math.floor(Date.now() / 1000) + 3600 }; - const validToken = `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`; - mockFetchIdToken.mockResolvedValue(validToken); - - const provider = new GoogleCredentialProvider(config); - const tokens = await provider.tokens(); - expect(tokens?.access_token).toBe(validToken); - expect(GoogleAuth.prototype.getIdTokenClient).toHaveBeenCalledWith( - 'test.run.app', - ); - expect(mockFetchIdToken).toHaveBeenCalledWith('test.run.app'); - }); - - it('should return undefined and log error when fetching ID token fails', async () => { - const config = { - url: 'https://test.run.app/path', - allow_unscoped_id_tokens_cloud_run: true, - } as MCPServerConfig; - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - mockFetchIdToken.mockRejectedValue(new Error('Fetch failed')); - - const provider = new GoogleCredentialProvider(config); - const tokens = await provider.tokens(); - expect(tokens).toBeUndefined(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to get ID token from Google ADC', - expect.any(Error), - ); - consoleErrorSpy.mockRestore(); - }); - - it('should not require scopes when flag allow_unscoped_id_tokens_cloud_run is true', () => { - const config = { - url: 'https://test.run.app', - allow_unscoped_id_tokens_cloud_run: true, - } as MCPServerConfig; - - expect(() => new GoogleCredentialProvider(config)).not.toThrow(); - }); - }); }); diff --git a/packages/core/src/mcp/google-auth-provider.ts b/packages/core/src/mcp/google-auth-provider.ts index 3159798095..d152b4d256 100644 --- a/packages/core/src/mcp/google-auth-provider.ts +++ b/packages/core/src/mcp/google-auth-provider.ts @@ -13,17 +13,12 @@ import type { } from '@modelcontextprotocol/sdk/shared/auth.js'; import { GoogleAuth } from 'google-auth-library'; import type { MCPServerConfig } from '../config/config.js'; -import { OAuthUtils, FIVE_MIN_BUFFER_MS } from './oauth-utils.js'; +import { FIVE_MIN_BUFFER_MS } from './oauth-utils.js'; -const CLOUD_RUN_HOST_REGEX = /^(.*\.)?run\.app$/; - -// An array of hosts that are allowed to use the Google Credential provider. const ALLOWED_HOSTS = [/^.+\.googleapis\.com$/, /^(.*\.)?luci\.app$/]; export class GoogleCredentialProvider implements OAuthClientProvider { private readonly auth: GoogleAuth; - private readonly useIdToken: boolean = false; - private readonly audience?: string; private cachedToken?: OAuthTokens; private tokenExpiryTime?: number; @@ -47,35 +42,20 @@ export class GoogleCredentialProvider implements OAuthClientProvider { } const hostname = new URL(url).hostname; - const isRunAppHost = CLOUD_RUN_HOST_REGEX.test(hostname); - if (!this.config?.allow_unscoped_id_tokens_cloud_run && isRunAppHost) { - throw new Error( - `To enable the Cloud Run MCP Server at ${url} please set allow_unscoped_id_tokens_cloud_run:true in the MCP Server config.`, - ); - } - if (this.config?.allow_unscoped_id_tokens_cloud_run && isRunAppHost) { - this.useIdToken = true; - } - this.audience = hostname; - - if ( - !this.useIdToken && - !ALLOWED_HOSTS.some((pattern) => pattern.test(hostname)) - ) { + if (!ALLOWED_HOSTS.some((pattern) => pattern.test(hostname))) { throw new Error( `Host "${hostname}" is not an allowed host for Google Credential provider.`, ); } - // If we are using the access token flow, we MUST have scopes. - if (!this.useIdToken && !this.config?.oauth?.scopes) { + 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 (or enable allow_unscoped_id_tokens_for_cloud_run to use ID tokens for Cloud Run endpoints)', + 'Scopes must be provided in the oauth config for Google Credentials provider', ); } - this.auth = new GoogleAuth({ - scopes: this.config?.oauth?.scopes, + scopes, }); } @@ -101,31 +81,6 @@ export class GoogleCredentialProvider implements OAuthClientProvider { this.cachedToken = undefined; this.tokenExpiryTime = undefined; - // If allow_unscoped_id_tokens_for_cloud_run is configured, use ID tokens. - if (this.useIdToken) { - try { - const idClient = await this.auth.getIdTokenClient(this.audience!); - const idToken = await idClient.idTokenProvider.fetchIdToken( - this.audience!, - ); - - const newToken: OAuthTokens = { - access_token: idToken, - token_type: 'Bearer', - }; - - const expiryTime = OAuthUtils.parseTokenExpiry(idToken); - if (expiryTime) { - this.tokenExpiryTime = expiryTime; - this.cachedToken = newToken; - } - return newToken; - } catch (e) { - console.error('Failed to get ID token from Google ADC', e); - return undefined; - } - } - const client = await this.auth.getClient(); const accessTokenResponse = await client.getAccessToken();