ext-apps/docs/migrate_from_openai_apps.md
Jonathan Hefner b231396d2b Add "Server-Side" section to OpenAI migration guide
Add "Server-Side" migration documentation covering:
- Tool metadata mapping (`_meta["openai/..."]` → `_meta.ui.*`)
- Resource metadata (CSP, permissions, domain, border preferences)
- Resource MIME type (`text/html+skybridge` → `text/html;profile=mcp-app`)
- Before/after code examples using `registerAppTool()` and
  `registerAppResource()`
- Key differences summary including not-yet-implemented features

Reorganize existing client-side content under updated heading structure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:18:05 -06:00

18 KiB

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 connect_domains → connectDomains, resource_domains → resourceDomains, etc.
— _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

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(),
        },
      ],
    }),
  );

  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(),
        },
      ],
    }),
  );

  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 Property Names: snake_case → camelCase (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

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

  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