diff --git a/packages/cli/src/ui/commands/mcpCommand.test.ts b/packages/cli/src/ui/commands/mcpCommand.test.ts index bcd27f4d8d..857ed71545 100644 --- a/packages/cli/src/ui/commands/mcpCommand.test.ts +++ b/packages/cli/src/ui/commands/mcpCommand.test.ts @@ -904,6 +904,7 @@ describe('mcpCommand', () => { 'test-server', { enabled: true }, 'http://localhost:3000', + expect.any(Object), ); expect(mockToolRegistry.discoverToolsForServer).toHaveBeenCalledWith( 'test-server', diff --git a/packages/cli/src/ui/commands/mcpCommand.ts b/packages/cli/src/ui/commands/mcpCommand.ts index 79f6bd3951..abf5134ee7 100644 --- a/packages/cli/src/ui/commands/mcpCommand.ts +++ b/packages/cli/src/ui/commands/mcpCommand.ts @@ -22,6 +22,7 @@ import { getErrorMessage, MCPOAuthTokenStorage, } from '@google/gemini-cli-core'; +import { appEvents, AppEvent } from '../../utils/events.js'; const COLOR_GREEN = '\u001b[32m'; const COLOR_YELLOW = '\u001b[33m'; @@ -368,6 +369,12 @@ const authCommand: SlashCommand = { // Always attempt OAuth authentication, even if not explicitly configured // The authentication process will discover OAuth requirements automatically + const displayListener = (message: string) => { + context.ui.addItem({ type: 'info', text: message }, Date.now()); + }; + + appEvents.on(AppEvent.OauthDisplayMessage, displayListener); + try { context.ui.addItem( { @@ -385,10 +392,14 @@ const authCommand: SlashCommand = { oauthConfig = { enabled: false }; } - // Pass the MCP server URL for OAuth discovery const mcpServerUrl = server.httpUrl || server.url; const authProvider = new MCPOAuthProvider(new MCPOAuthTokenStorage()); - await authProvider.authenticate(serverName, oauthConfig, mcpServerUrl); + await authProvider.authenticate( + serverName, + oauthConfig, + mcpServerUrl, + appEvents, + ); context.ui.addItem( { @@ -430,6 +441,8 @@ const authCommand: SlashCommand = { messageType: 'error', content: `Failed to authenticate with MCP server '${serverName}': ${getErrorMessage(error)}`, }; + } finally { + appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener); } }, completion: async (context: CommandContext, partialArg: string) => { diff --git a/packages/cli/src/utils/events.ts b/packages/cli/src/utils/events.ts index 735967f88b..c29f740bd3 100644 --- a/packages/cli/src/utils/events.ts +++ b/packages/cli/src/utils/events.ts @@ -9,6 +9,7 @@ import { EventEmitter } from 'node:events'; export enum AppEvent { OpenDebugConsole = 'open-debug-console', LogError = 'log-error', + OauthDisplayMessage = 'oauth-display-message', } export const appEvents = new EventEmitter(); diff --git a/packages/core/src/mcp/oauth-provider.ts b/packages/core/src/mcp/oauth-provider.ts index 3f116b7d7e..645f44fa02 100644 --- a/packages/core/src/mcp/oauth-provider.ts +++ b/packages/core/src/mcp/oauth-provider.ts @@ -7,12 +7,15 @@ import * as http from 'node:http'; import * as crypto from 'node:crypto'; import { URL } from 'node:url'; +import type { EventEmitter } from 'node:events'; import { openBrowserSecurely } from '../utils/secure-browser-launcher.js'; import type { OAuthToken } from './token-storage/types.js'; import { MCPOAuthTokenStorage } from './oauth-token-storage.js'; import { getErrorMessage } from '../utils/errors.js'; import { OAuthUtils } from './oauth-utils.js'; +export const OAUTH_DISPLAY_MESSAGE_EVENT = 'oauth-display-message' as const; + /** * OAuth configuration for an MCP server. */ @@ -575,18 +578,28 @@ export class MCPOAuthProvider { * @param serverName The name of the MCP server * @param config OAuth configuration * @param mcpServerUrl Optional MCP server URL for OAuth discovery + * @param messageHandler Optional handler for displaying user-facing messages * @returns The obtained OAuth token */ async authenticate( serverName: string, config: MCPOAuthConfig, mcpServerUrl?: string, + events?: EventEmitter, ): Promise { + // Helper function to display messages through handler or fallback to console.log + const displayMessage = (message: string) => { + if (events) { + events.emit(OAUTH_DISPLAY_MESSAGE_EVENT, message); + } else { + console.log(message); + } + }; + // If no authorization URL is provided, try to discover OAuth configuration if (!config.authorizationUrl && mcpServerUrl) { - console.log( - 'No authorization URL provided, attempting OAuth discovery...', - ); + console.debug(`Starting OAuth for MCP server "${serverName}"… +✓ No authorization URL; using OAuth discovery`); // First check if the server requires authentication via WWW-Authenticate header try { @@ -662,9 +675,7 @@ export class MCPOAuthProvider { const authUrl = new URL(config.authorizationUrl); const serverUrl = `${authUrl.protocol}//${authUrl.host}`; - console.log( - 'No client ID provided, attempting dynamic client registration...', - ); + console.debug('→ Attempting dynamic client registration...'); // Get the authorization server metadata for registration const authServerMetadataUrl = new URL( @@ -694,7 +705,7 @@ export class MCPOAuthProvider { config.clientSecret = clientRegistration.client_secret; } - console.log('Dynamic client registration successful'); + console.debug('✓ Dynamic client registration successful'); } else { throw new Error( 'No client ID provided and dynamic registration not supported', @@ -719,30 +730,13 @@ export class MCPOAuthProvider { mcpServerUrl, ); - console.log('\nOpening browser for OAuth authentication...'); - console.log('If the browser does not open, please visit:'); - console.log(''); + displayMessage(`→ Opening your browser for OAuth sign-in... - // Get terminal width or default to 80 - const terminalWidth = process.stdout.columns || 80; - const separatorLength = Math.min(terminalWidth - 2, 80); - const separator = '━'.repeat(separatorLength); +If the browser does not open, copy and paste this URL into your browser: +${authUrl} - console.log(separator); - console.log( - 'COPY THE ENTIRE URL BELOW (select all text between the lines):', - ); - console.log(separator); - console.log(authUrl); - console.log(separator); - console.log(''); - console.log( - '💡 TIP: Triple-click to select the entire URL, then copy and paste it into your browser.', - ); - console.log( - '⚠️ Make sure to copy the COMPLETE URL - it may wrap across multiple lines.', - ); - console.log(''); +💡 TIP: Triple-click to select the entire URL, then copy and paste it into your browser. +⚠️ Make sure to copy the COMPLETE URL - it may wrap across multiple lines.`); // Start callback server const callbackPromise = this.startCallbackServer(pkceParams.state); @@ -760,7 +754,7 @@ export class MCPOAuthProvider { // Wait for callback const { code } = await callbackPromise; - console.log('\nAuthorization code received, exchanging for tokens...'); + console.debug('✓ Authorization code received, exchanging for tokens...'); // Exchange code for tokens const tokenResponse = await this.exchangeCodeForToken( @@ -795,16 +789,20 @@ export class MCPOAuthProvider { config.tokenUrl, mcpServerUrl, ); - console.log('Authentication successful! Token saved.'); + console.debug('✓ Authentication successful! Token saved.'); // Verify token was saved const savedToken = await this.tokenStorage.getCredentials(serverName); if (savedToken && savedToken.token && savedToken.token.accessToken) { - const tokenPreview = - savedToken.token.accessToken.length > 20 - ? `${savedToken.token.accessToken.substring(0, 20)}...` - : '[token]'; - console.log(`Token verification successful: ${tokenPreview}`); + // Avoid leaking token material; log a short SHA-256 fingerprint instead. + const tokenFingerprint = crypto + .createHash('sha256') + .update(savedToken.token.accessToken) + .digest('hex') + .slice(0, 8); + console.debug( + `✓ Token verification successful (fingerprint: ${tokenFingerprint})`, + ); } else { console.error( 'Token verification failed: token not found or invalid after save',