Skip to content

Commit cac214f

Browse files
Merge pull request #290 from jonathanhefner/migrate-oai-app-skill
Add "Migrate from OpenAI App" skill to Claude Code plugin
2 parents e6983b3 + 90dad1a commit cac214f

File tree

6 files changed

+230
-19
lines changed

6 files changed

+230
-19
lines changed

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

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 |

plugins/mcp-apps/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,17 @@ Install via Claude Code:
1212

1313
## Usage
1414

15-
Invoke the skill by asking Claude Code to:
15+
Invoke skills by asking Claude Code to:
16+
17+
### Create MCP App
1618

1719
- "Create an MCP App"
1820
- "Add a UI to an MCP tool"
1921
- "Build an interactive MCP widget"
2022
- "Scaffold an MCP App"
23+
24+
### Migrate from OpenAI
25+
26+
- "Migrate from OpenAI Apps SDK"
27+
- "Convert OpenAI App to MCP"
28+
- "Port from window.openai"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Each template includes:
7171

7272
### API Reference (Source Files)
7373

74-
Read JSDoc documentation directly from source:
74+
Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
7575

7676
| File | Contents |
7777
|------|----------|
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
name: Migrate from OpenAI App
3+
description: This skill should be used when the user asks to "migrate from OpenAI Apps SDK", "convert OpenAI App to MCP", "port from window.openai", "migrate from skybridge", "convert openai/outputTemplate", or needs guidance on converting OpenAI Apps SDK applications to MCP Apps SDK. Provides step-by-step migration guidance with API mapping tables.
4+
---
5+
6+
# Migrate OpenAI App to MCP
7+
8+
Migrate existing OpenAI Apps SDK applications to the MCP Apps SDK (`@modelcontextprotocol/ext-apps`). The MCP Apps SDK provides a standardized, open protocol for interactive UIs in conversational clients.
9+
10+
## Best Practices
11+
12+
- Use your package manager to add dependencies (e.g., `npm install`, `pnpm add`, `yarn add`) instead of manually writing version numbers. This lets the package manager resolve the latest compatible versions. Never specify version numbers from memory.
13+
- Preemptively add a final todo item with this exact wording: "Re-read the 'Before Finishing' checklist in this skill and address each checkbox individually, stating what you did for each one, before marking this todo complete."
14+
15+
## Getting Reference Code
16+
17+
Clone the SDK repository for complete migration documentation and working examples:
18+
19+
```bash
20+
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
21+
```
22+
23+
### Migration Reference Guide
24+
25+
Read the migration reference guide with "before/after" mapping tables: `/tmp/mcp-ext-apps/docs/migrate_from_openai_apps.md`
26+
27+
### API Reference (Source Files)
28+
29+
Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/*`:
30+
31+
| File | Contents |
32+
|------|----------|
33+
| `src/app.ts` | `App` class, handlers, lifecycle |
34+
| `src/server/index.ts` | `registerAppTool`, `registerAppResource` |
35+
| `src/spec.types.ts` | Type definitions |
36+
| `src/react/useApp.tsx` | `useApp` hook for React apps |
37+
| `src/react/use*.ts*` | Other `use*` hooks for React apps |
38+
39+
### Front-End Framework Examples
40+
41+
See `/tmp/mcp-ext-apps/examples/basic-server-{framework}/` for basic SDK usage examples organized by front-end framework:
42+
43+
| Template | Key Files |
44+
|----------|-----------|
45+
| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
46+
| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
47+
| `basic-server-vue/` | `server.ts`, `src/App.vue` |
48+
| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
49+
| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
50+
| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
51+
52+
## CSP Investigation
53+
54+
MCP Apps HTML is served as an MCP resource, not as a web page, and runs in a sandboxed iframe with no same-origin server. **Every** origin must be declared in CSP—including the origin serving your JS/CSS bundles (`localhost` in dev, your CDN in production). Missing origins fail silently.
55+
56+
**Before writing any migration code**, build the app and investigate all origins it references:
57+
58+
1. Build the app using the existing build command
59+
2. Search the resulting HTML, CSS, and JS for **every** origin (not just "external" origins—every network request will need CSP approval)
60+
3. For each origin found, trace back to source:
61+
- If it comes from a constant → universal (same in dev and prod)
62+
- If it comes from an env var or conditional → note the mechanism and identify both dev and prod values
63+
4. Check for third-party libraries that may make their own requests (analytics, error tracking, etc.)
64+
65+
**Document your findings** as three lists, and note for each origin whether it's universal, dev-only, or prod-only:
66+
67+
- **resourceDomains**: origins serving images, fonts, styles, scripts
68+
- **connectDomains**: origins for API/fetch requests
69+
- **frameDomains**: origins for nested iframes
70+
71+
If no origins are found, the app may not need custom CSP domains.
72+
73+
## CORS Configuration
74+
75+
MCP clients make cross-origin requests. If using Express, `app.use(cors())` handles this.
76+
77+
For raw HTTP servers, configure standard CORS and additionally:
78+
- Allow headers: `mcp-session-id`, `mcp-protocol-version`, `last-event-id`
79+
- Expose headers: `mcp-session-id`
80+
81+
## Key Conceptual Changes
82+
83+
### Server-Side
84+
85+
Use `registerAppTool()` and `registerAppResource()` helpers instead of raw `server.registerTool()` / `server.registerResource()`. These helpers handle the MCP Apps metadata format automatically.
86+
87+
See `/tmp/mcp-ext-apps/docs/migrate_from_openai_apps.md` for server-side mapping tables.
88+
89+
### Client-Side
90+
91+
The fundamental paradigm shift: OpenAI uses a synchronous global object (`window.openai.toolInput`, `window.openai.theme`) that's pre-populated before your code runs. MCP Apps uses an `App` instance with async event handlers.
92+
93+
Key differences:
94+
- Create an `App` instance and register handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`) **before** calling `connect()`. (Events may fire immediately after connection, so handlers must be registered first.)
95+
- Access tool data via handlers: `app.ontoolinput` for `window.openai.toolInput`, `app.ontoolresult` for `window.openai.toolOutput`.
96+
- Access host environment (theme, locale, etc.) via `app.getHostContext()`.
97+
98+
For React apps, the `useApp` hook manages this lifecycle automatically—see `basic-server-react/` for the pattern.
99+
100+
See `/tmp/mcp-ext-apps/docs/migrate_from_openai_apps.md` for client-side mapping tables.
101+
102+
### Features Not Yet Available in MCP Apps
103+
104+
These OpenAI features don't have MCP equivalents yet:
105+
106+
**Server-side:**
107+
| OpenAI Feature | Status/Workaround |
108+
|----------------|-------------------|
109+
| `_meta["openai/toolInvocation/invoking"]` / `_meta["openai/toolInvocation/invoked"]` | Progress indicators not yet available |
110+
| `_meta["openai/widgetDescription"]` | Use `app.updateModelContext()` for dynamic context |
111+
112+
**Client-side:**
113+
| OpenAI Feature | Status/Workaround |
114+
|----------------|-------------------|
115+
| `window.openai.widgetState` / `setWidgetState()` | Use `localStorage` or server-side state |
116+
| `window.openai.uploadFile()` / `getFileDownloadUrl()` | File operations not yet available |
117+
| `window.openai.requestModal()` / `requestClose()` | Modal management not yet available |
118+
| `window.openai.view` | Not yet available |
119+
120+
## Before Finishing
121+
122+
Slow down and carefully follow each item in this checklist:
123+
124+
- [ ] Search for and migrate any remaining server-side OpenAI patterns:
125+
126+
| Pattern | Indicates |
127+
|---------|-----------|
128+
| `"openai/` | Old metadata keys → `_meta.ui.*` |
129+
| `text/html+skybridge` | Old MIME type → `RESOURCE_MIME_TYPE` constant |
130+
| `text/html;profile=mcp-app` | New MIME type, but prefer `RESOURCE_MIME_TYPE` constant |
131+
| `_domains"` or `_domains:` | snake_case CSP → camelCase (`connect_domains``connectDomains`) |
132+
133+
- [ ] Search for and migrate any remaining client-side OpenAI patterns:
134+
135+
| Pattern | Indicates |
136+
|---------|-----------|
137+
| `window.openai.toolInput` | Old global → `params.arguments` in `ontoolinput` handler |
138+
| `window.openai.toolOutput` | Old global → `params.structuredContent` in `ontoolresult` |
139+
| `window.openai` | Old global API → `App` instance methods |
140+
141+
- [ ] For each origin from your CSP investigation, show where it appears in the `registerAppResource()` CSP config. **Every** origin from the CSP investigation (universal, dev-only, prod-only) must be included in the CSP config—MCP Apps HTML runs in a sandboxed iframe **with no same-origin server**. If an origin was not included in the CSP config, add it now.
142+
143+
- [ ] For each conditional (dev-only, prod-only) origin from your CSP investigation, show the code where the same configuration setting (env var, config file, etc.) controls both the runtime URL and the CSP entry. If the CSP has a hardcoded origin that should be conditional, fix it now—the app must be production-ready.
144+
145+
## Testing
146+
147+
### Using basic-host
148+
149+
Test the migrated app with the basic-host example:
150+
151+
```bash
152+
# Terminal 1: Build and run your server
153+
npm run build && npm run serve
154+
155+
# Terminal 2: Run basic-host (from cloned repo)
156+
cd /tmp/mcp-ext-apps/examples/basic-host
157+
npm install
158+
SERVERS='["http://localhost:3001/mcp"]' npm run start
159+
# Open http://localhost:8080
160+
```
161+
162+
### Verify Runtime Behavior
163+
164+
Once the app loads in basic-host, confirm:
165+
1. App loads without console errors
166+
2. `ontoolinput` handler fires with tool arguments
167+
3. `ontoolresult` handler fires with tool result

src/server/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,22 @@ export function registerAppTool<
243243
* ```typescript
244244
* registerAppResource(server, "Music Player", "ui://music/player.html", {
245245
* description: "Audio player with external soundfonts",
246-
* _meta: {
247-
* ui: {
248-
* csp: {
249-
* connectDomains: ["https://api.example.com"], // For fetch/WebSocket
250-
* resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images
246+
* }, async () => ({
247+
* contents: [{
248+
* uri: "ui://music/player.html",
249+
* mimeType: RESOURCE_MIME_TYPE,
250+
* text: PLAYER_HTML,
251+
* // CSP must be on the content item, not the resource config
252+
* _meta: {
253+
* ui: {
254+
* csp: {
255+
* connectDomains: ["https://api.example.com"], // For fetch/WebSocket
256+
* resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images
257+
* },
251258
* },
252259
* },
253-
* },
254-
* }, readCallback);
260+
* }],
261+
* }));
255262
* ```
256263
*
257264
* @see {@link registerAppTool} to register tools that reference this resource

0 commit comments

Comments
 (0)