diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 1be80e76..bf2f535c 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -151,6 +151,20 @@ interface McpUiResourceCsp { * ["https://cdn.example.com"] */ baseUriDomains?: string[], + /** + * Trusted Types policy names for controlled runtime code evaluation. + * + * When declared, enables Trusted Types enforcement and allows the app to + * create named policies that produce trusted script content for use with + * `eval()` or `new Function()`. + * + * Each name must match a policy the app explicitly creates via + * `trustedTypes.createPolicy()`. + * + * @example + * ["formula-evaluator", "template-engine"] + */ + trustedTypes?: string[], } interface UIResourceMeta { @@ -245,6 +259,7 @@ The resource content is returned via `resources/read`: resourceDomains?: string[]; // Origins for static resources (scripts, images, styles, fonts). frameDomains?: string[]; // Origins for nested iframes (frame-src directive). baseUriDomains?: string[]; // Allowed base URIs for the document (base-uri directive). + trustedTypes?: string[]; // Trusted Types policy names for runtime code evaluation }; permissions?: { camera?: {}; // Request camera access @@ -903,7 +918,7 @@ type McpUiStyleVariableKey = } ``` - Views can use the `applyHostStyleVariables` utility (or `useHostStyleVariables` if they prefer a React hook) to easily populate the host-provided CSS variables into their style sheet -- Views can use the `applyDocumentTheme` utility (or `useDocumentTheme` if they prefer a React hook) to easily respond to Host Context `theme` changes in a way that is compatible with the host's light/dark color variables +- Views can use the `applyDocumentTheme` utility (or `useDocumentTheme` if they prefer a React hook) to easily respond to Host Context `theme` changes in a way that is compatible with the host's light/dark color variables Example usage of standardized CSS variables: @@ -1728,9 +1743,12 @@ Hosts MUST enforce Content Security Policies based on resource metadata. const csp = resource._meta?.ui?.csp; // `resource` is extracted from the `contents` of the `resources/read` result const permissions = resource._meta?.ui?.permissions; -const cspValue = ` +// Only add 'unsafe-eval' when Trusted Types is declared (provides controlled evaluation) +const evalDirective = csp?.trustedTypes ? " 'unsafe-eval'" : ''; + +let cspValue = ` default-src 'none'; - script-src 'self' 'unsafe-inline' ${csp?.resourceDomains?.join(' ') || ''}; + script-src 'self' 'unsafe-inline'${evalDirective} ${csp?.resourceDomains?.join(' ') || ''}; style-src 'self' 'unsafe-inline' ${csp?.resourceDomains?.join(' ') || ''}; connect-src 'self' ${csp?.connectDomains?.join(' ') || ''}; img-src 'self' data: ${csp?.resourceDomains?.join(' ') || ''}; @@ -1741,6 +1759,14 @@ const cspValue = ` base-uri ${csp?.baseUriDomains?.join(' ') || "'self'"}; `; +// Add Trusted Types directives when trustedTypes is declared +if (csp?.trustedTypes) { + cspValue += ` + require-trusted-types-for 'script'; + trusted-types ${csp.trustedTypes.join(' ')}; +`; +} + // Permission Policy for iframe allow attribute const allowList: string[] = []; if (permissions?.camera) allowList.push('camera'); @@ -1760,6 +1786,59 @@ const allowAttribute = allowList.join(' '); - **Social engineering:** UI can still display misleading content. Hosts should clearly indicate sandboxed UI boundaries. - **Resource consumption:** Malicious View can consume CPU/memory. Hosts should implement resource limits. +#### 5. Trusted Types for Runtime Code Evaluation + +Apps that need runtime code evaluation (e.g., `eval()`, `new Function()`) can declare Trusted Types policies. This provides controlled evaluation through browser-enforced policy callbacks. + +**Example resource declaration:** + +```json +{ + "contents": [{ + "uri": "ui://formula-evaluator/calculator", + "mimeType": "text/html;profile=mcp-app", + "text": "...", + "_meta": { + "ui": { + "csp": { + "trustedTypes": ["formula-evaluator"] + } + } + } + }] +} +``` + +**Example app usage:** + +```typescript +// Create a Trusted Types policy in the app +const formulaPolicy = trustedTypes.createPolicy('formula-evaluator', { + createScript: (input: string) => { + // Validate/sanitize the input before allowing evaluation + if (!isValidFormula(input)) { + throw new Error('Invalid formula syntax'); + } + return input; + } +}); + +// Use the policy to safely evaluate formulas +// Note: a workaround is currently required for Chromium https://github.com/w3c/trusted-types/wiki/Trusted-Types-for-function-constructor +function evaluateFormula(formula: string, variables: Record) { + const trustedScript = formulaPolicy.createScript(` + return (${formula}); + `); + + const fn = new Function(...Object.keys(variables), trustedScript); + return fn(...Object.values(variables)); +} +``` + +**Sandbox proxy considerations:** + +Web-based hosts that use a sandbox proxy to load View HTML content may need to declare an additional Trusted Types policy for the sandbox itself. When Trusted Types is enforced, the sandbox cannot use `innerHTML` or `document.write()` to inject the View's HTML without a policy. The sandbox SHOULD create its own policy named `mcp-app-sandbox` for this purpose, separate from any policies declared by the View. Apps MUST not themselves declare a policy named `mcp-app-sandbox`. + ## Reservations in MCP - The resource prefix `ui://` will be reserved for MCP Apps diff --git a/src/generated/schema.json b/src/generated/schema.json index bdbb058e..c0f9fe31 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -191,6 +191,13 @@ "items": { "type": "string" } + }, + "trustedTypes": { + "description": "Trusted Types policy names (trusted-types directive)", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -2537,6 +2544,13 @@ "items": { "type": "string" } + }, + "trustedTypes": { + "description": "Trusted Types policy names (trusted-types directive)", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -3900,6 +3914,13 @@ "items": { "type": "string" } + }, + "trustedTypes": { + "description": "Trusted Types policy names (trusted-types directive)", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -3939,6 +3960,13 @@ "items": { "type": "string" } + }, + "trustedTypes": { + "description": "Trusted Types policy names (trusted-types directive)", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -4108,6 +4136,13 @@ "items": { "type": "string" } + }, + "trustedTypes": { + "description": "Trusted Types policy names (trusted-types directive)", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 4719ada3..d3ca40e2 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -209,6 +209,11 @@ export const McpUiResourceCspSchema = z.object({ .array(z.string()) .optional() .describe("Allowed base URIs for the document (base-uri directive)."), + /** @description Trusted Types policy names (trusted-types directive) */ + trustedTypes: z + .array(z.string()) + .optional() + .describe("Trusted Types policy names (trusted-types directive)"), }); /** diff --git a/src/spec.types.ts b/src/spec.types.ts index 7cea3daf..581087c6 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -548,6 +548,8 @@ export interface McpUiResourceCsp { frameDomains?: string[]; /** @description Allowed base URIs for the document (base-uri directive). */ baseUriDomains?: string[]; + /** @description Trusted Types policy names (trusted-types directive) */ + trustedTypes?: string[]; } /**