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,
);
}