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/development/chatgpt-app-builder-neversight" ~/.claude/skills/diegosouzapw-awesome-omni-skill-chatgpt-app-builder-9ac98d && rm -rf "$T"
skills/development/chatgpt-app-builder-neversight/SKILL.mdChatGPT 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 Apps SDK template:
npx create-mcp-use-app my-chatgpt-app --template apps-sdk 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
folder - auto-registeredresources/ - ✅ Built-in
hook with state, props, tool callsuseWidget() - ✅ Automatic bundling with hot reload
- ✅ Automatic CSP configuration
- ✅ Built-in Inspector for testing
Creating Widgets
Simple Widget (Single File)
Create
resources/weather-display.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
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:
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: Apps SDK metadata appsSdkMetadata: { 'openai/widgetDescription': 'Interactive weather display', 'openai/toolInvocation/invoking': 'Loading weather...', 'openai/toolInvocation/invoked': 'Weather loaded', 'openai/widgetCSP': { connect_domains: ['https://api.weather.com'], resource_domains: ['https://cdn.weather.com'], }, }, };
Important:
: Used for tool and resource descriptionsdescription
: Zod schema defines widget input parametersprops
: Set toexposeAsTool
if only using widget via custom toolsfalse- Default Apps SDK metadata is auto-generated if not specified
useWidget Hook
The
useWidget hook provides everything you need:
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:
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
Request different display modes:
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', }); 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 }) => { // Fetch data from API const data = await fetchWeatherAPI(city); // Return widget with runtime data 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:
on tool definitionwidget: { name, invoking, invoked }
helper returns runtime datawidget({ props, output })
passed to widget,props
shown to modeloutput- Widget must exist in
folderresources/
Static Assets
Use the
public/ folder for images, fonts, etc:
my-app/ ├── resources/ ├── public/ # Static assets │ ├── 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'; function MyWidget() { return ( <div> <Image src="/images/photo.jpg" alt="Photo" /> <Image src="data:image/png;base64,..." alt="Data URL" /> </div> ); }
ErrorBoundary
Graceful error handling:
import { ErrorBoundary } from 'mcp-use/react'; function MyWidget() { return ( <ErrorBoundary fallback={<div>Something went wrong</div>} onError={(error) => console.error(error)} > <MyComponent /> </ErrorBoundary> ); }
Testing
Using the Inspector
-
Start development server:
yarn dev -
Open Inspector:
- Navigate to
http://localhost:3000/inspector
- Navigate to
-
Test widgets:
- Click Tools tab
- Find your widget tool
- Enter test parameters
- Execute to see widget render
-
Debug interactions:
- Use browser console
- Check RPC logs
- Test state persistence
- Verify tool calls
Testing in ChatGPT
-
Enable Developer Mode:
- Settings → Connectors → Advanced → Developer mode
-
Add your server:
- Go to Connectors tab
- Add remote MCP server URL
-
Test in conversation:
- Select Developer Mode from Plus menu
- Choose your connector
- Ask ChatGPT to use your 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' }"
Best Practices
Schema Design
Use descriptive schemas:
// ✅ Good 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 const schema = z.object({ city: z.string(), temp: z.number(), });
Theme Support
Always support both themes:
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 />; } // Now safe to access props.field return <div>{props.field}</div>;
Widget Focus
Keep widgets focused:
// ✅ 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
Handle errors gracefully:
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
Set base URL for production:
const server = new MCPServer({ name: 'my-app', version: '1.0.0', baseUrl: process.env.MCP_URL || 'https://myserver.com' });
Environment Variables
# Server URL MCP_URL=https://myserver.com # For static deployments MCP_SERVER_URL=https://myserver.com/api CSP_URLS=https://cdn.example.com,https://api.example.com
Variable usage:
: Base URL for widget assets and CSPMCP_URL
: MCP server URL for tool calls (static deployments)MCP_SERVER_URL
: Additional domains for Content Security PolicyCSP_URLS
Deployment
Deploy to mcp-use Cloud
# Login npx mcp-use login # Deploy yarn deploy
Build for Production
# Build yarn build # Start yarn start
Build process:
- Compiles TypeScript
- Bundles React widgets
- Optimizes assets
- Generates production HTML
Common Patterns
Data Fetching Widget
const DataWidget: React.FC = () => { const { props, isPending, callTool } = useWidget(); if (isPending) { return <div>Loading...</div>; } const refresh = async () => { await callTool('fetch-data', { id: props.id }); }; return ( <div> <h1>{props.title}</h1> <button onClick={refresh}>Refresh</button> </div> ); };
Stateful Widget
const CounterWidget: React.FC = () => { const { state, setState } = useWidget(); const increment = async () => { await setState({ count: (state?.count || 0) + 1 }); }; return ( <div> <p>Count: {state?.count || 0}</p> <button onClick={increment}>+1</button> </div> ); };
Themed Widget
const ThemedWidget: React.FC = () => { const { theme } = useWidget(); return ( <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}> Content </div> ); };
Troubleshooting
Widget Not Appearing
Problem: Widget file exists but tool doesn't appear
Solutions:
- Ensure
extension.tsx - Export
objectwidgetMetadata - Export default React component
- Check server logs for errors
- Verify widget name matches file/folder name
Props Not Received
Problem: Component receives empty props
Solutions:
- 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
Problem: Widget loads but assets fail
Solutions:
- Set
in server configbaseUrl - Add domains to CSP via
appsSdkMetadata - Use HTTPS for all resources
- Check browser console for CSP violations
Learn More
- Documentation: https://docs.mcp-use.com
- Widget Guide: https://docs.mcp-use.com/typescript/server/ui-widgets
- Apps SDK Tutorial: https://docs.mcp-use.com/typescript/server/creating-apps-sdk-server
- 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
Quick Reference
Commands:
- Bootstrapnpx create-mcp-use-app my-app --template apps-sdk
- Development with hot reloadyarn dev
- Build for productionyarn build
- Run production serveryarn start
- Deploy to mcp-use Cloudyarn deploy
Widget structure:
- Single file widgetresources/widget-name.tsx
- Folder-based widget entryresources/widget-name/widget.tsx
- Static assetspublic/
Widget metadata:
- Widget descriptiondescription
- Zod schema for inputprops
- Auto-register as tool (default: true)exposeAsTool
- Apps SDK configurationappsSdkMetadata
useWidget hook:
- Widget input parametersprops
- Loading state flagisPending
- Persistent statestate, setState
- Call other toolscallTool
- Current theme (light/dark)theme
- Display controldisplayMode, requestDisplayMode