Skip to content

Conversation

@ochafik
Copy link
Contributor

@ochafik ochafik commented Dec 16, 2025

Summary

Enable MCP Apps to run transparently in ChatGPT by bridging the MCP Apps protocol to OpenAI's window.openai API.

Client-side: Same app code works in both environments:

const app = new App({ name: "MyApp", version: "1.0.0" }, {});
await app.connect(); // Auto-detects: OpenAI or MCP

Server-side: Single registration serves both platforms:

import { registerAppTool, registerAppResource } from '@modelcontextprotocol/ext-apps/server';

// Automatically adds OpenAI metadata (openai/outputTemplate)
registerAppTool(server, "weather", {
  _meta: { ui: { resourceUri: "ui://weather/widget.html" } }
}, handler);

// Registers both MCP and OpenAI (+skybridge) resource variants
registerAppResource(server, "Weather", "ui://weather/widget.html", config, readHtml);

Motivation

MCP Apps and OpenAI Apps SDK solve similar problems with different protocols. Rather than forcing developers to maintain two codebases, this PR allows a single MCP App to run in both:

  • MCP-compatible hosts (Claude, etc.) via PostMessageTransport
  • ChatGPT via the new OpenAITransport

This aligns with broader cross-platform goals discussed in #169 (A2UI interoperability) and #34 (iframe-embed reuse pattern).

Design Principles

  1. Bridge, don't extend: Only maps existing MCP Apps protocol messages to OpenAI equivalents. No new protocol additions.
  2. Auto-detection: experimentalOAICompatibility (default: true) enables transparent platform detection.
  3. Graceful degradation: Reports capabilities dynamically based on what window.openai actually supports.

Protocol Mapping

Fully Bridged (App → Host)

MCP Apps Protocol OpenAI Apps SDK Notes
ui/initialize Properties synthesis Theme, locale, displayMode, viewport, safeArea
tools/call callTool() Full support
ui/message sendFollowUpMessage() Text content extracted
ui/open-link openExternal() URL mapping
ui/request-display-mode requestDisplayMode() Mode passthrough
ui/notifications/size-changed notifyIntrinsicHeight() Height only (OpenAI limitation)

Fully Bridged (Host → App)

OpenAI Apps SDK MCP Apps Protocol Notes
toolInput ui/notifications/tool-input Delivered after connect
toolOutput ui/notifications/tool-result Delivered after connect
toolResponseMetadata _meta in tool-result Using existing spec field

Capability Detection

Capabilities are reported dynamically based on actual window.openai availability:

Capability Detection Reported In
serverTools !!openai.callTool hostCapabilities
openLinks !!openai.openExternal hostCapabilities
logging Always (→ console.log) hostCapabilities
availableDisplayModes !!openai.requestDisplayMode hostContext

This addresses part of #41 (display mode negotiation) by accurately reporting available modes.

Server-Side Dual Registration

The server helpers automatically handle platform differences:

What MCP Apps OpenAI Apps SDK
Tool metadata _meta.ui.resourceUri _meta["openai/outputTemplate"] (auto-added)
Resource URI ui://weather/widget.html ui://weather/widget.html+skybridge
Resource MIME text/html;profile=mcp-app text/html+skybridge

When you call registerAppResource(), it registers two resources:

  1. MCP resource at the base URI
  2. OpenAI resource at URI + +skybridge suffix

Custom MIME types in the callback are preserved; only the default MCP MIME type is converted to skybridge.

Not Bridged (Would Require Spec Additions)

OpenAI Feature Gap Related Issue/PR
widgetState / setWidgetState State persistence not in MCP spec #62, #125
view property Navigation context not in MCP spec #147
requestClose() Close request not in MCP spec
uploadFile() / getFileDownloadUrl() File ops not in MCP spec
requestModal() Modal API not in MCP spec

Test Coverage

  • 76 tests total
  • Environment detection (isOpenAIEnvironment)
  • Transport construction and lifecycle
  • All protocol message mappings
  • Dynamic capability detection
  • Initial state delivery (toolInput, toolOutput, _meta)
  • Server helpers: dual registration, OpenAI metadata, MIME type preservation

Breaking Changes

None. The experimentalOAICompatibility option defaults to true but falls back gracefully when window.openai is not present.


Related Issues: #169, #41, #62, #34, #147
Related PRs: #125

🤖 Generated with Claude Code

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 16, 2025

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/ext-apps@172

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-basic-react@172

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-basic-vanillajs@172

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-budget-allocator@172

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-cohort-heatmap@172

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-customer-segmentation@172

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-map@172

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-pdf@172

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-scenario-modeler@172

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-shadertoy@172

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-sheet-music@172

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-system-monitor@172

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-threejs@172

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-transcript@172

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-video-resource@172

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-wiki-explorer@172

commit: e0a587c

ochafik and others added 9 commits January 8, 2026 10:54
Add transparent support for OpenAI's Apps SDK environment alongside MCP.

- `transport.ts` - OpenAITransport implementing MCP Transport interface
- `types.ts` - TypeScript types for OpenAI Apps SDK (`window.openai`)
- `transport.test.ts` - Comprehensive tests

- Add `experimentalOAICompatibility` option (default: `true`)
- Auto-detect platform: check for `window.openai` → use OpenAI, else MCP
- `connect()` creates appropriate transport automatically

- Add `experimentalOAICompatibility` prop to `UseAppOptions`
- Pass through to App constructor

Apps work transparently in both environments:

```typescript
// Works in both MCP hosts and ChatGPT
const app = new App(appInfo, capabilities);
await app.connect(); // Auto-detects platform

// Force MCP-only mode
const app = new App(appInfo, capabilities, {
  experimentalOAICompatibility: false
});
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Dynamic capability detection based on window.openai availability
- Report availableDisplayModes when requestDisplayMode is available
- Include toolResponseMetadata as _meta in tool-result notification
- registerAppTool adds openai/outputTemplate metadata automatically
- registerAppResource registers both MCP and OpenAI (+skybridge) variants
- Preserve custom MIME types in OpenAI resource callback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The learn_threejs tool was added in #173, which adds a second option
in the Tool dropdown. This updates the golden snapshot to match.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Change _meta: null to _meta: undefined in OpenAI transport
- Register default no-op handlers for all tool notifications in App constructor

The SDK's Protocol class throws 'Unknown message type' for unhandled
notifications. Now all tool-related notifications have default handlers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Test that null _meta is converted to undefined in OpenAI transport
- Test that default no-op handlers accept tool notifications without error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Check for both null and undefined before delivering tool-result
notification. Previously null passed through and was stringified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Handle different shapes of toolOutput from ChatGPT:
- Array of content blocks: use directly
- Single content block {type, text}: wrap in array
- Object with just {text}: extract and wrap
- Other: stringify as fallback

This prevents double-stringification when ChatGPT passes content
in different formats.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When toolOutput contains structuredContent, include it in the
tool-result notification. Also auto-extract structuredContent
from plain objects that aren't content arrays.

This allows apps to access structured data directly without
parsing JSON from text content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@ochafik ochafik force-pushed the ochafik/openai-compatibility branch from 7386688 to 35dc00a Compare January 8, 2026 11:02
ochafik and others added 14 commits January 8, 2026 16:08
The React useApp hook was overriding the entire options object when
passing experimentalOAICompatibility, causing autoResize to be
undefined instead of true. This prevented automatic size notifications
from being set up.
Add transparent support for OpenAI's Apps SDK environment alongside MCP.

- `transport.ts` - OpenAITransport implementing MCP Transport interface
- `types.ts` - TypeScript types for OpenAI Apps SDK (`window.openai`)
- `transport.test.ts` - Comprehensive tests

- Add `experimentalOAICompatibility` option (default: `true`)
- Auto-detect platform: check for `window.openai` → use OpenAI, else MCP
- `connect()` creates appropriate transport automatically

- Add `experimentalOAICompatibility` prop to `UseAppOptions`
- Pass through to App constructor

Apps work transparently in both environments:

```typescript
// Works in both MCP hosts and ChatGPT
const app = new App(appInfo, capabilities);
await app.connect(); // Auto-detects platform

// Force MCP-only mode
const app = new App(appInfo, capabilities, {
  experimentalOAICompatibility: false
});
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Dynamic capability detection based on window.openai availability
- Report availableDisplayModes when requestDisplayMode is available
- Include toolResponseMetadata as _meta in tool-result notification
- registerAppTool adds openai/outputTemplate metadata automatically
- registerAppResource registers both MCP and OpenAI (+skybridge) variants
- Preserve custom MIME types in OpenAI resource callback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The learn_threejs tool was added in #173, which adds a second option
in the Tool dropdown. This updates the golden snapshot to match.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Change _meta: null to _meta: undefined in OpenAI transport
- Register default no-op handlers for all tool notifications in App constructor

The SDK's Protocol class throws 'Unknown message type' for unhandled
notifications. Now all tool-related notifications have default handlers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Test that null _meta is converted to undefined in OpenAI transport
- Test that default no-op handlers accept tool notifications without error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Check for both null and undefined before delivering tool-result
notification. Previously null passed through and was stringified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Handle different shapes of toolOutput from ChatGPT:
- Array of content blocks: use directly
- Single content block {type, text}: wrap in array
- Object with just {text}: extract and wrap
- Other: stringify as fallback

This prevents double-stringification when ChatGPT passes content
in different formats.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When toolOutput contains structuredContent, include it in the
tool-result notification. Also auto-extract structuredContent
from plain objects that aren't content arrays.

This allows apps to access structured data directly without
parsing JSON from text content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The React useApp hook was overriding the entire options object when
passing experimentalOAICompatibility, causing autoResize to be
undefined instead of true. This prevented automatic size notifications
from being set up.
- Add StructuredWidgetState type with modelContent/privateContent/imageIds
- Add onwidgetstate handler for receiving persisted state on load
- Add updateModelContext method to persist state and update model context
- Add uploadFile and getFileDownloadUrl methods for file handling
- Handle image content blocks in sendMessage by uploading and adding to context
- Wire all new functionality in OpenAITransport
- Add comprehensive tests for all new features
- Update migration doc with new feature mappings

This enables MCP Apps to:
- Persist UI state across widget renders
- Provide context visible to ChatGPT for follow-up turns
- Keep private UI state hidden from the model
- Upload and reference images in model context
- Wire ui/update-model-context request to setWidgetState in OpenAI transport
- Translate MCP content/structuredContent to OpenAI's modelContent format
- Update tests to use request-based API instead of notification
- Keep widget state notification for hydration (onwidgetstate)
ochafik added a commit that referenced this pull request Jan 13, 2026
- Remove AGENTS.md reference to migration guide
- Add note that OpenAITransport is not yet available (PR #172)
- Remove unnecessary note about return type ordering
ochafik added a commit that referenced this pull request Jan 13, 2026
* Create migrate_from_openai_apps.md

* Update AGENTS.md

* Update docs/migrate_from_openai_apps.md

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>

* address PR review comments

- Remove AGENTS.md reference to migration guide
- Add note that OpenAITransport is not yet available (PR #172)
- Remove unnecessary note about return type ordering

---------

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
- Add event listener for 'openai:set_globals' in OpenAITransport.start()
  to forward runtime property changes (theme, displayMode, safeArea,
  maxHeight, locale, userAgent) as ui/notifications/host-context-changed

- Clean up event listener in OpenAITransport.close()

- Add unit tests for the new event forwarding functionality

- Update map-server to handle safe area insets:
  - Adjust fullscreen button position based on insets
  - Keep map full-bleed while ensuring controls aren't obscured
  - Listen for runtime inset changes via onhostcontextchanged
Add a debug-server example that exercises all MCP Apps SDK capabilities:

Server (server.ts):
- debug-tool: Configurable tool testing all content types (text, image,
  audio, resource, resourceLink, mixed), with options for multiple blocks,
  structuredContent, _meta, error simulation, and delays
- debug-refresh: App-only tool (hidden from model) for polling server state

Guest UI (src/mcp-app.ts):
- Event Log: Real-time log of all SDK events with filtering and timestamps
- Host Info: Display of context, capabilities, container dimensions, styles
- Callback Status: Table showing all registered callbacks with call counts
- Action Buttons: Test every SDK method:
  - Messages (text and image)
  - Logging (debug/info/warning/error)
  - Model context updates (text and structured)
  - Display mode requests (inline/fullscreen/pip)
  - Link opening
  - Manual/auto resize controls
  - Server tool calls with full configuration
  - File upload and URL retrieval

This example serves as both a testing tool and reference implementation
for all SDK features.
- Add --log-file argument (default: /tmp/mcp-apps-debug-server.log)
- Add debug-log app-private tool for app to send logs to file
- App now logs all events to console AND server log file
- Wrap server log calls in try/catch to prevent failures from breaking app
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants