Skip to main content
MCP Apps SDK

Overview

Unlike regular web apps, MCP Apps run in sandboxed iframes 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. Declare all origins in _meta.ui.csp — including localhost during development. No external connections are allowed by default. CORS controls what the API server allows. Public APIs that respond with Access-Control-Allow-Origin: * work without CORS configuration. For APIs that allowlist specific origins, use _meta.ui.domain to give the app a stable origin.

CSP configuration

Set CSP fields in McpUiResourceCsp via _meta.ui.csp on resource content items:
FieldControlsExample
connectDomainsfetch, XHR, WebSocket["https://api.example.com"]
resourceDomainsScripts, images, fonts, stylesheets["https://cdn.example.com"]
frameDomainsNested iframes["https://embed.example.com"]
baseUriDomainsAllowed base URIs["https://example.com"]
import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";

registerAppResource(server, "Dashboard", "ui://dashboard/view.html", {
  description: "Interactive dashboard",
}, async () => ({
  contents: [{
    uri: "ui://dashboard/view.html",
    mimeType: RESOURCE_MIME_TYPE,
    text: dashboardHtml,
    _meta: {
      ui: {
        csp: {
          connectDomains: ["https://api.example.com"],
          resourceDomains: ["https://cdn.example.com"],
        },
      },
    },
  }],
}));
CSP is restrictive by default. If you don’t declare domains, no external connections are allowed. This prevents data exfiltration to undeclared servers.

CORS

Public APIs with Access-Control-Allow-Origin: * or API key authentication work without CORS configuration. For APIs that allowlist specific origins, you need a stable origin. MCP Apps served as srcdoc or blob: URLs don’t have a meaningful origin by default. Use _meta.ui.domain to assign one.

Stable origins for Claude

Claude computes a stable origin from the MCP server URL. Use this pattern to set _meta.ui.domain:
import crypto from "node:crypto";

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");
Then set both CSP and domain in the resource read callback:
registerAppResource(server, "Dashboard", "ui://dashboard/view.html", {
  description: "Dashboard with API access",
}, 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
        domain: APP_DOMAIN,
      },
    },
  }],
}));
The domain format is host-specific. Check each host’s documentation for its supported format. The Claude pattern above uses SHA-256 hashing of the MCP server URL.

Metadata location

_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. This allows dynamic CSP based on request context. CSP can also be declared in resources/list responses as static defaults. When both are present, the value in resources/read (content item) takes precedence.

See also

Resource Metadata

Full reference for McpUiResourceMeta including CSP and permissions.

McpUiResourceCsp

TypeScript type definition for CSP fields.
In the sunpeak framework, set CSP in your resource file’s _meta.ui.csp field. See the resource file reference for an example.