UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

271 lines (270 loc) • 12.9 kB
import { t as createLazyRuntimeMethod } from "./lazy-runtime-CJrzFzqU.mjs"; import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { i as logWarn } from "./logger-DskmrSVa.mjs"; import { c as subscribeMcpAppModelContext, l as updateMcpAppModelContext, o as removeMcpAppModelContextItem, r as getMcpAppModelContext } from "./mcp-app-model-context-XPQPOrco.mjs"; import { r as GatewayErrorDetailCodes, t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs"; import { f as errorShape } from "./error-codes-Dh56LniQ.mjs"; import { n as authorizeOperatorScopesForMethod } from "./method-scopes-BrMPjfvW.mjs"; import { n as resolveRequestedSessionAgentId } from "./session-request-agent-Bxi11ET4.mjs"; import { l as buildMcpAppSandboxPath } from "./mcp-ui-resource-BFfH0TzA.mjs"; import { a as readGatewayRequestMutationAuthority } from "./session-mutation-guards-Cw1VPolZ.mjs"; import { l as resolveMcpAppRequesterId, o as requireMcpAppInteraction, r as executeMcpAppOperation, s as resolveMcpAppActiveView, t as McpAppViewExpiredError, u as withMcpAppActiveView } from "./mcp-app-operations-DR8z7nxr.mjs"; import { t as createMcpAppStandaloneTicket } from "./mcp-app-standalone-WSJPeb6K.mjs"; import { t as retainSessionScopedRead } from "./session-scoped-read-T8lBdRDm.mjs"; //#region src/gateway/server-methods/mcp-app.ts const fileRuntime = () => import("./mcp-app-host-files-BOek_I_f.mjs"); const readMcpAppHostFile = createLazyRuntimeMethod(fileRuntime, (runtime) => runtime.readMcpAppHostFile); const writeMcpAppHostFile = createLazyRuntimeMethod(fileRuntime, (runtime) => runtime.writeMcpAppHostFile); const subscribeMcpAppHostFile = createLazyRuntimeMethod(fileRuntime, (runtime) => runtime.subscribeMcpAppHostFile); const canOpenMcpAppFiles = createLazyRuntimeMethod(fileRuntime, (runtime) => runtime.canOpenMcpAppFiles); const openMcpAppFile = createLazyRuntimeMethod(fileRuntime, (runtime) => runtime.openMcpAppFile); function requireString(params, key) { const value = params[key]; if (typeof value !== "string" || !value.trim()) throw new Error(`${key} is required`); return value.trim(); } function optionalRecord(params, key) { const value = params[key]; if (value === void 0) return; const record = asOptionalRecord(value); if (!record) throw new Error(`${key} must be an object`); return record; } function optionalCursor(params) { const cursor = params.cursor; return typeof cursor === "string" ? { cursor } : void 0; } var McpAppRequestError = class extends Error { constructor(shape) { super(shape.message); this.shape = shape; } }; function resolveMcpAppSessionOwner(params, cfg) { const sessionKey = requireString(params, "sessionKey"); const explicitAgentId = typeof params.agentId === "string" && params.agentId.trim() ? params.agentId.trim() : void 0; const owner = resolveRequestedSessionAgentId(cfg, sessionKey, explicitAgentId); if (!owner.ok) throw new McpAppRequestError(owner.error); return owner.agentId; } function resolveRequestedMcpAppView({ params, context, client }) { return resolveMcpAppActiveView({ sessionKey: requireString(params, "sessionKey"), agentId: resolveMcpAppSessionOwner(params, context.getRuntimeConfig()), viewId: requireString(params, "viewId"), requesterId: resolveMcpAppRequesterId(client), cfg: context.getRuntimeConfig(), restore: false }); } function operationHandler(buildOperation) { return mcpAppHandler(async (options) => { const { params, context, client } = options; const requestAuthority = readGatewayRequestMutationAuthority(options); const operation = buildOperation(params); const cfg = context.getRuntimeConfig(); const requesterId = resolveMcpAppRequesterId(client); const active = await resolveMcpAppActiveView({ sessionKey: requireString(params, "sessionKey"), agentId: resolveMcpAppSessionOwner(params, cfg), viewId: requireString(params, "viewId"), requesterId, cfg: context.getRuntimeConfig(), restore: false }); const read = retainSessionScopedRead(options, requireString(params, "sessionKey"), active.view.agentId); try { const result = operation.method === "resources/read" && operation.params.uri.startsWith("openclaw-file://") ? await readMcpAppHostFile(options, active.view, operation.params) : await executeMcpAppOperation(active, operation, { options, assertCurrent: () => { requestAuthority.assertCurrent(); read?.assertCurrent(); options.sessionMutationAuthorization?.assertCurrent(); if (resolveMcpAppRequesterId(client) !== requesterId || active.view.requesterId !== void 0 && active.view.requesterId !== requesterId) throw new McpAppViewExpiredError(); } }); read?.assertCurrent(); return result; } finally { read?.release(); } }); } function mcpAppHandler(operation) { return async (options) => { try { options.respond(true, await operation(options)); } catch (error) { options.respond(false, void 0, error instanceof McpAppRequestError ? error.shape : errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error), error instanceof McpAppViewExpiredError ? { details: { code: GatewayErrorDetailCodes.MCP_APP_VIEW_EXPIRED } } : void 0)); } }; } const modelContextSubscriptions = /* @__PURE__ */ new WeakMap(); function hostFileHandler(operation) { return mcpAppHandler(async (options) => { const active = await resolveRequestedMcpAppView(options); return withMcpAppActiveView(active, "read", () => operation(options, active.view)); }); } const mcpAppHandlers = { "mcp.app.formResource": mcpAppHandler(async (options) => { const { executeMcpAppFormResource } = await import("./mcp-app-form-resources-BGbvA02A.mjs"); return executeMcpAppFormResource(options); }), "mcp.app.view": mcpAppHandler(async (options) => { const { params, context, client } = options; const sessionKey = requireString(params, "sessionKey"); const agentId = resolveMcpAppSessionOwner(params, context.getRuntimeConfig()); const requesterId = resolveMcpAppRequesterId(client); const read = retainSessionScopedRead(options, sessionKey, agentId); try { const active = await resolveMcpAppActiveView({ sessionKey: requireString(params, "sessionKey"), agentId: resolveMcpAppSessionOwner(params, context.getRuntimeConfig()), viewId: requireString(params, "viewId"), requesterId: resolveMcpAppRequesterId(client), cfg: context.getRuntimeConfig() }); read?.assertCurrent(); const payload = await withMcpAppActiveView(active, "read", async () => { const { view } = active; if (client?.connId) { const byConnection = modelContextSubscriptions.get(view) ?? /* @__PURE__ */ new Map(); const previous = byConnection.get(client.connId); previous?.(); if (previous) view.disposeCallbacks?.delete(previous); if (byConnection.size >= 32 && !byConnection.has(client.connId)) throw new Error("MCP App connection limit reached"); const connId = client.connId; let updateId = getMcpAppModelContext(active.runtime, view)?.updateId; const unsubscribe = subscribeMcpAppModelContext(view, (state) => { const clearedUpdateId = updateId; updateId = state?.updateId; context.broadcastToConnIds("mcp.app.hostContextChanged", { viewId: view.viewId, ...state === null ? { modelContext: null, updateId: clearedUpdateId } : {} }, /* @__PURE__ */ new Set([connId])); }); const stop = () => { unsubscribe(); client.connectionSignal?.removeEventListener("abort", stop); if (byConnection.get(connId) === stop) byConnection.delete(connId); view.disposeCallbacks?.delete(stop); }; client.connectionSignal?.addEventListener("abort", stop, { once: true }); byConnection.set(connId, stop); modelContextSubscriptions.set(view, byConnection); view.disposeCallbacks ??= /* @__PURE__ */ new Set(); view.disposeCallbacks.add(stop); if (client.connectionSignal?.aborted) stop(); } let interactive = false; try { await requireMcpAppInteraction(view); interactive = true; } catch {} const openFilesSupported = interactive && Boolean(active.runtime.sessionKey) && await canOpenMcpAppFiles(view); const updateModelContextSupported = interactive && Boolean(active.runtime.sessionKey) && active.runtime.mcpAppModelContextRevoked !== true; const sandboxPort = context.getMcpAppSandboxPort?.() ?? await context.ensureSandboxHostPort?.(); if (sandboxPort === void 0) throw new Error("MCP App sandbox listener is unavailable; restart the Gateway"); const configuredOrigin = context.getRuntimeConfig().mcp?.apps?.sandboxOrigin; let standalone = void 0; try { standalone = createMcpAppStandaloneTicket({ sessionKey: requireString(params, "sessionKey"), view, toolOperationsAuthorized: authorizeOperatorScopesForMethod("mcp.app.callTool", client?.connect?.scopes ?? []).allowed }); } catch (error) { logWarn(`mcp-app: standalone ticket unavailable: ${formatErrorMessage(error)}`); } read?.assertCurrent(); if (requesterId !== resolveMcpAppRequesterId(client)) throw new McpAppViewExpiredError(); return { sandboxUrl: buildMcpAppSandboxPath(view.csp), sandboxPort, ...configuredOrigin ? { sandboxOrigin: new URL(configuredOrigin).origin } : {}, html: view.html, ...view.csp ? { csp: view.csp } : {}, toolInput: view.toolInput, toolResult: view.toolResult, hostContext: { "openai/modelContext": getMcpAppModelContext(active.runtime, view), ...view.deepLink ? { "openai/deepLink": view.deepLink } : {} }, ...view.displayModes ? { displayModes: view.displayModes } : {}, ...view.displayMode ? { displayMode: view.displayMode } : {}, richModelContextSupported: updateModelContextSupported && view.richModelContextSupported !== false, fileResourcesSupported: interactive && Boolean(view.hostFile), openFilesSupported, ...standalone ? { standaloneUrl: standalone.url, standaloneExpiresAtMs: standalone.expiresAtMs } : {}, messageSupported: interactive, updateModelContextSupported }; }); read?.assertCurrent(); if (requesterId !== resolveMcpAppRequesterId(client)) throw new McpAppViewExpiredError(); return payload; } finally { read?.release(); } }), "mcp.app.updateModelContext": mcpAppHandler(async (options) => { const { params } = options; const active = await resolveRequestedMcpAppView(options); return await withMcpAppActiveView(active, "read", async () => { await requireMcpAppInteraction(active.view); return updateMcpAppModelContext(active.runtime, active.view, params); }); }), "mcp.app.modelContext": mcpAppHandler(async (options) => { const active = await resolveRequestedMcpAppView(options); await requireMcpAppInteraction(active.view); return { state: getMcpAppModelContext(active.runtime, active.view) }; }), "mcp.app.removeModelContext": mcpAppHandler(async (options) => { const { params } = options; const active = await resolveRequestedMcpAppView(options); await requireMcpAppInteraction(active.view); if (params.index !== void 0 && typeof params.index !== "number") throw new Error("index must be a number"); return { state: removeMcpAppModelContextItem(active.runtime, active.view, requireString(params, "updateId"), params.index) }; }), "mcp.app.writeResource": hostFileHandler((options, view) => writeMcpAppHostFile(options, view, options.params)), "mcp.app.subscribeResource": hostFileHandler((options, view) => subscribeMcpAppHostFile(options, view, requireString(options.params, "uri"), true)), "mcp.app.unsubscribeResource": hostFileHandler((options, view) => subscribeMcpAppHostFile(options, view, requireString(options.params, "uri"), false)), "mcp.app.openFile": hostFileHandler((options, view) => openMcpAppFile(options, view, requireString(options.params, "path"))), "mcp.app.callTool": operationHandler((params) => ({ method: "tools/call", params: { name: requireString(params, "toolName"), arguments: optionalRecord(params, "arguments") ?? {} } })), "mcp.app.listTools": operationHandler((params) => ({ method: "tools/list", params: optionalCursor(params) ?? {} })), "mcp.app.listResources": operationHandler((params) => ({ method: "resources/list", params: optionalCursor(params) ?? {} })), "mcp.app.listResourceTemplates": operationHandler((params) => ({ method: "resources/templates/list", params: optionalCursor(params) ?? {} })), "mcp.app.readResource": operationHandler((params) => ({ method: "resources/read", params: { uri: requireString(params, "uri"), _meta: optionalRecord(params, "_meta") } })) }; //#endregion export { mcpAppHandlers };