MCP Apps — Quick Reference
Server (FastMCP) ↔ Host (Claude/ChatGPT) ↔ View (sandboxed iframe). The View and Server never talk directly.
Supporting Files
- patterns.md — MCP Apps patterns: polling, chunked data, theming, fullscreen, view persistence (Brick Builder), per-view state, autoResize, command queues
- csp-cors.md — CSP configuration, CORS, stable origins, connectDomains vs resourceDomains
- testing.md — Testing with basic-host, Claude.ai, VS Code, tunnels
Data Flow
| Field |
LLM sees? |
View sees? |
Use for |
content |
Yes |
Yes |
Short text summary for model context |
structuredContent |
No* |
Yes |
Rich data for UI rendering |
_meta |
No |
Yes |
viewUUID, timestamps, metadata |
*ChatGPT exposes structuredContent to both.
Tool Visibility
| Visibility |
LLM? |
View? |
Use case |
["model", "app"] (default) |
Yes |
Yes |
General tools |
["model"] |
Yes |
No |
LLM-only triggers |
["app"] |
No |
Yes |
Pagination, refresh, polling, dangerous actions |
Server Quick Start (FastMCP 3.4+)
from fastmcp import FastMCP, Context
from fastmcp.apps import AppConfig, ResourceCSP, UI_EXTENSION_ID
from fastmcp.tools import ToolResult
from mcp import types
mcp = FastMCP("My App")
RESOURCE_URI = "ui://my-app/view.html"
# Tool with UI — creates an iframe
@mcp.tool(app=AppConfig(resource_uri=RESOURCE_URI))
async def show_data(query: str, ctx: Context) -> ToolResult:
has_ui = ctx.client_supports_extension(UI_EXTENSION_ID)
return ToolResult(
content=[types.TextContent(type="text", text="Summary for LLM")],
structured_content={"data": [...], "query": query},
)
# App-only tool — View calls this, LLM never sees it
@mcp.tool(app=AppConfig(resource_uri=RESOURCE_URI, visibility=["app"]))
async def load_page(page: int) -> ToolResult: ...
# Resource — serves the bundled HTML
@mcp.resource(uri=RESOURCE_URI)
def get_ui() -> str:
return Path("dist/mcp-app.html").read_text()
View Quick Start (Svelte 5)
<script lang="ts">
import { onMount } from "svelte";
import { App, applyDocumentTheme, applyHostStyleVariables, applyHostFonts, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
let app = $state<App | null>(null);
let hostContext = $state<McpUiHostContext | undefined>();
let data = $state.raw<MyData | null>(null);
$effect(() => {
if (hostContext?.theme) applyDocumentTheme(hostContext.theme);
if (hostContext?.styles?.variables) applyHostStyleVariables(hostContext.styles.variables);
if (hostContext?.styles?.css?.fonts) applyHostFonts(hostContext.styles.css.fonts);
});
onMount(async () => {
const instance = new App(
{ name: "My App", version: "1.0.0" },
{ availableDisplayModes: ["inline", "fullscreen"] },
{ autoResize: false }, // Required for flexible-height content — see patterns.md
);
instance.ontoolresult = (result) => { data = result.structuredContent; };
instance.onhostcontextchanged = (ctx) => { hostContext = { ...hostContext, ...ctx }; };
instance.onteardown = async () => { /* cleanup */ return {}; };
await instance.connect();
app = instance;
hostContext = instance.getHostContext();
});
</script>
Decision Checklist
| Question |
Answer |
| What does the LLM need? |
content |
| What does the UI need? |
structuredContent |
| LLM triggers this? |
visibility: ["model"] or default |
| UI-only action? |
visibility: ["app"] |
| Data >100KB? |
Chunked app-only tool — see patterns.md |
| External APIs? |
Declare in ResourceCSP — see csp-cors.md |
| Fullscreen? |
availableDisplayModes: ["inline", "fullscreen"] |
| View should persist across LLM tool calls? |
Brick Builder pattern — see patterns.md |
| Multi-user server? |
Per-view state with MemoryStore — see patterns.md |
Gotchas
- Tool with
resourceUri = new iframe. Never put AppConfig(resource_uri=...) on update tools.
autoResize: false required for flexible-height content to prevent infinite resize loops.
onteardown — always implement to stop polling and clean up.
- CSP deny-by-default. External images silently don't load without
resource_domains.
asyncio.gather for parallel fetches — sequential await calls wait for each other.
- Multi-user servers need per-view state (MemoryStore + view_id), not module-level globals.
1---2name: ra-mcp-apps3description: MCP Apps guide: FastMCP 3.4+ server, Svelte 5 view, tool visibility, polling, sizing, fullscreen, view persistence. Use for any MCP App/UI work.4---56# MCP Apps — Quick Reference78Server (FastMCP) ↔ Host (Claude/ChatGPT) ↔ View (sandboxed iframe). The View and Server never talk directly.910## Supporting Files1112- [patterns.md](patterns.md) — MCP Apps patterns: polling, chunked data, theming, fullscreen, view persistence (Brick Builder), per-view state, autoResize, command queues13- [csp-cors.md](csp-cors.md) — CSP configuration, CORS, stable origins, connectDomains vs resourceDomains14- [testing.md](testing.md) — Testing with basic-host, Claude.ai, VS Code, tunnels1516## Data Flow1718| Field | LLM sees? | View sees? | Use for |19|-------|-----------|------------|---------|20| `content` | Yes | Yes | Short text summary for model context |21| `structuredContent` | No* | Yes | Rich data for UI rendering |22| `_meta` | No | Yes | viewUUID, timestamps, metadata |2324*ChatGPT exposes structuredContent to both.2526## Tool Visibility2728| Visibility | LLM? | View? | Use case |29|------------|-------|-------|----------|30| `["model", "app"]` (default) | Yes | Yes | General tools |31| `["model"]` | Yes | No | LLM-only triggers |32| `["app"]` | No | Yes | Pagination, refresh, polling, dangerous actions |3334## Server Quick Start (FastMCP 3.4+)3536```python37from fastmcp import FastMCP, Context38from fastmcp.apps import AppConfig, ResourceCSP, UI_EXTENSION_ID39from fastmcp.tools import ToolResult40from mcp import types4142mcp = FastMCP("My App")43RESOURCE_URI = "ui://my-app/view.html"4445# Tool with UI — creates an iframe46@mcp.tool(app=AppConfig(resource_uri=RESOURCE_URI))47async def show_data(query: str, ctx: Context) -> ToolResult:48 has_ui = ctx.client_supports_extension(UI_EXTENSION_ID)49 return ToolResult(50 content=[types.TextContent(type="text", text="Summary for LLM")],51 structured_content={"data": [...], "query": query},52 )5354# App-only tool — View calls this, LLM never sees it55@mcp.tool(app=AppConfig(resource_uri=RESOURCE_URI, visibility=["app"]))56async def load_page(page: int) -> ToolResult: ...5758# Resource — serves the bundled HTML59@mcp.resource(uri=RESOURCE_URI)60def get_ui() -> str:61 return Path("dist/mcp-app.html").read_text()62```6364## View Quick Start (Svelte 5)6566```svelte67<script lang="ts">68import { onMount } from "svelte";69import { App, applyDocumentTheme, applyHostStyleVariables, applyHostFonts, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";7071let app = $state<App | null>(null);72let hostContext = $state<McpUiHostContext | undefined>();73let data = $state.raw<MyData | null>(null);7475$effect(() => {76 if (hostContext?.theme) applyDocumentTheme(hostContext.theme);77 if (hostContext?.styles?.variables) applyHostStyleVariables(hostContext.styles.variables);78 if (hostContext?.styles?.css?.fonts) applyHostFonts(hostContext.styles.css.fonts);79});8081onMount(async () => {82 const instance = new App(83 { name: "My App", version: "1.0.0" },84 { availableDisplayModes: ["inline", "fullscreen"] },85 { autoResize: false }, // Required for flexible-height content — see patterns.md86 );87 instance.ontoolresult = (result) => { data = result.structuredContent; };88 instance.onhostcontextchanged = (ctx) => { hostContext = { ...hostContext, ...ctx }; };89 instance.onteardown = async () => { /* cleanup */ return {}; };90 await instance.connect();91 app = instance;92 hostContext = instance.getHostContext();93});94</script>95```9697## Decision Checklist9899| Question | Answer |100|----------|--------|101| What does the LLM need? | `content` |102| What does the UI need? | `structuredContent` |103| LLM triggers this? | `visibility: ["model"]` or default |104| UI-only action? | `visibility: ["app"]` |105| Data >100KB? | Chunked app-only tool — see [patterns.md](patterns.md) |106| External APIs? | Declare in `ResourceCSP` — see [csp-cors.md](csp-cors.md) |107| Fullscreen? | `availableDisplayModes: ["inline", "fullscreen"]` |108| View should persist across LLM tool calls? | Brick Builder pattern — see [patterns.md](patterns.md) |109| Multi-user server? | Per-view state with MemoryStore — see [patterns.md](patterns.md) |110111## Gotchas112113- **Tool with `resourceUri` = new iframe.** Never put `AppConfig(resource_uri=...)` on update tools.114- **`autoResize: false`** required for flexible-height content to prevent infinite resize loops.115- **`onteardown`** — always implement to stop polling and clean up.116- **CSP deny-by-default.** External images silently don't load without `resource_domains`.117- **`asyncio.gather`** for parallel fetches — sequential `await` calls wait for each other.118- **Multi-user servers** need per-view state (MemoryStore + view_id), not module-level globals.