diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.test.ts | 230 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 83 |
2 files changed, 299 insertions, 14 deletions
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 1d33d218..5466e887 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -56,7 +56,11 @@ import { type SlashCommandActionReturn, } from './slashCommandProcessor.js'; import { MessageType } from '../types.js'; -import { type Config } from '@gemini-code/core'; +import { + type Config, + MCPServerStatus, + getMCPServerStatus, +} from '@gemini-code/core'; import * as ShowMemoryCommandModule from './useShowMemoryCommand.js'; import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; @@ -378,39 +382,54 @@ Add any other context about the problem here. expect(commandResult).toBe(true); }); - it('should display available tools when tools are found', async () => { - const mockTools = [{ name: 'tool1' }, { name: 'tool2' }]; + it('should display only Gemini CLI tools (filtering out MCP tools)', async () => { + // Create mock tools - some with serverName property (MCP tools) and some without (Gemini CLI tools) + const mockTools = [ + { name: 'tool1' }, + { name: 'tool2' }, + { name: 'mcp_tool1', serverName: 'mcp-server1' }, + { name: 'mcp_tool2', serverName: 'mcp-server1' }, + ]; + mockConfig = { ...mockConfig, getToolRegistry: vi.fn().mockResolvedValue({ getAllTools: vi.fn().mockReturnValue(mockTools), }), } as unknown as Config; + const { handleSlashCommand } = getProcessor(); let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { commandResult = handleSlashCommand('/tools'); }); + // Should only show tool1 and tool2, not the MCP tools expect(mockAddItem).toHaveBeenNthCalledWith( 2, expect.objectContaining({ type: MessageType.INFO, - text: 'Available tools:\n\ntool1\ntool2', + text: 'Available Gemini CLI tools:\n\ntool1\ntool2', }), expect.any(Number), ); expect(commandResult).toBe(true); }); - it('should display a message when no tools are available', async () => { - const mockTools: unknown[] = []; + it('should display a message when no Gemini CLI tools are available', async () => { + // Only MCP tools available + const mockTools = [ + { name: 'mcp_tool1', serverName: 'mcp-server1' }, + { name: 'mcp_tool2', serverName: 'mcp-server1' }, + ]; + mockConfig = { ...mockConfig, getToolRegistry: vi.fn().mockResolvedValue({ getAllTools: vi.fn().mockReturnValue(mockTools), }), } as unknown as Config; + const { handleSlashCommand } = getProcessor(); let commandResult: SlashCommandActionReturn | boolean = false; await act(async () => { @@ -421,10 +440,207 @@ Add any other context about the problem here. 2, expect.objectContaining({ type: MessageType.INFO, - text: 'Available tools:\n\n', + text: 'Available Gemini CLI tools:\n\n', + }), + expect.any(Number), + ); + expect(commandResult).toBe(true); + }); + }); + + describe('/mcp command', () => { + beforeEach(() => { + // Mock the core module with getMCPServerStatus + vi.mock('@gemini-code/core', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + MCPServerStatus: { + CONNECTED: 'connected', + CONNECTING: 'connecting', + DISCONNECTED: 'disconnected', + }, + getMCPServerStatus: vi.fn(), + }; + }); + }); + + 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: SlashCommandActionReturn | boolean = false; + await act(async () => { + commandResult = handleSlashCommand('/mcp'); + }); + + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.ERROR, + text: 'Could not retrieve tool registry.', + }), + expect.any(Number), + ); + expect(commandResult).toBe(true); + }); + + it('should display a message when no MCP servers are configured', async () => { + mockConfig = { + ...mockConfig, + getToolRegistry: vi.fn().mockResolvedValue({ + getToolsByServer: vi.fn().mockReturnValue([]), + }), + getMcpServers: vi.fn().mockReturnValue({}), + } as unknown as Config; + + const { handleSlashCommand } = getProcessor(); + let commandResult: SlashCommandActionReturn | boolean = false; + await act(async () => { + commandResult = handleSlashCommand('/mcp'); + }); + + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.INFO, + text: 'No MCP servers configured.', + }), + expect.any(Number), + ); + expect(commandResult).toBe(true); + }); + + 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 + vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { + if (serverName === 'server1') return MCPServerStatus.CONNECTED; + if (serverName === 'server2') return MCPServerStatus.CONNECTING; + return MCPServerStatus.DISCONNECTED; // Default for server3 and others + }); + + // 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: SlashCommandActionReturn | boolean = false; + await act(async () => { + commandResult = handleSlashCommand('/mcp'); + }); + + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.INFO, + text: expect.stringContaining('Configured MCP servers and tools:'), }), expect.any(Number), ); + + // Check that the message contains details about both servers and their tools + const message = mockAddItem.mock.calls[1][0].text; + // Server 1 - Connected (green dot) + expect(message).toContain('🟢 server1 (2 tools):'); + expect(message).toContain('server1_tool1'); + expect(message).toContain('server1_tool2'); + + // Server 2 - Connecting (yellow dot) + expect(message).toContain('🟡 server2 (1 tools):'); + expect(message).toContain('server2_tool1'); + + // Server 3 - No status, should default to Disconnected (red dot) + expect(message).toContain('🔴 server3 (1 tools):'); + expect(message).toContain('server3_tool1'); + + expect(commandResult).toBe(true); + }); + + 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; + }); + + // Mock tools from each server - server2 has no tools + const mockServer1Tools = [{ name: 'server1_tool1' }]; + + const mockServer2Tools = []; + + 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: SlashCommandActionReturn | boolean = false; + await act(async () => { + commandResult = handleSlashCommand('/mcp'); + }); + + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.INFO, + text: expect.stringContaining('Configured MCP servers and tools:'), + }), + 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('🟢 server1 (1 tools):'); + expect(message).toContain('server1_tool1'); + expect(message).toContain('🔴 server2 (0 tools):'); + expect(message).toContain('No tools available'); + expect(commandResult).toBe(true); }); }); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 01e04d70..68f53873 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -9,7 +9,7 @@ import { type PartListUnion } from '@google/genai'; import open from 'open'; import process from 'node:process'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; -import { Config } from '@gemini-code/core'; +import { Config, MCPServerStatus, getMCPServerStatus } from '@gemini-code/core'; import { Message, MessageType, HistoryItemWithoutId } from '../types.js'; import { createShowMemoryAction } from './useShowMemoryCommand.js'; import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; @@ -138,9 +138,74 @@ export const useSlashCommandProcessor = ( }, }, { + name: 'mcp', + description: 'list configured MCP servers and tools', + action: async (_mainCommand, _subCommand, _args) => { + 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) { + addMessage({ + type: MessageType.INFO, + content: 'No MCP servers configured.', + timestamp: new Date(), + }); + return; + } + + let message = 'Configured MCP servers and tools:\n\n'; + + for (const serverName of serverNames) { + const serverTools = toolRegistry.getToolsByServer(serverName); + const status = getMCPServerStatus(serverName); + + // Add status indicator + let statusDot = ''; + switch (status) { + case MCPServerStatus.CONNECTED: + statusDot = '🟢'; // Green dot for connected + break; + case MCPServerStatus.CONNECTING: + statusDot = '🟡'; // Yellow dot for connecting + break; + case MCPServerStatus.DISCONNECTED: + default: + statusDot = '🔴'; // Red dot for disconnected + break; + } + + message += `${statusDot} ${serverName} (${serverTools.length} tools):\n`; + if (serverTools.length > 0) { + serverTools.forEach((tool) => { + message += ` - ${tool.name}\n`; + }); + } else { + message += ' No tools available\n'; + } + message += '\n'; + } + + addMessage({ + type: MessageType.INFO, + content: message, + timestamp: new Date(), + }); + }, + }, + { name: 'memory', description: - 'Manage memory. Usage: /memory <show|refresh|add> [text for add]', + 'manage memory. Usage: /memory <show|refresh|add> [text for add]', action: (mainCommand, subCommand, args) => { switch (subCommand) { case 'show': @@ -163,7 +228,7 @@ export const useSlashCommandProcessor = ( }, { name: 'tools', - description: 'list available tools', + description: 'list available Gemini CLI tools', action: async (_mainCommand, _subCommand, _args) => { const toolRegistry = await config?.getToolRegistry(); const tools = toolRegistry?.getAllTools(); @@ -175,10 +240,14 @@ export const useSlashCommandProcessor = ( }); return; } - const toolList = tools.map((tool) => tool.name); + + // Filter out MCP tools by checking if they have a serverName property + const geminiTools = tools.filter((tool) => !('serverName' in tool)); + const geminiToolList = geminiTools.map((tool) => tool.name); + addMessage({ type: MessageType.INFO, - content: `Available tools:\n\n${toolList.join('\n')}`, + content: `Available Gemini CLI tools:\n\n${geminiToolList.join('\n')}`, timestamp: new Date(), }); }, @@ -191,7 +260,7 @@ export const useSlashCommandProcessor = ( }, { name: 'about', - description: 'Show version info', + description: 'show version info', action: (_mainCommand, _subCommand, _args) => { const osVersion = process.platform; let sandboxEnv = 'no sandbox'; @@ -214,7 +283,7 @@ export const useSlashCommandProcessor = ( }, { name: 'bug', - description: 'Submit a bug report.', + description: 'submit a bug report', action: (_mainCommand, _subCommand, args) => { let bugDescription = _subCommand || ''; if (args) { |
