Skip to main content

chatgpt-app-builder

Build ChatGPT apps with interactive widgets using mcp-use and OpenAI Apps SDK. Use when creating ChatGPT apps, building MCP servers with widgets, defining React widgets, working with Apps SDK, or when user mentions ChatGPT widgets, mcp-use widgets, or Apps SDK development.

Source facts

Repository
mcp-use/skills
Last source activity
January 31, 2026 at 16:32
Detected SKILL.md language
English
Stars
1
Forks
2

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
chatgpt-app-builder
description
Build ChatGPT apps with interactive widgets using mcp-use and OpenAI Apps SDK. Use when creating ChatGPT apps, building MCP servers with widgets, defining React widgets, working with Apps SDK, or when user mentions ChatGPT widgets, mcp-use widgets, or Apps SDK development.
# ChatGPT App Builder Build production-ready ChatGPT apps with interactive widgets using the mcp-use framework and OpenAI Apps SDK. This skill provides zero-config widget development with automatic registration and built-in React hooks. ## Quick Start **Always bootstrap with the MCP Apps template:** ```bash npx create-mcp-use-app my-chatgpt-app --template mcp-apps cd my-chatgpt-app yarn install yarn dev ``` This creates a project structure: ``` my-chatgpt-app/ ├── resources/ # React widgets (auto-registered!) │ ├── display-weather.tsx # Example widget │ └── product-card.tsx # Another widget ├── public/ # Static assets │ └── images/ ├── index.ts # MCP server entry ├── package.json ├── tsconfig.json └── README.md ``` ## Why mcp-use for ChatGPT Apps? Traditional OpenAI Apps SDK requires significant manual setup: - Separate project structure (server/ and web/ folders) - Manual esbuild/webpack configuration - Custom useWidgetState hook implementation - Manual React mounting code - Manual CSP configuration - Manual widget registration **mcp-use simplifies everything:** - ✅ Single command setup - ✅ Drop widgets in `resources/` folder - auto-registered - ✅ Built-in `useWidget()` hook with state, props, tool calls - ✅ Automatic bundling with hot reload - ✅ Automatic CSP configuration - ✅ Built-in Inspector for testing - ✅ Dual-protocol support (works with ChatGPT AND MCP Apps clients) ## MCP Apps vs ChatGPT Apps SDK mcp-use supports multiple widget protocols, giving you maximum compatibility: | Protocol | Use Case | Compatibility | Status | | ---------------------------------------- | ---------------------- | ----------------------------- | --------------- | | **MCP Apps** (`type: "mcpApps"`) | Maximum compatibility | ✅ ChatGPT + MCP Apps clients | **Recommended** | | **ChatGPT Apps SDK** (`type: "appsSdk"`) | ChatGPT-only features | ✅ ChatGPT only | Supported | | **MCP-UI** | Simple, static content | ✅ MCP clients only | Specialized | ### Why MCP Apps? MCP Apps is the **official standard** (SEP-1865) for interactive widgets in the Model Context Protocol: - **Universal**: Works with ChatGPT, Claude Desktop, Goose, and all MCP Apps clients - **Future-proof**: Based on open specification, ensuring long-term compatibility - **Secure**: Double-iframe sandbox with granular CSP control - **Zero config**: With `type: "mcpApps"`, mcp-use automatically generates metadata for BOTH protocols **Key Point**: When you use `type: "mcpApps"` in your server configuration, your widgets automatically work with both ChatGPT (Apps SDK protocol) and MCP Apps clients. You write the widget once, and mcp-use handles the protocol translation. ## Creating Widgets ### Simple Widget (Single File) Create `resources/weather-display.tsx`: ```tsx import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react"; import { z } from "zod"; // Define widget metadata export const widgetMetadata: WidgetMetadata = { description: "Display current weather for a city", props: z.object({ city: z.string().describe("City name"), temperature: z.number().describe("Temperature in Celsius"), conditions: z.string().describe("Weather conditions"), humidity: z.number().describe("Humidity percentage"), }), }; const WeatherDisplay: React.FC = () => { const { props, isPending } = useWidget(); // Always handle loading state first if (isPending) { return ( <McpUseProvider autoSize> <div className="animate-pulse p-4">Loading weather...</div> </McpUseProvider> ); } return ( <McpUseProvider autoSize> <div className="weather-card p-4 rounded-lg shadow"> <h2 className="text-2xl font-bold">{props.city}</h2> <div className="temp text-4xl">{props.temperature}°C</div> <p className="conditions">{props.conditions}</p> <p className="humidity">Humidity: {props.humidity}%</p> </div> </McpUseProvider> ); }; export default WeatherDisplay; ``` That's it! The widget is automatically: - Registered as MCP tool `weather-display` - Registered as MCP resource `ui://widget/weather-display.html` - Bundled for Apps SDK compatibility - Ready to use in ChatGPT ### Complex Widget (Folder Structure) For widgets with multiple components: ``` resources/ └── product-search/ ├── widget.tsx # Entry point (required name) ├── components/ │ ├── ProductCard.tsx │ └── FilterBar.tsx ├── hooks/ │ └── useFilter.ts ├── types.ts └── constants.ts ``` **Entry point (`widget.tsx`):** ```tsx import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react"; import { z } from "zod"; import { ProductCard } from "./components/ProductCard"; import { FilterBar } from "./components/FilterBar"; export const widgetMetadata: WidgetMetadata = { description: "Display product search results with filtering", props: z.object({ products: z.array( z.object({ id: z.string(), name: z.string(), price: z.number(), image: z.string(), }) ), query: z.string(), }), }; const ProductSearch: React.FC = () => { const { props, isPending, state, setState } = useWidget(); if (isPending) { return ( <McpUseProvider autoSize> <div>Loading...</div> </McpUseProvider> ); } return ( <McpUseProvider autoSize> <div> <h1>Search: {props.query}</h1> <FilterBar onFilter={(filters) => setState({ filters })} /> <div className="grid grid-cols-3 gap-4"> {props.products.map((p) => ( <ProductCard key={p.id} product={p} /> ))} </div> </div> </McpUseProvider> ); }; export default ProductSearch; ``` ## Widget Metadata Required metadata for automatic registration: ```typescript export const widgetMetadata: WidgetMetadata = { // Required: Human-readable description description: "Display weather information", // Required: Zod schema for widget props props: z.object({ city: z.string().describe("City name"), temperature: z.number(), }), // Optional: Disable automatic tool registration exposeAsTool: true, // default // Optional: Unified metadata (works for BOTH ChatGPT and MCP Apps) metadata: { csp: { connectDomains: ["https://api.weather.com"], resourceDomains: ["https://cdn.weather.com"], }, prefersBorder: true, autoResize: true, widgetDescription: "Interactive weather display", }, }; ``` **Important:** - `description`: Used for tool and resource descriptions - `props`: Zod schema defines widget input parameters - `exposeAsTool`: Set to `false` if only using widget via custom tools - `metadata`: Unified configuration that works for both protocols (recommended) ## Content Security Policy (CSP) Control what external resources your widget can access using CSP configuration: ```typescript export const widgetMetadata: WidgetMetadata = { description: "Weather widget", props: z.object({ city: z.string() }), metadata: { csp: { // APIs your widget needs to call connectDomains: ["https://api.weather.com", "https://weather-backup.com"], // Static assets (images, fonts, stylesheets) resourceDomains: ["https://cdn.weather.com"], // External content to embed in iframes frameDomains: ["https://embed.weather.com"], // Script CSP directives (use carefully!) scriptDirectives: ["'unsafe-inline'"], }, }, }; ``` **CSP Field Reference:** - **`connectDomains`**: APIs to call via fetch, WebSocket, XMLHttpRequest - **`resourceDomains`**: Load images, fonts, stylesheets, videos - **`frameDomains`**: Embed external content in iframes - **`scriptDirectives`**: Script-src CSP directives (avoid `'unsafe-eval'` in production) **Security Best Practices:** - ✅ Specify exact domains: `https://api.weather.com` - ❌ Avoid wildcards: `https://*.weather.com` (less secure) - ❌ Never use `'unsafe-eval'` unless absolutely necessary - ✅ Test CSP in development before deploying ## Metadata Configuration Options ### Modern Unified Approach (Recommended) Use the `metadata` field for dual-protocol support: ```typescript export const widgetMetadata: WidgetMetadata = { description: "Weather widget", props: propSchema, metadata: { // Works for BOTH MCP Apps AND ChatGPT csp: { connectDomains: ["https://api.weather.com"], resourceDomains: ["https://cdn.weather.com"], }, prefersBorder: true, autoResize: true, widgetDescription: "Displays current weather", }, }; ``` ### Legacy Apps SDK Approach (Deprecated) The old ChatGPT-only format (still supported but not recommended): ```typescript export const widgetMetadata: WidgetMetadata = { description: "Weather widget", props: propSchema, appsSdkMetadata: { // ChatGPT only - snake_case with openai/ prefix "openai/widgetCSP": { connect_domains: ["https://api.weather.com"], resource_domains: ["https://cdn.weather.com"], }, "openai/widgetPrefersBorder": true, "openai/toolInvocation/invoking": "Loading...", "openai/toolInvocation/invoked": "Loaded", }, }; ``` **Migration Note**: The old format uses `appsSdkMetadata` with `openai/` prefixes and snake_case (e.g., `connect_domains`). The new format uses `metadata` with camelCase (e.g., `connectDomains`) and works for both protocols. ### Using Both for Custom ChatGPT Features You can combine both fields to use standard metadata plus ChatGPT-specific overrides: ```typescript export const widgetMetadata: WidgetMetadata = { description: "Weather widget", props: propSchema, // Unified metadata (dual-protocol) metadata: { csp: { connectDomains: ["https://api.weather.com"] }, prefersBorder: true, }, // ChatGPT-specific overrides/additions appsSdkMetadata: { "openai/widgetDescription": "ChatGPT-specific description", "openai/customFeature": "some-value", // Any custom OpenAI metadata "openai/locale": "en-US", }, }; ``` **Use Case**: When you need to pass custom OpenAI-specific metadata that doesn't exist in the unified format, add it to `appsSdkMetadata`. The fields will be passed directly to ChatGPT with the `openai/` prefix ## useWidget Hook The `useWidget` hook provides everything you need: ```tsx const { // Widget props from tool input props, // Loading state (true = tool still executing) isPending, // Persistent widget state state, setState, // Theme from host (light/dark) theme, // Call other MCP tools callTool, // Display mode control displayMode, requestDisplayMode, // Additional tool output output, } = useWidget<MyPropsType, MyOutputType>(); ``` ### Props and Loading States **Critical:** Widgets render BEFORE tool execution completes. Always handle `isPending`: ```tsx const { props, isPending } = useWidget<WeatherProps>(); // Pattern 1: Early return if (isPending) { return <div>Loading...</div>; } // Now props are safe to use // Pattern 2: Conditional rendering return <div>{isPending ? <LoadingSpinner /> : <div>{props.city}</div>}</div>; // Pattern 3: Optional chaining (partial UI) return ( <div> <h1>{props.city ?? "Loading..."}</h1> </div> ); ``` ### Widget State Persist data across widget interactions: ```tsx const { state, setState } = useWidget(); // Save state (persists in ChatGPT localStorage) const addFavorite = async (city: string) => { await setState({ favorites: [...(state?.favorites || []), city], }); }; // Update with function
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub