ext-apps/docs/migrate_from_openai_apps.md
Den Delimarsky 76f2de7b19
Add Security documentation section and SEO improvements (#524)
* Add Security docs section, SEO improvements, and doc site enhancements

- Add Authorization guide covering per-server and per-tool OAuth patterns
- Add CSP & CORS guide extracted from Patterns doc
- Group both under a new Security section in the documentation nav
- Replace CSP & CORS section in Patterns with cross-reference to new guide
- Rename doc site title from package name to 'MCP Apps'
- Rename 'Modules' nav section to 'API Documentation'
- Ensure Security section appears before API Documentation in TOC
- Add SEO plugin with JSON-LD structured data (TechArticle/WebPage)
- Add per-page meta descriptions from frontmatter
- Add description frontmatter to all documentation pages
- Add hostedBaseUrl for sitemap.xml generation and canonical URLs
- Normalize document page slugs to lowercase hyphenated format
- Update compressed navigation and search index data to match new slugs

* Add favicons with light/dark mode support

- Download favicons from modelcontextprotocol.io (light + dark variants)
- Inject favicon link tags into every page with correct relative paths
- Support prefers-color-scheme media queries for light/dark mode
- Include apple-touch-icon for iOS
- Copy favicon assets to output directory during doc build

* Fix HTML sanitization in SEO plugin with proper parser

Replace regex-based HTML tag stripping with htmlparser2 for
extracting plain-text descriptions from rendered pages.

* Add custom theme to align with modelcontextprotocol.io styling

- Import Inter and JetBrains Mono fonts (matching MCP site)
- Override light mode colors: pure white backgrounds, softer gray borders
- Override dark mode colors: match MCP site dark background (#0f1117)
- Taller header (3.5rem) with backdrop-blur frosted glass effect
- Sidebar: adjusted spacing, font size, and hover/active states
- Content: increased line-height (1.6), more heading breathing room
- Code blocks: JetBrains Mono, subtle border, more padding
- Fix CSS load order: custom.css now loads after theme stylesheets

* Align sidebar nav styling with modelcontextprotocol.io

- Active item: tinted blue background with blue text (matching MCP's
  primary-color tinted active state)
- Non-active items: muted gray text, subtle hover background
- Larger border-radius (0.75rem/rounded-xl) matching MCP sidebar
- Group headers: semibold with slight letter spacing
- Proper dark mode variants for all states

* Fix sidebar active item: dark gray background with bold text

* Make sidebar active item more prominent: bolder weight, stronger background

* Highlight current page in sidebar nav, normalize code font size

Inject a small script via the SEO plugin that marks the active sidebar
link with a "current" class (TypeDoc doesn't do this natively for
document pages). Uses a MutationObserver since the sidebar is populated
asynchronously. Pathname comparison strips .html extensions to support
clean-URL servers.

Also bumps code/pre font-size from 0.85em to 1em so code snippets match
the surrounding body text size.

* Split styling into dedicated plugin, fix auth terminology, improve breadcrumbs

Extract CSS cascade fix, breadcrumb labels, and sidebar nav highlighting
from the SEO plugin into a new typedoc-plugin-mcpstyle.mjs to keep
concerns separated.

Fix authorization doc: use "authorization" consistently instead of mixing
with "authentication", link to MCP spec and RFC 9728 for Protected
Resource Metadata, remove redundant spec link paragraph.

Rename "Documents" nav section to "Getting Started" and add explicit
group frontmatter to all doc pages. Breadcrumbs now show the section
name instead of duplicating the page title.
2026-03-06 13:45:15 +00:00

20 KiB

title group description
Migrate OpenAI App Getting Started 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 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://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_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)
— 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.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)

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