Skip to content

Commit 586c67e

Browse files
committed
Merge remote-tracking branch 'origin/main' into ochafik/mcp-apps-widgetSessionId
2 parents 225d9cf + f6ee5d5 commit 586c67e

76 files changed

Lines changed: 3385 additions & 232 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ intermediate-findings/
1111
# Playwright
1212
playwright-report/
1313
test-results/
14+
__pycache__/
15+
*.pyc

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,7 @@ Uses npm workspaces. Examples in `examples/` are separate packages:
8888

8989
## Claude Code Plugin
9090

91-
The `plugins/mcp-apps/` directory contains a Claude Code plugin distributed via the plugin marketplace. It provides the "Create MCP App" skill (`plugins/mcp-apps/skills/create-mcp-app/SKILL.md`) that guides users through building MCP Apps with interactive UIs.
91+
The `plugins/mcp-apps/` directory contains a Claude Code plugin distributed via the plugin marketplace. It provides the following Claude Code skills files:
92+
93+
- `plugins/mcp-apps/skills/create-mcp-app/SKILL.md` — for creating an MCP App
94+
- `plugins/mcp-apps/skills/migrate-oai-app/SKILL.md` — for migrating an app from the OpenAI Apps SDK to the MCP Apps SDK

README.md

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ Or edit your `package.json` manually:
6767
| [**Scenario Modeler**](examples/scenario-modeler-server) | [**Budget Allocator**](examples/budget-allocator-server) | [**Customer Segmentation**](examples/customer-segmentation-server) |
6868
| [![System Monitor](examples/system-monitor-server/grid-cell.png "Real-time OS metrics")](examples/system-monitor-server) | [![Transcript](examples/transcript-server/grid-cell.png "Live speech transcription")](examples/transcript-server) | [![Video Resource](examples/video-resource-server/grid-cell.png "Binary video via MCP resources")](examples/video-resource-server) |
6969
| [**System Monitor**](examples/system-monitor-server) | [**Transcript**](examples/transcript-server) | [**Video Resource**](examples/video-resource-server) |
70-
| [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](examples/pdf-server) | [![QR Code](examples/qr-server/grid-cell.png "QR code generator")](examples/qr-server) | |
71-
| [**PDF Server**](examples/pdf-server) | [**QR Code (Python)**](examples/qr-server) | |
70+
| [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](examples/pdf-server) | [![QR Code](examples/qr-server/grid-cell.png "QR code generator")](examples/qr-server) | [![Say Demo](examples/say-server/grid-cell.png "Text-to-speech demo")](examples/say-server) |
71+
| [**PDF Server**](examples/pdf-server) | [**QR Code (Python)**](examples/qr-server) | [**Say Demo**](examples/say-server) |
7272

7373
### Starter Templates
7474

@@ -79,7 +79,231 @@ Or edit your `package.json` manually:
7979

8080
The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases.
8181

82-
To run all examples:
82+
<details>
83+
<summary>MCP client configuration for all examples</summary>
84+
85+
Add to your MCP client configuration (stdio transport):
86+
87+
```json
88+
{
89+
"mcpServers": {
90+
"basic-react": {
91+
"command": "npx",
92+
"args": [
93+
"-y",
94+
"--silent",
95+
"--registry=https://registry.npmjs.org/",
96+
"@modelcontextprotocol/server-basic-react",
97+
"--stdio"
98+
]
99+
},
100+
"basic-vanillajs": {
101+
"command": "npx",
102+
"args": [
103+
"-y",
104+
"--silent",
105+
"--registry=https://registry.npmjs.org/",
106+
"@modelcontextprotocol/server-basic-vanillajs",
107+
"--stdio"
108+
]
109+
},
110+
"basic-vue": {
111+
"command": "npx",
112+
"args": [
113+
"-y",
114+
"--silent",
115+
"--registry=https://registry.npmjs.org/",
116+
"@modelcontextprotocol/server-basic-vue",
117+
"--stdio"
118+
]
119+
},
120+
"basic-svelte": {
121+
"command": "npx",
122+
"args": [
123+
"-y",
124+
"--silent",
125+
"--registry=https://registry.npmjs.org/",
126+
"@modelcontextprotocol/server-basic-svelte",
127+
"--stdio"
128+
]
129+
},
130+
"basic-preact": {
131+
"command": "npx",
132+
"args": [
133+
"-y",
134+
"--silent",
135+
"--registry=https://registry.npmjs.org/",
136+
"@modelcontextprotocol/server-basic-preact",
137+
"--stdio"
138+
]
139+
},
140+
"basic-solid": {
141+
"command": "npx",
142+
"args": [
143+
"-y",
144+
"--silent",
145+
"--registry=https://registry.npmjs.org/",
146+
"@modelcontextprotocol/server-basic-solid",
147+
"--stdio"
148+
]
149+
},
150+
"budget-allocator": {
151+
"command": "npx",
152+
"args": [
153+
"-y",
154+
"--silent",
155+
"--registry=https://registry.npmjs.org/",
156+
"@modelcontextprotocol/server-budget-allocator",
157+
"--stdio"
158+
]
159+
},
160+
"cohort-heatmap": {
161+
"command": "npx",
162+
"args": [
163+
"-y",
164+
"--silent",
165+
"--registry=https://registry.npmjs.org/",
166+
"@modelcontextprotocol/server-cohort-heatmap",
167+
"--stdio"
168+
]
169+
},
170+
"customer-segmentation": {
171+
"command": "npx",
172+
"args": [
173+
"-y",
174+
"--silent",
175+
"--registry=https://registry.npmjs.org/",
176+
"@modelcontextprotocol/server-customer-segmentation",
177+
"--stdio"
178+
]
179+
},
180+
"map": {
181+
"command": "npx",
182+
"args": [
183+
"-y",
184+
"--silent",
185+
"--registry=https://registry.npmjs.org/",
186+
"@modelcontextprotocol/server-map",
187+
"--stdio"
188+
]
189+
},
190+
"pdf": {
191+
"command": "npx",
192+
"args": [
193+
"-y",
194+
"--silent",
195+
"--registry=https://registry.npmjs.org/",
196+
"@modelcontextprotocol/server-pdf",
197+
"--stdio"
198+
]
199+
},
200+
"scenario-modeler": {
201+
"command": "npx",
202+
"args": [
203+
"-y",
204+
"--silent",
205+
"--registry=https://registry.npmjs.org/",
206+
"@modelcontextprotocol/server-scenario-modeler",
207+
"--stdio"
208+
]
209+
},
210+
"shadertoy": {
211+
"command": "npx",
212+
"args": [
213+
"-y",
214+
"--silent",
215+
"--registry=https://registry.npmjs.org/",
216+
"@modelcontextprotocol/server-shadertoy",
217+
"--stdio"
218+
]
219+
},
220+
"sheet-music": {
221+
"command": "npx",
222+
"args": [
223+
"-y",
224+
"--silent",
225+
"--registry=https://registry.npmjs.org/",
226+
"@modelcontextprotocol/server-sheet-music",
227+
"--stdio"
228+
]
229+
},
230+
"system-monitor": {
231+
"command": "npx",
232+
"args": [
233+
"-y",
234+
"--silent",
235+
"--registry=https://registry.npmjs.org/",
236+
"@modelcontextprotocol/server-system-monitor",
237+
"--stdio"
238+
]
239+
},
240+
"threejs": {
241+
"command": "npx",
242+
"args": [
243+
"-y",
244+
"--silent",
245+
"--registry=https://registry.npmjs.org/",
246+
"@modelcontextprotocol/server-threejs",
247+
"--stdio"
248+
]
249+
},
250+
"transcript": {
251+
"command": "npx",
252+
"args": [
253+
"-y",
254+
"--silent",
255+
"--registry=https://registry.npmjs.org/",
256+
"@modelcontextprotocol/server-transcript",
257+
"--stdio"
258+
]
259+
},
260+
"video-resource": {
261+
"command": "npx",
262+
"args": [
263+
"-y",
264+
"--silent",
265+
"--registry=https://registry.npmjs.org/",
266+
"@modelcontextprotocol/server-video-resource",
267+
"--stdio"
268+
]
269+
},
270+
"wiki-explorer": {
271+
"command": "npx",
272+
"args": [
273+
"-y",
274+
"--silent",
275+
"--registry=https://registry.npmjs.org/",
276+
"@modelcontextprotocol/server-wiki-explorer",
277+
"--stdio"
278+
]
279+
},
280+
"qr": {
281+
"command": "uv",
282+
"args": [
283+
"run",
284+
"/path/to/ext-apps/examples/qr-server/server.py",
285+
"--stdio"
286+
]
287+
},
288+
"say": {
289+
"command": "uv",
290+
"args": [
291+
"run",
292+
"--default-index",
293+
"https://pypi.org/simple",
294+
"https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/refs/heads/main/examples/say-server/server.py",
295+
"--stdio"
296+
]
297+
}
298+
}
299+
}
300+
```
301+
302+
> **Note:** The `qr` server requires cloning the repository first. See [qr-server README](examples/qr-server) for details.
303+
304+
</details>
305+
306+
To run all examples locally in dev mode:
83307

84308
```bash
85309
npm install

docs/migrate_from_openai_apps.md

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,30 @@ This guide helps you migrate from the OpenAI Apps SDK to the MCP Apps SDK (`@mod
2424

2525
### Resource Metadata
2626

27-
| OpenAI | MCP Apps | Notes |
28-
| ------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
29-
| `_meta["openai/widgetCSP"]` | `_meta.ui.csp` | `connect_domains``connectDomains`, `resource_domains``resourceDomains`, etc. |
30-
|| `_meta.ui.permissions` | MCP adds: permissions for camera, microphone, geolocation, clipboard |
31-
| `_meta["openai/widgetDomain"]` | `_meta.ui.domain` | Dedicated sandbox origin |
32-
| `_meta["openai/widgetPrefersBorder"]` | `_meta.ui.prefersBorder` | Visual boundary preference |
33-
| `_meta["openai/widgetDescription"]` || Not yet implemented; use `app.updateModelContext()` for dynamic context |
27+
| OpenAI | MCP Apps | Notes |
28+
| ------------------------------------- | ------------------------ | ----------------------------------------------------------------------- |
29+
| `_meta["openai/widgetCSP"]` | `_meta.ui.csp` | See [CSP field mapping](#csp-field-mapping) below |
30+
|| `_meta.ui.permissions` | MCP adds: permissions for camera, microphone, geolocation, clipboard |
31+
| `_meta["openai/widgetDomain"]` | `_meta.ui.domain` | Dedicated sandbox origin |
32+
| `_meta["openai/widgetPrefersBorder"]` | `_meta.ui.prefersBorder` | Visual boundary preference |
33+
| `_meta["openai/widgetDescription"]` || Not yet implemented; use `app.updateModelContext()` for dynamic context |
3434

3535
### Resource MIME Type
3636

3737
| OpenAI | MCP Apps | Notes |
3838
| --------------------- | --------------------------- | -------------------------------------------------------------------------------- |
3939
| `text/html+skybridge` | `text/html;profile=mcp-app` | Auto-set by `registerAppResource()`; use `RESOURCE_MIME_TYPE` constant if manual |
4040

41+
### CSP Field Mapping
42+
43+
| OpenAI | MCP Apps | Notes |
44+
| ------------------ | ----------------- | ---------------------------------------------------------- |
45+
| `resource_domains` | `resourceDomains` | Origins for static assets (images, fonts, styles, scripts) |
46+
| `connect_domains` | `connectDomains` | Origins for fetch/XHR/WebSocket requests |
47+
| `frame_domains` | `frameDomains` | Origins for nested iframes |
48+
| `redirect_domains` || OpenAI-only: origins for `openExternal` redirects |
49+
|| `baseUriDomains` | MCP-only: `base-uri` CSP directive |
50+
4151
### Server-Side Migration Example
4252

4353
### Before (OpenAI)
@@ -84,6 +94,13 @@ function createServer() {
8494
uri: "ui://widget/cart.html",
8595
mimeType: "text/html+skybridge",
8696
text: getCartHtml(),
97+
_meta: {
98+
"openai/widgetCSP": {
99+
resource_domains: ["https://cdn.example.com"],
100+
connect_domains: ["https://api.example.com"],
101+
frame_domains: ["https://embed.example.com"],
102+
},
103+
},
87104
},
88105
],
89106
}),
@@ -139,6 +156,15 @@ function createServer() {
139156
uri: "ui://widget/cart.html",
140157
mimeType: RESOURCE_MIME_TYPE,
141158
text: getCartHtml(),
159+
_meta: {
160+
ui: {
161+
csp: {
162+
resourceDomains: ["https://cdn.example.com"],
163+
connectDomains: ["https://api.example.com"],
164+
frameDomains: ["https://embed.example.com"],
165+
},
166+
},
167+
},
142168
},
143169
],
144170
}),
@@ -152,7 +178,7 @@ function createServer() {
152178

153179
1. **Metadata Structure**: OpenAI uses flat `_meta["openai/..."]` properties; MCP uses nested `_meta.ui.*` structure
154180
2. **Tool Visibility**: OpenAI uses boolean/string (`true`/`"public"`); MCP uses string arrays (`["app", "model"]`)
155-
3. **CSP Property Names**: snake_case → camelCase (`connect_domains``connectDomains`)
181+
3. **CSP Field Names**: snake_case → camelCase (e.g., `connect_domains``connectDomains`)
156182
4. **App Permissions**: MCP adds `_meta.ui.permissions` for camera, microphone, geolocation, clipboard (not in OpenAI)
157183
5. **Resource MIME Type**: `text/html+skybridge``text/html;profile=mcp-app` (use `RESOURCE_MIME_TYPE` constant)
158184
6. **Helper Functions**: MCP provides `registerAppTool()` and `registerAppResource()` helpers
@@ -172,7 +198,7 @@ function createServer() {
172198

173199
| OpenAI | MCP Apps | Notes |
174200
| -------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
175-
| `window.openai` (auto-available) | `const app = new App({name, version}, {})` | MCP requires explicit instantiation |
201+
| `window.openai` (auto-available) | `const app = new App({name, version})` | MCP requires explicit instantiation |
176202
| (implicit) | Vanilla: `await app.connect()` / React: `useApp()` | MCP requires async connection; auto-detects OpenAI env |
177203
|| `await app.connect(new OpenAITransport())` | Force OpenAI mode (not yet available, see [PR #172](https://github.com/modelcontextprotocol/ext-apps/pull/172)) |
178204
|| `await app.connect(new PostMessageTransport(...))` | Force MCP mode explicitly |

examples/basic-host/src/implementation.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
22
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
34
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
45
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
56

@@ -24,11 +25,8 @@ export interface ServerInfo {
2425

2526

2627
export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
27-
const client = new Client(IMPLEMENTATION);
28-
2928
log.info("Connecting to server:", serverUrl.href);
30-
await client.connect(new StreamableHTTPClientTransport(serverUrl));
31-
log.info("Connection successful");
29+
const client = await connectWithFallback(serverUrl);
3230

3331
const name = client.getServerVersion()?.name ?? serverUrl.href;
3432

@@ -39,6 +37,28 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
3937
return { name, client, tools, appHtmlCache: new Map() };
4038
}
4139

40+
async function connectWithFallback(serverUrl: URL): Promise<Client> {
41+
// Try Streamable HTTP first (modern transport)
42+
try {
43+
const client = new Client(IMPLEMENTATION);
44+
await client.connect(new StreamableHTTPClientTransport(serverUrl));
45+
log.info("Connected via Streamable HTTP transport");
46+
return client;
47+
} catch (streamableError) {
48+
log.info("Streamable HTTP failed:", streamableError);
49+
}
50+
51+
// Fall back to SSE (deprecated but needed for older servers)
52+
try {
53+
const client = new Client(IMPLEMENTATION);
54+
await client.connect(new SSEClientTransport(serverUrl));
55+
log.info("Connected via SSE transport");
56+
return client;
57+
} catch (sseError) {
58+
throw new Error(`Could not connect with any transport. SSE error: ${sseError}`);
59+
}
60+
}
61+
4262

4363
interface UiResourceData {
4464
html: string;

0 commit comments

Comments
 (0)