@agent-native/core 0.22.21 → 0.22.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +17 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +42 -1
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +79 -23
- package/dist/mcp/embed-app.js.map +1 -1
- package/docs/content/actions.md +4 -0
- package/docs/content/external-agents.md +7 -0
- package/docs/content/mcp-protocol.md +4 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed-app.js","sourceRoot":"","sources":["../../src/mcp/embed-app.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ActionMcpAppResourceConfig } from \"../action.js\";\nimport { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from \"../shared/embed-auth.js\";\n\nconst MCP_APP_IMPORT =\n \"https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps\";\n\nexport const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = \"$requestOrigin\";\nconst MCP_APP_WRAPPER_CHROME_HEIGHT = 44;\nexport const DEFAULT_MCP_APP_SHELL_HEIGHT = 560;\nexport const DEFAULT_MCP_APP_VIEWPORT_HEIGHT =\n DEFAULT_MCP_APP_SHELL_HEIGHT - MCP_APP_WRAPPER_CHROME_HEIGHT;\n\nexport interface EmbedAppOptions {\n title?: string;\n description?: string;\n iframeTitle?: string;\n openLabel?: string;\n embedByDefault?: boolean;\n startToolName?: string;\n frameDomains?: string[];\n height?: number;\n}\n\nfunction attr(value: string | undefined): string {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/\"/g, \""\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nexport function embedApp(\n options: EmbedAppOptions = {},\n): ActionMcpAppResourceConfig {\n const title = options.title ?? \"Open app\";\n const iframeTitle = options.iframeTitle ?? \"Agent Native app\";\n const openLabel = options.openLabel ?? \"Open in app\";\n const startToolName = options.startToolName ?? \"create_embed_session\";\n const embedByDefault = options.embedByDefault !== false;\n const height = Math.max(\n 320,\n Math.min(900, options.height ?? DEFAULT_MCP_APP_SHELL_HEIGHT),\n );\n const viewportHeight = height - MCP_APP_WRAPPER_CHROME_HEIGHT;\n const frameDomains = [\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ];\n\n return {\n title,\n ...(options.description ? { description: options.description } : {}),\n html: () => `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <style>\n :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: Canvas; color: CanvasText; --agent-native-shell-height: ${height}px; --agent-native-viewport-height: ${viewportHeight}px; }\n * { box-sizing: border-box; }\n body { margin: 0; }\n .shell { display: grid; gap: 8px; min-height: var(--agent-native-shell-height); padding: 0; }\n .bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 36px; padding: 6px 8px; border-bottom: 1px solid color-mix(in srgb, CanvasText 12%, Canvas); }\n .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 700; color: color-mix(in srgb, CanvasText 72%, Canvas); }\n .actions { display: flex; align-items: center; gap: 6px; }\n button { min-height: 28px; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 7px; background: Canvas; color: CanvasText; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; padding: 0 9px; }\n button:disabled { opacity: .55; cursor: default; }\n .stage { position: relative; min-height: var(--agent-native-viewport-height); }\n iframe { display: block; width: 100%; height: var(--agent-native-viewport-height); border: 0; background: Canvas; }\n .message { display: grid; place-items: center; min-height: var(--agent-native-viewport-height); padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }\n .fallback { display: grid; align-content: center; justify-items: center; gap: 12px; min-height: var(--agent-native-viewport-height); padding: 24px; background: Canvas; color: CanvasText; text-align: center; }\n .fallback-title { max-width: 440px; font-size: 14px; font-weight: 700; }\n .fallback-copy { max-width: 520px; color: color-mix(in srgb, CanvasText 64%, Canvas); font-size: 13px; line-height: 1.45; }\n .fallback-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }\n .fallback-url { max-width: min(560px, 100%); overflow-wrap: anywhere; color: color-mix(in srgb, CanvasText 76%, Canvas); font-size: 12px; }\n </style>\n</head>\n<body\n data-app-title=\"${attr(title)}\"\n data-iframe-title=\"${attr(iframeTitle)}\"\n data-open-label=\"${attr(openLabel)}\"\n data-start-tool=\"${attr(startToolName)}\"\n data-embed-default=\"${embedByDefault ? \"1\" : \"0\"}\"\n>\n <main class=\"shell\">\n <div class=\"bar\">\n <div class=\"title\" data-title-label>${attr(title)}</div>\n <div class=\"actions\">\n <button type=\"button\" data-display hidden disabled>Fullscreen</button>\n <button type=\"button\" data-open disabled>${attr(openLabel)}</button>\n </div>\n </div>\n <section class=\"stage\" data-stage>\n <div class=\"message\">Preparing app</div>\n </section>\n </main>\n <script type=\"module\">\n const body = document.body;\n const stage = document.querySelector(\"[data-stage]\");\n const titleEl = document.querySelector(\"[data-title-label]\");\n const openButton = document.querySelector(\"[data-open]\");\n const displayButton = document.querySelector(\"[data-display]\");\n const startTool = body.dataset.startTool || \"create_embed_session\";\n const embedByDefault = body.dataset.embedDefault !== \"0\";\n const chatBridgeParam = ${JSON.stringify(MCP_APP_CHAT_BRIDGE_QUERY_PARAM)};\n const defaultIntrinsicHeight = ${height};\n const chromeHeight = ${MCP_APP_WRAPPER_CHROME_HEIGHT};\n const frameReadyMessageDelays = [0, 200, 500, 1500, 3000, 7000, 15000, 30000];\n const frameReadyTimeoutMs = 45000;\n const frameLoadTimeoutMs = 45000;\n let app = null;\n let openAiBridge = null;\n let toolInput = {};\n let openUrl = \"\";\n let startedFor = \"\";\n let appFrame = null;\n let appFrameReady = false;\n let appFrameReadyTimer = null;\n let appFrameLoadTimer = null;\n let lastFrameSrc = \"\";\n\n function esc(value) {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n }\n\n function parseJson(value, fallback) {\n if (value && typeof value === \"object\") return value;\n if (typeof value !== \"string\" || !value.trim()) return fallback;\n try { return JSON.parse(value); } catch { return fallback; }\n }\n\n function objectValue(value) {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value\n : {};\n }\n\n function finiteNumber(value) {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0\n ? value\n : null;\n }\n\n function contextMaxHeight(context) {\n if (!context || typeof context !== \"object\") return null;\n return finiteNumber(context.maxHeight) ||\n finiteNumber(context.containerDimensions && context.containerDimensions.maxHeight);\n }\n\n function visibleIntrinsicHeight() {\n const context = hostState().context || {};\n const hostMaxHeight = contextMaxHeight(context);\n if (hostMaxHeight) return Math.floor(hostMaxHeight);\n const viewportHeight = finiteNumber(window.visualViewport && window.visualViewport.height) ||\n finiteNumber(window.innerHeight);\n return Math.floor(viewportHeight || defaultIntrinsicHeight);\n }\n\n function applyIntrinsicHeight(nextHeight) {\n const boundedHeight = Math.min(\n defaultIntrinsicHeight,\n Math.floor(nextHeight || defaultIntrinsicHeight)\n );\n const height = Math.max(320, boundedHeight);\n const viewportHeight = Math.max(0, height - chromeHeight);\n document.documentElement.style.setProperty(\"--agent-native-shell-height\", height + \"px\");\n document.documentElement.style.setProperty(\"--agent-native-viewport-height\", viewportHeight + \"px\");\n if (appFrame) appFrame.style.height = viewportHeight + \"px\";\n return height;\n }\n\n function parseToolResult(params) {\n if (!params) return {};\n if (params.result && typeof params.result === \"object\") {\n return parseToolResult(params.result);\n }\n if (params.toolResult && typeof params.toolResult === \"object\") {\n return parseToolResult(params.toolResult);\n }\n if (params.structuredContent && typeof params.structuredContent === \"object\") {\n return params.structuredContent;\n }\n const parts = Array.isArray(params.content) ? params.content : [];\n const textPart = parts.find((part) => part && part.type === \"text\" && typeof part.text === \"string\");\n return parseJson(textPart ? textPart.text : \"\", {});\n }\n\n function openLinkFrom(params, data) {\n const openLink = params && params._meta && params._meta[\"agent-native/openLink\"];\n const metaUrl = openLink && typeof openLink === \"object\" && typeof openLink.webUrl === \"string\"\n ? openLink.webUrl\n : \"\";\n const record = data && typeof data === \"object\" ? data : {};\n return metaUrl || record.url || record.deepLink || record.openUrl || \"\";\n }\n\n function hostState() {\n if (openAiBridge) {\n return {\n context: {\n displayMode: openAiBridge.displayMode,\n availableDisplayModes: typeof openAiBridge.requestDisplayMode === \"function\"\n ? [\"inline\", \"fullscreen\", \"pip\"]\n : [],\n maxHeight: openAiBridge.maxHeight,\n locale: openAiBridge.locale,\n theme: openAiBridge.theme,\n view: openAiBridge.view\n },\n capabilities: { openai: true },\n version: openAiBridge.userAgent\n };\n }\n return {\n context: app && app.getHostContext ? app.getHostContext() : undefined,\n capabilities: app && app.getHostCapabilities ? app.getHostCapabilities() : undefined,\n version: app && app.getHostVersion ? app.getHostVersion() : undefined\n };\n }\n\n function sendToAppFrame(message) {\n if (!appFrame || !appFrame.contentWindow) return;\n try { appFrame.contentWindow.postMessage(message, \"*\"); } catch {}\n }\n\n function sendHostContext() {\n sendToAppFrame({ type: \"agentNative.mcpHostContext\", data: hostState() });\n }\n\n function sendFrameReadyMessages(frame) {\n const originPayload = { type: \"agentNative.frameOrigin\", origin: window.location.origin };\n frameReadyMessageDelays.forEach((delay) => {\n setTimeout(() => {\n try { frame.contentWindow && frame.contentWindow.postMessage(originPayload, \"*\"); } catch {}\n sendHostContext();\n }, delay);\n });\n }\n\n function withChatBridgeParam(value) {\n if (typeof value !== \"string\" || !value) return value;\n try {\n const base = \"http://agent-native.invalid\";\n const url = value.startsWith(\"/\") ? new URL(value, base) : new URL(value);\n url.searchParams.set(chatBridgeParam, \"1\");\n return value.startsWith(\"/\")\n ? url.pathname + url.search + url.hash\n : url.toString();\n } catch {\n return value;\n }\n }\n\n function embedSessionArgsFor(value) {\n const chrome = typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\";\n return typeof value === \"string\" && value.startsWith(\"/\")\n ? { path: value, chrome }\n : { url: value, chrome };\n }\n\n function isEmbedStartUrl(value) {\n if (typeof value !== \"string\" || !value) return false;\n try {\n const url = new URL(value, window.location.href);\n return /\\\\/_agent-native\\\\/embed\\\\/start$/.test(url.pathname);\n } catch {\n return false;\n }\n }\n\n function localPathFromUrl(url, includeToken) {\n const next = new URL(url.href);\n if (!includeToken) next.searchParams.delete(\"__an_embed_token\");\n return next.pathname + next.search + next.hash;\n }\n\n function rewriteRootRelativeHtmlUrls(html, appOrigin) {\n return String(html).replace(\n /\\\\b(src|href|poster|action)\\\\s*=\\\\s*([\"'])\\\\/(?!\\\\/)/gi,\n (_match, name, quote) => String(name) + \"=\" + quote + appOrigin + \"/\"\n );\n }\n\n function removeHtmlCspMeta(html) {\n return String(html).replace(\n /<meta\\\\s+[^>]*http-equiv\\\\s*=\\\\s*([\"'])?content-security-policy\\\\1?[^>]*>/gi,\n \"\"\n );\n }\n\n function embedConfigForAppUrl(appUrl) {\n const sanitizedTarget = localPathFromUrl(appUrl, false);\n return {\n origin: appUrl.origin,\n href: appUrl.href,\n baseHref: appUrl.origin + appUrl.pathname,\n target: sanitizedTarget,\n token: appUrl.searchParams.get(\"__an_embed_token\") || \"\",\n chatBridgeActive: appUrl.searchParams.get(chatBridgeParam) === \"1\",\n chatBridgeParam,\n embedTokenParam: \"__an_embed_token\",\n embedTargetHeader: \"x-agent-native-embed-target\"\n };\n }\n\n function installExternalEmbedRuntime(config) {\n window.__AGENT_NATIVE_EXTERNAL_EMBED = config;\n try {\n if (config.target) {\n window.history.replaceState(window.history.state, \"\", config.target);\n }\n } catch (_err) {}\n try {\n if (config.token) {\n sessionStorage.setItem(\"agent-native:embed-auth-token\", config.token);\n }\n if (config.chatBridgeActive && config.token) {\n sessionStorage.setItem(\"agent-native:mcp-chat-bridge\", config.token);\n }\n } catch (_err) {}\n if (window.__agentNativeExternalEmbedRuntimeInstalled) return;\n window.__agentNativeExternalEmbedRuntimeInstalled = true;\n function appOrigin() {\n try {\n return new URL(config.origin).origin;\n } catch (_err) {\n return \"\";\n }\n }\n function targetPath() {\n return config.target || location.pathname + location.search;\n }\n function rewrittenUrl(value, appendToken) {\n const origin = appOrigin();\n if (!origin) return null;\n let url;\n try {\n url = new URL(value, location.href);\n } catch (_err) {\n return null;\n }\n if (url.origin !== location.origin && url.origin !== origin) return null;\n if (url.origin !== origin) {\n const app = new URL(origin);\n url.protocol = app.protocol;\n url.host = app.host;\n }\n if (appendToken && config.token && url.pathname === \"/_agent-native/events\") {\n url.searchParams.set(config.embedTokenParam, config.token);\n }\n return url.toString();\n }\n function authHeaders(input, init) {\n const headers = new Headers(\n init && init.headers ? init.headers : input instanceof Request ? input.headers : undefined\n );\n if (config.token && !headers.has(\"Authorization\")) {\n headers.set(\"Authorization\", \"Bearer \" + config.token);\n }\n if (!headers.has(config.embedTargetHeader)) {\n headers.set(config.embedTargetHeader, targetPath());\n }\n return headers;\n }\n if (typeof fetch === \"function\") {\n const originalFetch = fetch.bind(window);\n window.fetch = function(input, init) {\n const raw = input instanceof Request ? input.url : String(input);\n const url = rewrittenUrl(raw, false);\n if (!url) return originalFetch(input, init);\n const nextInit = Object.assign({}, init || {}, {\n headers: authHeaders(input, init),\n credentials: \"omit\"\n });\n if (input instanceof Request) {\n return originalFetch(new Request(url, input), nextInit);\n }\n return originalFetch(url, nextInit);\n };\n }\n if (typeof XMLHttpRequest !== \"undefined\") {\n const originalOpen = XMLHttpRequest.prototype.open;\n const originalSend = XMLHttpRequest.prototype.send;\n XMLHttpRequest.prototype.open = function(method, url) {\n const rewritten = rewrittenUrl(url, false);\n this.__agentNativeExternalEmbed = !!rewritten;\n return originalOpen.call(\n this,\n method,\n rewritten || url,\n arguments.length > 2 ? arguments[2] : true,\n arguments[3],\n arguments[4]\n );\n };\n XMLHttpRequest.prototype.send = function(body) {\n if (this.__agentNativeExternalEmbed) {\n try {\n if (config.token) this.setRequestHeader(\"Authorization\", \"Bearer \" + config.token);\n this.setRequestHeader(config.embedTargetHeader, targetPath());\n } catch (_err) {}\n }\n return originalSend.call(this, body);\n };\n }\n if (typeof EventSource !== \"undefined\") {\n const OriginalEventSource = EventSource;\n window.EventSource = function(url, options) {\n return new OriginalEventSource(rewrittenUrl(url, true) || url, options);\n };\n window.EventSource.prototype = OriginalEventSource.prototype;\n }\n }\n\n function copyDocumentElementAttributes(source) {\n const target = document.documentElement;\n for (const attr of Array.from(target.attributes)) {\n target.removeAttribute(attr.name);\n }\n for (const attr of Array.from(source.attributes)) {\n target.setAttribute(attr.name, attr.value);\n }\n }\n\n function importChildren(source, target) {\n target.replaceChildren(\n ...Array.from(source.childNodes).map((node) => document.importNode(node, true))\n );\n }\n\n function isModuleScript(script) {\n return (script.getAttribute(\"type\") || \"\").trim().toLowerCase() === \"module\";\n }\n\n function isRunnableClassicScript(script) {\n const type = (script.getAttribute(\"type\") || \"\").trim().toLowerCase();\n return !type || type === \"text/javascript\" || type === \"application/javascript\";\n }\n\n function runClassicScript(script) {\n const next = document.createElement(\"script\");\n for (const attr of Array.from(script.attributes)) {\n if (attr.name === \"type\") continue;\n next.setAttribute(attr.name, attr.value);\n }\n if (script.src) {\n next.src = script.src;\n } else {\n next.textContent = script.textContent || \"\";\n }\n document.body.appendChild(next);\n next.remove();\n }\n\n function rootRelativeSpecifiersToAbsolute(code, appOrigin) {\n return String(code).replace(/([\"'])\\\\/(?!\\\\/)/g, \"$1\" + appOrigin + \"/\");\n }\n\n function moduleCodeToClassicAsync(code, appOrigin) {\n return rootRelativeSpecifiersToAbsolute(code, appOrigin)\n .replace(\n /\\\\bimport\\\\s+\\\\*\\\\s+as\\\\s+([A-Za-z_$][\\\\w$]*)\\\\s+from\\\\s+([\"'][^\"']+[\"'])\\\\s*;?/g,\n \"const $1 = await import($2);\"\n )\n .replace(/\\\\bimport\\\\s+([\"'][^\"']+[\"'])\\\\s*;?/g, \"await import($1);\")\n .replace(/\\\\bimport\\\\(([\"'][^\"']+[\"'])\\\\)\\\\s*;?/g, \"await import($1);\");\n }\n\n function runModuleScriptAsClassic(script, appOrigin) {\n const code = moduleCodeToClassicAsync(script.textContent || \"\", appOrigin);\n const runner = document.createElement(\"script\");\n runner.textContent =\n \"(async()=>{\" +\n code +\n \"})().catch((err)=>{console.error('[agent-native] transplanted app module failed',err);document.body.setAttribute('data-agent-native-hydration-error',String(err&&err.message||err));});\";\n document.body.appendChild(runner);\n runner.remove();\n }\n\n function mountTransplantedHtml(html, appUrl) {\n const config = embedConfigForAppUrl(appUrl);\n installExternalEmbedRuntime(config);\n const parsed = new DOMParser().parseFromString(\n rewriteRootRelativeHtmlUrls(removeHtmlCspMeta(html), appUrl.origin),\n \"text/html\"\n );\n const scripts = Array.from(parsed.querySelectorAll(\"script\"));\n copyDocumentElementAttributes(parsed.documentElement);\n importChildren(parsed.head, document.head);\n const base = document.createElement(\"base\");\n base.href = config.baseHref;\n document.head.prepend(base);\n importChildren(parsed.body, document.body);\n for (const script of scripts) {\n if (isRunnableClassicScript(script)) runClassicScript(script);\n }\n for (const script of scripts) {\n if (isModuleScript(script)) runModuleScriptAsClassic(script, appUrl.origin);\n }\n }\n\n async function transplantAppDocument(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n lastFrameSrc = src;\n setMessage(\"Loading app\");\n const response = await fetch(src, {\n credentials: \"omit\",\n redirect: \"follow\",\n headers: { Accept: \"text/html\" }\n });\n if (!response.ok) {\n throw new Error(\"Embedded app returned HTTP \" + response.status + \".\");\n }\n const html = await response.text();\n const appUrl = new URL(response.url || src);\n try {\n window.history.replaceState(window.history.state, \"\", localPathFromUrl(appUrl, false));\n } catch {}\n mountTransplantedHtml(html, appUrl);\n notifyHostHeightRepeatedly();\n }\n\n function wantsEmbed() {\n if (toolInput.embed === false || toolInput.embed === \"false\") return false;\n if (embedByDefault) return true;\n return toolInput.embed === true || toolInput.embed === \"true\";\n }\n\n function supportedDisplayMode(mode) {\n if (openAiBridge && typeof openAiBridge.requestDisplayMode === \"function\") {\n return mode === \"inline\" || mode === \"fullscreen\" || mode === \"pip\";\n }\n const modes = hostState().context && hostState().context.availableDisplayModes;\n return Array.isArray(modes) && modes.includes(mode);\n }\n\n async function requestHostDisplayMode(mode) {\n let result;\n if (openAiBridge && typeof openAiBridge.requestDisplayMode === \"function\") {\n result = await openAiBridge.requestDisplayMode({ mode });\n } else {\n if (!app || typeof app.requestDisplayMode !== \"function\") {\n throw new Error(\"Display mode changes are not available in this host.\");\n }\n result = await app.requestDisplayMode({ mode });\n }\n updateDisplayButton();\n sendHostContext();\n return result;\n }\n\n function updateDisplayButton() {\n const context = hostState().context || {};\n const nextMode = context.displayMode === \"fullscreen\" ? \"inline\" : \"fullscreen\";\n const supported = supportedDisplayMode(nextMode);\n displayButton.hidden = !supported;\n displayButton.disabled = !supported;\n displayButton.textContent = nextMode === \"fullscreen\" ? \"Fullscreen\" : \"Inline\";\n displayButton.onclick = () => {\n if (!supportedDisplayMode(nextMode)) return;\n void requestHostDisplayMode(nextMode).catch((err) => {\n console.warn(\"[agent-native] MCP host rejected display mode request\", err);\n });\n };\n }\n\n function setMessage(message) {\n stage.innerHTML = '<div class=\"message\">' + esc(message) + '</div>';\n }\n\n function clearFrameReadyTimer() {\n if (!appFrameReadyTimer) return;\n clearTimeout(appFrameReadyTimer);\n appFrameReadyTimer = null;\n }\n\n function clearFrameLoadTimer() {\n if (!appFrameLoadTimer) return;\n clearTimeout(appFrameLoadTimer);\n appFrameLoadTimer = null;\n }\n\n function startFrameReadyTimer(frame) {\n clearFrameReadyTimer();\n appFrameReadyTimer = setTimeout(() => {\n if (!appFrameReady && appFrame === frame) renderFrameFallback();\n }, frameReadyTimeoutMs);\n }\n\n function renderFrameFallback() {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n stage.innerHTML =\n '<div class=\"fallback\">' +\n '<div class=\"fallback-title\">Open this app in its own tab</div>' +\n '<div class=\"fallback-copy\">This chat host did not allow the embedded app frame to load inline. You can still open the same app route through the host or use the URL below.</div>' +\n '<div class=\"fallback-actions\">' +\n '<button type=\"button\" data-fallback-open>Open app</button>' +\n '<button type=\"button\" data-fallback-retry>Try inline again</button>' +\n '</div>' +\n (openUrl ? '<a class=\"fallback-url\" href=\"' + esc(openUrl) + '\" target=\"_blank\" rel=\"noreferrer\">' + esc(openUrl) + '</a>' : '') +\n '</div>';\n const fallbackOpen = stage.querySelector(\"[data-fallback-open]\");\n const fallbackRetry = stage.querySelector(\"[data-fallback-retry]\");\n if (fallbackOpen) {\n fallbackOpen.disabled = !openUrl;\n fallbackOpen.onclick = () => {\n if (openUrl) void openFallbackExternal();\n };\n }\n if (fallbackRetry) {\n fallbackRetry.disabled = !lastFrameSrc;\n fallbackRetry.onclick = () => {\n if (lastFrameSrc) renderFrame(lastFrameSrc);\n };\n }\n }\n\n async function openFallbackExternal() {\n let url = withChatBridgeParam(openUrl);\n try {\n const result = await callEmbedSessionTool(embedSessionArgsFor(url));\n const data = parseToolResult(result);\n if (typeof data.startUrl === \"string\" && data.startUrl) {\n url = data.startUrl;\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP fallback could not mint a fresh app session\", err);\n }\n await openHostLink({ url });\n }\n\n function renderFrame(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n const frame = document.createElement(\"iframe\");\n frame.title = body.dataset.iframeTitle || \"Agent Native app\";\n frame.src = src;\n frame.allow = \"clipboard-read; clipboard-write\";\n appFrame = frame;\n appFrameReady = false;\n lastFrameSrc = src;\n frame.addEventListener(\"load\", () => {\n if (appFrame !== frame) return;\n clearFrameLoadTimer();\n sendFrameReadyMessages(frame);\n startFrameReadyTimer(frame);\n });\n stage.replaceChildren(frame);\n notifyHostHeight();\n appFrameLoadTimer = setTimeout(() => {\n if (!appFrameReady && appFrame === frame) renderFrameFallback();\n }, frameLoadTimeoutMs);\n }\n\n function shouldSelfNavigateToApp() {\n const mode = typeof toolInput.embedMode === \"string\"\n ? toolInput.embedMode\n : typeof toolInput.renderMode === \"string\"\n ? toolInput.renderMode\n : \"\";\n if (mode === \"iframe\" || mode === \"nested\") return false;\n if (toolInput.nested === true || toolInput.frame === \"iframe\") return false;\n return true;\n }\n\n function shouldTransplantAppDocument() {\n const mode = typeof toolInput.embedMode === \"string\"\n ? toolInput.embedMode\n : typeof toolInput.renderMode === \"string\"\n ? toolInput.renderMode\n : \"\";\n return (\n mode === \"transplant\" ||\n toolInput.frame === \"transplant\" ||\n isClaudeMcpContentHost()\n );\n }\n\n function isClaudeMcpContentHost() {\n try {\n return /(^|\\\\.)claudemcpcontent\\\\.com$/i.test(window.location.hostname || \"\");\n } catch {\n return false;\n }\n }\n\n function isChatGptSandboxHost() {\n try {\n const host = window.location.hostname || \"\";\n const appParam = new URL(window.location.href).searchParams.get(\"app\");\n return /(^|\\\\.)oaiusercontent\\\\.com$/i.test(host) || appParam === \"chatgpt\";\n } catch {\n return false;\n }\n }\n\n function shouldRenderControlledAppFrame() {\n return !!openAiBridge || isChatGptSandboxHost();\n }\n\n function navigateToAppFrame(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n lastFrameSrc = src;\n setMessage(\"Opening app\");\n try {\n window.location.replace(src);\n } catch (err) {\n console.warn(\"[agent-native] MCP app self-navigation failed\", err);\n renderFrameFallback();\n }\n }\n\n async function updateHostModelContext(data) {\n const params = {};\n if (Array.isArray(data && data.content)) params.content = data.content;\n if (data && data.structuredContent && typeof data.structuredContent === \"object\") {\n params.structuredContent = data.structuredContent;\n }\n if (openAiBridge && typeof openAiBridge.setWidgetState === \"function\") {\n openAiBridge.setWidgetState({\n ...objectValue(openAiBridge.widgetState),\n agentNativeModelContext: params\n });\n return { ok: true };\n }\n if (!app || typeof app.updateModelContext !== \"function\") return { ok: false };\n await app.updateModelContext(params);\n return { ok: true };\n }\n\n async function openHostLink(data) {\n const url = typeof (data && data.url) === \"string\" ? data.url : \"\";\n if (!url) return { isError: true };\n if (openAiBridge && typeof openAiBridge.openExternal === \"function\") {\n return await openAiBridge.openExternal({ href: url, redirectUrl: false });\n }\n if (app && typeof app.openLink === \"function\") {\n return await app.openLink({ url });\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n return { ok: true };\n }\n\n function notifyHostHeight() {\n const height = applyIntrinsicHeight(visibleIntrinsicHeight());\n if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== \"function\") {\n if (app && typeof app.sendSizeChanged === \"function\") {\n try {\n app.sendSizeChanged({ height });\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected size update\", err);\n }\n }\n return;\n }\n try {\n openAiBridge.notifyIntrinsicHeight({ height });\n } catch (err) {\n console.warn(\"[agent-native] ChatGPT rejected intrinsic height update\", err);\n }\n }\n\n function respondToAppFrame(requestId, work) {\n if (!requestId) return;\n Promise.resolve(work)\n .then((result) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: { requestId, ok: true, result }\n });\n })\n .catch((err) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: {\n requestId,\n ok: false,\n error: err && err.message ? err.message : String(err)\n }\n });\n });\n }\n\n async function sendHostChat(chat) {\n if (!chat || chat.submit === false) return;\n const message = typeof chat.message === \"string\" ? chat.message : \"\";\n if (!message.trim()) return;\n const context = typeof chat.context === \"string\" ? chat.context : \"\";\n if (context.trim()) {\n try {\n if (openAiBridge && typeof openAiBridge.setWidgetState === \"function\") {\n openAiBridge.setWidgetState({\n ...objectValue(openAiBridge.widgetState),\n agentNativeChatContext: context\n });\n } else if (app && typeof app.updateModelContext === \"function\") {\n await app.updateModelContext({\n content: [{ type: \"text\", text: context }]\n });\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected model context update\", err);\n }\n }\n try {\n if (openAiBridge && typeof openAiBridge.sendFollowUpMessage === \"function\") {\n await openAiBridge.sendFollowUpMessage({\n prompt: context.trim() ? context.trim() + \"\\\\n\\\\n\" + message : message,\n scrollToBottom: true\n });\n return;\n }\n if (!app || typeof app.sendMessage !== \"function\") return;\n const result = await app.sendMessage({\n role: \"user\",\n content: [{ type: \"text\", text: message }]\n });\n if (result && result.isError) {\n console.warn(\"[agent-native] MCP host rejected chat message\", result);\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host chat bridge failed\", err);\n }\n }\n\n window.addEventListener(\"message\", (event) => {\n if (!appFrame || event.source !== appFrame.contentWindow) return;\n if (!event.data) return;\n const data = event.data.data || {};\n if (event.data.type === \"agentNative.embeddedAppReady\") {\n appFrameReady = true;\n clearFrameLoadTimer();\n clearFrameReadyTimer();\n return;\n }\n if (event.data.type === \"agentNative.submitChat\") {\n void sendHostChat(data);\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.updateModelContext\") {\n respondToAppFrame(data.requestId, updateHostModelContext(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.openLink\") {\n respondToAppFrame(data.requestId, openHostLink(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.requestDisplayMode\") {\n respondToAppFrame(data.requestId, requestHostDisplayMode(data.mode));\n }\n });\n\n function notifyHostHeightSoon() {\n requestAnimationFrame(() => notifyHostHeight());\n }\n\n function notifyHostHeightRepeatedly() {\n notifyHostHeight();\n [0, 250, 1000, 2500].forEach((delay) => {\n setTimeout(() => notifyHostHeight(), delay);\n });\n }\n\n window.addEventListener(\"resize\", notifyHostHeightSoon, { passive: true });\n if (window.visualViewport) {\n window.visualViewport.addEventListener(\"resize\", notifyHostHeightSoon, { passive: true });\n }\n\n async function launchEmbed() {\n if (!openUrl) {\n setMessage(\"Open link was not available.\");\n return;\n }\n if (!wantsEmbed()) {\n setMessage(\"Ready to open.\");\n return;\n }\n if (startedFor === openUrl) return;\n startedFor = openUrl;\n setMessage(\"Loading app\");\n try {\n const selfNavigate = shouldSelfNavigateToApp();\n const embedUrl = withChatBridgeParam(openUrl);\n if (selfNavigate && isEmbedStartUrl(embedUrl)) {\n if (isClaudeMcpContentHost() && shouldTransplantAppDocument()) {\n await transplantAppDocument(embedUrl);\n } else if (shouldRenderControlledAppFrame()) {\n renderFrame(embedUrl);\n } else {\n navigateToAppFrame(embedUrl);\n }\n return;\n }\n if (!selfNavigate && isEmbedStartUrl(embedUrl)) {\n renderFrame(embedUrl);\n return;\n }\n const result = await callEmbedSessionTool(embedSessionArgsFor(embedUrl));\n const data = parseToolResult(result);\n if (typeof data.startUrl !== \"string\" || !data.startUrl) {\n startedFor = \"\";\n setMessage(data.error || \"This app can be opened, but not embedded from this MCP server.\");\n return;\n }\n if (selfNavigate) {\n if (isClaudeMcpContentHost() && shouldTransplantAppDocument()) {\n await transplantAppDocument(data.startUrl);\n } else if (shouldRenderControlledAppFrame()) {\n renderFrame(data.startUrl);\n } else {\n navigateToAppFrame(data.startUrl);\n }\n } else {\n renderFrame(data.startUrl);\n }\n } catch (err) {\n startedFor = \"\";\n setMessage(err && err.message ? err.message : \"Could not launch embedded app.\");\n }\n }\n\n async function callEmbedSessionTool(args) {\n if (openAiBridge && typeof openAiBridge.callTool === \"function\") {\n return await openAiBridge.callTool(startTool, args);\n }\n if (!app || typeof app.callServerTool !== \"function\") {\n throw new Error(\"Host tool calls are not available.\");\n }\n return await app.callServerTool({ name: startTool, arguments: args });\n }\n\n function updateHostOpenInAppUrl() {\n if (!openAiBridge || !openUrl || typeof openAiBridge.setOpenInAppUrl !== \"function\") {\n return;\n }\n try {\n openAiBridge.setOpenInAppUrl({ href: openUrl });\n } catch (err) {\n console.warn(\"[agent-native] ChatGPT rejected open-in-app URL\", err);\n }\n }\n\n function updateOpenButton() {\n openButton.disabled = !openUrl;\n openButton.onclick = () => {\n if (openUrl) void openHostLink({ url: openUrl });\n };\n updateHostOpenInAppUrl();\n }\n\n function updateTitle(data) {\n const label = data.label || data.app || data.view || body.dataset.appTitle || \"App\";\n titleEl.textContent = String(label);\n }\n\n function readOpenAiBridge() {\n return window.openai && typeof window.openai === \"object\"\n ? window.openai\n : null;\n }\n\n function openAiToolResultParams(bridge) {\n const params = {};\n if (bridge && bridge.toolOutput !== undefined) {\n if (bridge.toolOutput && typeof bridge.toolOutput === \"object\") {\n params.structuredContent = bridge.toolOutput;\n } else {\n params.content = [{ type: \"text\", text: String(bridge.toolOutput) }];\n }\n }\n if (bridge && bridge.toolResponseMetadata && typeof bridge.toolResponseMetadata === \"object\") {\n params._meta = bridge.toolResponseMetadata;\n }\n return params;\n }\n\n function syncOpenAiBridge(bridge) {\n if (!bridge) return false;\n openAiBridge = bridge;\n toolInput = objectValue(bridge.toolInput);\n const params = openAiToolResultParams(bridge);\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n if (openUrl) {\n void launchEmbed();\n } else if (!appFrame) {\n setMessage(\"Waiting for app result\");\n }\n return true;\n }\n\n function waitForOpenAiBridge() {\n const existing = readOpenAiBridge();\n if (existing) return Promise.resolve(existing);\n return new Promise((resolve) => {\n let settled = false;\n const finish = (bridge) => {\n if (settled) return;\n settled = true;\n window.removeEventListener(\"openai:set_globals\", onGlobals);\n clearTimeout(timer);\n resolve(bridge || readOpenAiBridge());\n };\n const onGlobals = () => finish(readOpenAiBridge());\n const timer = setTimeout(() => finish(null), 200);\n window.addEventListener(\"openai:set_globals\", onGlobals, { passive: true });\n });\n }\n\n window.addEventListener(\"openai:set_globals\", () => {\n const bridge = readOpenAiBridge();\n if (bridge && (!appFrame || openAiBridge)) syncOpenAiBridge(bridge);\n }, { passive: true });\n\n async function startMcpAppsBridge() {\n const { App } = await import(\"${MCP_APP_IMPORT}\");\n app = new App(\n { name: \"Agent Native Embed\", version: \"1.0.0\" },\n {},\n { autoResize: false }\n );\n app.ontoolinput = (params) => {\n toolInput = params.arguments || {};\n };\n app.ontoolresult = (params) => {\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n void launchEmbed();\n };\n app.onhostcontextchanged = () => {\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n };\n await app.connect();\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n }\n\n const initialOpenAiBridge = await waitForOpenAiBridge();\n if (!syncOpenAiBridge(initialOpenAiBridge)) {\n await startMcpAppsBridge();\n }\n </script>\n</body>\n</html>`,\n csp: {\n connectDomains: [\"https://esm.sh\", MCP_APP_REQUEST_ORIGIN_CSP_SOURCE],\n resourceDomains: [\n \"https://esm.sh\",\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n baseUriDomains: [MCP_APP_REQUEST_ORIGIN_CSP_SOURCE],\n frameDomains,\n },\n prefersBorder: false,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"embed-app.js","sourceRoot":"","sources":["../../src/mcp/embed-app.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ActionMcpAppResourceConfig } from \"../action.js\";\nimport { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from \"../shared/embed-auth.js\";\n\nconst MCP_APP_IMPORT =\n \"https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps\";\n\nexport const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = \"$requestOrigin\";\nconst MCP_APP_WRAPPER_CHROME_HEIGHT = 44;\nexport const DEFAULT_MCP_APP_SHELL_HEIGHT = 560;\nexport const DEFAULT_MCP_APP_VIEWPORT_HEIGHT =\n DEFAULT_MCP_APP_SHELL_HEIGHT - MCP_APP_WRAPPER_CHROME_HEIGHT;\n\nexport interface EmbedAppOptions {\n title?: string;\n description?: string;\n iframeTitle?: string;\n openLabel?: string;\n embedByDefault?: boolean;\n startToolName?: string;\n frameDomains?: string[];\n height?: number;\n}\n\nfunction attr(value: string | undefined): string {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/\"/g, \""\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nexport function embedApp(\n options: EmbedAppOptions = {},\n): ActionMcpAppResourceConfig {\n const title = options.title ?? \"Open app\";\n const iframeTitle = options.iframeTitle ?? \"Agent Native app\";\n const openLabel = options.openLabel ?? \"Open in app\";\n const startToolName = options.startToolName ?? \"create_embed_session\";\n const embedByDefault = options.embedByDefault !== false;\n const height = Math.max(\n 320,\n Math.min(900, options.height ?? DEFAULT_MCP_APP_SHELL_HEIGHT),\n );\n const viewportHeight = height - MCP_APP_WRAPPER_CHROME_HEIGHT;\n const frameDomains = [\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ];\n\n return {\n title,\n ...(options.description ? { description: options.description } : {}),\n html: () => `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <style>\n :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: Canvas; color: CanvasText; --agent-native-shell-height: ${height}px; --agent-native-viewport-height: ${viewportHeight}px; }\n * { box-sizing: border-box; }\n body { margin: 0; }\n .shell { display: grid; gap: 8px; min-height: var(--agent-native-shell-height); padding: 0; }\n .bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 36px; padding: 6px 8px; border-bottom: 1px solid color-mix(in srgb, CanvasText 12%, Canvas); }\n .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 700; color: color-mix(in srgb, CanvasText 72%, Canvas); }\n .actions { display: flex; align-items: center; gap: 6px; }\n button { min-height: 28px; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 7px; background: Canvas; color: CanvasText; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; padding: 0 9px; }\n button:disabled { opacity: .55; cursor: default; }\n .stage { position: relative; min-height: var(--agent-native-viewport-height); }\n iframe { display: block; width: 100%; height: var(--agent-native-viewport-height); border: 0; background: Canvas; }\n .message { display: grid; place-items: center; min-height: var(--agent-native-viewport-height); padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }\n .fallback { display: grid; align-content: center; justify-items: center; gap: 12px; min-height: var(--agent-native-viewport-height); padding: 24px; background: Canvas; color: CanvasText; text-align: center; }\n .fallback-title { max-width: 440px; font-size: 14px; font-weight: 700; }\n .fallback-copy { max-width: 520px; color: color-mix(in srgb, CanvasText 64%, Canvas); font-size: 13px; line-height: 1.45; }\n .fallback-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }\n .fallback-url { max-width: min(560px, 100%); overflow-wrap: anywhere; color: color-mix(in srgb, CanvasText 76%, Canvas); font-size: 12px; }\n </style>\n</head>\n<body\n data-app-title=\"${attr(title)}\"\n data-iframe-title=\"${attr(iframeTitle)}\"\n data-open-label=\"${attr(openLabel)}\"\n data-start-tool=\"${attr(startToolName)}\"\n data-embed-default=\"${embedByDefault ? \"1\" : \"0\"}\"\n>\n <main class=\"shell\">\n <div class=\"bar\">\n <div class=\"title\" data-title-label>${attr(title)}</div>\n <div class=\"actions\">\n <button type=\"button\" data-display hidden disabled>Fullscreen</button>\n <button type=\"button\" data-open disabled>${attr(openLabel)}</button>\n </div>\n </div>\n <section class=\"stage\" data-stage>\n <div class=\"message\">Preparing app</div>\n </section>\n </main>\n <script type=\"module\">\n const body = document.body;\n const stage = document.querySelector(\"[data-stage]\");\n const titleEl = document.querySelector(\"[data-title-label]\");\n const openButton = document.querySelector(\"[data-open]\");\n const displayButton = document.querySelector(\"[data-display]\");\n const startTool = body.dataset.startTool || \"create_embed_session\";\n const embedByDefault = body.dataset.embedDefault !== \"0\";\n const chatBridgeParam = ${JSON.stringify(MCP_APP_CHAT_BRIDGE_QUERY_PARAM)};\n const defaultIntrinsicHeight = ${height};\n const chromeHeight = ${MCP_APP_WRAPPER_CHROME_HEIGHT};\n const frameReadyMessageDelays = [0, 200, 500, 1500, 3000, 7000, 15000, 30000];\n const frameReadyTimeoutMs = 45000;\n const frameLoadTimeoutMs = 45000;\n let app = null;\n let openAiBridge = null;\n let toolInput = {};\n let openUrl = \"\";\n let openStartUrl = \"\";\n let startedFor = \"\";\n let appFrame = null;\n let appFrameReady = false;\n let appFrameReadyTimer = null;\n let appFrameLoadTimer = null;\n let lastFrameSrc = \"\";\n\n function esc(value) {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n }\n\n function parseJson(value, fallback) {\n if (value && typeof value === \"object\") return value;\n if (typeof value !== \"string\" || !value.trim()) return fallback;\n try { return JSON.parse(value); } catch { return fallback; }\n }\n\n function objectValue(value) {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value\n : {};\n }\n\n function finiteNumber(value) {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0\n ? value\n : null;\n }\n\n function contextMaxHeight(context) {\n if (!context || typeof context !== \"object\") return null;\n return finiteNumber(context.maxHeight) ||\n finiteNumber(context.containerDimensions && context.containerDimensions.maxHeight);\n }\n\n function visibleIntrinsicHeight() {\n const context = hostState().context || {};\n const hostMaxHeight = contextMaxHeight(context);\n if (hostMaxHeight) return Math.floor(hostMaxHeight);\n const viewportHeight = finiteNumber(window.visualViewport && window.visualViewport.height) ||\n finiteNumber(window.innerHeight);\n return Math.floor(viewportHeight || defaultIntrinsicHeight);\n }\n\n function applyIntrinsicHeight(nextHeight) {\n const boundedHeight = Math.min(\n defaultIntrinsicHeight,\n Math.floor(nextHeight || defaultIntrinsicHeight)\n );\n const height = Math.max(320, boundedHeight);\n const viewportHeight = Math.max(0, height - chromeHeight);\n document.documentElement.style.setProperty(\"--agent-native-shell-height\", height + \"px\");\n document.documentElement.style.setProperty(\"--agent-native-viewport-height\", viewportHeight + \"px\");\n if (appFrame) appFrame.style.height = viewportHeight + \"px\";\n return height;\n }\n\n function parseToolResult(params) {\n if (!params) return {};\n if (params.result && typeof params.result === \"object\") {\n return parseToolResult(params.result);\n }\n if (params.toolResult && typeof params.toolResult === \"object\") {\n return parseToolResult(params.toolResult);\n }\n if (params.structuredContent && typeof params.structuredContent === \"object\") {\n return params.structuredContent;\n }\n const parts = Array.isArray(params.content) ? params.content : [];\n const textPart = parts.find((part) => part && part.type === \"text\" && typeof part.text === \"string\");\n return parseJson(textPart ? textPart.text : \"\", {});\n }\n\n function openLinkRecordFrom(value) {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value\n : {};\n }\n\n function openLinkWebUrlFrom(value) {\n const record = openLinkRecordFrom(value);\n return typeof record.webUrl === \"string\" ? record.webUrl : \"\";\n }\n\n function firstNonEmbedStartUrl(values) {\n for (const value of values) {\n if (typeof value === \"string\" && value && !isEmbedStartUrl(value)) return value;\n }\n return \"\";\n }\n\n function firstEmbedStartUrl(values) {\n for (const value of values) {\n if (typeof value === \"string\" && value && isEmbedStartUrl(value)) {\n return withChatBridgeParam(value);\n }\n }\n return \"\";\n }\n\n function openLinkFrom(params, data) {\n const openLink = params && params._meta && params._meta[\"agent-native/openLink\"];\n const metaUrl = openLinkWebUrlFrom(openLink);\n const record = data && typeof data === \"object\" ? data : {};\n const structuredOpenLinkUrl = openLinkWebUrlFrom(record.openLink);\n return firstNonEmbedStartUrl([\n record.embedTargetPath,\n record.deepLinkUrl,\n record.deepLink,\n record.openUrl,\n record.url,\n structuredOpenLinkUrl,\n metaUrl\n ]);\n }\n\n function embedStartUrlFrom(params, data) {\n const openLink = params && params._meta && params._meta[\"agent-native/openLink\"];\n const metaUrl = openLinkWebUrlFrom(openLink);\n const record = data && typeof data === \"object\" ? data : {};\n return firstEmbedStartUrl([\n record.embedStartUrl,\n record.startUrl,\n record.url,\n openLinkWebUrlFrom(record.openLink),\n metaUrl\n ]);\n }\n\n function hostState() {\n if (openAiBridge) {\n return {\n context: {\n displayMode: openAiBridge.displayMode,\n availableDisplayModes: typeof openAiBridge.requestDisplayMode === \"function\"\n ? [\"inline\", \"fullscreen\", \"pip\"]\n : [],\n maxHeight: openAiBridge.maxHeight,\n locale: openAiBridge.locale,\n theme: openAiBridge.theme,\n view: openAiBridge.view\n },\n capabilities: { openai: true },\n version: openAiBridge.userAgent\n };\n }\n return {\n context: app && app.getHostContext ? app.getHostContext() : undefined,\n capabilities: app && app.getHostCapabilities ? app.getHostCapabilities() : undefined,\n version: app && app.getHostVersion ? app.getHostVersion() : undefined\n };\n }\n\n function sendToAppFrame(message) {\n if (!appFrame || !appFrame.contentWindow) return;\n try { appFrame.contentWindow.postMessage(message, \"*\"); } catch {}\n }\n\n function sendHostContext() {\n sendToAppFrame({ type: \"agentNative.mcpHostContext\", data: hostState() });\n }\n\n function sendFrameReadyMessages(frame) {\n const originPayload = { type: \"agentNative.frameOrigin\", origin: window.location.origin };\n frameReadyMessageDelays.forEach((delay) => {\n setTimeout(() => {\n try { frame.contentWindow && frame.contentWindow.postMessage(originPayload, \"*\"); } catch {}\n sendHostContext();\n }, delay);\n });\n }\n\n function withChatBridgeParam(value) {\n if (typeof value !== \"string\" || !value) return value;\n try {\n const base = \"http://agent-native.invalid\";\n const url = value.startsWith(\"/\") ? new URL(value, base) : new URL(value);\n url.searchParams.set(chatBridgeParam, \"1\");\n return value.startsWith(\"/\")\n ? url.pathname + url.search + url.hash\n : url.toString();\n } catch {\n return value;\n }\n }\n\n function embedSessionArgsFor(value) {\n const chrome = typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\";\n return typeof value === \"string\" && value.startsWith(\"/\")\n ? { path: value, chrome }\n : { url: value, chrome };\n }\n\n function isEmbedStartUrl(value) {\n if (typeof value !== \"string\" || !value) return false;\n try {\n const url = new URL(value, window.location.href);\n return url.pathname.endsWith(\"/_agent-native/embed/start\");\n } catch {\n return false;\n }\n }\n\n function localPathFromUrl(url, includeToken) {\n const next = new URL(url.href);\n if (!includeToken) next.searchParams.delete(\"__an_embed_token\");\n return next.pathname + next.search + next.hash;\n }\n\n function rewriteRootRelativeHtmlUrls(html, appOrigin) {\n return String(html).replace(\n /\\\\b(src|href|poster|action)\\\\s*=\\\\s*([\"'])\\\\/(?!\\\\/)/gi,\n (_match, name, quote) => String(name) + \"=\" + quote + appOrigin + \"/\"\n );\n }\n\n function removeHtmlCspMeta(html) {\n return String(html).replace(\n /<meta\\\\s+[^>]*http-equiv\\\\s*=\\\\s*([\"'])?content-security-policy\\\\1?[^>]*>/gi,\n \"\"\n );\n }\n\n function embedConfigForAppUrl(appUrl) {\n const sanitizedTarget = localPathFromUrl(appUrl, false);\n return {\n origin: appUrl.origin,\n href: appUrl.href,\n baseHref: appUrl.origin + appUrl.pathname,\n target: sanitizedTarget,\n token: appUrl.searchParams.get(\"__an_embed_token\") || \"\",\n chatBridgeActive: appUrl.searchParams.get(chatBridgeParam) === \"1\",\n chatBridgeParam,\n embedTokenParam: \"__an_embed_token\",\n embedTargetHeader: \"x-agent-native-embed-target\"\n };\n }\n\n function installExternalEmbedRuntime(config) {\n window.__AGENT_NATIVE_EXTERNAL_EMBED = config;\n try {\n if (config.target) {\n window.history.replaceState(window.history.state, \"\", config.target);\n }\n } catch (_err) {}\n try {\n if (config.token) {\n sessionStorage.setItem(\"agent-native:embed-auth-token\", config.token);\n }\n if (config.chatBridgeActive && config.token) {\n sessionStorage.setItem(\"agent-native:mcp-chat-bridge\", config.token);\n }\n } catch (_err) {}\n if (window.__agentNativeExternalEmbedRuntimeInstalled) return;\n window.__agentNativeExternalEmbedRuntimeInstalled = true;\n function appOrigin() {\n try {\n return new URL(config.origin).origin;\n } catch (_err) {\n return \"\";\n }\n }\n function targetPath() {\n return config.target || location.pathname + location.search;\n }\n function rewrittenUrl(value, appendToken) {\n const origin = appOrigin();\n if (!origin) return null;\n let url;\n try {\n url = new URL(value, location.href);\n } catch (_err) {\n return null;\n }\n if (url.origin !== location.origin && url.origin !== origin) return null;\n if (url.origin !== origin) {\n const app = new URL(origin);\n url.protocol = app.protocol;\n url.host = app.host;\n }\n if (appendToken && config.token && url.pathname === \"/_agent-native/events\") {\n url.searchParams.set(config.embedTokenParam, config.token);\n }\n return url.toString();\n }\n function authHeaders(input, init) {\n const headers = new Headers(\n init && init.headers ? init.headers : input instanceof Request ? input.headers : undefined\n );\n if (config.token && !headers.has(\"Authorization\")) {\n headers.set(\"Authorization\", \"Bearer \" + config.token);\n }\n if (!headers.has(config.embedTargetHeader)) {\n headers.set(config.embedTargetHeader, targetPath());\n }\n return headers;\n }\n if (typeof fetch === \"function\") {\n const originalFetch = fetch.bind(window);\n window.fetch = function(input, init) {\n const raw = input instanceof Request ? input.url : String(input);\n const url = rewrittenUrl(raw, false);\n if (!url) return originalFetch(input, init);\n const nextInit = Object.assign({}, init || {}, {\n headers: authHeaders(input, init),\n credentials: \"omit\"\n });\n if (input instanceof Request) {\n return originalFetch(new Request(url, input), nextInit);\n }\n return originalFetch(url, nextInit);\n };\n }\n if (typeof XMLHttpRequest !== \"undefined\") {\n const originalOpen = XMLHttpRequest.prototype.open;\n const originalSend = XMLHttpRequest.prototype.send;\n XMLHttpRequest.prototype.open = function(method, url) {\n const rewritten = rewrittenUrl(url, false);\n this.__agentNativeExternalEmbed = !!rewritten;\n return originalOpen.call(\n this,\n method,\n rewritten || url,\n arguments.length > 2 ? arguments[2] : true,\n arguments[3],\n arguments[4]\n );\n };\n XMLHttpRequest.prototype.send = function(body) {\n if (this.__agentNativeExternalEmbed) {\n try {\n if (config.token) this.setRequestHeader(\"Authorization\", \"Bearer \" + config.token);\n this.setRequestHeader(config.embedTargetHeader, targetPath());\n } catch (_err) {}\n }\n return originalSend.call(this, body);\n };\n }\n if (typeof EventSource !== \"undefined\") {\n const OriginalEventSource = EventSource;\n window.EventSource = function(url, options) {\n return new OriginalEventSource(rewrittenUrl(url, true) || url, options);\n };\n window.EventSource.prototype = OriginalEventSource.prototype;\n }\n }\n\n function copyDocumentElementAttributes(source) {\n const target = document.documentElement;\n for (const attr of Array.from(target.attributes)) {\n target.removeAttribute(attr.name);\n }\n for (const attr of Array.from(source.attributes)) {\n target.setAttribute(attr.name, attr.value);\n }\n }\n\n function importChildren(source, target) {\n target.replaceChildren(\n ...Array.from(source.childNodes).map((node) => document.importNode(node, true))\n );\n }\n\n function isModuleScript(script) {\n return (script.getAttribute(\"type\") || \"\").trim().toLowerCase() === \"module\";\n }\n\n function isRunnableClassicScript(script) {\n const type = (script.getAttribute(\"type\") || \"\").trim().toLowerCase();\n return !type || type === \"text/javascript\" || type === \"application/javascript\";\n }\n\n function runClassicScript(script) {\n const next = document.createElement(\"script\");\n for (const attr of Array.from(script.attributes)) {\n if (attr.name === \"type\") continue;\n next.setAttribute(attr.name, attr.value);\n }\n if (script.src) {\n next.src = script.src;\n } else {\n next.textContent = script.textContent || \"\";\n }\n document.body.appendChild(next);\n next.remove();\n }\n\n function rootRelativeSpecifiersToAbsolute(code, appOrigin) {\n return String(code).replace(/([\"'])\\\\/(?!\\\\/)/g, \"$1\" + appOrigin + \"/\");\n }\n\n function moduleCodeToClassicAsync(code, appOrigin) {\n return rootRelativeSpecifiersToAbsolute(code, appOrigin)\n .replace(\n /\\\\bimport\\\\s+\\\\*\\\\s+as\\\\s+([A-Za-z_$][\\\\w$]*)\\\\s+from\\\\s+([\"'][^\"']+[\"'])\\\\s*;?/g,\n \"const $1 = await import($2);\"\n )\n .replace(/\\\\bimport\\\\s+([\"'][^\"']+[\"'])\\\\s*;?/g, \"await import($1);\")\n .replace(/\\\\bimport\\\\(([\"'][^\"']+[\"'])\\\\)\\\\s*;?/g, \"await import($1);\");\n }\n\n function runModuleScriptAsClassic(script, appOrigin) {\n const code = moduleCodeToClassicAsync(script.textContent || \"\", appOrigin);\n const runner = document.createElement(\"script\");\n runner.textContent =\n \"(async()=>{\" +\n code +\n \"})().catch((err)=>{console.error('[agent-native] transplanted app module failed',err);document.body.setAttribute('data-agent-native-hydration-error',String(err&&err.message||err));});\";\n document.body.appendChild(runner);\n runner.remove();\n }\n\n function mountTransplantedHtml(html, appUrl) {\n const config = embedConfigForAppUrl(appUrl);\n installExternalEmbedRuntime(config);\n const parsed = new DOMParser().parseFromString(\n rewriteRootRelativeHtmlUrls(removeHtmlCspMeta(html), appUrl.origin),\n \"text/html\"\n );\n const scripts = Array.from(parsed.querySelectorAll(\"script\"));\n copyDocumentElementAttributes(parsed.documentElement);\n importChildren(parsed.head, document.head);\n const base = document.createElement(\"base\");\n base.href = config.baseHref;\n document.head.prepend(base);\n importChildren(parsed.body, document.body);\n for (const script of scripts) {\n if (isRunnableClassicScript(script)) runClassicScript(script);\n }\n for (const script of scripts) {\n if (isModuleScript(script)) runModuleScriptAsClassic(script, appUrl.origin);\n }\n }\n\n async function transplantAppDocument(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n lastFrameSrc = src;\n setMessage(\"Loading app\");\n const response = await fetch(src, {\n credentials: \"omit\",\n redirect: \"follow\",\n headers: { Accept: \"text/html\" }\n });\n if (!response.ok) {\n throw new Error(\"Embedded app returned HTTP \" + response.status + \".\");\n }\n const html = await response.text();\n const appUrl = new URL(response.url || src);\n try {\n window.history.replaceState(window.history.state, \"\", localPathFromUrl(appUrl, false));\n } catch {}\n mountTransplantedHtml(html, appUrl);\n notifyHostHeightRepeatedly();\n }\n\n function wantsEmbed() {\n if (toolInput.embed === false || toolInput.embed === \"false\") return false;\n if (embedByDefault) return true;\n return toolInput.embed === true || toolInput.embed === \"true\";\n }\n\n function supportedDisplayMode(mode) {\n if (openAiBridge && typeof openAiBridge.requestDisplayMode === \"function\") {\n return mode === \"inline\" || mode === \"fullscreen\" || mode === \"pip\";\n }\n const modes = hostState().context && hostState().context.availableDisplayModes;\n return Array.isArray(modes) && modes.includes(mode);\n }\n\n async function requestHostDisplayMode(mode) {\n let result;\n if (openAiBridge && typeof openAiBridge.requestDisplayMode === \"function\") {\n result = await openAiBridge.requestDisplayMode({ mode });\n } else {\n if (!app || typeof app.requestDisplayMode !== \"function\") {\n throw new Error(\"Display mode changes are not available in this host.\");\n }\n result = await app.requestDisplayMode({ mode });\n }\n updateDisplayButton();\n sendHostContext();\n return result;\n }\n\n function updateDisplayButton() {\n const context = hostState().context || {};\n const nextMode = context.displayMode === \"fullscreen\" ? \"inline\" : \"fullscreen\";\n const supported = supportedDisplayMode(nextMode);\n displayButton.hidden = !supported;\n displayButton.disabled = !supported;\n displayButton.textContent = nextMode === \"fullscreen\" ? \"Fullscreen\" : \"Inline\";\n displayButton.onclick = () => {\n if (!supportedDisplayMode(nextMode)) return;\n void requestHostDisplayMode(nextMode).catch((err) => {\n console.warn(\"[agent-native] MCP host rejected display mode request\", err);\n });\n };\n }\n\n function setMessage(message) {\n stage.innerHTML = '<div class=\"message\">' + esc(message) + '</div>';\n }\n\n function clearFrameReadyTimer() {\n if (!appFrameReadyTimer) return;\n clearTimeout(appFrameReadyTimer);\n appFrameReadyTimer = null;\n }\n\n function clearFrameLoadTimer() {\n if (!appFrameLoadTimer) return;\n clearTimeout(appFrameLoadTimer);\n appFrameLoadTimer = null;\n }\n\n function startFrameReadyTimer(frame) {\n clearFrameReadyTimer();\n appFrameReadyTimer = setTimeout(() => {\n if (!appFrameReady && appFrame === frame) renderFrameFallback();\n }, frameReadyTimeoutMs);\n }\n\n function renderFrameFallback() {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n stage.innerHTML =\n '<div class=\"fallback\">' +\n '<div class=\"fallback-title\">Open this app in its own tab</div>' +\n '<div class=\"fallback-copy\">This chat host did not allow the embedded app frame to load inline. You can still open the same app route through the host or use the URL below.</div>' +\n '<div class=\"fallback-actions\">' +\n '<button type=\"button\" data-fallback-open>Open app</button>' +\n '<button type=\"button\" data-fallback-retry>Try inline again</button>' +\n '</div>' +\n (openUrl || openStartUrl ? '<a class=\"fallback-url\" href=\"' + esc(openUrl || openStartUrl) + '\" target=\"_blank\" rel=\"noreferrer\">' + esc(openUrl || openStartUrl) + '</a>' : '') +\n '</div>';\n const fallbackOpen = stage.querySelector(\"[data-fallback-open]\");\n const fallbackRetry = stage.querySelector(\"[data-fallback-retry]\");\n if (fallbackOpen) {\n fallbackOpen.disabled = !(openUrl || openStartUrl);\n fallbackOpen.onclick = () => {\n if (openUrl || openStartUrl) void openFallbackExternal();\n };\n }\n if (fallbackRetry) {\n fallbackRetry.disabled = !lastFrameSrc;\n fallbackRetry.onclick = () => {\n if (lastFrameSrc) renderFrame(lastFrameSrc);\n };\n }\n }\n\n async function openFallbackExternal() {\n let url = withChatBridgeParam(openUrl);\n try {\n if (url) {\n const result = await callEmbedSessionTool(embedSessionArgsFor(url));\n const data = parseToolResult(result);\n if (typeof data.startUrl === \"string\" && data.startUrl) {\n url = withChatBridgeParam(data.startUrl);\n }\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP fallback could not mint a fresh app session\", err);\n }\n if (!url) url = withChatBridgeParam(openStartUrl);\n await openHostLink({ url });\n }\n\n function renderFrame(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n const frame = document.createElement(\"iframe\");\n frame.title = body.dataset.iframeTitle || \"Agent Native app\";\n frame.src = src;\n frame.allow = \"clipboard-read; clipboard-write\";\n appFrame = frame;\n appFrameReady = false;\n lastFrameSrc = src;\n frame.addEventListener(\"load\", () => {\n if (appFrame !== frame) return;\n clearFrameLoadTimer();\n sendFrameReadyMessages(frame);\n startFrameReadyTimer(frame);\n });\n stage.replaceChildren(frame);\n notifyHostHeight();\n appFrameLoadTimer = setTimeout(() => {\n if (!appFrameReady && appFrame === frame) renderFrameFallback();\n }, frameLoadTimeoutMs);\n }\n\n function shouldSelfNavigateToApp() {\n const mode = typeof toolInput.embedMode === \"string\"\n ? toolInput.embedMode\n : typeof toolInput.renderMode === \"string\"\n ? toolInput.renderMode\n : \"\";\n if (mode === \"iframe\" || mode === \"nested\") return false;\n if (toolInput.nested === true || toolInput.frame === \"iframe\") return false;\n return true;\n }\n\n function shouldTransplantAppDocument() {\n const mode = typeof toolInput.embedMode === \"string\"\n ? toolInput.embedMode\n : typeof toolInput.renderMode === \"string\"\n ? toolInput.renderMode\n : \"\";\n return (\n mode === \"transplant\" ||\n toolInput.frame === \"transplant\" ||\n isClaudeMcpContentHost()\n );\n }\n\n function isClaudeMcpContentHost() {\n try {\n return /(^|\\\\.)claudemcpcontent\\\\.com$/i.test(window.location.hostname || \"\");\n } catch {\n return false;\n }\n }\n\n function isChatGptSandboxHost() {\n try {\n const host = window.location.hostname || \"\";\n const appParam = new URL(window.location.href).searchParams.get(\"app\");\n return /(^|\\\\.)oaiusercontent\\\\.com$/i.test(host) || appParam === \"chatgpt\";\n } catch {\n return false;\n }\n }\n\n function shouldRenderControlledAppFrame() {\n return !!openAiBridge || isChatGptSandboxHost();\n }\n\n function navigateToAppFrame(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n lastFrameSrc = src;\n setMessage(\"Opening app\");\n try {\n window.location.replace(src);\n } catch (err) {\n console.warn(\"[agent-native] MCP app self-navigation failed\", err);\n renderFrameFallback();\n }\n }\n\n async function updateHostModelContext(data) {\n const params = {};\n if (Array.isArray(data && data.content)) params.content = data.content;\n if (data && data.structuredContent && typeof data.structuredContent === \"object\") {\n params.structuredContent = data.structuredContent;\n }\n if (openAiBridge && typeof openAiBridge.setWidgetState === \"function\") {\n openAiBridge.setWidgetState({\n ...objectValue(openAiBridge.widgetState),\n agentNativeModelContext: params\n });\n return { ok: true };\n }\n if (!app || typeof app.updateModelContext !== \"function\") return { ok: false };\n await app.updateModelContext(params);\n return { ok: true };\n }\n\n async function openHostLink(data) {\n const url = typeof (data && data.url) === \"string\" ? data.url : \"\";\n if (!url) return { isError: true };\n if (openAiBridge && typeof openAiBridge.openExternal === \"function\") {\n return await openAiBridge.openExternal({ href: url, redirectUrl: false });\n }\n if (app && typeof app.openLink === \"function\") {\n return await app.openLink({ url });\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n return { ok: true };\n }\n\n function notifyHostHeight() {\n const height = applyIntrinsicHeight(visibleIntrinsicHeight());\n if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== \"function\") {\n if (app && typeof app.sendSizeChanged === \"function\") {\n try {\n app.sendSizeChanged({ height });\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected size update\", err);\n }\n }\n return;\n }\n try {\n openAiBridge.notifyIntrinsicHeight({ height });\n } catch (err) {\n console.warn(\"[agent-native] ChatGPT rejected intrinsic height update\", err);\n }\n }\n\n function respondToAppFrame(requestId, work) {\n if (!requestId) return;\n Promise.resolve(work)\n .then((result) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: { requestId, ok: true, result }\n });\n })\n .catch((err) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: {\n requestId,\n ok: false,\n error: err && err.message ? err.message : String(err)\n }\n });\n });\n }\n\n async function sendHostChat(chat) {\n if (!chat || chat.submit === false) return;\n const message = typeof chat.message === \"string\" ? chat.message : \"\";\n if (!message.trim()) return;\n const context = typeof chat.context === \"string\" ? chat.context : \"\";\n if (context.trim()) {\n try {\n if (openAiBridge && typeof openAiBridge.setWidgetState === \"function\") {\n openAiBridge.setWidgetState({\n ...objectValue(openAiBridge.widgetState),\n agentNativeChatContext: context\n });\n } else if (app && typeof app.updateModelContext === \"function\") {\n await app.updateModelContext({\n content: [{ type: \"text\", text: context }]\n });\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected model context update\", err);\n }\n }\n try {\n if (openAiBridge && typeof openAiBridge.sendFollowUpMessage === \"function\") {\n await openAiBridge.sendFollowUpMessage({\n prompt: context.trim() ? context.trim() + \"\\\\n\\\\n\" + message : message,\n scrollToBottom: true\n });\n return;\n }\n if (!app || typeof app.sendMessage !== \"function\") return;\n const result = await app.sendMessage({\n role: \"user\",\n content: [{ type: \"text\", text: message }]\n });\n if (result && result.isError) {\n console.warn(\"[agent-native] MCP host rejected chat message\", result);\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host chat bridge failed\", err);\n }\n }\n\n window.addEventListener(\"message\", (event) => {\n if (!appFrame || event.source !== appFrame.contentWindow) return;\n if (!event.data) return;\n const data = event.data.data || {};\n if (event.data.type === \"agentNative.embeddedAppReady\") {\n appFrameReady = true;\n clearFrameLoadTimer();\n clearFrameReadyTimer();\n return;\n }\n if (event.data.type === \"agentNative.submitChat\") {\n void sendHostChat(data);\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.updateModelContext\") {\n respondToAppFrame(data.requestId, updateHostModelContext(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.openLink\") {\n respondToAppFrame(data.requestId, openHostLink(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.requestDisplayMode\") {\n respondToAppFrame(data.requestId, requestHostDisplayMode(data.mode));\n }\n });\n\n function notifyHostHeightSoon() {\n requestAnimationFrame(() => notifyHostHeight());\n }\n\n function notifyHostHeightRepeatedly() {\n notifyHostHeight();\n [0, 250, 1000, 2500].forEach((delay) => {\n setTimeout(() => notifyHostHeight(), delay);\n });\n }\n\n window.addEventListener(\"resize\", notifyHostHeightSoon, { passive: true });\n if (window.visualViewport) {\n window.visualViewport.addEventListener(\"resize\", notifyHostHeightSoon, { passive: true });\n }\n\n async function launchEmbed() {\n const launchUrl = openStartUrl || openUrl;\n if (!launchUrl) {\n setMessage(\"Open link was not available.\");\n return;\n }\n if (!wantsEmbed()) {\n setMessage(\"Ready to open.\");\n return;\n }\n if (startedFor === launchUrl) return;\n startedFor = launchUrl;\n setMessage(\"Loading app\");\n try {\n const selfNavigate = shouldSelfNavigateToApp();\n const embedUrl = withChatBridgeParam(launchUrl);\n if (selfNavigate && isEmbedStartUrl(embedUrl)) {\n if (isClaudeMcpContentHost() && shouldTransplantAppDocument()) {\n await transplantAppDocument(embedUrl);\n } else if (shouldRenderControlledAppFrame()) {\n renderFrame(embedUrl);\n } else {\n navigateToAppFrame(embedUrl);\n }\n return;\n }\n if (!selfNavigate && isEmbedStartUrl(embedUrl)) {\n renderFrame(embedUrl);\n return;\n }\n const result = await callEmbedSessionTool(embedSessionArgsFor(embedUrl));\n const data = parseToolResult(result);\n if (typeof data.startUrl !== \"string\" || !data.startUrl) {\n startedFor = \"\";\n setMessage(data.error || \"This app can be opened, but not embedded from this MCP server.\");\n return;\n }\n const startUrl = withChatBridgeParam(data.startUrl);\n if (selfNavigate) {\n if (isClaudeMcpContentHost() && shouldTransplantAppDocument()) {\n await transplantAppDocument(startUrl);\n } else if (shouldRenderControlledAppFrame()) {\n renderFrame(startUrl);\n } else {\n navigateToAppFrame(startUrl);\n }\n } else {\n renderFrame(startUrl);\n }\n } catch (err) {\n startedFor = \"\";\n setMessage(err && err.message ? err.message : \"Could not launch embedded app.\");\n }\n }\n\n async function callEmbedSessionTool(args) {\n if (openAiBridge && typeof openAiBridge.callTool === \"function\") {\n return await openAiBridge.callTool(startTool, args);\n }\n if (!app || typeof app.callServerTool !== \"function\") {\n throw new Error(\"Host tool calls are not available.\");\n }\n return await app.callServerTool({ name: startTool, arguments: args });\n }\n\n function updateHostOpenInAppUrl() {\n if (!openAiBridge || !openUrl || typeof openAiBridge.setOpenInAppUrl !== \"function\") {\n return;\n }\n try {\n openAiBridge.setOpenInAppUrl({ href: openUrl });\n } catch (err) {\n console.warn(\"[agent-native] ChatGPT rejected open-in-app URL\", err);\n }\n }\n\n function updateOpenButton() {\n const buttonUrl = openUrl || openStartUrl;\n openButton.disabled = !buttonUrl;\n openButton.onclick = () => {\n if (buttonUrl) void openHostLink({ url: buttonUrl });\n };\n updateHostOpenInAppUrl();\n }\n\n function updateTitle(data) {\n const label = data.label || data.app || data.view || body.dataset.appTitle || \"App\";\n titleEl.textContent = String(label);\n }\n\n function readOpenAiBridge() {\n return window.openai && typeof window.openai === \"object\"\n ? window.openai\n : null;\n }\n\n function openAiToolResultParams(bridge) {\n const params = {};\n if (bridge && bridge.toolOutput !== undefined) {\n if (bridge.toolOutput && typeof bridge.toolOutput === \"object\") {\n params.structuredContent = bridge.toolOutput;\n } else {\n params.content = [{ type: \"text\", text: String(bridge.toolOutput) }];\n }\n }\n if (bridge && bridge.toolResponseMetadata && typeof bridge.toolResponseMetadata === \"object\") {\n params._meta = bridge.toolResponseMetadata;\n }\n return params;\n }\n\n function syncOpenAiBridge(bridge) {\n if (!bridge) return false;\n openAiBridge = bridge;\n toolInput = objectValue(bridge.toolInput);\n const params = openAiToolResultParams(bridge);\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n openStartUrl = embedStartUrlFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n if (openUrl || openStartUrl) {\n void launchEmbed();\n } else if (!appFrame) {\n setMessage(\"Waiting for app result\");\n }\n return true;\n }\n\n function waitForOpenAiBridge() {\n const existing = readOpenAiBridge();\n if (existing) return Promise.resolve(existing);\n return new Promise((resolve) => {\n let settled = false;\n const finish = (bridge) => {\n if (settled) return;\n settled = true;\n window.removeEventListener(\"openai:set_globals\", onGlobals);\n clearTimeout(timer);\n resolve(bridge || readOpenAiBridge());\n };\n const onGlobals = () => finish(readOpenAiBridge());\n const timer = setTimeout(() => finish(null), 200);\n window.addEventListener(\"openai:set_globals\", onGlobals, { passive: true });\n });\n }\n\n window.addEventListener(\"openai:set_globals\", () => {\n const bridge = readOpenAiBridge();\n if (bridge && (!appFrame || openAiBridge)) syncOpenAiBridge(bridge);\n }, { passive: true });\n\n async function startMcpAppsBridge() {\n const { App } = await import(\"${MCP_APP_IMPORT}\");\n app = new App(\n { name: \"Agent Native Embed\", version: \"1.0.0\" },\n {},\n { autoResize: false }\n );\n app.ontoolinput = (params) => {\n toolInput = params.arguments || {};\n };\n app.ontoolresult = (params) => {\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n openStartUrl = embedStartUrlFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n void launchEmbed();\n };\n app.onhostcontextchanged = () => {\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n };\n await app.connect();\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n }\n\n const initialOpenAiBridge = await waitForOpenAiBridge();\n if (!syncOpenAiBridge(initialOpenAiBridge)) {\n await startMcpAppsBridge();\n }\n </script>\n</body>\n</html>`,\n csp: {\n connectDomains: [\"https://esm.sh\", MCP_APP_REQUEST_ORIGIN_CSP_SOURCE],\n resourceDomains: [\n \"https://esm.sh\",\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n baseUriDomains: [MCP_APP_REQUEST_ORIGIN_CSP_SOURCE],\n frameDomains,\n },\n prefersBorder: false,\n };\n}\n"]}
|
package/docs/content/actions.md
CHANGED
|
@@ -199,6 +199,10 @@ export default defineAction({
|
|
|
199
199
|
This advertises the MCP Apps extension (`io.modelcontextprotocol/ui`), exposes the HTML via MCP resources/templates, and includes standard MCP Apps plus ChatGPT Apps SDK widget metadata for compatible hosts. Keep `link` as the fallback for CLI and non-UI MCP clients; see [External Agents](/docs/external-agents#mcp-apps).
|
|
200
200
|
|
|
201
201
|
The helper launches the action's `link` target through `/_agent-native/embed/start` with a short-lived browser session, so routes such as full dashboards, filtered inboxes, drafts, and extension pages can reuse the app's React components directly.
|
|
202
|
+
Same-app `open_app({ embed: true })` mints that embed-start ticket during the
|
|
203
|
+
original tool call, and custom actions can return `embedStartUrl` for the same
|
|
204
|
+
fast path; otherwise the resource falls back to the app-only
|
|
205
|
+
`create_embed_session` helper.
|
|
202
206
|
Standard hosts navigate the MCP App frame directly to that signed route.
|
|
203
207
|
Claude web uses a single-frame transplant path that hydrates the signed app
|
|
204
208
|
HTML inside Claude's MCP App iframe because Claude does not reliably allow
|
|
@@ -230,6 +230,13 @@ same signed app route and render the normal route and React components. Design
|
|
|
230
230
|
embedded routes so a reload with the same signed URL reconstructs the same
|
|
231
231
|
view.
|
|
232
232
|
|
|
233
|
+
For same-app `open_app({ embed: true })`, the framework mints the embed-start
|
|
234
|
+
ticket during the original tool call and returns `embedStartUrl` in the hidden
|
|
235
|
+
structured payload. Custom actions can do the same. When no `embedStartUrl` is
|
|
236
|
+
present, the resource falls back to the app-only `create_embed_session` helper.
|
|
237
|
+
This keeps production hosts that restrict iframe-initiated tool calls on the
|
|
238
|
+
direct route.
|
|
239
|
+
|
|
233
240
|
ChatGPT gets a dedicated compatibility path through `window.openai`: the launch
|
|
234
241
|
document reads `toolInput`, `toolOutput`, and `toolResponseMetadata` directly,
|
|
235
242
|
then calls `create_embed_session` via `window.openai.callTool(...)`. Standard
|
|
@@ -88,6 +88,10 @@ host APIs and bounded height control.
|
|
|
88
88
|
For normal action authoring, use `embedRoute()` when the action's
|
|
89
89
|
`link` and `mcpApp` should come from the same pure route builder. The route
|
|
90
90
|
itself should derive state from the URL and normal app data fetching.
|
|
91
|
+
Same-app `open_app({ embed: true })` returns a server-minted `embedStartUrl`
|
|
92
|
+
so the resource can launch without a second iframe-originated tool call;
|
|
93
|
+
custom actions can return the same field when they already know the target
|
|
94
|
+
route.
|
|
91
95
|
|
|
92
96
|
The outer MCP resource reports a bounded inline height to the host and the app
|
|
93
97
|
route scrolls internally. Do not rely on host auto-resize measuring the full
|
package/package.json
CHANGED