@agent-native/core 0.22.17 → 0.22.19
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/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +19 -6
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/embed-auth.d.ts +5 -0
- package/dist/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +100 -13
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +1 -0
- package/dist/client/frame.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts +5 -0
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +267 -2
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/client/theme.js +1 -1
- package/dist/client/theme.js.map +1 -1
- package/dist/client/vite-dev-recovery-script.d.ts.map +1 -1
- package/dist/client/vite-dev-recovery-script.js +9 -0
- package/dist/client/vite-dev-recovery-script.js.map +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +63 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +332 -34
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/server/open-route.d.ts.map +1 -1
- package/dist/server/open-route.js +7 -2
- package/dist/server/open-route.js.map +1 -1
- package/docs/content/client.md +6 -3
- package/docs/content/external-agents.md +34 -16
- package/docs/content/mcp-protocol.md +17 -7
- package/package.json +1 -1
package/dist/mcp/embed-app.js
CHANGED
|
@@ -40,6 +40,11 @@ export function embedApp(options = {}) {
|
|
|
40
40
|
.stage { position: relative; min-height: ${viewportHeight}px; }
|
|
41
41
|
iframe { display: block; width: 100%; height: ${viewportHeight}px; border: 0; background: Canvas; }
|
|
42
42
|
.message { display: grid; place-items: center; min-height: ${viewportHeight}px; padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }
|
|
43
|
+
.fallback { display: grid; align-content: center; justify-items: center; gap: 12px; min-height: ${viewportHeight}px; padding: 24px; background: Canvas; color: CanvasText; text-align: center; }
|
|
44
|
+
.fallback-title { max-width: 440px; font-size: 14px; font-weight: 700; }
|
|
45
|
+
.fallback-copy { max-width: 520px; color: color-mix(in srgb, CanvasText 64%, Canvas); font-size: 13px; line-height: 1.45; }
|
|
46
|
+
.fallback-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
|
|
47
|
+
.fallback-url { max-width: min(560px, 100%); overflow-wrap: anywhere; color: color-mix(in srgb, CanvasText 76%, Canvas); font-size: 12px; }
|
|
43
48
|
</style>
|
|
44
49
|
</head>
|
|
45
50
|
<body
|
|
@@ -62,9 +67,6 @@ export function embedApp(options = {}) {
|
|
|
62
67
|
</section>
|
|
63
68
|
</main>
|
|
64
69
|
<script type="module">
|
|
65
|
-
import { App } from "${MCP_APP_IMPORT}";
|
|
66
|
-
|
|
67
|
-
const app = new App({ name: "Agent Native Embed", version: "1.0.0" }, {});
|
|
68
70
|
const body = document.body;
|
|
69
71
|
const stage = document.querySelector("[data-stage]");
|
|
70
72
|
const titleEl = document.querySelector("[data-title-label]");
|
|
@@ -73,10 +75,17 @@ export function embedApp(options = {}) {
|
|
|
73
75
|
const startTool = body.dataset.startTool || "create_embed_session";
|
|
74
76
|
const embedByDefault = body.dataset.embedDefault !== "0";
|
|
75
77
|
const chatBridgeParam = ${JSON.stringify(MCP_APP_CHAT_BRIDGE_QUERY_PARAM)};
|
|
78
|
+
const intrinsicHeight = ${height};
|
|
79
|
+
let app = null;
|
|
80
|
+
let openAiBridge = null;
|
|
76
81
|
let toolInput = {};
|
|
77
82
|
let openUrl = "";
|
|
78
83
|
let startedFor = "";
|
|
79
84
|
let appFrame = null;
|
|
85
|
+
let appFrameReady = false;
|
|
86
|
+
let appFrameReadyTimer = null;
|
|
87
|
+
let appFrameLoadTimer = null;
|
|
88
|
+
let lastFrameSrc = "";
|
|
80
89
|
|
|
81
90
|
function esc(value) {
|
|
82
91
|
return String(value ?? "")
|
|
@@ -92,8 +101,20 @@ export function embedApp(options = {}) {
|
|
|
92
101
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
function objectValue(value) {
|
|
105
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
106
|
+
? value
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
function parseToolResult(params) {
|
|
96
111
|
if (!params) return {};
|
|
112
|
+
if (params.result && typeof params.result === "object") {
|
|
113
|
+
return parseToolResult(params.result);
|
|
114
|
+
}
|
|
115
|
+
if (params.toolResult && typeof params.toolResult === "object") {
|
|
116
|
+
return parseToolResult(params.toolResult);
|
|
117
|
+
}
|
|
97
118
|
if (params.structuredContent && typeof params.structuredContent === "object") {
|
|
98
119
|
return params.structuredContent;
|
|
99
120
|
}
|
|
@@ -103,17 +124,34 @@ export function embedApp(options = {}) {
|
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
function openLinkFrom(params, data) {
|
|
106
|
-
const
|
|
107
|
-
|
|
127
|
+
const openLink = params && params._meta && params._meta["agent-native/openLink"];
|
|
128
|
+
const metaUrl = openLink && typeof openLink === "object" && typeof openLink.webUrl === "string"
|
|
129
|
+
? openLink.webUrl
|
|
108
130
|
: "";
|
|
109
131
|
return metaUrl || data.url || data.deepLink || data.openUrl || "";
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
function hostState() {
|
|
135
|
+
if (openAiBridge) {
|
|
136
|
+
return {
|
|
137
|
+
context: {
|
|
138
|
+
displayMode: openAiBridge.displayMode,
|
|
139
|
+
availableDisplayModes: typeof openAiBridge.requestDisplayMode === "function"
|
|
140
|
+
? ["inline", "fullscreen", "pip"]
|
|
141
|
+
: [],
|
|
142
|
+
maxHeight: openAiBridge.maxHeight,
|
|
143
|
+
locale: openAiBridge.locale,
|
|
144
|
+
theme: openAiBridge.theme,
|
|
145
|
+
view: openAiBridge.view
|
|
146
|
+
},
|
|
147
|
+
capabilities: { openai: true },
|
|
148
|
+
version: openAiBridge.userAgent
|
|
149
|
+
};
|
|
150
|
+
}
|
|
113
151
|
return {
|
|
114
|
-
context: app.getHostContext ? app.getHostContext() : undefined,
|
|
115
|
-
capabilities: app.getHostCapabilities ? app.getHostCapabilities() : undefined,
|
|
116
|
-
version: app.getHostVersion ? app.getHostVersion() : undefined
|
|
152
|
+
context: app && app.getHostContext ? app.getHostContext() : undefined,
|
|
153
|
+
capabilities: app && app.getHostCapabilities ? app.getHostCapabilities() : undefined,
|
|
154
|
+
version: app && app.getHostVersion ? app.getHostVersion() : undefined
|
|
117
155
|
};
|
|
118
156
|
}
|
|
119
157
|
|
|
@@ -157,12 +195,23 @@ export function embedApp(options = {}) {
|
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
function supportedDisplayMode(mode) {
|
|
198
|
+
if (openAiBridge && typeof openAiBridge.requestDisplayMode === "function") {
|
|
199
|
+
return mode === "inline" || mode === "fullscreen" || mode === "pip";
|
|
200
|
+
}
|
|
160
201
|
const modes = hostState().context && hostState().context.availableDisplayModes;
|
|
161
202
|
return Array.isArray(modes) && modes.includes(mode);
|
|
162
203
|
}
|
|
163
204
|
|
|
164
205
|
async function requestHostDisplayMode(mode) {
|
|
165
|
-
|
|
206
|
+
let result;
|
|
207
|
+
if (openAiBridge && typeof openAiBridge.requestDisplayMode === "function") {
|
|
208
|
+
result = await openAiBridge.requestDisplayMode({ mode });
|
|
209
|
+
} else {
|
|
210
|
+
if (!app || typeof app.requestDisplayMode !== "function") {
|
|
211
|
+
throw new Error("Display mode changes are not available in this host.");
|
|
212
|
+
}
|
|
213
|
+
result = await app.requestDisplayMode({ mode });
|
|
214
|
+
}
|
|
166
215
|
updateDisplayButton();
|
|
167
216
|
sendHostContext();
|
|
168
217
|
return result;
|
|
@@ -187,14 +236,118 @@ export function embedApp(options = {}) {
|
|
|
187
236
|
stage.innerHTML = '<div class="message">' + esc(message) + '</div>';
|
|
188
237
|
}
|
|
189
238
|
|
|
239
|
+
function clearFrameReadyTimer() {
|
|
240
|
+
if (!appFrameReadyTimer) return;
|
|
241
|
+
clearTimeout(appFrameReadyTimer);
|
|
242
|
+
appFrameReadyTimer = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function clearFrameLoadTimer() {
|
|
246
|
+
if (!appFrameLoadTimer) return;
|
|
247
|
+
clearTimeout(appFrameLoadTimer);
|
|
248
|
+
appFrameLoadTimer = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function startFrameReadyTimer(frame) {
|
|
252
|
+
clearFrameReadyTimer();
|
|
253
|
+
appFrameReadyTimer = setTimeout(() => {
|
|
254
|
+
if (!appFrameReady && appFrame === frame) renderFrameFallback();
|
|
255
|
+
}, 7000);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderFrameFallback() {
|
|
259
|
+
clearFrameReadyTimer();
|
|
260
|
+
clearFrameLoadTimer();
|
|
261
|
+
appFrame = null;
|
|
262
|
+
stage.innerHTML =
|
|
263
|
+
'<div class="fallback">' +
|
|
264
|
+
'<div class="fallback-title">Open this app in its own tab</div>' +
|
|
265
|
+
'<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>' +
|
|
266
|
+
'<div class="fallback-actions">' +
|
|
267
|
+
'<button type="button" data-fallback-open>Open app</button>' +
|
|
268
|
+
'<button type="button" data-fallback-retry>Try inline again</button>' +
|
|
269
|
+
'</div>' +
|
|
270
|
+
(openUrl ? '<a class="fallback-url" href="' + esc(openUrl) + '" target="_blank" rel="noreferrer">' + esc(openUrl) + '</a>' : '') +
|
|
271
|
+
'</div>';
|
|
272
|
+
const fallbackOpen = stage.querySelector("[data-fallback-open]");
|
|
273
|
+
const fallbackRetry = stage.querySelector("[data-fallback-retry]");
|
|
274
|
+
if (fallbackOpen) {
|
|
275
|
+
fallbackOpen.disabled = !openUrl;
|
|
276
|
+
fallbackOpen.onclick = () => {
|
|
277
|
+
if (openUrl) void openFallbackExternal();
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (fallbackRetry) {
|
|
281
|
+
fallbackRetry.disabled = !lastFrameSrc;
|
|
282
|
+
fallbackRetry.onclick = () => {
|
|
283
|
+
if (lastFrameSrc) renderFrame(lastFrameSrc);
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function openFallbackExternal() {
|
|
289
|
+
let url = withChatBridgeParam(openUrl);
|
|
290
|
+
try {
|
|
291
|
+
const result = await callEmbedSessionTool({
|
|
292
|
+
url,
|
|
293
|
+
chrome: typeof toolInput.chrome === "string" ? toolInput.chrome : "full"
|
|
294
|
+
});
|
|
295
|
+
const data = parseToolResult(result);
|
|
296
|
+
if (typeof data.startUrl === "string" && data.startUrl) {
|
|
297
|
+
url = data.startUrl;
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.warn("[agent-native] MCP fallback could not mint a fresh app session", err);
|
|
301
|
+
}
|
|
302
|
+
await openHostLink({ url });
|
|
303
|
+
}
|
|
304
|
+
|
|
190
305
|
function renderFrame(src) {
|
|
306
|
+
clearFrameReadyTimer();
|
|
307
|
+
clearFrameLoadTimer();
|
|
191
308
|
const frame = document.createElement("iframe");
|
|
192
309
|
frame.title = body.dataset.iframeTitle || "Agent Native app";
|
|
193
310
|
frame.src = src;
|
|
194
311
|
frame.allow = "clipboard-read; clipboard-write";
|
|
195
312
|
appFrame = frame;
|
|
196
|
-
|
|
313
|
+
appFrameReady = false;
|
|
314
|
+
lastFrameSrc = src;
|
|
315
|
+
frame.addEventListener("load", () => {
|
|
316
|
+
if (appFrame !== frame) return;
|
|
317
|
+
clearFrameLoadTimer();
|
|
318
|
+
sendFrameReadyMessages(frame);
|
|
319
|
+
startFrameReadyTimer(frame);
|
|
320
|
+
});
|
|
197
321
|
stage.replaceChildren(frame);
|
|
322
|
+
notifyHostHeight();
|
|
323
|
+
appFrameLoadTimer = setTimeout(() => {
|
|
324
|
+
if (!appFrameReady && appFrame === frame) renderFrameFallback();
|
|
325
|
+
}, 30000);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function shouldSelfNavigateToApp() {
|
|
329
|
+
const mode = typeof toolInput.embedMode === "string"
|
|
330
|
+
? toolInput.embedMode
|
|
331
|
+
: typeof toolInput.renderMode === "string"
|
|
332
|
+
? toolInput.renderMode
|
|
333
|
+
: "";
|
|
334
|
+
if (mode === "iframe" || mode === "nested") return false;
|
|
335
|
+
if (toolInput.nested === true || toolInput.frame === "iframe") return false;
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function navigateToAppFrame(src) {
|
|
340
|
+
clearFrameReadyTimer();
|
|
341
|
+
clearFrameLoadTimer();
|
|
342
|
+
appFrame = null;
|
|
343
|
+
lastFrameSrc = src;
|
|
344
|
+
setMessage("Opening app");
|
|
345
|
+
try {
|
|
346
|
+
window.location.replace(src);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.warn("[agent-native] MCP app self-navigation failed", err);
|
|
349
|
+
renderFrameFallback();
|
|
350
|
+
}
|
|
198
351
|
}
|
|
199
352
|
|
|
200
353
|
async function updateHostModelContext(data) {
|
|
@@ -203,13 +356,40 @@ export function embedApp(options = {}) {
|
|
|
203
356
|
if (data && data.structuredContent && typeof data.structuredContent === "object") {
|
|
204
357
|
params.structuredContent = data.structuredContent;
|
|
205
358
|
}
|
|
359
|
+
if (openAiBridge && typeof openAiBridge.setWidgetState === "function") {
|
|
360
|
+
openAiBridge.setWidgetState({
|
|
361
|
+
...objectValue(openAiBridge.widgetState),
|
|
362
|
+
agentNativeModelContext: params
|
|
363
|
+
});
|
|
364
|
+
return { ok: true };
|
|
365
|
+
}
|
|
366
|
+
if (!app || typeof app.updateModelContext !== "function") return { ok: false };
|
|
206
367
|
await app.updateModelContext(params);
|
|
368
|
+
return { ok: true };
|
|
207
369
|
}
|
|
208
370
|
|
|
209
371
|
async function openHostLink(data) {
|
|
210
372
|
const url = typeof (data && data.url) === "string" ? data.url : "";
|
|
211
373
|
if (!url) return { isError: true };
|
|
212
|
-
|
|
374
|
+
if (openAiBridge && typeof openAiBridge.openExternal === "function") {
|
|
375
|
+
return await openAiBridge.openExternal({ href: url, redirectUrl: false });
|
|
376
|
+
}
|
|
377
|
+
if (app && typeof app.openLink === "function") {
|
|
378
|
+
return await app.openLink({ url });
|
|
379
|
+
}
|
|
380
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
381
|
+
return { ok: true };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function notifyHostHeight() {
|
|
385
|
+
if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== "function") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
openAiBridge.notifyIntrinsicHeight({ height: intrinsicHeight });
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.warn("[agent-native] ChatGPT rejected intrinsic height update", err);
|
|
392
|
+
}
|
|
213
393
|
}
|
|
214
394
|
|
|
215
395
|
function respondToAppFrame(requestId, work) {
|
|
@@ -240,14 +420,29 @@ export function embedApp(options = {}) {
|
|
|
240
420
|
const context = typeof chat.context === "string" ? chat.context : "";
|
|
241
421
|
if (context.trim()) {
|
|
242
422
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
423
|
+
if (openAiBridge && typeof openAiBridge.setWidgetState === "function") {
|
|
424
|
+
openAiBridge.setWidgetState({
|
|
425
|
+
...objectValue(openAiBridge.widgetState),
|
|
426
|
+
agentNativeChatContext: context
|
|
427
|
+
});
|
|
428
|
+
} else if (app && typeof app.updateModelContext === "function") {
|
|
429
|
+
await app.updateModelContext({
|
|
430
|
+
content: [{ type: "text", text: context }]
|
|
431
|
+
});
|
|
432
|
+
}
|
|
246
433
|
} catch (err) {
|
|
247
434
|
console.warn("[agent-native] MCP host rejected model context update", err);
|
|
248
435
|
}
|
|
249
436
|
}
|
|
250
437
|
try {
|
|
438
|
+
if (openAiBridge && typeof openAiBridge.sendFollowUpMessage === "function") {
|
|
439
|
+
await openAiBridge.sendFollowUpMessage({
|
|
440
|
+
prompt: context.trim() ? context.trim() + "\\n\\n" + message : message,
|
|
441
|
+
scrollToBottom: true
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!app || typeof app.sendMessage !== "function") return;
|
|
251
446
|
const result = await app.sendMessage({
|
|
252
447
|
role: "user",
|
|
253
448
|
content: [{ type: "text", text: message }]
|
|
@@ -264,6 +459,12 @@ export function embedApp(options = {}) {
|
|
|
264
459
|
if (!appFrame || event.source !== appFrame.contentWindow) return;
|
|
265
460
|
if (!event.data) return;
|
|
266
461
|
const data = event.data.data || {};
|
|
462
|
+
if (event.data.type === "agentNative.embeddedAppReady") {
|
|
463
|
+
appFrameReady = true;
|
|
464
|
+
clearFrameLoadTimer();
|
|
465
|
+
clearFrameReadyTimer();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
267
468
|
if (event.data.type === "agentNative.submitChat") {
|
|
268
469
|
void sendHostChat(data);
|
|
269
470
|
return;
|
|
@@ -294,32 +495,56 @@ export function embedApp(options = {}) {
|
|
|
294
495
|
startedFor = openUrl;
|
|
295
496
|
setMessage("Loading app");
|
|
296
497
|
try {
|
|
498
|
+
const selfNavigate = shouldSelfNavigateToApp();
|
|
297
499
|
const embedUrl = withChatBridgeParam(openUrl);
|
|
298
|
-
const result = await
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
url: embedUrl,
|
|
302
|
-
chrome: typeof toolInput.chrome === "string" ? toolInput.chrome : "full"
|
|
303
|
-
}
|
|
500
|
+
const result = await callEmbedSessionTool({
|
|
501
|
+
url: embedUrl,
|
|
502
|
+
chrome: typeof toolInput.chrome === "string" ? toolInput.chrome : "full"
|
|
304
503
|
});
|
|
305
504
|
const data = parseToolResult(result);
|
|
306
|
-
if (!data.startUrl) {
|
|
505
|
+
if (typeof data.startUrl !== "string" || !data.startUrl) {
|
|
307
506
|
startedFor = "";
|
|
308
507
|
setMessage(data.error || "This app can be opened, but not embedded from this MCP server.");
|
|
309
508
|
return;
|
|
310
509
|
}
|
|
311
|
-
|
|
510
|
+
if (selfNavigate) {
|
|
511
|
+
navigateToAppFrame(data.startUrl);
|
|
512
|
+
} else {
|
|
513
|
+
renderFrame(data.startUrl);
|
|
514
|
+
}
|
|
312
515
|
} catch (err) {
|
|
313
516
|
startedFor = "";
|
|
314
517
|
setMessage(err && err.message ? err.message : "Could not launch embedded app.");
|
|
315
518
|
}
|
|
316
519
|
}
|
|
317
520
|
|
|
521
|
+
async function callEmbedSessionTool(args) {
|
|
522
|
+
if (openAiBridge && typeof openAiBridge.callTool === "function") {
|
|
523
|
+
return await openAiBridge.callTool(startTool, args);
|
|
524
|
+
}
|
|
525
|
+
if (!app || typeof app.callServerTool !== "function") {
|
|
526
|
+
throw new Error("Host tool calls are not available.");
|
|
527
|
+
}
|
|
528
|
+
return await app.callServerTool({ name: startTool, arguments: args });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function updateHostOpenInAppUrl() {
|
|
532
|
+
if (!openAiBridge || !openUrl || typeof openAiBridge.setOpenInAppUrl !== "function") {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
openAiBridge.setOpenInAppUrl({ href: openUrl });
|
|
537
|
+
} catch (err) {
|
|
538
|
+
console.warn("[agent-native] ChatGPT rejected open-in-app URL", err);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
318
542
|
function updateOpenButton() {
|
|
319
543
|
openButton.disabled = !openUrl;
|
|
320
544
|
openButton.onclick = () => {
|
|
321
|
-
if (openUrl) void
|
|
545
|
+
if (openUrl) void openHostLink({ url: openUrl });
|
|
322
546
|
};
|
|
547
|
+
updateHostOpenInAppUrl();
|
|
323
548
|
}
|
|
324
549
|
|
|
325
550
|
function updateTitle(data) {
|
|
@@ -327,23 +552,96 @@ export function embedApp(options = {}) {
|
|
|
327
552
|
titleEl.textContent = String(label);
|
|
328
553
|
}
|
|
329
554
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
555
|
+
function readOpenAiBridge() {
|
|
556
|
+
return window.openai && typeof window.openai === "object"
|
|
557
|
+
? window.openai
|
|
558
|
+
: null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function openAiToolResultParams(bridge) {
|
|
562
|
+
const params = {};
|
|
563
|
+
if (bridge && bridge.toolOutput !== undefined) {
|
|
564
|
+
if (bridge.toolOutput && typeof bridge.toolOutput === "object") {
|
|
565
|
+
params.structuredContent = bridge.toolOutput;
|
|
566
|
+
} else {
|
|
567
|
+
params.content = [{ type: "text", text: String(bridge.toolOutput) }];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (bridge && bridge.toolResponseMetadata && typeof bridge.toolResponseMetadata === "object") {
|
|
571
|
+
params._meta = bridge.toolResponseMetadata;
|
|
572
|
+
}
|
|
573
|
+
return params;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function syncOpenAiBridge(bridge) {
|
|
577
|
+
if (!bridge) return false;
|
|
578
|
+
openAiBridge = bridge;
|
|
579
|
+
toolInput = objectValue(bridge.toolInput);
|
|
580
|
+
const params = openAiToolResultParams(bridge);
|
|
334
581
|
const data = parseToolResult(params);
|
|
335
582
|
openUrl = openLinkFrom(params, data);
|
|
336
583
|
updateTitle(data);
|
|
337
584
|
updateOpenButton();
|
|
338
|
-
void launchEmbed();
|
|
339
|
-
};
|
|
340
|
-
app.onhostcontextchanged = () => {
|
|
341
585
|
updateDisplayButton();
|
|
586
|
+
notifyHostHeight();
|
|
342
587
|
sendHostContext();
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
588
|
+
if (openUrl) {
|
|
589
|
+
void launchEmbed();
|
|
590
|
+
} else if (!appFrame) {
|
|
591
|
+
setMessage("Waiting for app result");
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function waitForOpenAiBridge() {
|
|
597
|
+
const existing = readOpenAiBridge();
|
|
598
|
+
if (existing) return Promise.resolve(existing);
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
let settled = false;
|
|
601
|
+
const finish = (bridge) => {
|
|
602
|
+
if (settled) return;
|
|
603
|
+
settled = true;
|
|
604
|
+
window.removeEventListener("openai:set_globals", onGlobals);
|
|
605
|
+
clearTimeout(timer);
|
|
606
|
+
resolve(bridge || readOpenAiBridge());
|
|
607
|
+
};
|
|
608
|
+
const onGlobals = () => finish(readOpenAiBridge());
|
|
609
|
+
const timer = setTimeout(() => finish(null), 200);
|
|
610
|
+
window.addEventListener("openai:set_globals", onGlobals, { passive: true });
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
window.addEventListener("openai:set_globals", () => {
|
|
615
|
+
const bridge = readOpenAiBridge();
|
|
616
|
+
if (bridge && (!appFrame || openAiBridge)) syncOpenAiBridge(bridge);
|
|
617
|
+
}, { passive: true });
|
|
618
|
+
|
|
619
|
+
async function startMcpAppsBridge() {
|
|
620
|
+
const { App } = await import("${MCP_APP_IMPORT}");
|
|
621
|
+
app = new App({ name: "Agent Native Embed", version: "1.0.0" }, {});
|
|
622
|
+
app.ontoolinput = (params) => {
|
|
623
|
+
toolInput = params.arguments || {};
|
|
624
|
+
};
|
|
625
|
+
app.ontoolresult = (params) => {
|
|
626
|
+
const data = parseToolResult(params);
|
|
627
|
+
openUrl = openLinkFrom(params, data);
|
|
628
|
+
updateTitle(data);
|
|
629
|
+
updateOpenButton();
|
|
630
|
+
void launchEmbed();
|
|
631
|
+
};
|
|
632
|
+
app.onhostcontextchanged = () => {
|
|
633
|
+
updateDisplayButton();
|
|
634
|
+
sendHostContext();
|
|
635
|
+
};
|
|
636
|
+
await app.connect();
|
|
637
|
+
updateDisplayButton();
|
|
638
|
+
sendHostContext();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const initialOpenAiBridge = await waitForOpenAiBridge();
|
|
642
|
+
if (!syncOpenAiBridge(initialOpenAiBridge)) {
|
|
643
|
+
await startMcpAppsBridge();
|
|
644
|
+
}
|
|
347
645
|
</script>
|
|
348
646
|
</body>
|
|
349
647
|
</html>`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed-app.js","sourceRoot":"","sources":["../../src/mcp/embed-app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAC;AAE1E,MAAM,cAAc,GAClB,mEAAmE,CAAC;AAEtE,MAAM,CAAC,MAAM,iCAAiC,GAAG,gBAAgB,CAAC;AAClE,MAAM,6BAA6B,GAAG,EAAE,CAAC;AACzC,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AACnD,MAAM,CAAC,MAAM,4BAA4B,GACvC,+BAA+B,GAAG,6BAA6B,CAAC;AAalE,SAAS,IAAI,CAAC,KAAyB;IACrC,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,UAA2B,EAAE;IAE7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC;IAC1C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,aAAa,CAAC;IACrD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACtE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,KAAK,KAAK,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CACrB,GAAG,EACH,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,IAAI,4BAA4B,CAAC,CAC9D,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,GAAG,6BAA6B,CAAC;IAE9D,OAAO;QACL,KAAK;QACL,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,IAAI,EAAE,GAAG,EAAE,CAAC;;;;;;;;;oDASoC,MAAM;;;;;;+CAMX,cAAc;oDACT,cAAc;iEACD,cAAc;;;;oBAI3D,IAAI,CAAC,KAAK,CAAC;uBACR,IAAI,CAAC,WAAW,CAAC;qBACnB,IAAI,CAAC,SAAS,CAAC;qBACf,IAAI,CAAC,aAAa,CAAC;wBAChB,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;;;;4CAIN,IAAI,CAAC,KAAK,CAAC;;;mDAGJ,IAAI,CAAC,SAAS,CAAC;;;;;;;;2BAQvC,cAAc;;;;;;;;;;8BAUX,IAAI,CAAC,SAAS,CAAC,+BAA+B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkRrE;QACJ,GAAG,EAAE;YACH,cAAc,EAAE,CAAC,gBAAgB,CAAC;YAClC,eAAe,EAAE;gBACf,gBAAgB;gBAChB,iCAAiC;gBACjC,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;aAChC;YACD,YAAY,EAAE;gBACZ,iCAAiC;gBACjC,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;aAChC;SACF;QACD,aAAa,EAAE,KAAK;KACrB,CAAC;AACJ,CAAC","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_VIEWPORT_HEIGHT = 720;\nexport const DEFAULT_MCP_APP_SHELL_HEIGHT =\n DEFAULT_MCP_APP_VIEWPORT_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\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; }\n * { box-sizing: border-box; }\n body { margin: 0; }\n .shell { display: grid; gap: 8px; min-height: ${height}px; 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: ${viewportHeight}px; }\n iframe { display: block; width: 100%; height: ${viewportHeight}px; border: 0; background: Canvas; }\n .message { display: grid; place-items: center; min-height: ${viewportHeight}px; padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }\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 import { App } from \"${MCP_APP_IMPORT}\";\n\n const app = new App({ name: \"Agent Native Embed\", version: \"1.0.0\" }, {});\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 let toolInput = {};\n let openUrl = \"\";\n let startedFor = \"\";\n let appFrame = null;\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 parseToolResult(params) {\n if (!params) return {};\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 metaUrl = params && params._meta && params._meta[\"agent-native/openLink\"]\n ? params._meta[\"agent-native/openLink\"].webUrl\n : \"\";\n return metaUrl || data.url || data.deepLink || data.openUrl || \"\";\n }\n\n function hostState() {\n return {\n context: app.getHostContext ? app.getHostContext() : undefined,\n capabilities: app.getHostCapabilities ? app.getHostCapabilities() : undefined,\n version: 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 [0, 200, 500, 1500].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 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 const modes = hostState().context && hostState().context.availableDisplayModes;\n return Array.isArray(modes) && modes.includes(mode);\n }\n\n async function requestHostDisplayMode(mode) {\n const result = await app.requestDisplayMode({ mode });\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 renderFrame(src) {\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 frame.addEventListener(\"load\", () => sendFrameReadyMessages(frame));\n stage.replaceChildren(frame);\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 await app.updateModelContext(params);\n }\n\n async function openHostLink(data) {\n const url = typeof (data && data.url) === \"string\" ? data.url : \"\";\n if (!url) return { isError: true };\n return await app.openLink({ url });\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 await app.updateModelContext({\n content: [{ type: \"text\", text: context }]\n });\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected model context update\", err);\n }\n }\n try {\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.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 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 embedUrl = withChatBridgeParam(openUrl);\n const result = await app.callServerTool({\n name: startTool,\n arguments: {\n url: embedUrl,\n chrome: typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\"\n }\n });\n const data = parseToolResult(result);\n if (!data.startUrl) {\n startedFor = \"\";\n setMessage(data.error || \"This app can be opened, but not embedded from this MCP server.\");\n return;\n }\n renderFrame(data.startUrl);\n } catch (err) {\n startedFor = \"\";\n setMessage(err && err.message ? err.message : \"Could not launch embedded app.\");\n }\n }\n\n function updateOpenButton() {\n openButton.disabled = !openUrl;\n openButton.onclick = () => {\n if (openUrl) void app.openLink({ url: openUrl });\n };\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 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 sendHostContext();\n };\n await app.connect();\n updateDisplayButton();\n sendHostContext();\n </script>\n</body>\n</html>`,\n csp: {\n connectDomains: [\"https://esm.sh\"],\n resourceDomains: [\n \"https://esm.sh\",\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n frameDomains: [\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n },\n prefersBorder: false,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"embed-app.js","sourceRoot":"","sources":["../../src/mcp/embed-app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAC;AAE1E,MAAM,cAAc,GAClB,mEAAmE,CAAC;AAEtE,MAAM,CAAC,MAAM,iCAAiC,GAAG,gBAAgB,CAAC;AAClE,MAAM,6BAA6B,GAAG,EAAE,CAAC;AACzC,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AACnD,MAAM,CAAC,MAAM,4BAA4B,GACvC,+BAA+B,GAAG,6BAA6B,CAAC;AAalE,SAAS,IAAI,CAAC,KAAyB;IACrC,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,UAA2B,EAAE;IAE7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC;IAC1C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,aAAa,CAAC;IACrD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACtE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,KAAK,KAAK,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CACrB,GAAG,EACH,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,IAAI,4BAA4B,CAAC,CAC9D,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,GAAG,6BAA6B,CAAC;IAE9D,OAAO;QACL,KAAK;QACL,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,IAAI,EAAE,GAAG,EAAE,CAAC;;;;;;;;;oDASoC,MAAM;;;;;;+CAMX,cAAc;oDACT,cAAc;iEACD,cAAc;sGACuB,cAAc;;;;;;;;oBAQhG,IAAI,CAAC,KAAK,CAAC;uBACR,IAAI,CAAC,WAAW,CAAC;qBACnB,IAAI,CAAC,SAAS,CAAC;qBACf,IAAI,CAAC,aAAa,CAAC;wBAChB,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;;;;4CAIN,IAAI,CAAC,KAAK,CAAC;;;mDAGJ,IAAI,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;8BAepC,IAAI,CAAC,SAAS,CAAC,+BAA+B,CAAC;8BAC/C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCA8hBE,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;QA2B5C;QACJ,GAAG,EAAE;YACH,cAAc,EAAE,CAAC,gBAAgB,CAAC;YAClC,eAAe,EAAE;gBACf,gBAAgB;gBAChB,iCAAiC;gBACjC,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;aAChC;YACD,YAAY,EAAE;gBACZ,iCAAiC;gBACjC,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;aAChC;SACF;QACD,aAAa,EAAE,KAAK;KACrB,CAAC;AACJ,CAAC","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_VIEWPORT_HEIGHT = 720;\nexport const DEFAULT_MCP_APP_SHELL_HEIGHT =\n DEFAULT_MCP_APP_VIEWPORT_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\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; }\n * { box-sizing: border-box; }\n body { margin: 0; }\n .shell { display: grid; gap: 8px; min-height: ${height}px; 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: ${viewportHeight}px; }\n iframe { display: block; width: 100%; height: ${viewportHeight}px; border: 0; background: Canvas; }\n .message { display: grid; place-items: center; min-height: ${viewportHeight}px; 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: ${viewportHeight}px; 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 intrinsicHeight = ${height};\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 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 return metaUrl || data.url || data.deepLink || data.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 [0, 200, 500, 1500].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 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 }, 7000);\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({\n url,\n chrome: typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\"\n });\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 }, 30000);\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 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 if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== \"function\") {\n return;\n }\n try {\n openAiBridge.notifyIntrinsicHeight({ height: intrinsicHeight });\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 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 const result = await callEmbedSessionTool({\n url: embedUrl,\n chrome: typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\"\n });\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 navigateToAppFrame(data.startUrl);\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({ name: \"Agent Native Embed\", version: \"1.0.0\" }, {});\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 sendHostContext();\n };\n await app.connect();\n updateDisplayButton();\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\"],\n resourceDomains: [\n \"https://esm.sh\",\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n frameDomains: [\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n },\n prefersBorder: false,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open-route.d.ts","sourceRoot":"","sources":["../../src/server/open-route.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"open-route.d.ts","sourceRoot":"","sources":["../../src/server/open-route.ts"],"names":[],"mappings":"AA4DA,MAAM,WAAW,gBAAgB;IAC/B;;yEAEqE;IACrE,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE;QACzB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAChC,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACjC;AA6DD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,gBAAqB,2FAoIpE"}
|
|
@@ -2,7 +2,7 @@ import { defineEventHandler, getMethod } from "h3";
|
|
|
2
2
|
import { getSession, getConfiguredLoginHtml } from "./auth.js";
|
|
3
3
|
import { appStatePut, appStateGet } from "../application-state/store.js";
|
|
4
4
|
import { AGENT_SIDEBAR_QUERY_PARAM, withCollapsedAgentSidebarParam, } from "../shared/agent-sidebar-url.js";
|
|
5
|
-
import { EMBED_MODE_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, } from "../shared/embed-auth.js";
|
|
5
|
+
import { EMBED_MODE_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, } from "../shared/embed-auth.js";
|
|
6
6
|
import { getConfiguredAppBasePath } from "./app-base-path.js";
|
|
7
7
|
/** Query keys that are route control, not navigation payload. */
|
|
8
8
|
const RESERVED = new Set([
|
|
@@ -12,6 +12,7 @@ const RESERVED = new Set([
|
|
|
12
12
|
"compose",
|
|
13
13
|
EMBED_MODE_QUERY_PARAM,
|
|
14
14
|
EMBED_TOKEN_QUERY_PARAM,
|
|
15
|
+
MCP_APP_CHAT_BRIDGE_QUERY_PARAM,
|
|
15
16
|
AGENT_SIDEBAR_QUERY_PARAM,
|
|
16
17
|
]);
|
|
17
18
|
// Control-char guard (NUL..US + DEL). Defined via codepoints so the source
|
|
@@ -196,7 +197,11 @@ export function createOpenRouteHandler(options = {}) {
|
|
|
196
197
|
}
|
|
197
198
|
target = appendSearchParams(target, filters);
|
|
198
199
|
const embedParams = new URLSearchParams();
|
|
199
|
-
for (const key of [
|
|
200
|
+
for (const key of [
|
|
201
|
+
EMBED_MODE_QUERY_PARAM,
|
|
202
|
+
EMBED_TOKEN_QUERY_PARAM,
|
|
203
|
+
MCP_APP_CHAT_BRIDGE_QUERY_PARAM,
|
|
204
|
+
]) {
|
|
200
205
|
const value = search.get(key);
|
|
201
206
|
if (value)
|
|
202
207
|
embedParams.set(key, value);
|