Skip to main content

OpenAI SDK Architecture

This guide explains how MCPJam Inspector implements the OpenAI Apps SDK to render custom UI components for MCP tool results. This enables MCP server developers to create interactive visualizations for their tool outputs.
MCPJam Inspector also supports MCP-UI for simpler component-based UIs. See the Playground Architecture docs for MCP-UI implementation details.

Overview

MCPJam Inspector supports the OpenAI Apps SDK, letting MCP tools return custom UI components that render in iframes with a sandboxed window.openai API bridge.
This document covers the V1 Playground implementation. As of PR #773, Playground V2 (ChatTabV2.tsx) also supports OpenAI Apps with a streamlined implementation using chatgpt-app-renderer.tsx. The V2 implementation uses MCP resources API to fetch widget templates and renders them with similar window.openai bridge capabilities.

Key Features

  • Custom UI Rendering: Display tool results using custom HTML/React components
  • Interactive Widgets: Components can call other MCP tools and send followup messages
  • State Persistence: Widget state persists across sessions via localStorage
  • Theme Synchronization: Widgets automatically receive theme updates (light/dark mode)
  • Secure Isolation: Components run in sandboxed iframes with CSP headers
  • Server-Side Storage: Widget context stored server-side with 1-hour TTL for iframe access
  • Modal Support: Widgets can open modal dialogs with separate view contexts
  • Dual Mode Support:
    • ui:// URIs for server-provided HTML content
    • External URLs for remotely hosted components

OpenAI Apps SDK vs MCP-UI

MCPJam Inspector supports two approaches for custom UI rendering: When to use OpenAI Apps SDK:
  • Need full JavaScript framework support (React, Vue, etc.)
  • Require persistent state across sessions
  • Building complex interactive visualizations
  • Need access to external APIs and libraries
When to use MCP-UI:
  • Simple interactive components (buttons, cards)
  • Prefer open standards over proprietary APIs
  • Don’t need state persistence
  • Want faster rendering without iframe overhead
See Playground Architecture - MCP-UI Integration for MCP-UI implementation details.

Architecture Overview

MCPJam Inspector implements a triple-iframe architecture that matches ChatGPT’s implementation for compatibility and security isolation:

Triple-Iframe Architecture

The triple-iframe design provides cross-origin isolation:
  1. Outer Iframe (about:blank) - Container with popup permissions
  2. Middle Iframe (cross-origin sandbox proxy) - Uses localhost ↔ 127.0.0.1 origin swap for isolation
  3. Inner Iframe (widget content via src URL) - Actual widget with proper URL context
Why Three Iframes?
  • Cross-origin isolation: Middle iframe runs on different origin (localhost vs 127.0.0.1)
  • URL context preservation: Widget loads via src (not srcdoc) for Next.js/React Router compatibility
  • Security boundaries: Each layer enforces sandbox restrictions
  • ChatGPT parity: Matches OpenAI’s production implementation exactly

Component Flow

1. Tool Execution & Detection

When a tool is executed that returns OpenAI SDK components or MCP Apps, the system detects this in multiple ways: Method A: _meta["openai/outputTemplate"] field (ChatGPT Apps)
Method B: _meta.ui.resourceUri field (MCP Apps)
Method C: ui:// resource in response

ResultsPanel Detection Logic

Located in client/src/components/tools/ResultsPanel.tsx:
The system detects custom UI components by checking for:
  1. openai/outputTemplate metadata (ChatGPT Apps)
  2. ui.resourceUri metadata (MCP Apps)
  3. ui:// URIs in tool results via resolveUIResource
The resolveUIResource function searches for ui:// URIs in:
  1. Direct resource field at root level
  2. content array items with type: "resource"

2. Widget Data Storage Flow

Before rendering, widget data must be stored server-side for iframe access: Why Store Server-Side?
  • Iframes need access to toolInput and toolOutput for window.openai API
  • Client localStorage can’t be shared across iframe sandbox boundaries
  • Server becomes the source of truth for widget initialization data

Storage Implementation

Located in server/routes/mcp/chatgpt.ts:14-51:

3. Triple-Iframe Widget Loading

The system uses a triple-iframe architecture with cross-origin isolation: Why Triple-Iframe Architecture?
  • Cross-origin isolation: Middle iframe uses localhost ↔ 127.0.0.1 origin swap for true security boundary
  • URL context preservation: Widget loads via src (not srcdoc) for Next.js/React Router compatibility
  • Message relay: Each layer forwards postMessage events up/down the chain
  • ChatGPT parity: Matches OpenAI’s production implementation exactly
  • Sandbox enforcement: Each iframe layer enforces its own sandbox restrictions

Sandbox Proxy Implementation

The middle iframe serves as a cross-origin relay between the host and widget. Located in server/routes/mcp/chatgpt-sandbox-proxy.html:
Key Features:
  • Cross-origin isolation: Proxy runs on different origin (localhost ↔ 127.0.0.1)
  • Message relay: Bidirectional forwarding between host and widget
  • Sandbox validation: Only allows whitelisted sandbox tokens
  • Widget loading via src: Preserves URL context for React Router
  • Ready signal: Notifies host when proxy is initialized

Widget Content Injection

The /widget-content/:toolId endpoint performs the following:
  1. Retrieve widget data from store (includes host-controlled globals)
  2. Read HTML from MCP server via readResource(uri)
  3. Parse view mode and params from query string (view_mode, view_params)
  4. Inject window.openai API script with:
    • Host-controlled locale (BCP 47 format, e.g., ‘en-US’)
    • Host-controlled deviceType (‘mobile’, ‘tablet’, ‘desktop’)
    • Host-controlled userLocation (coarse IP-based geolocation)
    • Host-controlled maxHeight constraint (ChatGPT uses ~500px for inline)
  5. Add security headers (CSP, X-Frame-Options)
  6. Set cache control headers (no-cache for fresh content)
Host-Controlled Globals (New in PR #1001): Per the OpenAI Apps SDK spec, these values are controlled by the host (Inspector), not computed in the widget:
  • locale: Browser language from navigator.language (e.g., ‘en-US’, ‘es-ES’)
  • deviceType: Computed from viewport width (mobile: <768px, tablet: <1024px, desktop: ≥1024px)
  • userLocation: Coarse IP-based geolocation { country, region, city } or null (uses ip-api.com, no API key required)
  • maxHeight: Maximum allowed height for inline mode (ChatGPT default: ~500px)
These values are passed to the widget via window.openai and cannot be overridden by widget code.

4. window.openai API Bridge

The injected script provides the OpenAI Apps SDK API to widget code. Messages flow through the triple-iframe chain:

API Implementation

Located in server/routes/mcp/chatgpt.ts:213-376: Core API Methods:
Device Globals: The window.openai API provides device context to widgets through several properties:
  • userAgent.device.type - Device type: 'mobile', 'tablet', or 'desktop'. In the UI Playground, this is controlled by the device type selector. Outside the playground, it’s automatically detected from the browser window size.
  • userAgent.capabilities - Input capabilities:
    • hover (boolean) - Whether the device supports hover interactions
    • touch (boolean) - Whether the device supports touch input
    In the UI Playground, these are controlled by the hover and touch toggle buttons. Outside the playground, they default to { hover: true, touch: false }.
  • safeArea.insets - Safe area insets in pixels for device notches, rounded corners, and gesture areas:
    • top (number) - Top inset in pixels
    • bottom (number) - Bottom inset in pixels
    • left (number) - Left inset in pixels
    • right (number) - Right inset in pixels
    In the UI Playground, these are configured through the safe area editor with presets for common devices (iPhone notch, Dynamic Island, Android gesture navigation). Outside the playground, all insets default to 0.
Widgets can use these values to adapt their layout and interactions for different device contexts. For example, a widget might disable hover-based tooltips on touch devices or adjust padding to account for safe area insets. Security Notes:
  • API is frozen with writable: false, configurable: false
  • 30-second timeout on tool calls prevents hanging requests
  • Origin validation in parent ensures only iframe messages are processed
OAuth Challenge Handling (New in PR #1001): When a tool returns a 401 status, the system checks for OAuth challenges per RFC 7235:
The system displays user-friendly toast notifications when OAuth is required, guiding users to configure OAuth in server settings.

5. Modal Support

As of PR #931, widgets can request modal dialogs using window.openai.requestModal(). This enables widgets to display secondary views or detailed information in a separate modal context. Located in client/src/components/chat-v2/chatgpt-app-renderer.tsx:380-388:
When a widget calls window.openai.requestModal(), the parent:
  1. Extracts modal title and params from the message
  2. Opens a Dialog component
  3. Mounts a new iframe with the same widget URL
  4. Appends query params: ?view_mode=modal&view_params=<encoded_json>

View Mode Detection

Widgets receive view context via window.openai.view:

Widget State Synchronization

Modal and inline views share widget state automatically. When either view calls setWidgetState(), the state is propagated to the other view via openai:pushWidgetState messages. Located in client/src/components/chat-v2/chatgpt-app-renderer.tsx:269-284:
The receiving view updates its local state and dispatches a openai:widget_state event: Located in server/routes/mcp/chatgpt.ts:410-428:
Example Usage:

6. Display Mode Support

As of PR #927, widgets can request different display modes to optimize their presentation:
  • Inline (default) - Widget renders within the chat message flow with configurable height
  • Picture-in-Picture (PiP) - Widget floats at the top of the screen in a fixed overlay
  • Fullscreen - Widget expands to fill the entire viewport

Display Mode Implementation

The display mode system uses React state to track which widget (if any) is in PiP mode, and applies different CSS classes based on the current mode: State Management (client/src/components/chat-v2/thread.tsx):
Mode Detection (client/src/components/chat-v2/chatgpt-app-renderer.tsx:440-442):
CSS Classes (client/src/components/chat-v2/chatgpt-app-renderer.tsx:444-476):
Exit Button (client/src/components/chat-v2/chatgpt-app-renderer.tsx:481-493):
Key Features:
  • Single PiP Widget: Only one widget can be in PiP mode at a time. Requesting PiP on a different widget automatically exits the current PiP widget.
  • Automatic Inline Fallback: If a widget is in PiP mode but another widget becomes the active PiP, the first widget automatically returns to inline mode.
  • Z-Index Layering: Fullscreen widgets use z-50, PiP widgets use z-40, ensuring proper stacking order.
  • Transform Isolation: The chat container uses transform: translateZ(0) to create a new stacking context, preventing z-index conflicts.
  • Backdrop Blur: PiP widgets use backdrop blur for a modern floating effect with semi-transparent background.

Requesting Display Mode from Widgets

Widgets can request display mode changes using the window.openai.requestDisplayMode() API:
The parent component handles these requests and updates the widget’s display mode accordingly.

Fullscreen Navigation (PR #1033)

When a widget is in fullscreen mode, a navigation header is rendered at the top with back/forward buttons, widget title, and close button. This enables users to navigate multi-page widgets without leaving fullscreen mode. Architecture: The fullscreen navigation system uses host-backed navigation tracking to mirror the widget’s internal history state: Implementation Details:
  1. History Wrapping (server/routes/mcp/chatgpt.ts:352-407):
  1. Parent-Side Navigation Handler (client/src/components/chat-v2/chatgpt-app-renderer.tsx:888-896):
  1. Navigation Command Handler (server/routes/mcp/chatgpt.ts:535-551):
  1. Fullscreen Header UI (client/src/components/chat-v2/chatgpt-app-renderer.tsx:1117-1164):
Key Features:
  • Host-Backed Navigation: Parent component mirrors widget’s history state, enabling navigation controls outside the iframe
  • State Persistence: Navigation index stored in history.state.__navIndex to survive popstate events
  • Automatic Tracking: All pushState, replaceState, and popstate events automatically update navigation state
  • Disabled State Management: Back/forward buttons are disabled when navigation is not available
  • Transform Isolation Fix: Parent container’s transform: translateZ(0) is removed in fullscreen mode to prevent z-index stacking issues
Use Cases:
  • Multi-page dashboards with internal routing
  • Wizard-style forms with step navigation
  • Documentation browsers with history
  • Any widget using React Router or similar routing libraries

7. Parent-Side Message Handling

Located in client/src/components/chat-v2/chatgpt-app-renderer.tsx:312-347:

Display Mode Synchronization

The component automatically resets to inline mode if another widget takes over PiP mode: Located in client/src/components/chat-v2/chatgpt-app-renderer.tsx:368-372:
This ensures only one widget can be in PiP mode at a time, preventing overlapping floating widgets.

Global Values Synchronization

The parent component automatically sends global values to widgets, including theme, display mode, locale, and maxHeight. These values update whenever the user changes settings in the playground. Located in client/src/components/chat-v2/chatgpt-app-renderer.tsx:831-850:
Widgets can listen for global value changes using the openai:set_globals event:
Available Global Values:
  • theme - Current theme mode ("light" or "dark")
  • displayMode - Current display mode ("inline", "pip", or "fullscreen")
  • locale - BCP 47 locale code (e.g., "en-US", "es-ES", "ja-JP")
  • maxHeight - Maximum height in pixels for inline mode (optional)

Widget State Propagation to Model

As of PR #891, widget state changes are now propagated to the LLM model as hidden assistant messages. This allows the AI to understand and reason about widget interactions. Implementation (client/src/components/chat-v2/chatgpt-app-renderer.tsx:232-244):
Chat Integration (client/src/components/ChatTabV2.tsx:281-326):
UI Hiding (client/src/components/chat-v2/thread.tsx:99):
Key Features:
  • Hidden Messages: Widget state messages are prefixed with widget-state- and hidden from the UI
  • Deduplication: State changes are only propagated if the serialized state actually changed
  • Model Context: The LLM receives state updates as assistant messages, enabling it to reason about widget interactions
  • Null Handling: Setting state to null removes the state message entirely
  • Example: When a chart widget updates its selected date range, the model receives: "The state of widget tool_123 is: {\"startDate\":\"2024-01-01\",\"endDate\":\"2024-01-31\"}"
This enables use cases like:
  • LLM understanding user interactions with widgets
  • Contextual follow-up questions based on widget state
  • Multi-turn conversations that reference widget selections
  • Debugging widget behavior through model awareness

Tool Execution Bridge

Located in client/src/components/ChatTab.tsx:181-207:

Security Architecture

Content Security Policy

MCPJam Inspector implements configurable CSP enforcement for widget sandboxing with two modes: CSP Modes:
  • Permissive (default) - Allows all HTTPS resources for development and testing
  • Strict (widget-declared) - Only allows domains declared in the widget’s openai/widgetCSP metadata

Widget CSP Declaration

Widgets can declare required domains using the openai/widgetCSP metadata field in their resource:
Domain Types:
  • connect_domains - Allowed origins for fetch(), XMLHttpRequest, and WebSocket connections (maps to connect-src)
  • resource_domains - Allowed origins for scripts, styles, images, fonts, and media (maps to script-src, style-src, img-src, font-src, media-src)

CSP Header Generation

Located in server/routes/mcp/chatgpt.ts:571-701:
The CSP header is applied in the /widget-content/:toolId endpoint based on the csp_mode query parameter.

CSP Violation Reporting

Widgets automatically report CSP violations to the parent for debugging. The injected widget script includes a violation listener: Located in server/routes/mcp/chatgpt.ts:458-473:
Violations are collected in the widget debug store and displayed in the CSP debug panel with actionable suggestions for fixing CSP declarations.

CSP Debug Panel

The UI Playground includes a CSP debug tab (shield icon) that shows:
  • Suggested fix - Automatically generated openai/widgetCSP JSON snippet based on violations
  • Blocked requests - List of all CSP violations with directive, URI, and source location
  • Declared domains - Current connect_domains and resource_domains from widget metadata
  • Full CSP header - Complete CSP header string for advanced debugging
Located in client/src/components/chat-v2/csp-debug-panel.tsx.

Development vs Production

  • Development: Use permissive mode to avoid CSP issues while building widgets
  • Testing: Switch to strict mode in UI Playground to identify required domains
  • Production: Always use strict mode with properly declared openai/widgetCSP metadata
The 'unsafe-inline' and 'unsafe-eval' directives are currently required for React and other frameworks. Future versions may support stricter CSP with nonces or hashes.

Iframe Sandbox

Located in client/src/components/chat-v2/chatgpt-app-renderer.tsx:518-530:
Middle Iframe (injected into outer iframe):
Inner Iframe (created by sandbox proxy):
Sandbox Permissions:
  • allow-scripts: Enable JavaScript execution
  • allow-same-origin: Allow localStorage access (required for state)
  • allow-forms: Support form submissions
  • allow-popups: Enable external link navigation (outer iframe only)
  • allow-popups-to-escape-sandbox: Allow popup windows to load normally (outer iframe only)
Security Architecture:
  • Cross-origin isolation: Middle iframe uses different origin (localhost ↔ 127.0.0.1) for true security boundary
  • Message relay: Each layer validates and forwards messages, preventing direct access
  • CSP enforcement: Each iframe layer enforces Content Security Policy
  • Sandbox layering: Permissions are progressively restricted at each level
  • Widget trust model: Widgets are semi-trusted code with limited host access

Complete Data Flow Example

This example traces a full interaction where a widget calls a tool:

Development Guide

Testing OpenAI SDK Widgets Locally

  1. Create a test MCP server with OpenAI SDK support:
  1. Add server to MCPJam Inspector config:
  1. Test in Inspector:
    • Connect to server in Servers tab
    • Navigate to Chat tab
    • Execute: “Call the hello_widget tool with name John”
    • Widget should render with interactive buttons

Debugging Widget Issues

Common Problems:
  1. Widget doesn’t load (404)
    • Check that widgetDataStore contains toolId
    • Verify storage TTL hasn’t expired (1 hour default)
    • Confirm MCP server returns valid HTML for ui:// resource
  2. window.openai is undefined
    • Verify script injection in Stage 2 content endpoint
    • Check browser console for CSP violations
    • Ensure <head> tag exists in HTML for injection
  3. Widget data not found (404)
    • Check that widget data was successfully stored via POST /api/mcp/openai/widget/store
    • Verify toolId matches between store and load requests
    • Check server logs for storage errors
  4. Tool calls timeout
    • Check network tab for /api/mcp/tools/execute failures
    • Verify MCP server is connected and responsive
    • Increase timeout in callTool implementation (default: 30s)
  5. React Router 404 errors
    • Confirm Stage 1 executes history.replaceState('/') before loading
    • Check that widget uses BrowserRouter not HashRouter
    • Verify <base href=\"/\"> is present in HTML
  6. State doesn’t persist
    • Check localStorage in browser DevTools
    • Verify widgetStateKey format: openai-widget-state:${toolName}:${toolId}
    • Confirm setWidgetState postMessage handler is working
  7. Theme not updating
    • Check that openai:set_globals messages are being sent from parent
    • Verify widget has event listener for theme changes
    • Inspect window.openai.theme value in widget console
  8. Modal not opening
    • Verify requestModal postMessage is being sent
    • Check that Dialog component state is updating (modalOpen)
    • Inspect modal iframe URL includes view_mode=modal query param
    • Confirm window.openai.view.mode is 'modal' in modal iframe
  9. State not syncing between inline and modal
    • Check that both iframes are mounted and have valid contentWindow
    • Verify openai:pushWidgetState messages are being sent
    • Inspect localStorage for widget state key
    • Confirm both views have openai:widget_state event listener
  10. CSP violations blocking resources
    • Check the CSP debug tab (shield icon) for violation details
    • Switch to permissive mode in UI Playground to bypass CSP temporarily
    • Add blocked domains to openai/widgetCSP metadata in your widget
    • Verify CSP header in Network tab matches expected configuration
    • Use the suggested fix snippet from the CSP debug panel
Debug Tools:

Extending the Implementation

Adding New OpenAI API Methods:
  1. Update server-side injection script (server/routes/mcp/chatgpt.ts:213-376)
  2. Add postMessage handler in parent (client/src/components/chat-v2/chatgpt-app-renderer.tsx)
  3. Update TypeScript types if needed
Example: Adding a custom method:
Note on Message Flow: Remember that messages flow through the triple-iframe chain:
  • Widget → Middle Proxy → Outer Container → Host
  • Each layer forwards messages bidirectionally
  • Origin validation occurs at each boundary

Performance Considerations

Widget Data Storage

  • TTL: 1 hour default, configurable in chatgpt.ts:22
  • Cleanup: Runs every 5 minutes
  • Memory: Each widget stores ~1-10KB (toolInput + toolOutput + host globals)
  • Scale: 1000 concurrent widgets ≈ 10MB memory
  • Recommendation: For production, use Redis instead of Map

Iframe Rendering

  • Initial Load: 300-700ms (triple-iframe setup + resource fetch + cross-origin relay)
  • Tool Calls: 100-300ms (postMessage relay through 3 iframes + backend + MCP)
  • Message Latency: Additional 5-10ms per iframe layer (3 layers = 15-30ms overhead)
  • Optimization:
    • Cache MCP resource reads (currently disabled with no-cache)
    • Preload widget data before iframe creation
    • Use service workers for offline support
    • Consider WebSocket for high-frequency tool calls

postMessage Overhead

  • Latency: 5-15ms per message round-trip
  • Payload: JSON serialization for all data
  • Bottleneck: Large tool results (>1MB) slow down significantly
  • Mitigation: Use streaming or chunked responses for large data

Security Best Practices

  1. Validate postMessage Origins:
  2. Sanitize Tool Parameters:
  3. Limit Widget Capabilities:
    • Only expose necessary MCP tools to widgets
    • Implement rate limiting on tool calls
    • Restrict network access via CSP
  4. Content Security Policy:
    • Remove unsafe-eval if possible (breaks some frameworks)
    • Whitelist only trusted CDNs
    • Consider using nonces for inline scripts
  5. Audit Widget Code:
    • Widgets have semi-trusted status
    • Review HTML content from MCP servers
    • Scan for XSS vulnerabilities
    • Monitor for suspicious postMessage patterns
  • client/src/components/tools/ResultsPanel.tsx - Detects OpenAI components
  • client/src/components/chat-v2/chatgpt-app-renderer.tsx - Main renderer component with triple-iframe architecture
  • client/src/components/ui/chatgpt-sandboxed-iframe.tsx - Triple-iframe implementation with cross-origin isolation
  • client/src/components/chat-v2/thread.tsx - Manages PiP state across all widgets in the thread
  • client/src/components/ChatTabV2.tsx - Chat integration with transform isolation for z-index stacking
  • server/routes/mcp/chatgpt.ts - Widget storage, serving, and OpenAI bridge injection
  • server/routes/mcp/chatgpt-sandbox-proxy.html - Middle iframe proxy for cross-origin message relay
  • server/routes/mcp/index.ts - Mounts ChatGPT routes at /chatgpt
  • client/src/components/chat-v2/csp-debug-panel.tsx - CSP debug panel UI
  • client/src/components/ui-playground/PlaygroundMain.tsx - UI Playground with CSP mode selector
  • client/src/stores/ui-playground-store.ts - CSP mode state management
  • client/src/stores/widget-debug-store.ts - CSP violation tracking
  • client/src/lib/mcp-tools-api.ts - Tool execution API

Resources

Contributing

When contributing to the OpenAI SDK integration:
  1. Test with real MCP servers - Don’t just mock the API
  2. Check security implications - All changes to iframe/postMessage code need review
  3. Update this documentation - Keep architecture diagrams current
  4. Add debug logging - Use console.log with [OpenAI Widget] prefix
  5. Consider backwards compatibility - Existing widgets should continue working
For questions or issues, open a GitHub issue or join our Discord community.