Added /mcp command support and cleaned up broken tests (#817)

This commit is contained in:
Bryan Morgan
2025-06-07 15:06:18 -04:00
committed by GitHub
parent 6ea4479064
commit 28ff62e7b1
9 changed files with 449 additions and 33 deletions

View File

@@ -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,11 +440,208 @@ 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);
});
});
});

View File

@@ -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';
@@ -137,10 +137,75 @@ export const useSlashCommandProcessor = (
openThemeDialog();
},
},
{
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) {