Skip to content

Commit 61951a5

Browse files
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>
1 parent c000ba6 commit 61951a5

1 file changed

Lines changed: 47 additions & 205 deletions

File tree

  • plugins/mcp-apps/skills/create-mcp-app

plugins/mcp-apps/skills/create-mcp-app/SKILL.md

Lines changed: 47 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ Every MCP App requires two parts linked together:
1313

1414
1. **Tool** - Called by the LLM/host, returns data
1515
2. **Resource** - Serves the bundled HTML UI that displays the data
16-
3. **Link** - The tool's `_meta.ui.resourceUri` references the resource
1716

18-
```
19-
Host calls tool → Server returns result → Host renders resource UI → UI receives result
20-
```
17+
The tool's `_meta.ui.resourceUri` references the resource's URI.
18+
19+
Host calls tool → Host renders resource UI → Server returns result → UI receives result.
2120

2221
## Quick Start Decision Tree
2322

@@ -63,10 +62,12 @@ Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
6362
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
6463

6564
Each template includes:
66-
- Complete `server.ts` with `registerAppTool` and `registerAppResource`
67-
- Client-side app with all lifecycle handlers
68-
- `vite.config.ts` with `vite-plugin-singlefile`
69-
- `package.json` with all required dependencies
65+
- `server.ts` with `registerAppTool` and `registerAppResource`
66+
- `main.ts` entry point with HTTP and stdio transport setup
67+
- Client-side app (e.g., `src/mcp-app.ts`, `src/mcp-app.tsx`) with lifecycle handlers
68+
- `src/global.css` with global styles and host style variable fallbacks
69+
- `vite.config.ts` using `vite-plugin-singlefile`
70+
- `package.json` with `npm run` scripts and required dependencies
7071
- `.gitignore` excluding `node_modules/` and `dist/`
7172

7273
### API Reference (Source Files)
@@ -75,54 +76,58 @@ Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
7576

7677
| File | Contents |
7778
|------|----------|
78-
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
79-
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, tool visibility options |
80-
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
79+
| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`, etc.), lifecycle |
80+
| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, helper functions |
81+
| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, `McpUiStyleVariableKey` (CSS variable names), `McpUiResourceCsp` (CSP configuration), etc. |
8182
| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
8283
| `src/react/useApp.tsx` | `useApp` hook for React apps |
83-
| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
84-
85-
### Advanced Examples
86-
87-
| Example | Pattern Demonstrated |
88-
|---------|---------------------|
89-
| `examples/shadertoy-server/` | **Streaming partial input** + visibility-based pause/play (best practice for large inputs) |
90-
| `examples/wiki-explorer-server/` | `callServerTool` for interactive data fetching |
91-
| `examples/system-monitor-server/` | Polling pattern with interval management |
92-
| `examples/video-resource-server/` | Binary/blob resources |
93-
| `examples/sheet-music-server/` | `ontoolinput` - processing tool args before execution completes |
94-
| `examples/threejs-server/` | `ontoolinputpartial` - streaming/progressive rendering |
95-
| `examples/map-server/` | `updateModelContext` - keeping model informed of UI state |
96-
| `examples/transcript-server/` | `updateModelContext` + `sendMessage` - background context updates + user-initiated messages |
97-
| `examples/basic-host/` | Reference host implementation using `AppBridge` |
84+
85+
### Advanced Patterns
86+
87+
See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes:
88+
89+
- **App-only tools**`visibility: ["app"]`, hiding tools from model
90+
- **Polling** — real-time dashboards, interval management
91+
- **Chunked responses** — large files, pagination, base64 encoding
92+
- **Error handling**`isError`, informing model of failures
93+
- **Binary resources** — audio/video/etc via `resources/read`, blob field
94+
- **Network requests** — assets, fetch, CSP, `_meta.ui.csp`, CORS, `_meta.ui.domain`
95+
- **Host context** — theme, styling, fonts, safe area insets
96+
- **Fullscreen mode**`requestDisplayMode`, display mode changes
97+
- **Model context**`updateModelContext`, `sendMessage`, keeping model informed
98+
- **View state**`viewUUID`, localStorage, state recovery
99+
- **Visibility-based pause** — IntersectionObserver, pausing animations/WebGL
100+
- **Streaming input**`ontoolinputpartial`, progressive rendering
101+
102+
### Reference Host Implementation
103+
104+
`/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.
98105

99106
## Critical Implementation Notes
100107

101108
### Adding Dependencies
102109

103-
Use `npm install` to add dependencies rather than manually writing version numbers:
110+
**Always** use `npm install` to add dependencies rather than manually writing version numbers:
104111

105112
```bash
106-
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
113+
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod express cors
114+
npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors
107115
```
108116

109-
This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
117+
This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory.
110118

111119
### TypeScript Server Execution
112120

113-
Use `tsx` as a devDependency for running TypeScript server files:
121+
Unless the user has specified otherwise, use `tsx` for running TypeScript server files. For example:
114122

115123
```bash
116124
npm install -D tsx
117-
```
118125

119-
```json
120-
"scripts": {
121-
"serve": "tsx server.ts"
122-
}
126+
npm pkg set scripts.dev="cross-env NODE_ENV=development concurrently 'cross-env INPUT=mcp-app.html vite build --watch' 'tsx --watch main.ts'"
123127
```
124128

125-
Note: The SDK examples use `bun` but generated projects should use `tsx` for broader compatibility.
129+
> [!NOTE]
130+
> The SDK examples use `bun` but generated projects should default to `tsx` for broader compatibility.
126131
127132
### Handler Registration Order
128133

@@ -136,182 +141,19 @@ app.ontoolinput = (params) => { /* handle input */ };
136141
app.ontoolresult = (result) => { /* handle result */ };
137142
app.onhostcontextchanged = (ctx) => { /* handle context */ };
138143
app.onteardown = async () => { return {}; };
144+
// etc.
139145

140146
// Then connect
141147
await app.connect();
142148
```
143149

144-
### Tool Visibility
145-
146-
Control who can access tools via `_meta.ui.visibility`:
147-
148-
```typescript
149-
// Default: visible to both model and app
150-
_meta: { ui: { resourceUri, visibility: ["model", "app"] } }
151-
152-
// UI-only (hidden from model) - for refresh buttons, form submissions
153-
_meta: { ui: { resourceUri, visibility: ["app"] } }
154-
155-
// Model-only (app cannot call)
156-
_meta: { ui: { resourceUri, visibility: ["model"] } }
157-
```
158-
159-
### Host Styling Integration
160-
161-
**Vanilla JS** - Use helper functions:
162-
```typescript
163-
import { applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
164-
165-
app.onhostcontextchanged = (ctx) => {
166-
if (ctx.theme) applyDocumentTheme(ctx.theme);
167-
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
168-
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
169-
};
170-
```
171-
172-
**React** - Use hooks:
173-
```typescript
174-
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
175-
176-
const { app } = useApp({ appInfo, capabilities, onAppCreated });
177-
useHostStyles(app); // Injects CSS variables to document, making var(--*) available
178-
```
179-
180-
**Using variables in CSS** - After applying, use `var()`:
181-
```css
182-
.container {
183-
background: var(--color-background-secondary);
184-
color: var(--color-text-primary);
185-
font-family: var(--font-sans);
186-
border-radius: var(--border-radius-md);
187-
}
188-
.code {
189-
font-family: var(--font-mono);
190-
font-size: var(--font-text-sm-size);
191-
line-height: var(--font-text-sm-line-height);
192-
color: var(--color-text-secondary);
193-
}
194-
.heading {
195-
font-size: var(--font-heading-lg-size);
196-
font-weight: var(--font-weight-semibold);
197-
}
198-
```
199-
200-
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.
201-
202-
### Safe Area Handling
203-
204-
Always respect `safeAreaInsets`:
205-
206-
```typescript
207-
app.onhostcontextchanged = (ctx) => {
208-
if (ctx.safeAreaInsets) {
209-
const { top, right, bottom, left } = ctx.safeAreaInsets;
210-
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
211-
}
212-
};
213-
```
214-
215-
### Streaming Partial Input
216-
217-
For large tool inputs, use `ontoolinputpartial` to show progress during LLM generation. The partial JSON is healed (always valid), enabling progressive UI updates.
218-
219-
**Spec:** [ui/notifications/tool-input-partial](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx#streaming-tool-input)
220-
221-
```typescript
222-
app.ontoolinputpartial = (params) => {
223-
const args = params.arguments; // Healed partial JSON - always valid, fields appear as generated
224-
// Use args directly for progressive rendering
225-
};
226-
227-
app.ontoolinput = (params) => {
228-
// Final complete input - switch from preview to full render
229-
};
230-
```
231-
232-
**Use cases:**
233-
| Pattern | Example |
234-
|---------|---------|
235-
| Code preview | Show streaming code in `<pre>`, render on complete (`examples/shadertoy-server/`) |
236-
| Progressive form | Fill form fields as they stream in |
237-
| Live chart | Add data points to chart as array grows |
238-
| Partial render | Render incomplete structured data (tables, lists, trees) |
239-
240-
**Simple pattern (code preview):**
241-
```typescript
242-
app.ontoolinputpartial = (params) => {
243-
codePreview.textContent = params.arguments?.code ?? "";
244-
codePreview.style.display = "block";
245-
canvas.style.display = "none";
246-
};
247-
app.ontoolinput = (params) => {
248-
codePreview.style.display = "none";
249-
canvas.style.display = "block";
250-
render(params.arguments);
251-
};
252-
```
253-
254-
### Visibility-Based Resource Management
255-
256-
Pause expensive operations (animations, WebGL, polling) when view scrolls out of viewport:
257-
258-
```typescript
259-
const observer = new IntersectionObserver((entries) => {
260-
entries.forEach((entry) => {
261-
if (entry.isIntersecting) {
262-
animation.play(); // or: startPolling(), shaderToy.play()
263-
} else {
264-
animation.pause(); // or: stopPolling(), shaderToy.pause()
265-
}
266-
});
267-
});
268-
observer.observe(document.querySelector(".main"));
269-
```
270-
271-
### Fullscreen Mode
272-
273-
Request fullscreen via `app.requestDisplayMode()`. Check availability in host context:
274-
275-
```typescript
276-
let currentMode: "inline" | "fullscreen" = "inline";
277-
278-
app.onhostcontextchanged = (ctx) => {
279-
// Check if fullscreen available
280-
if (ctx.availableDisplayModes?.includes("fullscreen")) {
281-
fullscreenBtn.style.display = "block";
282-
}
283-
// Track current mode
284-
if (ctx.displayMode) {
285-
currentMode = ctx.displayMode;
286-
container.classList.toggle("fullscreen", currentMode === "fullscreen");
287-
}
288-
};
289-
290-
async function toggleFullscreen() {
291-
const newMode = currentMode === "fullscreen" ? "inline" : "fullscreen";
292-
const result = await app.requestDisplayMode({ mode: newMode });
293-
currentMode = result.mode;
294-
}
295-
```
296-
297-
**CSS pattern** - Remove border radius in fullscreen:
298-
```css
299-
.main { border-radius: var(--border-radius-lg); overflow: hidden; }
300-
.main.fullscreen { border-radius: 0; }
301-
```
302-
303-
See `examples/shadertoy-server/` for complete implementation.
304-
305150
## Common Mistakes to Avoid
306151

307-
1. **Handlers after connect()** - Register ALL handlers BEFORE calling `app.connect()`
308-
2. **Missing single-file bundling** - Must use `vite-plugin-singlefile`
309-
3. **Forgetting resource registration** - Both tool AND resource must be registered
310-
4. **Missing resourceUri link** - Tool must have `_meta.ui.resourceUri`
311-
5. **Ignoring safe area insets** - Always handle `ctx.safeAreaInsets`
312-
6. **No text fallback** - Always provide `content` array for non-UI hosts
313-
7. **Hardcoded styles** - Use host CSS variables for theme integration
314-
8. **No streaming for large inputs** - Use `ontoolinputpartial` to show progress during generation
152+
1. **No text fallback** - Always provide `content` array for non-UI hosts
153+
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
154+
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
155+
4. **Handlers after app.connect()** - Register ALL handlers BEFORE calling `app.connect()`
156+
5. **No streaming for large inputs** - Use `ontoolinputpartial` to show progress during input generation
315157

316158
## Testing
317159

0 commit comments

Comments
 (0)