483 lines
15 KiB
JavaScript
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
|