diff --git a/apps/api/src/mcp/__tests__/mcp.controller.spec.ts b/apps/api/src/mcp/__tests__/mcp.controller.spec.ts new file mode 100644 index 00000000..73f4a097 --- /dev/null +++ b/apps/api/src/mcp/__tests__/mcp.controller.spec.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { HttpException } from '@nestjs/common'; +import { McpController } from '../mcp.controller'; + +describe('McpController', () => { + let client: { getInfoParsed: jest.Mock }; + let registry: { get: jest.Mock }; + let controller: McpController; + + beforeEach(() => { + client = { + getInfoParsed: jest.fn().mockResolvedValue({ memory: { used_memory: '1024' } }), + }; + registry = { + get: jest.fn().mockReturnValue(client), + }; + + controller = new McpController( + registry as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + ); + }); + + it('passes the requested INFO section to the connection client', async () => { + await expect(controller.getInfo('conn-1', 'memory')).resolves.toEqual({ + memory: { used_memory: '1024' }, + }); + + expect(registry.get).toHaveBeenCalledWith('conn-1'); + expect(client.getInfoParsed).toHaveBeenCalledWith(['memory']); + }); + + it('requests all INFO sections when no section is provided', async () => { + await controller.getInfo('conn-1', undefined); + + expect(client.getInfoParsed).toHaveBeenCalledWith(undefined); + }); + + it('wraps INFO lookup failures in an HTTP exception', async () => { + client.getInfoParsed.mockRejectedValueOnce(new Error('connection failed')); + + await expect(controller.getInfo('conn-1', 'stats')).rejects.toBeInstanceOf(HttpException); + }); +}); diff --git a/apps/api/src/mcp/mcp.controller.ts b/apps/api/src/mcp/mcp.controller.ts index c7e4a8f5..caea1266 100644 --- a/apps/api/src/mcp/mcp.controller.ts +++ b/apps/api/src/mcp/mcp.controller.ts @@ -56,10 +56,14 @@ export class McpController { } @Get('instance/:id/info') - async getInfo(@Param('id', ValidateInstanceIdPipe) id: string) { + async getInfo( + @Param('id', ValidateInstanceIdPipe) id: string, + @Query('section') section?: string, + ) { try { const client = this.registry.get(id); - const info = await client.getInfoParsed(); + const sections = section ? [section] : undefined; + const info = await client.getInfoParsed(sections); return info; } catch (error) { this.logger.error(`Failed to get info for ${id}`, error instanceof Error ? error.stack : error); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 853af24f..5bcf7b88 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -346,12 +346,8 @@ server.tool( }, async ({ section, instanceId }) => withTelemetry('get_info', async () => { const id = resolveInstanceId(instanceId); - const data = await apiFetch(`/mcp/instance/${id}/info`) as Record; - if (section && data[section] !== undefined) { - return { - content: [{ type: 'text' as const, text: JSON.stringify({ [section]: data[section] }, null, 2) }], - }; - } + const query = section ? `?section=${encodeURIComponent(section)}` : ''; + const data = await apiFetch(`/mcp/instance/${id}/info${query}`) as Record; return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], };