@@ -13,11 +13,10 @@ Every MCP App requires two parts linked together:
1313
14141 . ** Tool** - Called by the LLM/host, returns data
15152 . ** 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
6564Each 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
116124npm 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 */ };
136141app .ontoolresult = (result ) => { /* handle result */ };
137142app .onhostcontextchanged = (ctx ) => { /* handle context */ };
138143app .onteardown = async () => { return {}; };
144+ // etc.
139145
140146// Then connect
141147await 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