Apps

CSP & build pipeline

Content Security Policy, allow-listed domains, and how HTML bundles are produced.

CSP & Resource Allow-Lists

The toolkit injects a conservative Content Security Policy into every app HTML. By default the iframe can:

  • Run only its own inline script.
  • Render images and styles only from the same response.
  • Talk over postMessage to its parent host.

If your UI needs external assets or APIs, allow them explicitly:

app/mcp/color-picker.vue
<script setup lang="ts">
defineMcpApp({
  description: 'Pick a colour and preview a palette.',
  csp: {
    resourceDomains: ['https://images.unsplash.com'],
    connectDomains: ['https://api.example.com'],
  },
  handler: async ({ base }) => ({ structuredContent: await $fetch('/api/palette', { query: { base } }) }),
})
</script>

The CSP is mirrored into _meta.ui.csp (and openai/widgetCSP for ChatGPT) so hosts that enforce CSP at the iframe level pick up the same rules.

Pass csp: false only as a last resort — and only if you fully control the assets the iframe loads. The default policy is what makes apps safe to render across hosts.
FieldWhat it allows
resourceDomains<img>, <style>, <link>, fonts loaded from these origins
connectDomainsfetch(), XHR, WebSocket, EventSource to these origins

Origins should use http(s):// or ws(s)://. The toolkit rejects empty values, unsupported URL schemes, whitespace, quotes, and semicolons before the app response is served.

How It's Wired

Behind the scenes, each .vue file under the configured apps directory becomes three artifacts at build time:

  1. A tool definition registered on your MCP handler (input schema, description, server handler).
  2. A UI resource at ui://mcp-app/<name> returning text/html;profile=mcp-app.
  3. A single-file HTML bundle of the Vue SFC (Vue runtime, your code, scoped CSS, assets) inlined into the resource response.

When the LLM calls the tool:

  1. Your handler runs on the server and produces structuredContent.
  2. The toolkit injects that data into the bundled HTML as <script type="application/json" id="__mcp_app_data__">…</script>.
  3. The host returns the resource to the user; its iframe boots and useMcpApp() reads the inline data into data synchronously.
  4. From there, the iframe and the host exchange messages over a JSON-RPC bridge for callTool, sendPrompt, openLink, and theme/size updates.
Everything ships in one HTML response. No extra HTTP request from the iframe, no waterfall, no flicker.
Copyright © 2026