rdesign/frontend/node_modules/@copilotkitnext/react/dist/components/MCPAppsActivityRenderer.mjs

483 lines
15 KiB
JavaScript

"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { z } from "zod";
//#region src/components/MCPAppsActivityRenderer.tsx
const PROTOCOL_VERSION = "2025-06-18";
function buildSandboxHTML(extraCspDomains) {
const baseScriptSrc = "'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*";
const baseFrameSrc = "* blob: data: http://localhost:* https://localhost:*";
const extra = extraCspDomains?.length ? " " + extraCspDomains.join(" ") : "";
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<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';" />
<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>
</head>
<body>
<script>
if(window.self===window.top){throw new Error("This file must be used in an iframe.")}
const inner=document.createElement("iframe");
inner.style="width:100%;height:100%;border:none;";
inner.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms");
document.body.appendChild(inner);
window.addEventListener("message",async(event)=>{
if(event.source===window.parent){
if(event.data&&event.data.method==="ui/notifications/sandbox-resource-ready"){
const{html,sandbox}=event.data.params;
if(typeof sandbox==="string")inner.setAttribute("sandbox",sandbox);
if(typeof html==="string")inner.srcdoc=html;
}else if(inner&&inner.contentWindow){
inner.contentWindow.postMessage(event.data,"*");
}
}else if(event.source===inner.contentWindow){
window.parent.postMessage(event.data,"*");
}
});
window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/sandbox-proxy-ready",params:{}},"*");
<\/script>
</body>
</html>`;
}
/**
* Queue for serializing MCP app requests to an agent.
* Ensures requests wait for the agent to stop running and are processed one at a time.
*/
var MCPAppsRequestQueue = class {
queues = /* @__PURE__ */ new Map();
processing = /* @__PURE__ */ new Map();
/**
* Add a request to the queue for a specific agent thread.
* Returns a promise that resolves when the request completes.
*/
async enqueue(agent, request) {
const threadId = agent.threadId || "default";
return new Promise((resolve, reject) => {
let queue = this.queues.get(threadId);
if (!queue) {
queue = [];
this.queues.set(threadId, queue);
}
queue.push({
execute: request,
resolve,
reject
});
this.processQueue(threadId, agent);
});
}
async processQueue(threadId, agent) {
if (this.processing.get(threadId)) return;
this.processing.set(threadId, true);
try {
const queue = this.queues.get(threadId);
if (!queue) return;
while (queue.length > 0) {
const item = queue[0];
try {
await this.waitForAgentIdle(agent);
const result = await item.execute();
item.resolve(result);
} catch (error) {
item.reject(error instanceof Error ? error : new Error(String(error)));
}
queue.shift();
}
} finally {
this.processing.set(threadId, false);
}
}
waitForAgentIdle(agent) {
return new Promise((resolve) => {
if (!agent.isRunning) {
resolve();
return;
}
let done = false;
const finish = () => {
if (done) return;
done = true;
clearInterval(checkInterval);
sub.unsubscribe();
resolve();
};
const sub = agent.subscribe({
onRunFinalized: finish,
onRunFailed: finish
});
const checkInterval = setInterval(() => {
if (!agent.isRunning) finish();
}, 500);
});
}
};
const mcpAppsRequestQueue = new MCPAppsRequestQueue();
/**
* Activity type for MCP Apps events - must match the middleware's MCPAppsActivityType
*/
const MCPAppsActivityType = "mcp-apps";
const MCPAppsActivityContentSchema = z.object({
result: z.object({
content: z.array(z.any()).optional(),
structuredContent: z.any().optional(),
isError: z.boolean().optional()
}),
resourceUri: z.string(),
serverHash: z.string(),
serverId: z.string().optional(),
toolInput: z.record(z.unknown()).optional()
});
function isRequest(msg) {
return "id" in msg && "method" in msg;
}
function isNotification(msg) {
return !("id" in msg) && "method" in msg;
}
/**
* MCP Apps Extension Activity Renderer
*
* Renders MCP Apps UI in a sandboxed iframe with full protocol support.
* Fetches resource content on-demand via proxied MCP requests.
*/
const MCPAppsActivityRenderer = function MCPAppsActivityRenderer({ content, agent }) {
const containerRef = useRef(null);
const iframeRef = useRef(null);
const [iframeReady, setIframeReady] = useState(false);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [iframeSize, setIframeSize] = useState({});
const [fetchedResource, setFetchedResource] = useState(null);
const contentRef = useRef(content);
contentRef.current = content;
const agentRef = useRef(agent);
agentRef.current = agent;
const fetchStateRef = useRef({
inProgress: false,
promise: null,
resourceUri: null
});
const sendToIframe = useCallback((msg) => {
if (iframeRef.current?.contentWindow) {
console.log("[MCPAppsRenderer] Sending to iframe:", msg);
iframeRef.current.contentWindow.postMessage(msg, "*");
}
}, []);
const sendResponse = useCallback((id, result) => {
sendToIframe({
jsonrpc: "2.0",
id,
result
});
}, [sendToIframe]);
const sendErrorResponse = useCallback((id, code, message) => {
sendToIframe({
jsonrpc: "2.0",
id,
error: {
code,
message
}
});
}, [sendToIframe]);
const sendNotification = useCallback((method, params) => {
sendToIframe({
jsonrpc: "2.0",
method,
params: params || {}
});
}, [sendToIframe]);
useEffect(() => {
const { resourceUri, serverHash, serverId } = content;
if (fetchStateRef.current.inProgress && fetchStateRef.current.resourceUri === resourceUri) {
fetchStateRef.current.promise?.then((resource) => {
if (resource) {
setFetchedResource(resource);
setIsLoading(false);
}
}).catch((err) => {
setError(err instanceof Error ? err : new Error(String(err)));
setIsLoading(false);
});
return;
}
if (!agent) {
setError(/* @__PURE__ */ new Error("No agent available to fetch resource"));
setIsLoading(false);
return;
}
fetchStateRef.current.inProgress = true;
fetchStateRef.current.resourceUri = resourceUri;
const fetchPromise = (async () => {
try {
const resource = (await mcpAppsRequestQueue.enqueue(agent, () => agent.runAgent({ forwardedProps: { __proxiedMCPRequest: {
serverHash,
serverId,
method: "resources/read",
params: { uri: resourceUri }
} } }))).result?.contents?.[0];
if (!resource) throw new Error("No resource content in response");
return resource;
} catch (err) {
console.error("[MCPAppsRenderer] Failed to fetch resource:", err);
throw err;
} finally {
fetchStateRef.current.inProgress = false;
}
})();
fetchStateRef.current.promise = fetchPromise;
fetchPromise.then((resource) => {
if (resource) {
setFetchedResource(resource);
setIsLoading(false);
}
}).catch((err) => {
setError(err instanceof Error ? err : new Error(String(err)));
setIsLoading(false);
});
}, [agent, content]);
useEffect(() => {
if (isLoading || !fetchedResource) return;
const container = containerRef.current;
if (!container) return;
let mounted = true;
let messageHandler = null;
let initialListener = null;
let createdIframe = null;
const setup = async () => {
try {
const iframe = document.createElement("iframe");
createdIframe = iframe;
iframe.style.width = "100%";
iframe.style.height = "100px";
iframe.style.border = "none";
iframe.style.backgroundColor = "transparent";
iframe.style.display = "block";
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
const sandboxReady = new Promise((resolve) => {
initialListener = (event) => {
if (event.source === iframe.contentWindow) {
if (event.data?.method === "ui/notifications/sandbox-proxy-ready") {
if (initialListener) {
window.removeEventListener("message", initialListener);
initialListener = null;
}
resolve();
}
}
};
window.addEventListener("message", initialListener);
});
if (!mounted) {
if (initialListener) {
window.removeEventListener("message", initialListener);
initialListener = null;
}
return;
}
const cspDomains = fetchedResource._meta?.ui?.csp?.resourceDomains;
iframe.srcdoc = buildSandboxHTML(cspDomains);
iframeRef.current = iframe;
container.appendChild(iframe);
await sandboxReady;
if (!mounted) return;
console.log("[MCPAppsRenderer] Sandbox proxy ready");
messageHandler = async (event) => {
if (event.source !== iframe.contentWindow) return;
const msg = event.data;
if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") return;
console.log("[MCPAppsRenderer] Received from iframe:", msg);
if (isRequest(msg)) switch (msg.method) {
case "ui/initialize":
sendResponse(msg.id, {
protocolVersion: PROTOCOL_VERSION,
hostInfo: {
name: "CopilotKit MCP Apps Host",
version: "1.0.0"
},
hostCapabilities: {
openLinks: {},
logging: {}
},
hostContext: {
theme: "light",
platform: "web"
}
});
break;
case "ui/message": {
const currentAgent = agentRef.current;
if (!currentAgent) {
console.warn("[MCPAppsRenderer] ui/message: No agent available");
sendResponse(msg.id, { isError: false });
break;
}
try {
const params = msg.params;
const textContent = params.content?.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n") || "";
if (textContent) currentAgent.addMessage({
id: crypto.randomUUID(),
role: params.role || "user",
content: textContent
});
sendResponse(msg.id, { isError: false });
} catch (err) {
console.error("[MCPAppsRenderer] ui/message error:", err);
sendResponse(msg.id, { isError: true });
}
break;
}
case "ui/open-link": {
const url = msg.params?.url;
if (url) {
window.open(url, "_blank", "noopener,noreferrer");
sendResponse(msg.id, { isError: false });
} else sendErrorResponse(msg.id, -32602, "Missing url parameter");
break;
}
case "tools/call": {
const { serverHash, serverId } = contentRef.current;
const currentAgent = agentRef.current;
if (!serverHash) {
sendErrorResponse(msg.id, -32603, "No server hash available for proxying");
break;
}
if (!currentAgent) {
sendErrorResponse(msg.id, -32603, "No agent available for proxying");
break;
}
try {
const runResult = await mcpAppsRequestQueue.enqueue(currentAgent, () => currentAgent.runAgent({ forwardedProps: { __proxiedMCPRequest: {
serverHash,
serverId,
method: "tools/call",
params: msg.params
} } }));
sendResponse(msg.id, runResult.result || {});
} catch (err) {
console.error("[MCPAppsRenderer] tools/call error:", err);
sendErrorResponse(msg.id, -32603, String(err));
}
break;
}
default: sendErrorResponse(msg.id, -32601, `Method not found: ${msg.method}`);
}
if (isNotification(msg)) switch (msg.method) {
case "ui/notifications/initialized":
console.log("[MCPAppsRenderer] Inner iframe initialized");
if (mounted) setIframeReady(true);
break;
case "ui/notifications/size-changed": {
const { width, height } = msg.params || {};
console.log("[MCPAppsRenderer] Size change:", {
width,
height
});
if (mounted) setIframeSize({
width: typeof width === "number" ? width : void 0,
height: typeof height === "number" ? height : void 0
});
break;
}
case "notifications/message":
console.log("[MCPAppsRenderer] App log:", msg.params);
break;
}
};
window.addEventListener("message", messageHandler);
let html;
if (fetchedResource.text) html = fetchedResource.text;
else if (fetchedResource.blob) html = atob(fetchedResource.blob);
else throw new Error("Resource has no text or blob content");
sendNotification("ui/notifications/sandbox-resource-ready", { html });
} catch (err) {
console.error("[MCPAppsRenderer] Setup error:", err);
if (mounted) setError(err instanceof Error ? err : new Error(String(err)));
}
};
setup();
return () => {
mounted = false;
if (initialListener) {
window.removeEventListener("message", initialListener);
initialListener = null;
}
if (messageHandler) window.removeEventListener("message", messageHandler);
if (createdIframe) {
createdIframe.remove();
createdIframe = null;
}
iframeRef.current = null;
};
}, [
isLoading,
fetchedResource,
sendNotification,
sendResponse,
sendErrorResponse
]);
useEffect(() => {
if (iframeRef.current) {
if (iframeSize.width !== void 0) {
iframeRef.current.style.minWidth = `min(${iframeSize.width}px, 100%)`;
iframeRef.current.style.width = "100%";
}
if (iframeSize.height !== void 0) iframeRef.current.style.height = `${iframeSize.height}px`;
}
}, [iframeSize]);
useEffect(() => {
if (iframeReady && content.toolInput) {
console.log("[MCPAppsRenderer] Sending tool input:", content.toolInput);
sendNotification("ui/notifications/tool-input", { arguments: content.toolInput });
}
}, [
iframeReady,
content.toolInput,
sendNotification
]);
useEffect(() => {
if (iframeReady && content.result) {
console.log("[MCPAppsRenderer] Sending tool result:", content.result);
sendNotification("ui/notifications/tool-result", content.result);
}
}, [
iframeReady,
content.result,
sendNotification
]);
const borderStyle = fetchedResource?._meta?.ui?.prefersBorder === true ? {
borderRadius: "8px",
backgroundColor: "#f9f9f9",
border: "1px solid #e0e0e0"
} : {};
return /* @__PURE__ */ jsxs("div", {
ref: containerRef,
style: {
width: "100%",
height: iframeSize.height ? `${iframeSize.height}px` : "auto",
minHeight: "100px",
overflow: "hidden",
position: "relative",
...borderStyle
},
children: [isLoading && /* @__PURE__ */ jsx("div", {
style: {
padding: "1rem",
color: "#666"
},
children: "Loading..."
}), error && /* @__PURE__ */ jsxs("div", {
style: {
color: "red",
padding: "1rem"
},
children: ["Error: ", error.message]
})]
});
};
//#endregion
export { MCPAppsActivityContentSchema, MCPAppsActivityRenderer, MCPAppsActivityType };
//# sourceMappingURL=MCPAppsActivityRenderer.mjs.map