diff options
Diffstat (limited to 'packages/cli/src/ui/hooks')
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.test.ts | 455 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 209 |
2 files changed, 2 insertions, 662 deletions
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 20c8d7fe..3a0428d9 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -67,14 +67,7 @@ import { import open from 'open'; import { useSlashCommandProcessor } from './slashCommandProcessor.js'; import { MessageType, SlashCommandProcessorResult } from '../types.js'; -import { - Config, - MCPDiscoveryState, - MCPServerStatus, - getMCPDiscoveryState, - getMCPServerStatus, - GeminiClient, -} from '@google/gemini-cli-core'; +import { Config, GeminiClient } from '@google/gemini-cli-core'; import { useSessionStats } from '../contexts/SessionContext.js'; import { LoadedSettings } from '../../config/settings.js'; import * as ShowMemoryCommandModule from './useShowMemoryCommand.js'; @@ -102,8 +95,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { await importOriginal<typeof import('@google/gemini-cli-core')>(); return { ...actual, - getMCPServerStatus: vi.fn(), - getMCPDiscoveryState: vi.fn(), }; }); @@ -760,448 +751,4 @@ describe('useSlashCommandProcessor', () => { expect(commandResult).toEqual({ type: 'handled' }); }); }); - - describe('/mcp command', () => { - it('should show an error if tool registry is not available', async () => { - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue(undefined), - } as unknown as Config; - const { handleSlashCommand } = getProcessor(); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.ERROR, - text: 'Could not retrieve tool registry.', - }), - expect.any(Number), - ); - expect(commandResult).toEqual({ type: 'handled' }); - }); - - it('should display a message with a URL when no MCP servers are configured in a sandbox', async () => { - process.env.SANDBOX = 'sandbox'; - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: vi.fn().mockReturnValue([]), - }), - getMcpServers: vi.fn().mockReturnValue({}), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.INFO, - text: `No MCP servers configured. Please open the following URL in your browser to view documentation:\nhttps://goo.gle/gemini-cli-docs-mcp`, - }), - expect.any(Number), - ); - expect(commandResult).toEqual({ type: 'handled' }); - delete process.env.SANDBOX; - }); - - it('should display a message and open a URL when no MCP servers are configured outside a sandbox', async () => { - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: vi.fn().mockReturnValue([]), - }), - getMcpServers: vi.fn().mockReturnValue({}), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.INFO, - text: 'No MCP servers configured. Opening documentation in your browser: https://goo.gle/gemini-cli-docs-mcp', - }), - expect.any(Number), - ); - expect(open).toHaveBeenCalledWith('https://goo.gle/gemini-cli-docs-mcp'); - expect(commandResult).toEqual({ type: 'handled' }); - }); - - it('should display configured MCP servers with status indicators and their tools', async () => { - // Mock MCP servers configuration - const mockMcpServers = { - server1: { command: 'cmd1' }, - server2: { command: 'cmd2' }, - server3: { command: 'cmd3' }, - }; - - // Setup getMCPServerStatus mock implementation - use all CONNECTED to avoid startup message in this test - vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { - if (serverName === 'server1') return MCPServerStatus.CONNECTED; - if (serverName === 'server2') return MCPServerStatus.CONNECTED; - return MCPServerStatus.DISCONNECTED; // Default for server3 and others - }); - - // Setup getMCPDiscoveryState mock to return completed so no startup message is shown - vi.mocked(getMCPDiscoveryState).mockReturnValue( - MCPDiscoveryState.COMPLETED, - ); - - // Mock tools from each server - const mockServer1Tools = [ - { name: 'server1_tool1' }, - { name: 'server1_tool2' }, - ]; - - const mockServer2Tools = [{ name: 'server2_tool1' }]; - - const mockServer3Tools = [{ name: 'server3_tool1' }]; - - const mockGetToolsByServer = vi.fn().mockImplementation((serverName) => { - if (serverName === 'server1') return mockServer1Tools; - if (serverName === 'server2') return mockServer2Tools; - if (serverName === 'server3') return mockServer3Tools; - return []; - }); - - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: mockGetToolsByServer, - }), - getMcpServers: vi.fn().mockReturnValue(mockMcpServers), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.INFO, - text: expect.stringContaining('Configured MCP servers:'), - }), - expect.any(Number), - ); - - // Check that the message contains details about servers and their tools - const message = mockAddItem.mock.calls[1][0].text; - // Server 1 - Connected - expect(message).toContain( - '🟢 \u001b[1mserver1\u001b[0m - Ready (2 tools)', - ); - expect(message).toContain('\u001b[36mserver1_tool1\u001b[0m'); - expect(message).toContain('\u001b[36mserver1_tool2\u001b[0m'); - - // Server 2 - Connected - expect(message).toContain( - '🟢 \u001b[1mserver2\u001b[0m - Ready (1 tools)', - ); - expect(message).toContain('\u001b[36mserver2_tool1\u001b[0m'); - - // Server 3 - Disconnected - expect(message).toContain( - '🔴 \u001b[1mserver3\u001b[0m - Disconnected (1 tools cached)', - ); - expect(message).toContain('\u001b[36mserver3_tool1\u001b[0m'); - - expect(commandResult).toEqual({ type: 'handled' }); - }); - - it('should display tool descriptions when showToolDescriptions is true', async () => { - // Mock MCP servers configuration with server description - const mockMcpServers = { - server1: { - command: 'cmd1', - description: 'This is a server description', - }, - }; - - // Setup getMCPServerStatus mock implementation - vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { - if (serverName === 'server1') return MCPServerStatus.CONNECTED; - return MCPServerStatus.DISCONNECTED; - }); - - // Setup getMCPDiscoveryState mock to return completed - vi.mocked(getMCPDiscoveryState).mockReturnValue( - MCPDiscoveryState.COMPLETED, - ); - - // Mock tools from server with descriptions - const mockServerTools = [ - { name: 'tool1', description: 'This is tool 1 description' }, - { name: 'tool2', description: 'This is tool 2 description' }, - ]; - - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: vi.fn().mockReturnValue(mockServerTools), - }), - getMcpServers: vi.fn().mockReturnValue(mockMcpServers), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(true); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.INFO, - text: expect.stringContaining('Configured MCP servers:'), - }), - expect.any(Number), - ); - - const message = mockAddItem.mock.calls[1][0].text; - - // Check that server description is included (with ANSI color codes) - expect(message).toContain('\u001b[1mserver1\u001b[0m - Ready (2 tools)'); - expect(message).toContain( - '\u001b[32mThis is a server description\u001b[0m', - ); - - // Check that tool descriptions are included (with ANSI color codes) - expect(message).toContain('\u001b[36mtool1\u001b[0m'); - expect(message).toContain( - '\u001b[32mThis is tool 1 description\u001b[0m', - ); - expect(message).toContain('\u001b[36mtool2\u001b[0m'); - expect(message).toContain( - '\u001b[32mThis is tool 2 description\u001b[0m', - ); - - expect(commandResult).toEqual({ type: 'handled' }); - }); - - it('should indicate when a server has no tools', async () => { - // Mock MCP servers configuration - const mockMcpServers = { - server1: { command: 'cmd1' }, - server2: { command: 'cmd2' }, - }; - - // Setup getMCPServerStatus mock implementation - vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { - if (serverName === 'server1') return MCPServerStatus.CONNECTED; - if (serverName === 'server2') return MCPServerStatus.DISCONNECTED; - return MCPServerStatus.DISCONNECTED; - }); - - // Setup getMCPDiscoveryState mock to return completed - vi.mocked(getMCPDiscoveryState).mockReturnValue( - MCPDiscoveryState.COMPLETED, - ); - - // Mock tools from each server - server2 has no tools - const mockServer1Tools = [{ name: 'server1_tool1' }]; - - const mockServer2Tools: Array<{ name: string }> = []; - - const mockGetToolsByServer = vi.fn().mockImplementation((serverName) => { - if (serverName === 'server1') return mockServer1Tools; - if (serverName === 'server2') return mockServer2Tools; - return []; - }); - - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: mockGetToolsByServer, - }), - getMcpServers: vi.fn().mockReturnValue(mockMcpServers), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.INFO, - text: expect.stringContaining('Configured MCP servers:'), - }), - expect.any(Number), - ); - - // Check that the message contains details about both servers and their tools - const message = mockAddItem.mock.calls[1][0].text; - expect(message).toContain( - '🟢 \u001b[1mserver1\u001b[0m - Ready (1 tools)', - ); - expect(message).toContain('\u001b[36mserver1_tool1\u001b[0m'); - expect(message).toContain( - '🔴 \u001b[1mserver2\u001b[0m - Disconnected (0 tools cached)', - ); - expect(message).toContain('No tools available'); - - expect(commandResult).toEqual({ type: 'handled' }); - }); - - it('should show startup indicator when servers are connecting', async () => { - // Mock MCP servers configuration - const mockMcpServers = { - server1: { command: 'cmd1' }, - server2: { command: 'cmd2' }, - }; - - // Setup getMCPServerStatus mock implementation with one server connecting - vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { - if (serverName === 'server1') return MCPServerStatus.CONNECTED; - if (serverName === 'server2') return MCPServerStatus.CONNECTING; - return MCPServerStatus.DISCONNECTED; - }); - - // Setup getMCPDiscoveryState mock to return in progress - vi.mocked(getMCPDiscoveryState).mockReturnValue( - MCPDiscoveryState.IN_PROGRESS, - ); - - // Mock tools from each server - const mockServer1Tools = [{ name: 'server1_tool1' }]; - const mockServer2Tools = [{ name: 'server2_tool1' }]; - - const mockGetToolsByServer = vi.fn().mockImplementation((serverName) => { - if (serverName === 'server1') return mockServer1Tools; - if (serverName === 'server2') return mockServer2Tools; - return []; - }); - - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: mockGetToolsByServer, - }), - getMcpServers: vi.fn().mockReturnValue(mockMcpServers), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp'); - }); - - const message = mockAddItem.mock.calls[1][0].text; - - // Check that startup indicator is shown - expect(message).toContain( - '⏳ MCP servers are starting up (1 initializing)...', - ); - expect(message).toContain( - 'Note: First startup may take longer. Tool availability will update automatically.', - ); - - // Check server statuses - expect(message).toContain( - '🟢 \u001b[1mserver1\u001b[0m - Ready (1 tools)', - ); - expect(message).toContain( - '🔄 \u001b[1mserver2\u001b[0m - Starting... (first startup may take longer) (tools will appear when ready)', - ); - - expect(commandResult).toEqual({ type: 'handled' }); - }); - }); - - describe('/mcp schema', () => { - it('should display tool schemas and descriptions', async () => { - // Mock MCP servers configuration with server description - const mockMcpServers = { - server1: { - command: 'cmd1', - description: 'This is a server description', - }, - }; - - // Setup getMCPServerStatus mock implementation - vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { - if (serverName === 'server1') return MCPServerStatus.CONNECTED; - return MCPServerStatus.DISCONNECTED; - }); - - // Setup getMCPDiscoveryState mock to return completed - vi.mocked(getMCPDiscoveryState).mockReturnValue( - MCPDiscoveryState.COMPLETED, - ); - - // Mock tools from server with descriptions - const mockServerTools = [ - { - name: 'tool1', - description: 'This is tool 1 description', - schema: { - parameters: [{ name: 'param1', type: 'string' }], - }, - }, - { - name: 'tool2', - description: 'This is tool 2 description', - schema: { - parameters: [{ name: 'param2', type: 'number' }], - }, - }, - ]; - - mockConfig = { - ...mockConfig, - getToolRegistry: vi.fn().mockResolvedValue({ - getToolsByServer: vi.fn().mockReturnValue(mockServerTools), - }), - getMcpServers: vi.fn().mockReturnValue(mockMcpServers), - } as unknown as Config; - - const { handleSlashCommand } = getProcessor(true); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand('/mcp schema'); - }); - - expect(mockAddItem).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - type: MessageType.INFO, - text: expect.stringContaining('Configured MCP servers:'), - }), - expect.any(Number), - ); - - const message = mockAddItem.mock.calls[1][0].text; - - // Check that server description is included - expect(message).toContain('Ready (2 tools)'); - expect(message).toContain('This is a server description'); - - // Check that tool schemas are included - expect(message).toContain('tool 1 description'); - expect(message).toContain('param1'); - expect(message).toContain('string'); - expect(message).toContain('tool 2 description'); - expect(message).toContain('param2'); - expect(message).toContain('number'); - - expect(commandResult).toEqual({ type: 'handled' }); - }); - }); }); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 67dbfcdd..8355ea19 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -10,15 +10,7 @@ import open from 'open'; import process from 'node:process'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; import { useStateAndRef } from './useStateAndRef.js'; -import { - Config, - GitService, - Logger, - MCPDiscoveryState, - MCPServerStatus, - getMCPDiscoveryState, - getMCPServerStatus, -} from '@google/gemini-cli-core'; +import { Config, GitService, Logger } from '@google/gemini-cli-core'; import { useSessionStats } from '../contexts/SessionContext.js'; import { Message, @@ -235,205 +227,6 @@ export const useSlashCommandProcessor = ( action: (_mainCommand, _subCommand, _args) => openEditorDialog(), }, { - name: 'mcp', - description: 'list configured MCP servers and tools', - action: async (_mainCommand, _subCommand, _args) => { - // Check if the _subCommand includes a specific flag to control description visibility - let useShowDescriptions = showToolDescriptions; - if (_subCommand === 'desc' || _subCommand === 'descriptions') { - useShowDescriptions = true; - } else if ( - _subCommand === 'nodesc' || - _subCommand === 'nodescriptions' - ) { - useShowDescriptions = false; - } else if (_args === 'desc' || _args === 'descriptions') { - useShowDescriptions = true; - } else if (_args === 'nodesc' || _args === 'nodescriptions') { - useShowDescriptions = false; - } - // Check if the _subCommand includes a specific flag to show detailed tool schema - let useShowSchema = false; - if (_subCommand === 'schema' || _args === 'schema') { - useShowSchema = true; - } - - const toolRegistry = await config?.getToolRegistry(); - if (!toolRegistry) { - addMessage({ - type: MessageType.ERROR, - content: 'Could not retrieve tool registry.', - timestamp: new Date(), - }); - return; - } - - const mcpServers = config?.getMcpServers() || {}; - const serverNames = Object.keys(mcpServers); - - if (serverNames.length === 0) { - const docsUrl = 'https://goo.gle/gemini-cli-docs-mcp'; - if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { - addMessage({ - type: MessageType.INFO, - content: `No MCP servers configured. Please open the following URL in your browser to view documentation:\n${docsUrl}`, - timestamp: new Date(), - }); - } else { - addMessage({ - type: MessageType.INFO, - content: `No MCP servers configured. Opening documentation in your browser: ${docsUrl}`, - timestamp: new Date(), - }); - await open(docsUrl); - } - return; - } - - // Check if any servers are still connecting - const connectingServers = serverNames.filter( - (name) => getMCPServerStatus(name) === MCPServerStatus.CONNECTING, - ); - const discoveryState = getMCPDiscoveryState(); - - let message = ''; - - // Add overall discovery status message if needed - if ( - discoveryState === MCPDiscoveryState.IN_PROGRESS || - connectingServers.length > 0 - ) { - message += `\u001b[33m⏳ MCP servers are starting up (${connectingServers.length} initializing)...\u001b[0m\n`; - message += `\u001b[90mNote: First startup may take longer. Tool availability will update automatically.\u001b[0m\n\n`; - } - - message += 'Configured MCP servers:\n\n'; - - for (const serverName of serverNames) { - const serverTools = toolRegistry.getToolsByServer(serverName); - const status = getMCPServerStatus(serverName); - - // Add status indicator with descriptive text - let statusIndicator = ''; - let statusText = ''; - switch (status) { - case MCPServerStatus.CONNECTED: - statusIndicator = '🟢'; - statusText = 'Ready'; - break; - case MCPServerStatus.CONNECTING: - statusIndicator = '🔄'; - statusText = 'Starting... (first startup may take longer)'; - break; - case MCPServerStatus.DISCONNECTED: - default: - statusIndicator = '🔴'; - statusText = 'Disconnected'; - break; - } - - // Get server description if available - const server = mcpServers[serverName]; - - // Format server header with bold formatting and status - message += `${statusIndicator} \u001b[1m${serverName}\u001b[0m - ${statusText}`; - - // Add tool count with conditional messaging - if (status === MCPServerStatus.CONNECTED) { - message += ` (${serverTools.length} tools)`; - } else if (status === MCPServerStatus.CONNECTING) { - message += ` (tools will appear when ready)`; - } else { - message += ` (${serverTools.length} tools cached)`; - } - - // Add server description with proper handling of multi-line descriptions - if ((useShowDescriptions || useShowSchema) && server?.description) { - const greenColor = '\u001b[32m'; - const resetColor = '\u001b[0m'; - - const descLines = server.description.trim().split('\n'); - if (descLines) { - message += ':\n'; - for (const descLine of descLines) { - message += ` ${greenColor}${descLine}${resetColor}\n`; - } - } else { - message += '\n'; - } - } else { - message += '\n'; - } - - // Reset formatting after server entry - message += '\u001b[0m'; - - if (serverTools.length > 0) { - serverTools.forEach((tool) => { - if ( - (useShowDescriptions || useShowSchema) && - tool.description - ) { - // Format tool name in cyan using simple ANSI cyan color - message += ` - \u001b[36m${tool.name}\u001b[0m`; - - // Apply green color to the description text - const greenColor = '\u001b[32m'; - const resetColor = '\u001b[0m'; - - // Handle multi-line descriptions by properly indenting and preserving formatting - const descLines = tool.description.trim().split('\n'); - if (descLines) { - message += ':\n'; - for (const descLine of descLines) { - message += ` ${greenColor}${descLine}${resetColor}\n`; - } - } else { - message += '\n'; - } - // Reset is handled inline with each line now - } else { - // Use cyan color for the tool name even when not showing descriptions - message += ` - \u001b[36m${tool.name}\u001b[0m\n`; - } - if (useShowSchema) { - // Prefix the parameters in cyan - message += ` \u001b[36mParameters:\u001b[0m\n`; - // Apply green color to the parameter text - const greenColor = '\u001b[32m'; - const resetColor = '\u001b[0m'; - - const paramsLines = JSON.stringify( - tool.schema.parameters, - null, - 2, - ) - .trim() - .split('\n'); - if (paramsLines) { - for (const paramsLine of paramsLines) { - message += ` ${greenColor}${paramsLine}${resetColor}\n`; - } - } - } - }); - } else { - message += ' No tools available\n'; - } - message += '\n'; - } - - // Make sure to reset any ANSI formatting at the end to prevent it from affecting the terminal - message += '\u001b[0m'; - - addMessage({ - type: MessageType.INFO, - content: message, - timestamp: new Date(), - }); - }, - }, - { name: 'tools', description: 'list available Gemini CLI tools', action: async (_mainCommand, _subCommand, _args) => { |
