Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions examples/basic-host/src/implementation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, type McpUiResourceSandbox, buildAllowAttribute, buildSandboxAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
Expand Down Expand Up @@ -66,6 +66,7 @@ interface UiResourceData {
html: string;
csp?: McpUiResourceCsp;
permissions?: McpUiResourcePermissions;
sandbox?: McpUiResourceSandbox;
}

export interface ToolCallInfo {
Expand Down Expand Up @@ -136,20 +137,23 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes
const contentMeta = (content as any)._meta || (content as any).meta;
const csp = contentMeta?.ui?.csp;
const permissions = contentMeta?.ui?.permissions;
const sandbox = contentMeta?.ui?.sandbox;

return { html, csp, permissions };
return { html, csp, permissions, sandbox };
}


export function loadSandboxProxy(
iframe: HTMLIFrameElement,
csp?: McpUiResourceCsp,
permissions?: McpUiResourcePermissions,
sandbox?: McpUiResourceSandbox,
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter type for sandbox should be McpUiResourceSandbox | string (union type) to match the specification and the type of McpUiSandboxResourceReadyNotification['params']['sandbox']. The spec allows both structured flags objects and raw string overrides. While buildSandboxAttribute() will still work correctly since it only accepts McpUiResourceSandbox | undefined, the function signature should align with the spec to properly accept string values that apps might provide.

Copilot uses AI. Check for mistakes.
): Promise<boolean> {
// Prevent reload
if (iframe.src) return Promise.resolve(false);

iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
// Set sandbox attribute on outer iframe (must match inner iframe capabilities)
iframe.setAttribute("sandbox", buildSandboxAttribute(sandbox));

// Set Permission Policy allow attribute based on requested permissions
const allowAttribute = buildAllowAttribute(permissions);
Expand Down Expand Up @@ -199,10 +203,10 @@ export async function initializeApp(
new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!),
);

// 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 });
// Load inner iframe HTML with CSP, permissions, and sandbox metadata
const { html, csp, permissions, sandbox } = await appResourcePromise;
log.info("Sending UI resource HTML to MCP App", csp ? `(CSP: ${JSON.stringify(csp)})` : "", permissions ? `(Permissions: ${JSON.stringify(permissions)})` : "", sandbox ? `(Sandbox: ${JSON.stringify(sandbox)})` : "");
await appBridge.sendSandboxResourceReady({ html, csp, permissions, sandbox });

// Wait for inner iframe to be ready
log.info("Waiting for MCP App to initialize...");
Expand Down
6 changes: 3 additions & 3 deletions examples/basic-host/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -431,10 +431,10 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
useEffect(() => {
const iframe = iframeRef.current!;

// First get CSP and permissions from resource, then load sandbox
// First get CSP, permissions, and sandbox from resource, then load sandbox
// CSP is set via HTTP headers (tamper-proof), permissions via iframe allow attribute
toolCallInfo.appResourcePromise.then(({ csp, permissions }) => {
loadSandboxProxy(iframe, csp, permissions).then((firstTime) => {
toolCallInfo.appResourcePromise.then(({ csp, permissions, sandbox }) => {
loadSandboxProxy(iframe, csp, permissions, sandbox).then((firstTime) => {
// The `firstTime` check guards against React Strict Mode's double
// invocation (mount → unmount → remount simulation in development).
// Outside of Strict Mode, this `useEffect` runs only once per
Expand Down
13 changes: 8 additions & 5 deletions examples/basic-host/src/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "../../../dist/src/types";
import { buildAllowAttribute } from "../../../dist/src/app-bridge";
import { buildAllowAttribute, buildSandboxAttribute } from "../../../dist/src/app-bridge";

const ALLOWED_REFERRER_PATTERN = /^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/;

Expand Down Expand Up @@ -43,7 +43,7 @@ try {
// origins.
const inner = document.createElement("iframe");
inner.style = "width:100%; height:100%; border:none;";
inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
inner.setAttribute("sandbox", "allow-scripts allow-same-origin");
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
// based on the permissions requested by the app
document.body.appendChild(inner);
Expand Down Expand Up @@ -85,9 +85,12 @@ window.addEventListener("message", async (event) => {

if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
const { html, sandbox, permissions } = event.data.params;
if (typeof sandbox === "string") {
inner.setAttribute("sandbox", sandbox);
}
// sandbox can be a string (raw override) or object (structured flags)
const sandboxAttr = typeof sandbox === "string"
? sandbox
: buildSandboxAttribute(sandbox);
console.log("[Sandbox] Setting sandbox attribute:", sandboxAttr);
inner.setAttribute("sandbox", sandboxAttr);
// Set Permission Policy allow attribute if permissions are requested
const allowAttribute = buildAllowAttribute(permissions);
if (allowAttribute) {
Expand Down
22 changes: 0 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 49 additions & 1 deletion specification/draft/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,40 @@ interface UIResourceMeta {
*/
clipboardWrite?: {},
},
/**
* Sandbox flags requested by the UI
*
* Servers declare which sandbox capabilities their UI needs beyond baseline (allow-scripts, allow-same-origin).
* Hosts MAY honor these by adding flags to the iframe sandbox attribute.
* Apps SHOULD NOT assume flags are granted; use feature detection as fallback.
*
*/
sandbox?: {
/**
* Allow form submission
*
* Maps to sandbox `allow-forms` flag
*/
forms?: {},
/**
* Allow window.open popups
*
* Maps to sandbox `allow-popups` flag
*/
popups?: {},
/**
* Allow alert/confirm/prompt/print dialogs
*
* Maps to sandbox `allow-modals` flag
*/
modals?: {},
/**
* Allow file downloads
*
* Maps to sandbox `allow-downloads` flag
*/
downloads?: {},
},
/**
* Dedicated origin for view
*
Expand Down Expand Up @@ -480,6 +514,7 @@ If the Host is a web page, it MUST wrap the View and communicate with it through
- Block dangerous features (`object-src 'none'`)
- Apply restrictive defaults if no CSP metadata is provided
- If `permissions` is declared, the Sandbox MAY set the inner iframe's `allow` attribute accordingly
- If `sandbox` is declared (as object or string), the Sandbox MAY set the inner iframe's `sandbox` attribute accordingly (baseline: `allow-scripts allow-same-origin`)
6. The Sandbox MUST forward messages sent by the Host to the View, and vice versa, for any method that doesn't start with `ui/notifications/sandbox-`. This includes lifecycle messages, e.g., `ui/initialize` request & `ui/notifications/initialized` notification both sent by the View. The Host MUST NOT send any request or notification to the View before it receives an `initialized` notification.
7. The Sandbox SHOULD NOT create/send any requests to the Host or to the View (this would require synthesizing new request ids).
8. The Host MAY forward any message from the View (coming via the Sandbox) to the MCP Apps server, for any method that doesn't start with `ui/`. While the Host SHOULD ensure the View's MCP connection is spec-compliant, it MAY decide to block some messages or subject them to further user approval.
Expand Down Expand Up @@ -660,6 +695,13 @@ interface HostCapabilities {
/** Approved base URIs for the document (base-uri directive). */
baseUriDomains?: string[];
};
/** Sandbox flags granted by the host. */
flags?: {
forms?: {};
popups?: {};
modals?: {};
downloads?: {};
};
};
}
```
Expand Down Expand Up @@ -1260,7 +1302,13 @@ These messages are reserved for web-based hosts that implement the recommended d
microphone?: {},
geolocation?: {},
clipboardWrite?: {},
}
},
sandbox?: { // Sandbox flags from resource metadata (or raw string override)
forms?: {}, // Allow form submission (allow-forms)
popups?: {}, // Allow window.open popups (allow-popups)
modals?: {}, // Allow alert/confirm/prompt/print (allow-modals)
downloads?: {}, // Allow file downloads (allow-downloads)
} | string // Raw sandbox attribute override
}
}
```
Expand Down
Loading
Loading