From 699549ee3c14ccbf95949ecbe637c10374e6eaf7 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Tue, 16 Dec 2025 00:36:04 +0200 Subject: [PATCH 1/3] feat: enhance sandbox capability negotiation --- examples/basic-host/src/implementation.ts | 20 +++- examples/basic-host/src/sandbox.ts | 39 ++++++- examples/simple-host/sandbox.html | 112 ++++++++++++++++++ specification/draft/apps.mdx | 87 +++++++++++++- src/generated/schema.json | 129 +++++++++++++++++++++ src/generated/schema.test.ts | 22 +++- src/generated/schema.ts | 135 +++++++++++++++++----- src/spec.types.ts | 33 ++++++ src/types.ts | 2 + 9 files changed, 525 insertions(+), 54 deletions(-) create mode 100644 examples/simple-host/sandbox.html diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 9ab57591..331c1995 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -45,6 +45,13 @@ interface UiResourceData { csp?: { connectDomains?: string[]; resourceDomains?: string[]; + frameDomains?: string[]; + baseUriDomains?: string[]; + }; + permissions?: { + camera?: boolean; + microphone?: boolean; + geolocation?: boolean; }; } @@ -118,15 +125,16 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise`; } +// Build iframe allow attribute from permissions +function buildAllowAttribute(permissions?: { + camera?: boolean; + microphone?: boolean; + geolocation?: boolean; +}): string { + if (!permissions) return ""; + + const allowList: string[] = []; + if (permissions.camera) allowList.push("camera"); + if (permissions.microphone) allowList.push("microphone"); + if (permissions.geolocation) allowList.push("geolocation"); + + return allowList.join("; "); +} + window.addEventListener("message", async (event) => { if (event.source === window.parent) { // NOTE: In production you'll also want to validate `event.origin` against // your Host domain. if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) { - const { html, sandbox, csp } = event.data.params; + const { html, sandbox, csp, permissions } = event.data.params; if (typeof sandbox === "string") { inner.setAttribute("sandbox", sandbox); } + // Set Permission Policy allow attribute if permissions are requested + const allowAttribute = buildAllowAttribute(permissions); + if (allowAttribute) { + console.log("[Sandbox] Setting allow attribute:", allowAttribute); + inner.setAttribute("allow", allowAttribute); + } if (typeof html === "string") { // Inject CSP meta tag at the start of if CSP is provided console.log("[Sandbox] Received CSP:", csp); diff --git a/examples/simple-host/sandbox.html b/examples/simple-host/sandbox.html new file mode 100644 index 00000000..2937aff5 --- /dev/null +++ b/examples/simple-host/sandbox.html @@ -0,0 +1,112 @@ + + + + + + + + MCP-UI Proxy + + + + + + diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index d9485675..89076c15 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -138,6 +138,53 @@ interface UIResourceMeta { * ["https://cdn.jsdelivr.net", "https://*.cloudflare.com"] */ resourceDomains?: string[], + /** + * Origins for nested iframes + * + * - Empty or omitted = no nested iframes allowed (`frame-src 'none'`) + * - Maps to CSP `frame-src` directive + * + * @example + * ["https://www.youtube.com", "https://player.vimeo.com"] + */ + frameDomains?: string[], + /** + * Allowed base URIs for the document + * + * - Empty or omitted = only same origin allowed (`base-uri 'self'`) + * - Maps to CSP `base-uri` directive + * + * @example + * ["https://cdn.example.com"] + */ + baseUriDomains?: string[], + }, + /** + * Sandbox permissions requested by the UI + * + * Servers declare which browser capabilities their UI needs. + * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. + */ + permissions?: { + /** + * Request camera access + * + * Maps to Permission Policy `camera` feature + */ + camera?: boolean, + /** + * Request microphone access + * + * Maps to Permission Policy `microphone` feature + */ + microphone?: boolean, + /** + * Request geolocation access + * + * Maps to Permission Policy `geolocation` feature + */ + geolocation?: boolean, }, /** * Dedicated origin for widget @@ -180,6 +227,13 @@ The resource content is returned via `resources/read`: csp?: { connectDomains?: string[]; // Origins for fetch/XHR/WebSocket resourceDomains?: string[]; // Origins for images, scripts, etc + frameDomains?: string[]; // Origins for nested iframes + baseUriDomains?: string[]; // Allowed base URIs + }; + permissions?: { + camera?: boolean; // Request camera access + microphone?: boolean; // Request microphone access + geolocation?: boolean; // Request geolocation access }; domain?: string; prefersBorder?: boolean; @@ -362,9 +416,11 @@ If the Host is a web page, it MUST wrap the Guest UI and communicate with it thr 4. Once the Sandbox is ready, the Host MUST send the raw HTML resource to load in a `ui/notifications/sandbox-resource-ready` notification. 5. The Sandbox MUST load the raw HTML of the Guest UI with CSP settings that: - Enforce the domains declared in `ui.csp` metadata - - Prevent nested iframes (`frame-src 'none'`) - - Block dangerous features (`object-src 'none'`, `base-uri 'self'`) + - If `frameDomains` is provided, allow nested iframes from declared origins; otherwise use `frame-src 'none'` + - If `baseUriDomains` is provided, allow base URIs from declared origins; otherwise use `base-uri 'self'` + - 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 6. The Sandbox MUST forward messages sent by the Host to the Guest UI, 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 Guest UI. The Host MUST NOT send any request or notification to the Guest UI before it receives an `initialized` notification. 7. The Sandbox SHOULD NOT create/send any requests to the Host or to the Guest UI (this would require synthesizing new request ids). 8. The Host MAY forward any message from the Guest UI (coming via the Sandbox) to the MCP Apps server, for any method that doesn’t start with `ui/`. While the Host SHOULD ensure the Guest UI’s MCP connection is spec-compliant, it MAY decide to block some messages or subject them to further user approval. @@ -826,12 +882,23 @@ These messages are reserved for web-based hosts that implement the recommended d method: "ui/notifications/sandbox-resource-ready", params: { html: string, // HTML content to load - sandbox: string // Optional override for inner iframe `sandbox` attribute + sandbox?: string, // Optional override for inner iframe `sandbox` attribute + csp?: { // CSP configuration from resource metadata + connectDomains?: string[], + resourceDomains?: string[], + frameDomains?: string[], + baseUriDomains?: string[], + }, + permissions?: { // Sandbox permissions from resource metadata + camera?: boolean, + microphone?: boolean, + geolocation?: boolean, + } } } ``` -These messages facilitate the communication between the outer sandbox proxy iframe and the host, enabling secure loading of untrusted HTML content. +These messages facilitate the communication between the outer sandbox proxy iframe and the host, enabling secure loading of untrusted HTML content. The `permissions` field maps to the inner iframe's `allow` attribute for Permission Policy features. ### Lifecycle @@ -1269,6 +1336,7 @@ Hosts MUST enforce Content Security Policies based on resource metadata. ```typescript const csp = resource._meta?.ui?.csp; +const permissions = resource._meta?.ui?.permissions; const cspValue = ` default-src 'none'; @@ -1278,10 +1346,17 @@ const cspValue = ` img-src 'self' data: ${csp?.resourceDomains?.join(' ') || ''}; font-src 'self' ${csp?.resourceDomains?.join(' ') || ''}; media-src 'self' data: ${csp?.resourceDomains?.join(' ') || ''}; - frame-src 'none'; + frame-src ${csp?.frameDomains?.join(' ') || "'none'"}; object-src 'none'; - base-uri 'self'; + base-uri ${csp?.baseUriDomains?.join(' ') || "'self'"}; `; + +// Permission Policy for iframe allow attribute +const allowList: string[] = []; +if (permissions?.camera) allowList.push('camera'); +if (permissions?.microphone) allowList.push('microphone'); +if (permissions?.geolocation) allowList.push('geolocation'); +const allowAttribute = allowList.join('; '); ``` **Security Requirements:** diff --git a/src/generated/schema.json b/src/generated/schema.json index c85ab081..59473696 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -89,6 +89,21 @@ "type": "object", "properties": {}, "additionalProperties": false + }, + "csp": { + "description": "CSP overrides the host supports for sandbox proxies.", + "type": "object", + "properties": { + "frameDomains": { + "description": "Host supports frame-src domain allowlisting.", + "type": "boolean" + }, + "baseUriDomains": { + "description": "Host supports base-uri domain allowlisting.", + "type": "boolean" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1826,6 +1841,21 @@ "type": "object", "properties": {}, "additionalProperties": false + }, + "csp": { + "description": "CSP overrides the host supports for sandbox proxies.", + "type": "object", + "properties": { + "frameDomains": { + "description": "Host supports frame-src domain allowlisting.", + "type": "boolean" + }, + "baseUriDomains": { + "description": "Host supports base-uri domain allowlisting.", + "type": "boolean" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2862,6 +2892,20 @@ "items": { "type": "string" } + }, + "frameDomains": { + "description": "Origins for nested iframes (frame-src directive).", + "type": "array", + "items": { + "type": "string" + } + }, + "baseUriDomains": { + "description": "Allowed base URIs for the document (base-uri directive).", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -2887,6 +2931,39 @@ "items": { "type": "string" } + }, + "frameDomains": { + "description": "Origins for nested iframes (frame-src directive).", + "type": "array", + "items": { + "type": "string" + } + }, + "baseUriDomains": { + "description": "Allowed base URIs for the document (base-uri directive).", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "permissions": { + "description": "Sandbox permissions requested by the UI.", + "type": "object", + "properties": { + "camera": { + "description": "Request camera access (Permission Policy `camera` feature).", + "type": "boolean" + }, + "microphone": { + "description": "Request microphone access (Permission Policy `microphone` feature).", + "type": "boolean" + }, + "geolocation": { + "description": "Request geolocation access (Permission Policy `geolocation` feature).", + "type": "boolean" } }, "additionalProperties": false @@ -2902,6 +2979,25 @@ }, "additionalProperties": false }, + "McpUiResourcePermissions": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "camera": { + "description": "Request camera access (Permission Policy `camera` feature).", + "type": "boolean" + }, + "microphone": { + "description": "Request microphone access (Permission Policy `microphone` feature).", + "type": "boolean" + }, + "geolocation": { + "description": "Request geolocation access (Permission Policy `geolocation` feature).", + "type": "boolean" + } + }, + "additionalProperties": false + }, "McpUiResourceTeardownRequest": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -2980,6 +3076,39 @@ "items": { "type": "string" } + }, + "frameDomains": { + "description": "Origins for nested iframes (frame-src directive).", + "type": "array", + "items": { + "type": "string" + } + }, + "baseUriDomains": { + "description": "Allowed base URIs for the document (base-uri directive).", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "permissions": { + "description": "Sandbox permissions from resource metadata.", + "type": "object", + "properties": { + "camera": { + "description": "Request camera access (Permission Policy `camera` feature).", + "type": "boolean" + }, + "microphone": { + "description": "Request microphone access (Permission Policy `microphone` feature).", + "type": "boolean" + }, + "geolocation": { + "description": "Request geolocation access (Permission Policy `geolocation` feature).", + "type": "boolean" } }, "additionalProperties": false diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 47f2a37c..92fd1ebd 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -43,8 +43,8 @@ export type McpUiSandboxProxyReadyNotificationSchemaInferredType = z.infer< typeof generated.McpUiSandboxProxyReadyNotificationSchema >; -export type McpUiSandboxResourceReadyNotificationSchemaInferredType = z.infer< - typeof generated.McpUiSandboxResourceReadyNotificationSchema +export type McpUiResourcePermissionsSchemaInferredType = z.infer< + typeof generated.McpUiResourcePermissionsSchema >; export type McpUiSizeChangedNotificationSchemaInferredType = z.infer< @@ -99,6 +99,10 @@ export type McpUiMessageRequestSchemaInferredType = z.infer< typeof generated.McpUiMessageRequestSchema >; +export type McpUiSandboxResourceReadyNotificationSchemaInferredType = z.infer< + typeof generated.McpUiSandboxResourceReadyNotificationSchema +>; + export type McpUiToolResultNotificationSchemaInferredType = z.infer< typeof generated.McpUiToolResultNotificationSchema >; @@ -151,11 +155,11 @@ expectType( expectType( {} as spec.McpUiSandboxProxyReadyNotification, ); -expectType( - {} as McpUiSandboxResourceReadyNotificationSchemaInferredType, +expectType( + {} as McpUiResourcePermissionsSchemaInferredType, ); -expectType( - {} as spec.McpUiSandboxResourceReadyNotification, +expectType( + {} as spec.McpUiResourcePermissions, ); expectType( {} as McpUiSizeChangedNotificationSchemaInferredType, @@ -223,6 +227,12 @@ expectType( expectType( {} as spec.McpUiMessageRequest, ); +expectType( + {} as McpUiSandboxResourceReadyNotificationSchemaInferredType, +); +expectType( + {} as spec.McpUiSandboxResourceReadyNotification, +); expectType( {} as McpUiToolResultNotificationSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 84682286..0d3751ec 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -180,39 +180,30 @@ export const McpUiSandboxProxyReadyNotificationSchema = z.object({ }); /** - * @description Notification containing HTML resource for the sandbox proxy to load. - * @internal - * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy + * @description Sandbox permissions requested by the UI resource. + * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. */ -export const McpUiSandboxResourceReadyNotificationSchema = z.object({ - method: z.literal("ui/notifications/sandbox-resource-ready"), - 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. */ - sandbox: z - .string() - .optional() - .describe("Optional override for the inner iframe's sandbox attribute."), - /** @description CSP configuration from resource metadata. */ - csp: z - .object({ - /** @description Origins for network requests (fetch/XHR/WebSocket). */ - connectDomains: z - .array(z.string()) - .optional() - .describe("Origins for network requests (fetch/XHR/WebSocket)."), - /** @description Origins for static resources (scripts, images, styles, fonts). */ - resourceDomains: z - .array(z.string()) - .optional() - .describe( - "Origins for static resources (scripts, images, styles, fonts).", - ), - }) - .optional() - .describe("CSP configuration from resource metadata."), - }), +export const McpUiResourcePermissionsSchema = z.object({ + /** @description Request camera access (Permission Policy `camera` feature). */ + camera: z + .boolean() + .optional() + .describe("Request camera access (Permission Policy `camera` feature)."), + /** @description Request microphone access (Permission Policy `microphone` feature). */ + microphone: z + .boolean() + .optional() + .describe( + "Request microphone access (Permission Policy `microphone` feature).", + ), + /** @description Request geolocation access (Permission Policy `geolocation` feature). */ + geolocation: z + .boolean() + .optional() + .describe( + "Request geolocation access (Permission Policy `geolocation` feature).", + ), }); /** @@ -352,6 +343,22 @@ export const McpUiHostCapabilitiesSchema = z.object({ .describe("Host can proxy resource reads to the MCP server."), /** @description Host accepts log messages. */ logging: z.object({}).optional().describe("Host accepts log messages."), + /** @description CSP overrides the host supports for sandbox proxies. */ + csp: z + .object({ + /** @description Host supports frame-src domain allowlisting. */ + frameDomains: z + .boolean() + .optional() + .describe("Host supports frame-src domain allowlisting."), + /** @description Host supports base-uri domain allowlisting. */ + baseUriDomains: z + .boolean() + .optional() + .describe("Host supports base-uri domain allowlisting."), + }) + .optional() + .describe("CSP overrides the host supports for sandbox proxies."), }); /** @@ -400,6 +407,16 @@ export const McpUiResourceCspSchema = z.object({ .array(z.string()) .optional() .describe("Origins for static resources (scripts, images, styles, fonts)."), + /** @description Origins for nested iframes (frame-src directive). */ + frameDomains: z + .array(z.string()) + .optional() + .describe("Origins for nested iframes (frame-src directive)."), + /** @description Allowed base URIs for the document (base-uri directive). */ + baseUriDomains: z + .array(z.string()) + .optional() + .describe("Allowed base URIs for the document (base-uri directive)."), }); /** @@ -410,6 +427,10 @@ export const McpUiResourceMetaSchema = z.object({ csp: McpUiResourceCspSchema.optional().describe( "Content Security Policy configuration.", ), + /** @description Sandbox permissions requested by the UI. */ + permissions: McpUiResourcePermissionsSchema.optional().describe( + "Sandbox permissions requested by the UI.", + ), /** @description Dedicated origin for widget sandbox. */ domain: z .string() @@ -442,6 +463,56 @@ export const McpUiMessageRequestSchema = z.object({ }), }); +/** + * @description Notification containing HTML resource for the sandbox proxy to load. + * @internal + * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy + */ +export const McpUiSandboxResourceReadyNotificationSchema = z.object({ + method: z.literal("ui/notifications/sandbox-resource-ready"), + 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. */ + sandbox: z + .string() + .optional() + .describe("Optional override for the inner iframe's sandbox attribute."), + /** @description CSP configuration from resource metadata. */ + csp: z + .object({ + /** @description Origins for network requests (fetch/XHR/WebSocket). */ + connectDomains: z + .array(z.string()) + .optional() + .describe("Origins for network requests (fetch/XHR/WebSocket)."), + /** @description Origins for static resources (scripts, images, styles, fonts). */ + resourceDomains: z + .array(z.string()) + .optional() + .describe( + "Origins for static resources (scripts, images, styles, fonts).", + ), + /** @description Origins for nested iframes (frame-src directive). */ + frameDomains: z + .array(z.string()) + .optional() + .describe("Origins for nested iframes (frame-src directive)."), + /** @description Allowed base URIs for the document (base-uri directive). */ + baseUriDomains: z + .array(z.string()) + .optional() + .describe("Allowed base URIs for the document (base-uri directive)."), + }) + .optional() + .describe("CSP configuration from resource metadata."), + /** @description Sandbox permissions from resource metadata. */ + permissions: McpUiResourcePermissionsSchema.optional().describe( + "Sandbox permissions from resource metadata.", + ), + }), +}); + /** * @description Notification containing tool execution result (Host -> Guest UI). */ diff --git a/src/spec.types.ts b/src/spec.types.ts index 2f1d2533..2af92d3d 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -222,7 +222,13 @@ export interface McpUiSandboxResourceReadyNotification { connectDomains?: string[]; /** @description Origins for static resources (scripts, images, styles, fonts). */ resourceDomains?: string[]; + /** @description Origins for nested iframes (frame-src directive). */ + frameDomains?: string[]; + /** @description Allowed base URIs for the document (base-uri directive). */ + baseUriDomains?: string[]; }; + /** @description Sandbox permissions from resource metadata. */ + permissions?: McpUiResourcePermissions; }; } @@ -403,6 +409,13 @@ export interface McpUiHostCapabilities { }; /** @description Host accepts log messages. */ logging?: {}; + /** @description CSP overrides the host supports for sandbox proxies. */ + csp?: { + /** @description Host supports frame-src domain allowlisting. */ + frameDomains?: boolean; + /** @description Host supports base-uri domain allowlisting. */ + baseUriDomains?: boolean; + }; } /** @@ -472,6 +485,24 @@ export interface McpUiResourceCsp { connectDomains?: string[]; /** @description Origins for static resources (scripts, images, styles, fonts). */ resourceDomains?: string[]; + /** @description Origins for nested iframes (frame-src directive). */ + frameDomains?: string[]; + /** @description Allowed base URIs for the document (base-uri directive). */ + baseUriDomains?: string[]; +} + +/** + * @description Sandbox permissions requested by the UI resource. + * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. + */ +export interface McpUiResourcePermissions { + /** @description Request camera access (Permission Policy `camera` feature). */ + camera?: boolean; + /** @description Request microphone access (Permission Policy `microphone` feature). */ + microphone?: boolean; + /** @description Request geolocation access (Permission Policy `geolocation` feature). */ + geolocation?: boolean; } /** @@ -480,6 +511,8 @@ export interface McpUiResourceCsp { export interface McpUiResourceMeta { /** @description Content Security Policy configuration. */ csp?: McpUiResourceCsp; + /** @description Sandbox permissions requested by the UI. */ + permissions?: McpUiResourcePermissions; /** @description Dedicated origin for widget 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 ac6eb1b5..dd501203 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,7 @@ export { type McpUiInitializeResult, type McpUiInitializedNotification, type McpUiResourceCsp, + type McpUiResourcePermissions, type McpUiResourceMeta, } from "./spec.types.js"; @@ -88,6 +89,7 @@ export { McpUiInitializeResultSchema, McpUiInitializedNotificationSchema, McpUiResourceCspSchema, + McpUiResourcePermissionsSchema, McpUiResourceMetaSchema, } from "./generated/schema.js"; From 7aef0d8ece0d881c83ccc0ff99d486c53644447e Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Tue, 16 Dec 2025 02:39:41 +0200 Subject: [PATCH 2/3] Update specification/draft/apps.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- specification/draft/apps.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 89076c15..38124157 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1356,7 +1356,7 @@ const allowList: string[] = []; if (permissions?.camera) allowList.push('camera'); if (permissions?.microphone) allowList.push('microphone'); if (permissions?.geolocation) allowList.push('geolocation'); -const allowAttribute = allowList.join('; '); +const allowAttribute = allowList.join(' '); ``` **Security Requirements:** From e038020b50c85a3250b68a67b15a03a7553e48b7 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Thu, 18 Dec 2025 22:31:00 +0200 Subject: [PATCH 3/3] add all csp and permissions to host capabilities + update SEP --- examples/basic-host/package.json | 1 + examples/basic-server-react/package.json | 1 + examples/basic-server-vanillajs/package.json | 1 + examples/budget-allocator-server/package.json | 1 + examples/cohort-heatmap-server/package.json | 1 + .../customer-segmentation-server/package.json | 1 + examples/scenario-modeler-server/package.json | 1 + examples/simple-host/sandbox.html | 18 ++- examples/system-monitor-server/package.json | 1 + examples/threejs-server/package.json | 1 + examples/wiki-explorer-server/package.json | 1 + package-lock.json | 24 ++-- specification/draft/apps.mdx | 46 +++++++ src/generated/schema.json | 124 +++++++++++++++--- src/generated/schema.test.ts | 28 ++-- src/generated/schema.ts | 122 +++++++++-------- src/spec.types.ts | 12 +- 17 files changed, 271 insertions(+), 113 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 78cf826f..cb775a64 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -25,6 +25,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "prettier": "^3.6.2", "vite": "^6.0.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 5927040c..e21d4484 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -26,6 +26,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 5bc5a71c..5554546e 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -21,6 +21,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 86a95100..6a1f325c 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -25,6 +25,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index aff80002..499f46a8 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -29,6 +29,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index b8b7f197..157efa01 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -25,6 +25,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 95c13fc1..33b14c58 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -30,6 +30,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/simple-host/sandbox.html b/examples/simple-host/sandbox.html index 2937aff5..14849447 100644 --- a/examples/simple-host/sandbox.html +++ b/examples/simple-host/sandbox.html @@ -57,10 +57,21 @@ inner.setAttribute('sandbox', 'allow-scripts allow-same-origin'); document.body.appendChild(inner); + // Build iframe allow attribute from permissions + function buildAllowAttribute(permissions) { + if (!permissions) return ''; + const allowList = []; + if (permissions.camera) allowList.push('camera'); + if (permissions.microphone) allowList.push('microphone'); + if (permissions.geolocation) allowList.push('geolocation'); + return allowList.join('; '); + } + window.addEventListener('message', async (event) => { if (event.source === window.parent) { if (event.data && event.data.method === 'ui/notifications/sandbox-resource-ready') { - const { html, sandbox } = event.data.params || {}; + const { html, sandbox, permissions } = event.data.params || {}; + // Note: csp is not extracted here - CSP is set via HTTP response headers in serve.ts if (typeof sandbox === 'string') { // Ensure allow-same-origin is present for document.write to work let finalSandbox = sandbox; @@ -69,6 +80,11 @@ } inner.setAttribute('sandbox', finalSandbox); } + // Set Permission Policy allow attribute if permissions are provided + const allowAttribute = buildAllowAttribute(permissions); + if (allowAttribute) { + inner.setAttribute('allow', allowAttribute); + } if (typeof html === 'string') { // Use document.write instead of srcdoc to avoid CSP base-uri issues // document.write allows the browser to resolve relative URLs correctly diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 6adfbb91..485df6f5 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -26,6 +26,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index bdc00f55..e88463a8 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -31,6 +31,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 5c389c31..daef0e9f 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -25,6 +25,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "force-graph": "^1.49.0", "typescript": "^5.9.3", diff --git a/package-lock.json b/package-lock.json index 2a1bf159..2a5b60ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "prettier": "^3.6.2", "typescript": "^5.9.3", @@ -128,6 +129,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -164,6 +166,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -201,6 +204,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -242,6 +246,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -279,6 +284,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -321,6 +327,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -359,6 +366,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -402,6 +410,7 @@ "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "typescript": "^5.9.3", "vite": "^6.0.0", @@ -439,6 +448,7 @@ "@types/node": "^22.0.0", "concurrently": "^9.2.1", "cors": "^2.8.5", + "cross-env": "^10.1.0", "express": "^5.1.0", "force-graph": "^1.49.0", "typescript": "^5.9.3", @@ -492,7 +502,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2098,7 +2107,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2123,7 +2131,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2624,7 +2631,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3340,7 +3346,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -3788,7 +3793,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", @@ -5321,7 +5325,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5420,7 +5423,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6191,7 +6193,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6941,7 +6942,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7035,7 +7035,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7169,7 +7168,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7422,7 +7420,6 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -7467,7 +7464,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 01ff639c..c43881f2 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -578,6 +578,52 @@ Example: } ``` +### Host Capabilities + +`HostCapabilities` are sent to the Guest UI as part of the response to `ui/initialize` (inside `McpUiInitializeResult`). +They describe the features and capabilities that the Host supports. + +```typescript +interface HostCapabilities { + /** Experimental features (structure TBD). */ + experimental?: {}; + /** Host supports opening external URLs. */ + openLinks?: {}; + /** Host can proxy tool calls to the MCP server. */ + serverTools?: { + /** Host supports tools/list_changed notifications. */ + listChanged?: boolean; + }; + /** Host can proxy resource reads to the MCP server. */ + serverResources?: { + /** Host supports resources/list_changed notifications. */ + listChanged?: boolean; + }; + /** Host accepts log messages. */ + logging?: {}; + /** Sandbox configuration applied by the host. */ + sandbox?: { + /** Permissions granted by the host (camera, microphone, geolocation). */ + permissions?: { + camera?: boolean; + microphone?: boolean; + geolocation?: boolean; + }; + /** CSP domains approved by the host. */ + csp?: { + /** Approved origins for network requests (fetch/XHR/WebSocket). */ + connectDomains?: string[]; + /** Approved origins for static resources (scripts, images, styles, fonts). */ + resourceDomains?: string[]; + /** Approved origins for nested iframes (frame-src directive). */ + frameDomains?: string[]; + /** Approved base URIs for the document (base-uri directive). */ + baseUriDomains?: string[]; + }; + }; +} +``` + ### Theming Hosts can optionally pass CSS custom properties via `HostContext.styles.variables` for visual cohesion with the host environment. diff --git a/src/generated/schema.json b/src/generated/schema.json index 80835036..ee56ecfc 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -90,17 +90,63 @@ "properties": {}, "additionalProperties": false }, - "csp": { - "description": "CSP overrides the host supports for sandbox proxies.", + "sandbox": { + "description": "Sandbox configuration applied by the host.", "type": "object", "properties": { - "frameDomains": { - "description": "Host supports frame-src domain allowlisting.", - "type": "boolean" + "permissions": { + "description": "Permissions granted by the host (camera, microphone, geolocation).", + "type": "object", + "properties": { + "camera": { + "description": "Request camera access (Permission Policy `camera` feature).", + "type": "boolean" + }, + "microphone": { + "description": "Request microphone access (Permission Policy `microphone` feature).", + "type": "boolean" + }, + "geolocation": { + "description": "Request geolocation access (Permission Policy `geolocation` feature).", + "type": "boolean" + } + }, + "additionalProperties": false }, - "baseUriDomains": { - "description": "Host supports base-uri domain allowlisting.", - "type": "boolean" + "csp": { + "description": "CSP domains approved by the host.", + "type": "object", + "properties": { + "connectDomains": { + "description": "Origins for network requests (fetch/XHR/WebSocket).", + "type": "array", + "items": { + "type": "string" + } + }, + "resourceDomains": { + "description": "Origins for static resources (scripts, images, styles, fonts).", + "type": "array", + "items": { + "type": "string" + } + }, + "frameDomains": { + "description": "Origins for nested iframes (frame-src directive).", + "type": "array", + "items": { + "type": "string" + } + }, + "baseUriDomains": { + "description": "Allowed base URIs for the document (base-uri directive).", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1898,17 +1944,63 @@ "properties": {}, "additionalProperties": false }, - "csp": { - "description": "CSP overrides the host supports for sandbox proxies.", + "sandbox": { + "description": "Sandbox configuration applied by the host.", "type": "object", "properties": { - "frameDomains": { - "description": "Host supports frame-src domain allowlisting.", - "type": "boolean" + "permissions": { + "description": "Permissions granted by the host (camera, microphone, geolocation).", + "type": "object", + "properties": { + "camera": { + "description": "Request camera access (Permission Policy `camera` feature).", + "type": "boolean" + }, + "microphone": { + "description": "Request microphone access (Permission Policy `microphone` feature).", + "type": "boolean" + }, + "geolocation": { + "description": "Request geolocation access (Permission Policy `geolocation` feature).", + "type": "boolean" + } + }, + "additionalProperties": false }, - "baseUriDomains": { - "description": "Host supports base-uri domain allowlisting.", - "type": "boolean" + "csp": { + "description": "CSP domains approved by the host.", + "type": "object", + "properties": { + "connectDomains": { + "description": "Origins for network requests (fetch/XHR/WebSocket).", + "type": "array", + "items": { + "type": "string" + } + }, + "resourceDomains": { + "description": "Origins for static resources (scripts, images, styles, fonts).", + "type": "array", + "items": { + "type": "string" + } + }, + "frameDomains": { + "description": "Origins for nested iframes (frame-src directive).", + "type": "array", + "items": { + "type": "string" + } + }, + "baseUriDomains": { + "description": "Allowed base URIs for the document (base-uri directive).", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index becbd115..b2f77fbd 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -79,20 +79,20 @@ export type McpUiResourceTeardownResultSchemaInferredType = z.infer< typeof generated.McpUiResourceTeardownResultSchema >; -export type McpUiHostCapabilitiesSchemaInferredType = z.infer< - typeof generated.McpUiHostCapabilitiesSchema +export type McpUiResourceCspSchemaInferredType = z.infer< + typeof generated.McpUiResourceCspSchema >; export type McpUiAppCapabilitiesSchemaInferredType = z.infer< typeof generated.McpUiAppCapabilitiesSchema >; -export type McpUiInitializedNotificationSchemaInferredType = z.infer< - typeof generated.McpUiInitializedNotificationSchema +export type McpUiHostCapabilitiesSchemaInferredType = z.infer< + typeof generated.McpUiHostCapabilitiesSchema >; -export type McpUiResourceCspSchemaInferredType = z.infer< - typeof generated.McpUiResourceCspSchema +export type McpUiInitializedNotificationSchemaInferredType = z.infer< + typeof generated.McpUiInitializedNotificationSchema >; export type McpUiResourceMetaSchemaInferredType = z.infer< @@ -221,26 +221,26 @@ expectType( expectType( {} as spec.McpUiResourceTeardownResult, ); -expectType( - {} as McpUiHostCapabilitiesSchemaInferredType, -); -expectType( - {} as spec.McpUiHostCapabilities, -); +expectType({} as McpUiResourceCspSchemaInferredType); +expectType({} as spec.McpUiResourceCsp); expectType( {} as McpUiAppCapabilitiesSchemaInferredType, ); expectType( {} as spec.McpUiAppCapabilities, ); +expectType( + {} as McpUiHostCapabilitiesSchemaInferredType, +); +expectType( + {} as spec.McpUiHostCapabilities, +); expectType( {} as McpUiInitializedNotificationSchemaInferredType, ); expectType( {} as spec.McpUiInitializedNotification, ); -expectType({} as McpUiResourceCspSchemaInferredType); -expectType({} as spec.McpUiResourceCsp); expectType({} as McpUiResourceMetaSchemaInferredType); expectType({} as spec.McpUiResourceMeta); expectType( diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 15b38f62..85ecd91e 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -324,6 +324,55 @@ export const McpUiResourceTeardownResultSchema = z.record( z.unknown(), ); +/** + * @description Content Security Policy configuration for UI resources. + */ +export const McpUiResourceCspSchema = z.object({ + /** @description Origins for network requests (fetch/XHR/WebSocket). */ + connectDomains: z + .array(z.string()) + .optional() + .describe("Origins for network requests (fetch/XHR/WebSocket)."), + /** @description Origins for static resources (scripts, images, styles, fonts). */ + resourceDomains: z + .array(z.string()) + .optional() + .describe("Origins for static resources (scripts, images, styles, fonts)."), + /** @description Origins for nested iframes (frame-src directive). */ + frameDomains: z + .array(z.string()) + .optional() + .describe("Origins for nested iframes (frame-src directive)."), + /** @description Allowed base URIs for the document (base-uri directive). */ + baseUriDomains: z + .array(z.string()) + .optional() + .describe("Allowed base URIs for the document (base-uri directive)."), +}); + +/** + * @description Capabilities provided by the Guest UI (App). + * @see {@link McpUiInitializeRequest} for the initialization request that includes these capabilities + */ +export const McpUiAppCapabilitiesSchema = z.object({ + /** @description Experimental features (structure TBD). */ + experimental: z + .object({}) + .optional() + .describe("Experimental features (structure TBD)."), + /** @description App exposes MCP-style tools that the host can call. */ + tools: z + .object({ + /** @description App supports tools/list_changed notifications. */ + listChanged: z + .boolean() + .optional() + .describe("App supports tools/list_changed notifications."), + }) + .optional() + .describe("App exposes MCP-style tools that the host can call."), +}); + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities @@ -363,45 +412,20 @@ export const McpUiHostCapabilitiesSchema = z.object({ .describe("Host can proxy resource reads to the MCP server."), /** @description Host accepts log messages. */ logging: z.object({}).optional().describe("Host accepts log messages."), - /** @description CSP overrides the host supports for sandbox proxies. */ - csp: z - .object({ - /** @description Host supports frame-src domain allowlisting. */ - frameDomains: z - .boolean() - .optional() - .describe("Host supports frame-src domain allowlisting."), - /** @description Host supports base-uri domain allowlisting. */ - baseUriDomains: z - .boolean() - .optional() - .describe("Host supports base-uri domain allowlisting."), - }) - .optional() - .describe("CSP overrides the host supports for sandbox proxies."), -}); - -/** - * @description Capabilities provided by the Guest UI (App). - * @see {@link McpUiInitializeRequest} for the initialization request that includes these capabilities - */ -export const McpUiAppCapabilitiesSchema = z.object({ - /** @description Experimental features (structure TBD). */ - experimental: z - .object({}) - .optional() - .describe("Experimental features (structure TBD)."), - /** @description App exposes MCP-style tools that the host can call. */ - tools: z + /** @description Sandbox configuration applied by the host. */ + sandbox: z .object({ - /** @description App supports tools/list_changed notifications. */ - listChanged: z - .boolean() - .optional() - .describe("App supports tools/list_changed notifications."), + /** @description Permissions granted by the host (camera, microphone, geolocation). */ + permissions: McpUiResourcePermissionsSchema.optional().describe( + "Permissions granted by the host (camera, microphone, geolocation).", + ), + /** @description CSP domains approved by the host. */ + csp: McpUiResourceCspSchema.optional().describe( + "CSP domains approved by the host.", + ), }) .optional() - .describe("App exposes MCP-style tools that the host can call."), + .describe("Sandbox configuration applied by the host."), }); /** @@ -413,32 +437,6 @@ export const McpUiInitializedNotificationSchema = z.object({ params: z.object({}).optional(), }); -/** - * @description Content Security Policy configuration for UI resources. - */ -export const McpUiResourceCspSchema = z.object({ - /** @description Origins for network requests (fetch/XHR/WebSocket). */ - connectDomains: z - .array(z.string()) - .optional() - .describe("Origins for network requests (fetch/XHR/WebSocket)."), - /** @description Origins for static resources (scripts, images, styles, fonts). */ - resourceDomains: z - .array(z.string()) - .optional() - .describe("Origins for static resources (scripts, images, styles, fonts)."), - /** @description Origins for nested iframes (frame-src directive). */ - frameDomains: z - .array(z.string()) - .optional() - .describe("Origins for nested iframes (frame-src directive)."), - /** @description Allowed base URIs for the document (base-uri directive). */ - baseUriDomains: z - .array(z.string()) - .optional() - .describe("Allowed base URIs for the document (base-uri directive)."), -}); - /** * @description UI Resource metadata for security and rendering configuration. */ diff --git a/src/spec.types.ts b/src/spec.types.ts index 122b757c..6f1662fe 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -420,12 +420,12 @@ export interface McpUiHostCapabilities { }; /** @description Host accepts log messages. */ logging?: {}; - /** @description CSP overrides the host supports for sandbox proxies. */ - csp?: { - /** @description Host supports frame-src domain allowlisting. */ - frameDomains?: boolean; - /** @description Host supports base-uri domain allowlisting. */ - baseUriDomains?: boolean; + /** @description Sandbox configuration applied by the host. */ + sandbox?: { + /** @description Permissions granted by the host (camera, microphone, geolocation). */ + permissions?: McpUiResourcePermissions; + /** @description CSP domains approved by the host. */ + csp?: McpUiResourceCsp; }; }