- add list_scenarios, get_scenario, create_scenario, update_scenario, delete_scenario - add create_scenario_step, get_scenario_step, update_scenario_step, delete_scenario_step - add list_scenario_runs, run_scenario - import ScenarioModule into McpModule so ScenarioService is injectable - move tool registration to private registerTools(server) method; create fresh McpServer per request in handle() to satisfy SDK one-connection rule - fix mcp.controller.spec: parse SSE data line instead of res.body; assert exact status 200 everywhere; remove vague toBeLessThan(500) guards
132 lines
5.3 KiB
TypeScript
132 lines
5.3 KiB
TypeScript
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();
|
|
});
|
|
|
|
/** Parse the JSON-RPC payload from an SSE response body. */
|
|
function parseSse(text: string): Record<string, unknown> {
|
|
const match = text.match(/^data:\s*(.+)$/m);
|
|
if (!match) throw new Error(`No SSE data line found in: ${text}`);
|
|
return JSON.parse(match[1]) as Record<string, unknown>;
|
|
}
|
|
|
|
/** Send a single MCP tool call and return the parsed response body. */
|
|
async function mcpCall(toolName: string, args: Record<string, unknown> = {}) {
|
|
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 { status: res.status, rpc: parseSse(res.text) };
|
|
}
|
|
|
|
// ── 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).toBe(200);
|
|
});
|
|
});
|
|
|
|
// ── list_keys tool ─────────────────────────────────────────────────────────
|
|
|
|
describe('list_keys', () => {
|
|
it('returns a result with text content containing a JSON array', async () => {
|
|
const { status, rpc } = await mcpCall('list_keys');
|
|
expect(status).toBe(200);
|
|
const result = rpc.result as { content: { text: string }[] };
|
|
expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── list_sessions tool ─────────────────────────────────────────────────────
|
|
|
|
describe('list_sessions', () => {
|
|
it('returns a result with text content containing a JSON array', async () => {
|
|
const { status, rpc } = await mcpCall('list_sessions');
|
|
expect(status).toBe(200);
|
|
const result = rpc.result as { content: { text: string }[] };
|
|
expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── list_environments tool ─────────────────────────────────────────────────
|
|
|
|
describe('list_environments', () => {
|
|
it('returns a result with text content containing a JSON array', async () => {
|
|
const { status, rpc } = await mcpCall('list_environments');
|
|
expect(status).toBe(200);
|
|
const result = rpc.result as { content: { text: string }[] };
|
|
expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── create_environment tool ────────────────────────────────────────────────
|
|
|
|
describe('create_environment', () => {
|
|
it('creates an environment via MCP', async () => {
|
|
const { status, rpc } = await mcpCall('create_environment', {
|
|
name: 'mcp-test-env',
|
|
urls: { id_url: 'https://id.example.com' },
|
|
});
|
|
expect(status).toBe(200);
|
|
const result = rpc.result as { content: { text: string }[] };
|
|
const created = JSON.parse(result.content[0].text) as { name: string };
|
|
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 { status, rpc } = await mcpCall('delete_session', { id: 999999 });
|
|
expect(status).toBe(200);
|
|
// MCP wraps service errors as isError:true content, not HTTP errors
|
|
const result = rpc.result as { isError: boolean };
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|
|
});
|