Migrating from OpenAI Apps SDK to MCP Apps SDK
This guide helps you migrate from the OpenAI Apps SDK to the MCP Apps SDK (@modelcontextprotocol/ext-apps).
Server-Side
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 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)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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: { userId: z.string() },
annotations: { readOnlyHint: true },
_meta: {
"openai/outputTemplate": "ui://widget/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 Widget",
"ui://widget/cart.html",
{ mimeType: "text/html+skybridge" },
async () => ({
contents: [
{
uri: "ui://widget/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)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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: { userId: z.string() },
annotations: { readOnlyHint: true },
_meta: { ui: { resourceUri: "ui://widget/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 Widget",
"ui://widget/cart.html",
{ description: "Shopping cart UI" },
async () => ({
contents: [
{
uri: "ui://widget/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
- Metadata Structure: OpenAI uses flat
_meta["openai/..."] properties; MCP uses nested _meta.ui.* structure
- Tool Visibility: OpenAI uses boolean/string (
true/"public"); MCP uses string arrays (["app", "model"])
- CSP Field Names: snake_case → camelCase (e.g.,
connect_domains → connectDomains)
- App Permissions: MCP adds
_meta.ui.permissions for camera, microphone, geolocation, clipboard (not in OpenAI)
- Resource MIME Type:
text/html+skybridge → text/html;profile=mcp-app (use RESOURCE_MIME_TYPE constant)
- Helper Functions: MCP provides
registerAppTool() and registerAppResource() helpers
- Not Yet Implemented:
_meta["openai/toolInvocation/invoking"], _meta["openai/toolInvocation/invoked"], and _meta["openai/widgetDescription"] don't have MCP equivalents yet
Client-side
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) |
| — |
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.view |
— |
Not yet mapped |
Event Handling
| OpenAI |
MCP Apps |
Notes |
Read window.openai.* on load |
app.ontoolinput = (params) => {...} |
Register before connect() |
Read window.openai.* on load |
app.ontoolresult = (params) => {...} |
Register before connect() |
| Poll or re-read properties |
app.onhostcontextchanged = (ctx) => {...} |
MCP pushes context changes |
| — |
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. |
Full Migration Example
Before (OpenAI)
// 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)
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
- Initialization: OpenAI is implicit; MCP requires
new App() + await app.connect()
- Data Flow: OpenAI pre-populates; MCP uses async notifications (register handlers before
connect())
- Auto-resize: MCP has built-in ResizeObserver support via
autoResize option
- Structured Content: MCP uses
{ type: "text", text: "..." } arrays for messages
- Context Changes: MCP pushes updates via
onhostcontextchanged; no polling needed
- Capabilities: MCP lets you check what the host supports before calling methods