@copilotkit/mcp-apps-renderer 1.71.2

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/activity.cjs +9 -0
  4. package/dist/activity.d.cts +4 -0
  5. package/dist/activity.d.mts +4 -0
  6. package/dist/activity.mjs +5 -0
  7. package/dist/activity.umd.js +91 -0
  8. package/dist/activity.umd.js.map +1 -0
  9. package/dist/constants.cjs +32 -0
  10. package/dist/constants.cjs.map +1 -0
  11. package/dist/constants.d.cts +23 -0
  12. package/dist/constants.d.cts.map +1 -0
  13. package/dist/constants.d.mts +23 -0
  14. package/dist/constants.d.mts.map +1 -0
  15. package/dist/constants.mjs +30 -0
  16. package/dist/constants.mjs.map +1 -0
  17. package/dist/content-schema.cjs +23 -0
  18. package/dist/content-schema.cjs.map +1 -0
  19. package/dist/content-schema.d.cts +51 -0
  20. package/dist/content-schema.d.cts.map +1 -0
  21. package/dist/content-schema.d.mts +51 -0
  22. package/dist/content-schema.d.mts.map +1 -0
  23. package/dist/content-schema.mjs +23 -0
  24. package/dist/content-schema.mjs.map +1 -0
  25. package/dist/follow-up.cjs +34 -0
  26. package/dist/follow-up.cjs.map +1 -0
  27. package/dist/follow-up.d.cts +42 -0
  28. package/dist/follow-up.d.cts.map +1 -0
  29. package/dist/follow-up.d.mts +42 -0
  30. package/dist/follow-up.d.mts.map +1 -0
  31. package/dist/follow-up.mjs +33 -0
  32. package/dist/follow-up.mjs.map +1 -0
  33. package/dist/index.d.mts +7 -0
  34. package/dist/index.mjs +8 -0
  35. package/dist/request-queue.d.mts +31 -0
  36. package/dist/request-queue.d.mts.map +1 -0
  37. package/dist/request-queue.mjs +93 -0
  38. package/dist/request-queue.mjs.map +1 -0
  39. package/dist/sandbox.d.mts +16 -0
  40. package/dist/sandbox.d.mts.map +1 -0
  41. package/dist/sandbox.mjs +52 -0
  42. package/dist/sandbox.mjs.map +1 -0
  43. package/dist/session.d.mts +81 -0
  44. package/dist/session.d.mts.map +1 -0
  45. package/dist/session.mjs +221 -0
  46. package/dist/session.mjs.map +1 -0
  47. package/package.json +77 -0
@@ -0,0 +1,33 @@
1
+ //#region src/follow-up.ts
2
+ /**
3
+ * Run an MCP app `ui/message` follow-up, scoped to the thread it was enqueued
4
+ * for (issue #5819).
5
+ *
6
+ * The MCP request queue delays follow-up work until the agent is idle. There is
7
+ * a single shared registry agent per id, and switching threads overwrites its
8
+ * `threadId`/`messages` in place. So if the host switches threads while a
9
+ * follow-up is queued, running it now would execute against — and stream into —
10
+ * the now-foreground thread.
11
+ *
12
+ * - **Same thread** (the common case): run on the shared agent, unchanged.
13
+ * - **Thread changed**: the shared agent has moved on, so the follow-up can no
14
+ * longer run in its originating thread's context. Drop it rather than leak it
15
+ * into the current thread. (The MCP app already received its `ui/message` ack
16
+ * at enqueue time; only the optional agent turn is skipped.)
17
+ *
18
+ * @internal exported for testing.
19
+ */
20
+ async function ɵrunMcpFollowUp({ host, agent, capturedThreadId }) {
21
+ const currentThreadId = agent.threadId || "default";
22
+ const originThreadId = capturedThreadId || "default";
23
+ if (currentThreadId === originThreadId) return host.runAgent({ agent });
24
+ console.warn(`[MCPAppsRenderer] ui/message follow-up dropped: the thread changed (${originThreadId} → ${currentThreadId}) between enqueue and execution, so running it would leak into the now-foreground thread.`);
25
+ return {
26
+ result: void 0,
27
+ newMessages: []
28
+ };
29
+ }
30
+
31
+ //#endregion
32
+ export { ɵrunMcpFollowUp };
33
+ //# sourceMappingURL=follow-up.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow-up.mjs","names":[],"sources":["../src/follow-up.ts"],"sourcesContent":["import type { AbstractAgent, RunAgentResult } from \"@ag-ui/client\";\n\n/**\n * The subset of `CopilotKitCore` that {@link ɵrunMcpFollowUp} depends on.\n * Declared structurally so the runner can be unit-tested without a full core.\n */\nexport interface ɵMcpFollowUpHost {\n runAgent(params: { agent: AbstractAgent }): Promise<RunAgentResult>;\n}\n\n/**\n * Run an MCP app `ui/message` follow-up, scoped to the thread it was enqueued\n * for (issue #5819).\n *\n * The MCP request queue delays follow-up work until the agent is idle. There is\n * a single shared registry agent per id, and switching threads overwrites its\n * `threadId`/`messages` in place. So if the host switches threads while a\n * follow-up is queued, running it now would execute against — and stream into —\n * the now-foreground thread.\n *\n * - **Same thread** (the common case): run on the shared agent, unchanged.\n * - **Thread changed**: the shared agent has moved on, so the follow-up can no\n * longer run in its originating thread's context. Drop it rather than leak it\n * into the current thread. (The MCP app already received its `ui/message` ack\n * at enqueue time; only the optional agent turn is skipped.)\n *\n * @internal exported for testing.\n */\nexport async function ɵrunMcpFollowUp({\n host,\n agent,\n capturedThreadId,\n}: {\n host: ɵMcpFollowUpHost;\n agent: AbstractAgent;\n capturedThreadId: string;\n}): Promise<RunAgentResult> {\n const currentThreadId = agent.threadId || \"default\";\n const originThreadId = capturedThreadId || \"default\";\n\n if (currentThreadId === originThreadId) {\n return host.runAgent({ agent });\n }\n\n console.warn(\n \"[MCPAppsRenderer] ui/message follow-up dropped: the thread changed \" +\n `(${originThreadId} → ${currentThreadId}) between enqueue and execution, ` +\n \"so running it would leak into the now-foreground thread.\",\n );\n return { result: undefined, newMessages: [] };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,gBAAgB,EACpC,MACA,OACA,oBAK0B;CAC1B,MAAM,kBAAkB,MAAM,YAAY;CAC1C,MAAM,iBAAiB,oBAAoB;AAE3C,KAAI,oBAAoB,eACtB,QAAO,KAAK,SAAS,EAAE,OAAO,CAAC;AAGjC,SAAQ,KACN,uEACM,eAAe,KAAK,gBAAgB,2FAE3C;AACD,QAAO;EAAE,QAAQ;EAAW,aAAa,EAAE;EAAE"}
@@ -0,0 +1,7 @@
1
+ import { MCPAppsActivityType, MCP_OPEN_LINK_BLOCKED_SCHEMES } from "./constants.mjs";
2
+ import { MCPAppsActivityContent, MCPAppsActivityContentSchema } from "./content-schema.mjs";
3
+ import { buildSandboxHTML } from "./sandbox.mjs";
4
+ import { MCPAppsRequestQueue, mcpAppsRequestQueue } from "./request-queue.mjs";
5
+ import { ɵMcpFollowUpHost, ɵrunMcpFollowUp } from "./follow-up.mjs";
6
+ import { BindMcpAppOptions, FetchedResource, MCP_APPS_PROTOCOL_VERSION, McpAppSession, McpAppSessionHooks, bindMcpApp } from "./session.mjs";
7
+ export { BindMcpAppOptions, FetchedResource, MCPAppsActivityContent, MCPAppsActivityContentSchema, MCPAppsActivityType, MCPAppsRequestQueue, MCP_APPS_PROTOCOL_VERSION, MCP_OPEN_LINK_BLOCKED_SCHEMES, McpAppSession, McpAppSessionHooks, bindMcpApp, buildSandboxHTML, mcpAppsRequestQueue, ɵMcpFollowUpHost, ɵrunMcpFollowUp };
package/dist/index.mjs ADDED
@@ -0,0 +1,8 @@
1
+ import { MCPAppsActivityType, MCP_OPEN_LINK_BLOCKED_SCHEMES } from "./constants.mjs";
2
+ import { MCPAppsActivityContentSchema } from "./content-schema.mjs";
3
+ import { buildSandboxHTML } from "./sandbox.mjs";
4
+ import { MCPAppsRequestQueue, mcpAppsRequestQueue } from "./request-queue.mjs";
5
+ import { ɵrunMcpFollowUp } from "./follow-up.mjs";
6
+ import { MCP_APPS_PROTOCOL_VERSION, bindMcpApp } from "./session.mjs";
7
+
8
+ export { MCPAppsActivityContentSchema, MCPAppsActivityType, MCPAppsRequestQueue, MCP_APPS_PROTOCOL_VERSION, MCP_OPEN_LINK_BLOCKED_SCHEMES, bindMcpApp, buildSandboxHTML, mcpAppsRequestQueue, ɵrunMcpFollowUp };
@@ -0,0 +1,31 @@
1
+ import { AbstractAgent, RunAgentResult } from "@ag-ui/client";
2
+
3
+ //#region src/request-queue.d.ts
4
+ /**
5
+ * Queue for serializing MCP app requests to an agent.
6
+ * Ensures requests wait for the agent to stop running and are processed one at a time.
7
+ */
8
+ declare class MCPAppsRequestQueue {
9
+ private queues;
10
+ private processing;
11
+ /**
12
+ * Add a request to the queue for a specific agent thread.
13
+ * Returns a promise that resolves when the request completes.
14
+ */
15
+ enqueue(agent: AbstractAgent, request: () => Promise<RunAgentResult>): Promise<RunAgentResult>;
16
+ /**
17
+ * Drain a thread's queue one request at a time, waiting for the agent to go
18
+ * idle before each. Re-entrant-safe (a single processor per thread) and drops
19
+ * the thread's map entries once fully drained to keep the shared queue bounded.
20
+ */
21
+ private processQueue;
22
+ /**
23
+ * Resolve once the agent is not running. Subscribes to run-finalized/failed and
24
+ * also polls as a fallback for reconnect scenarios where events do not fire.
25
+ */
26
+ private waitForAgentIdle;
27
+ }
28
+ declare const mcpAppsRequestQueue: MCPAppsRequestQueue;
29
+ //#endregion
30
+ export { MCPAppsRequestQueue, mcpAppsRequestQueue };
31
+ //# sourceMappingURL=request-queue.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-queue.d.mts","names":[],"sources":["../src/request-queue.ts"],"mappings":";;;;;AAMA;;cAAa,mBAAA;EAAA,QACH,MAAA;EAAA,QAQA,UAAA;EAQS;;;;EAFX,OAAA,CACJ,KAAA,EAAO,aAAA,EACP,OAAA,QAAe,OAAA,CAAQ,cAAA,IACtB,OAAA,CAAQ,cAAA;EAjBH;;;;;EAAA,QAyCM,YAAA;EAzBW;;;;EAAA,QA6EjB,gBAAA;AAAA;AAAA,cA8BG,mBAAA,EAAmB,mBAAA"}
@@ -0,0 +1,93 @@
1
+ //#region src/request-queue.ts
2
+ /**
3
+ * Queue for serializing MCP app requests to an agent.
4
+ * Ensures requests wait for the agent to stop running and are processed one at a time.
5
+ */
6
+ var MCPAppsRequestQueue = class {
7
+ constructor() {
8
+ this.queues = /* @__PURE__ */ new Map();
9
+ this.processing = /* @__PURE__ */ new Map();
10
+ }
11
+ /**
12
+ * Add a request to the queue for a specific agent thread.
13
+ * Returns a promise that resolves when the request completes.
14
+ */
15
+ async enqueue(agent, request) {
16
+ const threadId = agent.threadId || "default";
17
+ return new Promise((resolve, reject) => {
18
+ let queue = this.queues.get(threadId);
19
+ if (!queue) {
20
+ queue = [];
21
+ this.queues.set(threadId, queue);
22
+ }
23
+ queue.push({
24
+ execute: request,
25
+ resolve,
26
+ reject
27
+ });
28
+ this.processQueue(threadId, agent);
29
+ });
30
+ }
31
+ /**
32
+ * Drain a thread's queue one request at a time, waiting for the agent to go
33
+ * idle before each. Re-entrant-safe (a single processor per thread) and drops
34
+ * the thread's map entries once fully drained to keep the shared queue bounded.
35
+ */
36
+ async processQueue(threadId, agent) {
37
+ if (this.processing.get(threadId)) return;
38
+ this.processing.set(threadId, true);
39
+ try {
40
+ const queue = this.queues.get(threadId);
41
+ if (!queue) return;
42
+ while (queue.length > 0) {
43
+ const item = queue[0];
44
+ try {
45
+ await this.waitForAgentIdle(agent);
46
+ const result = await item.execute();
47
+ item.resolve(result);
48
+ } catch (error) {
49
+ item.reject(error instanceof Error ? error : new Error(String(error)));
50
+ }
51
+ queue.shift();
52
+ }
53
+ } finally {
54
+ const queue = this.queues.get(threadId);
55
+ if (!queue || queue.length === 0) {
56
+ this.queues.delete(threadId);
57
+ this.processing.delete(threadId);
58
+ } else this.processing.set(threadId, false);
59
+ }
60
+ }
61
+ /**
62
+ * Resolve once the agent is not running. Subscribes to run-finalized/failed and
63
+ * also polls as a fallback for reconnect scenarios where events do not fire.
64
+ */
65
+ waitForAgentIdle(agent) {
66
+ return new Promise((resolve) => {
67
+ if (!agent.isRunning) {
68
+ resolve();
69
+ return;
70
+ }
71
+ let done = false;
72
+ const finish = () => {
73
+ if (done) return;
74
+ done = true;
75
+ clearInterval(checkInterval);
76
+ sub.unsubscribe();
77
+ resolve();
78
+ };
79
+ const sub = agent.subscribe({
80
+ onRunFinalized: finish,
81
+ onRunFailed: finish
82
+ });
83
+ const checkInterval = setInterval(() => {
84
+ if (!agent.isRunning) finish();
85
+ }, 500);
86
+ });
87
+ }
88
+ };
89
+ const mcpAppsRequestQueue = new MCPAppsRequestQueue();
90
+
91
+ //#endregion
92
+ export { MCPAppsRequestQueue, mcpAppsRequestQueue };
93
+ //# sourceMappingURL=request-queue.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-queue.mjs","names":[],"sources":["../src/request-queue.ts"],"sourcesContent":["import type { AbstractAgent, RunAgentResult } from \"@ag-ui/client\";\n\n/**\n * Queue for serializing MCP app requests to an agent.\n * Ensures requests wait for the agent to stop running and are processed one at a time.\n */\nexport class MCPAppsRequestQueue {\n private queues = new Map<\n string,\n Array<{\n execute: () => Promise<RunAgentResult>;\n resolve: (result: RunAgentResult) => void;\n reject: (error: Error) => void;\n }>\n >();\n private processing = new Map<string, boolean>();\n\n /**\n * Add a request to the queue for a specific agent thread.\n * Returns a promise that resolves when the request completes.\n */\n async enqueue(\n agent: AbstractAgent,\n request: () => Promise<RunAgentResult>,\n ): Promise<RunAgentResult> {\n const threadId = agent.threadId || \"default\";\n\n return new Promise((resolve, reject) => {\n // Get or create queue for this thread\n let queue = this.queues.get(threadId);\n if (!queue) {\n queue = [];\n this.queues.set(threadId, queue);\n }\n\n // Add request to queue\n queue.push({ execute: request, resolve, reject });\n\n // Start processing if not already running\n this.processQueue(threadId, agent);\n });\n }\n\n /**\n * Drain a thread's queue one request at a time, waiting for the agent to go\n * idle before each. Re-entrant-safe (a single processor per thread) and drops\n * the thread's map entries once fully drained to keep the shared queue bounded.\n */\n private async processQueue(\n threadId: string,\n agent: AbstractAgent,\n ): Promise<void> {\n // If already processing this queue, return\n if (this.processing.get(threadId)) {\n return;\n }\n\n this.processing.set(threadId, true);\n\n try {\n const queue = this.queues.get(threadId);\n if (!queue) return;\n\n while (queue.length > 0) {\n const item = queue[0]!;\n\n try {\n // Wait for any active run to complete before processing\n await this.waitForAgentIdle(agent);\n\n // Execute the request\n const result = await item.execute();\n item.resolve(result);\n } catch (error) {\n item.reject(\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n\n // Remove processed item\n queue.shift();\n }\n } finally {\n // Drop the drained thread entries from both maps. `mcpAppsRequestQueue` is\n // shared for the page lifetime, so retaining an entry per thread id would\n // grow unbounded as threads come and go.\n const queue = this.queues.get(threadId);\n if (!queue || queue.length === 0) {\n this.queues.delete(threadId);\n this.processing.delete(threadId);\n } else {\n this.processing.set(threadId, false);\n }\n }\n }\n\n /**\n * Resolve once the agent is not running. Subscribes to run-finalized/failed and\n * also polls as a fallback for reconnect scenarios where events do not fire.\n */\n private waitForAgentIdle(agent: AbstractAgent): Promise<void> {\n return new Promise((resolve) => {\n if (!agent.isRunning) {\n resolve();\n return;\n }\n\n let done = false;\n const finish = () => {\n if (done) return;\n done = true;\n clearInterval(checkInterval);\n sub.unsubscribe();\n resolve();\n };\n\n const sub = agent.subscribe({\n onRunFinalized: finish,\n onRunFailed: finish,\n });\n\n // Fallback for reconnect scenarios where events don't fire\n const checkInterval = setInterval(() => {\n if (!agent.isRunning) finish();\n }, 500);\n });\n }\n}\n\n// Shared per-thread queue instance for all MCP app requests.\nexport const mcpAppsRequestQueue = new MCPAppsRequestQueue();\n"],"mappings":";;;;;AAMA,IAAa,sBAAb,MAAiC;;gCACd,IAAI,KAOlB;oCACkB,IAAI,KAAsB;;;;;;CAM/C,MAAM,QACJ,OACA,SACyB;EACzB,MAAM,WAAW,MAAM,YAAY;AAEnC,SAAO,IAAI,SAAS,SAAS,WAAW;GAEtC,IAAI,QAAQ,KAAK,OAAO,IAAI,SAAS;AACrC,OAAI,CAAC,OAAO;AACV,YAAQ,EAAE;AACV,SAAK,OAAO,IAAI,UAAU,MAAM;;AAIlC,SAAM,KAAK;IAAE,SAAS;IAAS;IAAS;IAAQ,CAAC;AAGjD,QAAK,aAAa,UAAU,MAAM;IAClC;;;;;;;CAQJ,MAAc,aACZ,UACA,OACe;AAEf,MAAI,KAAK,WAAW,IAAI,SAAS,CAC/B;AAGF,OAAK,WAAW,IAAI,UAAU,KAAK;AAEnC,MAAI;GACF,MAAM,QAAQ,KAAK,OAAO,IAAI,SAAS;AACvC,OAAI,CAAC,MAAO;AAEZ,UAAO,MAAM,SAAS,GAAG;IACvB,MAAM,OAAO,MAAM;AAEnB,QAAI;AAEF,WAAM,KAAK,iBAAiB,MAAM;KAGlC,MAAM,SAAS,MAAM,KAAK,SAAS;AACnC,UAAK,QAAQ,OAAO;aACb,OAAO;AACd,UAAK,OACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAC1D;;AAIH,UAAM,OAAO;;YAEP;GAIR,MAAM,QAAQ,KAAK,OAAO,IAAI,SAAS;AACvC,OAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,SAAK,OAAO,OAAO,SAAS;AAC5B,SAAK,WAAW,OAAO,SAAS;SAEhC,MAAK,WAAW,IAAI,UAAU,MAAM;;;;;;;CAS1C,AAAQ,iBAAiB,OAAqC;AAC5D,SAAO,IAAI,SAAS,YAAY;AAC9B,OAAI,CAAC,MAAM,WAAW;AACpB,aAAS;AACT;;GAGF,IAAI,OAAO;GACX,MAAM,eAAe;AACnB,QAAI,KAAM;AACV,WAAO;AACP,kBAAc,cAAc;AAC5B,QAAI,aAAa;AACjB,aAAS;;GAGX,MAAM,MAAM,MAAM,UAAU;IAC1B,gBAAgB;IAChB,aAAa;IACd,CAAC;GAGF,MAAM,gBAAgB,kBAAkB;AACtC,QAAI,CAAC,MAAM,UAAW,SAAQ;MAC7B,IAAI;IACP;;;AAKN,MAAa,sBAAsB,IAAI,qBAAqB"}
@@ -0,0 +1,16 @@
1
+ //#region src/sandbox.d.ts
2
+ /**
3
+ * Build the sandbox-proxy HTML document loaded into the outer iframe.
4
+ *
5
+ * The proxy relays postMessage between the host and the inner sandboxed widget
6
+ * and announces `ui/notifications/sandbox-proxy-ready` once ready. `extraCspDomains`
7
+ * (from the resource's `_meta.ui.csp`) are appended to the `script-src`/`frame-src`
8
+ * CSP directives so a widget can load its own approved origins.
9
+ *
10
+ * @param extraCspDomains Optional additional origins allowed by the sandbox CSP.
11
+ * @returns The complete sandbox-proxy HTML document as a string.
12
+ */
13
+ declare function buildSandboxHTML(extraCspDomains?: string[]): string;
14
+ //#endregion
15
+ export { buildSandboxHTML };
16
+ //# sourceMappingURL=sandbox.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.mts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;AAWA;;;;;;;;;;iBAAgB,gBAAA,CAAiB,eAAA"}
@@ -0,0 +1,52 @@
1
+ //#region src/sandbox.ts
2
+ /**
3
+ * Build the sandbox-proxy HTML document loaded into the outer iframe.
4
+ *
5
+ * The proxy relays postMessage between the host and the inner sandboxed widget
6
+ * and announces `ui/notifications/sandbox-proxy-ready` once ready. `extraCspDomains`
7
+ * (from the resource's `_meta.ui.csp`) are appended to the `script-src`/`frame-src`
8
+ * CSP directives so a widget can load its own approved origins.
9
+ *
10
+ * @param extraCspDomains Optional additional origins allowed by the sandbox CSP.
11
+ * @returns The complete sandbox-proxy HTML document as a string.
12
+ */
13
+ function buildSandboxHTML(extraCspDomains) {
14
+ const baseScriptSrc = "'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*";
15
+ const baseFrameSrc = "* blob: data: http://localhost:* https://localhost:*";
16
+ const extra = extraCspDomains?.length ? " " + extraCspDomains.join(" ") : "";
17
+ return `<!doctype html>
18
+ <html>
19
+ <head>
20
+ <meta charset="utf-8" />
21
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data: blob: 'unsafe-inline'; media-src * blob: data:; font-src * blob: data:; script-src ${baseScriptSrc + extra}; style-src * blob: data: 'unsafe-inline'; connect-src *; frame-src ${baseFrameSrc + extra}; base-uri 'self';" />
22
+ <style>html,body{margin:0;padding:0;height:100%;width:100%;overflow:hidden}*{box-sizing:border-box}iframe{background-color:transparent;border:none;padding:0;overflow:hidden;width:100%;height:100%}</style>
23
+ </head>
24
+ <body>
25
+ <script>
26
+ if(window.self===window.top){throw new Error("This file must be used in an iframe.")}
27
+ const inner=document.createElement("iframe");
28
+ inner.style="width:100%;height:100%;border:none;";
29
+ inner.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms");
30
+ document.body.appendChild(inner);
31
+ window.addEventListener("message",async(event)=>{
32
+ if(event.source===window.parent){
33
+ if(event.data&&event.data.method==="ui/notifications/sandbox-resource-ready"){
34
+ const{html,sandbox}=event.data.params;
35
+ if(typeof sandbox==="string")inner.setAttribute("sandbox",sandbox);
36
+ if(typeof html==="string")inner.srcdoc=html;
37
+ }else if(inner&&inner.contentWindow){
38
+ inner.contentWindow.postMessage(event.data,"*");
39
+ }
40
+ }else if(event.source===inner.contentWindow){
41
+ window.parent.postMessage(event.data,"*");
42
+ }
43
+ });
44
+ window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/sandbox-proxy-ready",params:{}},"*");
45
+ <\/script>
46
+ </body>
47
+ </html>`;
48
+ }
49
+
50
+ //#endregion
51
+ export { buildSandboxHTML };
52
+ //# sourceMappingURL=sandbox.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.mjs","names":[],"sources":["../src/sandbox.ts"],"sourcesContent":["/**\n * Build the sandbox-proxy HTML document loaded into the outer iframe.\n *\n * The proxy relays postMessage between the host and the inner sandboxed widget\n * and announces `ui/notifications/sandbox-proxy-ready` once ready. `extraCspDomains`\n * (from the resource's `_meta.ui.csp`) are appended to the `script-src`/`frame-src`\n * CSP directives so a widget can load its own approved origins.\n *\n * @param extraCspDomains Optional additional origins allowed by the sandbox CSP.\n * @returns The complete sandbox-proxy HTML document as a string.\n */\nexport function buildSandboxHTML(extraCspDomains?: string[]): string {\n const baseScriptSrc =\n \"'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*\";\n const baseFrameSrc = \"* blob: data: http://localhost:* https://localhost:*\";\n const extra = extraCspDomains?.length ? \" \" + extraCspDomains.join(\" \") : \"\";\n const scriptSrc = baseScriptSrc + extra;\n const frameSrc = baseFrameSrc + extra;\n\n return `<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; img-src * data: blob: 'unsafe-inline'; media-src * blob: data:; font-src * blob: data:; script-src ${scriptSrc}; style-src * blob: data: 'unsafe-inline'; connect-src *; frame-src ${frameSrc}; base-uri 'self';\" />\n<style>html,body{margin:0;padding:0;height:100%;width:100%;overflow:hidden}*{box-sizing:border-box}iframe{background-color:transparent;border:none;padding:0;overflow:hidden;width:100%;height:100%}</style>\n</head>\n<body>\n<script>\nif(window.self===window.top){throw new Error(\"This file must be used in an iframe.\")}\nconst inner=document.createElement(\"iframe\");\ninner.style=\"width:100%;height:100%;border:none;\";\ninner.setAttribute(\"sandbox\",\"allow-scripts allow-same-origin allow-forms\");\ndocument.body.appendChild(inner);\nwindow.addEventListener(\"message\",async(event)=>{\nif(event.source===window.parent){\nif(event.data&&event.data.method===\"ui/notifications/sandbox-resource-ready\"){\nconst{html,sandbox}=event.data.params;\nif(typeof sandbox===\"string\")inner.setAttribute(\"sandbox\",sandbox);\nif(typeof html===\"string\")inner.srcdoc=html;\n}else if(inner&&inner.contentWindow){\ninner.contentWindow.postMessage(event.data,\"*\");\n}\n}else if(event.source===inner.contentWindow){\nwindow.parent.postMessage(event.data,\"*\");\n}\n});\nwindow.parent.postMessage({jsonrpc:\"2.0\",method:\"ui/notifications/sandbox-proxy-ready\",params:{}},\"*\");\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,iBAAiB,iBAAoC;CACnE,MAAM,gBACJ;CACF,MAAM,eAAe;CACrB,MAAM,QAAQ,iBAAiB,SAAS,MAAM,gBAAgB,KAAK,IAAI,GAAG;AAI1E,QAAO;;;;6KAHW,gBAAgB,MAOmJ,sEANpK,eAAe,MAMoO"}
@@ -0,0 +1,81 @@
1
+ import { MCPAppsActivityContent } from "./content-schema.mjs";
2
+ import { ɵMcpFollowUpHost } from "./follow-up.mjs";
3
+ import { AbstractAgent } from "@ag-ui/client";
4
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5
+
6
+ //#region src/session.d.ts
7
+ /**
8
+ * The MCP Apps protocol version this host negotiates. Sourced directly from the
9
+ * ext-apps bridge (single source of truth, no hand-maintained literal). It lives
10
+ * here (a bridge-side module) rather than in the bridge-free `./constants` /
11
+ * `./activity` entry so the lightweight activity-registration surface stays free
12
+ * of the ext-apps bundle; consumers that need the version import it from the
13
+ * package root, which already loads the bridge.
14
+ */
15
+ declare const MCP_APPS_PROTOCOL_VERSION = "2026-01-26";
16
+ /** A resource fetched from the MCP server via the agent proxy. */
17
+ interface FetchedResource {
18
+ uri: string;
19
+ mimeType?: string;
20
+ text?: string;
21
+ blob?: string;
22
+ _meta?: {
23
+ ui?: {
24
+ prefersBorder?: boolean;
25
+ csp?: {
26
+ connectDomains?: string[];
27
+ resourceDomains?: string[];
28
+ };
29
+ };
30
+ };
31
+ }
32
+ /** Reactive callbacks the framework adapter wires to its own state. */
33
+ interface McpAppSessionHooks {
34
+ /** The widget reported a new content size (ui/notifications/size-changed). */
35
+ onSizeChanged?(size: {
36
+ width?: number;
37
+ height?: number;
38
+ }): void;
39
+ /** The widget finished initializing (safe to push tool input/result). */
40
+ onInitialized?(): void;
41
+ /** The fetched resource metadata (e.g. prefersBorder) is available. */
42
+ onResource?(resource: FetchedResource): void;
43
+ /** Setup failed (resource fetch, connect, ...). */
44
+ onError?(err: Error): void;
45
+ }
46
+ interface BindMcpAppOptions {
47
+ /**
48
+ * The sandbox iframe. The adapter creates and OWNS this element (mounts it in
49
+ * its render model, sizes it, removes it on unmount). The session only
50
+ * configures the sandbox contract (sandbox attr, testid, srcdoc) and talks to
51
+ * it through the bridge - it never creates, moves, or removes the iframe.
52
+ */
53
+ iframe: HTMLIFrameElement;
54
+ /** Returns the current activity content (resourceUri, serverHash, tool input/result). */
55
+ getContent: () => MCPAppsActivityContent;
56
+ /** Returns the current agent (may change across renders). */
57
+ getAgent: () => AbstractAgent | undefined;
58
+ /** CopilotKit host, for ui/message follow-up runs (issue #5819). */
59
+ host: ɵMcpFollowUpHost;
60
+ hooks?: McpAppSessionHooks;
61
+ }
62
+ interface McpAppSession {
63
+ /** Forward the tool call input to the widget (host -> app). Buffered until ready. */
64
+ sendToolInput(args: Record<string, unknown>): void;
65
+ /** Forward the tool result to the widget (host -> app). Buffered until ready. */
66
+ sendToolResult(result: CallToolResult): void;
67
+ /** Disconnect the bridge and release listeners. Does NOT remove the iframe. */
68
+ teardown(): void;
69
+ }
70
+ /**
71
+ * Bind an MCP App to a host-provided sandbox iframe: fetch the widget resource
72
+ * through the agent, connect the ext-apps `AppBridge` over a PostMessage
73
+ * transport, and wire the app<->host protocol (ui/message, ui/open-link,
74
+ * tools/call + resources/read proxy, size, host context). Framework-agnostic:
75
+ * the React/Vue/Angular renderers create the iframe and wire reactive state via
76
+ * `hooks`, but all protocol logic lives here.
77
+ */
78
+ declare function bindMcpApp(opts: BindMcpAppOptions): McpAppSession;
79
+ //#endregion
80
+ export { BindMcpAppOptions, FetchedResource, MCP_APPS_PROTOCOL_VERSION, McpAppSession, McpAppSessionHooks, bindMcpApp };
81
+ //# sourceMappingURL=session.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../src/session.ts"],"mappings":";;;;;;;;AAwBA;;;;;AA0BA;cA1Ba,yBAAA;;UA0BI,eAAA;EACf,GAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;EACA,KAAA;IACE,EAAA;MACE,aAAA;MACA,GAAA;QACE,cAAA;QACA,eAAA;MAAA;IAAA;EAAA;AAAA;;UAOS,kBAAA;EAEf;EAAA,aAAA,EAAe,IAAA;IAAQ,KAAA;IAAgB,MAAA;EAAA;EAIvC;EAFA,aAAA;EAEY;EAAZ,UAAA,EAAY,QAAA,EAAU,eAAA;EAER;EAAd,OAAA,EAAS,GAAA,EAAK,KAAA;AAAA;AAAA,UAGC,iBAAA;EAAA;;;;;;EAOf,MAAA,EAAQ,iBAAA;EAOA;EALR,UAAA,QAAkB,sBAAA;EAKQ;EAH1B,QAAA,QAAgB,aAAA;EAJR;EAMR,IAAA,EAAM,gBAAA;EACN,KAAA,GAAQ,kBAAA;AAAA;AAAA,UAGO,aAAA;EAJf;EAMA,aAAA,CAAc,IAAA,EAAM,MAAA;EALpB;EAOA,cAAA,CAAe,MAAA,EAAQ,cAAA;EAPG;EAS1B,QAAA;AAAA;;;;;;;;;iBAWc,UAAA,CAAW,IAAA,EAAM,iBAAA,GAAoB,aAAA"}
@@ -0,0 +1,221 @@
1
+ import { MCP_OPEN_LINK_BLOCKED_SCHEMES } from "./constants.mjs";
2
+ import { buildSandboxHTML } from "./sandbox.mjs";
3
+ import { mcpAppsRequestQueue } from "./request-queue.mjs";
4
+ import { ɵrunMcpFollowUp } from "./follow-up.mjs";
5
+ import { z } from "zod";
6
+ import { AppBridge, LATEST_PROTOCOL_VERSION, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";
7
+ import { randomUUID } from "@copilotkit/shared";
8
+
9
+ //#region src/session.ts
10
+ /**
11
+ * The MCP Apps protocol version this host negotiates. Sourced directly from the
12
+ * ext-apps bridge (single source of truth, no hand-maintained literal). It lives
13
+ * here (a bridge-side module) rather than in the bridge-free `./constants` /
14
+ * `./activity` entry so the lightweight activity-registration surface stays free
15
+ * of the ext-apps bundle; consumers that need the version import it from the
16
+ * package root, which already loads the bridge.
17
+ */
18
+ const MCP_APPS_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
19
+ /**
20
+ * Permissive `ui/message` schema. ext-apps restricts the request to
21
+ * `role: "user"` with no `followUp`, but CopilotKit intentionally extends
22
+ * `ui/message` with `role` ("user" | "assistant") and `followUp` (documented
23
+ * behavior with dedicated tests). We register our own handler (instead of the
24
+ * bridge's strict `onmessage`) so those extensions survive.
25
+ *
26
+ * Going forward, widgets SHOULD pass the extensions under
27
+ * `params._meta.copilotkit`; the top-level `role`/`followUp` fields are the
28
+ * legacy channel, kept for backward compatibility and slated for deprecation.
29
+ */
30
+ const CopilotKitUiMessageSchema = z.object({
31
+ method: z.literal("ui/message"),
32
+ params: z.object({
33
+ role: z.string().optional(),
34
+ content: z.array(z.any()).optional(),
35
+ followUp: z.boolean().optional(),
36
+ _meta: z.record(z.string(), z.any()).optional()
37
+ }).passthrough()
38
+ });
39
+ /**
40
+ * Bind an MCP App to a host-provided sandbox iframe: fetch the widget resource
41
+ * through the agent, connect the ext-apps `AppBridge` over a PostMessage
42
+ * transport, and wire the app<->host protocol (ui/message, ui/open-link,
43
+ * tools/call + resources/read proxy, size, host context). Framework-agnostic:
44
+ * the React/Vue/Angular renderers create the iframe and wire reactive state via
45
+ * `hooks`, but all protocol logic lives here.
46
+ */
47
+ function bindMcpApp(opts) {
48
+ const { iframe, getContent, getAgent, host, hooks } = opts;
49
+ let disposed = false;
50
+ let ready = false;
51
+ let bridge = null;
52
+ let pendingToolInput;
53
+ let pendingToolResult;
54
+ /** Flush any buffered tool input/result to the widget once it is initialized. */
55
+ const flushPending = () => {
56
+ if (!ready || !bridge) return;
57
+ if (pendingToolInput !== void 0) {
58
+ bridge.sendToolInput({ arguments: pendingToolInput });
59
+ pendingToolInput = void 0;
60
+ }
61
+ if (pendingToolResult !== void 0) {
62
+ bridge.sendToolResult(pendingToolResult);
63
+ pendingToolResult = void 0;
64
+ }
65
+ };
66
+ /** Fetch the widget resource (`resources/read`) through the agent proxy queue. */
67
+ const fetchResource = async () => {
68
+ const agent = getAgent();
69
+ if (!agent) throw new Error("No agent available to fetch resource");
70
+ const { resourceUri, serverHash, serverId } = getContent();
71
+ const resource = (await mcpAppsRequestQueue.enqueue(agent, () => agent.runAgent({ forwardedProps: { __proxiedMCPRequest: {
72
+ serverHash,
73
+ serverId,
74
+ method: "resources/read",
75
+ params: { uri: resourceUri }
76
+ } } }))).result?.contents?.[0];
77
+ if (!resource) throw new Error("No resource content in response");
78
+ return resource;
79
+ };
80
+ /**
81
+ * Fetch the resource, configure + load the sandbox iframe, construct the
82
+ * AppBridge, wire the app->host handlers, and connect the transport.
83
+ */
84
+ const setup = async () => {
85
+ try {
86
+ const resource = await fetchResource();
87
+ if (disposed) return;
88
+ hooks?.onResource?.(resource);
89
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
90
+ iframe.setAttribute("data-testid", "mcp-app-iframe");
91
+ iframe.setAttribute("title", "Interactive MCP application");
92
+ const cspDomains = resource._meta?.ui?.csp?.resourceDomains;
93
+ iframe.srcdoc = buildSandboxHTML(cspDomains);
94
+ const win = iframe.contentWindow;
95
+ if (!win) throw new Error("Sandbox iframe has no contentWindow");
96
+ let html;
97
+ if (resource.text) html = resource.text;
98
+ else if (resource.blob) html = atob(resource.blob);
99
+ else throw new Error("Resource has no text or blob content");
100
+ bridge = new AppBridge(null, {
101
+ name: "CopilotKit MCP Apps Host",
102
+ version: "1.0.0"
103
+ }, {
104
+ openLinks: {},
105
+ logging: {},
106
+ message: { text: {} }
107
+ }, { hostContext: {
108
+ theme: "light",
109
+ platform: "web"
110
+ } });
111
+ bridge.onsandboxready = () => {
112
+ bridge?.sendSandboxResourceReady({ html });
113
+ };
114
+ bridge.setRequestHandler(CopilotKitUiMessageSchema, async (req) => {
115
+ const currentAgent = getAgent();
116
+ if (!currentAgent) {
117
+ console.warn("[MCPAppsRenderer] ui/message: No agent available");
118
+ return { isError: false };
119
+ }
120
+ try {
121
+ const params = req.params;
122
+ const ck = params._meta?.copilotkit ?? {};
123
+ const role = ck.role || params.role || "user";
124
+ const textContent = params.content?.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n") || "";
125
+ if (textContent) currentAgent.addMessage({
126
+ id: randomUUID(),
127
+ role,
128
+ content: textContent
129
+ });
130
+ if ((ck.followUp ?? params.followUp ?? role === "user") && textContent) {
131
+ const capturedThreadId = currentAgent.threadId || "default";
132
+ mcpAppsRequestQueue.enqueue(currentAgent, () => ɵrunMcpFollowUp({
133
+ host,
134
+ agent: currentAgent,
135
+ capturedThreadId
136
+ })).catch((err) => console.error("[MCPAppsRenderer] ui/message agent run failed:", err));
137
+ }
138
+ return { isError: false };
139
+ } catch (err) {
140
+ console.error("[MCPAppsRenderer] ui/message error:", err);
141
+ return { isError: true };
142
+ }
143
+ });
144
+ bridge.onopenlink = async ({ url }) => {
145
+ let parsed;
146
+ try {
147
+ parsed = new URL(url);
148
+ } catch {
149
+ console.warn("[MCPAppsRenderer] ui/open-link rejected: unparseable url");
150
+ return { isError: true };
151
+ }
152
+ if (MCP_OPEN_LINK_BLOCKED_SCHEMES.has(parsed.protocol)) {
153
+ console.warn("[MCPAppsRenderer] ui/open-link rejected: blocked scheme", parsed.protocol);
154
+ return { isError: true };
155
+ }
156
+ window.open(url, "_blank", "noopener,noreferrer");
157
+ return { isError: false };
158
+ };
159
+ bridge.oncalltool = async (params) => {
160
+ const { serverHash, serverId } = getContent();
161
+ const currentAgent = getAgent();
162
+ if (!serverHash) throw new Error("No server hash available for proxying");
163
+ if (!currentAgent) throw new Error("No agent available for proxying");
164
+ return (await mcpAppsRequestQueue.enqueue(currentAgent, () => currentAgent.runAgent({ forwardedProps: { __proxiedMCPRequest: {
165
+ serverHash,
166
+ serverId,
167
+ method: "tools/call",
168
+ params
169
+ } } }))).result || { content: [] };
170
+ };
171
+ bridge.onsizechange = (p) => {
172
+ if (disposed) return;
173
+ const { width, height } = p || {};
174
+ hooks?.onSizeChanged?.({
175
+ width: typeof width === "number" ? width : void 0,
176
+ height: typeof height === "number" ? height : void 0
177
+ });
178
+ };
179
+ bridge.oninitialized = () => {
180
+ if (disposed) return;
181
+ ready = true;
182
+ hooks?.onInitialized?.();
183
+ flushPending();
184
+ };
185
+ bridge.onloggingmessage = (p) => {
186
+ console.log("[MCPAppsRenderer] App log:", p);
187
+ };
188
+ const transport = new PostMessageTransport(win, win);
189
+ await bridge.connect(transport);
190
+ if (disposed) {
191
+ await bridge.close();
192
+ bridge = null;
193
+ return;
194
+ }
195
+ } catch (err) {
196
+ console.error("[MCPAppsRenderer] Setup error:", err);
197
+ if (!disposed) hooks?.onError?.(err instanceof Error ? err : new Error(String(err)));
198
+ }
199
+ };
200
+ setup();
201
+ return {
202
+ sendToolInput(args) {
203
+ pendingToolInput = args;
204
+ flushPending();
205
+ },
206
+ sendToolResult(result) {
207
+ pendingToolResult = result;
208
+ flushPending();
209
+ },
210
+ teardown() {
211
+ disposed = true;
212
+ const b = bridge;
213
+ bridge = null;
214
+ b?.close();
215
+ }
216
+ };
217
+ }
218
+
219
+ //#endregion
220
+ export { MCP_APPS_PROTOCOL_VERSION, bindMcpApp };
221
+ //# sourceMappingURL=session.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.mjs","names":[],"sources":["../src/session.ts"],"sourcesContent":["import {\n AppBridge,\n LATEST_PROTOCOL_VERSION,\n PostMessageTransport,\n} from \"@modelcontextprotocol/ext-apps/app-bridge\";\nimport type { AbstractAgent } from \"@ag-ui/client\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\nimport { randomUUID } from \"@copilotkit/shared\";\nimport { buildSandboxHTML } from \"./sandbox\";\nimport { mcpAppsRequestQueue } from \"./request-queue\";\nimport { ɵrunMcpFollowUp } from \"./follow-up\";\nimport type { ɵMcpFollowUpHost } from \"./follow-up\";\nimport { MCP_OPEN_LINK_BLOCKED_SCHEMES } from \"./constants\";\nimport type { MCPAppsActivityContent } from \"./content-schema\";\n\n/**\n * The MCP Apps protocol version this host negotiates. Sourced directly from the\n * ext-apps bridge (single source of truth, no hand-maintained literal). It lives\n * here (a bridge-side module) rather than in the bridge-free `./constants` /\n * `./activity` entry so the lightweight activity-registration surface stays free\n * of the ext-apps bundle; consumers that need the version import it from the\n * package root, which already loads the bridge.\n */\nexport const MCP_APPS_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;\n\n/**\n * Permissive `ui/message` schema. ext-apps restricts the request to\n * `role: \"user\"` with no `followUp`, but CopilotKit intentionally extends\n * `ui/message` with `role` (\"user\" | \"assistant\") and `followUp` (documented\n * behavior with dedicated tests). We register our own handler (instead of the\n * bridge's strict `onmessage`) so those extensions survive.\n *\n * Going forward, widgets SHOULD pass the extensions under\n * `params._meta.copilotkit`; the top-level `role`/`followUp` fields are the\n * legacy channel, kept for backward compatibility and slated for deprecation.\n */\nconst CopilotKitUiMessageSchema = z.object({\n method: z.literal(\"ui/message\"),\n params: z\n .object({\n role: z.string().optional(),\n content: z.array(z.any()).optional(),\n followUp: z.boolean().optional(),\n _meta: z.record(z.string(), z.any()).optional(),\n })\n .passthrough(),\n});\n\n/** A resource fetched from the MCP server via the agent proxy. */\nexport interface FetchedResource {\n uri: string;\n mimeType?: string;\n text?: string;\n blob?: string;\n _meta?: {\n ui?: {\n prefersBorder?: boolean;\n csp?: {\n connectDomains?: string[];\n resourceDomains?: string[];\n };\n };\n };\n}\n\n/** Reactive callbacks the framework adapter wires to its own state. */\nexport interface McpAppSessionHooks {\n /** The widget reported a new content size (ui/notifications/size-changed). */\n onSizeChanged?(size: { width?: number; height?: number }): void;\n /** The widget finished initializing (safe to push tool input/result). */\n onInitialized?(): void;\n /** The fetched resource metadata (e.g. prefersBorder) is available. */\n onResource?(resource: FetchedResource): void;\n /** Setup failed (resource fetch, connect, ...). */\n onError?(err: Error): void;\n}\n\nexport interface BindMcpAppOptions {\n /**\n * The sandbox iframe. The adapter creates and OWNS this element (mounts it in\n * its render model, sizes it, removes it on unmount). The session only\n * configures the sandbox contract (sandbox attr, testid, srcdoc) and talks to\n * it through the bridge - it never creates, moves, or removes the iframe.\n */\n iframe: HTMLIFrameElement;\n /** Returns the current activity content (resourceUri, serverHash, tool input/result). */\n getContent: () => MCPAppsActivityContent;\n /** Returns the current agent (may change across renders). */\n getAgent: () => AbstractAgent | undefined;\n /** CopilotKit host, for ui/message follow-up runs (issue #5819). */\n host: ɵMcpFollowUpHost;\n hooks?: McpAppSessionHooks;\n}\n\nexport interface McpAppSession {\n /** Forward the tool call input to the widget (host -> app). Buffered until ready. */\n sendToolInput(args: Record<string, unknown>): void;\n /** Forward the tool result to the widget (host -> app). Buffered until ready. */\n sendToolResult(result: CallToolResult): void;\n /** Disconnect the bridge and release listeners. Does NOT remove the iframe. */\n teardown(): void;\n}\n\n/**\n * Bind an MCP App to a host-provided sandbox iframe: fetch the widget resource\n * through the agent, connect the ext-apps `AppBridge` over a PostMessage\n * transport, and wire the app<->host protocol (ui/message, ui/open-link,\n * tools/call + resources/read proxy, size, host context). Framework-agnostic:\n * the React/Vue/Angular renderers create the iframe and wire reactive state via\n * `hooks`, but all protocol logic lives here.\n */\nexport function bindMcpApp(opts: BindMcpAppOptions): McpAppSession {\n const { iframe, getContent, getAgent, host, hooks } = opts;\n\n let disposed = false;\n let ready = false;\n let bridge: AppBridge | null = null;\n let pendingToolInput: Record<string, unknown> | undefined;\n let pendingToolResult: CallToolResult | undefined;\n\n /** Flush any buffered tool input/result to the widget once it is initialized. */\n const flushPending = () => {\n if (!ready || !bridge) return;\n if (pendingToolInput !== undefined) {\n void bridge.sendToolInput({ arguments: pendingToolInput });\n pendingToolInput = undefined;\n }\n if (pendingToolResult !== undefined) {\n void bridge.sendToolResult(pendingToolResult);\n pendingToolResult = undefined;\n }\n };\n\n /** Fetch the widget resource (`resources/read`) through the agent proxy queue. */\n const fetchResource = async (): Promise<FetchedResource> => {\n const agent = getAgent();\n if (!agent) {\n throw new Error(\"No agent available to fetch resource\");\n }\n const { resourceUri, serverHash, serverId } = getContent();\n const runResult = await mcpAppsRequestQueue.enqueue(agent, () =>\n agent.runAgent({\n forwardedProps: {\n __proxiedMCPRequest: {\n serverHash,\n serverId,\n method: \"resources/read\",\n params: { uri: resourceUri },\n },\n },\n }),\n );\n const resultData = runResult.result as\n | { contents?: FetchedResource[] }\n | undefined;\n const resource = resultData?.contents?.[0];\n if (!resource) {\n throw new Error(\"No resource content in response\");\n }\n return resource;\n };\n\n /**\n * Fetch the resource, configure + load the sandbox iframe, construct the\n * AppBridge, wire the app->host handlers, and connect the transport.\n */\n const setup = async () => {\n try {\n const resource = await fetchResource();\n if (disposed) return;\n hooks?.onResource?.(resource);\n\n // Configure the sandbox iframe (contract shared across frontends).\n iframe.setAttribute(\n \"sandbox\",\n \"allow-scripts allow-same-origin allow-forms\",\n );\n // Cross-frontend MCP-apps surface contract: every frontend must expose the\n // sandbox iframe under the SAME testid so one shared probe (harness\n // `d5-mcp-apps`) and one shared e2e spec can assert the surface mounted\n // without per-frontend selectors.\n iframe.setAttribute(\"data-testid\", \"mcp-app-iframe\");\n iframe.setAttribute(\"title\", \"Interactive MCP application\");\n\n const cspDomains = resource._meta?.ui?.csp?.resourceDomains;\n iframe.srcdoc = buildSandboxHTML(cspDomains);\n\n const win = iframe.contentWindow;\n if (!win) {\n throw new Error(\"Sandbox iframe has no contentWindow\");\n }\n\n let html: string;\n if (resource.text) {\n html = resource.text;\n } else if (resource.blob) {\n html = atob(resource.blob);\n } else {\n throw new Error(\"Resource has no text or blob content\");\n }\n\n bridge = new AppBridge(\n null,\n { name: \"CopilotKit MCP Apps Host\", version: \"1.0.0\" },\n { openLinks: {}, logging: {}, message: { text: {} } },\n // Seed the host context at construction so it is already in place when\n // the widget's ui/initialize is handled (deterministic, not a race).\n { hostContext: { theme: \"light\", platform: \"web\" } },\n );\n\n // Sandbox handshake: on proxy ready, load the widget HTML into the inner\n // sandboxed iframe.\n bridge.onsandboxready = () => {\n void bridge?.sendSandboxResourceReady({ html });\n };\n\n // --- App -> host requests ---\n // ui/message: custom handler preserving CopilotKit role/followUp extensions\n // (via _meta.copilotkit first, then legacy top-level fields).\n bridge.setRequestHandler(CopilotKitUiMessageSchema, async (req) => {\n const currentAgent = getAgent();\n if (!currentAgent) {\n console.warn(\"[MCPAppsRenderer] ui/message: No agent available\");\n return { isError: false };\n }\n try {\n const params = req.params;\n const ck = (params._meta?.copilotkit ?? {}) as {\n role?: string;\n followUp?: boolean;\n };\n const role =\n (ck.role as \"user\" | \"assistant\") ||\n (params.role as \"user\" | \"assistant\") ||\n \"user\";\n const textContent =\n (\n params.content as\n | Array<{ type: string; text?: string }>\n | undefined\n )\n ?.filter((c) => c.type === \"text\" && c.text)\n .map((c) => c.text)\n .join(\"\\n\") || \"\";\n if (textContent) {\n currentAgent.addMessage({\n id: randomUUID(),\n role,\n content: textContent,\n });\n }\n const followUp = ck.followUp ?? params.followUp;\n const shouldFollowUp = followUp ?? role === \"user\";\n if (shouldFollowUp && textContent) {\n const capturedThreadId = currentAgent.threadId || \"default\";\n mcpAppsRequestQueue\n .enqueue(currentAgent, () =>\n ɵrunMcpFollowUp({\n host,\n agent: currentAgent,\n capturedThreadId,\n }),\n )\n .catch((err) =>\n console.error(\n \"[MCPAppsRenderer] ui/message agent run failed:\",\n err,\n ),\n );\n }\n return { isError: false };\n } catch (err) {\n console.error(\"[MCPAppsRenderer] ui/message error:\", err);\n return { isError: true };\n }\n });\n\n bridge.onopenlink = async ({ url }) => {\n // The bridge validates `url` as a string but not the scheme. Block only\n // the script-executing / attacker-HTML schemes; everything else\n // (https universal links, custom-scheme deep links) is allowed.\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n console.warn(\n \"[MCPAppsRenderer] ui/open-link rejected: unparseable url\",\n );\n return { isError: true };\n }\n if (MCP_OPEN_LINK_BLOCKED_SCHEMES.has(parsed.protocol)) {\n console.warn(\n \"[MCPAppsRenderer] ui/open-link rejected: blocked scheme\",\n parsed.protocol,\n );\n return { isError: true };\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n return { isError: false };\n };\n\n bridge.oncalltool = async (params) => {\n const { serverHash, serverId } = getContent();\n const currentAgent = getAgent();\n if (!serverHash) {\n throw new Error(\"No server hash available for proxying\");\n }\n if (!currentAgent) {\n throw new Error(\"No agent available for proxying\");\n }\n const runResult = await mcpAppsRequestQueue.enqueue(currentAgent, () =>\n currentAgent.runAgent({\n forwardedProps: {\n __proxiedMCPRequest: {\n serverHash,\n serverId,\n method: \"tools/call\",\n params,\n },\n },\n }),\n );\n return (runResult.result as CallToolResult) || { content: [] };\n };\n\n // --- App -> host notifications ---\n bridge.onsizechange = (p) => {\n if (disposed) return;\n const { width, height } = (p || {}) as {\n width?: number;\n height?: number;\n };\n hooks?.onSizeChanged?.({\n width: typeof width === \"number\" ? width : undefined,\n height: typeof height === \"number\" ? height : undefined,\n });\n };\n bridge.oninitialized = () => {\n if (disposed) return;\n ready = true;\n hooks?.onInitialized?.();\n flushPending();\n };\n bridge.onloggingmessage = (p) => {\n console.log(\"[MCPAppsRenderer] App log:\", p);\n };\n\n const transport = new PostMessageTransport(win, win);\n await bridge.connect(transport);\n if (disposed) {\n await bridge.close();\n bridge = null;\n return;\n }\n } catch (err) {\n console.error(\"[MCPAppsRenderer] Setup error:\", err);\n if (!disposed) {\n hooks?.onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n }\n };\n\n void setup();\n\n return {\n sendToolInput(args) {\n pendingToolInput = args;\n flushPending();\n },\n sendToolResult(result) {\n pendingToolResult = result;\n flushPending();\n },\n teardown() {\n disposed = true;\n const b = bridge;\n bridge = null;\n void b?.close();\n },\n };\n}\n"],"mappings":"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"}