Curated repos, tools, and frameworks shaping the developer ecosystem.
Live data from GitHub.
A comprehensive reference implementation demonstrating how to build custom ChatGPT applications using the OpenAI Apps SDK, Model Context Protocol, and Next.js. This project serves as both a working foundation and an interactive gallery of widget patterns showcasing the complete capabilities of the SDK.
This starter enables you to build custom applications that extend ChatGPT by:
Think of it as building mini-applications that live inside ChatGPT—where the model decides when to invoke your functionality, and your UI handles the presentation and interaction.
ChatGPT apps are built on a three-layer architecture:
┌─────────────────────────────────────────────────────────┐
│ Your MCP Server │
│ (Registers tools, handles requests, serves resources) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ChatGPT Runtime │
│ (Orchestrates tool calls, fetches resources) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Your Widget │
│ (React component with window.openai bridge access) │
└─────────────────────────────────────────────────────────┘
window.openaiMCP is the standard protocol for exposing tools and resources to AI models. Your server implements MCP endpoints that:
When your React widget renders inside ChatGPT, it gains access to a powerful API for bidirectional communication:
Methods:
callTool(name, params) - Invoke MCP tools from your widgetsendFollowUpMessage(text) - Insert messages into the conversationrequestDisplayMode(mode) - Change layout (inline, picture-in-picture, fullscreen)setWidgetState(state) - Persist component state across sessionsopenExternal(url) - Navigate to external URLsReactive Properties:
theme - ChatGPT's current theme (light/dark)locale - User's language preferencedisplayMode - Current layout modetoolOutput - Structured data from tool responseswidgetState - Persisted component stateOpenAI extends standard MCP with specialized metadata fields that control behavior:
Tool Metadata (_meta in tool descriptors):
openai/outputTemplate - Links tool to widget resource URIopenai/widgetAccessible - Enables widget-to-tool communicationopenai/toolInvocation/invoking - Loading state text (≤64 chars)openai/toolInvocation/invoked - Completion state text (≤64 chars)Resource Metadata (_meta in resource descriptors):
openai/widgetDescription - Human-readable widget summaryopenai/widgetPrefersBorder - UI rendering hintopenai/widgetCSP - Content Security Policy configurationopenai/widgetDomain - Optional dedicated subdomainTool responses support three fields with different visibility scopes:
{
structuredContent: { /* ... */ }, // Visible to: Model + Widget
content: "Human readable text", // Visible to: Model + Transcript
_meta: { /* ... */ } // Visible to: Widget only
}
This separation allows you to:
structuredContentcontent_metaapp/
├── mcp/
│ └── route.ts # MCP server - registers tools and resources
├── widgets/
│ ├── read-only/ # Simple data display widget
│ ├── widget-accessible/ # Interactive counter using callTool()
│ ├── send-message/ # Message injection example
│ ├── open-external/ # External link handling
│ ├── display-mode/ # Layout mode transitions
│ └── widget-state/ # State persistence example
├── hooks/
│ ├── use-tool-output.ts # Access tool response data
│ ├── use-call-tool.ts # Call tools from widgets
│ ├── use-send-message.ts # Send follow-up messages
│ ├── use-widget-state.ts # Persist component state
│ ├── use-display-mode.ts # React to layout changes
│ └── ... # Additional hooks wrapping window.openai
├── components/
│ └── ui/ # Radix UI + Tailwind components
├── layout.tsx # Root layout with SDK bootstrap
└── page.tsx # Home page
middleware.ts # CORS handling for RSC fetching
next.config.ts # Asset prefix configuration
baseUrl.ts # Environment-aware URL detection
app/mcp/route.ts)The heart of your application. This 600+ line file demonstrates:
next.config.ts)Critical for iframe rendering. Sets assetPrefix to ensure /_next/ static assets load from the correct origin:
const nextConfig: NextConfig = {
assetPrefix: baseURL, // Prevents 404s in iframe
};
Without this, Next.js attempts to load assets from the iframe URL, causing failures.
middleware.ts)Handles OPTIONS preflight requests required for cross-origin React Server Component fetching during client-side navigation.
app/layout.tsx)The NextChatSDKBootstrap component patches browser APIs to work within ChatGPT's iframe:
fetch - Rewrites same-origin requests to use correct base URLhistory.pushState/replaceState - Prevents full-origin URLs in historyRequired configuration:
<html lang="en" suppressHydrationWarning>
<head>
<NextChatSDKBootstrap baseUrl={baseURL} />
</head>
<body>{children}</body>
</html>
Note: suppressHydrationWarning is required because ChatGPT modifies initial HTML before Next.js hydrates.
app/hooks/)React hooks that wrap window.openai for clean, testable component code. Each hook provides reactive access to a specific bridge capability:
const weather = useToolOutput<WeatherData>();
const callTool = useCallTool();
const [state, setState] = useWidgetState();
const sendMessage = useSendMessage();
const theme = useTheme();
pnpm install
pnpm dev
The application runs on http://localhost:3009. The MCP server is available at http://localhost:3009/mcp.
Navigate to the home page and browse the widget gallery. Each example demonstrates a specific capability:
callTool()sendFollowUpMessage()openExternal()setWidgetState()Deploy to Vercel with one click:
The baseUrl.ts configuration automatically detects Vercel environments:
VERCEL_PROJECT_PRODUCTION_URLVERCEL_BRANCH_URL/mcp path (e.g., https://your-app.vercel.app/mcp)Note: Connecting MCP servers to ChatGPT requires developer mode access. See the connection guide for setup instructions.
Register a new tool in app/mcp/route.ts:
{
name: "my_tool",
description: "What this tool does",
inputSchema: { /* ... */ },
outputSchema: { /* ... */ },
_meta: {
"openai/outputTemplate": "template://widgets/my-widget",
"openai/widgetAccessible": true, // Enable callTool()
"openai/toolInvocation/invoking": "Loading...",
"openai/toolInvocation/invoked": "Loaded"
}
}
Build a React component in app/widgets/my-widget/page.tsx:
export default function MyWidget() {
const data = useToolOutput<MyDataType>();
const theme = useTheme();
const callTool = useCallTool();
return (
<div className={theme === 'dark' ? 'dark' : ''}>
{/* Your UI here */}
</div>
);
}
Add resource registration in app/mcp/route.ts:
{
uri: "template://widgets/my-widget",
name: "My Widget",
mimeType: "text/html+skybridge",
_meta: {
"openai/widgetDescription": "Clear description for the model",
"openai/widgetPrefersBorder": true,
"openai/widgetCSP": {
connect_domains: ["api.example.com"],
resource_domains: ["cdn.example.com"]
}
}
}
Implement the tool handler:
if (request.params.name === "my_tool") {
return {
structuredContent: { /* Data for model and widget */ },
content: "Human-readable summary",
_meta: { /* Widget-only metadata */ }
};
}
This project includes comprehensive documentation:
window.openai API reference and best practiceswindow.openai for cleaner, testable componentswidgetDescription to reduce redundant model narrationwidgetState under ~4k tokens for optimal performancestructuredContent for data shared between model and widget_meta for widget-only data like API keys or debug informationreadOnlyHint: true for non-mutating operations to help model planninginvoking and invoked statesHere's how all pieces connect for a weather widget:
Tool Registration:
{
name: "get_weather",
inputSchema: { location: "string" },
_meta: {
"openai/outputTemplate": "template://widgets/weather",
"openai/widgetAccessible": true
}
}
Resource Registration:
{
uri: "template://widgets/weather",
_meta: {
"openai/widgetDescription": "Shows current weather with forecast",
"openai/widgetCSP": {
connect_domains: ["api.weather.com"]
}
}
}
Tool Response:
{
structuredContent: {
temperature: 72,
condition: "sunny",
location: "San Francisco"
},
content: "The weather in San Francisco is 72°F and sunny."
}
Widget Component:
export default function WeatherWidget() {
const weather = useToolOutput<WeatherData>();
const callTool = useCallTool();
async function refresh() {
await callTool("get_weather", { location: weather.location });
}
return (
<Card>
<h2>{weather.location}</h2>
<p>{weather.temperature}°F - {weather.condition}</p>
<Button onClick={refresh}>Refresh</Button>
</Card>
);
}
This is a production-ready starting point for building sophisticated ChatGPT applications. Explore the examples, understand the patterns, and build something unique.
A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code.