Refactor create-mcp-app skill to reference patterns.md (#416)
Move detailed implementation recipes (tool visibility, host styling, streaming, fullscreen, etc.) out of the skill and point to `docs/patterns.md` instead. This keeps the skill focused on essentials while the patterns doc provides comprehensive examples. Key changes: - Fix protocol flow diagram: host renders UI before server returns result - Update template file list to reflect actual contents (`main.ts`, `global.css`) - Add critical CSP/CORS warnings to "Common Mistakes" section - Replace "Advanced Examples" table with categorized patterns list - Add note clarifying `basic-host` is for testing, not a host behavior guarantee Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c000ba6dd5
commit
61951a5c78
1 changed files with 47 additions and 205 deletions
|
|
@ -13,11 +13,10 @@ Every MCP App requires two parts linked together:
|
||||||
|
|
||||||
1. **Tool** - Called by the LLM/host, returns data
|
1. **Tool** - Called by the LLM/host, returns data
|
||||||
2. **Resource** - Serves the bundled HTML UI that displays the data
|
2. **Resource** - Serves the bundled HTML UI that displays the data
|
||||||
3. **Link** - The tool's `_meta.ui.resourceUri` references the resource
|
|
||||||
|
|
||||||
```
|
The tool's `_meta.ui.resourceUri` references the resource's URI.
|
||||||
Host calls tool → Server returns result → Host renders resource UI → UI receives result
|
|
||||||
```
|
Host calls tool → Host renders resource UI → Server returns result → UI receives result.
|
||||||
|
|
||||||
## Quick Start Decision Tree
|
## Quick Start Decision Tree
|
||||||
|
|
||||||
|
|
@ -63,10 +62,12 @@ Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
|
||||||
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
|
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
|
||||||
|
|
||||||
Each template includes:
|
Each template includes:
|
||||||
- Complete `server.ts` with `registerAppTool` and `registerAppResource`
|
- `server.ts` with `registerAppTool` and `registerAppResource`
|
||||||
- Client-side app with all lifecycle handlers
|
- `main.ts` entry point with HTTP and stdio transport setup
|
||||||
- `vite.config.ts` with `vite-plugin-singlefile`
|
- Client-side app (e.g., `src/mcp-app.ts`, `src/mcp-app.tsx`) with lifecycle handlers
|
||||||
- `package.json` with all required dependencies
|
- `src/global.css` with global styles and host style variable fallbacks
|
||||||
|
- `vite.config.ts` using `vite-plugin-singlefile`
|
||||||
|
- `package.json` with `npm run` scripts and required dependencies
|
||||||
- `.gitignore` excluding `node_modules/` and `dist/`
|
- `.gitignore` excluding `node_modules/` and `dist/`
|
||||||
|
|
||||||
### API Reference (Source Files)
|
### API Reference (Source Files)
|
||||||
|
|
@ -75,54 +76,58 @@ Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
|
||||||
|
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
|
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`, etc.), lifecycle |
|
||||||
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, tool visibility options |
|
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, helper functions |
|
||||||
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
|
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, `McpUiStyleVariableKey` (CSS variable names), `McpUiResourceCsp` (CSP configuration), etc. |
|
||||||
| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
|
| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
|
||||||
| `src/react/useApp.tsx` | `useApp` hook for React apps |
|
| `src/react/useApp.tsx` | `useApp` hook for React apps |
|
||||||
| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
|
|
||||||
|
|
||||||
### Advanced Examples
|
### Advanced Patterns
|
||||||
|
|
||||||
| Example | Pattern Demonstrated |
|
See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes:
|
||||||
|---------|---------------------|
|
|
||||||
| `examples/shadertoy-server/` | **Streaming partial input** + visibility-based pause/play (best practice for large inputs) |
|
- **App-only tools** — `visibility: ["app"]`, hiding tools from model
|
||||||
| `examples/wiki-explorer-server/` | `callServerTool` for interactive data fetching |
|
- **Polling** — real-time dashboards, interval management
|
||||||
| `examples/system-monitor-server/` | Polling pattern with interval management |
|
- **Chunked responses** — large files, pagination, base64 encoding
|
||||||
| `examples/video-resource-server/` | Binary/blob resources |
|
- **Error handling** — `isError`, informing model of failures
|
||||||
| `examples/sheet-music-server/` | `ontoolinput` - processing tool args before execution completes |
|
- **Binary resources** — audio/video/etc via `resources/read`, blob field
|
||||||
| `examples/threejs-server/` | `ontoolinputpartial` - streaming/progressive rendering |
|
- **Network requests** — assets, fetch, CSP, `_meta.ui.csp`, CORS, `_meta.ui.domain`
|
||||||
| `examples/map-server/` | `updateModelContext` - keeping model informed of UI state |
|
- **Host context** — theme, styling, fonts, safe area insets
|
||||||
| `examples/transcript-server/` | `updateModelContext` + `sendMessage` - background context updates + user-initiated messages |
|
- **Fullscreen mode** — `requestDisplayMode`, display mode changes
|
||||||
| `examples/basic-host/` | Reference host implementation using `AppBridge` |
|
- **Model context** — `updateModelContext`, `sendMessage`, keeping model informed
|
||||||
|
- **View state** — `viewUUID`, localStorage, state recovery
|
||||||
|
- **Visibility-based pause** — IntersectionObserver, pausing animations/WebGL
|
||||||
|
- **Streaming input** — `ontoolinputpartial`, progressive rendering
|
||||||
|
|
||||||
|
### Reference Host Implementation
|
||||||
|
|
||||||
|
`/tmp/mcp-ext-apps/examples/basic-host/` shows one way an MCP Apps-capable host could be implemented. Real-world hosts like Claude Desktop are more sophisticated—use basic-host for local testing and protocol understanding, not as a guarantee of host behavior.
|
||||||
|
|
||||||
## Critical Implementation Notes
|
## Critical Implementation Notes
|
||||||
|
|
||||||
### Adding Dependencies
|
### Adding Dependencies
|
||||||
|
|
||||||
Use `npm install` to add dependencies rather than manually writing version numbers:
|
**Always** use `npm install` to add dependencies rather than manually writing version numbers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
|
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod express cors
|
||||||
|
npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors
|
||||||
```
|
```
|
||||||
|
|
||||||
This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
|
This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory.
|
||||||
|
|
||||||
### TypeScript Server Execution
|
### TypeScript Server Execution
|
||||||
|
|
||||||
Use `tsx` as a devDependency for running TypeScript server files:
|
Unless the user has specified otherwise, use `tsx` for running TypeScript server files. For example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -D tsx
|
npm install -D tsx
|
||||||
|
|
||||||
|
npm pkg set scripts.dev="cross-env NODE_ENV=development concurrently 'cross-env INPUT=mcp-app.html vite build --watch' 'tsx --watch main.ts'"
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
> [!NOTE]
|
||||||
"scripts": {
|
> The SDK examples use `bun` but generated projects should default to `tsx` for broader compatibility.
|
||||||
"serve": "tsx server.ts"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The SDK examples use `bun` but generated projects should use `tsx` for broader compatibility.
|
|
||||||
|
|
||||||
### Handler Registration Order
|
### Handler Registration Order
|
||||||
|
|
||||||
|
|
@ -136,182 +141,19 @@ app.ontoolinput = (params) => { /* handle input */ };
|
||||||
app.ontoolresult = (result) => { /* handle result */ };
|
app.ontoolresult = (result) => { /* handle result */ };
|
||||||
app.onhostcontextchanged = (ctx) => { /* handle context */ };
|
app.onhostcontextchanged = (ctx) => { /* handle context */ };
|
||||||
app.onteardown = async () => { return {}; };
|
app.onteardown = async () => { return {}; };
|
||||||
|
// etc.
|
||||||
|
|
||||||
// Then connect
|
// Then connect
|
||||||
await app.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
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
1. **Handlers after connect()** - Register ALL handlers BEFORE calling `app.connect()`
|
1. **No text fallback** - Always provide `content` array for non-UI hosts
|
||||||
2. **Missing single-file bundling** - Must use `vite-plugin-singlefile`
|
2. **Missing CSP configuration** - MCP Apps HTML is served as an MCP resource with no same-origin server; ALL network requests—even to `localhost`—require a CSP configuration
|
||||||
3. **Forgetting resource registration** - Both tool AND resource must be registered
|
3. **CSP or CORS config in wrong _meta object** - `_meta.ui.csp` and `_meta.ui.domain` go in the `contents[]` objects returned by `registerAppResource()`'s read callback, not in `registerAppResource()`'s config object
|
||||||
4. **Missing resourceUri link** - Tool must have `_meta.ui.resourceUri`
|
4. **Handlers after app.connect()** - Register ALL handlers BEFORE calling `app.connect()`
|
||||||
5. **Ignoring safe area insets** - Always handle `ctx.safeAreaInsets`
|
5. **No streaming for large inputs** - Use `ontoolinputpartial` to show progress during input generation
|
||||||
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
|
## Testing
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue