Awesome-omni-skill 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.
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/ai-agents/chatgpt-app-builder" ~/.claude/skills/diegosouzapw-awesome-omni-skill-chatgpt-app-builder && rm -rf "$T"
skills/ai-agents/chatgpt-app-builder/SKILL.md- references .env files
ChatGPT App Builder
Build production-ready ChatGPT apps with interactive widgets using mcp-use. Zero-config widget development with automatic registration and built-in React hooks.
Quick Start
npx create-mcp-use-app my-chatgpt-app --template mcp-apps cd my-chatgpt-app npm install && npm run dev
Project structure:
my-chatgpt-app/ ├── resources/ # React widgets (auto-registered!) │ ├── weather-display.tsx # Example widget │ └── product-card.tsx # Another widget ├── public/ # Static assets │ └── images/ ├── index.ts # MCP server entry ├── package.json └── tsconfig.json
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
folder - auto-registeredresources/ - Built-in
hook with state, props, tool callsuseWidget() - 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
| Protocol | Use Case | Compatibility | Status |
|---|---|---|---|
MCP Apps () | Maximum compatibility | ChatGPT + MCP Apps clients | Recommended |
ChatGPT Apps SDK () | ChatGPT-only features | ChatGPT only | Supported |
Why MCP Apps? It's the official standard (SEP-1865) for interactive widgets:
- Universal: Works with ChatGPT, Claude Desktop, Goose, and all MCP Apps clients
- Future-proof: Based on open specification
- Zero config: With
, mcp-use generates metadata for BOTH protocols automaticallytype: "mcpApps"
Creating Widgets
Simple Widget (Single File)
Create
resources/weather-display.tsx:
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react"; import { z } from "zod"; 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(); 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;
Widget is automatically:
- Registered as MCP tool
weather-display - Registered as MCP resource
ui://widget/weather-display.html - Bundled for Apps SDK compatibility
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
Entry point must be named
widget.tsx and export widgetMetadata + default component.
Widget Metadata
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", }, };
Key fields:
: Used for tool and resource descriptionsdescription
: Zod schema defines widget input parametersprops
: Set toexposeAsTool
if only using widget via custom toolsfalse
: Unified configuration for both protocols (recommended)metadata
Content Security Policy (CSP)
Control external resources your widget can access:
export const widgetMetadata: WidgetMetadata = { description: "Weather widget", props: z.object({ city: z.string() }), metadata: { csp: { // APIs to call (fetch, WebSocket, XMLHttpRequest) connectDomains: ["https://api.weather.com", "https://backup.weather.com"], // Static assets (images, fonts, stylesheets) resourceDomains: ["https://cdn.weather.com"], // External iframes frameDomains: ["https://embed.weather.com"], // Script directives (use carefully!) scriptDirectives: ["'unsafe-inline'"], }, }, };
Security tips:
- Specify exact domains:
https://api.weather.com - Avoid wildcards in production
- Never use
unless necessary'unsafe-eval'
For detailed CSP configuration and legacy format, see references/csp-and-metadata.md.
useWidget Hook
const { props, // Widget input from tool (empty {} while pending) isPending, // True while tool still executing state, // Persistent widget state setState, // Update persistent state theme, // 'light' | 'dark' from host callTool, // Call other MCP tools displayMode, // 'inline' | 'pip' | 'fullscreen' requestDisplayMode, // Request display mode change output, // Additional tool output } = useWidget<MyPropsType, MyOutputType>();
Props and Loading States
Critical: Widgets render BEFORE tool execution completes. Always handle
isPending:
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:
const { state, setState } = useWidget(); // Save state (persists in ChatGPT localStorage) const addFavorite = async (city: string) => { await setState({ favorites: [...(state?.favorites || []), city], }); }; // Update with function await setState((prev) => ({ ...prev, count: (prev?.count || 0) + 1, }));
Calling MCP Tools
Widgets can call other tools:
const { callTool } = useWidget(); const refreshData = async () => { try { const result = await callTool("get-weather", { city: "Tokyo" }); console.log("Result:", result.content); } catch (error) { console.error("Tool call failed:", error); } };
Display Mode Control
const { displayMode, requestDisplayMode } = useWidget(); const goFullscreen = async () => { await requestDisplayMode("fullscreen"); }; // Current mode: 'inline' | 'pip' | 'fullscreen' console.log(displayMode);
Custom Tools with Widgets
Create tools that return widgets:
import { MCPServer, widget, text } from "mcp-use/server"; import { z } from "zod"; const server = new MCPServer({ name: "weather-app", version: "1.0.0", baseUrl: process.env.MCP_URL || "http://localhost:3000", }); server.tool( { name: "get-weather", description: "Get current weather for a city", schema: z.object({ city: z.string().describe("City name"), }), // Widget config (registration-time metadata) widget: { name: "weather-display", // Must match widget in resources/ invoking: "Fetching weather...", invoked: "Weather data loaded", }, }, async ({ city }) => { const data = await fetchWeatherAPI(city); return widget({ props: { city, temperature: data.temp, conditions: data.conditions, humidity: data.humidity, }, output: text(`Weather in ${city}: ${data.temp}°C`), message: `Current weather for ${city}`, }); } ); server.listen();
Key points:
in server config enables proper asset loadingbaseUrl
on tool definitionwidget: { name, invoking, invoked }
helper returns runtime datawidget({ props, output })
passed to widget,props
shown to modeloutput
Static Assets
Use
public/ folder for images, fonts:
my-app/ ├── resources/ ├── public/ │ ├── images/ │ │ ├── logo.svg │ │ └── banner.png │ └── fonts/ └── index.ts
Using assets in widgets:
import { Image } from "mcp-use/react"; function MyWidget() { return ( <div> {/* Paths relative to public/ folder */} <Image src="/images/logo.svg" alt="Logo" /> <img src={window.__getFile?.("images/banner.png")} alt="Banner" /> </div> ); }
Components
McpUseProvider
Unified provider combining all common setup:
import { McpUseProvider } from "mcp-use/react"; function MyWidget() { return ( <McpUseProvider autoSize // Auto-resize widget viewControls // Add debug/fullscreen buttons debug // Show debug info > <div>Widget content</div> </McpUseProvider> ); }
Image Component
Handles both data URLs and public paths:
import { Image } from "mcp-use/react"; <Image src="/images/photo.jpg" alt="Photo" /> <Image src="data:image/png;base64,..." alt="Data URL" />
ErrorBoundary
import { ErrorBoundary } from "mcp-use/react"; <ErrorBoundary fallback={<div>Something went wrong</div>} onError={(error) => console.error(error)} > <MyComponent /> </ErrorBoundary>
For full component API, see references/components-api.md.
Testing
Using the Inspector
- Start development:
npm run dev - Open
http://localhost:3000/inspector - Click Tools tab → Find your widget → Enter parameters → Execute
- Debug with browser console, RPC logs, state inspection
Testing in ChatGPT
- Enable Developer Mode: Settings → Connectors → Advanced → Developer mode
- Add your server: Connectors tab → Add remote MCP server URL
- Test: Select Developer Mode from Plus menu → Choose connector → Use tools
Prompting tips:
- Be explicit: "Use the weather-app connector's get-weather tool..."
- Disallow alternatives: "Do not use built-in tools, only use my connector"
- Specify input: "Call get-weather with { city: 'Tokyo' }"
Dual-protocol note: With
type: "mcpApps", widgets work in both ChatGPT and MCP Apps clients without code changes.
Best Practices
Schema Design
// Good - descriptive const schema = z.object({ city: z.string().describe("City name (e.g., Tokyo, Paris)"), temperature: z.number().min(-50).max(60).describe("Temp in Celsius"), }); // Bad - no descriptions const schema = z.object({ city: z.string(), temp: z.number(), });
Theme Support
const { theme } = useWidget(); const bgColor = theme === "dark" ? "bg-gray-900" : "bg-white"; const textColor = theme === "dark" ? "text-white" : "text-gray-900";
Loading States
Always check
isPending first:
const { props, isPending } = useWidget<MyProps>(); if (isPending) return <LoadingSpinner />; return <div>{props.field}</div>;
Widget Focus
Keep widgets focused on one thing:
// Good: Single purpose export const widgetMetadata: WidgetMetadata = { description: "Display weather for a city", props: z.object({ city: z.string() }), }; // Bad: Too many responsibilities export const widgetMetadata: WidgetMetadata = { description: "Weather, forecast, map, news, and more", props: z.object({ /* many fields */ }), };
Error Handling
const { callTool } = useWidget(); const fetchData = async () => { try { const result = await callTool("fetch-data", { id: "123" }); if (result.isError) { console.error("Tool returned error"); } } catch (error) { console.error("Tool call failed:", error); } };
Configuration
Production Setup
const server = new MCPServer({ name: "my-app", version: "1.0.0", baseUrl: process.env.MCP_URL || "https://myserver.com", });
Environment Variables
MCP_URL=https://myserver.com MCP_SERVER_URL=https://myserver.com/api CSP_URLS=https://cdn.example.com,https://api.example.com
Deployment
npx mcp-use login npm run deploy
Build for production:
npm run build npm start
Troubleshooting
Widget Not Appearing
- Ensure
extension.tsx - Export
objectwidgetMetadata - Export default React component
- Check server logs for errors
- Verify widget name matches file/folder name
Props Not Received
- Check
first (props empty while pending)isPending - Use
hook (not React props)useWidget() - Verify
is valid Zod schemawidgetMetadata.props - Check tool parameters match schema
CSP Errors
- Set
in server configbaseUrl - Add domains to CSP via
metadata.csp - Use HTTPS for all resources
- Check browser console for CSP violations
Protocol Compatibility
- Use
for dual-protocol supporttype: "mcpApps" - Set
correctly in server configbaseUrl - Use
(camelCase) notmetadata
for dual-protocolappsSdkMetadata - Test in Inspector which supports both protocols
Quick Reference
Commands:
- Bootstrapnpx create-mcp-use-app my-app --template mcp-apps
- Development with hot reloadnpm run dev
- Build for productionnpm run build
- Run production servernpm start
- Deploy to mcp-use Cloudnpm run deploy
Widget structure:
- Single file widgetresources/widget-name.tsx
- Folder-based widget entryresources/widget-name/widget.tsx
- Static assetspublic/
Widget metadata:
- Widget description (required)description
- Zod schema for input (required)props
- Auto-register as tool (default: true)exposeAsTool
- Unified config (dual-protocol)metadata
- Content Security Policymetadata.csp
useWidget returns:
- Widget input parametersprops
- Loading state flagisPending
- Persistent statestate, setState
- Call other toolscallTool
- Current theme (light/dark)theme
- Display controldisplayMode, requestDisplayMode
References
- Widget Patterns - Complex widgets, data fetching, state, theming examples
- CSP and Metadata - Detailed CSP, legacy format, migration guide
- Components API - Full McpUseProvider, Image, ErrorBoundary API
Learn More
- Documentation: https://docs.mcp-use.com
- MCP Apps Standard: https://docs.mcp-use.com/typescript/server/mcp-apps
- Widget Guide: https://docs.mcp-use.com/typescript/server/ui-widgets
- ChatGPT Apps Flow: https://docs.mcp-use.com/guides/chatgpt-apps-flow
- Inspector Debugging: https://docs.mcp-use.com/inspector/debugging-chatgpt-apps
- GitHub: https://github.com/mcp-use/mcp-use