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 rich, 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 provides full support for the OpenAI Apps SDK, allowing MCP tools to return custom UI components that render in iframes with a sandboxedwindow.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 openai-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:| Feature | OpenAI Apps SDK | MCP-UI |
|---|---|---|
| Specification | OpenAI proprietary | MCP-UI (open standard) |
| Rendering | Sandboxed iframes | RemoteDOM components |
| Complexity | Full web applications | Simple interactive components |
| Tool calls | window.openai.callTool() | Action handlers |
| State persistence | window.openai.setWidgetState() | Not supported |
| Security | Full iframe sandbox | Component-level isolation |
| Best for | Complex dashboards, charts, forms | Buttons, cards, simple layouts |
- Need full JavaScript framework support (React, Vue, etc.)
- Require persistent state across sessions
- Building complex interactive visualizations
- Need access to external APIs and libraries
- Simple interactive components (buttons, cards)
- Prefer open standards over proprietary APIs
- Don’t need state persistence
- Want faster rendering without iframe overhead
Architecture Overview
Component Flow
1. Tool Execution & Detection
When a tool is executed that returns OpenAI SDK components, the system detects this in two ways: Method A:_meta["openai/outputTemplate"] field
ui:// resource in response
ResultsPanel Detection Logic
Located inclient/src/components/tools/ResultsPanel.tsx:100-104:
resolveUIResource function searches for ui:// URIs in:
- Direct
resourcefield at root level contentarray items withtype: "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
toolInputandtoolOutputforwindow.openaiAPI - Client localStorage can’t be shared across iframe sandbox boundaries
- Server becomes the source of truth for widget initialization data
Storage Implementation
Located inserver/routes/mcp/openai.ts:14-32:
3. Two-Stage Widget Loading
The system uses a clever two-stage loading process to ensure React Router compatibility: Why Two Stages?- Many widgets use React Router’s
BrowserRouterwhich expects clean URLs - Stage 1 changes URL to ”/” before widget code loads
- Stage 2 fetches actual content after URL is reset
- Prevents routing conflicts and 404 errors
Stage 1: Container Page
Located inserver/routes/mcp/openai.ts:81-120:
Stage 2: Content Injection
Located inserver/routes/mcp/openai.ts:123-459:
Key steps:
- Retrieve widget data from store
- Read HTML from MCP server via
readResource(uri) - Parse view mode and params from query string (
view_mode,view_params) - Inject
window.openaiAPI script with bridge implementation - Add security headers (CSP, X-Frame-Options)
- Set cache control headers (no-cache for fresh content)
4. window.openai API Bridge
The injected script provides the OpenAI Apps SDK API to widget code:API Implementation
Located inserver/routes/mcp/openai.ts:213-376:
Core API Methods:
- 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
5. Modal Support
As of PR #931, widgets can request modal dialogs usingwindow.openai.requestModal(). This enables widgets to display secondary views or detailed information in a separate modal context.
Modal Architecture
Modal Request Flow
Located inclient/src/components/chat-v2/openai-app-renderer.tsx:380-388:
window.openai.requestModal(), the parent:
- Extracts modal title and params from the message
- Opens a Dialog component
- Mounts a new iframe with the same widget URL
- Appends query params:
?view_mode=modal&view_params=<encoded_json>
View Mode Detection
Widgets receive view context viawindow.openai.view:
Widget State Synchronization
Modal and inline views share widget state automatically. When either view callssetWidgetState(), the state is propagated to the other view via openai:pushWidgetState messages.
Located in client/src/components/chat-v2/openai-app-renderer.tsx:269-284:
openai:widget_state event:
Located in server/routes/mcp/openai.ts:410-428:
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:62-72):
client/src/components/chat-v2/openai-app-renderer.tsx:440-442):
client/src/components/chat-v2/openai-app-renderer.tsx:444-476):
client/src/components/chat-v2/openai-app-renderer.tsx:481-493):
- 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 usez-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 thewindow.openai.requestDisplayMode() API:
7. Parent-Side Message Handling
Located inclient/src/components/chat-v2/openai-app-renderer.tsx:312-347:
Display Mode Synchronization
The component automatically resets to inline mode if another widget takes over PiP mode: Located inclient/src/components/chat-v2/openai-app-renderer.tsx:368-372:
Theme Synchronization
The parent component automatically sends theme updates to widgets when the user changes between light and dark mode: Located inclient/src/components/chat-v2/openai-app-renderer.tsx:293-307:
openai:set_globals event:
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/openai-app-renderer.tsx:232-244):
client/src/components/ChatTabV2.tsx:281-326):
client/src/components/chat-v2/thread.tsx:99):
- 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
nullremoves 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\"}"
- 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 inclient/src/components/ChatTab.tsx:181-207:
Security Architecture
Content Security Policy
Located inserver/routes/mcp/openai.ts:408-437:
Iframe Sandbox
Located inclient/src/components/chat-v2/openai-app-renderer.tsx:518-530:
allow-scripts: Enable JavaScript executionallow-same-origin: Allow localStorage access (required for state)allow-forms: Support form submissionsallow-popups: Enable external link navigationallow-popups-to-escape-sandbox: Allow popup windows to load normally
allow-same-origin+allow-scripts= Full JavaScript capabilities- Required for React Router and modern frameworks
- Mitigated by CSP headers and origin validation
- Widgets should be treated as semi-trusted code
Complete Data Flow Example
Let’s trace a complete interaction where a widget calls a tool:Development Guide
Testing OpenAI SDK Widgets Locally
- Create a test MCP server with OpenAI SDK support:
- Add server to MCPJam Inspector config:
- 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:-
Widget doesn’t load (404)
- Check that
widgetDataStorecontains toolId - Verify storage TTL hasn’t expired (1 hour default)
- Confirm MCP server returns valid HTML for
ui://resource
- Check that
-
window.openaiis undefined- Verify script injection in Stage 2 content endpoint
- Check browser console for CSP violations
- Ensure
<head>tag exists in HTML for injection
-
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
- Check that widget data was successfully stored via POST
-
Tool calls timeout
- Check network tab for
/api/mcp/tools/executefailures - Verify MCP server is connected and responsive
- Increase timeout in
callToolimplementation (default: 30s)
- Check network tab for
-
React Router 404 errors
- Confirm Stage 1 executes
history.replaceState('/')before loading - Check that widget uses
BrowserRouternotHashRouter - Verify
<base href=\"/\">is present in HTML
- Confirm Stage 1 executes
-
State doesn’t persist
- Check localStorage in browser DevTools
- Verify
widgetStateKeyformat:openai-widget-state:${toolName}:${toolId} - Confirm
setWidgetStatepostMessage handler is working
-
Theme not updating
- Check that
openai:set_globalsmessages are being sent from parent - Verify widget has event listener for theme changes
- Inspect
window.openai.themevalue in widget console
- Check that
-
Modal not opening
- Verify
requestModalpostMessage is being sent - Check that Dialog component state is updating (
modalOpen) - Inspect modal iframe URL includes
view_mode=modalquery param - Confirm
window.openai.view.modeis'modal'in modal iframe
- Verify
-
State not syncing between inline and modal
- Check that both iframes are mounted and have valid contentWindow
- Verify
openai:pushWidgetStatemessages are being sent - Inspect localStorage for widget state key
- Confirm both views have
openai:widget_stateevent listener
Extending the Implementation
Adding New OpenAI API Methods:- Update server-side injection script (
server/routes/mcp/resources.ts:250-376) - Add postMessage handler in parent (
client/src/components/chat/openai-component-renderer.tsx:118-196) - Update TypeScript types if needed
Performance Considerations
Widget Data Storage
- TTL: 1 hour default, configurable in
resources.ts:22 - Cleanup: Runs every 5 minutes
- Memory: Each widget stores ~1-10KB (toolInput + toolOutput)
- Scale: 1000 concurrent widgets ≈ 10MB memory
- Recommendation: For production, use Redis instead of Map
Iframe Rendering
- Initial Load: 200-500ms (Stage 1 + Stage 2 + resource fetch)
- Tool Calls: 100-300ms (postMessage + backend + MCP)
- Optimization:
- Cache MCP resource reads (currently disabled with
no-cache) - Preload widget data before iframe creation
- Use service workers for offline support
- Cache MCP resource reads (currently disabled with
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
-
Validate postMessage Origins:
-
Sanitize Tool Parameters:
-
Limit Widget Capabilities:
- Only expose necessary MCP tools to widgets
- Implement rate limiting on tool calls
- Restrict network access via CSP
-
Content Security Policy:
- Remove
unsafe-evalif possible (breaks some frameworks) - Whitelist only trusted CDNs
- Consider using nonces for inline scripts
- Remove
-
Audit Widget Code:
- Widgets have semi-trusted status
- Review HTML content from MCP servers
- Scan for XSS vulnerabilities
- Monitor for suspicious postMessage patterns
Related Files
client/src/components/tools/ResultsPanel.tsx- Detects OpenAI componentsclient/src/components/chat-v2/openai-app-renderer.tsx- Renders iframes, handles widget lifecycle, and manages display modesclient/src/components/chat-v2/thread.tsx- Manages PiP state across all widgets in the threadclient/src/components/ChatTabV2.tsx- Chat integration with transform isolation for z-index stackingserver/routes/mcp/openai.ts- Widget storage, serving, and OpenAI bridge injectionserver/routes/mcp/index.ts- Mounts OpenAI routes at/openaiclient/src/lib/mcp-tools-api.ts- Tool execution API
Resources
- OpenAI Apps SDK - Custom UX Guide
- OpenAI Apps SDK - API Reference
- MCP Specification
- MCPJam Inspector Repository
Contributing
When contributing to the OpenAI SDK integration:- Test with real MCP servers - Don’t just mock the API
- Check security implications - All changes to iframe/postMessage code need review
- Update this documentation - Keep architecture diagrams current
- Add debug logging - Use
console.logwith[OpenAI Widget]prefix - Consider backwards compatibility - Existing widgets should continue working

