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
postMessageto 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.| Field | What it allows |
|---|---|
resourceDomains | <img>, <style>, <link>, fonts loaded from these origins |
connectDomains | fetch(), 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:
- A tool definition registered on your MCP handler (input schema, description, server handler).
- A UI resource at
ui://mcp-app/<name>returningtext/html;profile=mcp-app. - 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:
- Your
handlerruns on the server and producesstructuredContent. - The toolkit injects that data into the bundled HTML as
<script type="application/json" id="__mcp_app_data__">…</script>. - The host returns the resource to the user; its iframe boots and
useMcpApp()reads the inline data intodatasynchronously. - 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.