diff --git a/docs/patterns.md b/docs/patterns.md index efdf6e9c..3a6302c9 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -296,6 +296,65 @@ videoEl.src = `data:${content.mimeType!};base64,${content.blob}`; > [!NOTE] > For a full example that implements this pattern, see: [`examples/video-resource-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/video-resource-server). +## Configuring CSP and CORS + +Unlike regular web apps, MCP Apps HTML is served as an MCP resource and runs in a sandboxed iframe with no same-origin server. Any app that makes network requests must configure Content Security Policy (CSP) and possibly CORS. + +**CSP** controls what the _browser_ allows. You must declare _all_ origins in {@link types!McpUiResourceMeta.csp `_meta.ui.csp`} ({@link types!McpUiResourceCsp `McpUiResourceCsp`}) — including `localhost` during development. Declare `connectDomains` for fetch/XHR/WebSocket requests and `resourceDomains` for scripts, stylesheets, images, and fonts. + +**CORS** controls what the _API server_ allows. Public APIs that respond with `Access-Control-Allow-Origin: *` or use API key authentication work without CORS configuration. For APIs that allowlist specific origins, use {@link types!McpUiResourceMeta.domain `_meta.ui.domain`} to give the app a stable origin that the API server can allowlist. The format is host-specific, so check each host's documentation for its supported format. + + +```ts source="../src/server/index.examples.ts#registerAppResource_withDomain" +// Computes a stable origin from an MCP server URL for hosting in Claude. +function computeAppDomainForClaude(mcpServerUrl: string): string { + const hash = crypto + .createHash("sha256") + .update(mcpServerUrl) + .digest("hex") + .slice(0, 32); + return `${hash}.claudemcpcontent.com`; +} + +const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp"); + +registerAppResource( + server, + "Company Dashboard", + "ui://dashboard/view.html", + { + description: "Internal dashboard with company data", + }, + async () => ({ + contents: [ + { + uri: "ui://dashboard/view.html", + mimeType: RESOURCE_MIME_TYPE, + text: dashboardHtml, + _meta: { + ui: { + // CSP: tell browser the app is allowed to make requests + csp: { + connectDomains: ["https://api.example.com"], + }, + // CORS: give app a stable origin for the API server to allowlist + // + // (Public APIs that use `Access-Control-Allow-Origin: *` or API + // key auth don't need this.) + domain: APP_DOMAIN, + }, + }, + }, + ], + }), +); +``` + +Note that `_meta.ui.csp` and `_meta.ui.domain` are set in the `contents[]` objects returned by the resource read callback, not in `registerAppResource()`'s config object. + +> [!NOTE] +> For full examples that configures CSP, see: [`examples/sheet-music-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) (`connectDomains`) and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) (`connectDomains` and `resourceDomains`). + ## Adapting to host context (theme, styling, fonts, and safe areas) The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout: diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts index bb392dea..5e1ced4f 100644 --- a/src/server/index.examples.ts +++ b/src/server/index.examples.ts @@ -7,6 +7,7 @@ * @module */ +import * as crypto from "node:crypto"; import * as fs from "node:fs/promises"; import type { McpServer, @@ -198,6 +199,59 @@ function registerAppResource_withCsp( //#endregion registerAppResource_withCsp } +/** + * Example: registerAppResource with stable origin for external API CORS allowlists. + */ +function registerAppResource_withDomain( + server: McpServer, + dashboardHtml: string, +) { + //#region registerAppResource_withDomain + // Computes a stable origin from an MCP server URL for hosting in Claude. + function computeAppDomainForClaude(mcpServerUrl: string): string { + const hash = crypto + .createHash("sha256") + .update(mcpServerUrl) + .digest("hex") + .slice(0, 32); + return `${hash}.claudemcpcontent.com`; + } + + const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp"); + + registerAppResource( + server, + "Company Dashboard", + "ui://dashboard/view.html", + { + description: "Internal dashboard with company data", + }, + async () => ({ + contents: [ + { + uri: "ui://dashboard/view.html", + mimeType: RESOURCE_MIME_TYPE, + text: dashboardHtml, + _meta: { + ui: { + // CSP: tell browser the app is allowed to make requests + csp: { + connectDomains: ["https://api.example.com"], + }, + // CORS: give app a stable origin for the API server to allowlist + // + // (Public APIs that use `Access-Control-Allow-Origin: *` or API + // key auth don't need this.) + domain: APP_DOMAIN, + }, + }, + }, + ], + }), + ); + //#endregion registerAppResource_withDomain +} + /** * Example: Check for MCP Apps support in server initialization. */ diff --git a/src/server/index.ts b/src/server/index.ts index 1503066c..0cda7dde 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -34,6 +34,7 @@ import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, + McpUiResourceCsp, McpUiResourceMeta, McpUiToolMeta, McpUiClientCapabilities, @@ -298,6 +299,54 @@ export function registerAppTool< * ); * ``` * + * @example With stable origin for external API CORS allowlists + * ```ts source="./index.examples.ts#registerAppResource_withDomain" + * // Computes a stable origin from an MCP server URL for hosting in Claude. + * function computeAppDomainForClaude(mcpServerUrl: string): string { + * const hash = crypto + * .createHash("sha256") + * .update(mcpServerUrl) + * .digest("hex") + * .slice(0, 32); + * return `${hash}.claudemcpcontent.com`; + * } + * + * const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp"); + * + * registerAppResource( + * server, + * "Company Dashboard", + * "ui://dashboard/view.html", + * { + * description: "Internal dashboard with company data", + * }, + * async () => ({ + * contents: [ + * { + * uri: "ui://dashboard/view.html", + * mimeType: RESOURCE_MIME_TYPE, + * text: dashboardHtml, + * _meta: { + * ui: { + * // CSP: tell browser the app is allowed to make requests + * csp: { + * connectDomains: ["https://api.example.com"], + * }, + * // CORS: give app a stable origin for the API server to allowlist + * // + * // (Public APIs that use `Access-Control-Allow-Origin: *` or API + * // key auth don't need this.) + * domain: APP_DOMAIN, + * }, + * }, + * }, + * ], + * }), + * ); + * ``` + * + * @see {@link McpUiResourceMeta `McpUiResourceMeta`} for `_meta.ui` configuration options + * @see {@link McpUiResourceCsp `McpUiResourceCsp`} for CSP domain allowlist configuration * @see {@link registerAppTool `registerAppTool`} to register tools that reference this resource */ export function registerAppResource(