mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
f2908fdd65
Covers OAuth integration flow, scope enforcement, addon-gated tool access, oauthService unit tests, and budget reorder/permission/reservation-sync scenarios.
73 lines
2.7 KiB
TypeScript
73 lines
2.7 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;
|
|
/** OAuth scopes to restrict tools; null = full access (default: null) */
|
|
scopes?: string[] | null;
|
|
/** Whether the session is authenticated via a static API token (default: false) */
|
|
isStaticToken?: boolean;
|
|
}
|
|
|
|
export async function createMcpHarness(options: McpHarnessOptions): Promise<McpHarness> {
|
|
const { userId, withResources = true, withTools = true, scopes = null, isStaticToken = false } = options;
|
|
|
|
const server = new McpServer({ name: 'trek-test', version: '1.0.0' });
|
|
|
|
if (withResources) registerResources(server, userId);
|
|
if (withTools) registerTools(server, userId, scopes ?? null, isStaticToken);
|
|
|
|
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);
|
|
}
|