| 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=>Loading weather...
);
}
(
);
};
;
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>Loading...;
}
(
);
};
;
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'],
},
},
};
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
- 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
},
: (),
:
});
});
server.();
Key Points:
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
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
-
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:
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
Keep widgets focused:
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
Variable usage:
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
yarn build
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
.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
Solutions:
- 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
Solutions:
- 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
Commands:
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
Widget structure:
resources/widget-name.tsx - Single file widget
resources/widget-name/widget.tsx - Folder-based widget entry
public/ - Static assets
Widget metadata:
description - Widget description
props - Zod schema for input
exposeAsTool - Auto-register as tool (default: true)
appsSdkMetadata - Apps SDK configuration
useWidget hook:
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