diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 434169a9..32ca2ce7 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 0b4fda77..3a514542 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 1caf1c5f..da73e12b 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 89d82cfe..910edd01 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 1be80e76..f185dfc0 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. */ + flags?: { + 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 66d5f830..f924148a 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 d8f7dee1..f1b563ba 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 bdbb058e..164ef9de 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.", "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 95ec2f21..45b84026 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 eaf8278b..80418cf5 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 (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."), @@ -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. */ + sandbox: McpUiResourceSandboxSchema.optional().describe( + "Sandbox flags requested by the UI.", + ), /** @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 711d8f0d..cbe7cfd8 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; @@ -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. */ + 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 a4770fda..6abbb74a 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,