154 lines · 4.6 KB · TypeScript
RawDownload
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; export type ToolCallHost = {  readonly callServerTool: (params: {    name: string;    arguments?: Record<string, unknown>;  }) => Promise<CallToolResult>;}; export type TrustedInteraction = {  executionId: string;  interaction: {    kind?: unknown;    message?: unknown;    url?: unknown;    requestedSchema?: unknown;  };}; export type TrustedInteractionResponse = {  action: "accept" | "decline" | "cancel";  content?: Record<string, unknown>;}; export type RequestTrustedInteraction = (  interaction: TrustedInteraction,) => Promise<TrustedInteractionResponse>; /** * Creates a tRPC-style recursive proxy that maps dotted tool paths * to execute-action calls through the MCP Apps bridge. * * Usage: tools.github.issues.create({ title: "Bug" }) * becomes: app.callServerTool("execute-action", { code: "return await tools.github.issues.create({\"title\":\"Bug\"})" }) */export function createToolsProxy(  app: ToolCallHost,  requestTrustedInteraction: RequestTrustedInteraction,): Record<string, unknown> {  function nest(path: string[]): unknown {    return new Proxy(function () {}, {      get(_target, key: string) {        if (key === "then" || key === "toJSON" || key === (Symbol.toPrimitive as unknown)) {          return undefined;        }        return nest([...path, key]);      },      apply(_target, _thisArg, args: unknown[]) {        const toolPath = path.join(".");        const serializedArgs = args.length > 0 ? JSON.stringify(args[0]) : "{}";        const code = `return await tools.${toolPath}(${serializedArgs})`;         return app          .callServerTool({            name: "execute-action",            arguments: { code },          })          .then((r) => resolveToolResult(app, r, requestTrustedInteraction));      },    });  }   return nest([]) as Record<string, unknown>;} /** * Creates the `run()` escape hatch for multi-step tool composition. * * Usage: const result = await run(` *   const me = await tools.github.users.me() *   return await tools.github.issues.create({ assignee: me.login, ... }) * `) */export function createRunFn(  app: ToolCallHost,  requestTrustedInteraction: RequestTrustedInteraction,): (code: string) => Promise<unknown> {  return (code: string) =>    app      .callServerTool({        name: "execute-action",        arguments: { code },      })      .then((r) => resolveToolResult(app, r, requestTrustedInteraction));} async function resolveToolResult(  app: ToolCallHost,  result: CallToolResult,  requestTrustedInteraction: RequestTrustedInteraction,): Promise<unknown> {  if (result.isError) {    const msg = result.content?.find((c) => c.type === "text")?.text ?? "Tool call failed";    throw new Error(msg);  }   const structured = result.structuredContent as Record<string, unknown> | undefined;  const pending = parseTrustedInteraction(structured);  if (pending) {    const response = await requestTrustedInteraction(pending);    const resumed = await app.callServerTool({      name: "execute-action-resume",      arguments: {        executionId: pending.executionId,        action: response.action,        content: JSON.stringify(response.content ?? {}),      },    });    return resolveToolResult(app, resumed, requestTrustedInteraction);  }   return unwrapResult(structured) ?? parseTextContent(result);} function parseTrustedInteraction(  structured: Record<string, unknown> | undefined,): TrustedInteraction | null {  if (!structured || structured.status !== "waiting_for_interaction") return null;  if (typeof structured.executionId !== "string") return null;  const interaction =    typeof structured.interaction === "object" &&    structured.interaction !== null &&    !Array.isArray(structured.interaction)      ? (structured.interaction as TrustedInteraction["interaction"])      : {};  return { executionId: structured.executionId, interaction };} /** * Unwrap execution result. The kernel wraps results as * `{ status: "completed", result: <actual>, logs: [...] }`. * Return just the inner result value. */function unwrapResult(structured: Record<string, unknown> | undefined | null): unknown {  if (    structured &&    typeof structured === "object" &&    "status" in structured &&    "result" in structured  ) {    return structured.result;  }  return structured;} function parseTextContent(r: { content?: Array<{ type: string; text?: string }> }): unknown {  const text = r.content?.find((c) => c.type === "text")?.text;  if (!text) return null;  try {    return JSON.parse(text);  } catch {    return text;  }}