feat: enhance sandbox capability negotiation (#158)
* feat: enhance sandbox capability negotiation * Update specification/draft/apps.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add all csp and permissions to host capabilities + update SEP * feat: add clipboard-write permission support Adds clipboardWrite to McpUiResourcePermissions for clipboard access. Maps to Permission Policy 'clipboard-write' feature. * rm files added by mistake * refactor: change McpUiResourcePermissions properties to empty objects * chore: remove scratch files and redundant cross-env dependency --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
This commit is contained in:
parent
9f09932baf
commit
9acc52cb96
2 changed files with 496 additions and 16 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
|
@ -43,6 +43,7 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
|||
interface UiResourceData {
|
||||
html: string;
|
||||
csp?: McpUiResourceCsp;
|
||||
permissions?: McpUiResourcePermissions;
|
||||
}
|
||||
|
||||
export interface ToolCallInfo {
|
||||
|
|
@ -105,15 +106,16 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes
|
|||
|
||||
const html = "blob" in content ? atob(content.blob) : content.text;
|
||||
|
||||
// Extract CSP metadata from resource content._meta.ui.csp (or content.meta for Python SDK)
|
||||
// Extract CSP and permissions metadata from resource content._meta.ui (or content.meta for Python SDK)
|
||||
log.info("Resource content keys:", Object.keys(content));
|
||||
log.info("Resource content._meta:", (content as any)._meta);
|
||||
|
||||
// Try both _meta (spec) and meta (Python SDK quirk)
|
||||
const contentMeta = (content as any)._meta || (content as any).meta;
|
||||
const csp = contentMeta?.ui?.csp;
|
||||
const permissions = contentMeta?.ui?.permissions;
|
||||
|
||||
return { html, csp };
|
||||
return { html, csp, permissions };
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -168,10 +170,10 @@ export async function initializeApp(
|
|||
new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!),
|
||||
);
|
||||
|
||||
// Load inner iframe HTML with CSP metadata
|
||||
const { html, csp } = await appResourcePromise;
|
||||
log.info("Sending UI resource HTML to MCP App", csp ? `(CSP: ${JSON.stringify(csp)})` : "");
|
||||
await appBridge.sendSandboxResourceReady({ html, csp });
|
||||
// Load inner iframe HTML with CSP and permissions metadata
|
||||
const { html, csp, permissions } = await appResourcePromise;
|
||||
log.info("Sending UI resource HTML to MCP App", csp ? `(CSP: ${JSON.stringify(csp)})` : "", permissions ? `(Permissions: ${JSON.stringify(permissions)})` : "");
|
||||
await appBridge.sendSandboxResourceReady({ html, csp, permissions });
|
||||
|
||||
// Wait for inner iframe to be ready
|
||||
log.info("Waiting for MCP App to initialize...");
|
||||
|
|
|
|||
|
|
@ -66,6 +66,24 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] =
|
|||
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
|
||||
// based on ?csp= query param). This is tamper-proof unlike meta tags.
|
||||
|
||||
// Build iframe allow attribute from permissions
|
||||
function buildAllowAttribute(permissions?: {
|
||||
camera?: boolean;
|
||||
microphone?: boolean;
|
||||
geolocation?: boolean;
|
||||
clipboardWrite?: boolean;
|
||||
}): string {
|
||||
if (!permissions) return "";
|
||||
|
||||
const allowList: string[] = [];
|
||||
if (permissions.camera) allowList.push("camera");
|
||||
if (permissions.microphone) allowList.push("microphone");
|
||||
if (permissions.geolocation) allowList.push("geolocation");
|
||||
if (permissions.clipboardWrite) allowList.push("clipboard-write");
|
||||
|
||||
return allowList.join("; ");
|
||||
}
|
||||
|
||||
window.addEventListener("message", async (event) => {
|
||||
if (event.source === window.parent) {
|
||||
// Validate that messages from parent come from the expected host origin.
|
||||
|
|
@ -81,10 +99,16 @@ window.addEventListener("message", async (event) => {
|
|||
}
|
||||
|
||||
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
||||
const { html, sandbox } = event.data.params;
|
||||
const { html, sandbox, permissions } = event.data.params;
|
||||
if (typeof sandbox === "string") {
|
||||
inner.setAttribute("sandbox", sandbox);
|
||||
}
|
||||
// Set Permission Policy allow attribute if permissions are requested
|
||||
const allowAttribute = buildAllowAttribute(permissions);
|
||||
if (allowAttribute) {
|
||||
console.log("[Sandbox] Setting allow attribute:", allowAttribute);
|
||||
inner.setAttribute("allow", allowAttribute);
|
||||
}
|
||||
if (typeof html === "string") {
|
||||
// Use document.write instead of srcdoc for WebGL compatibility.
|
||||
// srcdoc creates an opaque origin which prevents WebGL canvas updates
|
||||
|
|
|
|||
Loading…
Reference in a new issue