From f37e9854da23fe2885960b069e1cf78624dcceb2 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sun, 25 Jan 2026 23:28:00 +0200 Subject: [PATCH 1/3] feat: add sandbox flags negotiation --- examples/basic-host/src/implementation.ts | 18 ++- examples/basic-host/src/index.tsx | 6 +- examples/basic-host/src/sandbox.ts | 13 +- package-lock.json | 22 --- specification/draft/apps.mdx | 50 ++++++- src/app-bridge.test.ts | 87 ++++++++++++ src/app-bridge.ts | 48 +++++++ src/generated/schema.json | 162 +++++++++++++++++++++- src/generated/schema.test.ts | 10 ++ src/generated/schema.ts | 47 ++++++- src/spec.types.ts | 25 +++- src/types.ts | 2 + 12 files changed, 445 insertions(+), 45 deletions(-) diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 434169a95..32ca2ce7f 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"; @@ -66,6 +66,7 @@ interface UiResourceData { html: string; csp?: McpUiResourceCsp; permissions?: McpUiResourcePermissions; + sandbox?: McpUiResourceSandbox; } export interface ToolCallInfo { @@ -136,8 +137,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); @@ -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..."); diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 0b4fda774..3a5145429 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -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 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/package-lock.json b/package-lock.json index 89d82cfef..910edd016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -937,7 +937,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -2402,7 +2401,6 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", "license": "MIT", - "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -3482,7 +3480,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -3689,7 +3686,6 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3714,7 +3710,6 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4117,7 +4112,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4459,7 +4453,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5175,7 +5168,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5648,7 +5640,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7198,7 +7189,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -7408,7 +7398,6 @@ "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7530,7 +7519,6 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -7821,7 +7809,6 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", @@ -8000,7 +7987,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.48.0.tgz", "integrity": "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -8328,7 +8314,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9028,7 +9013,6 @@ "integrity": "sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@gerrit0/mini-shiki": "^3.17.0", "lunr": "^2.3.9", @@ -9092,7 +9076,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9186,7 +9169,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9363,7 +9345,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9482,7 +9463,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", @@ -9643,7 +9623,6 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9694,7 +9673,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 1be80e76f..82edccd51 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 * @@ -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. @@ -660,6 +695,13 @@ interface HostCapabilities { /** Approved base URIs for the document (base-uri directive). */ baseUriDomains?: string[]; }; + /** Sandbox flags granted by the host. */ + sandbox?: { + forms?: {}; + popups?: {}; + modals?: {}; + downloads?: {}; + }; }; } ``` @@ -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 } } ``` diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 66d5f8306..f924148a5 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, type McpUiHostCapabilities, } from "./app-bridge"; @@ -921,3 +922,89 @@ 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); + }); + }); +}); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index d8f7dee1b..f1b563bae 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -78,6 +78,7 @@ import { McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResult, McpUiResourcePermissions, + McpUiResourceSandbox, } from "./types"; export * from "./types"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; @@ -156,6 +157,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 bdbb058ef..8c9e1e74f 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -194,6 +194,37 @@ } }, "additionalProperties": false + }, + "sandbox": { + "description": "Sandbox flags granted by the host (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 + }, + "sandbox": { + "description": "Sandbox flags granted by the host (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 (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 + }, "domain": { "description": "Dedicated origin for view sandbox.", "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 eaf8278be..6b5922f0b 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. */ @@ -435,6 +466,10 @@ export const McpUiHostCapabilitiesSchema = z.object({ csp: McpUiResourceCspSchema.optional().describe( "CSP domains approved by the host.", ), + /** @description Sandbox flags granted by the host (popups, modals, downloads). */ + sandbox: McpUiResourceSandboxSchema.optional().describe( + "Sandbox flags granted by the host (popups, modals, downloads).", + ), }) .optional() .describe("Sandbox configuration applied by the host."), @@ -498,6 +533,10 @@ export const McpUiResourceMetaSchema = z.object({ permissions: McpUiResourcePermissionsSchema.optional().describe( "Sandbox permissions requested by the UI.", ), + /** @description Sandbox flags requested by the UI (popups, modals, downloads). */ + sandbox: McpUiResourceSandboxSchema.optional().describe( + "Sandbox flags requested by the UI (popups, modals, downloads).", + ), /** @description Dedicated origin for view sandbox. */ domain: z.string().optional().describe("Dedicated origin for view sandbox."), /** @description Visual boundary preference - true if UI prefers a visible border. */ @@ -615,11 +654,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 711d8f0dd..290ed4f94 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 (popups, modals, downloads). */ + sandbox?: McpUiResourceSandbox; }; /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ updateModelContext?: McpUiSupportedContentBlockModalities; @@ -550,6 +552,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. * Hosts MAY honor these by setting appropriate iframe `allow` attributes. @@ -574,6 +593,8 @@ export interface McpUiResourceMeta { csp?: McpUiResourceCsp; /** @description Sandbox permissions requested by the UI. */ permissions?: McpUiResourcePermissions; + /** @description Sandbox flags requested by the UI (popups, modals, downloads). */ + sandbox?: McpUiResourceSandbox; /** @description Dedicated origin for view sandbox. */ domain?: string; /** @description Visual boundary preference - true if UI prefers a visible border. */ 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, From 5895acd4314d7e5bc392f43b2f5a40ef4cbf39ee Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sun, 25 Jan 2026 23:36:03 +0200 Subject: [PATCH 2/3] rename to flags --- specification/draft/apps.mdx | 2 +- src/generated/schema.json | 8 ++++---- src/generated/schema.ts | 6 +++--- src/spec.types.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 82edccd51..f185dfc01 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -696,7 +696,7 @@ interface HostCapabilities { baseUriDomains?: string[]; }; /** Sandbox flags granted by the host. */ - sandbox?: { + flags?: { forms?: {}; popups?: {}; modals?: {}; diff --git a/src/generated/schema.json b/src/generated/schema.json index 8c9e1e74f..06f7e4ee3 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -195,8 +195,8 @@ }, "additionalProperties": false }, - "sandbox": { - "description": "Sandbox flags granted by the host (popups, modals, downloads).", + "flags": { + "description": "Sandbox flags granted by the host (forms, popups, modals, downloads).", "type": "object", "properties": { "forms": { @@ -2572,8 +2572,8 @@ }, "additionalProperties": false }, - "sandbox": { - "description": "Sandbox flags granted by the host (popups, modals, downloads).", + "flags": { + "description": "Sandbox flags granted by the host (forms, popups, modals, downloads).", "type": "object", "properties": { "forms": { diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 6b5922f0b..335c4fb72 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -466,9 +466,9 @@ export const McpUiHostCapabilitiesSchema = z.object({ csp: McpUiResourceCspSchema.optional().describe( "CSP domains approved by the host.", ), - /** @description Sandbox flags granted by the host (popups, modals, downloads). */ - sandbox: McpUiResourceSandboxSchema.optional().describe( - "Sandbox flags granted by the host (popups, modals, downloads).", + /** @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() diff --git a/src/spec.types.ts b/src/spec.types.ts index 290ed4f94..64ed3b13a 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -468,8 +468,8 @@ export interface McpUiHostCapabilities { permissions?: McpUiResourcePermissions; /** @description CSP domains approved by the host. */ csp?: McpUiResourceCsp; - /** @description Sandbox flags granted by the host (popups, modals, downloads). */ - sandbox?: McpUiResourceSandbox; + /** @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; From 4c75c0acc1d01fe72fec2c23f326e1216d9bc321 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Mon, 26 Jan 2026 00:25:49 +0200 Subject: [PATCH 3/3] remove examples --- src/generated/schema.json | 2 +- src/generated/schema.ts | 4 ++-- src/spec.types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 06f7e4ee3..164ef9ded 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -4037,7 +4037,7 @@ "additionalProperties": false }, "sandbox": { - "description": "Sandbox flags requested by the UI (popups, modals, downloads).", + "description": "Sandbox flags requested by the UI.", "type": "object", "properties": { "forms": { diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 335c4fb72..80418cf5d 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -533,9 +533,9 @@ export const McpUiResourceMetaSchema = z.object({ permissions: McpUiResourcePermissionsSchema.optional().describe( "Sandbox permissions requested by the UI.", ), - /** @description Sandbox flags requested by the UI (popups, modals, downloads). */ + /** @description Sandbox flags requested by the UI. */ sandbox: McpUiResourceSandboxSchema.optional().describe( - "Sandbox flags requested by the UI (popups, modals, downloads).", + "Sandbox flags requested by the UI.", ), /** @description Dedicated origin for view sandbox. */ domain: z.string().optional().describe("Dedicated origin for view sandbox."), diff --git a/src/spec.types.ts b/src/spec.types.ts index 64ed3b13a..cbe7cfd8a 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -593,7 +593,7 @@ export interface McpUiResourceMeta { csp?: McpUiResourceCsp; /** @description Sandbox permissions requested by the UI. */ permissions?: McpUiResourcePermissions; - /** @description Sandbox flags requested by the UI (popups, modals, downloads). */ + /** @description Sandbox flags requested by the UI. */ sandbox?: McpUiResourceSandbox; /** @description Dedicated origin for view sandbox. */ domain?: string;