import { INestApplication } from '@nestjs/common'; import request from 'supertest'; import { buildTestApp } from './app.harness'; /** * MCP controller integration tests. * * The MCP endpoint speaks the Model Context Protocol (streamable HTTP * transport). We test: * - that the endpoint is reachable and returns a recognised MCP response * - that tool invocations for read-only, non-browser tools work end-to-end * - that tools with bad inputs return error payloads (not HTTP 5xx) * * Browser-dependent tools (open_url, exec_code) require a live Playwright * session and are not covered here. */ describe('McpController', () => { let app: INestApplication; beforeAll(async () => { app = await buildTestApp(); }); afterAll(async () => { await app.close(); }); /** Send a single MCP tool call and return the parsed response body. */ async function mcpCall(toolName: string, args: Record = {}) { const res = await request(app.getHttpServer()) .post('/mcp') .set('Content-Type', 'application/json') .set('Accept', 'application/json, text/event-stream') .send({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: toolName, arguments: args }, }); return res; } // ── Connectivity ─────────────────────────────────────────────────────────── describe('POST /mcp — connectivity', () => { it('is reachable and returns a non-5xx status', async () => { const res = await request(app.getHttpServer()) .post('/mcp') .set('Content-Type', 'application/json') .set('Accept', 'application/json, text/event-stream') .send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '0' }, }, }); expect(res.status).toBeLessThan(500); }); }); // ── list_keys tool ───────────────────────────────────────────────────────── describe('list_keys', () => { it('returns a result with text content containing a JSON array', async () => { const res = await mcpCall('list_keys'); // MCP may respond with 200 (JSON body) or SSE stream; both are acceptable expect(res.status).toBeLessThan(500); if (res.status === 200 && res.body?.result) { const text = res.body.result.content?.[0]?.text; expect(() => JSON.parse(text)).not.toThrow(); expect(Array.isArray(JSON.parse(text))).toBe(true); } }); }); // ── list_sessions tool ───────────────────────────────────────────────────── describe('list_sessions', () => { it('returns a result with text content containing a JSON array', async () => { const res = await mcpCall('list_sessions'); expect(res.status).toBeLessThan(500); if (res.status === 200 && res.body?.result) { const text = res.body.result.content?.[0]?.text; expect(() => JSON.parse(text)).not.toThrow(); expect(Array.isArray(JSON.parse(text))).toBe(true); } }); }); // ── list_environments tool ───────────────────────────────────────────────── describe('list_environments', () => { it('returns a result with text content containing a JSON array', async () => { const res = await mcpCall('list_environments'); expect(res.status).toBeLessThan(500); if (res.status === 200 && res.body?.result) { const text = res.body.result.content?.[0]?.text; expect(() => JSON.parse(text)).not.toThrow(); expect(Array.isArray(JSON.parse(text))).toBe(true); } }); }); // ── create_environment tool ──────────────────────────────────────────────── describe('create_environment', () => { it('creates an environment via MCP', async () => { const res = await mcpCall('create_environment', { name: 'mcp-test-env', urls: { id_url: 'https://id.example.com' }, }); expect(res.status).toBeLessThan(500); if (res.status === 200 && res.body?.result) { const text = res.body.result.content?.[0]?.text; const created = JSON.parse(text); expect(created.name).toBe('mcp-test-env'); } }); }); // ── delete_session tool with unknown id ──────────────────────────────────── describe('delete_session', () => { it('returns an MCP error result for a non-existent session id', async () => { const res = await mcpCall('delete_session', { id: 999999 }); expect(res.status).toBeLessThan(500); // MCP wraps service errors as isError:true content, not HTTP errors if (res.status === 200 && res.body?.result) { expect(res.body.result.isError).toBe(true); } }); }); });