UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

339 lines (338 loc) 16.8 kB
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js"; import { r as racePromiseWithAbortSignal } from "./abort-signal-D2k14JsD.js"; import { m as resolveAgentWorkspaceDir, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import { O as parseAgentSessionKey, f as resolveAgentIdFromSessionKey } from "./session-key-BnWWjqNc.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import "./agent-scope-DbtJyKUL.js"; import { i as logWarn } from "./logger-DwECwNVZ.js"; import { o as getSessionMcpRequestSignal } from "./mcp-app-model-context-dSmaJhyr.js"; import { c as releaseSessionMcpRuntime, n as acquireSessionMcpRuntime, r as completeDeferredSessionMcpRuntimeRetirement, s as peekSessionMcpRuntime } from "./agent-bundle-mcp-manager-api-DCHQm4w1.js"; import { a as getMcpAppViewLeaseForSession, i as getMcpAppViewLease, r as fetchMcpAppView, t as acquireMcpAppViewRequest } from "./mcp-ui-resource-BIyljeqN.js"; import { m as visitSessionMessagesAsync } from "./session-transcript-readers-CYDRQsH5.js"; import { i as loadGatewaySessionEntryReadOnly } from "./session-utils-store-CInT2loy.js"; import "./session-utils-Cai0_C6U.js"; import { CallToolRequestSchema, ContentBlockSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; //#region src/gateway/mcp-app-reconstruction.ts const MCP_APP_RESTORE_IN_FLIGHT_KEY = Symbol.for("openclaw.mcpAppRestoreInFlight"); function readDescriptor(value) { const record = asOptionalRecord(value); const viewId = normalizeOptionalString(record?.viewId); const serverName = normalizeOptionalString(record?.serverName); const toolName = normalizeOptionalString(record?.toolName); const uiResourceUri = normalizeOptionalString(record?.uiResourceUri); const toolCallId = normalizeOptionalString(record?.toolCallId); const rawResultMetaState = record?.resultMetaState; const resultMetaState = rawResultMetaState === "unavailable" ? rawResultMetaState : void 0; if (!viewId || viewId.length > 128 || !serverName || serverName.length > 256 || !toolName || toolName.length > 256 || !uiResourceUri?.startsWith("ui://") || uiResourceUri.length > 2048 || !toolCallId || toolCallId.length > 512 || rawResultMetaState !== void 0 && resultMetaState === void 0) return; return { viewId, serverName, toolName, uiResourceUri, toolCallId, ...resultMetaState ? { resultMetaState } : {} }; } function readToolInputFromMessage(value, toolCallId, modelToolName) { const message = asOptionalRecord(value); if (normalizeOptionalString(message?.role)?.toLowerCase() !== "assistant") return; const content = Array.isArray(message?.content) ? message.content : []; for (const blockValue of content) { const block = asOptionalRecord(blockValue); if ((normalizeOptionalString(block?.id) ?? normalizeOptionalString(block?.toolCallId)) !== toolCallId) continue; const type = normalizeOptionalString(block?.type)?.toLowerCase(); if (type !== "toolcall" && type !== "tool_call" && type !== "tooluse" && type !== "tool_use") continue; if ((normalizeOptionalString(block?.name) ?? normalizeOptionalString(block?.toolName) ?? normalizeOptionalString(block?.tool_name)) !== modelToolName) continue; return { found: true, input: block?.arguments ?? block?.input ?? block?.args ?? {} }; } } function readCallToolResult(message, details) { return { content: Array.isArray(message.content) ? message.content.flatMap((value) => { const parsed = ContentBlockSchema.safeParse(value); return parsed.success ? [parsed.data] : []; }) : [], ...details.structuredContent !== void 0 ? { structuredContent: details.structuredContent } : {}, ...message.isError === true || details.status === "error" ? { isError: true } : {} }; } function matchesLookup(rawDescriptor, lookup) { if ("viewId" in lookup) return normalizeOptionalString(rawDescriptor?.viewId) === lookup.viewId; const descriptor = lookup.descriptor; return normalizeOptionalString(rawDescriptor?.serverName) === descriptor.serverName && normalizeOptionalString(rawDescriptor?.toolName) === descriptor.toolName && normalizeOptionalString(rawDescriptor?.uiResourceUri) === descriptor.uiResourceUri && normalizeOptionalString(rawDescriptor?.toolCallId) === descriptor.toolCallId; } function readTranscriptResult(value, lookup) { const message = asOptionalRecord(value); if (!message || normalizeOptionalString(message.role)?.toLowerCase() !== "toolresult") return; const details = asOptionalRecord(message.details); if (!details) return; const preview = asOptionalRecord(details.mcpAppPreview); const rawDescriptor = asOptionalRecord(preview?.mcpApp); if (!matchesLookup(rawDescriptor, lookup)) return; const descriptor = readDescriptor(rawDescriptor); const modelToolName = normalizeOptionalString(message.toolName) ?? normalizeOptionalString(message.tool_name); if (!descriptor || !modelToolName) return { kind: "unavailable" }; if (normalizeOptionalString(message.toolCallId) !== descriptor.toolCallId || normalizeOptionalString(details.mcpServer) !== descriptor.serverName || normalizeOptionalString(details.mcpTool) !== descriptor.toolName || descriptor.resultMetaState === "unavailable") return { kind: "unavailable" }; return { kind: "restorable", value: { descriptor, modelToolName, toolResult: readCallToolResult(message, details) } }; } /** Searches the full active transcript without retaining its messages in memory. */ async function findMcpAppReconstructionDataByVisit(visitTranscript, lookup) { let resultRead; let resultIndex = -1; let messageIndex = 0; await visitTranscript((message) => { const read = readTranscriptResult(message, lookup); if (read) { resultRead = read; resultIndex = messageIndex; } messageIndex += 1; }); if (!resultRead || resultRead.kind === "unavailable") return; const resolvedResult = resultRead.value; let toolInput; let foundInput = false; messageIndex = 0; await visitTranscript((message) => { if (messageIndex >= resultIndex) { messageIndex += 1; return; } const input = readToolInputFromMessage(message, resolvedResult.descriptor.toolCallId, resolvedResult.modelToolName); if (input) { foundInput = true; toolInput = input.input; } messageIndex += 1; }); if (!foundInput) return; const { modelToolName: _modelToolName, ...reconstruction } = resolvedResult; return { ...reconstruction, toolInput }; } function getRestoreInFlight() { const state = globalThis; const existing = state[MCP_APP_RESTORE_IN_FLIGHT_KEY]; if (existing) return existing; const created = /* @__PURE__ */ new Map(); state[MCP_APP_RESTORE_IN_FLIGHT_KEY] = created; return created; } async function reconstructMcpAppView(params) { const agentId = params.agentId ?? resolveAgentIdFromSessionKey(params.sessionKey); const loaded = loadGatewaySessionEntryReadOnly(params.sessionKey, { agentId }); const sessionId = loaded.entry?.sessionId; if (!sessionId) return; const transcriptScope = { agentId, sessionId, sessionKey: loaded.canonicalKey, storePath: loaded.storePath, sessionEntry: loaded.entry }; const data = await findMcpAppReconstructionDataByVisit(async (visit) => { await visitSessionMessagesAsync(transcriptScope, visit); }, params.lookup); if (!data) return; const acquisition = await acquireSessionMcpRuntime({ sessionId, sessionKey: loaded.canonicalKey, workspaceDir: resolveAgentWorkspaceDir(params.cfg, agentId), agentDir: resolveAgentDir(params.cfg, agentId), cfg: params.cfg }); const { runtime } = acquisition; try { if (runtime.mcpAppsEnabled !== true) return; const fetched = await fetchMcpAppView({ runtime, agentId, serverName: data.descriptor.serverName, toolName: data.descriptor.toolName, uiResourceUri: data.descriptor.uiResourceUri, toolCallId: data.descriptor.toolCallId, toolInput: data.toolInput, toolResult: data.toolResult, ...params.viewId ? { viewId: params.viewId } : {}, allowedAppToolNames: params.allowedAppToolNames, ...params.authorizeAppInteraction ? { authorizeAppInteraction: params.authorizeAppInteraction } : {}, ...params.readOnly ? { readOnly: true } : {} }); const view = fetched ? getMcpAppViewLease(fetched.viewId, runtime) : void 0; return view ? { runtime, view } : void 0; } finally { await releaseSessionMcpRuntime(acquisition); } } async function restoreMcpAppViewOnce(params) { if (!params.viewId.startsWith("mcp-app-") || params.viewId.length > 128) return; return await reconstructMcpAppView({ ...params, lookup: { viewId: params.viewId }, allowedAppToolNames: /* @__PURE__ */ new Set(), readOnly: true }); } async function mintMcpAppViewFromTranscript(params) { return await reconstructMcpAppView({ cfg: params.cfg, agentId: params.agentId, sessionKey: params.sessionKey, lookup: { descriptor: params.descriptor }, allowedAppToolNames: params.allowedAppToolNames, ...params.authorizeAppInteraction ? { authorizeAppInteraction: params.authorizeAppInteraction } : {}, readOnly: params.readOnly }); } async function restoreMcpAppView(params) { const key = `${params.agentId ?? ""}\0${params.sessionKey}\0${params.viewId}`; const inFlight = getRestoreInFlight(); return await getOrCreatePromise(inFlight, key, () => restoreMcpAppViewOnce(params), { evictOnSettled: true }); } //#endregion //#region src/gateway/mcp-app-operations.ts var McpAppViewExpiredError = class extends Error { constructor() { super("MCP App view expired or is not authorized for this session"); this.name = "McpAppViewExpiredError"; } }; function isAppCallableTool(tool) { return tool.uiVisibility === void 0 || tool.uiVisibility.includes("app"); } function isAppCallableListedTool(tool) { const { _meta: metadata } = tool; const ui = metadata?.ui && typeof metadata.ui === "object" && !Array.isArray(metadata.ui) ? metadata.ui : void 0; const visibility = Array.isArray(ui?.visibility) ? ui.visibility.filter((entry) => entry === "app" || entry === "model") : void 0; return visibility === void 0 || visibility.includes("app"); } function isAllowedByView(view, toolName) { return view.allowedAppToolNames === void 0 || view.allowedAppToolNames.has(toolName); } async function requireMcpAppInteraction(view) { if (view.readOnly === true || view.allowedAppToolNames === void 0) throw new Error("MCP App view is read-only"); if (view.authorizeAppInteraction && !await view.authorizeAppInteraction()) throw new Error("MCP App widget grant is no longer active"); } async function resolveMcpAppAllowedToolNames(active) { if (active.view.readOnly === true || active.view.allowedAppToolNames === void 0) return []; return (await active.runtime.getCatalog()).tools.filter((tool) => tool.serverName === active.view.serverName && isAppCallableTool(tool) && isAllowedByView(active.view, tool.toolName)).map((tool) => tool.toolName).filter((toolName, index, all) => all.indexOf(toolName) === index).toSorted(); } async function getRequestCatalog(runtime) { const signal = getSessionMcpRequestSignal(); signal?.throwIfAborted(); return racePromiseWithAbortSignal(runtime.getCatalog(), signal); } async function requireCallableTool(runtime, view, toolName) { await requireMcpAppInteraction(view); const tool = (await getRequestCatalog(runtime)).tools.find((entry) => entry.serverName === view.serverName && entry.toolName === toolName); if (!tool || !isAppCallableTool(tool) || !isAllowedByView(view, toolName)) throw new Error(`MCP tool "${toolName}" is not app-callable`); } async function resolveMcpAppActiveView(params) { if (params.cfg && params.cfg.mcp?.apps?.enabled !== true) throw new Error("MCP App runtime is unavailable"); const liveView = params.agentId ? getMcpAppViewLeaseForSession(params.viewId, params.sessionKey, params.agentId) : void 0; if (liveView) { if (liveView.runtime.mcpAppsEnabled !== true) throw new Error("MCP App runtime is unavailable"); return { runtime: liveView.runtime, view: liveView }; } const existingRuntime = params.agentId && !parseAgentSessionKey(params.sessionKey) ? void 0 : peekSessionMcpRuntime({ sessionKey: params.sessionKey }); if (existingRuntime && existingRuntime.mcpAppsEnabled !== true) throw new Error("MCP App runtime is unavailable"); const existingView = existingRuntime ? getMcpAppViewLease(params.viewId, existingRuntime) : void 0; const restored = existingRuntime?.mcpAppsEnabled === true && existingView ? { runtime: existingRuntime, view: existingView } : params.cfg ? await restoreMcpAppView({ cfg: params.cfg, agentId: params.agentId, sessionKey: params.sessionKey, viewId: params.viewId }) : void 0; if (!restored) throw new McpAppViewExpiredError(); return restored; } async function withMcpAppActiveView(active, kind, operation) { active.runtime.markUsed(); const release = acquireMcpAppViewRequest(active.view, kind); const releaseRuntimeLease = active.runtime.acquireLease?.(); try { return await operation(); } finally { release(); releaseRuntimeLease?.(); await completeDeferredSessionMcpRuntimeRetirement(active.runtime).catch((error) => { logWarn(`mcp-app: deferred runtime cleanup failed: ${formatErrorMessage(error)}`); }); } } async function withMcpAppResourceAuthority(active, operation) { return await withMcpAppActiveView(active, "read", async () => { await requireMcpAppInteraction(active.view); const result = await operation(); await requireMcpAppInteraction(active.view); return result; }); } async function executeMcpAppOperation(active, operation) { const { runtime, view } = active; switch (operation.method) { case "tools/call": return await withMcpAppActiveView(active, "tool", async () => { await requireCallableTool(runtime, view, operation.params.name); await requireMcpAppInteraction(view); return await runtime.callTool(view.serverName, operation.params.name, operation.params.arguments ?? {}); }); case "tools/list": return await withMcpAppActiveView(active, "read", async () => { await requireMcpAppInteraction(view); if (!runtime.listTools) throw new Error("MCP tools/list is unavailable"); const [listed, catalog] = await Promise.all([runtime.listTools(view.serverName, operation.params?.cursor ? { cursor: operation.params.cursor } : void 0), getRequestCatalog(runtime)]); const allowed = new Set(catalog.tools.filter((tool) => tool.serverName === view.serverName && isAppCallableTool(tool) && isAllowedByView(view, tool.toolName)).map((tool) => tool.toolName)); const result = { ...listed, tools: listed.tools.filter((tool) => allowed.has(tool.name.trim()) && isAppCallableListedTool(tool)) }; await requireMcpAppInteraction(view); return result; }); case "resources/list": return await withMcpAppResourceAuthority(active, async () => { if (!runtime.listResources) throw new Error("MCP resources/list is unavailable"); const resources = await runtime.listResources(view.serverName); return Array.isArray(resources) ? { resources } : resources; }); case "resources/templates/list": return await withMcpAppResourceAuthority(active, async () => { if (!runtime.listResourceTemplates) throw new Error("MCP resources/templates/list is unavailable"); return await runtime.listResourceTemplates(view.serverName, operation.params?.cursor ? { cursor: operation.params.cursor } : void 0); }); case "resources/read": return await withMcpAppResourceAuthority(active, async () => { if (!runtime.readResource) throw new Error("MCP resources/read is unavailable"); return await runtime.readResource(view.serverName, operation.params.uri); }); default: throw new Error(`Unsupported MCP App operation: ${String(operation)}`); } } function parseMcpAppOperation(value) { const method = value && typeof value === "object" && !Array.isArray(value) ? value.method : void 0; const schema = method === "tools/call" ? CallToolRequestSchema : method === "tools/list" ? ListToolsRequestSchema : method === "resources/list" ? ListResourcesRequestSchema : method === "resources/templates/list" ? ListResourceTemplatesRequestSchema : method === "resources/read" ? ReadResourceRequestSchema : void 0; if (!schema) return; const parsed = schema.safeParse(value); return parsed.success ? parsed.data : void 0; } //#endregion export { resolveMcpAppActiveView as a, mintMcpAppViewFromTranscript as c, requireMcpAppInteraction as i, executeMcpAppOperation as n, resolveMcpAppAllowedToolNames as o, parseMcpAppOperation as r, withMcpAppActiveView as s, McpAppViewExpiredError as t };