feat: add /mcp refresh command (#4566)

This commit is contained in:
Ramón Medrano Llamas 2025-07-25 03:14:45 +02:00 committed by GitHub
parent e9ee686ab6
commit 273e74c09d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 155 additions and 7 deletions

View File

@ -976,4 +976,84 @@ describe('mcpCommand', () => {
} }
}); });
}); });
describe('refresh subcommand', () => {
it('should refresh the list of tools and display the status', async () => {
const mockToolRegistry = {
discoverMcpTools: vi.fn(),
getAllTools: vi.fn().mockReturnValue([]),
};
const mockGeminiClient = {
setTools: vi.fn(),
};
const context = createMockCommandContext({
services: {
config: {
getMcpServers: vi.fn().mockReturnValue({ server1: {} }),
getBlockedMcpServers: vi.fn().mockReturnValue([]),
getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
},
},
});
const refreshCommand = mcpCommand.subCommands?.find(
(cmd) => cmd.name === 'refresh',
);
expect(refreshCommand).toBeDefined();
const result = await refreshCommand!.action!(context, '');
expect(context.ui.addItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Refreshing MCP servers and tools...',
},
expect.any(Number),
);
expect(mockToolRegistry.discoverMcpTools).toHaveBeenCalled();
expect(mockGeminiClient.setTools).toHaveBeenCalled();
expect(isMessageAction(result)).toBe(true);
if (isMessageAction(result)) {
expect(result.messageType).toBe('info');
expect(result.content).toContain('Configured MCP servers:');
}
});
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
},
});
const refreshCommand = mcpCommand.subCommands?.find(
(cmd) => cmd.name === 'refresh',
);
const result = await refreshCommand!.action!(contextWithoutConfig, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Config not loaded.',
});
});
it('should show an error if tool registry is not available', async () => {
mockConfig.getToolRegistry = vi.fn().mockResolvedValue(undefined);
const refreshCommand = mcpCommand.subCommands?.find(
(cmd) => cmd.name === 'refresh',
);
const result = await refreshCommand!.action!(mockContext, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Could not retrieve tool registry.',
});
});
});
}); });

View File

@ -417,12 +417,57 @@ const listCommand: SlashCommand = {
}, },
}; };
const refreshCommand: SlashCommand = {
name: 'refresh',
description: 'Refresh the list of MCP servers and tools',
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
): Promise<SlashCommandActionReturn> => {
const { config } = context.services;
if (!config) {
return {
type: 'message',
messageType: 'error',
content: 'Config not loaded.',
};
}
const toolRegistry = await config.getToolRegistry();
if (!toolRegistry) {
return {
type: 'message',
messageType: 'error',
content: 'Could not retrieve tool registry.',
};
}
context.ui.addItem(
{
type: 'info',
text: 'Refreshing MCP servers and tools...',
},
Date.now(),
);
await toolRegistry.discoverMcpTools();
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
if (geminiClient) {
await geminiClient.setTools();
}
return getMcpStatus(context, false, false, false);
},
};
export const mcpCommand: SlashCommand = { export const mcpCommand: SlashCommand = {
name: 'mcp', name: 'mcp',
description: description:
'list configured MCP servers and tools, or authenticate with OAuth-enabled servers', 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers',
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [listCommand, authCommand], subCommands: [listCommand, authCommand, refreshCommand],
// Default action when no subcommand is provided // Default action when no subcommand is provided
action: async (context: CommandContext, args: string) => action: async (context: CommandContext, args: string) =>
// If no subcommand, run the list command // If no subcommand, run the list command

View File

@ -23,7 +23,7 @@ import { GitService } from '../services/gitService.js';
vi.mock('../tools/tool-registry', () => { vi.mock('../tools/tool-registry', () => {
const ToolRegistryMock = vi.fn(); const ToolRegistryMock = vi.fn();
ToolRegistryMock.prototype.registerTool = vi.fn(); ToolRegistryMock.prototype.registerTool = vi.fn();
ToolRegistryMock.prototype.discoverTools = vi.fn(); ToolRegistryMock.prototype.discoverAllTools = vi.fn();
ToolRegistryMock.prototype.getAllTools = vi.fn(() => []); // Mock methods if needed ToolRegistryMock.prototype.getAllTools = vi.fn(() => []); // Mock methods if needed
ToolRegistryMock.prototype.getTool = vi.fn(); ToolRegistryMock.prototype.getTool = vi.fn();
ToolRegistryMock.prototype.getFunctionDeclarations = vi.fn(() => []); ToolRegistryMock.prototype.getFunctionDeclarations = vi.fn(() => []);

View File

@ -630,7 +630,7 @@ export class Config {
registerCoreTool(MemoryTool); registerCoreTool(MemoryTool);
registerCoreTool(WebSearchTool, this); registerCoreTool(WebSearchTool, this);
await registry.discoverTools(); await registry.discoverAllTools();
return registry; return registry;
} }
} }

View File

@ -312,7 +312,7 @@ describe('ToolRegistry', () => {
return mockChildProcess as any; return mockChildProcess as any;
}); });
await toolRegistry.discoverTools(); await toolRegistry.discoverAllTools();
const discoveredTool = toolRegistry.getTool('tool-with-bad-format'); const discoveredTool = toolRegistry.getTool('tool-with-bad-format');
expect(discoveredTool).toBeDefined(); expect(discoveredTool).toBeDefined();
@ -338,7 +338,7 @@ describe('ToolRegistry', () => {
}; };
vi.spyOn(config, 'getMcpServers').mockReturnValue(mcpServerConfigVal); vi.spyOn(config, 'getMcpServers').mockReturnValue(mcpServerConfigVal);
await toolRegistry.discoverTools(); await toolRegistry.discoverAllTools();
expect(mockDiscoverMcpTools).toHaveBeenCalledWith( expect(mockDiscoverMcpTools).toHaveBeenCalledWith(
mcpServerConfigVal, mcpServerConfigVal,
@ -360,7 +360,7 @@ describe('ToolRegistry', () => {
}; };
vi.spyOn(config, 'getMcpServers').mockReturnValue(mcpServerConfigVal); vi.spyOn(config, 'getMcpServers').mockReturnValue(mcpServerConfigVal);
await toolRegistry.discoverTools(); await toolRegistry.discoverAllTools();
expect(mockDiscoverMcpTools).toHaveBeenCalledWith( expect(mockDiscoverMcpTools).toHaveBeenCalledWith(
mcpServerConfigVal, mcpServerConfigVal,

View File

@ -153,8 +153,9 @@ export class ToolRegistry {
/** /**
* Discovers tools from project (if available and configured). * Discovers tools from project (if available and configured).
* Can be called multiple times to update discovered tools. * Can be called multiple times to update discovered tools.
* This will discover tools from the command line and from MCP servers.
*/ */
async discoverTools(): Promise<void> { async discoverAllTools(): Promise<void> {
// remove any previously discovered tools // remove any previously discovered tools
for (const tool of this.tools.values()) { for (const tool of this.tools.values()) {
if (tool instanceof DiscoveredTool || tool instanceof DiscoveredMCPTool) { if (tool instanceof DiscoveredTool || tool instanceof DiscoveredMCPTool) {
@ -173,6 +174,28 @@ export class ToolRegistry {
); );
} }
/**
* Discovers tools from project (if available and configured).
* Can be called multiple times to update discovered tools.
* This will NOT discover tools from the command line, only from MCP servers.
*/
async discoverMcpTools(): Promise<void> {
// remove any previously discovered tools
for (const tool of this.tools.values()) {
if (tool instanceof DiscoveredMCPTool) {
this.tools.delete(tool.name);
}
}
// discover tools using MCP servers, if configured
await discoverMcpTools(
this.config.getMcpServers() ?? {},
this.config.getMcpServerCommand(),
this,
this.config.getDebugMode(),
);
}
/** /**
* Discover or re-discover tools for a single MCP server. * Discover or re-discover tools for a single MCP server.
* @param serverName - The name of the server to discover tools from. * @param serverName - The name of the server to discover tools from.