/** * MCP test harness. * * Creates an McpServer + MCP Client connected via InMemoryTransport for unit testing * tools and resources without HTTP overhead. * * Usage: * const harness = await createMcpHarness({ userId, registerTools: true }); * const result = await harness.client.callTool({ name: 'create_trip', arguments: { title: 'Test' } }); * await harness.cleanup(); */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { Client } from '@modelcontextprotocol/sdk/client/index'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'; import { registerResources } from '../../src/mcp/resources'; import { registerTools } from '../../src/mcp/tools'; export interface McpHarness { client: Client; server: McpServer; cleanup: () => Promise; } export interface McpHarnessOptions { userId: number; /** Register read-only resources (default: true) */ withResources?: boolean; /** Register read-write tools (default: true) */ withTools?: boolean; } export async function createMcpHarness(options: McpHarnessOptions): Promise { const { userId, withResources = true, withTools = true } = options; const server = new McpServer({ name: 'trek-test', version: '1.0.0' }); if (withResources) registerResources(server, userId); if (withTools) registerTools(server, userId); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: 'test-client', version: '1.0.0' }); await server.connect(serverTransport); await client.connect(clientTransport); const cleanup = async () => { try { await client.close(); } catch { /* ignore */ } try { await server.close(); } catch { /* ignore */ } }; return { client, server, cleanup }; } /** Parse JSON from a callTool result (first text content item). */ export function parseToolResult(result: Awaited>): unknown { const text = result.content.find((c: { type: string }) => c.type === 'text') as { type: 'text'; text: string } | undefined; if (!text) throw new Error('No text content in tool result'); return JSON.parse(text.text); } /** Parse JSON from a readResource result (first content item). */ export function parseResourceResult(result: Awaited>): unknown { const item = result.contents[0] as { text?: string } | undefined; if (!item?.text) throw new Error('No text content in resource result'); return JSON.parse(item.text); }