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 rich, interactive visualizations for their tool outputs.

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 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 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
  • Secure Isolation: Components run in sandboxed iframes with CSP headers
  • Dual Mode Support:
    • ui:// URIs for server-provided HTML content
    • External URLs for remotely hosted components

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
Method B: ui:// resource in response

ResultsPanel Detection Logic

Located in client/src/components/tools/ResultsPanel.tsx:100-104:
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/resources.ts:6-30:

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 BrowserRouter which 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 in server/routes/mcp/resources.ts:131-170:

Stage 2: Content Injection

Located in server/routes/mcp/resources.ts:173-438: Key steps:
  1. Retrieve widget data from store
  2. Read HTML from MCP server via readResource(uri)
  3. Inject window.openai API script
  4. Add security headers (CSP, X-Frame-Options)
  5. 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 in server/routes/mcp/resources.ts:250-376: Core API Methods:
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

5. Parent-Side Message Handling

Located in client/src/components/chat/openai-component-renderer.tsx:118-196:

Tool Execution Bridge

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

Security Architecture

Content Security Policy

Located in server/routes/mcp/resources.ts:408-422:

Iframe Sandbox

Located in client/src/components/chat/openai-component-renderer.tsx:218-230:
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
  • allow-popups-to-escape-sandbox: Allow popup windows to load normally
Security Trade-offs:
  • 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

  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. 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)
  4. 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
  5. State doesn’t persist
    • Check localStorage in browser DevTools
    • Verify widgetStateKey format is consistent
    • Confirm setWidgetState postMessage handler is working
Debug Tools:

Extending the Implementation

Adding New OpenAI API Methods:
  1. Update server-side injection script (server/routes/mcp/resources.ts:250-376)
  2. Add postMessage handler in parent (client/src/components/chat/openai-component-renderer.tsx:118-196)
  3. Update TypeScript types if needed
Example: Adding openExternal method:

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

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/openai-component-renderer.tsx - Renders iframes
  • client/src/components/ChatTab.tsx - Chat integration
  • server/routes/mcp/resources.ts - Widget storage and serving
  • 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.