Skip to content

Commit ec0f217

Browse files
Merge pull request #415 from jonathanhefner/csp-and-cors-example
Add CSP and CORS section to patterns docs
2 parents bb4e5c5 + 13b6d06 commit ec0f217

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

docs/patterns.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,65 @@ videoEl.src = `data:${content.mimeType!};base64,${content.blob}`;
296296
> [!NOTE]
297297
> 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).
298298
299+
## Configuring CSP and CORS
300+
301+
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.
302+
303+
**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.
304+
305+
**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.
306+
307+
<!-- prettier-ignore -->
308+
```ts source="../src/server/index.examples.ts#registerAppResource_withDomain"
309+
// Computes a stable origin from an MCP server URL for hosting in Claude.
310+
function computeAppDomainForClaude(mcpServerUrl: string): string {
311+
const hash = crypto
312+
.createHash("sha256")
313+
.update(mcpServerUrl)
314+
.digest("hex")
315+
.slice(0, 32);
316+
return `${hash}.claudemcpcontent.com`;
317+
}
318+
319+
const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp");
320+
321+
registerAppResource(
322+
server,
323+
"Company Dashboard",
324+
"ui://dashboard/view.html",
325+
{
326+
description: "Internal dashboard with company data",
327+
},
328+
async () => ({
329+
contents: [
330+
{
331+
uri: "ui://dashboard/view.html",
332+
mimeType: RESOURCE_MIME_TYPE,
333+
text: dashboardHtml,
334+
_meta: {
335+
ui: {
336+
// CSP: tell browser the app is allowed to make requests
337+
csp: {
338+
connectDomains: ["https://api.example.com"],
339+
},
340+
// CORS: give app a stable origin for the API server to allowlist
341+
//
342+
// (Public APIs that use `Access-Control-Allow-Origin: *` or API
343+
// key auth don't need this.)
344+
domain: APP_DOMAIN,
345+
},
346+
},
347+
},
348+
],
349+
}),
350+
);
351+
```
352+
353+
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.
354+
355+
> [!NOTE]
356+
> 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`).
357+
299358
## Adapting to host context (theme, styling, fonts, and safe areas)
300359

301360
The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout:

src/server/index.examples.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @module
88
*/
99

10+
import * as crypto from "node:crypto";
1011
import * as fs from "node:fs/promises";
1112
import type {
1213
McpServer,
@@ -198,6 +199,59 @@ function registerAppResource_withCsp(
198199
//#endregion registerAppResource_withCsp
199200
}
200201

202+
/**
203+
* Example: registerAppResource with stable origin for external API CORS allowlists.
204+
*/
205+
function registerAppResource_withDomain(
206+
server: McpServer,
207+
dashboardHtml: string,
208+
) {
209+
//#region registerAppResource_withDomain
210+
// Computes a stable origin from an MCP server URL for hosting in Claude.
211+
function computeAppDomainForClaude(mcpServerUrl: string): string {
212+
const hash = crypto
213+
.createHash("sha256")
214+
.update(mcpServerUrl)
215+
.digest("hex")
216+
.slice(0, 32);
217+
return `${hash}.claudemcpcontent.com`;
218+
}
219+
220+
const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp");
221+
222+
registerAppResource(
223+
server,
224+
"Company Dashboard",
225+
"ui://dashboard/view.html",
226+
{
227+
description: "Internal dashboard with company data",
228+
},
229+
async () => ({
230+
contents: [
231+
{
232+
uri: "ui://dashboard/view.html",
233+
mimeType: RESOURCE_MIME_TYPE,
234+
text: dashboardHtml,
235+
_meta: {
236+
ui: {
237+
// CSP: tell browser the app is allowed to make requests
238+
csp: {
239+
connectDomains: ["https://api.example.com"],
240+
},
241+
// CORS: give app a stable origin for the API server to allowlist
242+
//
243+
// (Public APIs that use `Access-Control-Allow-Origin: *` or API
244+
// key auth don't need this.)
245+
domain: APP_DOMAIN,
246+
},
247+
},
248+
},
249+
],
250+
}),
251+
);
252+
//#endregion registerAppResource_withDomain
253+
}
254+
201255
/**
202256
* Example: Check for MCP Apps support in server initialization.
203257
*/

src/server/index.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import {
3535
RESOURCE_URI_META_KEY,
3636
RESOURCE_MIME_TYPE,
37+
McpUiResourceCsp,
3738
McpUiResourceMeta,
3839
McpUiToolMeta,
3940
McpUiClientCapabilities,
@@ -298,6 +299,54 @@ export function registerAppTool<
298299
* );
299300
* ```
300301
*
302+
* @example With stable origin for external API CORS allowlists
303+
* ```ts source="./index.examples.ts#registerAppResource_withDomain"
304+
* // Computes a stable origin from an MCP server URL for hosting in Claude.
305+
* function computeAppDomainForClaude(mcpServerUrl: string): string {
306+
* const hash = crypto
307+
* .createHash("sha256")
308+
* .update(mcpServerUrl)
309+
* .digest("hex")
310+
* .slice(0, 32);
311+
* return `${hash}.claudemcpcontent.com`;
312+
* }
313+
*
314+
* const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp");
315+
*
316+
* registerAppResource(
317+
* server,
318+
* "Company Dashboard",
319+
* "ui://dashboard/view.html",
320+
* {
321+
* description: "Internal dashboard with company data",
322+
* },
323+
* async () => ({
324+
* contents: [
325+
* {
326+
* uri: "ui://dashboard/view.html",
327+
* mimeType: RESOURCE_MIME_TYPE,
328+
* text: dashboardHtml,
329+
* _meta: {
330+
* ui: {
331+
* // CSP: tell browser the app is allowed to make requests
332+
* csp: {
333+
* connectDomains: ["https://api.example.com"],
334+
* },
335+
* // CORS: give app a stable origin for the API server to allowlist
336+
* //
337+
* // (Public APIs that use `Access-Control-Allow-Origin: *` or API
338+
* // key auth don't need this.)
339+
* domain: APP_DOMAIN,
340+
* },
341+
* },
342+
* },
343+
* ],
344+
* }),
345+
* );
346+
* ```
347+
*
348+
* @see {@link McpUiResourceMeta `McpUiResourceMeta`} for `_meta.ui` configuration options
349+
* @see {@link McpUiResourceCsp `McpUiResourceCsp`} for CSP domain allowlist configuration
301350
* @see {@link registerAppTool `registerAppTool`} to register tools that reference this resource
302351
*/
303352
export function registerAppResource(

0 commit comments

Comments
 (0)