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
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:
431
apps/mcp-app/.claude/skills/add-app-to-server/SKILL.md
Normal file
431
apps/mcp-app/.claude/skills/add-app-to-server/SKILL.md
Normal 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
|
||||
557
apps/mcp-app/.claude/skills/convert-web-app/SKILL.md
Normal file
557
apps/mcp-app/.claude/skills/convert-web-app/SKILL.md
Normal 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
|
||||
367
apps/mcp-app/.claude/skills/create-mcp-app/SKILL.md
Normal file
367
apps/mcp-app/.claude/skills/create-mcp-app/SKILL.md
Normal 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 } })
|
||||
```
|
||||
170
apps/mcp-app/.claude/skills/migrate-oai-app/SKILL.md
Normal file
170
apps/mcp-app/.claude/skills/migrate-oai-app/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user