393 lines
20 KiB
Markdown
393 lines
20 KiB
Markdown
---
|
|
title: Migrate OpenAI App
|
|
group: Getting Started
|
|
description: Migrate from the OpenAI Apps SDK to MCP Apps SDK with concept mapping tables, API equivalents, and complete before/after code examples.
|
|
---
|
|
|
|
# Migrating from OpenAI Apps SDK to MCP Apps SDK
|
|
|
|
This reference maps OpenAI Apps SDK concepts to their MCP Apps SDK (`@modelcontextprotocol/ext-apps`) equivalents. Use the tables below for quick lookup during migration, and refer to the code examples for complete before/after comparisons.
|
|
|
|
This guide covers server-side changes first (metadata, tools, resources), then client-side changes (setup, context, events).
|
|
|
|
> [!NOTE]
|
|
> Some OpenAI Apps SDK features don't have MCP equivalents yet. These are marked "Not yet implemented" in the tables below.
|
|
|
|
## Server-Side
|
|
|
|
The server-side changes involve updating metadata structure and using helper functions.
|
|
|
|
### Quick Start Comparison
|
|
|
|
| OpenAI Apps SDK | MCP Apps SDK |
|
|
| ---------------------------------------------------------- | -------------------------------------------------------------- |
|
|
| Flat metadata keys (`_meta["openai/..."]`) | Nested metadata structure (`_meta.ui.*`) |
|
|
| Direct `server.registerTool()`/`server.registerResource()` | Helper functions: `registerAppTool()`, `registerAppResource()` |
|
|
| UI Resource MIME type: `text/html+skybridge` | UI Resource MIME type: `text/html;profile=mcp-app` |
|
|
|
|
### Tool Metadata
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ---------------------------------------------- | ---------------------------------- | ----------------------------------------------------------- |
|
|
| `_meta["openai/outputTemplate"]` | `_meta.ui.resourceUri` | URI of UI resource |
|
|
| `_meta["openai/toolInvocation/invoking"]` | — | Not yet implemented |
|
|
| `_meta["openai/toolInvocation/invoked"]` | — | Not yet implemented |
|
|
| `_meta["openai/widgetAccessible"]` (`boolean`) | `_meta.ui.visibility` (`string[]`) | `true`/`false` → include/exclude `"app"` in array |
|
|
| `_meta["openai/visibility"]` (`string`) | `_meta.ui.visibility` (`string[]`) | `"public"`/`"private"` → include/exclude `"model"` in array |
|
|
|
|
### Resource Metadata
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------------------------- | ------------------------ | ----------------------------------------------------------------------- |
|
|
| `_meta["openai/widgetCSP"]` | `_meta.ui.csp` | See [CSP field mapping](#csp-field-mapping) below |
|
|
| — | `_meta.ui.permissions` | MCP adds: permissions for camera, microphone, geolocation, clipboard |
|
|
| `_meta["openai/widgetDomain"]` | `_meta.ui.domain` | Dedicated sandbox origin |
|
|
| `_meta["openai/widgetPrefersBorder"]` | `_meta.ui.prefersBorder` | Visual boundary preference |
|
|
| `_meta["openai/widgetDescription"]` | — | Not yet implemented; use `app.updateModelContext()` for dynamic context |
|
|
|
|
### Resource MIME Type
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| --------------------- | --------------------------- | -------------------------------------------------------------------------------- |
|
|
| `text/html+skybridge` | `text/html;profile=mcp-app` | Auto-set by `registerAppResource()`; use `RESOURCE_MIME_TYPE` constant if manual |
|
|
|
|
### CSP Field Mapping
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------ | ----------------- | ---------------------------------------------------------- |
|
|
| `resource_domains` | `resourceDomains` | Origins for static assets (images, fonts, styles, scripts) |
|
|
| `connect_domains` | `connectDomains` | Origins for fetch/XHR/WebSocket requests |
|
|
| `frame_domains` | `frameDomains` | Origins for nested iframes |
|
|
| `redirect_domains` | — | OpenAI-only: origins for `openExternal` redirects |
|
|
| — | `baseUriDomains` | MCP-only: `base-uri` CSP directive |
|
|
|
|
### Server-Side Migration Example
|
|
|
|
#### Before (OpenAI)
|
|
|
|
```typescript
|
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
import { z } from "zod";
|
|
|
|
function createServer() {
|
|
const server = new McpServer({ name: "shop", version: "1.0.0" });
|
|
|
|
// Register tool with OpenAI metadata
|
|
server.registerTool(
|
|
"shopping-cart",
|
|
{
|
|
title: "Shopping Cart",
|
|
description: "Display the user's shopping cart",
|
|
inputSchema: z.object({ userId: z.string() }),
|
|
annotations: { readOnlyHint: true },
|
|
_meta: {
|
|
"openai/outputTemplate": "ui://view/cart.html",
|
|
"openai/toolInvocation/invoking": "Loading cart...",
|
|
"openai/toolInvocation/invoked": "Cart ready",
|
|
"openai/widgetAccessible": true,
|
|
},
|
|
},
|
|
async (args) => {
|
|
const cart = await getCart(args.userId);
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(cart) }],
|
|
structuredContent: { cart },
|
|
};
|
|
},
|
|
);
|
|
|
|
// Register UI resource
|
|
server.registerResource(
|
|
"Cart View",
|
|
"ui://view/cart.html",
|
|
{ mimeType: "text/html+skybridge" },
|
|
async () => ({
|
|
contents: [
|
|
{
|
|
uri: "ui://view/cart.html",
|
|
mimeType: "text/html+skybridge",
|
|
text: getCartHtml(),
|
|
_meta: {
|
|
"openai/widgetCSP": {
|
|
resource_domains: ["https://cdn.example.com"],
|
|
connect_domains: ["https://api.example.com"],
|
|
frame_domains: ["https://embed.example.com"],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
return server;
|
|
}
|
|
```
|
|
|
|
#### After (MCP Apps)
|
|
|
|
```typescript
|
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
import {
|
|
registerAppTool,
|
|
registerAppResource,
|
|
RESOURCE_MIME_TYPE,
|
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
import { z } from "zod";
|
|
|
|
function createServer() {
|
|
const server = new McpServer({ name: "shop", version: "1.0.0" });
|
|
|
|
// Register tool with MCP Apps metadata
|
|
registerAppTool(
|
|
server,
|
|
"shopping-cart",
|
|
{
|
|
title: "Shopping Cart",
|
|
description: "Display the user's shopping cart",
|
|
inputSchema: z.object({ userId: z.string() }),
|
|
annotations: { readOnlyHint: true },
|
|
_meta: { ui: { resourceUri: "ui://view/cart.html" } },
|
|
},
|
|
async (args) => {
|
|
const cart = await getCart(args.userId);
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(cart) }],
|
|
structuredContent: { cart },
|
|
};
|
|
},
|
|
);
|
|
|
|
// Register UI resource
|
|
registerAppResource(
|
|
server,
|
|
"Cart View",
|
|
"ui://view/cart.html",
|
|
{ description: "Shopping cart UI" },
|
|
async () => ({
|
|
contents: [
|
|
{
|
|
uri: "ui://view/cart.html",
|
|
mimeType: RESOURCE_MIME_TYPE,
|
|
text: getCartHtml(),
|
|
_meta: {
|
|
ui: {
|
|
csp: {
|
|
resourceDomains: ["https://cdn.example.com"],
|
|
connectDomains: ["https://api.example.com"],
|
|
frameDomains: ["https://embed.example.com"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
return server;
|
|
}
|
|
```
|
|
|
|
### Key Differences Summary
|
|
|
|
1. **Metadata Structure**: OpenAI uses flat `_meta["openai/..."]` properties; MCP uses nested `_meta.ui.*` structure
|
|
2. **Tool Visibility**: OpenAI uses boolean/string (`true`/`"public"`); MCP uses string arrays (`["app", "model"]`)
|
|
3. **CSP Field Names**: snake_case → camelCase (e.g., `connect_domains` → `connectDomains`)
|
|
4. **App Permissions**: MCP adds `_meta.ui.permissions` for camera, microphone, geolocation, clipboard (not in OpenAI)
|
|
5. **Resource MIME Type**: `text/html+skybridge` → `text/html;profile=mcp-app` (use `RESOURCE_MIME_TYPE` constant)
|
|
6. **Helper Functions**: MCP provides `registerAppTool()` and `registerAppResource()` helpers
|
|
7. **Not Yet Implemented**: `_meta["openai/toolInvocation/invoking"]`, `_meta["openai/toolInvocation/invoked"]`, and `_meta["openai/widgetDescription"]` don't have MCP equivalents yet
|
|
|
|
## Client-Side
|
|
|
|
Client-side migration involves replacing the implicit `window.openai` global with an explicit `App` instance.
|
|
|
|
### Quick Start Comparison
|
|
|
|
| OpenAI Apps SDK | MCP Apps SDK |
|
|
| --------------------------------- | ---------------------------------- |
|
|
| Implicit global (`window.openai`) | Explicit instance (`new App(...)`) |
|
|
| Properties pre-populated on load | Async connection + notifications |
|
|
| Sync property access | Getters + event handlers |
|
|
|
|
### Setup & Connection
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| -------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
| `window.openai` (auto-available) | `const app = new App({name, version})` | MCP requires explicit instantiation |
|
|
| (implicit) | Vanilla: `await app.connect()` / React: `useApp()` | MCP requires async connection; auto-detects OpenAI env |
|
|
| — | `await app.connect(new OpenAITransport())` | Force OpenAI mode (not yet available, see [PR #172](https://github.com/modelcontextprotocol/ext-apps/pull/172)) |
|
|
| — | `await app.connect(new PostMessageTransport(...))` | Force MCP mode explicitly |
|
|
|
|
### Host Context Properties
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| --------------------------- | --------------------------------------------- | --------------------------------------- |
|
|
| `window.openai.theme` | `app.getHostContext()?.theme` | `"light"` \| `"dark"` |
|
|
| `window.openai.locale` | `app.getHostContext()?.locale` | BCP 47 language tag (e.g., `"en-US"`) |
|
|
| `window.openai.displayMode` | `app.getHostContext()?.displayMode` | `"inline"` \| `"pip"` \| `"fullscreen"` |
|
|
| `window.openai.maxHeight` | `app.getHostContext()?.viewport?.maxHeight` | Max container height in px |
|
|
| `window.openai.safeArea` | `app.getHostContext()?.safeAreaInsets` | `{ top, right, bottom, left }` |
|
|
| `window.openai.userAgent` | `app.getHostContext()?.userAgent` | Host user agent string |
|
|
| — | `app.getHostContext()?.availableDisplayModes` | MCP adds: which modes host supports |
|
|
| — | `app.getHostContext()?.toolInfo` | MCP adds: tool metadata during call |
|
|
|
|
### Tool Data (Input/Output)
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------------------------ | ------------------------------------------------------------- | ----------------------------------- |
|
|
| `window.openai.toolInput` | `app.ontoolinput = (params) => { params.arguments }` | Tool arguments; MCP uses callback |
|
|
| `window.openai.toolOutput` | `app.ontoolresult = (params) => { params.structuredContent }` | Tool result; MCP uses callback |
|
|
| `window.openai.toolResponseMetadata` | `app.ontoolresult` → `params._meta` | Widget-only metadata from server |
|
|
| — | `app.ontoolinputpartial = (params) => {...}` | MCP adds: streaming partial args |
|
|
| — | `app.ontoolcancelled = (params) => {...}` | MCP adds: cancellation notification |
|
|
|
|
### Calling Tools
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------------------------------ | ----------------------------------------------------- | ---------------------------- |
|
|
| `await window.openai.callTool(name, args)` | `await app.callServerTool({ name, arguments: args })` | Call another MCP server tool |
|
|
|
|
### Sending Messages
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ----------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------- |
|
|
| `await window.openai.sendFollowUpMessage({ prompt })` | `await app.sendMessage({ role: "user", content: [{ type: "text", text: prompt }] })` | MCP uses structured content array |
|
|
|
|
### External Links
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| -------------------------------------------- | ----------------------------------- | ------------------------------------ |
|
|
| `await window.openai.openExternal({ href })` | `await app.openLink({ url: href })` | Different param name: `href` → `url` |
|
|
|
|
### Display Mode
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| -------------------------------------------------- | --------------------------------------------------------- | ----------------------------------- |
|
|
| `await window.openai.requestDisplayMode({ mode })` | `await app.requestDisplayMode({ mode })` | Same API |
|
|
| — | Check `app.getHostContext()?.availableDisplayModes` first | MCP lets you check what's available |
|
|
|
|
### Size Reporting
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| --------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------- |
|
|
| `window.openai.notifyIntrinsicHeight(height)` | `app.sendSizeChanged({ width, height })` | MCP includes width |
|
|
| Manual only | `new App(appInfo, capabilities, { autoResize: true /* default */ })` | MCP auto-reports via `ResizeObserver` |
|
|
|
|
### State Persistence
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------------------------- | -------- | -------------------------------------------------------------------- |
|
|
| `window.openai.widgetState` | — | Not directly available in MCP |
|
|
| `window.openai.setWidgetState(state)` | — | Use alternative mechanisms (`localStorage`, server-side state, etc.) |
|
|
|
|
### File Operations (Not Yet in MCP Apps)
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ---------------------------------------------------- | -------- | ------------------- |
|
|
| `await window.openai.uploadFile(file)` | — | Not yet implemented |
|
|
| `await window.openai.getFileDownloadUrl({ fileId })` | — | Not yet implemented |
|
|
|
|
### Other (Not Yet in MCP Apps)
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------------------------------- | -------- | ------------------- |
|
|
| `await window.openai.requestModal(options)` | — | Not yet implemented |
|
|
| `window.openai.requestClose()` | — | Not yet implemented |
|
|
| `window.openai.setOpenInAppUrl({ href })` | — | Not yet implemented |
|
|
| `window.openai.view` | — | Not yet mapped |
|
|
|
|
### Event Handling
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| --------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------ |
|
|
| `window.openai.toolInput` (read on load) | `app.ontoolinput = (params) => {...}` | OpenAI: sync property; MCP: callback (register before `connect()`) |
|
|
| `window.openai.toolOutput` (read on load) | `app.ontoolresult = (params) => {...}` | OpenAI: sync property; MCP: callback (register before `connect()`) |
|
|
| `addEventListener("openai:set_globals", ...)` | `app.onhostcontextchanged = (ctx) => {...}` | Both push updates; MCP uses callback, OpenAI uses DOM event |
|
|
| — | `app.onteardown = async () => {...}` | MCP adds: cleanup before unmount |
|
|
|
|
### Logging
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------------------ | --------------------------------------------- | ------------------------------- |
|
|
| `console.log(...)` | `app.sendLog({ level: "info", data: "..." })` | MCP provides structured logging |
|
|
|
|
### Host Info
|
|
|
|
| OpenAI | MCP Apps | Notes |
|
|
| ------ | --------------------------- | ------------------------------------------------- |
|
|
| — | `app.getHostVersion()` | Returns `{ name, version }` of host |
|
|
| — | `app.getHostCapabilities()` | Check `serverTools`, `openLinks`, `logging`, etc. |
|
|
|
|
### Client-Side Migration Example
|
|
|
|
#### Before (OpenAI)
|
|
|
|
```typescript
|
|
// OpenAI Apps SDK
|
|
applyTheme(window.openai.theme);
|
|
console.log("Tool args:", window.openai.toolInput);
|
|
console.log("Tool result:", window.openai.toolOutput);
|
|
|
|
// Call a tool
|
|
const result = await window.openai.callTool("get_weather", { city: "Tokyo" });
|
|
|
|
// Send a message
|
|
await window.openai.sendFollowUpMessage({ prompt: "Weather updated!" });
|
|
|
|
// Report height
|
|
window.openai.notifyIntrinsicHeight(400);
|
|
|
|
// Open link
|
|
await window.openai.openExternal({ href: "https://example.com" });
|
|
```
|
|
|
|
#### After (MCP Apps)
|
|
|
|
```typescript
|
|
import { App } from "@modelcontextprotocol/ext-apps";
|
|
|
|
const app = new App({ name: "MyApp", version: "1.0.0" });
|
|
|
|
// Register handlers BEFORE connect (events may occur immediately after connect)
|
|
app.ontoolinput = (params) => {
|
|
console.log("Tool args:", params.arguments);
|
|
};
|
|
|
|
app.ontoolresult = (params) => {
|
|
console.log("Tool result:", params.structuredContent);
|
|
};
|
|
|
|
app.onhostcontextchanged = (ctx) => {
|
|
if (ctx.theme) applyTheme(ctx.theme);
|
|
};
|
|
|
|
// Connect (auto-detects OpenAI vs MCP)
|
|
await app.connect();
|
|
|
|
// Access context
|
|
applyTheme(app.getHostContext()?.theme);
|
|
|
|
// Call a tool
|
|
const result = await app.callServerTool({
|
|
name: "get_weather",
|
|
arguments: { city: "Tokyo" },
|
|
});
|
|
|
|
// Send a message
|
|
await app.sendMessage({
|
|
role: "user",
|
|
content: [{ type: "text", text: "Weather updated!" }],
|
|
});
|
|
|
|
// Open link (note: url not href)
|
|
await app.openLink({ url: "https://example.com" });
|
|
```
|
|
|
|
### Key Differences Summary
|
|
|
|
1. **Initialization**: OpenAI is implicit; MCP requires `new App()` + `await app.connect()`
|
|
2. **Data Flow**: OpenAI pre-populates; MCP uses async notifications (register handlers before `connect()`)
|
|
3. **Auto-resize**: MCP has built-in ResizeObserver support via `autoResize` option
|
|
4. **Structured Content**: MCP uses `{ type: "text", text: "..." }` arrays for messages
|
|
5. **Context Changes**: MCP pushes updates via `onhostcontextchanged`; no polling needed
|
|
6. **Capabilities**: MCP lets you check what the host supports before calling methods
|