Migrating from OpenAI Apps SDK to MCP Apps SDK

March 6, 2026 · View on GitHub

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 SDKMCP 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+skybridgeUI Resource MIME type: text/html;profile=mcp-app

Tool Metadata

OpenAIMCP AppsNotes
_meta["openai/outputTemplate"]_meta.ui.resourceUriURI 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

OpenAIMCP AppsNotes
_meta["openai/widgetCSP"]_meta.ui.cspSee CSP field mapping below
_meta.ui.permissionsMCP adds: permissions for camera, microphone, geolocation, clipboard
_meta["openai/widgetDomain"]_meta.ui.domainDedicated sandbox origin
_meta["openai/widgetPrefersBorder"]_meta.ui.prefersBorderVisual boundary preference
_meta["openai/widgetDescription"]Not yet implemented; use app.updateModelContext() for dynamic context

Resource MIME Type

OpenAIMCP AppsNotes
text/html+skybridgetext/html;profile=mcp-appAuto-set by registerAppResource(); use RESOURCE_MIME_TYPE constant if manual

CSP Field Mapping

OpenAIMCP AppsNotes
resource_domainsresourceDomainsOrigins for static assets (images, fonts, styles, scripts)
connect_domainsconnectDomainsOrigins for fetch/XHR/WebSocket requests
frame_domainsframeDomainsOrigins for nested iframes
redirect_domainsOpenAI-only: origins for openExternal redirects
baseUriDomainsMCP-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://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)

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://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_domainsconnectDomains)
  4. App Permissions: MCP adds _meta.ui.permissions for camera, microphone, geolocation, clipboard (not in OpenAI)
  5. Resource MIME Type: text/html+skybridgetext/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 SDKMCP Apps SDK
Implicit global (window.openai)Explicit instance (new App(...))
Properties pre-populated on loadAsync connection + notifications
Sync property accessGetters + event handlers

Setup & Connection

OpenAIMCP AppsNotes
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

OpenAIMCP AppsNotes
window.openai.themeapp.getHostContext()?.theme"light" | "dark"
window.openai.localeapp.getHostContext()?.localeBCP 47 language tag (e.g., "en-US")
window.openai.displayModeapp.getHostContext()?.displayMode"inline" | "pip" | "fullscreen"
window.openai.maxHeightapp.getHostContext()?.viewport?.maxHeightMax container height in px
window.openai.safeAreaapp.getHostContext()?.safeAreaInsets{ top, right, bottom, left }
window.openai.userAgentapp.getHostContext()?.userAgentHost user agent string
app.getHostContext()?.availableDisplayModesMCP adds: which modes host supports
app.getHostContext()?.toolInfoMCP adds: tool metadata during call

Tool Data (Input/Output)

OpenAIMCP AppsNotes
window.openai.toolInputapp.ontoolinput = (params) => { params.arguments }Tool arguments; MCP uses callback
window.openai.toolOutputapp.ontoolresult = (params) => { params.structuredContent }Tool result; MCP uses callback
window.openai.toolResponseMetadataapp.ontoolresultparams._metaWidget-only metadata from server
app.ontoolinputpartial = (params) => {...}MCP adds: streaming partial args
app.ontoolcancelled = (params) => {...}MCP adds: cancellation notification

Calling Tools

OpenAIMCP AppsNotes
await window.openai.callTool(name, args)await app.callServerTool({ name, arguments: args })Call another MCP server tool

Sending Messages

OpenAIMCP AppsNotes
await window.openai.sendFollowUpMessage({ prompt })await app.sendMessage({ role: "user", content: [{ type: "text", text: prompt }] })MCP uses structured content array
OpenAIMCP AppsNotes
await window.openai.openExternal({ href })await app.openLink({ url: href })Different param name: hrefurl

Display Mode

OpenAIMCP AppsNotes
await window.openai.requestDisplayMode({ mode })await app.requestDisplayMode({ mode })Same API
Check app.getHostContext()?.availableDisplayModes firstMCP lets you check what's available

Size Reporting

OpenAIMCP AppsNotes
window.openai.notifyIntrinsicHeight(height)app.sendSizeChanged({ width, height })MCP includes width
Manual onlynew App(appInfo, capabilities, { autoResize: true /* default */ })MCP auto-reports via ResizeObserver

State Persistence

OpenAIMCP AppsNotes
window.openai.widgetStateNot directly available in MCP
window.openai.setWidgetState(state)Use alternative mechanisms (localStorage, server-side state, etc.)

File Operations (Not Yet in MCP Apps)

OpenAIMCP AppsNotes
await window.openai.uploadFile(file)Not yet implemented
await window.openai.getFileDownloadUrl({ fileId })Not yet implemented

Other (Not Yet in MCP Apps)

OpenAIMCP AppsNotes
await window.openai.requestModal(options)Not yet implemented
window.openai.requestClose()Not yet implemented
window.openai.setOpenInAppUrl({ href })Not yet implemented
window.openai.viewNot yet mapped

Event Handling

OpenAIMCP AppsNotes
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

OpenAIMCP AppsNotes
console.log(...)app.sendLog({ level: "info", data: "..." })MCP provides structured logging

Host Info

OpenAIMCP AppsNotes
app.getHostVersion()Returns { name, version } of host
app.getHostCapabilities()Check serverTools, openLinks, logging, etc.

Client-Side 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

  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