fa-mcp-sdk 0.4.99 → 0.4.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-template/.claude/skills/mcp-app-add-to-server/SKILL.md +9 -3
- package/cli-template/.claude/skills/mcp-app-create/SKILL.md +9 -3
- package/cli-template/CLAUDE.md +19 -0
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +1 -0
- package/cli-template/FA-MCP-SDK-DOC/01-getting-started.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +885 -0
- package/cli-template/gitignore +1 -0
- package/cli-template/package.json +1 -1
- package/dist/core/_types_/types.d.ts +1 -1
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/mcp/create-mcp-server.js +1 -1
- package/dist/core/mcp/create-mcp-server.js.map +1 -1
- package/dist/core/web/server-http.js +2 -2
- package/dist/core/web/server-http.js.map +1 -1
- package/package.json +1 -1
- package/scripts/clone-mcp-ext-apps.js +327 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
# MCP Apps (10) — Self-Contained Protocol + SDK Digest
|
|
2
|
+
|
|
3
|
+
| Field | Value |
|
|
4
|
+
|-----------------|-------|
|
|
5
|
+
| Pinned SDK | `@modelcontextprotocol/ext-apps` v1.7.2 |
|
|
6
|
+
| Spec revision | 2026-01-26 (SEP-1865, status: Stable) |
|
|
7
|
+
| Upstream commit | `9a37ad7` |
|
|
8
|
+
| Regenerated | 2026-05-16 |
|
|
9
|
+
| Draft state | A non-empty `specification/draft/apps.mdx` exists upstream — it adds a "Metadata Location" |
|
|
10
|
+
| | clarification (resource-level `_meta.ui` may also appear in `resources/list`, content-item value |
|
|
11
|
+
| | takes precedence) and softens two sandbox-proxy MUSTs to SHOULDs. The digest documents the Stable |
|
|
12
|
+
| | revision; treat the draft as informative only. |
|
|
13
|
+
|
|
14
|
+
This file is the single self-contained reference for MCP Apps (UI-augmented MCP tools) used by
|
|
15
|
+
`fa-mcp-sdk`-based servers. It supersedes routine lookups into the upstream repository. Use the
|
|
16
|
+
Reference Index at the end of the document when source-of-truth is needed.
|
|
17
|
+
|
|
18
|
+
## 2. What & Why
|
|
19
|
+
|
|
20
|
+
An **MCP App** is a Tool + UI Resource pair linked through tool `_meta.ui.resourceUri`. The tool's
|
|
21
|
+
`content` text array MUST still exist so that hosts without MCP Apps support continue to work — the
|
|
22
|
+
UI is a progressive enhancement, never a replacement. The host fetches the referenced `ui://` HTML
|
|
23
|
+
resource, renders it inside a sandboxed iframe (the **View**), and proxies bidirectional JSON-RPC
|
|
24
|
+
between View and MCP server. The View can call server tools, read resources, request display-mode
|
|
25
|
+
changes, send chat messages, or push model-context updates.
|
|
26
|
+
|
|
27
|
+
This solves three problems with text-only MCP responses: (a) lack of standardized UI delivery across
|
|
28
|
+
hosts (MCP-UI vs. OpenAI Apps SDK fragmentation), (b) inconsistent security models for embedded
|
|
29
|
+
content, and (c) inability to ship rich interactive surfaces (dashboards, players, forms, maps)
|
|
30
|
+
through the protocol.
|
|
31
|
+
|
|
32
|
+
## 3. Architecture
|
|
33
|
+
|
|
34
|
+
Three entities cooperate over two transports:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
+-----------+ MCP (HTTP/SSE/stdio) +--------+ postMessage / JSON-RPC +-----------------+
|
|
38
|
+
| MCP |<------------------------>| Host |<-------------------------->| View (iframe) |
|
|
39
|
+
| Server | | | | = App instance |
|
|
40
|
+
+-----------+ +--------+ +-----------------+
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **Server** — a standard MCP server that registers tools whose `_meta.ui.resourceUri` points to a
|
|
44
|
+
`ui://` HTML resource. Built with `registerAppTool` / `registerAppResource` from
|
|
45
|
+
`@modelcontextprotocol/ext-apps/server`.
|
|
46
|
+
- **Host** — the chat client (Claude Desktop, Claude.ai, VS Code Insiders, Goose, etc.). Connects
|
|
47
|
+
to the server, renders the View, and proxies all View→server messages.
|
|
48
|
+
- **View** — the HTML/JS payload returned by the UI resource. Acts as an MCP client over
|
|
49
|
+
`PostMessageTransport`. Built around the `App` class from `@modelcontextprotocol/ext-apps`.
|
|
50
|
+
|
|
51
|
+
**Sandbox model.** Desktop/native hosts render the View directly in a sandboxed iframe. Web hosts
|
|
52
|
+
MUST insert an intermediate Sandbox Proxy iframe on a different origin from the host
|
|
53
|
+
(`allow-scripts allow-same-origin`); the proxy receives the raw HTML over
|
|
54
|
+
`ui/notifications/sandbox-resource-ready`, applies CSP/permissions, and transparently forwards every
|
|
55
|
+
non-`ui/notifications/sandbox-*` message between the inner View and the Host. The Host MUST NOT send
|
|
56
|
+
any message to the View before receiving `ui/notifications/initialized`.
|
|
57
|
+
|
|
58
|
+
**Capability negotiation.** The host advertises MCP Apps support via the
|
|
59
|
+
`io.modelcontextprotocol/ui` extension key in `initialize.capabilities.extensions`, declaring at
|
|
60
|
+
minimum the supported `mimeTypes` (`text/html;profile=mcp-app`). Servers SHOULD call
|
|
61
|
+
`getUiCapability(clientCapabilities)` before registering UI-enabled tools and MUST provide a
|
|
62
|
+
text-only fallback for hosts that don't support the extension.
|
|
63
|
+
|
|
64
|
+
## 4. Lifecycle
|
|
65
|
+
|
|
66
|
+
All four sequence diagrams below are reproduced verbatim from
|
|
67
|
+
`specification/2026-01-26/apps.mdx` § Lifecycle. They are the canonical message-order contract — do
|
|
68
|
+
not paraphrase, derive, or fold.
|
|
69
|
+
|
|
70
|
+
### 4.1 Connection & Discovery
|
|
71
|
+
|
|
72
|
+
```mermaid
|
|
73
|
+
sequenceDiagram
|
|
74
|
+
participant H as Host
|
|
75
|
+
participant S as MCP Server
|
|
76
|
+
|
|
77
|
+
autonumber
|
|
78
|
+
S -->> H: resources/list (includes ui:// resources)
|
|
79
|
+
S -->> H: tools/list (includes tools with _meta.ui metadata)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 4.2 UI Initialization (Desktop/Native Hosts)
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
sequenceDiagram
|
|
86
|
+
participant H as Host
|
|
87
|
+
participant UI as View (iframe)
|
|
88
|
+
participant P as Sandbox Proxy
|
|
89
|
+
participant S as MCP Server
|
|
90
|
+
|
|
91
|
+
autonumber
|
|
92
|
+
par UI Tool call
|
|
93
|
+
H ->> S: tools/call to Tool with _meta.ui metadata
|
|
94
|
+
and UI initialization
|
|
95
|
+
alt Desktop/Native hosts
|
|
96
|
+
H ->> H: Render View in an iframe (HTML from the ui:// resource)
|
|
97
|
+
else Web hosts
|
|
98
|
+
H ->> H: Render Sandbox Proxy in an iframe (different origin)
|
|
99
|
+
P ->> H: ui/notifications/sandbox-proxy-ready
|
|
100
|
+
H -->> P: ui/notifications/sandbox-resource-ready (HTML content)
|
|
101
|
+
P -> P: Render inner iframe with HTML
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
UI ->> H: ui/initialize
|
|
105
|
+
H -->> UI: McpUiInitializeResult (e.g., host-context, capabilities, etc.)
|
|
106
|
+
UI ->> H: ui/notifications/initialized
|
|
107
|
+
opt Stream Tool input to UI
|
|
108
|
+
H -->> UI: ui/notifications/tool-input-partial (0..n)
|
|
109
|
+
end
|
|
110
|
+
H -->> UI: ui/notifications/tool-input (complete)
|
|
111
|
+
end
|
|
112
|
+
alt Tool complete
|
|
113
|
+
H -->> UI: ui/notifications/tool-result
|
|
114
|
+
else Tool cancelled
|
|
115
|
+
H -->> UI: ui/notifications/tool-cancelled
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Note: when the View is rendered inside a sandbox, the sandbox transparently passes messages between
|
|
120
|
+
the View and the Host, except for messages named `ui/notifications/sandbox-*`.
|
|
121
|
+
|
|
122
|
+
### 4.3 Interactive Phase
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
sequenceDiagram
|
|
126
|
+
actor U as User / Agent
|
|
127
|
+
participant H as Host
|
|
128
|
+
participant UI as View (iframe)
|
|
129
|
+
participant S as MCP Server
|
|
130
|
+
loop Interactive phase
|
|
131
|
+
U ->> UI: interaction (e.g., click)
|
|
132
|
+
alt Tool call
|
|
133
|
+
UI ->> H: tools/call
|
|
134
|
+
H ->> S: tools/call
|
|
135
|
+
opt Stream Tool input to UI
|
|
136
|
+
H -->> UI: ui/notifications/tool-input-partial (0..n)
|
|
137
|
+
end
|
|
138
|
+
H -->> UI: ui/notifications/tool-input (complete)
|
|
139
|
+
H-->>UI: ui/notifications/tool-result
|
|
140
|
+
else Message
|
|
141
|
+
UI ->> H: ui/message
|
|
142
|
+
H -->> UI: ui/message response
|
|
143
|
+
H -->> H: Process message and follow up
|
|
144
|
+
else Context update
|
|
145
|
+
UI ->> H: ui/update-model-context
|
|
146
|
+
H ->> H: Store model context (overwrite existing)
|
|
147
|
+
H -->> UI: ui/update-model-context response
|
|
148
|
+
else Log
|
|
149
|
+
UI ->> H: notifications/message
|
|
150
|
+
H ->> H: Record log for debugging/telemetry
|
|
151
|
+
else Resource read
|
|
152
|
+
UI ->> H: resources/read
|
|
153
|
+
H ->> S: resources/read
|
|
154
|
+
S --> H: resources/read response
|
|
155
|
+
H --> UI: resources/read response
|
|
156
|
+
end
|
|
157
|
+
opt View notifications
|
|
158
|
+
UI ->> H: notifications (e.g., ui/notifications/size-changed)
|
|
159
|
+
end
|
|
160
|
+
opt Host notifications
|
|
161
|
+
H ->> UI: notifications (e.g., ui/notifications/host-context-changed)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 4.4 Cleanup
|
|
167
|
+
|
|
168
|
+
```mermaid
|
|
169
|
+
sequenceDiagram
|
|
170
|
+
participant H as Host
|
|
171
|
+
participant UI as View (iframe)
|
|
172
|
+
H ->> UI: ui/resource-teardown
|
|
173
|
+
UI --> UI: Graceful termination
|
|
174
|
+
UI -->> H: ui/resource-teardown response
|
|
175
|
+
H -x H: Tear down iframe and listeners
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Cleanup may be triggered at any point after initialization (user navigates away, host re-allocates
|
|
179
|
+
the slot, app calls `requestTeardown`). The host SHOULD wait for the View's response before
|
|
180
|
+
unmounting to prevent data loss.
|
|
181
|
+
|
|
182
|
+
## 5. Protocol Contract
|
|
183
|
+
|
|
184
|
+
Normative passages reuse the spec's MUST / SHOULD / MAY wording.
|
|
185
|
+
|
|
186
|
+
### 5.1 `ui://` URI scheme + `RESOURCE_MIME_TYPE`
|
|
187
|
+
|
|
188
|
+
- Every UI resource URI MUST start with `ui://`.
|
|
189
|
+
- The `mimeType` of a UI resource MUST be `text/html;profile=mcp-app`
|
|
190
|
+
(exported as `RESOURCE_MIME_TYPE` from `@modelcontextprotocol/ext-apps/server`). Other MIME types
|
|
191
|
+
are reserved for future extensions.
|
|
192
|
+
- Resource content MUST be returned via either `text` (string) or `blob` (base64) and MUST be a
|
|
193
|
+
valid HTML5 document.
|
|
194
|
+
- Hosts MAY prefetch and cache UI resource content. Since UI resources are primarily discovered
|
|
195
|
+
through tool metadata, servers MAY omit UI-only resources from `resources/list` and
|
|
196
|
+
`notifications/resources/list_changed`.
|
|
197
|
+
|
|
198
|
+
### 5.2 `_meta.ui` location matrix
|
|
199
|
+
|
|
200
|
+
| Where | Key | What it carries |
|
|
201
|
+
|-------|-----|------------------|
|
|
202
|
+
| Tool definition | `tool._meta.ui` | `McpUiToolMeta`: `resourceUri`, `visibility: ("model"\|"app")[]` |
|
|
203
|
+
| Tool definition (legacy) | `tool._meta["ui/resourceUri"]` | Deprecated flat form; SDK auto-mirrors with the |
|
|
204
|
+
| | | nested form for backward compatibility |
|
|
205
|
+
| Resource entry | `resource._meta.ui` | `McpUiResourceMeta` listing-level default (`csp`, `permissions`, |
|
|
206
|
+
| | | `domain`, `prefersBorder`) |
|
|
207
|
+
| Resource content item | `contents[i]._meta.ui` | `McpUiResourceMeta` — takes precedence over the listing-level |
|
|
208
|
+
| | | value when both are present |
|
|
209
|
+
|
|
210
|
+
Servers SHOULD place `_meta.ui` on the content item in `resources/read` for dynamic per-response
|
|
211
|
+
metadata; the listing-level `_meta.ui` is for static, reviewable defaults.
|
|
212
|
+
|
|
213
|
+
`visibility` defaults to `["model", "app"]`. `"model"`-only tools never appear in app-issued
|
|
214
|
+
`tools/call`s; `"app"`-only tools are excluded from the agent's `tools/list` and are callable only
|
|
215
|
+
through `app.callServerTool(...)` from the View. Cross-server tool calls are always blocked for
|
|
216
|
+
app-only tools.
|
|
217
|
+
|
|
218
|
+
### 5.3 Capability negotiation
|
|
219
|
+
|
|
220
|
+
Host advertises support in the standard `initialize` capabilities envelope:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"capabilities": {
|
|
225
|
+
"extensions": {
|
|
226
|
+
"io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Servers MUST use `getUiCapability(clientCapabilities)` (or equivalent) before registering UI-enabled
|
|
233
|
+
tools, and SHOULD register text-only fallback tool variants when the capability is absent. Tools
|
|
234
|
+
MUST return a meaningful `content[]` even when UI rendering is available.
|
|
235
|
+
|
|
236
|
+
### 5.4 Content Security Policy
|
|
237
|
+
|
|
238
|
+
The host MUST construct CSP headers from `_meta.ui.csp`. If `_meta.ui.csp` is absent, the host MUST
|
|
239
|
+
apply the restrictive default:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
default-src 'none';
|
|
243
|
+
script-src 'self' 'unsafe-inline';
|
|
244
|
+
style-src 'self' 'unsafe-inline';
|
|
245
|
+
img-src 'self' data:;
|
|
246
|
+
media-src 'self' data:;
|
|
247
|
+
connect-src 'none';
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Hosts MAY further restrict but MUST NOT permit undeclared domains. `McpUiResourceCsp` fields map
|
|
251
|
+
directly to CSP directives:
|
|
252
|
+
|
|
253
|
+
| Field | Directive(s) | Default when omitted |
|
|
254
|
+
|-------|--------------|----------------------|
|
|
255
|
+
| `connectDomains[]` | `connect-src` | `'none'` (no fetch/XHR/WebSocket) |
|
|
256
|
+
| `resourceDomains[]` | `img-src`, `script-src`, `style-src`, `font-src`, `media-src` | `'self' data:` for media/img |
|
|
257
|
+
| `frameDomains[]` | `frame-src` | `'none'` |
|
|
258
|
+
| `baseUriDomains[]` | `base-uri` | `'self'` |
|
|
259
|
+
|
|
260
|
+
`_meta.ui.permissions` (`camera`, `microphone`, `geolocation`, `clipboardWrite`) maps to the
|
|
261
|
+
iframe's `allow` Permission-Policy attribute; hosts MAY honor or deny these. Apps SHOULD use JS
|
|
262
|
+
feature detection rather than assume a permission was granted.
|
|
263
|
+
|
|
264
|
+
`_meta.ui.domain` provides a stable sandbox origin for CORS allowlisting or OAuth callbacks. The
|
|
265
|
+
exact format is **host-dependent** — hosts publish their own subdomain conventions (Claude uses
|
|
266
|
+
`{hash}.claudemcpcontent.com`, OpenAI uses `www-{domain}-com.oaiusercontent.com`).
|
|
267
|
+
|
|
268
|
+
### 5.5 Host ↔ View JSON-RPC messages
|
|
269
|
+
|
|
270
|
+
Lifecycle:
|
|
271
|
+
|
|
272
|
+
| Method | Direction | Kind | Purpose |
|
|
273
|
+
|--------|-----------|------|---------|
|
|
274
|
+
| `ui/initialize` | View → Host | Request | Handshake; carries `appInfo`, `appCapabilities`, `protocolVersion` |
|
|
275
|
+
| `ui/notifications/initialized` | View → Host | Notification | View signals readiness for tool input/result |
|
|
276
|
+
| `ping` | Either | Request | Connection health check |
|
|
277
|
+
|
|
278
|
+
Tool input/output (Host → View):
|
|
279
|
+
|
|
280
|
+
| Method | Kind | Notes |
|
|
281
|
+
|--------|------|-------|
|
|
282
|
+
| `ui/notifications/tool-input-partial` | Notification | MAY be sent 0..n times during agent streaming. Arguments |
|
|
283
|
+
| | | are healed JSON (unclosed brackets auto-closed). Views MAY |
|
|
284
|
+
| | | ignore; MUST NOT use for critical operations. |
|
|
285
|
+
| `ui/notifications/tool-input` | Notification | MUST be sent exactly once after `ui/initialize` completes and |
|
|
286
|
+
| | | before `ui/notifications/tool-result`. Carries complete |
|
|
287
|
+
| | | tool arguments. |
|
|
288
|
+
| `ui/notifications/tool-result` | Notification | MUST be sent when tool execution completes (if the View is |
|
|
289
|
+
| | | displayed). `params` is the standard `CallToolResult`. |
|
|
290
|
+
| `ui/notifications/tool-cancelled` | Notification | MUST be sent on cancellation (user, sampling error, |
|
|
291
|
+
| | | classifier intervention). `params: { reason: string }`. |
|
|
292
|
+
|
|
293
|
+
Host ↔ View runtime:
|
|
294
|
+
|
|
295
|
+
| Method | Direction | Kind | Notes |
|
|
296
|
+
|--------|-----------|------|-------|
|
|
297
|
+
| `tools/call` | View → Host (proxied to Server) | Request | Standard MCP. Issued via `App.callServerTool`. |
|
|
298
|
+
| `resources/read` | View → Host (proxied to Server) | Request | Standard MCP. |
|
|
299
|
+
| `resources/list` | View → Host (proxied to Server) | Request | Standard MCP. |
|
|
300
|
+
| `sampling/createMessage` | View → Host | Request | Standard MCP sampling, gated by `hostCapabilities.sampling`. |
|
|
301
|
+
| `notifications/message` | View → Host | Notification | Standard MCP log (debug/telemetry, not chat). |
|
|
302
|
+
| `ui/message` | View → Host | Request | Send `{ role, content }` to the conversation. Host SHOULD add |
|
|
303
|
+
| | | | to context, MAY ask for user consent. |
|
|
304
|
+
| `ui/update-model-context` | View → Host | Request | Replaces previous View-issued model context. Host MAY |
|
|
305
|
+
| | | | dedupe; only the last update before the next user turn |
|
|
306
|
+
| | | | reaches the model. |
|
|
307
|
+
| `ui/open-link` | View → Host | Request | Open URL in user's default browser. Host MAY deny. |
|
|
308
|
+
| `ui/request-display-mode` | View → Host | Request | Request switch to `"inline"\|"fullscreen"\|"pip"`. Result |
|
|
309
|
+
| | | | returns the actual mode (may differ from request). |
|
|
310
|
+
| `ui/notifications/size-changed` | View → Host | Notification | View reports current viewport size in px. SDK sends this |
|
|
311
|
+
| | | | automatically via `ResizeObserver` when `autoResize: true`. |
|
|
312
|
+
| `ui/notifications/host-context-changed` | Host → View | Notification | `Partial<HostContext>`; View MUST merge with current |
|
|
313
|
+
| | | | context. |
|
|
314
|
+
| `ui/resource-teardown` | Host → View | Request | Host MUST send before unmounting. Host SHOULD await the |
|
|
315
|
+
| | | | response to prevent data loss. |
|
|
316
|
+
|
|
317
|
+
Sandbox-proxy reserved (web hosts only):
|
|
318
|
+
|
|
319
|
+
| Method | Direction | Kind | Notes |
|
|
320
|
+
|--------|-----------|------|-------|
|
|
321
|
+
| `ui/notifications/sandbox-proxy-ready` | Proxy → Host | Notification | Proxy is alive and ready for HTML. |
|
|
322
|
+
| `ui/notifications/sandbox-resource-ready` | Host → Proxy | Notification | Carries the HTML payload plus inline `csp` / |
|
|
323
|
+
| | | | `permissions` for the inner iframe. |
|
|
324
|
+
|
|
325
|
+
SDK-only extensions (present in `@modelcontextprotocol/ext-apps` v1.7.2 but not yet promoted into
|
|
326
|
+
the Stable spec): `ui/download-file` (View → Host request for host-mediated file download) and
|
|
327
|
+
`ui/notifications/request-teardown` (View → Host notification asking the host to initiate teardown,
|
|
328
|
+
after which the host follows up with the standard `ui/resource-teardown` request). Hosts that
|
|
329
|
+
predate these messages MAY reject or ignore them; treat both as best-effort.
|
|
330
|
+
|
|
331
|
+
### 5.6 Capability shapes
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
interface McpUiAppCapabilities {
|
|
335
|
+
experimental?: {};
|
|
336
|
+
tools?: { listChanged?: boolean };
|
|
337
|
+
availableDisplayModes?: Array<"inline" | "fullscreen" | "pip">;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
interface McpUiHostCapabilities {
|
|
341
|
+
experimental?: {};
|
|
342
|
+
openLinks?: {};
|
|
343
|
+
downloadFile?: {};
|
|
344
|
+
serverTools?: { listChanged?: boolean };
|
|
345
|
+
serverResources?: { listChanged?: boolean };
|
|
346
|
+
logging?: {};
|
|
347
|
+
sandbox?: {
|
|
348
|
+
permissions?: { camera?: {}; microphone?: {}; geolocation?: {}; clipboardWrite?: {} };
|
|
349
|
+
csp?: McpUiResourceCsp;
|
|
350
|
+
};
|
|
351
|
+
updateModelContext?: McpUiSupportedContentBlockModalities;
|
|
352
|
+
message?: McpUiSupportedContentBlockModalities;
|
|
353
|
+
sampling?: { tools?: {} };
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Views MUST declare every display mode they can render in `appCapabilities.availableDisplayModes`.
|
|
358
|
+
Hosts MUST NOT switch a View to a mode that is not declared, MUST return the resulting (possibly
|
|
359
|
+
unchanged) mode in `ui/request-display-mode` responses, and MAY decline mode requests that the View
|
|
360
|
+
did not declare.
|
|
361
|
+
|
|
362
|
+
## 6. TypeScript SDK API
|
|
363
|
+
|
|
364
|
+
All snippets below assume:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
|
|
368
|
+
import {
|
|
369
|
+
registerAppTool, registerAppResource, getUiCapability,
|
|
370
|
+
RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY,
|
|
371
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 6.1 Server helpers
|
|
375
|
+
|
|
376
|
+
`registerAppTool(server, name, config, callback)` — wraps `server.registerTool` and normalizes
|
|
377
|
+
`_meta.ui.resourceUri` ↔ `_meta["ui/resourceUri"]` for backward compatibility. `config._meta.ui`
|
|
378
|
+
MAY contain `resourceUri` and `visibility: ("model"|"app")[]`.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
registerAppTool(
|
|
382
|
+
server,
|
|
383
|
+
"get-weather",
|
|
384
|
+
{
|
|
385
|
+
title: "Get Weather",
|
|
386
|
+
description: "Current weather with interactive dashboard",
|
|
387
|
+
inputSchema: { location: z.string() },
|
|
388
|
+
_meta: { ui: { resourceUri: "ui://weather/view.html" } },
|
|
389
|
+
},
|
|
390
|
+
async ({ location }) => {
|
|
391
|
+
const w = await fetchWeather(location);
|
|
392
|
+
return { content: [{ type: "text", text: JSON.stringify(w) }], structuredContent: w };
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`registerAppResource(server, name, uri, config, readCallback)` — defaults `mimeType` to
|
|
398
|
+
`RESOURCE_MIME_TYPE`. Put `_meta.ui.csp` / `_meta.ui.domain` / `_meta.ui.prefersBorder` on the
|
|
399
|
+
`contents[]` returned by `readCallback`, not in `config`.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
registerAppResource(
|
|
403
|
+
server,
|
|
404
|
+
"Weather View",
|
|
405
|
+
"ui://weather/view.html",
|
|
406
|
+
{ description: "Interactive weather display" },
|
|
407
|
+
async () => ({
|
|
408
|
+
contents: [{
|
|
409
|
+
uri: "ui://weather/view.html",
|
|
410
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
411
|
+
text: await fs.readFile("dist/view.html", "utf-8"),
|
|
412
|
+
_meta: { ui: { csp: { connectDomains: ["https://api.openweathermap.org"] }, prefersBorder: true } },
|
|
413
|
+
}],
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`getUiCapability(clientCapabilities)` returns the `McpUiClientCapabilities | undefined` payload from
|
|
419
|
+
the host's `extensions["io.modelcontextprotocol/ui"]`. Use it in `server.server.oninitialized` to
|
|
420
|
+
gate which tool variant to register.
|
|
421
|
+
|
|
422
|
+
`RESOURCE_MIME_TYPE === "text/html;profile=mcp-app"`. `RESOURCE_URI_META_KEY === "ui/resourceUri"`
|
|
423
|
+
(legacy flat key, prefer the nested `_meta.ui.resourceUri` form).
|
|
424
|
+
|
|
425
|
+
### 6.2 `App` class (View side)
|
|
426
|
+
|
|
427
|
+
Constructor: `new App(appInfo, capabilities = {}, options = { autoResize: true })`.
|
|
428
|
+
|
|
429
|
+
`AppOptions`:
|
|
430
|
+
|
|
431
|
+
| Option | Default | Effect |
|
|
432
|
+
|--------|---------|--------|
|
|
433
|
+
| `autoResize` | `true` | Observes `<html>` / `<body>` and sends `ui/notifications/size-changed` automatically |
|
|
434
|
+
| `strict` | `false` | When `true`, calling host-bound methods before `connect()` resolves throws instead of |
|
|
435
|
+
| | | logging a `console.warn`. Will become the default in a future release. |
|
|
436
|
+
| `allowUnsafeEval` | `false` | When `false`, sets `z.config({ jitless: true })` so zod parsing works under the |
|
|
437
|
+
| | | spec's default CSP (which disallows `unsafe-eval`). |
|
|
438
|
+
|
|
439
|
+
Connection: `await app.connect(transport?, options?)`. Defaults to
|
|
440
|
+
`new PostMessageTransport(window.parent, window.parent)`. Performs `ui/initialize` →
|
|
441
|
+
`ui/notifications/initialized` handshake, caches host capabilities/info/context, and (if
|
|
442
|
+
`autoResize`) attaches the `ResizeObserver`. On failure the transport is closed and the error
|
|
443
|
+
re-thrown.
|
|
444
|
+
|
|
445
|
+
Outbound requests/notifications (host-bound; all assert that `connect()` has completed):
|
|
446
|
+
|
|
447
|
+
| Method | Sends | Returns |
|
|
448
|
+
|--------|-------|---------|
|
|
449
|
+
| `callServerTool(params, opts?)` | `tools/call` | `CallToolResult` (check `result.isError`) |
|
|
450
|
+
| `readServerResource(params, opts?)` | `resources/read` | `ReadResourceResult` |
|
|
451
|
+
| `listServerResources(params?, opts?)` | `resources/list` | `ListResourcesResult` |
|
|
452
|
+
| `createSamplingMessage(params, opts?)` | `sampling/createMessage` | `CreateMessageResult` (or `…WithTools`) |
|
|
453
|
+
| `sendMessage(params, opts?)` | `ui/message` | `McpUiMessageResult` |
|
|
454
|
+
| `updateModelContext(params, opts?)` | `ui/update-model-context` | `{}` |
|
|
455
|
+
| `openLink(params, opts?)` | `ui/open-link` | `{ isError?: true }` |
|
|
456
|
+
| `downloadFile(params, opts?)` | `ui/download-file` (SDK-only) | `{ isError?: true }` |
|
|
457
|
+
| `requestDisplayMode(params, opts?)` | `ui/request-display-mode` | `{ mode }` |
|
|
458
|
+
| `requestTeardown(params?)` | `ui/notifications/request-teardown` notification | — |
|
|
459
|
+
| `sendSizeChanged(params)` | `ui/notifications/size-changed` notification | — |
|
|
460
|
+
| `sendLog(params)` | `notifications/message` notification | — |
|
|
461
|
+
| `sendToolListChanged(params?)` | `notifications/tools/list_changed` notification | — |
|
|
462
|
+
|
|
463
|
+
Inspection: `getHostCapabilities()`, `getHostVersion()` (returns `Implementation`),
|
|
464
|
+
`getHostContext()` (auto-merged from `ui/notifications/host-context-changed`).
|
|
465
|
+
|
|
466
|
+
Host-driven event handlers. Register **before** `app.connect()` to avoid missing one-shot events
|
|
467
|
+
like `ui/notifications/tool-result`. The DOM-style `on*` setters work but are JSDoc-deprecated in
|
|
468
|
+
favor of `app.addEventListener(event, handler)` / `removeEventListener` for composition + cleanup.
|
|
469
|
+
`onteardown` and the tool-server handlers (`oncalltool`, `onlisttools`) remain regular request
|
|
470
|
+
handlers.
|
|
471
|
+
|
|
472
|
+
| Setter | Event name | Payload |
|
|
473
|
+
|--------|------------|---------|
|
|
474
|
+
| `app.ontoolinput` | `"toolinput"` | `McpUiToolInputNotification["params"]` |
|
|
475
|
+
| `app.ontoolinputpartial` | `"toolinputpartial"` | `McpUiToolInputPartialNotification["params"]` |
|
|
476
|
+
| `app.ontoolresult` | `"toolresult"` | `CallToolResult` (the spec's `ui/notifications/tool-result` params) |
|
|
477
|
+
| `app.ontoolcancelled` | `"toolcancelled"` | `{ reason: string }` |
|
|
478
|
+
| `app.onhostcontextchanged` | `"hostcontextchanged"` | `Partial<McpUiHostContext>` |
|
|
479
|
+
| `app.onteardown` | (request) `ui/resource-teardown` | Async `() => McpUiResourceTeardownResult` |
|
|
480
|
+
| `app.oncalltool` | (request) `tools/call` | App-as-tool-server handler |
|
|
481
|
+
| `app.onlisttools` | (request) `tools/list` | App-as-tool-server handler |
|
|
482
|
+
|
|
483
|
+
App-as-tool-server: `app.registerTool(name, config, callback)` declares View-exposed tools that the
|
|
484
|
+
host (or its LLM) may call into the iframe. Tools auto-register `capabilities.tools` when no value
|
|
485
|
+
is provided. Use `app.sendToolListChanged()` after dynamic enable/disable.
|
|
486
|
+
|
|
487
|
+
### 6.3 `PostMessageTransport`
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
// In a View:
|
|
491
|
+
const transport = new PostMessageTransport(window.parent, window.parent);
|
|
492
|
+
await app.connect(transport);
|
|
493
|
+
|
|
494
|
+
// In a Host:
|
|
495
|
+
const transport = new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`eventSource` is validated against `event.source` and is required for security — messages from
|
|
499
|
+
unknown windows are dropped silently. Outbound `postMessage` uses `"*"` as the target origin;
|
|
500
|
+
receivers MUST validate the source themselves. Non-JSON-RPC messages and JSON-RPC `2.0` messages
|
|
501
|
+
that fail `JSONRPCMessageSchema` validation are dropped / surfaced via `onerror`.
|
|
502
|
+
|
|
503
|
+
### 6.4 Style helpers (framework-agnostic)
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
import {
|
|
507
|
+
applyDocumentTheme, applyHostStyleVariables, applyHostFonts, getDocumentTheme,
|
|
508
|
+
} from "@modelcontextprotocol/ext-apps";
|
|
509
|
+
|
|
510
|
+
if (ctx.theme) applyDocumentTheme(ctx.theme); // sets data-theme + color-scheme
|
|
511
|
+
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables); // sets CSS custom properties
|
|
512
|
+
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts); // injects <style id="__mcp-host-fonts">
|
|
513
|
+
const current = getDocumentTheme(); // "light" | "dark"
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`applyHostFonts` is idempotent (no-op if `<style id="__mcp-host-fonts">` already exists). All three
|
|
517
|
+
SHOULD be called once on `connect()` *and* re-applied inside `onhostcontextchanged`.
|
|
518
|
+
|
|
519
|
+
### 6.5 React hooks
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
import {
|
|
523
|
+
useApp, useHostStyles, useHostStyleVariables, useHostFonts,
|
|
524
|
+
useDocumentTheme, useAutoResize,
|
|
525
|
+
} from "@modelcontextprotocol/ext-apps/react";
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
| Hook | Signature | Notes |
|
|
529
|
+
|------|-----------|-------|
|
|
530
|
+
| `useApp(options)` | `{ appInfo, capabilities, onAppCreated?, autoResize?, strict? }` → `{ app, isConnected, error }` | Creates the |
|
|
531
|
+
| | | `App`, wires the |
|
|
532
|
+
| | | `PostMessageTransport` to `window.parent`, calls |
|
|
533
|
+
| | | `onAppCreated(app)` **before** `connect()`. Does |
|
|
534
|
+
| | | NOT re-run on options changes; does NOT close on |
|
|
535
|
+
| | | unmount (Strict-Mode safe). |
|
|
536
|
+
| `useHostStyles(app, initialContext?)` | composite | Combines `useHostStyleVariables` + `useHostFonts`. |
|
|
537
|
+
| `useHostStyleVariables(app, initialCtx?)` | applies vars + `applyDocumentTheme` on mount and on |
|
|
538
|
+
| | `hostcontextchanged` |
|
|
539
|
+
| `useHostFonts(app, initialContext?)` | injects host fonts on mount and on change |
|
|
540
|
+
| `useDocumentTheme()` | `() => McpUiTheme` | `MutationObserver` on `<html data-theme>` / `class`; re-renders on theme change |
|
|
541
|
+
| `useAutoResize(app, _ref?)` | only useful when the `App` was created with `autoResize: false`; otherwise redundant |
|
|
542
|
+
|
|
543
|
+
## 7. Host Context (`McpUiHostContext`)
|
|
544
|
+
|
|
545
|
+
Every field is optional and stable per `viewUUID`. Views SHOULD treat missing fields as "unknown"
|
|
546
|
+
and degrade gracefully. The full schema (verbatim from `src/spec.types.ts`):
|
|
547
|
+
|
|
548
|
+
| Field | Type | Notes |
|
|
549
|
+
|-------|------|-------|
|
|
550
|
+
| `toolInfo` | `{ id?: RequestId, tool: Tool }` | Metadata about the `tools/call` that opened the View |
|
|
551
|
+
| `theme` | `"light" \| "dark"` | Apply via `applyDocumentTheme(theme)` |
|
|
552
|
+
| `styles.variables` | `Record<McpUiStyleVariableKey, string \| undefined>` | See § 7.1 |
|
|
553
|
+
| `styles.css.fonts` | `string` | `@font-face` and/or `@import` rules; apply via `applyHostFonts` |
|
|
554
|
+
| `displayMode` | `"inline" \| "fullscreen" \| "pip"` | Current mode; update via `requestDisplayMode` |
|
|
555
|
+
| `availableDisplayModes` | `("inline"\|"fullscreen"\|"pip")[]` | Modes the host can grant |
|
|
556
|
+
| `containerDimensions` | `({ height } \| { maxHeight? }) & ({ width } \| { maxWidth? })` | Fixed vs flexible per axis |
|
|
557
|
+
| `locale` | `string` | BCP 47, e.g. `"en-US"` |
|
|
558
|
+
| `timeZone` | `string` | IANA, e.g. `"America/New_York"` |
|
|
559
|
+
| `userAgent` | `string` | Host application identifier |
|
|
560
|
+
| `platform` | `"web" \| "desktop" \| "mobile"` | For responsive decisions |
|
|
561
|
+
| `deviceCapabilities` | `{ touch?: boolean; hover?: boolean }` | Input affordances |
|
|
562
|
+
| `safeAreaInsets` | `{ top, right, bottom, left }` | Mobile notches / system overlays in px |
|
|
563
|
+
|
|
564
|
+
`McpUiHostContext` also has an open `[key: string]: unknown` index signature for forward
|
|
565
|
+
compatibility — unrecognized fields MUST be preserved verbatim when the View merges partial updates.
|
|
566
|
+
|
|
567
|
+
### 7.1 Standardized CSS custom properties (`McpUiStyleVariableKey`)
|
|
568
|
+
|
|
569
|
+
Exact list from `src/spec.types.ts` — Views SHOULD define `:root { --…: fallback; }` defaults for
|
|
570
|
+
every variable they consume to keep layouts intact when the host omits values.
|
|
571
|
+
|
|
572
|
+
- Background: `--color-background-{primary, secondary, tertiary, inverse, ghost, info, danger,
|
|
573
|
+
success, warning, disabled}`
|
|
574
|
+
- Text: `--color-text-{primary, secondary, tertiary, inverse, info, danger, success, warning,
|
|
575
|
+
disabled, ghost}`
|
|
576
|
+
- Border: `--color-border-{primary, secondary, tertiary, inverse, ghost, info, danger, success,
|
|
577
|
+
warning, disabled}`
|
|
578
|
+
- Ring: `--color-ring-{primary, secondary, inverse, info, danger, success, warning}`
|
|
579
|
+
- Typography (family): `--font-sans`, `--font-mono`
|
|
580
|
+
- Typography (weight): `--font-weight-{normal, medium, semibold, bold}`
|
|
581
|
+
- Typography (text sizes): `--font-text-{xs, sm, md, lg}-size`
|
|
582
|
+
- Typography (heading sizes): `--font-heading-{xs, sm, md, lg, xl, 2xl, 3xl}-size`
|
|
583
|
+
- Typography (text line heights): `--font-text-{xs, sm, md, lg}-line-height`
|
|
584
|
+
- Typography (heading line heights): `--font-heading-{xs, sm, md, lg, xl, 2xl, 3xl}-line-height`
|
|
585
|
+
- Border radius: `--border-radius-{xs, sm, md, lg, xl, full}`
|
|
586
|
+
- Border width: `--border-width-regular`
|
|
587
|
+
- Shadow: `--shadow-{hairline, sm, md, lg}`
|
|
588
|
+
|
|
589
|
+
Hosts SHOULD use `light-dark(…)` for theme-aware color values so that switching `data-theme` flips
|
|
590
|
+
colors without a re-render. Spacing variables are intentionally excluded from the standard — pass
|
|
591
|
+
spacing values inside the View itself.
|
|
592
|
+
|
|
593
|
+
## 8. Patterns / Recipes
|
|
594
|
+
|
|
595
|
+
Patterns below are aligned with upstream `docs/patterns.md`. Code is illustrative; production apps
|
|
596
|
+
should add error handling and feature detection.
|
|
597
|
+
|
|
598
|
+
### 8.1 App-only tools (`visibility: ["app"]`)
|
|
599
|
+
|
|
600
|
+
Hide LLM-irrelevant interactions (refresh buttons, mutations) from the agent's tool list while
|
|
601
|
+
keeping them callable from the View via `app.callServerTool`.
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
registerAppTool(
|
|
605
|
+
server, "update-quantity",
|
|
606
|
+
{ description: "Update cart line", inputSchema: { itemId: z.string(), quantity: z.number() },
|
|
607
|
+
_meta: { ui: { resourceUri: "ui://shop/cart.html", visibility: ["app"] } } },
|
|
608
|
+
async ({ itemId, quantity }) => ({
|
|
609
|
+
content: [{ type: "text", text: JSON.stringify(await updateCartItem(itemId, quantity)) }],
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### 8.2 Streaming partial input
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
app.ontoolinputpartial = (params) => preview.textContent = (params.arguments?.code as string) ?? "";
|
|
618
|
+
app.ontoolinput = (params) => render(params.arguments?.code as string);
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Partial JSON is "healed" (unclosed brackets auto-closed). Use only for preview UI; final state MUST
|
|
622
|
+
come from `ontoolinput`.
|
|
623
|
+
|
|
624
|
+
### 8.3 Polling with teardown cleanup
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
let id: number | null = null;
|
|
628
|
+
const poll = async () => updateUI((await app.callServerTool({ name: "poll-data", arguments: {} })).structuredContent);
|
|
629
|
+
const start = () => { if (id == null) { poll(); id = window.setInterval(poll, 2000); } };
|
|
630
|
+
const stop = () => { if (id != null) { clearInterval(id); id = null; } };
|
|
631
|
+
app.onteardown = async () => { stop(); return {}; };
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### 8.4 Pause expensive work when offscreen
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
const obs = new IntersectionObserver(([e]) => e.isIntersecting ? animation.play() : animation.pause());
|
|
638
|
+
obs.observe(container);
|
|
639
|
+
app.onteardown = async () => { obs.disconnect(); animation.pause(); return {}; };
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### 8.5 Chunked binary delivery (host size-limit workaround)
|
|
643
|
+
|
|
644
|
+
Register an app-only `read_data_bytes(id, offset, byteCount)` tool that returns
|
|
645
|
+
`{ bytes: base64, offset, byteCount, totalBytes, hasMore }` in `structuredContent`; the View loops
|
|
646
|
+
until `hasMore === false`, decoding each chunk into a `Uint8Array`.
|
|
647
|
+
|
|
648
|
+
### 8.6 Binary resources (e.g. video)
|
|
649
|
+
|
|
650
|
+
Server returns `{ contents: [{ uri, mimeType, blob: base64 }] }`. View reads with
|
|
651
|
+
`app.readServerResource({ uri })`, decodes to a data URL or `URL.createObjectURL(new Blob([bytes]))`.
|
|
652
|
+
|
|
653
|
+
### 8.7 View-state persistence
|
|
654
|
+
|
|
655
|
+
Server tool result includes `_meta.viewUUID: randomUUID()`. View receives it in `ontoolresult` and
|
|
656
|
+
uses it as a `localStorage` key. For user-effort state (bookmarks, annotations), prefer an app-only
|
|
657
|
+
server tool keyed by the same `viewUUID`.
|
|
658
|
+
|
|
659
|
+
### 8.8 Model context updates (`updateModelContext`)
|
|
660
|
+
|
|
661
|
+
Use YAML frontmatter so the model can index structured fields without losing prose context.
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
await app.updateModelContext({
|
|
665
|
+
content: [{ type: "text", text:
|
|
666
|
+
`---\nitem-count: ${items.length}\ntotal-cost: ${total}\n---\n\nCart contents:\n${items.map(i => `- ${i}`).join("\n")}` }],
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Only the **last** update before the next user message reaches the model. Identical updates may be
|
|
671
|
+
deduped.
|
|
672
|
+
|
|
673
|
+
### 8.9 Large follow-up via context + brief trigger
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
await app.updateModelContext({ content: [{ type: "text", text: longTranscriptMarkdown }] });
|
|
677
|
+
await app.sendMessage({ role: "user", content: [{ type: "text", text: "Summarize the key points" }] });
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### 8.10 Fullscreen toggle
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
const ctx = app.getHostContext();
|
|
684
|
+
const target = ctx?.displayMode === "inline" ? "fullscreen" : "inline";
|
|
685
|
+
if (ctx?.availableDisplayModes?.includes(target)) {
|
|
686
|
+
const { mode } = await app.requestDisplayMode({ mode: target });
|
|
687
|
+
container.classList.toggle("fullscreen", mode === "fullscreen");
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### 8.11 Reporting runtime degradation to the model
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
try { await navigator.mediaDevices.getUserMedia({ audio: true }); }
|
|
695
|
+
catch { await app.updateModelContext({ content: [{ type: "text", text: "Error: transcription unavailable" }] }); }
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 8.12 CSP for external resources
|
|
699
|
+
|
|
700
|
+
Set `_meta.ui.csp.connectDomains` for fetch/WebSocket targets and `_meta.ui.resourceDomains` for
|
|
701
|
+
CDN-hosted scripts/styles/fonts/images. Include `localhost` origins during dev. Set
|
|
702
|
+
`_meta.ui.domain` only when an upstream API requires a stable origin for its CORS allowlist.
|
|
703
|
+
|
|
704
|
+
### 8.13 Debug logging
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
app.sendLog({ level: "info", logger: "WeatherApp", data: "Refreshed forecast" });
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`basic-host` surfaces these in the console panel; production hosts MAY route them to telemetry.
|
|
711
|
+
|
|
712
|
+
## 9. Authorization
|
|
713
|
+
|
|
714
|
+
Apps inherit MCP's OAuth model (see the upstream MCP spec § Basic / Authorization). Two patterns
|
|
715
|
+
apply, and they compose:
|
|
716
|
+
|
|
717
|
+
**Per-server auth.** Every request to `/mcp` carries a Bearer token; the host runs the OAuth flow
|
|
718
|
+
once on connect. Use when every tool is sensitive. The MCP TypeScript SDK's `mcpAuthRouter` +
|
|
719
|
+
`ProxyOAuthServerProvider` handles this without custom code.
|
|
720
|
+
|
|
721
|
+
**Per-tool auth.** The HTTP endpoint inspects the raw JSON-RPC body, returns HTTP `401` with a
|
|
722
|
+
`WWW-Authenticate: Bearer resource_metadata="…"` header when a `tools/call` targets a protected
|
|
723
|
+
tool without a valid token, and lets all other tools pass through. The host discovers the
|
|
724
|
+
authorization server via the Protected Resource Metadata URL, completes the OAuth flow, and retries
|
|
725
|
+
with the acquired token. Tool handlers MUST still verify `authInfo` as defence-in-depth.
|
|
726
|
+
|
|
727
|
+
**UI-initiated step-up.** Mix public + protected tools in the same app: the View loads via a
|
|
728
|
+
public tool, then a button calls `app.callServerTool({ name: "protected_tool", arguments: {…} })`.
|
|
729
|
+
The first call returns `401`, the host runs OAuth transparently, and the retry returns the
|
|
730
|
+
protected data. This keeps the initial paint fast while gating sensitive operations.
|
|
731
|
+
|
|
732
|
+
OAuth discovery:
|
|
733
|
+
|
|
734
|
+
- `/.well-known/oauth-protected-resource` — served by `mcpAuthRouter`; identifies the resource
|
|
735
|
+
server and authorization server.
|
|
736
|
+
- `/.well-known/oauth-authorization-server` — advertise endpoints, scopes, and (preferred)
|
|
737
|
+
`client_id_metadata_document_supported: true` to use Client ID Metadata Documents instead of
|
|
738
|
+
Dynamic Client Registration.
|
|
739
|
+
|
|
740
|
+
Verify access tokens as JWTs against the IdP's JWKS endpoint (`createRemoteJWKSet(...)`,
|
|
741
|
+
`jwtVerify(token, JWKS, { issuer: IDP_DOMAIN })`) and confirm the token was issued for this MCP
|
|
742
|
+
server.
|
|
743
|
+
|
|
744
|
+
## 10. Testing
|
|
745
|
+
|
|
746
|
+
`basic-host` is the canonical local test harness:
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
git clone https://github.com/modelcontextprotocol/ext-apps.git && cd ext-apps
|
|
750
|
+
npm install && cd examples/basic-host
|
|
751
|
+
SERVERS='["http://localhost:3001/mcp"]' npm start
|
|
752
|
+
# open http://localhost:8080
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
The UI exposes panels for **Tool Input**, **Tool Result**, **Messages** (View → model), and
|
|
756
|
+
**Model Context**. Browser devtools show `[HOST]`-prefixed logs for server connections, tool calls,
|
|
757
|
+
App initialization, and View→host requests. `app.sendLog(...)` writes into the same stream.
|
|
758
|
+
|
|
759
|
+
Verification checklist for any new app:
|
|
760
|
+
|
|
761
|
+
1. With MCP Apps unsupported in the host, the tool's text `content[]` still renders sensibly.
|
|
762
|
+
2. With MCP Apps supported, the View mounts and `ontoolresult` fires within the same tool call.
|
|
763
|
+
3. `ui/notifications/host-context-changed` is honored (toggle theme, resize, change display mode).
|
|
764
|
+
4. CSP works for every external origin the View touches (check devtools Network/Console for blocks).
|
|
765
|
+
5. `app.onteardown` runs to completion before the iframe unmounts (cleanup observers, timers).
|
|
766
|
+
|
|
767
|
+
For remote hosts that can't reach `localhost`, expose the server with
|
|
768
|
+
`npx cloudflared tunnel --url http://localhost:3001` and register the generated `*.trycloudflare.com`
|
|
769
|
+
URL (plus the MCP path) as a remote MCP server in Claude.ai / VS Code Insiders / Goose.
|
|
770
|
+
|
|
771
|
+
## 11. Common Pitfalls
|
|
772
|
+
|
|
773
|
+
- **Handlers registered after `app.connect()`.** One-shot events (`toolinput`, `toolinputpartial`,
|
|
774
|
+
`toolresult`, `toolcancelled`) MAY have already fired. Assign every `on*` setter (or call
|
|
775
|
+
`addEventListener`) before `app.connect()`. With `strict: true` this throws; the default warns.
|
|
776
|
+
- **Forgotten text `content[]` fallback.** Tools that only return UI break in hosts without the
|
|
777
|
+
MCP Apps extension. Always include a meaningful `content[]` — text fallback is normative.
|
|
778
|
+
- **Misplaced `_meta.ui.csp` / `_meta.ui.domain`.** They belong on the resource **content item**
|
|
779
|
+
(`contents[i]._meta.ui`) returned by the resource callback, not in the `registerAppResource`
|
|
780
|
+
config object. The listing-level `_meta.ui` on the resource entry is only a static fallback.
|
|
781
|
+
- **Hardcoded theme colors.** Use `var(--color-…)` with `light-dark(…)` (or per-`data-theme`
|
|
782
|
+
selectors) and define `:root { --color-…: fallback; }` defaults for every variable consumed.
|
|
783
|
+
- **Missing bundler for single-file delivery.** The View must ship as one self-contained HTML
|
|
784
|
+
payload — there is no same-origin server inside the sandbox. `vite-plugin-singlefile` (or
|
|
785
|
+
equivalent) inlines JS/CSS into the HTML.
|
|
786
|
+
- **Calling host-bound methods before the handshake completes.** `callServerTool`, `sendMessage`,
|
|
787
|
+
`updateModelContext`, `openLink`, etc. all assert `_initializedSent === true`. With
|
|
788
|
+
`strict: false` they warn and proceed (and may race); with `strict: true` they throw. Await
|
|
789
|
+
`app.connect()` or move work into `ontoolresult`.
|
|
790
|
+
- **Version numbers from memory.** The exact name of a hook, request, or notification can change
|
|
791
|
+
across SDK versions. Cross-check against the Reference Index when in doubt instead of relying on
|
|
792
|
+
recall.
|
|
793
|
+
|
|
794
|
+
## 12. Examples — When to Consult Which
|
|
795
|
+
|
|
796
|
+
Curated map of upstream `examples/` (pinned to v1.7.2) by use case. The Recipes in § 8 cover
|
|
797
|
+
isolated patterns; this section points at full working servers when you need to see how a pattern
|
|
798
|
+
composes inside a real project (build config, file layout, framework integration, packaging).
|
|
799
|
+
|
|
800
|
+
### 12.1 Smallest end-to-end skeleton
|
|
801
|
+
|
|
802
|
+
| When | Example | What it shows |
|
|
803
|
+
|------|---------|---------------|
|
|
804
|
+
| First-time scaffolding; need the minimum working tool + UI resource pair | [`examples/quickstart/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/quickstart) | Single `get-time` tool, vanilla TS View, `vite-plugin-singlefile` bundling, Streamable HTTP + stdio transports |
|
|
805
|
+
| Reference host for local testing (not a production host) | [`examples/basic-host/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-host) | How a host wires `PostMessageTransport`, sandbox proxy, tool-input/result pipe; UI panels for Tool Input, Tool Result, Messages, Model Context |
|
|
806
|
+
|
|
807
|
+
### 12.2 Mixed tool patterns (App + plain + app-only in one server)
|
|
808
|
+
|
|
809
|
+
Use these when the new server has more than one tool and you want to see how App-augmented tools
|
|
810
|
+
coexist with plain agent-facing tools and UI-only mutations.
|
|
811
|
+
|
|
812
|
+
| Example | Tool composition |
|
|
813
|
+
|---------|------------------|
|
|
814
|
+
| [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/map-server) | `show-map` (App tool) + `geocode` (plain tool) |
|
|
815
|
+
| [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/pdf-server) | `display_pdf` (App tool) + `list_pdfs` (plain tool) + `read_pdf_bytes` (app-only chunked) |
|
|
816
|
+
| [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/system-monitor-server) | `get-system-info` (App tool) + `poll-system-stats` (app-only polling) |
|
|
817
|
+
|
|
818
|
+
### 12.3 Per-framework starter templates
|
|
819
|
+
|
|
820
|
+
Single-tool minimal servers that demonstrate idiomatic View code for each major frontend framework.
|
|
821
|
+
Pick the one matching your target framework, then layer patterns from § 8 on top.
|
|
822
|
+
|
|
823
|
+
| Framework | Example |
|
|
824
|
+
|-----------|---------|
|
|
825
|
+
| Vanilla TypeScript | [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-vanillajs) |
|
|
826
|
+
| React | [`examples/basic-server-react/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-react) |
|
|
827
|
+
| Vue | [`examples/basic-server-vue/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-vue) |
|
|
828
|
+
| Svelte | [`examples/basic-server-svelte/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-svelte) |
|
|
829
|
+
| Preact | [`examples/basic-server-preact/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-preact) |
|
|
830
|
+
| Solid | [`examples/basic-server-solid/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-solid) |
|
|
831
|
+
|
|
832
|
+
### 12.4 Domain references — pick by use case
|
|
833
|
+
|
|
834
|
+
When the View needs a non-trivial pattern (visualization, streaming, audio, browser API), consult
|
|
835
|
+
the matching domain server for working code.
|
|
836
|
+
|
|
837
|
+
| Domain | Example | What it shows |
|
|
838
|
+
|--------|---------|---------------|
|
|
839
|
+
| Charts / dashboards | [`scenario-modeler-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/scenario-modeler-server) | Chart.js with structured React (`hooks/`, `lib/`, `components/`); multi-scenario comparison |
|
|
840
|
+
| Analytics drill-down | [`cohort-heatmap-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/cohort-heatmap-server) | Heatmap with hover tooltips and click drill-down (React) |
|
|
841
|
+
| Scatter / bubble + filtering | [`customer-segmentation-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/customer-segmentation-server) | Segment clustering, metric switching, click-to-detail customer panel |
|
|
842
|
+
| Interactive numeric input | [`budget-allocator-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/budget-allocator-server) | Sliders / direct edit with real-time chart updates and over-budget validation |
|
|
843
|
+
| 3D visualization | [`threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/threejs-server) | Three.js + streaming tool input into canvas, OrbitControls, post-processing |
|
|
844
|
+
| WebGL / shaders | [`shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/shadertoy-server) | GLSL live compilation, fullscreen mode, `vendor/` pattern for custom JS libs |
|
|
845
|
+
| Graph visualization | [`wiki-explorer-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/wiki-explorer-server) | 3D force-directed graph (`force-graph`), web scraping with `cheerio` |
|
|
846
|
+
| Audio / music notation | [`sheet-music-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/sheet-music-server) | ABC notation → SVG render + MIDI synthesis (`abcjs`) |
|
|
847
|
+
| Streaming + audio | [`say-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/say-server) | `ontoolinputpartial` + async audio queue + multi-view lock (Python FastMCP) |
|
|
848
|
+
| Browser APIs | [`transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/transcript-server) | Web Speech API for live transcription |
|
|
849
|
+
| Binary / media resources | [`video-resource-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/video-resource-server) | Base64 video blobs, `ResourceTemplate`, large-payload limits |
|
|
850
|
+
| Image generation | [`qr-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/qr-server) | Minimal Python (`uv`) server generating customizable QR codes |
|
|
851
|
+
| SDK surface reference | [`debug-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/debug-server) | All content types in one place — PNG/WAV blobs, structured output, stateful counter, resource downloads |
|
|
852
|
+
| Full SDK API exercise | [`integration-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/integration-server) | E2E test reference — exercises `callServerTool`, `sendMessage`, `sendLog`, `openLink` together |
|
|
853
|
+
|
|
854
|
+
## 13. Reference Index
|
|
855
|
+
|
|
856
|
+
All upstream links are pinned to `v1.7.2` (the same tag recorded in the digest header). When this
|
|
857
|
+
digest is regenerated against a newer release the URLs are rewritten in lockstep — the version is a
|
|
858
|
+
single source of truth. Use these links to fetch the exact code corresponding to this digest when
|
|
859
|
+
the digest itself does not answer a specific question.
|
|
860
|
+
|
|
861
|
+
| Aspect | Upstream source (pinned to v1.7.2) | Why look here |
|
|
862
|
+
|--------|------------------------------------|---------------|
|
|
863
|
+
| Wire protocol (normative) | [`specification/2026-01-26/apps.mdx`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/specification/2026-01-26/apps.mdx) | MUST / SHOULD / MAY contract |
|
|
864
|
+
| Lifecycle diagrams | [`apps.mdx` § Lifecycle](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/specification/2026-01-26/apps.mdx#lifecycle) | Canonical message order (verbatim mermaid) |
|
|
865
|
+
| Draft delta tracking | [`specification/draft/apps.mdx`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/specification/draft/apps.mdx) | Pre-stable additions (metadata-location clarification, sandbox MUST → SHOULD softening) |
|
|
866
|
+
| `App` class + handlers | [`src/app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/app.ts) | Constructor, every `on*` setter, every host-bound method |
|
|
867
|
+
| Server helpers | [`src/server/index.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/server/index.ts) | `registerAppTool`, `registerAppResource`, `getUiCapability`, `RESOURCE_MIME_TYPE`, `RESOURCE_URI_META_KEY`, `EXTENSION_ID` |
|
|
868
|
+
| Type-level contract | [`src/spec.types.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/spec.types.ts) | `McpUiHostContext`, `McpUiHostCapabilities`, `McpUiAppCapabilities`, `McpUiResourceCsp`, `McpUiResourceMeta`, `McpUiToolMeta`, `McpUiStyleVariableKey`, all message schemas |
|
|
869
|
+
| Cross-cutting types | [`src/types.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/types.ts) | `AppRequest`, `AppNotification`, `AppResult`, `AppEventMap` re-exports |
|
|
870
|
+
| Style helpers | [`src/styles.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/styles.ts) | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts`, `getDocumentTheme` |
|
|
871
|
+
| Transport | [`src/message-transport.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/message-transport.ts) | `PostMessageTransport` source validation + dropped-message rules |
|
|
872
|
+
| React `useApp` | [`src/react/useApp.tsx`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/react/useApp.tsx) | `UseAppOptions`, `AppState`, mount/connect semantics |
|
|
873
|
+
| React style hooks | [`src/react/useHostStyles.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/react/useHostStyles.ts) | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` |
|
|
874
|
+
| React theme hook | [`src/react/useDocumentTheme.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/react/useDocumentTheme.ts) | `MutationObserver`-driven theme tracking |
|
|
875
|
+
| React auto-resize | [`src/react/useAutoResize.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/src/react/useAutoResize.ts) | Only needed when `App` was created with `autoResize: false` |
|
|
876
|
+
| Pattern catalog | [`docs/patterns.md`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/docs/patterns.md) | Authoritative recipes |
|
|
877
|
+
| CSP / CORS rules | [`docs/csp-cors.md`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/docs/csp-cors.md) | Where keys go (`contents[]`, not config) and stable-origin guidance |
|
|
878
|
+
| Authorization flows | [`docs/authorization.md`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/docs/authorization.md) | Per-server, per-tool, UI-initiated step-up; OAuth discovery |
|
|
879
|
+
| Testing harness | [`docs/testing-mcp-apps.md`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/docs/testing-mcp-apps.md) | `basic-host` workflow, panels, `sendLog`, `cloudflared` |
|
|
880
|
+
| Quickstart skeleton | [`docs/quickstart.md`](https://github.com/modelcontextprotocol/ext-apps/blob/v1.7.2/docs/quickstart.md) + [`examples/quickstart/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/quickstart) | Smallest end-to-end server + View |
|
|
881
|
+
| Mixed tool servers | [`map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/map-server), [`pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/pdf-server), [`system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/system-monitor-server) | App tool + plain tool + app-only tool combinations |
|
|
882
|
+
| Framework variants | [`vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-vanillajs), [`react`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-react), [`vue`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-vue), [`svelte`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-svelte), [`preact`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-preact), [`solid`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/basic-server-solid) | Minimal per-framework implementations |
|
|
883
|
+
| Domain examples | [`scenario-modeler-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/scenario-modeler-server), [`cohort-heatmap-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/cohort-heatmap-server), [`threejs-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/threejs-server), [`shadertoy-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/shadertoy-server), [`wiki-explorer-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/wiki-explorer-server), [`sheet-music-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/sheet-music-server), [`say-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/say-server), [`transcript-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/transcript-server), [`video-resource-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/video-resource-server), [`debug-server`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples/debug-server) | Domain-specific working code per use case |
|
|
884
|
+
| All examples (root) | [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2/examples) | Browse the full set |
|
|
885
|
+
| Repo root @ pinned tag | [`v1.7.2`](https://github.com/modelcontextprotocol/ext-apps/tree/v1.7.2) (commit `9a37ad7`) | Anything not listed above |
|