diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 26c3ed49..f904c51d 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -27,6 +27,9 @@ Slash commands provide meta-level control over the CLI itself. They can typicall - **`nodesc`** or **`nodescriptions`**: - **Description:** Hides tool descriptions, showing only the tool names. - **Action:** Displays a compact list with only tool names. + - **`schema`**: + - **Description:** Shows full schema of tool parameters. + - **Action:** Displays the full JSON schema for the tool's configured parameters. - **Keyboard Shortcut:** Press **Ctrl+T** at any time to toggle between showing and hiding tool descriptions. - **`/clear`** (Shortcut: **Ctrl+L**) diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 845cbe92..1bcfbdf5 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -1019,6 +1019,86 @@ Add any other context about the problem here. }); }); + 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: SlashCommandActionReturn | boolean = 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).toBe(true); + }); + }); + describe('/compress command', () => { it('should call tryCompressChat(true)', async () => { const { handleSlashCommand } = getProcessor(); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index f03761ff..a4b13d0a 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -238,6 +238,11 @@ export const useSlashCommandProcessor = ( } 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) { @@ -319,22 +324,18 @@ export const useSlashCommandProcessor = ( } // Add server description with proper handling of multi-line descriptions - if (useShowDescriptions && server?.description) { + if ((useShowDescriptions || useShowSchema) && server?.description) { const greenColor = '\u001b[32m'; const resetColor = '\u001b[0m'; - const descLines = server.description.split('\n'); - message += `: ${greenColor}${descLines[0]}${resetColor}`; - message += '\n'; - - // If there are multiple lines, add proper indentation for each line - if (descLines.length > 1) { - for (let i = 1; i < descLines.length; i++) { - // Skip empty lines at the end - if (i === descLines.length - 1 && descLines[i].trim() === '') - continue; + const descLines = server.description.trim().split('\n'); + if (descLines) { + message += ':\n'; + for (let i = 0; i < descLines.length; i++) { message += ` ${greenColor}${descLines[i]}${resetColor}\n`; } + } else { + message += '\n'; } } else { message += '\n'; @@ -345,35 +346,52 @@ export const useSlashCommandProcessor = ( if (serverTools.length > 0) { serverTools.forEach((tool) => { - if (useShowDescriptions && tool.description) { + if ( + (useShowDescriptions || useShowSchema) && + tool.description + ) { // Format tool name in cyan using simple ANSI cyan color - message += ` - \u001b[36m${tool.name}\u001b[0m: `; + 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.split('\n'); - message += `${greenColor}${descLines[0]}${resetColor}\n`; - - // If there are multiple lines, add proper indentation for each line - if (descLines.length > 1) { - for (let i = 1; i < descLines.length; i++) { - // Skip empty lines at the end - if ( - i === descLines.length - 1 && - descLines[i].trim() === '' - ) - continue; + const descLines = tool.description.trim().split('\n'); + if (descLines) { + message += ':\n'; + for (let i = 0; i < descLines.length; i++) { message += ` ${greenColor}${descLines[i]}${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 (let i = 0; i < paramsLines.length; i++) { + message += ` ${greenColor}${paramsLines[i]}${resetColor}\n`; + } + } + } }); } else { message += ' No tools available\n';