import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; export type ToolCallHost = { readonly callServerTool: (params: { name: string; arguments?: Record; }) => Promise; }; export type TrustedInteraction = { executionId: string; interaction: { kind?: unknown; message?: unknown; url?: unknown; requestedSchema?: unknown; /** * The paused call's tool address (`executor.coreTools.policies.create`), * put on the wire by `formatPausedExecution`. It is the only stable * identity a pause carries — the execution id is per-call — so it is what * the shell keys a remembered approval by. */ address?: unknown; }; }; export type TrustedInteractionResponse = { action: "accept" | "decline" | "cancel"; content?: Record; }; export type RequestTrustedInteraction = ( interaction: TrustedInteraction, ) => Promise; const TOOL_PATH_SEGMENT = /^[A-Za-z_$][\w$]*$/; /** * The ONE grammar the shell ever puts on the `execute-action` wire: * * return await tools.("")?(.)*() * * A single proxy-shaped tool call, nothing else — no statements, no loops, no * composition. The server parses `execute-action` against exactly this shape * (`parseToolCallCode` in `@executor-js/host-mcp`), so an iframe cannot smuggle * arbitrary code through the app channel even though the shell UI never offers * a way to write any. `tool-call-grammar.pin.test.ts` pins the two together. * * The path's head is an INTEGRATION, and the optional role tags which of the * artifact's connections for that integration to use. Neither the tier nor the * connection name is ever on this wire: the server resolves them from the * bindings stored on the artifact row. That is the whole point of the short * form — an iframe that fabricated an address would be naming something the * artifact was never bound to, and the server would refuse it. * * Role and args are both `JSON.stringify` output, so the emission stays * parseable and a role containing a quote cannot break out of the call. */ export function toolCallCode( path: readonly string[], args: readonly unknown[], role?: string, ): string { if (path.length === 0) throw new Error("Invalid tool path."); const parts = path.map((part) => { if (typeof part !== "string" || !TOOL_PATH_SEGMENT.test(part)) { throw new Error("Invalid tool path."); } return part; }); if (role !== undefined && (typeof role !== "string" || role.length === 0)) { throw new Error("Invalid tool role."); } const [head, ...rest] = parts; const tag = role === undefined ? "" : `(${JSON.stringify(role)})`; const trailer = rest.length > 0 ? `.${rest.join(".")}` : ""; return `return await tools.${head}${tag}${trailer}(${JSON.stringify(args[0] ?? {})})`; } /** * Calls one tool through the MCP Apps bridge, resolving any shell-owned * approval the execution pauses for. * * `tools.github.issues.create({ title: "Bug" })` in the generated iframe arrives * here as `(["github","issues","create"], [{ title: "Bug" }])` and becomes * `execute-action` with * `code: "return await tools.github.issues.create({\"title\":\"Bug\"})"`. * * `artifactId` rides alongside the code because the server cannot resolve the * call without it: the path names an integration role, and the bindings that * turn a role into a connection live on the artifact row. It is supplied by the * host (from the tool input the artifact was delivered with), never by the * generated component — and the server checks the caller owns it. */ export function createToolCaller( app: ToolCallHost, requestTrustedInteraction: RequestTrustedInteraction, getArtifactId?: () => string | undefined, ): (path: readonly string[], args: readonly unknown[], role?: string) => Promise { return (path, args, role) => { const artifactId = getArtifactId?.(); return app .callServerTool({ name: "execute-action", arguments: { code: toolCallCode(path, args, role), ...(artifactId === undefined ? {} : { artifactId }), }, }) .then((r) => resolveToolResult(app, r, requestTrustedInteraction)); }; } async function resolveToolResult( app: ToolCallHost, result: CallToolResult, requestTrustedInteraction: RequestTrustedInteraction, ): Promise { 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 | 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 | 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: , logs: [...] }`. * Return just the inner result value. */ function unwrapResult(structured: Record | 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; } }