From 03bcbcc10dee5b59ae352cbae60913f1b48735b5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 28 Aug 2025 21:53:56 +0200 Subject: [PATCH] Add MCP loading indicator when initializing Gemini CLI (#6923) --- packages/cli/src/config/config.ts | 2 + packages/cli/src/gemini.tsx | 55 ++++++++++++++++++- packages/core/src/config/config.ts | 6 +- packages/core/src/tools/mcp-client-manager.ts | 36 +++++++++++- packages/core/src/tools/tool-registry.ts | 4 +- 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index dc34f50c4a..2cbeb00975 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -38,6 +38,7 @@ import { annotateActiveExtensions } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; import { resolvePath } from '../utils/resolvePath.js'; +import { appEvents } from '../utils/events.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; @@ -568,6 +569,7 @@ export async function loadCliConfig( shouldUseNodePtyShell: settings.tools?.usePty, skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, enablePromptCompletion: settings.general?.enablePromptCompletion ?? false, + eventEmitter: appEvents, }); } diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 9f023a3da6..b11b0f29ec 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { render } from 'ink'; +import React, { useState, useEffect } from 'react'; +import { render, Box, Text } from 'ink'; +import Spinner from 'ink-spinner'; import { AppWrapper } from './ui/App.js'; import { loadCliConfig, parseArguments } from './config/config.js'; import { readStdin } from './utils/readStdin.js'; @@ -105,6 +106,39 @@ async function relaunchWithAdditionalArgs(additionalArgs: string[]) { await new Promise((resolve) => child.on('close', resolve)); process.exit(0); } + +const InitializingComponent = ({ initialTotal }: { initialTotal: number }) => { + const [total, setTotal] = useState(initialTotal); + const [connected, setConnected] = useState(0); + + useEffect(() => { + const onStart = ({ count }: { count: number }) => setTotal(count); + const onChange = () => { + setConnected((val) => val + 1); + }; + + appEvents.on('mcp-servers-discovery-start', onStart); + appEvents.on('mcp-server-connected', onChange); + appEvents.on('mcp-server-error', onChange); + + return () => { + appEvents.off('mcp-servers-discovery-start', onStart); + appEvents.off('mcp-server-connected', onChange); + appEvents.off('mcp-server-error', onChange); + }; + }, []); + + const message = `Connecting to MCP servers... (${connected}/${total})`; + + return ( + + + {message} + + + ); +}; + import { runZedIntegration } from './zed-integration/zedIntegration.js'; export function setupUnhandledRejectionHandler() { @@ -238,8 +272,25 @@ export async function main() { setMaxSizedBoxDebugging(config.getDebugMode()); + const mcpServers = config.getMcpServers(); + const mcpServersCount = mcpServers ? Object.keys(mcpServers).length : 0; + + let spinnerInstance; + if (config.isInteractive() && mcpServersCount > 0) { + spinnerInstance = render( + , + ); + } + await config.initialize(); + if (spinnerInstance) { + // Small UX detail to show the completion message for a bit before unmounting. + await new Promise((f) => setTimeout(f, 100)); + spinnerInstance.clear(); + spinnerInstance.unmount(); + } + if (config.getIdeMode()) { await config.getIdeClient().connect(); logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START)); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 686c14e5f3..0b7b9cdc7b 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -54,6 +54,7 @@ import type { AnyToolInvocation } from '../tools/tools.js'; import { WorkspaceContext } from '../utils/workspaceContext.js'; import { Storage } from './storage.js'; import { FileExclusions } from '../utils/ignorePatterns.js'; +import type { EventEmitter } from 'node:events'; export enum ApprovalMode { DEFAULT = 'default', @@ -207,6 +208,7 @@ export interface ConfigParameters { skipNextSpeakerCheck?: boolean; extensionManagement?: boolean; enablePromptCompletion?: boolean; + eventEmitter?: EventEmitter; } export class Config { @@ -282,6 +284,7 @@ export class Config { private initialized: boolean = false; readonly storage: Storage; private readonly fileExclusions: FileExclusions; + private readonly eventEmitter?: EventEmitter; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -356,6 +359,7 @@ export class Config { this.storage = new Storage(this.targetDir); this.enablePromptCompletion = params.enablePromptCompletion ?? false; this.fileExclusions = new FileExclusions(this); + this.eventEmitter = params.eventEmitter; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -803,7 +807,7 @@ export class Config { } async createToolRegistry(): Promise { - const registry = new ToolRegistry(this); + const registry = new ToolRegistry(this, this.eventEmitter); // helper to create & register core tools that are enabled // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/core/src/tools/mcp-client-manager.ts b/packages/core/src/tools/mcp-client-manager.ts index 0468fff42a..182977efe6 100644 --- a/packages/core/src/tools/mcp-client-manager.ts +++ b/packages/core/src/tools/mcp-client-manager.ts @@ -13,6 +13,7 @@ import { populateMcpServerCommand, } from './mcp-client.js'; import { getErrorMessage } from '../utils/errors.js'; +import type { EventEmitter } from 'node:events'; import type { WorkspaceContext } from '../utils/workspaceContext.js'; /** @@ -29,6 +30,7 @@ export class McpClientManager { private readonly debugMode: boolean; private readonly workspaceContext: WorkspaceContext; private discoveryState: MCPDiscoveryState = MCPDiscoveryState.NOT_STARTED; + private readonly eventEmitter?: EventEmitter; constructor( mcpServers: Record, @@ -37,6 +39,7 @@ export class McpClientManager { promptRegistry: PromptRegistry, debugMode: boolean, workspaceContext: WorkspaceContext, + eventEmitter?: EventEmitter, ) { this.mcpServers = mcpServers; this.mcpServerCommand = mcpServerCommand; @@ -44,6 +47,7 @@ export class McpClientManager { this.promptRegistry = promptRegistry; this.debugMode = debugMode; this.workspaceContext = workspaceContext; + this.eventEmitter = eventEmitter; } /** @@ -53,14 +57,28 @@ export class McpClientManager { */ async discoverAllMcpTools(): Promise { await this.stop(); - this.discoveryState = MCPDiscoveryState.IN_PROGRESS; + const servers = populateMcpServerCommand( this.mcpServers, this.mcpServerCommand, ); - const discoveryPromises = Object.entries(servers).map( - async ([name, config]) => { + const serverEntries = Object.entries(servers); + const total = serverEntries.length; + + this.eventEmitter?.emit('mcp-servers-discovery-start', { count: total }); + + this.discoveryState = MCPDiscoveryState.IN_PROGRESS; + + const discoveryPromises = serverEntries.map( + async ([name, config], index) => { + const current = index + 1; + this.eventEmitter?.emit('mcp-server-connecting', { + name, + current, + total, + }); + const client = new McpClient( name, config, @@ -70,10 +88,22 @@ export class McpClientManager { this.debugMode, ); this.clients.set(name, client); + try { await client.connect(); await client.discover(); + this.eventEmitter?.emit('mcp-server-connected', { + name, + current, + total, + }); } catch (error) { + this.eventEmitter?.emit('mcp-server-error', { + name, + current, + total, + error, + }); // Log the error but don't let a single failed server stop the others console.error( `Error during discovery for server '${name}': ${getErrorMessage( diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 07f9230975..ec054d1821 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -20,6 +20,7 @@ import { DiscoveredMCPTool } from './mcp-tool.js'; import { parse } from 'shell-quote'; import { ToolErrorType } from './tool-error.js'; import { safeJsonStringify } from '../utils/safeJsonStringify.js'; +import type { EventEmitter } from 'node:events'; type ToolParams = Record; @@ -170,7 +171,7 @@ export class ToolRegistry { private config: Config; private mcpClientManager: McpClientManager; - constructor(config: Config) { + constructor(config: Config, eventEmitter?: EventEmitter) { this.config = config; this.mcpClientManager = new McpClientManager( this.config.getMcpServers() ?? {}, @@ -179,6 +180,7 @@ export class ToolRegistry { this.config.getPromptRegistry(), this.config.getDebugMode(), this.config.getWorkspaceContext(), + eventEmitter, ); }