snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,431 @@
---
name: add-app-to-server
description: This skill should be used when the user asks to "add an app to my MCP server", "add UI to my MCP server", "add a view to my MCP tool", "enrich MCP tools with UI", "add interactive UI to existing server", "add MCP Apps to my server", or needs to add interactive UI capabilities to an existing MCP server that already has tools. Provides guidance for analyzing existing tools and adding MCP Apps UI resources.
---
# Add UI to MCP Server
Enrich an existing MCP server's tools with interactive UIs using the MCP Apps SDK (`@modelcontextprotocol/ext-apps`).
## How It Works
Existing tools get paired with HTML resources that render inline in the host's conversation. The tool continues to work for text-only clients — UI is an enhancement, not a replacement. Each tool that benefits from UI gets linked to a resource via `_meta.ui.resourceUri`, and the host renders that resource in a sandboxed iframe when the tool is called.
## Getting Reference Code
Clone the SDK repository for working examples and API documentation:
```bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
```
### API Reference (Source Files)
Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
| File | Contents |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, `getUiCapability`, tool visibility options |
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
| `src/react/useApp.tsx` | `useApp` hook for React apps |
| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
### Key Examples (Mixed Tool Patterns)
These examples demonstrate servers with both App-enhanced and plain tools — the exact pattern you're adding:
| Example | Pattern |
| --------------------------------- | -------------------------------------------------------------------------------------- |
| `examples/map-server/` | `show-map` (App tool) + `geocode` (plain tool) |
| `examples/pdf-server/` | `display_pdf` (App tool) + `list_pdfs` (plain tool) + `read_pdf_bytes` (app-only tool) |
| `examples/system-monitor-server/` | `get-system-info` (App tool) + `poll-system-stats` (app-only polling tool) |
### Framework Templates
Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
| Template | Key Files |
| ------------------------- | --------------------------------------------------- |
| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
| `basic-server-vue/` | `server.ts`, `src/App.vue` |
| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
## Step 1: Analyze Existing Tools
Before writing any code, analyze the server's existing tools and determine which ones benefit from UI.
1. Read the server source and list all registered tools
2. For each tool, assess whether it would benefit from UI (returns data that could be visualized, involves user interaction, etc.) vs. is fine as text-only (simple lookups, utility functions)
3. Identify tools that could become **app-only helpers** (data the UI needs to poll/fetch but the model doesn't need to call directly)
4. Present the analysis to the user and confirm which tools to enhance
### Decision Framework
| Tool output type | UI benefit | Example |
| -------------------------------- | ------------------------------------------- | --------------------------------- |
| Structured data / lists / tables | High — interactive table, search, filtering | List of items, search results |
| Metrics / numbers over time | High — charts, gauges, dashboards | System stats, analytics |
| Media / rich content | High — viewer, player, renderer | Maps, PDFs, images, video |
| Simple text / confirmations | Low — text is fine | "File created", "Setting updated" |
| Data for other tools | Consider app-only | Polling endpoints, chunk loaders |
## Step 2: Add Dependencies
```bash
npm install @modelcontextprotocol/ext-apps
npm install -D vite vite-plugin-singlefile
```
Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React).
Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
## Step 3: Set Up the Build Pipeline
### Vite Configuration
Create `vite.config.ts` with `vite-plugin-singlefile` to bundle the UI into a single HTML file:
```typescript
import { defineConfig } from 'vite'
import { viteSingleFile } from 'vite-plugin-singlefile'
export default defineConfig({
plugins: [viteSingleFile()],
build: {
outDir: 'dist',
rollupOptions: {
input: 'mcp-app.html', // one per UI, or one shared entry
},
},
})
```
### HTML Entry Point
Create `mcp-app.html` (or one per distinct UI if tools need different views):
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/mcp-app.ts"></script>
</body>
</html>
```
### Build Scripts
Add build scripts to `package.json`. The UI must be built before the server code bundles it:
```json
{
"scripts": {
"build:ui": "vite build",
"build:server": "tsc",
"build": "npm run build:ui && npm run build:server",
"serve": "tsx server.ts"
}
}
```
## Step 4: Convert Tools to App Tools
Transform plain MCP tools into App tools with UI.
**Before** (plain MCP tool):
```typescript
server.tool('my-tool', { param: z.string() }, async (args) => {
const data = await fetchData(args.param)
return { content: [{ type: 'text', text: JSON.stringify(data) }] }
})
```
**After** (App tool with UI):
```typescript
import {
registerAppTool,
registerAppResource,
RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server'
const resourceUri = 'ui://my-tool/mcp-app.html'
registerAppTool(
server,
'my-tool',
{
description: 'Shows data with an interactive UI',
inputSchema: { param: z.string() },
_meta: { ui: { resourceUri } },
},
async (args) => {
const data = await fetchData(args.param)
return {
content: [{ type: 'text', text: JSON.stringify(data) }], // text fallback for non-UI hosts
structuredContent: { data }, // structured data for the UI
}
}
)
```
Key guidance:
- **Always keep the `content` array** with a text fallback for text-only clients
- Add `structuredContent` for data the UI needs to render
- Link the tool to its resource via `_meta.ui.resourceUri`
- Leave tools that don't benefit from UI unchanged — they stay as plain tools
## Step 5: Register Resources
Register the HTML resource so the host can fetch it:
```typescript
import fs from 'node:fs/promises'
import path from 'node:path'
const resourceUri = 'ui://my-tool/mcp-app.html'
registerAppResource(
server,
{
uri: resourceUri,
name: 'My Tool UI',
mimeType: RESOURCE_MIME_TYPE,
},
async () => {
const html = await fs.readFile(
path.resolve(import.meta.dirname, 'dist', 'mcp-app.html'),
'utf-8'
)
return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }
}
)
```
If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration.
## Step 6: Build the UI
### Handler Registration
Register ALL handlers BEFORE calling `app.connect()`:
```typescript
import {
App,
PostMessageTransport,
applyDocumentTheme,
applyHostStyleVariables,
applyHostFonts,
} from '@modelcontextprotocol/ext-apps'
const app = new App({ name: 'My App', version: '1.0.0' })
app.ontoolinput = (params) => {
// Render the UI using params.arguments and/or params.structuredContent
}
app.ontoolresult = (result) => {
// Update UI with final tool result
}
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme)
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables)
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts)
if (ctx.safeAreaInsets) {
const { top, right, bottom, left } = ctx.safeAreaInsets
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`
}
}
app.onteardown = async () => {
return {}
}
await app.connect(new PostMessageTransport())
```
### Host Styling
Use host CSS variables for theme integration:
```css
.container {
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-family: var(--font-sans);
border-radius: var(--border-radius-md);
}
```
Key variable groups: `--color-background-*`, `--color-text-*`, `--color-border-*`, `--font-sans`, `--font-mono`, `--font-text-*-size`, `--font-heading-*-size`, `--border-radius-*`. See `src/spec.types.ts` for the full list.
For React apps, use the `useApp` and `useHostStyles` hooks instead — see `basic-server-react/` for the pattern.
## Optional Enhancements
### App-Only Helper Tools
Tools the UI calls but the model doesn't need to invoke directly (polling, pagination, chunk loading):
```typescript
registerAppTool(
server,
'poll-data',
{
description: 'Polls latest data for the UI',
_meta: { ui: { resourceUri, visibility: ['app'] } },
},
async () => {
const data = await getLatestData()
return { content: [{ type: 'text', text: JSON.stringify(data) }] }
}
)
```
The UI calls these via `app.callServerTool("poll-data", {})`.
### CSP Configuration
If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains:
```typescript
registerAppResource(
server,
{
uri: resourceUri,
name: 'My Tool UI',
mimeType: RESOURCE_MIME_TYPE,
_meta: {
ui: {
connectDomains: ['api.example.com'], // fetch/XHR targets
resourceDomains: ['cdn.example.com'], // scripts, styles, images
frameDomains: ['embed.example.com'], // nested iframes
},
},
},
async () => {
/* ... */
}
)
```
### Streaming Partial Input
For large tool inputs, show progress during LLM generation:
```typescript
app.ontoolinputpartial = (params) => {
const args = params.arguments // Healed partial JSON - always valid
// Render preview with partial data
}
app.ontoolinput = (params) => {
// Final complete input - switch to full render
}
```
### Graceful Degradation with `getUiCapability()`
Conditionally register App tools only when the client supports UI, falling back to text-only tools:
```typescript
import {
getUiCapability,
registerAppTool,
RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server'
server.server.oninitialized = () => {
const clientCapabilities = server.server.getClientCapabilities()
const uiCap = getUiCapability(clientCapabilities)
if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
// Client supports UI — register App tool
registerAppTool(
server,
'my-tool',
{
description: 'Shows data with interactive UI',
_meta: { ui: { resourceUri } },
},
appToolHandler
)
} else {
// Text-only client — register plain tool
server.tool('my-tool', 'Shows data', { param: z.string() }, plainToolHandler)
}
}
```
### Fullscreen Mode
Allow the UI to expand to fullscreen:
```typescript
app.onhostcontextchanged = (ctx) => {
if (ctx.availableDisplayModes?.includes('fullscreen')) {
fullscreenBtn.style.display = 'block'
}
if (ctx.displayMode) {
container.classList.toggle('fullscreen', ctx.displayMode === 'fullscreen')
}
}
async function toggleFullscreen() {
const newMode = currentMode === 'fullscreen' ? 'inline' : 'fullscreen'
const result = await app.requestDisplayMode({ mode: newMode })
currentMode = result.mode
}
```
## Common Mistakes to Avoid
1. **Forgetting text `content` fallback** — Always include `content` array with text for non-UI hosts
2. **Registering handlers after `connect()`** — Register ALL handlers BEFORE calling `app.connect()`
3. **Missing `vite-plugin-singlefile`** — Without it, assets won't load in the sandboxed iframe
4. **Forgetting resource registration** — The tool references a `resourceUri` that must have a matching resource
5. **Hardcoding styles** — Use host CSS variables (`var(--color-*)`) for theme integration
6. **Not handling safe area insets** — Always apply `ctx.safeAreaInsets` in `onhostcontextchanged`
## Testing
### Using basic-host
Test the enhanced server with the basic-host example:
```bash
# Terminal 1: Build and run your server
npm run build && npm run serve
# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080
```
Configure `SERVERS` with a JSON array of your server URLs (default: `http://localhost:3001/mcp`).
### Verify
1. Plain tools still work and return text output
2. App tools render their UI in the iframe
3. `ontoolinput` handler fires with tool arguments
4. `ontoolresult` handler fires with tool result
5. Host styling (theme, fonts, colors) applies correctly

View File

@@ -0,0 +1,557 @@
---
name: convert-web-app
description: This skill should be used when the user asks to "add MCP App support to my web app", "turn my web app into a hybrid MCP App", "make my web page work as an MCP App too", "wrap my existing UI as an MCP App", "convert iframe embed to MCP App", "turn my SPA into an MCP App", or needs to add MCP App support to an existing web application while keeping it working standalone. Provides guidance for analyzing existing web apps and creating a hybrid web + MCP App with server-side tool and resource registration.
---
# Add MCP App Support to a Web App
Add MCP App support to an existing web application so it works both as a standalone web app **and** as an MCP App that renders inline in MCP-enabled hosts like Claude Desktop — from a single codebase.
## How It Works
The existing web app stays intact. A thin initialization layer detects whether the app is running inside an MCP host or as a regular web page, and fetches parameters from the appropriate source. A new MCP server wraps the app's bundled HTML as a resource and registers a tool to display it.
```
Standalone: Browser loads page → App reads URL params / APIs → renders
MCP App: Host calls tool → Server returns result → Host renders app in iframe → App reads MCP lifecycle → renders
```
The app's rendering logic is shared — only the data source changes.
## Getting Reference Code
Clone the SDK repository for working examples and API documentation:
```bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
```
### API Reference (Source Files)
Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
| File | Contents |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, tool visibility options |
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
| `src/react/useApp.tsx` | `useApp` hook for React apps |
| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
### Framework Templates
Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
| Template | Key Files |
| ------------------------- | --------------------------------------------------- |
| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
| `basic-server-vue/` | `server.ts`, `src/App.vue` |
| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
### Reference Examples
| Example | Relevant Pattern |
| ------------------------------ | -------------------------------------------------------------------- |
| `examples/map-server/` | External API integration + CSP (`connectDomains`, `resourceDomains`) |
| `examples/sheet-music-server/` | Library that loads external assets (soundfonts) |
| `examples/pdf-server/` | Binary content handling + app-only helper tools |
## Step 1: Analyze the Existing Web App
Before writing any code, examine the existing web app to plan what needs to change.
### What to Investigate
1. **Data sources** — How does the app get its data? (URL params, API calls, props, hardcoded, localStorage)
2. **External dependencies** — CDN scripts, fonts, API endpoints, iframe embeds, WebSocket connections
3. **Build system** — Current bundler (Webpack, Vite, Rollup, none), framework (React, Vue, vanilla), entry points
4. **User interactions** — Does the app have inputs/forms that should map to tool parameters?
5. **Runtime detection** — How to tell if the app is running inside an MCP host (e.g., check the current origin, a query param, or whether `window.parent !== window`)
Present findings to the user and confirm the approach.
### Data Source Mapping
In hybrid mode, the app keeps its existing data sources for standalone use and adds MCP equivalents:
| Standalone data source | MCP App equivalent |
| ----------------------------- | ----------------------------------------------------------------------------------------------- |
| URL query parameters | `ontoolinput` / `ontoolresult` `arguments` or `structuredContent` |
| REST API calls | `app.callServerTool()` to server-side tools, or keep direct API calls with CSP `connectDomains` |
| Props / component inputs | `ontoolinput` `arguments` |
| localStorage / sessionStorage | Not available in sandboxed iframe — pass via `structuredContent` or server-side state |
| WebSocket connections | Keep with CSP `connectDomains`, or convert to polling via app-only tools |
| Hardcoded data | Move to tool `structuredContent` to make it dynamic |
## Step 2: Investigate CSP Requirements
MCP Apps HTML runs in a sandboxed iframe with no same-origin server. **Every** external origin must be declared in CSP — missing origins fail silently.
**Before writing any code**, build the app and investigate all origins it references:
1. Build the app using the existing build command
2. Search the resulting HTML, CSS, and JS for **every** origin (not just "external" origins — every network request will need CSP approval)
3. For each origin found, trace back to source:
- If it comes from a constant → universal (same in dev and prod)
- If it comes from an env var or conditional → note the mechanism and identify both dev and prod values
4. Check for third-party libraries that may make their own requests (analytics, error tracking, etc.)
**Document your findings** as three lists, and note for each origin whether it's universal, dev-only, or prod-only:
- **resourceDomains**: origins serving images, fonts, styles, scripts
- **connectDomains**: origins for API/fetch requests
- **frameDomains**: origins for nested iframes
If no origins are found, the app may not need custom CSP domains.
## Step 3: Set Up the MCP Server
Create a new MCP server with tool and resource registration. This wraps the existing web app for MCP hosts.
### Dependencies
```bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
npm install -D tsx vite vite-plugin-singlefile
```
Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
### Server Code
Create `server.ts`:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
registerAppTool,
registerAppResource,
RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server'
import fs from 'node:fs/promises'
import path from 'node:path'
import { z } from 'zod'
const server = new McpServer({ name: 'my-app', version: '1.0.0' })
const resourceUri = 'ui://my-app/mcp-app.html'
// Register the tool — inputSchema maps to the app's data sources
registerAppTool(
server,
'show-app',
{
description: 'Displays the app with the given parameters',
inputSchema: { query: z.string().describe('The search query') },
_meta: { ui: { resourceUri } },
},
async (args) => {
// Process args server-side if needed
return {
content: [{ type: 'text', text: `Showing app for: ${args.query}` }],
structuredContent: { query: args.query },
}
}
)
// Register the HTML resource
registerAppResource(
server,
{
uri: resourceUri,
name: 'My App UI',
mimeType: RESOURCE_MIME_TYPE,
// Add CSP domains from Step 2 if needed:
// _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } },
},
async () => {
const html = await fs.readFile(
path.resolve(import.meta.dirname, 'dist', 'mcp-app.html'),
'utf-8'
)
return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }
}
)
// Start the server
const transport = new StdioServerTransport()
await server.connect(transport)
```
### Package Scripts
Add to `package.json`:
```json
{
"scripts": {
"build:ui": "vite build",
"build:server": "tsc",
"build": "npm run build:ui && npm run build:server",
"serve": "tsx server.ts"
}
}
```
## Step 4: Adapt the Build Pipeline
The MCP App build must produce a single HTML file using `vite-plugin-singlefile`. The standalone web app build stays unchanged.
### Vite Configuration
Create or update `vite.config.ts`. If the app already uses Vite, add `vite-plugin-singlefile` and a separate entry point for the MCP App build. If it uses another bundler, add a Vite config alongside for the MCP App build only.
```typescript
import { defineConfig } from 'vite'
import { viteSingleFile } from 'vite-plugin-singlefile'
export default defineConfig({
plugins: [viteSingleFile()],
build: {
outDir: 'dist',
rollupOptions: {
input: 'mcp-app.html',
},
},
})
```
Add framework-specific Vite plugins as needed (e.g., `@vitejs/plugin-react` for React, `@vitejs/plugin-vue` for Vue).
### HTML Entry Point
Create `mcp-app.html` as a separate entry point for the MCP App build. This can point to the same app code — the runtime detection handles the rest:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
```
### Two-Phase Build
1. Vite bundles the UI → `dist/mcp-app.html` (single file with all assets inlined)
2. Server is compiled separately (TypeScript → JavaScript)
The standalone web app continues to build and deploy as before.
## Step 5: Add MCP App Initialization Alongside Existing Logic
This is the core step. Instead of replacing the app's data sources, add an alternative initialization path for MCP mode. The app detects its environment at startup and reads parameters from the right source.
### The Hybrid Pattern
```typescript
import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps'
// Detect whether we're running inside an MCP host.
// Choose a detection method that fits the app:
// - Origin check: window.location.origin !== 'https://myhost.com'
// - Null origin (sandboxed iframe): window.location.origin === 'null'
// - Query param: new URL(location.href).searchParams.has('mcp')
const isMcpApp = window.location.origin === 'null'
async function getParameters(): Promise<Record<string, string>> {
if (isMcpApp) {
// Running as MCP App — get params from tool lifecycle
const app = new App({ name: 'My App', version: '1.0.0' })
// Register handlers BEFORE connect()
const params = await new Promise<Record<string, string>>((resolve) => {
app.ontoolresult = (result) => resolve(result.structuredContent ?? {})
})
await app.connect(new PostMessageTransport())
return params
} else {
// Running as standalone web app — get params from URL
return Object.fromEntries(new URL(location.href).searchParams)
}
}
async function main() {
const params = await getParameters()
renderApp(params) // Same rendering logic for both modes
}
main().catch(console.error)
```
### URL Parameters (Hybrid)
```typescript
// Before (standalone only):
const query = new URL(location.href).searchParams.get('q')
renderApp(query)
// After (hybrid):
async function getQuery(): Promise<string> {
if (isMcpApp) {
const app = new App({ name: 'My App', version: '1.0.0' })
return new Promise((resolve) => {
app.ontoolinput = (params) => resolve(params.arguments?.q ?? '')
app.connect(new PostMessageTransport())
})
}
return new URL(location.href).searchParams.get('q') ?? ''
}
const query = await getQuery()
renderApp(query) // Unchanged rendering logic
```
### API Calls (Hybrid)
```typescript
// Before (standalone only):
const data = await fetch('/api/data').then((r) => r.json())
// After (hybrid):
async function fetchData(): Promise<any> {
if (isMcpApp) {
const result = await app.callServerTool('fetch-data', {})
return result.structuredContent
}
return fetch('/api/data').then((r) => r.json())
}
```
Or keep direct API calls in both modes with CSP `connectDomains`:
```typescript
// API calls can stay unchanged if the API is external and the CSP declares the domain
// Declare connectDomains: ["api.example.com"] in the resource registration
```
### localStorage / sessionStorage (Hybrid)
```typescript
// Before (standalone only):
const saved = localStorage.getItem('settings')
// After (hybrid) — localStorage isn't available in sandboxed iframes:
function getSettings(): any {
if (isMcpApp) {
// Will be provided via tool result
return null // or a default
}
return JSON.parse(localStorage.getItem('settings') ?? 'null')
}
```
### Complete Hybrid Example
```typescript
import {
App,
PostMessageTransport,
applyDocumentTheme,
applyHostStyleVariables,
applyHostFonts,
} from '@modelcontextprotocol/ext-apps'
const isMcpApp = window.location.origin === 'null'
async function initMcpApp(): Promise<Record<string, any>> {
const app = new App({ name: 'My App', version: '1.0.0' })
// Register ALL handlers BEFORE connect()
const params = await new Promise<Record<string, any>>((resolve) => {
app.ontoolinput = (input) => resolve(input.arguments ?? {})
})
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme)
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables)
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts)
if (ctx.safeAreaInsets) {
const { top, right, bottom, left } = ctx.safeAreaInsets
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`
}
}
app.onteardown = async () => {
return {}
}
await app.connect(new PostMessageTransport())
return params
}
async function initStandaloneApp(): Promise<Record<string, any>> {
return Object.fromEntries(new URL(location.href).searchParams)
}
async function main() {
const params = isMcpApp ? await initMcpApp() : await initStandaloneApp()
renderApp(params) // Same rendering logic — no fork needed
}
main().catch(console.error)
```
## Step 6: Add Host Styling Integration (MCP Mode Only)
When running as an MCP App, integrate with host styling for theme consistency. Use CSS variable fallbacks so the app looks correct in both modes.
**Vanilla JS** — use helper functions:
```typescript
import {
applyDocumentTheme,
applyHostStyleVariables,
applyHostFonts,
} from '@modelcontextprotocol/ext-apps'
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme)
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables)
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts)
}
```
**React** — use hooks:
```typescript
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react'
const { app } = useApp({ appInfo, capabilities, onAppCreated })
useHostStyles(app)
```
**Using variables in CSS** — use `var()` with fallbacks so standalone mode still looks right:
```css
.container {
background: var(--color-background-secondary, #f5f5f5);
color: var(--color-text-primary, #333);
font-family: var(--font-sans, system-ui);
border-radius: var(--border-radius-md, 8px);
}
```
Key variable groups: `--color-background-*`, `--color-text-*`, `--color-border-*`, `--font-sans`, `--font-mono`, `--font-text-*-size`, `--font-heading-*-size`, `--border-radius-*`. See `src/spec.types.ts` for the full list.
## Optional Enhancements
### App-Only Helper Tools
For data the UI needs to poll or fetch that the model doesn't need to call directly:
```typescript
registerAppTool(
server,
'refresh-data',
{
description: 'Fetches latest data for the UI',
_meta: { ui: { resourceUri, visibility: ['app'] } },
},
async () => {
const data = await getLatestData()
return { content: [{ type: 'text', text: JSON.stringify(data) }] }
}
)
```
The UI calls these via `app.callServerTool("refresh-data", {})`.
### Streaming Partial Input
For large tool inputs, use `ontoolinputpartial` to show progress during LLM generation:
```typescript
app.ontoolinputpartial = (params) => {
const args = params.arguments // Healed partial JSON - always valid
renderPreview(args)
}
app.ontoolinput = (params) => {
renderFull(params.arguments)
}
```
### Fullscreen Mode
```typescript
app.onhostcontextchanged = (ctx) => {
if (ctx.availableDisplayModes?.includes('fullscreen')) {
fullscreenBtn.style.display = 'block'
}
if (ctx.displayMode) {
container.classList.toggle('fullscreen', ctx.displayMode === 'fullscreen')
}
}
async function toggleFullscreen() {
const newMode = currentMode === 'fullscreen' ? 'inline' : 'fullscreen'
const result = await app.requestDisplayMode({ mode: newMode })
currentMode = result.mode
}
```
### Text Fallback
Always provide a `content` array for non-UI hosts:
```typescript
return {
content: [{ type: 'text', text: 'Fallback description of the result' }],
structuredContent: {
/* data for the UI */
},
}
```
## Common Mistakes to Avoid
1. **Forgetting CSP declarations for external origins** — fails silently in the sandboxed iframe
2. **Using `localStorage` / `sessionStorage` in MCP mode** — not available in sandboxed iframe; use fallbacks or pass via `structuredContent`
3. **Missing `vite-plugin-singlefile`** — external assets won't load in the iframe
4. **Registering handlers after `connect()`** — register ALL handlers BEFORE calling `app.connect()`
5. **Hardcoding styles without fallbacks** — use host CSS variables with `var(..., fallback)` so both modes look correct
6. **Not handling safe area insets** — always apply `ctx.safeAreaInsets` in `onhostcontextchanged`
7. **Forgetting text `content` fallback** — always provide `content` array for non-UI hosts
8. **Forgetting resource registration** — the tool references a `resourceUri` that must have a matching resource
9. **Replacing standalone logic instead of branching** — keep the original data sources intact; add the MCP path alongside them
## Testing
### Using basic-host
Test the MCP App mode with the basic-host example:
```bash
# Terminal 1: Build and run your server
npm run build && npm run serve
# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080
```
Configure `SERVERS` with a JSON array of your server URLs (default: `http://localhost:3001/mcp`).
### Verify
1. **MCP mode**: App loads in basic-host without console errors
2. `ontoolinput` handler fires with tool arguments
3. `ontoolresult` handler fires with tool result
4. Host styling (theme, fonts, colors) applies correctly
5. External resources load (if CSP domains are configured)
6. **Standalone mode**: App still works when opened directly in a browser

View File

@@ -0,0 +1,367 @@
---
name: create-mcp-app
description: This skill should be used when the user asks to "create an MCP App", "add a UI to an MCP tool", "build an interactive MCP View", "scaffold an MCP App", or needs guidance on MCP Apps SDK patterns, UI-resource registration, MCP App lifecycle, or host integration. Provides comprehensive guidance for building MCP Apps with interactive UIs.
---
# Create MCP App
Build interactive UIs that run inside MCP-enabled hosts like Claude Desktop. An MCP App combines an MCP tool with an HTML resource to display rich, interactive content.
## Core Concept: Tool + Resource
Every MCP App requires two parts linked together:
1. **Tool** - Called by the LLM/host, returns data
2. **Resource** - Serves the bundled HTML UI that displays the data
3. **Link** - The tool's `_meta.ui.resourceUri` references the resource
```
Host calls tool → Server returns result → Host renders resource UI → UI receives result
```
## Quick Start Decision Tree
### Framework Selection
| Framework | SDK Support | Best For |
| ----------------------- | ---------------------- | -------------------------------- |
| React | `useApp` hook provided | Teams familiar with React |
| Vanilla JS | Manual lifecycle | Simple apps, no build complexity |
| Vue/Svelte/Preact/Solid | Manual lifecycle | Framework preference |
### Project Context
**Adding to existing MCP server:**
- Import `registerAppTool`, `registerAppResource` from SDK
- Add tool registration with `_meta.ui.resourceUri`
- Add resource registration serving bundled HTML
**Creating new MCP server:**
- Set up server with transport (stdio or HTTP)
- Register tools and resources
- Configure build system with `vite-plugin-singlefile`
## Getting Reference Code
Clone the SDK repository for working examples and API documentation:
```bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
```
### Framework Templates
Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
| Template | Key Files |
| ------------------------- | --------------------------------------------------- |
| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
| `basic-server-vue/` | `server.ts`, `src/App.vue` |
| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
Each template includes:
- Complete `server.ts` with `registerAppTool` and `registerAppResource`
- Client-side app with all lifecycle handlers
- `vite.config.ts` with `vite-plugin-singlefile`
- `package.json` with all required dependencies
- `.gitignore` excluding `node_modules/` and `dist/`
### API Reference (Source Files)
Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
| File | Contents |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, tool visibility options |
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
| `src/react/useApp.tsx` | `useApp` hook for React apps |
| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
### Advanced Examples
| Example | Pattern Demonstrated |
| --------------------------------- | ------------------------------------------------------------------------------------------- |
| `examples/shadertoy-server/` | **Streaming partial input** + visibility-based pause/play (best practice for large inputs) |
| `examples/wiki-explorer-server/` | `callServerTool` for interactive data fetching |
| `examples/system-monitor-server/` | Polling pattern with interval management |
| `examples/video-resource-server/` | Binary/blob resources |
| `examples/sheet-music-server/` | `ontoolinput` - processing tool args before execution completes |
| `examples/threejs-server/` | `ontoolinputpartial` - streaming/progressive rendering |
| `examples/map-server/` | `updateModelContext` - keeping model informed of UI state |
| `examples/transcript-server/` | `updateModelContext` + `sendMessage` - background context updates + user-initiated messages |
| `examples/basic-host/` | Reference host implementation using `AppBridge` |
## Critical Implementation Notes
### Adding Dependencies
Use `npm install` to add dependencies rather than manually writing version numbers:
```bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
```
This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
### TypeScript Server Execution
Use `tsx` as a devDependency for running TypeScript server files:
```bash
npm install -D tsx
```
```json
"scripts": {
"serve": "tsx server.ts"
}
```
Note: The SDK examples use `bun` but generated projects should use `tsx` for broader compatibility.
### Handler Registration Order
Register ALL handlers BEFORE calling `app.connect()`:
```typescript
const app = new App({ name: 'My App', version: '1.0.0' })
// Register handlers first
app.ontoolinput = (params) => {
/* handle input */
}
app.ontoolresult = (result) => {
/* handle result */
}
app.onhostcontextchanged = (ctx) => {
/* handle context */
}
app.onteardown = async () => {
return {}
}
// Then connect
await app.connect()
```
### Tool Visibility
Control who can access tools via `_meta.ui.visibility`:
```typescript
// Default: visible to both model and app
_meta: { ui: { resourceUri, visibility: ["model", "app"] } }
// UI-only (hidden from model) - for refresh buttons, form submissions
_meta: { ui: { resourceUri, visibility: ["app"] } }
// Model-only (app cannot call)
_meta: { ui: { resourceUri, visibility: ["model"] } }
```
### Host Styling Integration
**Vanilla JS** - Use helper functions:
```typescript
import {
applyDocumentTheme,
applyHostStyleVariables,
applyHostFonts,
} from '@modelcontextprotocol/ext-apps'
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme)
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables)
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts)
}
```
**React** - Use hooks:
```typescript
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react'
const { app } = useApp({ appInfo, capabilities, onAppCreated })
useHostStyles(app) // Injects CSS variables to document, making var(--*) available
```
**Using variables in CSS** - After applying, use `var()`:
```css
.container {
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-family: var(--font-sans);
border-radius: var(--border-radius-md);
}
.code {
font-family: var(--font-mono);
font-size: var(--font-text-sm-size);
line-height: var(--font-text-sm-line-height);
color: var(--color-text-secondary);
}
.heading {
font-size: var(--font-heading-lg-size);
font-weight: var(--font-weight-semibold);
}
```
Key variable groups: `--color-background-*`, `--color-text-*`, `--color-border-*`, `--font-sans`, `--font-mono`, `--font-text-*-size`, `--font-heading-*-size`, `--border-radius-*`. See `src/spec.types.ts` for full list.
### Safe Area Handling
Always respect `safeAreaInsets`:
```typescript
app.onhostcontextchanged = (ctx) => {
if (ctx.safeAreaInsets) {
const { top, right, bottom, left } = ctx.safeAreaInsets
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`
}
}
```
### Streaming Partial Input
For large tool inputs, use `ontoolinputpartial` to show progress during LLM generation. The partial JSON is healed (always valid), enabling progressive UI updates.
**Spec:** [ui/notifications/tool-input-partial](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx#streaming-tool-input)
```typescript
app.ontoolinputpartial = (params) => {
const args = params.arguments // Healed partial JSON - always valid, fields appear as generated
// Use args directly for progressive rendering
}
app.ontoolinput = (params) => {
// Final complete input - switch from preview to full render
}
```
**Use cases:**
| Pattern | Example |
|---------|---------|
| Code preview | Show streaming code in `<pre>`, render on complete (`examples/shadertoy-server/`) |
| Progressive form | Fill form fields as they stream in |
| Live chart | Add data points to chart as array grows |
| Partial render | Render incomplete structured data (tables, lists, trees) |
**Simple pattern (code preview):**
```typescript
app.ontoolinputpartial = (params) => {
codePreview.textContent = params.arguments?.code ?? ''
codePreview.style.display = 'block'
canvas.style.display = 'none'
}
app.ontoolinput = (params) => {
codePreview.style.display = 'none'
canvas.style.display = 'block'
render(params.arguments)
}
```
### Visibility-Based Resource Management
Pause expensive operations (animations, WebGL, polling) when view scrolls out of viewport:
```typescript
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animation.play() // or: startPolling(), shaderToy.play()
} else {
animation.pause() // or: stopPolling(), shaderToy.pause()
}
})
})
observer.observe(document.querySelector('.main'))
```
### Fullscreen Mode
Request fullscreen via `app.requestDisplayMode()`. Check availability in host context:
```typescript
let currentMode: 'inline' | 'fullscreen' = 'inline'
app.onhostcontextchanged = (ctx) => {
// Check if fullscreen available
if (ctx.availableDisplayModes?.includes('fullscreen')) {
fullscreenBtn.style.display = 'block'
}
// Track current mode
if (ctx.displayMode) {
currentMode = ctx.displayMode
container.classList.toggle('fullscreen', currentMode === 'fullscreen')
}
}
async function toggleFullscreen() {
const newMode = currentMode === 'fullscreen' ? 'inline' : 'fullscreen'
const result = await app.requestDisplayMode({ mode: newMode })
currentMode = result.mode
}
```
**CSS pattern** - Remove border radius in fullscreen:
```css
.main {
border-radius: var(--border-radius-lg);
overflow: hidden;
}
.main.fullscreen {
border-radius: 0;
}
```
See `examples/shadertoy-server/` for complete implementation.
## Common Mistakes to Avoid
1. **Handlers after connect()** - Register ALL handlers BEFORE calling `app.connect()`
2. **Missing single-file bundling** - Must use `vite-plugin-singlefile`
3. **Forgetting resource registration** - Both tool AND resource must be registered
4. **Missing resourceUri link** - Tool must have `_meta.ui.resourceUri`
5. **Ignoring safe area insets** - Always handle `ctx.safeAreaInsets`
6. **No text fallback** - Always provide `content` array for non-UI hosts
7. **Hardcoded styles** - Use host CSS variables for theme integration
8. **No streaming for large inputs** - Use `ontoolinputpartial` to show progress during generation
## Testing
### Using basic-host
Test MCP Apps locally with the basic-host example:
```bash
# Terminal 1: Build and run your server
npm run build && npm run serve
# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080
```
Configure `SERVERS` with a JSON array of your server URLs (default: `http://localhost:3001/mcp`).
### Debug with sendLog
Send debug logs to the host application (rather than just the iframe's dev console):
```typescript
await app.sendLog({ level: 'info', data: 'Debug message' })
await app.sendLog({ level: 'error', data: { error: err.message } })
```

View File

@@ -0,0 +1,170 @@
---
name: migrate-oai-app
description: This skill should be used when the user asks to "migrate from OpenAI Apps SDK", "convert OpenAI App to MCP", "port from window.openai", "migrate from skybridge", "convert openai/outputTemplate", or needs guidance on converting OpenAI Apps SDK applications to MCP Apps SDK. Provides step-by-step migration guidance with API mapping tables.
---
# Migrate OpenAI App to MCP
Migrate existing OpenAI Apps SDK applications to the MCP Apps SDK (`@modelcontextprotocol/ext-apps`). The MCP Apps SDK provides a standardized, open protocol for interactive UIs in conversational clients.
## Best Practices
- Use your package manager to add dependencies (e.g., `npm install`, `pnpm add`, `yarn add`) instead of manually writing version numbers. This lets the package manager resolve the latest compatible versions. Never specify version numbers from memory.
- Preemptively add a final todo item with this exact wording: "Re-read the 'Before Finishing' checklist in this skill and address each checkbox individually, stating what you did for each one, before marking this todo complete."
## Getting Reference Code
Clone the SDK repository for complete migration documentation and working examples:
```bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
```
### Migration Reference Guide
Read the migration reference guide with "before/after" mapping tables: `/tmp/mcp-ext-apps/docs/migrate_from_openai_apps.md`
### API Reference (Source Files)
Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/*`:
| File | Contents |
| ---------------------- | ---------------------------------------- |
| `src/app.ts` | `App` class, handlers, lifecycle |
| `src/server/index.ts` | `registerAppTool`, `registerAppResource` |
| `src/spec.types.ts` | Type definitions |
| `src/react/useApp.tsx` | `useApp` hook for React apps |
| `src/react/use*.ts*` | Other `use*` hooks for React apps |
### Front-End Framework Examples
See `/tmp/mcp-ext-apps/examples/basic-server-{framework}/` for basic SDK usage examples organized by front-end framework:
| Template | Key Files |
| ------------------------- | --------------------------------------------------- |
| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
| `basic-server-vue/` | `server.ts`, `src/App.vue` |
| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
## CSP Investigation
MCP Apps HTML is served as an MCP resource, not as a web page, and runs in a sandboxed iframe with no same-origin server. **Every** origin must be declared in CSP—including the origin serving your JS/CSS bundles (`localhost` in dev, your CDN in production). Missing origins fail silently.
**Before writing any migration code**, build the app and investigate all origins it references:
1. Build the app using the existing build command
2. Search the resulting HTML, CSS, and JS for **every** origin (not just "external" origins—every network request will need CSP approval)
3. For each origin found, trace back to source:
- If it comes from a constant → universal (same in dev and prod)
- If it comes from an env var or conditional → note the mechanism and identify both dev and prod values
4. Check for third-party libraries that may make their own requests (analytics, error tracking, etc.)
**Document your findings** as three lists, and note for each origin whether it's universal, dev-only, or prod-only:
- **resourceDomains**: origins serving images, fonts, styles, scripts
- **connectDomains**: origins for API/fetch requests
- **frameDomains**: origins for nested iframes
If no origins are found, the app may not need custom CSP domains.
## CORS Configuration
MCP clients make cross-origin requests. If using Express, `app.use(cors())` handles this.
For raw HTTP servers, configure standard CORS and additionally:
- Allow headers: `mcp-session-id`, `mcp-protocol-version`, `last-event-id`
- Expose headers: `mcp-session-id`
## Key Conceptual Changes
### Server-Side
Use `registerAppTool()` and `registerAppResource()` helpers instead of raw `server.registerTool()` / `server.registerResource()`. These helpers handle the MCP Apps metadata format automatically.
See `/tmp/mcp-ext-apps/docs/migrate_from_openai_apps.md` for server-side mapping tables.
### Client-Side
The fundamental paradigm shift: OpenAI uses a synchronous global object (`window.openai.toolInput`, `window.openai.theme`) that's pre-populated before your code runs. MCP Apps uses an `App` instance with async event handlers.
Key differences:
- Create an `App` instance and register handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`) **before** calling `connect()`. (Events may fire immediately after connection, so handlers must be registered first.)
- Access tool data via handlers: `app.ontoolinput` for `window.openai.toolInput`, `app.ontoolresult` for `window.openai.toolOutput`.
- Access host environment (theme, locale, etc.) via `app.getHostContext()`.
For React apps, the `useApp` hook manages this lifecycle automatically—see `basic-server-react/` for the pattern.
See `/tmp/mcp-ext-apps/docs/migrate_from_openai_apps.md` for client-side mapping tables.
### Features Not Yet Available in MCP Apps
These OpenAI features don't have MCP equivalents yet:
**Server-side:**
| OpenAI Feature | Status/Workaround |
|----------------|-------------------|
| `_meta["openai/toolInvocation/invoking"]` / `_meta["openai/toolInvocation/invoked"]` | Progress indicators not yet available |
| `_meta["openai/widgetDescription"]` | Use `app.updateModelContext()` for dynamic context |
**Client-side:**
| OpenAI Feature | Status/Workaround |
|----------------|-------------------|
| `window.openai.widgetState` / `setWidgetState()` | Use `localStorage` or server-side state |
| `window.openai.uploadFile()` / `getFileDownloadUrl()` | File operations not yet available |
| `window.openai.requestModal()` / `requestClose()` | Modal management not yet available |
| `window.openai.view` | Not yet available |
## Before Finishing
Slow down and carefully follow each item in this checklist:
- [ ] Search for and migrate any remaining server-side OpenAI patterns:
| Pattern | Indicates |
| --------------------------- | ----------------------------------------------------------------- |
| `"openai/` | Old metadata keys → `_meta.ui.*` |
| `text/html+skybridge` | Old MIME type → `RESOURCE_MIME_TYPE` constant |
| `text/html;profile=mcp-app` | New MIME type, but prefer `RESOURCE_MIME_TYPE` constant |
| `_domains"` or `_domains:` | snake_case CSP → camelCase (`connect_domains` → `connectDomains`) |
- [ ] Search for and migrate any remaining client-side OpenAI patterns:
| Pattern | Indicates |
| -------------------------- | --------------------------------------------------------- |
| `window.openai.toolInput` | Old global → `params.arguments` in `ontoolinput` handler |
| `window.openai.toolOutput` | Old global → `params.structuredContent` in `ontoolresult` |
| `window.openai` | Old global API → `App` instance methods |
- [ ] For each origin from your CSP investigation, show where it appears in the `registerAppResource()` CSP config. **Every** origin from the CSP investigation (universal, dev-only, prod-only) must be included in the CSP config—MCP Apps HTML runs in a sandboxed iframe **with no same-origin server**. If an origin was not included in the CSP config, add it now.
- [ ] For each conditional (dev-only, prod-only) origin from your CSP investigation, show the code where the same configuration setting (env var, config file, etc.) controls both the runtime URL and the CSP entry. If the CSP has a hardcoded origin that should be conditional, fix it now—the app must be production-ready.
## Testing
### Using basic-host
Test the migrated app with the basic-host example:
```bash
# Terminal 1: Build and run your server
npm run build && npm run serve
# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080
```
### Verify Runtime Behavior
Once the app loads in basic-host, confirm:
1. App loads without console errors
2. `ontoolinput` handler fires with tool arguments
3. `ontoolresult` handler fires with tool result