
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.
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';
// 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:
description: Used for tool and resource descriptionsprops: Zod schema defines widget input parametersexposeAsTool: Set tofalseif 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 {
// 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:
widget: { name, invoking, invoked }on tool definitionwidget({ props, output })helper returns runtime datapropspassed to widget,outputshown 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
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:
MCP_URL: Base URL for widget assets and CSPMCP_SERVER_URL: MCP server URL for tool calls (static deployments)CSP_URLS: Additional domains for Content Security Policy
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
.tsxextension - Export
widgetMetadataobject - 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
isPendingfirst (props empty while pending) - Use
useWidget()hook (not React props) - Verify
widgetMetadata.propsis valid Zod schema - Check tool parameters match schema
CSP Errors
Problem: Widget loads but assets fail
Solutions:
- Set
baseUrlin server config - 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:
npx create-mcp-use-app my-app --template apps-sdk- Bootstrapyarn dev- Development with hot reloadyarn build- Build for productionyarn start- Run production serveryarn deploy- Deploy to mcp-use Cloud
Widget structure:
resources/widget-name.tsx- Single file widgetresources/widget-name/widget.tsx- Folder-based widget entrypublic/- Static assets
Widget metadata:
description- Widget descriptionprops- Zod schema for inputexposeAsTool- Auto-register as tool (default: true)appsSdkMetadata- Apps SDK configuration
useWidget hook:
props- Widget input parametersisPending- Loading state flagstate, setState- Persistent statecallTool- Call other toolstheme- Current theme (light/dark)displayMode, requestDisplayMode- Display control
You Might Also Like
Related Skills

mcporter
Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation.
openclaw
model-usage
Use CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
moltbot
strategic-compact
Suggests manual context compaction at logical intervals to preserve context through task phases rather than arbitrary auto-compaction.
affaan-m
skill-creation-guide
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
davila7
claude-code-guide
Claude Code 高级开发指南 - 全面的中文教程,涵盖工具使用、REPL 环境、开发工作流、MCP 集成、高级模式和最佳实践。适合学习 Claude Code 的高级功能和开发技巧。
2025Emma
filesystem-context
This skill should be used when the user asks to "offload context to files", "implement dynamic context discovery", "use filesystem for agent memory", "reduce context window bloat", or mentions file-based context management, tool output persistence, agent scratch pads, or just-in-time context loading.
muratcankoylan