diff --git a/.playwright-mcp/page-2026-01-29T16-44-20-392Z.png b/.playwright-mcp/page-2026-01-29T16-44-20-392Z.png new file mode 100644 index 000000000..55c20c620 Binary files /dev/null and b/.playwright-mcp/page-2026-01-29T16-44-20-392Z.png differ diff --git a/.playwright-mcp/page-2026-01-29T16-45-07-754Z.png b/.playwright-mcp/page-2026-01-29T16-45-07-754Z.png new file mode 100644 index 000000000..481d0babb Binary files /dev/null and b/.playwright-mcp/page-2026-01-29T16-45-07-754Z.png differ diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 36ecee0d8..c1df5c1c1 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -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"; @@ -72,6 +72,7 @@ interface UiResourceData { html: string; csp?: McpUiResourceCsp; permissions?: McpUiResourcePermissions; + sandbox?: McpUiResourceSandbox; } export interface ToolCallInfo { @@ -151,8 +152,9 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise { // 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); @@ -214,10 +218,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..."); diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 3d488a792..4f787e4c0 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -432,10 +432,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 diff --git a/examples/basic-host/src/sandbox.ts b/examples/basic-host/src/sandbox.ts index 1caf1c5f1..da73e12b3 100644 --- a/examples/basic-host/src/sandbox.ts +++ b/examples/basic-host/src/sandbox.ts @@ -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)(:|\/|$)/; @@ -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); @@ -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) { diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index cb73cabe1..89a37c537 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -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 * @@ -500,6 +534,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. @@ -680,6 +715,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?: {}; + }; }; } ``` @@ -1280,7 +1322,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 } } ``` diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index ca38e2aa3..dc330aad8 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -17,6 +17,7 @@ import { App } from "./app"; import { AppBridge, getToolUiResourceUri, + buildSandboxAttribute, isToolVisibilityModelOnly, isToolVisibilityAppOnly, type McpUiHostCapabilities, @@ -937,6 +938,92 @@ describe("getToolUiResourceUri", () => { }); }); +describe("buildSandboxAttribute", () => { + const BASELINE = "allow-scripts allow-same-origin"; + + describe("baseline handling", () => { + it("returns baseline for undefined", () => { + expect(buildSandboxAttribute(undefined)).toBe(BASELINE); + }); + + it("returns baseline for empty object", () => { + expect(buildSandboxAttribute({})).toBe(BASELINE); + }); + }); + + describe("single flags", () => { + it("adds forms flag", () => { + expect(buildSandboxAttribute({ forms: {} })).toBe( + `${BASELINE} allow-forms`, + ); + }); + + it("adds popups flag", () => { + expect(buildSandboxAttribute({ popups: {} })).toBe( + `${BASELINE} allow-popups`, + ); + }); + + it("adds modals flag", () => { + expect(buildSandboxAttribute({ modals: {} })).toBe( + `${BASELINE} allow-modals`, + ); + }); + + it("adds downloads flag", () => { + expect(buildSandboxAttribute({ downloads: {} })).toBe( + `${BASELINE} allow-downloads`, + ); + }); + }); + + describe("multiple flags", () => { + it("adds multiple flags", () => { + const result = buildSandboxAttribute({ + forms: {}, + popups: {}, + modals: {}, + downloads: {}, + }); + expect(result).toContain("allow-forms"); + expect(result).toContain("allow-popups"); + expect(result).toContain("allow-modals"); + expect(result).toContain("allow-downloads"); + expect(result.startsWith(BASELINE)).toBe(true); + }); + + it("adds forms and popups", () => { + const result = buildSandboxAttribute({ forms: {}, popups: {} }); + expect(result).toContain("allow-forms"); + expect(result).toContain("allow-popups"); + expect(result).not.toContain("allow-modals"); + expect(result).not.toContain("allow-downloads"); + }); + }); + + describe("undefined values in object", () => { + it("ignores undefined values", () => { + const result = buildSandboxAttribute({ + forms: {}, + popups: undefined, + modals: undefined, + downloads: undefined, + }); + expect(result).toBe(`${BASELINE} allow-forms`); + }); + + it("returns baseline when all values are undefined", () => { + const result = buildSandboxAttribute({ + forms: undefined, + popups: undefined, + modals: undefined, + downloads: undefined, + }); + expect(result).toBe(BASELINE); + }); + }); +}); + describe("isToolVisibilityModelOnly", () => { describe("returns true", () => { it("when visibility is exactly ['model']", () => { diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 3322a0dda..ac6a8b116 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -78,6 +78,7 @@ import { McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResult, McpUiResourcePermissions, + McpUiResourceSandbox, McpUiToolMeta, } from "./types"; export * from "./types"; @@ -185,6 +186,53 @@ export function buildAllowAttribute( return allowList.join("; "); } +/** + * Mapping of McpUiResourceSandbox keys to sandbox attribute values. + * @internal + */ +const SANDBOX_FLAG_MAP: Record = { + forms: "allow-forms", + popups: "allow-popups", + modals: "allow-modals", + downloads: "allow-downloads", +}; + +/** + * Baseline sandbox flags always included - required for SDK operation. + * @internal + */ +const BASELINE_SANDBOX = "allow-scripts allow-same-origin"; + +/** + * Build iframe `sandbox` attribute string from sandbox configuration. + * + * Maps McpUiResourceSandbox to sandbox attribute format, always including + * baseline flags (allow-scripts allow-same-origin). + * + * @param sandbox - Sandbox flags requested by the UI resource + * @returns Space-separated sandbox flags including baseline + * + * @example + * ```typescript + * const sandbox = buildSandboxAttribute({ forms: {}, popups: {} }); + * // Returns: "allow-scripts allow-same-origin allow-forms allow-popups" + * iframe.setAttribute("sandbox", sandbox); + * ``` + */ +export function buildSandboxAttribute( + sandbox: McpUiResourceSandbox | undefined, +): string { + if (!sandbox) return BASELINE_SANDBOX; + + const additional = Object.entries(sandbox) + .filter(([_, v]) => v !== undefined) + .map(([k]) => SANDBOX_FLAG_MAP[k as keyof McpUiResourceSandbox]) + .filter(Boolean); + + if (additional.length === 0) return BASELINE_SANDBOX; + return [BASELINE_SANDBOX, ...additional].join(" "); +} + /** * Options for configuring {@link AppBridge `AppBridge`} behavior. * diff --git a/src/generated/schema.json b/src/generated/schema.json index d9e4b582c..b28d65208 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -194,6 +194,37 @@ } }, "additionalProperties": false + }, + "flags": { + "description": "Sandbox flags granted by the host (forms, popups, modals, downloads).", + "type": "object", + "properties": { + "forms": { + "description": "Allow form submission (sandbox `allow-forms` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "popups": { + "description": "Allow window.open popups (sandbox `allow-popups` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "modals": { + "description": "Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "downloads": { + "description": "Allow file downloads (sandbox `allow-downloads` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2540,6 +2571,37 @@ } }, "additionalProperties": false + }, + "flags": { + "description": "Sandbox flags granted by the host (forms, popups, modals, downloads).", + "type": "object", + "properties": { + "forms": { + "description": "Allow form submission (sandbox `allow-forms` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "popups": { + "description": "Allow window.open popups (sandbox `allow-popups` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "modals": { + "description": "Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "downloads": { + "description": "Allow file downloads (sandbox `allow-downloads` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -3974,6 +4036,37 @@ }, "additionalProperties": false }, + "sandbox": { + "description": "Sandbox flags requested by the UI.", + "type": "object", + "properties": { + "forms": { + "description": "Allow form submission (sandbox `allow-forms` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "popups": { + "description": "Allow window.open popups (sandbox `allow-popups` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "modals": { + "description": "Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "downloads": { + "description": "Allow file downloads (sandbox `allow-downloads` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "domain": { "description": "Dedicated origin for view sandbox.\n\nUseful when views need stable, dedicated origins for OAuth callbacks, CORS policies, or API key allowlists.\n\n**Host-dependent:** The format and validation rules for this field are determined by each host. Servers MUST consult host-specific documentation for the expected domain format. Common patterns include:\n- Hash-based subdomains (e.g., `{hash}.claudemcpcontent.com`)\n- URL-derived subdomains (e.g., `www-example-com.oaiusercontent.com`)\n\nIf omitted, host uses default sandbox origin (typically per-conversation).", "type": "string" @@ -4016,6 +4109,37 @@ }, "additionalProperties": false }, + "McpUiResourceSandbox": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "forms": { + "description": "Allow form submission (sandbox `allow-forms` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "popups": { + "description": "Allow window.open popups (sandbox `allow-popups` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "modals": { + "description": "Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "downloads": { + "description": "Allow file downloads (sandbox `allow-downloads` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "McpUiResourceTeardownRequest": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -4074,8 +4198,42 @@ "description": "HTML content to load into the inner iframe." }, "sandbox": { - "description": "Optional override for the inner iframe's sandbox attribute.", - "type": "string" + "description": "Sandbox configuration: structured flags object or raw attribute string override.", + "anyOf": [ + { + "type": "object", + "properties": { + "forms": { + "description": "Allow form submission (sandbox `allow-forms` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "popups": { + "description": "Allow window.open popups (sandbox `allow-popups` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "modals": { + "description": "Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "downloads": { + "description": "Allow file downloads (sandbox `allow-downloads` flag).", + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "string" + } + ] }, "csp": { "description": "CSP configuration from resource metadata.", diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 95ec2f216..45b840261 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -43,6 +43,10 @@ export type McpUiSandboxProxyReadyNotificationSchemaInferredType = z.infer< typeof generated.McpUiSandboxProxyReadyNotificationSchema >; +export type McpUiResourceSandboxSchemaInferredType = z.infer< + typeof generated.McpUiResourceSandboxSchema +>; + export type McpUiResourceCspSchemaInferredType = z.infer< typeof generated.McpUiResourceCspSchema >; @@ -187,6 +191,12 @@ expectType( expectType( {} as spec.McpUiSandboxProxyReadyNotification, ); +expectType( + {} as McpUiResourceSandboxSchemaInferredType, +); +expectType( + {} as spec.McpUiResourceSandbox, +); expectType({} as McpUiResourceCspSchemaInferredType); expectType({} as spec.McpUiResourceCsp); expectType( diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 9c75c3632..860828cdf 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -185,6 +185,37 @@ export const McpUiSandboxProxyReadyNotificationSchema = z.object({ params: z.object({}), }); +/** + * @description Sandbox flags requested by the UI resource. + * These control iframe sandbox attribute beyond the baseline (scripts, 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. + */ +export const McpUiResourceSandboxSchema = z.object({ + /** @description Allow form submission (sandbox `allow-forms` flag). */ + forms: z + .object({}) + .optional() + .describe("Allow form submission (sandbox `allow-forms` flag)."), + /** @description Allow window.open popups (sandbox `allow-popups` flag). */ + popups: z + .object({}) + .optional() + .describe("Allow window.open popups (sandbox `allow-popups` flag)."), + /** @description Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag). */ + modals: z + .object({}) + .optional() + .describe( + "Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag).", + ), + /** @description Allow file downloads (sandbox `allow-downloads` flag). */ + downloads: z + .object({}) + .optional() + .describe("Allow file downloads (sandbox `allow-downloads` flag)."), +}); + /** * @description Content Security Policy configuration for UI resources. * @@ -511,6 +542,10 @@ export const McpUiHostCapabilitiesSchema = z.object({ csp: McpUiResourceCspSchema.optional().describe( "CSP domains approved by the host.", ), + /** @description Sandbox flags granted by the host (forms, popups, modals, downloads). */ + flags: McpUiResourceSandboxSchema.optional().describe( + "Sandbox flags granted by the host (forms, popups, modals, downloads).", + ), }) .optional() .describe("Sandbox configuration applied by the host."), @@ -574,6 +609,10 @@ export const McpUiResourceMetaSchema = z.object({ permissions: McpUiResourcePermissionsSchema.optional().describe( "Sandbox permissions requested by the UI resource.", ), + /** @description Sandbox flags requested by the UI. */ + sandbox: McpUiResourceSandboxSchema.optional().describe( + "Sandbox flags requested by the UI.", + ), /** * @description Dedicated origin for view sandbox. * @@ -727,11 +766,13 @@ export const McpUiSandboxResourceReadyNotificationSchema = z.object({ params: z.object({ /** @description HTML content to load into the inner iframe. */ html: z.string().describe("HTML content to load into the inner iframe."), - /** @description Optional override for the inner iframe's sandbox attribute. */ + /** @description Sandbox configuration: structured flags object or raw attribute string override. */ sandbox: z - .string() + .union([McpUiResourceSandboxSchema, z.string()]) .optional() - .describe("Optional override for the inner iframe's sandbox attribute."), + .describe( + "Sandbox configuration: structured flags object or raw attribute string override.", + ), /** @description CSP configuration from resource metadata. */ csp: McpUiResourceCspSchema.optional().describe( "CSP configuration from resource metadata.", diff --git a/src/spec.types.ts b/src/spec.types.ts index 469ca1908..bfe7a219f 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -216,8 +216,8 @@ export interface McpUiSandboxResourceReadyNotification { params: { /** @description HTML content to load into the inner iframe. */ html: string; - /** @description Optional override for the inner iframe's sandbox attribute. */ - sandbox?: string; + /** @description Sandbox configuration: structured flags object or raw attribute string override. */ + sandbox?: McpUiResourceSandbox | string; /** @description CSP configuration from resource metadata. */ csp?: McpUiResourceCsp; /** @description Sandbox permissions from resource metadata. */ @@ -468,6 +468,8 @@ export interface McpUiHostCapabilities { permissions?: McpUiResourcePermissions; /** @description CSP domains approved by the host. */ csp?: McpUiResourceCsp; + /** @description Sandbox flags granted by the host (forms, popups, modals, downloads). */ + flags?: McpUiResourceSandbox; }; /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ updateModelContext?: McpUiSupportedContentBlockModalities; @@ -598,6 +600,23 @@ export interface McpUiResourceCsp { baseUriDomains?: string[]; } +/** + * @description Sandbox flags requested by the UI resource. + * These control iframe sandbox attribute beyond the baseline (scripts, 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. + */ +export interface McpUiResourceSandbox { + /** @description Allow form submission (sandbox `allow-forms` flag). */ + forms?: {}; + /** @description Allow window.open popups (sandbox `allow-popups` flag). */ + popups?: {}; + /** @description Allow alert/confirm/prompt/print dialogs (sandbox `allow-modals` flag). */ + modals?: {}; + /** @description Allow file downloads (sandbox `allow-downloads` flag). */ + downloads?: {}; +} + /** * @description Sandbox permissions requested by the UI resource. * @@ -640,6 +659,8 @@ export interface McpUiResourceMeta { csp?: McpUiResourceCsp; /** @description Sandbox permissions requested by the UI resource. */ permissions?: McpUiResourcePermissions; + /** @description Sandbox flags requested by the UI. */ + sandbox?: McpUiResourceSandbox; /** * @description Dedicated origin for view sandbox. * diff --git a/src/types.ts b/src/types.ts index a4770fdaf..6abbb74a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,7 @@ export { type McpUiInitializedNotification, type McpUiResourceCsp, type McpUiResourcePermissions, + type McpUiResourceSandbox, type McpUiResourceMeta, type McpUiRequestDisplayModeRequest, type McpUiRequestDisplayModeResult, @@ -118,6 +119,7 @@ export { McpUiInitializedNotificationSchema, McpUiResourceCspSchema, McpUiResourcePermissionsSchema, + McpUiResourceSandboxSchema, McpUiResourceMetaSchema, McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResultSchema,