mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
fix(mcp): Display OAuth authentication messages in CLI UI instead of debug console (#6919)
Co-authored-by: Yoichiro Tanaka <yoichiro6642@gmail.com> Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -904,6 +904,7 @@ describe('mcpCommand', () => {
|
|||||||
'test-server',
|
'test-server',
|
||||||
{ enabled: true },
|
{ enabled: true },
|
||||||
'http://localhost:3000',
|
'http://localhost:3000',
|
||||||
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
expect(mockToolRegistry.discoverToolsForServer).toHaveBeenCalledWith(
|
expect(mockToolRegistry.discoverToolsForServer).toHaveBeenCalledWith(
|
||||||
'test-server',
|
'test-server',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
MCPOAuthTokenStorage,
|
MCPOAuthTokenStorage,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
import { appEvents, AppEvent } from '../../utils/events.js';
|
||||||
|
|
||||||
const COLOR_GREEN = '\u001b[32m';
|
const COLOR_GREEN = '\u001b[32m';
|
||||||
const COLOR_YELLOW = '\u001b[33m';
|
const COLOR_YELLOW = '\u001b[33m';
|
||||||
@@ -368,6 +369,12 @@ const authCommand: SlashCommand = {
|
|||||||
// Always attempt OAuth authentication, even if not explicitly configured
|
// Always attempt OAuth authentication, even if not explicitly configured
|
||||||
// The authentication process will discover OAuth requirements automatically
|
// 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 {
|
try {
|
||||||
context.ui.addItem(
|
context.ui.addItem(
|
||||||
{
|
{
|
||||||
@@ -385,10 +392,14 @@ const authCommand: SlashCommand = {
|
|||||||
oauthConfig = { enabled: false };
|
oauthConfig = { enabled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the MCP server URL for OAuth discovery
|
|
||||||
const mcpServerUrl = server.httpUrl || server.url;
|
const mcpServerUrl = server.httpUrl || server.url;
|
||||||
const authProvider = new MCPOAuthProvider(new MCPOAuthTokenStorage());
|
const authProvider = new MCPOAuthProvider(new MCPOAuthTokenStorage());
|
||||||
await authProvider.authenticate(serverName, oauthConfig, mcpServerUrl);
|
await authProvider.authenticate(
|
||||||
|
serverName,
|
||||||
|
oauthConfig,
|
||||||
|
mcpServerUrl,
|
||||||
|
appEvents,
|
||||||
|
);
|
||||||
|
|
||||||
context.ui.addItem(
|
context.ui.addItem(
|
||||||
{
|
{
|
||||||
@@ -430,6 +441,8 @@ const authCommand: SlashCommand = {
|
|||||||
messageType: 'error',
|
messageType: 'error',
|
||||||
content: `Failed to authenticate with MCP server '${serverName}': ${getErrorMessage(error)}`,
|
content: `Failed to authenticate with MCP server '${serverName}': ${getErrorMessage(error)}`,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
completion: async (context: CommandContext, partialArg: string) => {
|
completion: async (context: CommandContext, partialArg: string) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { EventEmitter } from 'node:events';
|
|||||||
export enum AppEvent {
|
export enum AppEvent {
|
||||||
OpenDebugConsole = 'open-debug-console',
|
OpenDebugConsole = 'open-debug-console',
|
||||||
LogError = 'log-error',
|
LogError = 'log-error',
|
||||||
|
OauthDisplayMessage = 'oauth-display-message',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appEvents = new EventEmitter();
|
export const appEvents = new EventEmitter();
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
import type { EventEmitter } from 'node:events';
|
||||||
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
|
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
|
||||||
import type { OAuthToken } from './token-storage/types.js';
|
import type { OAuthToken } from './token-storage/types.js';
|
||||||
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
|
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import { OAuthUtils } from './oauth-utils.js';
|
import { OAuthUtils } from './oauth-utils.js';
|
||||||
|
|
||||||
|
export const OAUTH_DISPLAY_MESSAGE_EVENT = 'oauth-display-message' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth configuration for an MCP server.
|
* OAuth configuration for an MCP server.
|
||||||
*/
|
*/
|
||||||
@@ -575,18 +578,28 @@ export class MCPOAuthProvider {
|
|||||||
* @param serverName The name of the MCP server
|
* @param serverName The name of the MCP server
|
||||||
* @param config OAuth configuration
|
* @param config OAuth configuration
|
||||||
* @param mcpServerUrl Optional MCP server URL for OAuth discovery
|
* @param mcpServerUrl Optional MCP server URL for OAuth discovery
|
||||||
|
* @param messageHandler Optional handler for displaying user-facing messages
|
||||||
* @returns The obtained OAuth token
|
* @returns The obtained OAuth token
|
||||||
*/
|
*/
|
||||||
async authenticate(
|
async authenticate(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
config: MCPOAuthConfig,
|
config: MCPOAuthConfig,
|
||||||
mcpServerUrl?: string,
|
mcpServerUrl?: string,
|
||||||
|
events?: EventEmitter,
|
||||||
): Promise<OAuthToken> {
|
): Promise<OAuthToken> {
|
||||||
|
// 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 no authorization URL is provided, try to discover OAuth configuration
|
||||||
if (!config.authorizationUrl && mcpServerUrl) {
|
if (!config.authorizationUrl && mcpServerUrl) {
|
||||||
console.log(
|
console.debug(`Starting OAuth for MCP server "${serverName}"…
|
||||||
'No authorization URL provided, attempting OAuth discovery...',
|
✓ No authorization URL; using OAuth discovery`);
|
||||||
);
|
|
||||||
|
|
||||||
// First check if the server requires authentication via WWW-Authenticate header
|
// First check if the server requires authentication via WWW-Authenticate header
|
||||||
try {
|
try {
|
||||||
@@ -662,9 +675,7 @@ export class MCPOAuthProvider {
|
|||||||
const authUrl = new URL(config.authorizationUrl);
|
const authUrl = new URL(config.authorizationUrl);
|
||||||
const serverUrl = `${authUrl.protocol}//${authUrl.host}`;
|
const serverUrl = `${authUrl.protocol}//${authUrl.host}`;
|
||||||
|
|
||||||
console.log(
|
console.debug('→ Attempting dynamic client registration...');
|
||||||
'No client ID provided, attempting dynamic client registration...',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the authorization server metadata for registration
|
// Get the authorization server metadata for registration
|
||||||
const authServerMetadataUrl = new URL(
|
const authServerMetadataUrl = new URL(
|
||||||
@@ -694,7 +705,7 @@ export class MCPOAuthProvider {
|
|||||||
config.clientSecret = clientRegistration.client_secret;
|
config.clientSecret = clientRegistration.client_secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Dynamic client registration successful');
|
console.debug('✓ Dynamic client registration successful');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No client ID provided and dynamic registration not supported',
|
'No client ID provided and dynamic registration not supported',
|
||||||
@@ -719,30 +730,13 @@ export class MCPOAuthProvider {
|
|||||||
mcpServerUrl,
|
mcpServerUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('\nOpening browser for OAuth authentication...');
|
displayMessage(`→ Opening your browser for OAuth sign-in...
|
||||||
console.log('If the browser does not open, please visit:');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Get terminal width or default to 80
|
If the browser does not open, copy and paste this URL into your browser:
|
||||||
const terminalWidth = process.stdout.columns || 80;
|
${authUrl}
|
||||||
const separatorLength = Math.min(terminalWidth - 2, 80);
|
|
||||||
const separator = '━'.repeat(separatorLength);
|
|
||||||
|
|
||||||
console.log(separator);
|
💡 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.`);
|
||||||
'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('');
|
|
||||||
|
|
||||||
// Start callback server
|
// Start callback server
|
||||||
const callbackPromise = this.startCallbackServer(pkceParams.state);
|
const callbackPromise = this.startCallbackServer(pkceParams.state);
|
||||||
@@ -760,7 +754,7 @@ export class MCPOAuthProvider {
|
|||||||
// Wait for callback
|
// Wait for callback
|
||||||
const { code } = await callbackPromise;
|
const { code } = await callbackPromise;
|
||||||
|
|
||||||
console.log('\nAuthorization code received, exchanging for tokens...');
|
console.debug('✓ Authorization code received, exchanging for tokens...');
|
||||||
|
|
||||||
// Exchange code for tokens
|
// Exchange code for tokens
|
||||||
const tokenResponse = await this.exchangeCodeForToken(
|
const tokenResponse = await this.exchangeCodeForToken(
|
||||||
@@ -795,16 +789,20 @@ export class MCPOAuthProvider {
|
|||||||
config.tokenUrl,
|
config.tokenUrl,
|
||||||
mcpServerUrl,
|
mcpServerUrl,
|
||||||
);
|
);
|
||||||
console.log('Authentication successful! Token saved.');
|
console.debug('✓ Authentication successful! Token saved.');
|
||||||
|
|
||||||
// Verify token was saved
|
// Verify token was saved
|
||||||
const savedToken = await this.tokenStorage.getCredentials(serverName);
|
const savedToken = await this.tokenStorage.getCredentials(serverName);
|
||||||
if (savedToken && savedToken.token && savedToken.token.accessToken) {
|
if (savedToken && savedToken.token && savedToken.token.accessToken) {
|
||||||
const tokenPreview =
|
// Avoid leaking token material; log a short SHA-256 fingerprint instead.
|
||||||
savedToken.token.accessToken.length > 20
|
const tokenFingerprint = crypto
|
||||||
? `${savedToken.token.accessToken.substring(0, 20)}...`
|
.createHash('sha256')
|
||||||
: '[token]';
|
.update(savedToken.token.accessToken)
|
||||||
console.log(`Token verification successful: ${tokenPreview}`);
|
.digest('hex')
|
||||||
|
.slice(0, 8);
|
||||||
|
console.debug(
|
||||||
|
`✓ Token verification successful (fingerprint: ${tokenFingerprint})`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
'Token verification failed: token not found or invalid after save',
|
'Token verification failed: token not found or invalid after save',
|
||||||
|
|||||||
Reference in New Issue
Block a user