Skip to content

Commit 5ef3120

Browse files
committed
Merge remote-tracking branch 'origin/main' into ochafik/readme-mcp-config
2 parents ac1130c + cac214f commit 5ef3120

Some content is hidden

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

58 files changed

+2711
-217
lines changed

.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: 2 additions & 2 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

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;

examples/basic-server-preact/src/global.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ html, body {
1010
code {
1111
font-size: 1em;
1212
}
13+
14+
/* Server time fills remaining width for consistent E2E screenshot masking */
15+
#server-time {
16+
flex: 1;
17+
min-width: 0;
18+
}

examples/basic-server-preact/src/mcp-app.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
margin-top: 0.5rem;
2929
}
3030

31+
/* Server time row: flex layout for consistent mask width in E2E tests */
32+
> p {
33+
display: flex;
34+
align-items: baseline;
35+
gap: 0.25em;
36+
}
37+
3138
/* Consistent font for form inputs (inherits from global.css) */
3239
textarea,
3340
input {

examples/basic-server-react/src/global.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ html, body {
1010
code {
1111
font-size: 1em;
1212
}
13+
14+
/* Server time fills remaining width for consistent E2E screenshot masking */
15+
#server-time {
16+
flex: 1;
17+
min-width: 0;
18+
}

examples/basic-server-react/src/mcp-app.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
margin-top: 0.5rem;
2929
}
3030

31+
/* Server time row: flex layout for consistent mask width in E2E tests */
32+
> p {
33+
display: flex;
34+
align-items: baseline;
35+
gap: 0.25em;
36+
}
37+
3138
/* Consistent font for form inputs (inherits from global.css) */
3239
textarea,
3340
input {

examples/basic-server-solid/src/global.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ html, body {
1010
code {
1111
font-size: 1em;
1212
}
13+
14+
/* Server time fills remaining width for consistent E2E screenshot masking */
15+
#server-time {
16+
flex: 1;
17+
min-width: 0;
18+
}

0 commit comments

Comments
 (0)