Skip to main content Home Creators diegosouzapw awesome-omni-skill chatgpt-app-builder
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.
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/diegosouzapw/awesome-omni-skill --skill chatgpt-app-builderThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository Token-efficient tracking for AI orchestration. CLI-first for status updates (~50 tokens), agent fallback for complex ops (~1KB). Use when: updating task status, querying blockers, creating progress files, validating phases.
AshAi extension guidelines for integrating AI capabilities with Ash Framework. Use when implementing vectorization/embeddings, exposing Ash actions as LLM tools, creating prompt-backed actions, or setting up MCP servers. Covers semantic search, LangChain integration, and structured outputs.
This skill should be used when solving hard questions, complex architectural problems, or debugging issues that benefit from GPT-5 Pro or GPT-5.1 thinking models with large file context. Use when standard Claude analysis needs deeper reasoning or extended context windows.
Related occupations SOC
Based on SOC occupation classification
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 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 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
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 ;
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 = {
description : 'Display weather information' ,
props : z.object ({
city : z.string ().describe ('City name' ),
temperature : z.number (),
}),
exposeAsTool : true ,
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' ],
},
},
};
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
Default Apps SDK metadata is auto-generated if not specified
useWidget Hook The useWidget hook provides everything you need:
const {
props,
isPending,
state,
setState,
theme,
callTool,
displayMode,
requestDisplayMode,
output,
} = useWidget<MyPropsType , MyOutputType >();
Props and Loading States Critical: Widgets render BEFORE tool execution completes. Always handle isPending:
const { props, isPending } = useWidget<WeatherProps >();
if (isPending) {
return <div > Loading...</div > ;
}
return (
<div >
{isPending ? (
<LoadingSpinner />
) : (
<div > {props.city}</div >
)}
</div >
);
return (
<div >
<h1 > {props.city ?? 'Loading...'}</h1 >
</div >
);
Widget State Persist data across widget interactions:
const { state, setState } = useWidget ();
const addFavorite = async (city : string ) => {
await setState ({
favorites : [...(state?.favorites || []), city]
});
};
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' );
};
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 : {
name : 'weather-display' ,
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 ();
widget: { name, invoking, invoked } on tool definition
widget({ props, output }) helper returns runtime data
props passed to widget, output shown to model
Widget must exist in resources/ folder
Static Assets Use the public/ folder for images, fonts, etc:
my-app/
├── resources/
├── public/ # Static assets
│ ├── images/
│ │ ├── logo.svg
│ │ └── banner.png
│ └── fonts/
└── index.ts
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 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
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
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
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' ),
});
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 /> ;
}
return <div > {props.field}</div > ;
Widget Focus
export const widgetMetadata : WidgetMetadata = {
description : 'Display weather for a city' ,
props : z.object ({ city : z.string () }),
};
export const widgetMetadata : WidgetMetadata = {
description : 'Weather, forecast, map, news, and more' ,
props : z.object ({ }),
};
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
MCP_URL: Base URL for widget assets and CSP
MCP_SERVER_URL: MCP server URL for tool calls (static deployments)
CSP_URLS: Additional domains for Content Security Policy
Deployment
Deploy to mcp-use Cloud
npx mcp-use login
yarn deploy
Build for Production
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
Ensure .tsx extension
Export widgetMetadata object
Export default React component
Check server logs for errors
Verify widget name matches file/folder name
Props Not Received Problem: Component receives empty props
Check isPending first (props empty while pending)
Use useWidget() hook (not React props)
Verify widgetMetadata.props is valid Zod schema
Check tool parameters match schema
CSP Errors Problem: Widget loads but assets fail
Set baseUrl in server config
Add domains to CSP via appsSdkMetadata
Use HTTPS for all resources
Check browser console for CSP violations
Learn More
Quick Reference
npx create-mcp-use-app my-app --template apps-sdk - Bootstrap
yarn dev - Development with hot reload
yarn build - Build for production
yarn start - Run production server
yarn deploy - Deploy to mcp-use Cloud
resources/widget-name.tsx - Single file widget
resources/widget-name/widget.tsx - Folder-based widget entry
public/ - Static assets
description - Widget description
props - Zod schema for input
exposeAsTool - Auto-register as tool (default: true)
appsSdkMetadata - Apps SDK configuration
props - Widget input parameters
isPending - Loading state flag
state, setState - Persistent state
callTool - Call other tools
theme - Current theme (light/dark)
displayMode, requestDisplayMode - Display control