Replace all db.prepare() calls in mcp/index.ts, mcp/resources.ts, and mcp/tools.ts with calls to the service layer. Add missing service functions: - authService: isDemoUser, verifyMcpToken, verifyJwtToken - adminService: isAddonEnabled - atlasService: listVisitedCountries - tripService: getTripSummary, listTrips with null archived param Also fix getAssignmentWithPlace and formatAssignmentWithPlace to expose place_id, assignment_time, and assignment_end_time at the top level, and fix updateDay to correctly handle null title for clearing. Add comprehensive unit and integration test suite for the MCP layer (821 tests all passing).
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
/**
|
|
* 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<void>;
|
|
}
|
|
|
|
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<McpHarness> {
|
|
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<ReturnType<Client['callTool']>>): 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<ReturnType<Client['readResource']>>): 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);
|
|
}
|