import { type Static, Type } from "typebox"; import { Check, Errors } from "typebox/value"; /** * MCP bridge for eval cells (port of pi-fabric's McpProvider call surface: * server/tool listing and explicit server.tool calls). * * The host does not expose a fixed MCP tool set, so the bridge routes through * a pluggable {@link McpClient}. The reserved tool name is RESERVED_MCP_TOOL * ("__mcp__"); preludes expose it as `mcp.list()` and `mcp.call(server, tool, * args)`. Sessions without a wired client list no servers, and any call * reports a clear unavailability error. */ /** One tool on an MCP server, mirroring pi-fabric's tool descriptor fields. */ export interface McpToolInfo { readonly name: string; readonly description?: string; readonly inputSchema?: unknown; } /** One MCP server with its tool list, as returned by `mcp.list()`. */ export interface McpServerTools { readonly name: string; readonly tools: readonly McpToolInfo[]; } /** * Pluggable MCP client backing the reserved bridge. The host wires a real * client (mcporter-backed, as in pi-fabric); tests inject fakes. */ export interface McpClient { readonly listServers: () => readonly string[] | Promise; readonly listTools: (server: string) => Promise; readonly callTool: ( server: string, tool: string, args: Readonly> ) => Promise; } export interface RunEvalMcpOptions { /** MCP client; undefined when the session has no MCP wiring. */ readonly client: McpClient | undefined; readonly signal?: AbortSignal; } const mcpArgsSchema = Type.Object( { op: Type.Union([Type.Literal("list"), Type.Literal("call")]), server: Type.Optional(Type.String({ minLength: 1 })), tool: Type.Optional(Type.String({ minLength: 1 })), args: Type.Optional(Type.Record(Type.String(), Type.Unknown())), }, { additionalProperties: false } ); type McpArgs = Static; class McpArgumentsError extends Error { readonly name = "McpArgumentsError"; constructor(summary: string) { super(`mcp() received invalid arguments: ${summary}`); } } class McpUnavailableError extends Error { readonly name = "McpUnavailableError"; constructor() { super("mcp() unavailable: no MCP client is registered in this session"); } } const normalizeMcpResult = (result: unknown): unknown => { if (typeof result !== "object" || result === null || Array.isArray(result)) { return result; } const record = result as Record; if (!Array.isArray(record.content)) { return result; } const text = record.content .filter( (part): part is { type: "text"; text: string } => typeof part === "object" && part !== null && (part as Record).type === "text" && typeof (part as Record).text === "string" ) .map((part) => part.text) .join("\n"); if (record.isError === true) { throw new Error(text || "MCP tool returned an error"); } return { text, content: record.content, structuredContent: record.structuredContent ?? null, }; }; export async function runEvalMcp( args: unknown, options: RunEvalMcpOptions ): Promise { const parsed = parseMcpArgs(args); const client = options.client; if (parsed.op === "list") { if (!client) { return []; } const servers = await client.listServers(); const settled = await Promise.allSettled( servers.map(async (server) => ({ name: server, tools: await client.listTools(server), })) ); return settled.flatMap((entry) => entry.status === "fulfilled" ? [entry.value] : [] ); } if (!client) { throw new McpUnavailableError(); } if (options.signal?.aborted) { throw new Error("MCP call cancelled"); } const server = parsed.server; const tool = parsed.tool; if (server === undefined || tool === undefined) { throw new McpArgumentsError("call requires server and tool"); } const servers = await client.listServers(); if (!servers.includes(server)) { throw new Error(`Unknown MCP server: ${server}`); } const tools = await client.listTools(server); if (!tools.some((entry) => entry.name === tool)) { throw new Error(`Unknown MCP tool: ${server}.${tool}`); } if (options.signal?.aborted) { throw new Error("MCP call cancelled"); } const result = await client.callTool(server, tool, parsed.args ?? {}); return normalizeMcpResult(result); } function parseMcpArgs(value: unknown): McpArgs { if (Check(mcpArgsSchema, value)) { return value; } const summary = Errors(mcpArgsSchema, value) .map((error) => `${error.instancePath || "/"} ${error.message}`) .join("; "); throw new McpArgumentsError(summary || "invalid value"); }