Skip to main content
MCP Apps SDK

Overview

Resource _meta.ui controls security, rendering, and sandbox behavior for MCP App Views. All fields are optional. This metadata is set on resources registered via registerAppResource.
_meta: {
  ui: {
    csp: { connectDomains: ["https://api.example.com"] },
    permissions: { camera: {} },
    domain: "a904794854a047f6.claudemcpcontent.com",
    prefersBorder: false,
  },
}

Where to Set _meta

Set _meta.ui on resources registered via registerAppResource. The SDK passes it through to both the resources/list and resources/read responses automatically.
import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";

registerAppResource(server, "Music Player", "ui://music/player.html", {
  _meta: {
    ui: {
      csp: { connectDomains: ["https://api.spotify.com"] },
      permissions: { microphone: {} },
      prefersBorder: false,
    },
  },
}, readCallback);
When using registerAppResource, you can set _meta in two places:
  1. Listing-level — in the config object. Hosts see this in the resources/list response at connection time.
  2. Content-item level — on individual contents[] items returned by the read callback. Content-item values take precedence.
This distinction matters when metadata varies per read. Most apps set the same values for both.

Fields

csp — Content Security Policy

csp
McpUiResourceCsp
Declares which external origins your View needs. The host uses these to build CSP headers for the sandboxed iframe.
MCP App HTML runs in a sandboxed iframe with no same-origin server. You must declare all origins — including where your bundled JS/CSS is served from (localhost in dev, your CDN in production).
FieldCSP DirectivePurpose
connectDomainsconnect-srcfetch, XHR, and WebSocket origins
resourceDomainsimg-src, script-src, style-src, font-src, media-srcStatic resource origins (images, scripts, stylesheets, fonts, media)
frameDomainsframe-srcNested iframe origins (e.g., YouTube embeds)
baseUriDomainsbase-uriAllowed base URIs for the document
All fields accept arrays of origin strings. Wildcard subdomains are supported (e.g., https://*.example.com). Empty or omitted fields default to no external access — this is the secure default.
_meta: {
  ui: {
    csp: {
      connectDomains: ["https://api.example.com", "wss://realtime.example.com"],
      resourceDomains: ["https://cdn.example.com", "https://*.cloudflare.com"],
      frameDomains: ["https://www.youtube.com", "https://player.vimeo.com"],
      baseUriDomains: ["https://cdn.example.com"],
    },
  },
}

connectDomains

Origins for network requests — fetch(), XMLHttpRequest, and WebSocket connections. Maps to the CSP connect-src directive. If omitted, no network connections are allowed from the View (secure default).

resourceDomains

Origins for static resources — images, scripts, stylesheets, fonts, and media files. Maps to CSP img-src, script-src, style-src, font-src, and media-src directives. If omitted, no external resources can be loaded (secure default).

frameDomains

Origins for nested iframes within your View. Maps to the CSP frame-src directive. Use this for embedding third-party content like YouTube or Vimeo players. If omitted, nested iframes are blocked (frame-src 'none').

baseUriDomains

Allowed base URIs for the document. Maps to the CSP base-uri directive. If omitted, only the same origin is allowed (base-uri 'self').

permissions — Sandbox Permissions

permissions
McpUiResourcePermissions
Requests browser capabilities for the View’s iframe. Each permission is declared as an empty object {} — its presence requests the capability.
FieldPermission PolicyPurpose
cameracameraCamera access (e.g., for photo capture, video calls)
microphonemicrophoneMicrophone access (e.g., for voice input, audio recording)
geolocationgeolocationLocation access (e.g., for maps, local search)
clipboardWriteclipboard-writeClipboard write access (e.g., for “copy to clipboard” buttons)
Hosts MAY honor these permissions by setting appropriate iframe allow attributes, but are not required to. Your View should use JavaScript feature detection as a fallback and degrade gracefully when permissions are not granted.
_meta: {
  ui: {
    permissions: {
      camera: {},
      microphone: {},
      geolocation: {},
      clipboardWrite: {},
    },
  },
}

domain — Stable Sandbox Origin

domain
string
Requests a stable, dedicated origin for the View’s sandbox iframe. The value is not your server’s domain — it’s a subdomain within the host’s sandbox domain space.
By default, hosts assign each View an ephemeral sandbox origin (typically per-conversation). Setting domain tells the host to use a stable, deterministic origin instead — useful when external services need to recognize your app by origin. Use cases:
  • OAuth callbacks — redirect URIs require a stable origin on the allowlist
  • CORS policies — API servers that check Origin headers need a known value to allowlist
  • API key restrictions — external services that restrict by origin
This field is host-specific. The value is a subdomain within the host’s own sandbox domain — not your server’s domain. Each host has its own format:
HostDomain formatExample
Claude{sha256-hash}.claudemcpcontent.coma904794854a047f6.claudemcpcontent.com
ChatGPT{url-derived}.oaiusercontent.comwww-example-com.oaiusercontent.com
A cross-platform server must compute different values per host. For APIs using Access-Control-Allow-Origin: * or API key auth, you don’t need domain at all.
import crypto from "node:crypto";

// Claude: hash-based subdomain derived from your MCP server URL
function computeDomainForClaude(mcpServerUrl: string): string {
  const hash = crypto
    .createHash("sha256")
    .update(mcpServerUrl)
    .digest("hex")
    .slice(0, 32);
  return `${hash}.claudemcpcontent.com`;
}

// Usage
_meta: {
  ui: {
    domain: computeDomainForClaude("https://example.com/mcp"),
  },
}
If omitted, the host uses its default sandbox origin.

prefersBorder — Visual Boundary

prefersBorder
boolean
Controls whether the host renders a visible border and background around the View.
ValueBehavior
trueRequest visible border and background from the host
falseRequest no visible border or background (seamless appearance)
omittedHost decides — defaults vary by platform
_meta: { ui: { prefersBorder: false } }
Explicitly set prefersBorder rather than relying on the host default, since defaults vary between ChatGPT and Claude.

Complete Example

import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";

registerAppResource(
  server,
  "Music Player",
  "ui://music/player.html",
  {
    description: "Interactive music player",
    _meta: {
      ui: { prefersBorder: false },
    },
  },
  async () => ({
    contents: [
      {
        uri: "ui://music/player.html",
        mimeType: RESOURCE_MIME_TYPE,
        text: musicPlayerHtml,
        _meta: {
          ui: {
            csp: {
              connectDomains: ["https://api.spotify.com"],
              resourceDomains: ["https://i.scdn.co"],
            },
            permissions: { microphone: {} },
            prefersBorder: false,
          },
        },
      },
    ],
  }),
);

TypeScript Types

import type {
  McpUiResourceMeta,
  McpUiResourceCsp,
  McpUiResourcePermissions,
} from "@modelcontextprotocol/ext-apps";

interface McpUiResourceMeta {
  csp?: McpUiResourceCsp;
  permissions?: McpUiResourcePermissions;
  domain?: string;
  prefersBorder?: boolean;
}

interface McpUiResourceCsp {
  connectDomains?: string[];
  resourceDomains?: string[];
  frameDomains?: string[];
  baseUriDomains?: string[];
}

interface McpUiResourcePermissions {
  camera?: {};
  microphone?: {};
  geolocation?: {};
  clipboardWrite?: {};
}
In the sunpeak framework, resource metadata is co-located with resource components. See the Tool File Reference.