diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index bfd316ba44..1491c4ff44 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -451,6 +451,7 @@ export async function loadCliConfig( workspaceDir: cwd, enabledExtensionOverrides: argv.extensions, eventEmitter: appEvents as EventEmitter, + clientVersion: await getVersion(), }); await extensionManager.loadExtensions(); @@ -653,6 +654,7 @@ export async function loadCliConfig( return new Config({ sessionId, + clientVersion: await getVersion(), embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: sandboxConfig, targetDir: cwd, diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index 45ca5a0d8a..3af4c59f3a 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -76,6 +76,7 @@ interface ExtensionManagerParams { requestSetting: ((setting: ExtensionSetting) => Promise) | null; workspaceDir: string; eventEmitter?: EventEmitter; + clientVersion?: string; } /** @@ -105,6 +106,7 @@ export class ExtensionManager extends ExtensionLoader { telemetry: options.settings.telemetry, interactive: false, sessionId: randomUUID(), + clientVersion: options.clientVersion ?? 'unknown', targetDir: options.workspaceDir, cwd: options.workspaceDir, model: '', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index bc6de83988..cfb7709161 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -290,6 +290,7 @@ export interface SandboxConfig { export interface ConfigParameters { sessionId: string; + clientVersion?: string; embeddingModel?: string; sandbox?: SandboxConfig; targetDir: string; @@ -415,6 +416,7 @@ export class Config { private agentRegistry!: AgentRegistry; private skillManager!: SkillManager; private sessionId: string; + private clientVersion: string; private fileSystemService: FileSystemService; private contentGeneratorConfig!: ContentGeneratorConfig; private contentGenerator!: ContentGenerator; @@ -553,6 +555,7 @@ export class Config { constructor(params: ConfigParameters) { this.sessionId = params.sessionId; + this.clientVersion = params.clientVersion ?? 'unknown'; this.embeddingModel = params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL; this.fileSystemService = new StandardFileSystemService(); @@ -810,6 +813,7 @@ export class Config { this.toolRegistry = await this.createToolRegistry(); discoverToolsHandle?.end(); this.mcpClientManager = new McpClientManager( + this.clientVersion, this.toolRegistry, this, this.eventEmitter, diff --git a/packages/core/src/tools/mcp-client-manager.test.ts b/packages/core/src/tools/mcp-client-manager.test.ts index 27c6984a7c..18b8ab3ff7 100644 --- a/packages/core/src/tools/mcp-client-manager.test.ts +++ b/packages/core/src/tools/mcp-client-manager.test.ts @@ -66,7 +66,7 @@ describe('McpClientManager', () => { mockConfig.getMcpServers.mockReturnValue({ 'test-server': {}, }); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(mockedMcpClient.connect).toHaveBeenCalledOnce(); expect(mockedMcpClient.discover).toHaveBeenCalledOnce(); @@ -79,7 +79,7 @@ describe('McpClientManager', () => { 'server-2': {}, 'server-3': {}, }); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); // Each client should be connected/discovered @@ -94,7 +94,7 @@ describe('McpClientManager', () => { mockConfig.getMcpServers.mockReturnValue({ 'test-server': {}, }); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); expect(manager.getDiscoveryState()).toBe(MCPDiscoveryState.NOT_STARTED); const promise = manager.startConfiguredMcpServers(); expect(manager.getDiscoveryState()).toBe(MCPDiscoveryState.IN_PROGRESS); @@ -107,7 +107,7 @@ describe('McpClientManager', () => { 'test-server': {}, }); mockConfig.isTrustedFolder.mockReturnValue(false); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(mockedMcpClient.connect).not.toHaveBeenCalled(); expect(mockedMcpClient.discover).not.toHaveBeenCalled(); @@ -118,7 +118,7 @@ describe('McpClientManager', () => { 'test-server': {}, }); mockConfig.getBlockedMcpServers.mockReturnValue(['test-server']); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(mockedMcpClient.connect).not.toHaveBeenCalled(); expect(mockedMcpClient.discover).not.toHaveBeenCalled(); @@ -130,14 +130,14 @@ describe('McpClientManager', () => { 'another-server': {}, }); mockConfig.getAllowedMcpServers.mockReturnValue(['another-server']); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(mockedMcpClient.connect).toHaveBeenCalledOnce(); expect(mockedMcpClient.discover).toHaveBeenCalledOnce(); }); it('should start servers from extensions', async () => { - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startExtension({ name: 'test-extension', mcpServers: { @@ -154,7 +154,7 @@ describe('McpClientManager', () => { }); it('should not start servers from disabled extensions', async () => { - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startExtension({ name: 'test-extension', mcpServers: { @@ -175,7 +175,7 @@ describe('McpClientManager', () => { 'test-server': {}, }); mockConfig.getBlockedMcpServers.mockReturnValue(['test-server']); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(manager.getBlockedMcpServers()).toEqual([ { name: 'test-server', extensionName: '' }, @@ -188,7 +188,7 @@ describe('McpClientManager', () => { 'test-server': {}, }); mockedMcpClient.getServerConfig.mockReturnValue({}); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(mockedMcpClient.connect).toHaveBeenCalledTimes(1); @@ -207,7 +207,7 @@ describe('McpClientManager', () => { 'test-server': {}, }); mockedMcpClient.getServerConfig.mockReturnValue({}); - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await manager.startConfiguredMcpServers(); expect(mockedMcpClient.connect).toHaveBeenCalledTimes(1); @@ -221,7 +221,7 @@ describe('McpClientManager', () => { }); it('should throw an error if the server does not exist', async () => { - const manager = new McpClientManager(toolRegistry, mockConfig); + const manager = new McpClientManager('0.0.1', toolRegistry, mockConfig); await expect(manager.restartServer('non-existent')).rejects.toThrow( 'No MCP server registered with the name "non-existent"', ); @@ -247,7 +247,11 @@ describe('McpClientManager', () => { }) as unknown as McpClient, ); - const manager = new McpClientManager({} as ToolRegistry, mockConfig); + const manager = new McpClientManager( + '0.0.1', + {} as ToolRegistry, + mockConfig, + ); mockConfig.getMcpServers.mockReturnValue({ 'server-with-instructions': {}, @@ -282,7 +286,11 @@ describe('McpClientManager', () => { 'test-server': {}, }); - const manager = new McpClientManager({} as ToolRegistry, mockConfig); + const manager = new McpClientManager( + '0.0.1', + {} as ToolRegistry, + mockConfig, + ); await expect(manager.startConfiguredMcpServers()).resolves.not.toThrow(); }); @@ -301,7 +309,11 @@ describe('McpClientManager', () => { 'test-server': {}, }); - const manager = new McpClientManager({} as ToolRegistry, mockConfig); + const manager = new McpClientManager( + '0.0.1', + {} as ToolRegistry, + mockConfig, + ); await manager.startConfiguredMcpServers(); await expect(manager.restartServer('test-server')).resolves.not.toThrow(); diff --git a/packages/core/src/tools/mcp-client-manager.ts b/packages/core/src/tools/mcp-client-manager.ts index a4619756f0..e9407c1c7b 100644 --- a/packages/core/src/tools/mcp-client-manager.ts +++ b/packages/core/src/tools/mcp-client-manager.ts @@ -27,6 +27,7 @@ import { debugLogger } from '../utils/debugLogger.js'; */ export class McpClientManager { private clients: Map = new Map(); + private readonly clientVersion: string; private readonly toolRegistry: ToolRegistry; private readonly cliConfig: Config; // If we have ongoing MCP client discovery, this completes once that is done. @@ -40,10 +41,12 @@ export class McpClientManager { }> = []; constructor( + clientVersion: string, toolRegistry: ToolRegistry, cliConfig: Config, eventEmitter?: EventEmitter, ) { + this.clientVersion = clientVersion; this.toolRegistry = toolRegistry; this.cliConfig = cliConfig; this.eventEmitter = eventEmitter; @@ -183,6 +186,7 @@ export class McpClientManager { this.cliConfig.getWorkspaceContext(), this.cliConfig, this.cliConfig.getDebugMode(), + this.clientVersion, async () => { debugLogger.log('Tools changed, updating Gemini context...'); await this.scheduleMcpContextRefresh(); diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index a448fd288b..eb63779bc2 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -133,6 +133,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -213,6 +214,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -264,6 +266,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await expect(client.discover({} as Config)).rejects.toThrow( @@ -319,6 +322,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await expect(client.discover({} as Config)).rejects.toThrow( @@ -378,6 +382,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -451,6 +456,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -527,6 +533,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -610,6 +617,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -690,6 +698,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); await client.discover({} as Config); @@ -739,6 +748,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); @@ -775,6 +785,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); @@ -830,6 +841,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', onToolsUpdatedSpy, ); @@ -900,6 +912,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); @@ -970,6 +983,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', onToolsUpdatedSpy, ); @@ -982,6 +996,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', onToolsUpdatedSpy, ); @@ -1064,6 +1079,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', ); await client.connect(); @@ -1128,6 +1144,7 @@ describe('mcp-client', () => { workspaceContext, { sanitizationConfig: EMPTY_CONFIG } as Config, false, + '0.0.1', onToolsUpdatedSpy, ); @@ -1675,6 +1692,7 @@ describe('connectToMcpServer with OAuth', () => { ); const client = await connectToMcpServer( + '0.0.1', 'test-server', { httpUrl: serverUrl, oauth: { enabled: true } }, false, @@ -1720,6 +1738,7 @@ describe('connectToMcpServer with OAuth', () => { ); const client = await connectToMcpServer( + '0.0.1', 'test-server', { httpUrl: serverUrl, oauth: { enabled: true } }, false, @@ -1775,6 +1794,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => { await expect( connectToMcpServer( + '0.0.1', 'test-server', { url: 'http://test-server', type: 'http' }, false, @@ -1794,6 +1814,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => { await expect( connectToMcpServer( + '0.0.1', 'test-server', { url: 'http://test-server', type: 'sse' }, false, @@ -1812,6 +1833,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => { .mockResolvedValueOnce(undefined); const client = await connectToMcpServer( + '0.0.1', 'test-server', { url: 'http://test-server' }, false, @@ -1834,6 +1856,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => { await expect( connectToMcpServer( + '0.0.1', 'test-server', { url: 'http://test-server' }, false, @@ -1851,6 +1874,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => { .mockResolvedValueOnce(undefined); const client = await connectToMcpServer( + '0.0.1', 'test-server', { url: 'http://test-server' }, false, @@ -1921,6 +1945,7 @@ describe('connectToMcpServer - OAuth with transport fallback', () => { .mockResolvedValueOnce(undefined); const client = await connectToMcpServer( + '0.0.1', 'test-server', { url: 'http://test-server', oauth: { enabled: true } }, false, diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index 1f96d34169..872a5019d4 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -122,6 +122,7 @@ export class McpClient { private readonly workspaceContext: WorkspaceContext, private readonly cliConfig: Config, private readonly debugMode: boolean, + private readonly clientVersion: string, private readonly onToolsUpdated?: (signal?: AbortSignal) => Promise, ) {} @@ -137,6 +138,7 @@ export class McpClient { this.updateStatus(MCPServerStatus.CONNECTING); try { this.client = await connectToMcpServer( + this.clientVersion, this.serverName, this.serverConfig, this.debugMode, @@ -715,6 +717,7 @@ async function createTransportWithOAuth( */ export async function discoverMcpTools( + clientVersion: string, mcpServers: Record, mcpServerCommand: string | undefined, toolRegistry: ToolRegistry, @@ -730,6 +733,7 @@ export async function discoverMcpTools( const discoveryPromises = Object.entries(mcpServers).map( ([mcpServerName, mcpServerConfig]) => connectAndDiscover( + clientVersion, mcpServerName, mcpServerConfig, toolRegistry, @@ -808,6 +812,7 @@ export function populateMcpServerCommand( * @returns Promise that resolves when discovery is complete */ export async function connectAndDiscover( + clientVersion: string, mcpServerName: string, mcpServerConfig: MCPServerConfig, toolRegistry: ToolRegistry, @@ -821,6 +826,7 @@ export async function connectAndDiscover( let mcpClient: Client | undefined; try { mcpClient = await connectToMcpServer( + clientVersion, mcpServerName, mcpServerConfig, debugMode, @@ -1331,6 +1337,7 @@ async function retryWithOAuth( * @throws An error if the connection fails or the configuration is invalid. */ export async function connectToMcpServer( + clientVersion: string, mcpServerName: string, mcpServerConfig: MCPServerConfig, debugMode: boolean, @@ -1340,7 +1347,7 @@ export async function connectToMcpServer( const mcpClient = new Client( { name: 'gemini-cli-mcp-client', - version: '0.0.1', + version: clientVersion, }, { // Use a tolerant validator so bad output schemas don't block discovery.