openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
645 lines (644 loc) • 25.9 kB
JavaScript
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { Dr as ZodError, Et as _enum, Fn as object, Jn as string, Lt as boolean, Nt as array, Pn as number, Un as record, or as union } from "./schemas-BOYIvvln.mjs";
import { a as resolveAgentDir, d as resolveAgentWorkspaceDir } from "./agent-scope-config-CDYpzA0r.mjs";
import { i as getGatewayPluginMetadataSnapshot } from "./current-plugin-metadata-state-CfLPTM0e.mjs";
import "./agent-scope-CG17uXFn.mjs";
import { i as logWarn } from "./logger-DskmrSVa.mjs";
import { n as readMcpAppSettings } from "./mcp-app-extension-metadata-moidhg1Q.mjs";
import { i as resolveProjectedMcpCodexToolApprovalMode, r as requiresMcpCodexToolApproval } from "./mcp-codex-tool-approval-DQZI_si1.mjs";
import { n as runWithSessionMcpRequestSignal } from "./agent-bundle-mcp-request-context-SqXfuSsz.mjs";
import { n as acquireSessionMcpRuntime } from "./agent-bundle-mcp-manager-api-IlYicdWC.mjs";
import { f as releaseSessionMcpRuntime } from "./mcp-app-model-context-XPQPOrco.mjs";
import { t as loadSessionMcpConfig } from "./agent-bundle-mcp-runtime-config-BL8MME2J.mjs";
import { n as normalizeMcpToolFilter, t as isMcpToolAllowed } from "./mcp-tool-filter-BiXzv8v3.mjs";
import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs";
import { f as errorShape } from "./error-codes-Dh56LniQ.mjs";
import { n as captureGatewayOperatorRunAuthority } from "./operator-run-authority-DfunQUuj.mjs";
import { n as getRegisteredAgentHarness } from "./registry-x-yKA-v_.mjs";
import { h as resolveSessionPinnedHarnessId } from "./agent-harness-session-key-D8EQIzja.mjs";
import { s as resolveEffectiveAgentRuntime } from "./thinking-runtime--sshEpUp.mjs";
import { r as getPluginToolMeta } from "./tool-metadata-DEC0Rqn3.mjs";
import { n as resolveSandboxRuntimeStatus } from "./runtime-status-ClNFv3m-.mjs";
import { t as buildBundleMcpToolsFromCatalog } from "./agent-bundle-mcp-materialize-CCVCKgZP.mjs";
import { i as getMcpAppViewLease, n as buildMcpAppCanvasPayload, r as fetchMcpAppView } from "./mcp-ui-resource-BFfH0TzA.mjs";
import { t as resolveSessionSelectedModelRef } from "./session-utils-model-selection-BIBdj57O.mjs";
import { n as getSessionRowProjection } from "./session-row-projection-access-C2RDnj28.mjs";
import { t as iteratePluginRootContributions } from "./plugin-root-contributions-DAzxospV.mjs";
import { l as resolveMcpAppRequesterId, n as callMcpAppToolWithElicitation } from "./mcp-app-operations-DR8z7nxr.mjs";
import { t as resolveSessionResourceToolPolicy } from "./session-resource-tool-policy-CuXAC3Ke.mjs";
import { r as prepareMcpAppHostFile } from "./mcp-app-host-files-D5Bpa84e.mjs";
import { t as requestMcpAppToolApproval } from "./mcp-app-tool-approval-C-OleeXn.mjs";
import { randomUUID } from "node:crypto";
import { ResourceLinkSchema } from "@modelcontextprotocol/sdk/types.js";
//#region src/gateway/mcp-app-extension-runtime.ts
function runtimeSelection(entry, model, harnessId) {
return JSON.stringify([
resolveSessionPinnedHarnessId(entry),
model.provider,
model.model,
harnessId,
entry.agentRuntimeOverride,
entry.modelSelectionLocked,
entry.providerOverride,
entry.modelOverride,
entry.authProfileOverride,
entry.authProfileOverrideSource,
entry.sandboxMode
]);
}
/** A request borrows the current owner; it never substitutes a different native transport. */
async function prepareMcpAppExtensionRuntime(options) {
const access = options.sessionAccessAuthority;
const projection = getSessionRowProjection(options.context);
if (!access || !projection) throw new Error("MCP App session authority is unavailable");
access.assertCurrent();
const { agentId, sessionKey, sessionId } = access.target;
const query = {
agentId,
key: sessionKey
};
const initial = projection.sharingTarget(query);
if (!initial || initial.entry.sessionId !== sessionId) throw new Error("MCP App session changed");
const cfg = options.context.getRuntimeConfig();
if (cfg.mcp?.apps?.enabled !== true) throw new Error("MCP Apps are disabled");
const selectRuntime = (entry) => {
const model = resolveSessionSelectedModelRef({
cfg,
agentId,
sessionKey,
source: {
entry,
readSourceEntry: (key) => projection.sharingTarget({
agentId,
key
})?.entry
},
manifestPlugins: getGatewayPluginMetadataSnapshot() ?? []
});
const harnessId = resolveEffectiveAgentRuntime({
cfg,
provider: model.provider,
modelId: model.model,
agentScope: {
kind: "prepared",
agentId
},
sessionKey,
sessionEntry: entry
});
return {
model,
harnessId,
fingerprint: runtimeSelection(entry, model, harnessId)
};
};
const selected = selectRuntime(initial.entry);
const current = (assertAccess = access.assertCurrent) => {
assertAccess();
if (options.context.getRuntimeConfig() !== cfg) throw new Error("MCP App configuration changed; reopen the App");
const target = projection.sharingTarget(query);
if (!target || target.entry.sessionId !== sessionId) throw new Error("MCP App session changed");
if (selectRuntime(target.entry).fingerprint !== selected.fingerprint) throw new Error("MCP App session runtime selection changed; reopen the App");
return target;
};
const target = current();
const workspaceDir = target.entry.spawnedWorkspaceDir ?? resolveAgentWorkspaceDir(cfg, agentId);
const requesterId = resolveMcpAppRequesterId(options.client);
const { model: selectedModel, harnessId } = selected;
const registered = harnessId ? getRegisteredAgentHarness(harnessId) : void 0;
if (harnessId !== "openclaw" && !registered) throw new Error("The selected session harness is unavailable");
const harness = registered?.harness;
const { loaded } = loadSessionMcpConfig({
workspaceDir,
cfg,
toolOverrides: target.entry.toolOverrides
});
let lease;
if (harnessId !== "openclaw") {
if (!harness?.loadMcpToolCatalog || !harness.acquireMcpAppRuntime) throw new Error("The session harness cannot open MCP Apps");
const preparationOwner = {};
let preparing;
const prepareSession = () => {
preparing ??= (async () => {
current();
const isolation = resolveSandboxRuntimeStatus({
cfg,
agentId,
sessionKey,
preparedSessionEntry: current().entry
});
if (access.sandboxRequired || isolation.sandboxed || isolation.sandboxRequired) throw new Error("Native App initialization cannot run outside required session isolation");
if (!registered?.ownerPluginId) throw new Error("Native harness owner is unavailable");
const retained = access.retain();
preparationOwner.access = retained;
const signal = options.signal ? AbortSignal.any([retained.signal, options.signal]) : retained.signal;
const assertPreparationCurrent = () => {
current();
retained.assertCurrent();
signal.throwIfAborted();
};
assertPreparationCurrent();
const source = await captureGatewayOperatorRunAuthority({
client: options.client,
context: options.context,
hasCurrentClientAuthority: options.hasCurrentClientAuthority,
invocationAuthority: {
assertCurrent: assertPreparationCurrent,
signal
}
});
preparationOwner.source = source;
assertPreparationCurrent();
const live = current();
const { prepareAgentHarnessSessionRuntime } = await import("./session-preparation-CcSzYTno.mjs");
const setup = await prepareAgentHarnessSessionRuntime({
ownerPluginId: registered.ownerPluginId,
nativeModelPolicySupport: harness.nativeModelPolicySupport,
sourceAuthority: source?.authority,
assertCurrent: assertPreparationCurrent,
input: {
config: cfg,
agentId,
agentDir: resolveAgentDir(cfg, agentId),
sessionId,
sessionKey,
workspaceDir,
provider: selectedModel.provider,
modelId: selectedModel.model,
authProfileId: live.entry.authProfileOverride,
authProfileIdSource: live.entry.authProfileOverrideSource === "auto" ? "auto" : live.entry.authProfileOverride ? "user" : void 0,
senderId: requesterId,
messageChannel: "webchat",
messageProvider: "webchat",
abortSignal: signal,
permissionMode: live.entry.permissionMode,
sessionRoot: live.entry.sessionRoot,
toolOverrides: live.entry.toolOverrides,
spawnedBy: live.entry.spawnedBy,
groupId: live.entry.groupId,
groupChannel: live.entry.groupChannel,
groupSpace: live.entry.space,
sessionTarget: {
agentId,
sessionId,
sessionKey,
storePath: live.storePath,
expectedLifecycleRevision: live.entry.lifecycleRevision
}
}
});
preparationOwner.setup = setup;
assertPreparationCurrent();
return setup.preparation;
})();
return preparing;
};
try {
lease = await harness.acquireMcpAppRuntime({
config: cfg,
agentId,
sessionId,
sessionKey,
workspaceDir,
mcpServerNames: Object.keys(loaded.mcpServers),
toolOverrides: target.entry.toolOverrides,
assertCurrent: access.assertCurrent,
appRequester: requesterId ? {
kind: "gateway-profile",
profileId: requesterId
} : void 0,
prepareSession
});
} finally {
try {
preparationOwner.setup?.dispose();
} finally {
try {
preparationOwner.source?.release();
} finally {
preparationOwner.access?.release();
}
}
}
} else lease = await acquireSessionMcpRuntime({
sessionId,
sessionKey,
workspaceDir,
agentDir: resolveAgentDir(cfg, agentId),
cfg,
requesterSenderId: requesterId,
messageChannel: "webchat",
toolOverrides: target.entry.toolOverrides
});
if (!lease) throw new Error("Open a conversation with this plugin before launching its App");
const acquired = lease;
const runtime = acquired.runtime;
const approvalReleases = [];
try {
current();
const listed = await runtime.getCatalog();
const catalog = {
...listed,
servers: Object.fromEntries(Object.entries(listed.servers).map(([name, server]) => [name, {
...server,
pluginId: server.pluginId ?? loaded.pluginIdsByServer?.[name]
}]))
};
current();
const projected = buildBundleMcpToolsFromCatalog({
catalog,
includeAppOnlyInventory: true
});
const names = new Map(projected.flatMap((tool) => {
const mcp = getPluginToolMeta(tool)?.mcp;
return mcp?.operation === "tool" ? [[JSON.stringify([mcp.serverName, mcp.toolName]), tool.name]] : [];
}));
const assertTool = (tool, interaction = false, assertAccess = access.assertCurrent) => {
const live = current(assertAccess);
if (options.context.getRuntimeConfig().mcp?.apps?.enabled !== true) throw new Error("MCP Apps are disabled");
const rawServer = loaded.mcpServers[tool.serverName];
if (!rawServer && !(runtime.assertOwnerCurrent && catalog.servers[tool.serverName]?.pluginId) || rawServer && !isMcpToolAllowed(normalizeMcpToolFilter(rawServer.toolFilter), tool.toolName)) throw new Error("MCP App tool is denied by server policy");
const name = names.get(JSON.stringify([tool.serverName, tool.toolName]));
if (!name || live.entry.toolOverrides?.mcpServers?.[tool.serverName] === false || live.entry.toolOverrides?.mcpToolsDeny?.[tool.serverName]?.includes(tool.toolName)) throw new Error("MCP App tool is denied");
resolveSessionResourceToolPolicy({
config: options.context.getRuntimeConfig(),
client: options.client,
current: live,
readPreparedSessionEntry: (source) => projection.sharingTarget(source)?.entry,
toolName: name,
assertNativeRuntimeCurrent: runtime.assertOwnerCurrent
});
return interaction && requiresMcpCodexToolApproval({
mode: (rawServer ? resolveProjectedMcpCodexToolApprovalMode(tool.serverName, rawServer) : "prompt") ?? catalog.servers[tool.serverName]?.codexApprovalMode,
fullPermission: live.entry.permissionMode === "full",
annotations: tool.codexAnnotations
});
};
const approveTool = async (tool, input) => {
const required = assertTool(tool, true);
if (required) {
const retained = access.retain();
approvalReleases.push(retained.release);
await requestMcpAppToolApproval({
options,
agentId,
sessionKey,
serverName: tool.serverName,
toolName: tool.toolName,
input,
signal: retained.signal,
assertCurrent: () => {
retained.assertCurrent();
assertTool(tool, true);
}
});
}
const assertExecutionCurrent = () => {
if (assertTool(tool, true) && !required) throw new Error("MCP App approval policy changed before execution");
};
assertExecutionCurrent();
return assertExecutionCurrent;
};
const tools = catalog.tools.filter((tool) => {
try {
assertTool(tool);
return true;
} catch {
return false;
}
});
const retainViewAuthority = (viewTools) => {
const retained = access.retain();
const prepareToolCall = async (request) => {
const tool = viewTools.find((candidate) => candidate.toolName === request.toolName);
if (!tool) throw new Error("MCP App tool is not granted to this view");
const assertCurrent = () => {
request.assertCurrent();
retained.assertCurrent();
assertTool(tool, false, retained.assertCurrent);
};
assertCurrent();
const approvalRequired = assertTool(tool, true, retained.assertCurrent);
if (approvalRequired) await requestMcpAppToolApproval({
options: request.options,
agentId,
sessionKey,
serverName: tool.serverName,
toolName: tool.toolName,
input: request.input,
view: request.view,
requesterId: resolveMcpAppRequesterId(request.options.client),
assertCurrent,
signal: request.signal ? AbortSignal.any([retained.signal, request.signal]) : retained.signal
});
const assertExecutionCurrent = () => {
assertCurrent();
if (assertTool(tool, true, retained.assertCurrent) && !approvalRequired) throw new Error("MCP App approval policy changed before execution");
};
assertExecutionCurrent();
return assertExecutionCurrent;
};
return {
...retained,
prepareToolCall,
assertCurrent: () => {
retained.assertCurrent();
if (current(retained.assertCurrent).entry.permissionMode !== target.entry.permissionMode) throw new Error("MCP App permission mode changed; reopen the App");
for (const tool of viewTools) assertTool(tool, false, retained.assertCurrent);
}
};
};
return {
approveTool,
retainViewAuthority,
options,
runtime,
catalog: {
...catalog,
tools
},
agentId,
sessionKey,
sessionId,
workspaceDir,
requesterId,
access,
assertTool,
assertCurrent: () => {
current();
},
dispose: async () => {
for (const release of approvalReleases.splice(0)) release();
await releaseSessionMcpRuntime(acquired);
await runtime.joinCleanup?.();
}
};
} catch (error) {
for (const release of approvalReleases.splice(0)) release();
await releaseSessionMcpRuntime(lease);
throw error;
}
}
//#endregion
//#region src/gateway/server-methods/mcp-app-extensions.ts
var UnsupportedMcpMentionResultError = class extends Error {};
const text = string().trim().min(1).max(2048);
const targetSchema = object({
sessionKey: text,
agentId: text.optional()
});
const launchSchema = targetSchema.extend({
serverName: text,
toolName: text,
entrypointType: _enum([
"global",
"thread",
"file",
"settings"
]),
quickAction: boolean().optional(),
filePath: text.optional(),
deepLink: text.optional()
});
const settingsSchema = targetSchema.extend({
serverName: text,
action: _enum([
"read",
"update",
"tool"
]),
toolName: text.optional(),
arguments: object({ set: record(string(), union([
string(),
number().finite(),
boolean()
])) }).optional()
});
const mentionSchema = targetSchema.extend({
serverName: text,
query: string().max(2048)
});
function toolFor(active, serverName, toolName) {
const tool = active.catalog.tools.find((candidate) => candidate.serverName === serverName && candidate.toolName === toolName);
if (!tool) throw new Error("MCP App tool is unavailable or denied");
active.assertTool(tool);
return tool;
}
function appToolsFor(active, serverName) {
return active.catalog.tools.filter((candidate) => {
if (candidate.serverName !== serverName || candidate.uiVisibility !== void 0 && !candidate.uiVisibility.includes("app")) return false;
try {
active.assertTool(candidate);
return true;
} catch {
return false;
}
});
}
async function uploadFor(active, serverName, assertCurrent) {
const { prepareMcpAppFormUpload } = await import("./mcp-form-resource-upload-G0Bm9won.mjs");
return await prepareMcpAppFormUpload({
runtime: active.runtime,
serverName,
agentId: active.agentId,
sessionKey: active.sessionKey,
requesterId: active.requesterId,
assertCurrent
});
}
async function call(active, tool, input) {
const assertCurrent = await active.approveTool(tool, input);
const formAuthority = active.retainViewAuthority(appToolsFor(active, tool.serverName));
let result;
try {
result = await callMcpAppToolWithElicitation({
options: active.options,
uploadResources: await uploadFor(active, tool.serverName, assertCurrent),
origin: {
runtime: active.runtime,
serverName: tool.serverName,
agentId: active.agentId,
sessionKey: active.sessionKey,
requesterId: active.requesterId,
assertCurrent,
prepareToolCall: formAuthority.prepareToolCall
},
toolName: tool.toolName,
input,
assertCurrent
});
} finally {
formAuthority.release();
}
active.assertCurrent();
return result;
}
async function launch(active, tool, input, extra) {
if (!tool.uiResourceUri?.startsWith("ui://")) throw new Error("MCP App resource is unavailable");
const result = await call(active, tool, input);
if (result.isError) return { toolResult: result };
const viewTools = appToolsFor(active, tool.serverName);
const retained = active.retainViewAuthority(viewTools);
let transferred = false;
try {
const viewRuntime = active.runtime;
const allowedAppToolNames = new Set(viewTools.map((candidate) => candidate.toolName));
const view = await fetchMcpAppView({
runtime: viewRuntime,
agentId: active.agentId,
serverName: tool.serverName,
toolName: tool.toolName,
uiResourceUri: tool.uiResourceUri,
toolCallId: randomUUID(),
toolInput: input,
toolResult: result,
allowedAppToolNames,
requesterId: active.requesterId,
displayMode: tool.appExtensions?.preferredModelDisplayMode,
prepareToolCall: retained.prepareToolCall,
uploadResources: await uploadFor(active, tool.serverName, retained.assertCurrent),
...extra,
authorizeAppInteraction: () => {
retained.assertCurrent();
return true;
}
});
if (!view) throw new Error("MCP App resource could not be loaded");
const preview = buildMcpAppCanvasPayload({
...view,
originSessionKey: active.sessionKey
});
active.assertCurrent();
const lease = getMcpAppViewLease(view.viewId, viewRuntime);
if (lease) {
lease.disposeCallbacks ??= /* @__PURE__ */ new Set();
lease.disposeCallbacks.add(retained.release);
transferred = true;
}
return {
viewId: view.viewId,
preview
};
} finally {
if (!transferred) retained.release();
}
}
function handler(schema, operation, disabledResult) {
return async (options) => {
let active;
try {
const params = schema.parse(options.params);
if (disabledResult && options.context.getRuntimeConfig().mcp?.apps?.enabled !== true) {
options.sessionAccessAuthority?.assertCurrent();
options.respond(true, disabledResult);
return;
}
await runWithSessionMcpRequestSignal(options.signal, async () => {
active = await prepareMcpAppExtensionRuntime(options);
const result = await operation(active, params);
active.assertCurrent();
options.respond(true, result);
});
} catch (error) {
options.respond(false, void 0, errorShape(error instanceof ZodError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, error instanceof Error ? error.message : String(error), error instanceof UnsupportedMcpMentionResultError ? { details: { code: "MCP_APP_UNSUPPORTED_MENTION_RESULT" } } : void 0));
} finally {
await active?.dispose().catch((error) => logWarn(`mcp-app: cleanup failed: ${String(error)}`));
}
};
}
const mcpAppExtensionHandlers = {
"mcp.app.discover": handler(targetSchema, (active) => ({
onboarding: (() => {
const metadata = getGatewayPluginMetadataSnapshot();
return metadata ? [...iteratePluginRootContributions({
metadataSnapshot: metadata,
config: active.options.context.getRuntimeConfig(),
contribution: "skills"
})].flatMap(({ record }) => record.onboardingSkill ? [{
pluginId: record.id,
title: record.name ?? record.id
}] : []) : [];
})(),
servers: Object.values(active.catalog.servers).flatMap((server) => {
const install = server.pluginId ? getGatewayPluginMetadataSnapshot()?.index.installRecords[server.pluginId] : void 0;
const pluginId = install?.marketplacePlugin ?? server.pluginId;
const marketplace = install?.marketplaceName ?? server.marketplace;
const tools = active.catalog.tools.filter((tool) => tool.serverName === server.serverName);
const entrypoints = tools.flatMap((tool) => tool.uiResourceUri?.startsWith("ui://") ? (tool.appExtensions?.entrypoints ?? []).map((entrypoint) => ({
toolName: tool.toolName,
title: tool.title ?? tool.toolName,
resourceUri: tool.uiResourceUri,
icons: tool.appExtensions?.icons ?? server.icons,
entrypoint
})) : []);
const settings = server.settings && tools.some((tool) => tool.toolName === server.settings?.readTool) && tools.some((tool) => tool.toolName === server.settings?.updateTool) ? server.settings : void 0;
const mentionTool = tools.find((tool) => tool.appExtensions?.mentionSearch)?.toolName;
return entrypoints.length || settings || mentionTool ? [{
serverName: server.serverName,
...pluginId ? { pluginId } : {},
...marketplace ? { marketplace } : {},
label: server.title ?? server.serverName,
icons: server.icons,
entrypoints,
...settings ? { settings } : {},
...mentionTool ? { mentionTool } : {}
}] : [];
})
}), {
servers: [],
onboarding: []
}),
"mcp.app.launch": handler(launchSchema, async (active, params) => {
const tool = toolFor(active, params.serverName, params.toolName);
const entrypoint = tool.appExtensions?.entrypoints?.find((entry) => entry.type === params.entrypointType);
if (!entrypoint) throw new Error("The requested MCP App entrypoint is not advertised");
if (params.quickAction) {
if (entrypoint.type !== "global" || !entrypoint.quickAction) throw new Error("MCP App quick action is unavailable");
const action = entrypoint.quickAction.target;
const actionTool = toolFor(active, params.serverName, action.name);
return actionTool.uiResourceUri ? launch(active, actionTool, action.arguments ?? {}) : { toolResult: await call(active, actionTool, action.arguments ?? {}) };
}
if (entrypoint.type === "file") {
if (!params.filePath) throw new Error("Choose a session file to open");
const hostFile = await prepareMcpAppHostFile(active.options, {
sessionKey: active.sessionKey,
agentId: active.agentId,
path: params.filePath
});
active.assertCurrent();
if (!entrypoint.extensions.some((extension) => hostFile.name.toLowerCase().endsWith(extension.toLowerCase()))) throw new Error("The file extension is not supported by this App");
return await launch(active, tool, { file: {
name: hostFile.name,
resourceUri: hostFile.resourceUri
} }, { hostFile });
}
if (params.deepLink && (entrypoint.type !== "global" || !params.deepLink.startsWith("/") || params.deepLink.startsWith("//") || params.deepLink.includes("#"))) throw new Error("Invalid App-relative deep link");
return await launch(active, tool, {}, params.deepLink ? { deepLink: { url: params.deepLink } } : void 0);
}),
"mcp.app.settings": handler(settingsSchema, async (active, params) => {
const capability = active.catalog.servers[params.serverName]?.settings;
if (!capability) throw new Error("Structured MCP settings are unavailable");
if (params.action === "update") {
if (!params.arguments) throw new Error("Changed settings are required");
return { toolResult: await call(active, toolFor(active, params.serverName, capability.updateTool), params.arguments) };
}
const result = await call(active, toolFor(active, params.serverName, capability.readTool), {});
if (result.isError) throw new Error("The MCP settings read tool failed");
const settings = readMcpAppSettings(result.structuredContent);
if (params.action === "read") return settings;
if (!settings.layout?.some((group) => group.items.some((item) => item.kind === "tool" && item.tool === params.toolName))) throw new Error("Settings action is not advertised in the current layout");
const tool = toolFor(active, params.serverName, params.toolName);
return tool.uiResourceUri ? launch(active, tool, {}) : { toolResult: await call(active, tool, {}) };
}),
"mcp.app.mention": handler(mentionSchema, async (active, params) => {
const tool = active.catalog.tools.find((candidate) => candidate.serverName === params.serverName && candidate.appExtensions?.mentionSearch);
if (!tool) throw new Error("MCP mention search is unavailable");
const result = await call(active, tool, { query: params.query });
if (result.isError) throw new Error("MCP mention search failed");
const resources = array(ResourceLinkSchema).max(200).safeParse(asOptionalRecord(result.structuredContent)?.items);
if (!resources.success) throw new UnsupportedMcpMentionResultError("This app returned an unsupported resource list");
return { resources: resources.data };
})
};
//#endregion
export { mcpAppExtensionHandlers };