| name | MCP Widget Development |
| description | 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. |
| version | 0.1.0 |
MCP 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 |
window.openai | Bridge to ChatGPT runtime |
_meta | 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
const input = window.openai.toolInput;
const output = window.openai.toolOutput;
const meta = window.openai.toolResponseMetadata;
Context Information
const theme = window.openai.theme;
const displayMode = window.openai.displayMode;
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
await window.openai.setWidgetState({ key: "value" });
const prevState = window.openai.toolResponseMetadata?.widgetState;
Layout Control
await window.openai.requestModal();
await window.openai.requestDisplayMode("modal");
window.openai.notifyIntrinsicHeight(400);
File Handling
const { fileId } = await window.openai.uploadFile(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);
}
body.dark {
--bg-color: #1a1a1a;
--text-color: #ffffff;
}
</style>
</head>
<body>
<div id="app">Loading...</div>
<script>
(.?. === ) {
...();
}
data = .?.?.;
meta = .?.?.;
app = .();
(data) {
app. = ;
}
.?.(..);
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 = =>Take Action
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": {...},
"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
notifyIntrinsicHeight after render
- Use semantic HTML - Improve accessibility
- Avoid external dependencies - Inline all code when possible
Additional Resources
Reference Files
For detailed patterns and examples:
references/window-openai-api.md - Complete window.openai API reference
references/csp-guide.md - CSP configuration guide
Example Files
Working examples in examples/:
examples/basic-widget.html - Simple data display widget
examples/interactive-widget.html - Widget with tool calls
Official Documentation