Awesome-omni-skill MCP Widget Development
This skill should be used when the user asks to "build a widget", "create UI component", "ChatGPT UI", "window.openai API", "widget template", "skybridge", "render in ChatGPT", "CSP configuration", or needs guidance on building interactive UI components for OpenAI Apps SDK that render inside ChatGPT.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/mcp-widget-development" ~/.claude/skills/diegosouzapw-awesome-omni-skill-mcp-widget-development && rm -rf "$T"
skills/development/mcp-widget-development/SKILL.mdMCP Widget Development for OpenAI Apps SDK
Overview
Widgets are interactive UI components that render inside ChatGPT conversations. Built with HTML, CSS, and JavaScript, they run in a sandboxed iframe and communicate with the MCP server through the
window.openai bridge.
Widget Architecture
Tool Call → Server Returns _meta.openai/outputTemplate → ChatGPT Loads Widget HTML → Widget Reads window.openai.toolOutput → Widget Renders UI → User Interacts → Widget Calls Tools (optional)
Key Components
| Component | Purpose |
|---|---|
| HTML Template | Widget markup and styles |
| Bridge to ChatGPT runtime |
| Widget-only data from server |
| CSP Config | Security allowlists |
Registering Widget Templates
Widgets are served as MCP resources with the special mime type
text/html+skybridge.
Python
@mcp.resource("ui://widget/main.html") def main_widget() -> str: return """<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style>/* styles */</style> </head> <body> <div id="app"></div> <script>/* widget code */</script> </body> </html>"""
TypeScript
server.setRequestHandler(ReadResourceRequestSchema, async (request) => ({ contents: [{ uri: request.params.uri, mimeType: "text/html+skybridge", text: widgetHtml }] }));
The window.openai Bridge
The
window.openai object provides access to ChatGPT runtime:
Data Access
// Tool input parameters const input = window.openai.toolInput; // Tool output (structuredContent + _meta) const output = window.openai.toolOutput; // Response metadata const meta = window.openai.toolResponseMetadata;
Context Information
// Theme: "light" or "dark" const theme = window.openai.theme; // Display mode: "inline" or "modal" const displayMode = window.openai.displayMode; // User's locale (BCP 47) const locale = window.openai.locale;
Tool Invocation
Call tools from the widget (requires
openai/widgetAccessible: true):
const result = await window.openai.callTool("tool_name", { param1: "value1", param2: "value2" });
State Management
// Save widget state (persists across conversation turns) await window.openai.setWidgetState({ key: "value" }); // Read previous state const prevState = window.openai.toolResponseMetadata?.widgetState;
Layout Control
// Request modal display await window.openai.requestModal(); // Set display mode await window.openai.requestDisplayMode("modal"); // or "inline" // Report content height for proper sizing window.openai.notifyIntrinsicHeight(400);
File Handling
// Upload a file const { fileId } = await window.openai.uploadFile(file); // Get download URL for a file const { downloadUrl } = await window.openai.getFileDownloadUrl({ fileId });
Widget Template Structure
Basic Template
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; background: var(--bg-color, #ffffff); color: var(--text-color, #000000); } /* Theme support */ body.dark { --bg-color: #1a1a1a; --text-color: #ffffff; } </style> </head> <body> <div id="app">Loading...</div> <script> // Apply theme if (window.openai?.theme === 'dark') { document.body.classList.add('dark'); } // Get data from tool response const data = window.openai?.toolOutput?.structuredContent; const meta = window.openai?.toolOutput?._meta; // Render content const app = document.getElementById('app'); if (data) { app.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`; } // Report height window.openai?.notifyIntrinsicHeight(document.body.scrollHeight); </script> </body> </html>
Interactive Widget
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> .card { padding: 16px; border: 1px solid #ddd; border-radius: 8px; } .btn { padding: 8px 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; } .btn:hover { background: #0055aa; } </style> </head> <body> <div class="card"> <h2 id="title">Item</h2> <p id="description"></p> <button class="btn" id="action">Take Action</button> </div> <script> const data = window.openai?.toolOutput?._meta?.item; document.getElementById('title').textContent = data?.name || 'Unknown'; document.getElementById('description').textContent = data?.description || ''; document.getElementById('action').addEventListener('click', async () => { try { const result = await window.openai.callTool('process_item', { itemId: data?.id }); alert('Success: ' + JSON.stringify(result)); } catch (err) { alert('Error: ' + err.message); } }); </script> </body> </html>
Linking Tools to Widgets
Server-Side
Return the widget URI in
_meta.openai/outputTemplate:
@mcp.tool() def get_dashboard() -> dict: return { "structuredContent": {"summary": "Dashboard loaded"}, "_meta": { "fullData": {...}, # Widget-only "openai/outputTemplate": "ui://widget/dashboard.html" } }
Widget-Accessible Tools
Enable tools to be called from widgets:
@mcp.tool() def refresh_data() -> dict: return { "structuredContent": {"data": [...]}, "_meta": { "openai/widgetAccessible": True } }
CSP Configuration
Configure Content Security Policy for widgets:
@mcp.resource("ui://widget/main.html") def widget() -> dict: return { "contents": [{ "uri": "ui://widget/main.html", "mimeType": "text/html+skybridge", "text": html_content }], "_meta": { "openai/widgetCSP": { "connect_domains": ["api.example.com"], "resource_domains": ["cdn.example.com"], "frame_domains": ["embed.example.com"] } } }
Theme Support
Handle light and dark themes:
const theme = window.openai?.theme || 'light'; document.documentElement.setAttribute('data-theme', theme);
:root { --bg: #ffffff; --text: #000000; } [data-theme="dark"] { --bg: #1a1a1a; --text: #ffffff; } body { background: var(--bg); color: var(--text); }
Best Practices
- Keep widgets lightweight - Minimize bundle size for fast loading
- Handle missing data gracefully - Check for null/undefined
- Support both themes - Test in light and dark mode
- Report content height - Call
after rendernotifyIntrinsicHeight - Use semantic HTML - Improve accessibility
- Avoid external dependencies - Inline all code when possible
Additional Resources
Reference Files
For detailed patterns and examples:
- Complete window.openai API referencereferences/window-openai-api.md
- CSP configuration guidereferences/csp-guide.md
Example Files
Working examples in
examples/:
- Simple data display widgetexamples/basic-widget.html
- Widget with tool callsexamples/interactive-widget.html
Official Documentation
- Apps SDK UI Guidelines: https://developers.openai.com/apps-sdk/concepts/ui-guidelines/
- Widget Reference: https://developers.openai.com/apps-sdk/reference/