Add CSP and CORS section to patterns docs
Adds a new section to the patterns guide explaining how to configure Content Security Policy and CORS for MCP Apps that make network requests: - CSP via `_meta.ui.csp` controls browser-side permissions (`connectDomains`, `resourceDomains`) - CORS via `_meta.ui.domain` provides a stable origin for API servers to allowlist Includes a type-checked example showing `computeAppDomainForClaude()` helper that hashes an MCP server URL to create a stable `*.claudemcpcontent.com` subdomain. Also adds `@see` links to `McpUiResourceMeta` and `McpUiResourceCsp` in `registerAppResource` JSDoc. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c4f4969b1
commit
13b6d06399
3 changed files with 162 additions and 0 deletions
|
|
@ -296,6 +296,65 @@ videoEl.src = `data:${content.mimeType!};base64,${content.blob}`;
|
||||||
> [!NOTE]
|
> [!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).
|
> 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.
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
```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)
|
## 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:
|
The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as crypto from "node:crypto";
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import type {
|
import type {
|
||||||
McpServer,
|
McpServer,
|
||||||
|
|
@ -198,6 +199,59 @@ function registerAppResource_withCsp(
|
||||||
//#endregion 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.
|
* Example: Check for MCP Apps support in server initialization.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
import {
|
import {
|
||||||
RESOURCE_URI_META_KEY,
|
RESOURCE_URI_META_KEY,
|
||||||
RESOURCE_MIME_TYPE,
|
RESOURCE_MIME_TYPE,
|
||||||
|
McpUiResourceCsp,
|
||||||
McpUiResourceMeta,
|
McpUiResourceMeta,
|
||||||
McpUiToolMeta,
|
McpUiToolMeta,
|
||||||
McpUiClientCapabilities,
|
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
|
* @see {@link registerAppTool `registerAppTool`} to register tools that reference this resource
|
||||||
*/
|
*/
|
||||||
export function registerAppResource(
|
export function registerAppResource(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue