Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- 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)

The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout:
Expand Down
54 changes: 54 additions & 0 deletions src/server/index.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @module
*/

import * as crypto from "node:crypto";
import * as fs from "node:fs/promises";
import type {
McpServer,
Expand Down Expand Up @@ -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.
*/
Expand Down
49 changes: 49 additions & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import {
RESOURCE_URI_META_KEY,
RESOURCE_MIME_TYPE,
McpUiResourceCsp,
McpUiResourceMeta,
McpUiToolMeta,
McpUiClientCapabilities,
Expand Down Expand Up @@ -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(
Expand Down
Loading