---
title: "CSP & build pipeline"
description: "Content Security Policy, allow-listed domains, and how HTML bundles are produced."
canonical_url: "https://mcp-toolkit.nuxt.dev/apps/csp-and-wiring"
---
# 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:

```vue [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.

<callout color="warning" icon="i-lucide-shield">

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.

</callout>

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      What it allows
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        resourceDomains
      </code>
    </td>
    
    <td>
      <code>
        <img>
      </code>
      
      , <code>
        <style>
      </code>
      
      , <code>
        <link>
      </code>
      
      , fonts loaded from these origins
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        connectDomains
      </code>
    </td>
    
    <td>
      <code>
        fetch()
      </code>
      
      , <code>
        XHR
      </code>
      
      , <code>
        WebSocket
      </code>
      
      , <code>
        EventSource
      </code>
      
       to these origins
    </td>
  </tr>
</tbody>
</table>

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 host reads the `ui://` resource via `resources/read` and mounts the bundled HTML in an iframe.
3. The host pushes the tool result into the iframe, and `useMcpApp()` exposes its `structuredContent` as `data`.
4. From there, the iframe and the host exchange messages over a JSON-RPC bridge for `callTool`, `sendPrompt`, `openLink`, and theme/size updates.

<callout color="primary" icon="i-lucide-zap">

The whole view ships in **one HTML response** — the iframe makes no extra HTTP request for scripts, styles, or assets.

</callout>


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
