| name | chatgpt-app-builder |
| description | Build production-ready ChatGPT Apps (MCP Servers) with React widgets, Vite HMR, and TypeScript. Use for creating new projects, adding tools/widgets, or understanding the ChatGPT Apps SDK architecture. Trigger phrases include 'create a ChatGPT App', 'add a tool to my app', 'add a widget', 'initialize MCP server', 'ChatGPT Apps SDK', 'OpenAI MCP'. |
ChatGPT App Builder
Build ChatGPT Apps using official OpenAI patterns WITHOUT third-party framework lock-in.
Quick Reference
| Task | Action |
|---|
| Initialize project | Run scripts/init-project.sh <name> |
| Add widget + tool | 1. Create src/widgets/my-widget/ 2. Register widget & tool in server |
| Start dev server | npm run dev |
| Build for production | npm run build |
Project Structure
my-chatgpt-app/
├── package.json
├── tsconfig.json
├── vite.config.ts # Multi-entry widget builds
├── nodemon.json # Server auto-restart
├── scripts/
│ └── generate-manifest.ts # Production build manifest
├── src/
│ ├── main.css # Global styles + Tailwind
│ ├── lib/
│ │ └── openai/ # OpenAI bridge hooks
│ │ ├── useToolOutput.ts
│ │ ├── useCallTool.ts
│ │ ├── useWidgetState.ts
│ │ ├── useSendMessage.ts
│ │ ├── useOpenAiGlobal.ts
│ │ ├── useMaxHeight.ts
│ │ └── index.ts
│ ├── types/
│ │ └── openai.types.ts # TypeScript declarations
│ ├── widgets/
│ │ └── hello/ # Each widget has its own directory
│ │ ├── index.tsx # Entry point
│ │ └── Hello.widget.tsx
│ └── server/
│ ├── index.ts # Entry point
│ └── lib/
│ └── chatgpt-app.ts # MCP server backbone
Adding a Widget + Tool
Step 1: Create Widget Directory
mkdir src/widgets/stock-ticker
Step 2: Create Entry Point
src/widgets/stock-ticker/index.tsx:
import '../main.css';
import { createRoot } from 'react-dom/client';
import { StockTickerWidget } from './StockTicker.widget';
createRoot(document.getElementById('widget-root')!).render(<StockTickerWidget />);
Step 3: Create Widget Component
src/widgets/stock-ticker/StockTicker.widget.tsx:
import { useToolOutput, useOpenAiGlobal } from '@/lib/openai';
import { AppsSDKUIProvider } from '@openai/apps-sdk-ui/components/AppsSDKUIProvider';
interface StockData {
symbol: string;
price: number;
change: string;
}
export function StockTickerWidget() {
const data = useToolOutput<StockData>();
const theme = useOpenAiGlobal('theme');
if (!data) return <div>Loading...</div>;
return (
<AppsSDKUIProvider>
<div className={`p-4 ${theme === 'dark' ? 'dark' : ''}`}>
<h2 className="text-xl font-bold">{data.symbol}</h2>
<p className="text-3xl">${data.price}</p>
< =>{data.change}
);
}
Step 4: Register Widget + Tool on Server
src/server/index.ts:
import { ChatGPTApp } from './lib/chatgpt-app.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { z } from 'zod';
const app = new ChatGPTApp({
name: 'my-stock-app',
widgetDomain: 'https://widgets.myapp.com',
devMode: process.env.NODE_ENV !== 'production',
});
app.registerWidget('stock-ticker', {
title: 'Stock Price Ticker',
description: 'Real-time stock price display',
invokingMessage: 'Fetching stock price...',
invokedMessage: 'Price loaded',
csp: {
connectDomains: ['https://api.stocks.com'],
resourceDomains: ['https://cdn.myapp.com'],
},
});
app.registerTool({
name: 'get_stock_price',
description: 'Get the current price of a stock',
schema: zodToJsonSchema(z.object({ symbol: z.string() })),
: ,
: ({ }: { : }) => {
price = ();
{ , price, : };
},
});
app.();
Development Workflow
npm run dev
Testing in ChatGPT
- Expose local server:
ngrok http 3000
- Go to ChatGPT → Settings → Apps → Advanced → Create App
- Enter ngrok URL +
/mcp
- Test by typing
@YourAppName in chat
Key Concepts
Widget Data Flow
- ChatGPT calls your tool → handler returns data
- Data is passed to widget via
window.openai.toolOutput
- Widget uses
useToolOutput() to reactively access data
OpenAI Bridge Hooks
| Hook | Purpose |
|---|
useToolOutput<T>() | Get data from tool execution |
useCallTool() | Call another tool from widget |
useWidgetState() | Persist UI state across renders |
useSendMessage() | Send follow-up message to ChatGPT |
useOpenAiGlobal(key) | Access theme, displayMode, maxHeight, etc. |
useMaxHeight() | Get widget max height for fullscreen |
CSP Configuration
When your widget needs to make external requests, configure CSP:
app.registerWidget('my-widget', {
title: 'My Widget',
csp: {
connectDomains: ['https://api.example.com'],
resourceDomains: ['https://cdn.example.com'],
},
});
Without proper CSP, external requests will be blocked by ChatGPT's sandbox.
References
| Document | Purpose |
|---|
references/architecture.md | MCP protocol, SSE transport, data flow |
references/hooks.md | Complete hooks API reference |
references/metadata.md | All _meta fields documentation |
Production Deployment
- Build widgets:
npm run build
- Deploy
dist/ to CDN
- Set environment variables:
NODE_ENV=production
WIDGET_DOMAIN=https://widgets.yourapp.com
WIDGET_ASSETS_URL=https://cdn.yourapp.com
- Deploy server to your hosting platform