UI Widgets
UI widgets allow you to create rich, interactive user interfaces that MCP clients can display. The framework supports multiple widget formats including MCP-UI and OpenAI Apps SDK components.
Automatic Widget Registration
Themcp-use framework automatically registers all React components in your resources/ folder as MCP tools and resources, eliminating the need for manual configuration and boilerplate code. This powerful feature allows you to focus on building UI components while the framework handles the MCP integration.
Overview
When you create a React component in theresources/ folder with proper metadata, mcp-use automatically:
- Registers an MCP Tool - LLMs can invoke it with parameters
- Registers an MCP Resource - Clients can discover and access it
- Extracts Input Schema - Tool parameters are derived from component props
- Serves with HMR - Hot Module Replacement for rapid development
- Builds for Production - Optimized Apps SDK widgets
- Handles Props Mapping - From LLM → Tool arguments → Apps SDK → React component
Quick Start
1. Create a Project
Start with the Apps SDK template which includes automatic widget registration:2. Create a Widget
Widget Organization
Widgets can be organized in two ways: single-file widgets or folder-based widgets. Choose the organization style that best fits your widget’s complexity.Single-File Widgets
For simple widgets, a single file is sufficient:.tsx file in the resources/ folder becomes a widget. The widget name is derived from the filename (without extension).
Folder-Based Widgets
For complex widgets with multiple components, hooks, or utilities, organize them in folders:- The folder name becomes the widget name (e.g.,
product-search-result) - The entry point must be named
widget.tsx(notindex.tsx) - You can organize sub-components, hooks, utilities, and types within the folder
- The
widget.tsxfile should exportwidgetMetadataand the default component
When to Use Each Approach
Use Single-File Widgets When:- Widget is simple and self-contained
- No need for sub-components or custom hooks
- Quick prototypes or simple displays
- Widget has multiple components
- You need custom hooks or utilities
- Widget is complex and benefits from organization
- You want to share code between components
- Widget needs constants, types, or configuration files
Widget Name Resolution
- Single-file:
user-card.tsx→ widget name:user-card - Folder-based:
product-search-result/widget.tsx→ widget name:product-search-result
- MCP tool registration (
callTool('product-search-result', {...})) - MCP resource URI (
ui://widget/product-search-result) - Widget discovery and routing
2. Create a Widget
Add a new file inresources/, for example resources/user-card.tsx:
3. That’s It!
The widget is automatically:- Registered as tool:
user-card - Available as resource:
ui://widget/user-card.html - Ready in ChatGPT: LLM can call it with name, email, avatar, role
server.tool() or server.uiResource() calls needed!
How It Works
The Flow
1. Widget Discovery
When you callserver.listen(), the framework automatically calls mountWidgets() which:
- Scans the
resources/directory for:- Single-file widgets:
.tsxand.tsfiles directly inresources/ - Folder-based widgets: Folders containing
widget.tsxentry point
- Single-file widgets:
- Creates a Vite dev server for each widget with HMR support
- Loads widget metadata using Vite SSR
- Widget names are derived from:
- Filename (without extension) for single-file widgets
- Folder name for folder-based widgets
mcp-use build or npm run build:
- Widgets are discovered using the same logic
- Public folder (
public/) is automatically copied todist/public/ - Widget metadata is extracted and included in the build manifest
- Production-optimized builds are created for each widget
2. Metadata Extraction
The framework looks for thewidgetMetadata export in each widget file:
inputs Zod schema defines:
- What props the component needs
- What parameters the tool accepts
- Type validation and descriptions for the LLM
3. Tool Registration
The framework automatically registers a tool with:- Name: Derived from filename (single-file) or folder name (folder-based)
- Single-file:
display-weather.tsx→display-weather - Folder-based:
product-search-result/widget.tsx→product-search-result
- Single-file:
- Description: From
widgetMetadata.description - Inputs: Converted from Zod schema to MCP input definitions
- Callback: Returns the widget resource with a unique URI
4. Resource Registration
The framework registers both:- Static Resource:
ui://widget/{name}.html- Base resource - Dynamic Template:
ui://widget/{name}-{id}.html- Unique per invocation
- HTML Template: With proper script tags and styles
- Apps SDK Metadata: CSP configuration, widget description, etc.
- MIME Type:
text/html+skybridgefor Apps SDK compatibility
5. Props Flow
When an LLM invokes the tool:- LLM fills tool parameters based on context and inputs schema
- MCP Server returns widget resource URI +
structuredContentwith params - Apps SDK (ChatGPT) loads the widget HTML
- Apps SDK injects params as
window.openai.toolInput - useWidget Hook reads from
window.openaiand provides props to React component - React Component renders with the props
Widget Metadata
widgetMetadata Object
Contains the information that the MCP resource (and the tool that exposes it) will use when are automatically built by mcp-use.Props Schema with Zod
Theinputs field uses Zod for:
- Type Safety: TypeScript inference for props
- Runtime Validation: Ensure correct data types
- LLM Context:
.describe()helps LLM understand what to fill
Apps SDK Metadata
Configure Apps SDK-specific settings including Content Security Policy (CSP) for external resources:*.oaistatic.com, *.oaiusercontent.com, *.openai.com, and your server’s base URL). This ensures your widget can access both your custom resources and OpenAI’s required domains.
The useWidget Hook
TheuseWidget hook is the bridge between Apps SDK and React, providing type-safe access to all widget capabilities:
Type Parameters
TheuseWidget hook accepts four optional type parameters:
Helper Hooks
For convenience, there are specialized hooks for common use cases:Key Features
1. Props Without Props Components don’t accept props via React props. Instead, props come from the hook:- Apps SDK (ChatGPT): Reads from
window.openai - MCP-UI: Reads from URL parameters
- Standalone: Uses default props
Components & Hooks
mcp-use provides a comprehensive set of React components and hooks for building OpenAI Apps SDK widgets. These components handle common setup tasks like theme management, error handling, routing, and debugging.Components
| Component | Description | Link |
|---|---|---|
| McpUseProvider | Unified provider that combines all common React setup (StrictMode, ThemeProvider, BrowserRouter, WidgetControls, ErrorBoundary) | McpUseProvider → |
| WidgetControls | Debug button and view controls (fullscreen/pip) with customizable positioning | WidgetControls → |
| ErrorBoundary | Error boundary component for graceful error handling in widgets | ErrorBoundary → |
| Image | Image component that handles both data URLs and public file paths | Image → |
| ThemeProvider | Theme provider for consistent theme management across widgets | ThemeProvider → |
Hooks
| Hook | Description | Link |
|---|---|---|
| useWidget | Main hook providing type-safe access to all widget capabilities (props, state, theme, actions) | useWidget → |
| useWidgetProps | Get only the widget props | useWidget → |
| useWidgetTheme | Get only the theme value | useWidget → |
| useWidgetState | Get state management (state and setState) | useWidget → |
Quick Start with Components
The recommended way to set up a widget is usingMcpUseProvider, which includes everything you need:
- ✅ Theme management (syncs with ChatGPT)
- ✅ Error handling (graceful failures)
- ✅ Routing support (React Router)
- ✅ Debug controls (optional)
- ✅ View controls (fullscreen/pip, optional)
- ✅ Auto-sizing (optional, for dynamic content)
Public Folder Support
Widgets can use static assets from apublic/ folder. The framework automatically serves these assets and copies them during build.
Folder Structure
Using Public Assets
In development, assets are served from/mcp-use/public/. In production, they’re copied to dist/public/ during build.
Using the Image Component:
window.__mcpPublicUrl: Base URL for public assets (e.g.,http://localhost:3000/mcp-use/public)window.__getFile: Helper function to get file URLs
Build-Time Copying
Duringnpm run build, the CLI automatically:
- Copies the
public/folder todist/public/ - Preserves the folder structure
- Makes assets available in production builds
Best Practices
- Use the
Imagecomponent for images (handles URL resolution automatically) - Keep assets organized in subfolders (e.g.,
public/images/,public/icons/) - Use relative paths starting with
/(e.g.,/fruits/apple.png) - The
Imagecomponent handles both data URLs and public file paths
Complete Example: Weather Widget
Here’s a complete widget example usingMcpUseProvider:
Testing in ChatGPT
Once your server is running, you can test in ChatGPT: User: “Show me the weather in Paris. It’s 22 degrees and sunny.” ChatGPT (behind the scenes):- Recognizes it should use
display-weathertool - Fills parameters:
{ city: "Paris", weather: "sunny", temperature: 22 } - Calls the tool
- Receives widget resource URI
- Loads and displays the widget
Advanced Features
Accessing Tool Output
Widgets can access the output of their own tool execution viastructuredContent:
Persistent Widget State
Widgets can maintain state across interactions. State is persisted in localStorage and visible to ChatGPT:Widget state is scoped to the widget instance and conversation message. State is sent to ChatGPT and can influence model behavior, so keep it under 4k tokens for performance. See window.openai API Emulation for details.
Calling Other Tools
Widgets can call other MCP tools directly usingcallTool:
Display Mode Control
Request different display modes (inline, pip, or fullscreen):'inline'- Default embedded view in conversation'pip'- Picture-in-Picture floating window'fullscreen'- Full browser window (on mobile, PiP coerces to fullscreen)
Development Workflow
1. Start Dev Server
- MCP server with auto-widget registration
- Vite dev server with HMR for each widget
- Inspector UI for testing
2. Create Widget
Add a new file inresources/:
3. Test in Inspector
- Open
http://localhost:3000/inspector - Navigate to Tools
- Find
my-new-widgettool (automatically registered!) - Test with parameters:
{ "title": "Hello" } - See the widget render
4. Edit with Hot Reload
Edit your widget file and save. Changes appear instantly thanks to HMR!5. Build for Production
Under the Hood
Server Initialization
In yourindex.ts:
mountWidgets() Implementation
ThemountWidgets() method (called automatically by listen()):
- Scans the
resources/directory for.tsxand.tsfiles - Creates Vite dev server with React and Tailwind plugins
- Generates entry files for each widget in
.mcp-use/temp directory - Serves widgets at
/mcp-use/widgets/{name}with HMR - Extracts metadata using Vite SSR module loading
- Registers both tool and resource for each widget
Widget URL Structure
Development:- Widget:
http://localhost:3000/mcp-use/widgets/display-weather - Assets:
http://localhost:3000/mcp-use/widgets/display-weather/assets/...
- Widget:
https://myserver.com/mcp-use/widgets/display-weather - Assets:
https://myserver.com/mcp-use/widgets/display-weather/assets/...
Apps SDK CSP Configuration
The framework automatically configures Content Security Policy for Apps SDK:Configuration
Custom Resources Directory
Base URL for Production
Set theMCP_URL environment variable or pass baseUrl:
- Widget URLs use the correct domain
- Apps SDK CSP automatically includes your server
- Works behind proxies and custom domains
Environment Variables
MCP_URL: Base URL for widget assets and public files. Used by Vite’sbaseoption during build. Also used by the server to configure CSP.MCP_SERVER_URL: (Optional) MCP server URL for API calls. When set, URLs are injected at build time for static deployments where widgets are served from storage rather than the MCP server.CSP_URLS: (Optional) Additional domains to whitelist in widget Content Security Policy. Supports comma-separated list. For Supabase, use the base project URL without path (e.g.,https://nnpumlykjksvxivhywwo.supabase.co). Required for static deployments where widget assets are served from different domains.
Static Deployments: Set
MCP_URL (for assets), MCP_SERVER_URL (for API calls), and CSP_URLS (for CSP whitelisting) when deploying to platforms like Supabase where widgets are served from static storage.Alternative CSP Configuration: Instead of using the global CSP_URLS environment variable, you can configure CSP per-widget in your widget’s appsSdkMetadata['openai/widgetCSP'] (see Apps SDK Metadata section above).Best Practices
1. Use Descriptive Schemas
Help the LLM understand your inputs:2. Provide Defaults
Make optional fields with sensible defaults:3. Keep Widgets Focused
Each widget should do one thing well:4. Theme Support
Always support both themes:5. Error Handling
Handle missing or invalid props gracefully:6. Performance
Keep widgets lightweight:- Minimize dependencies
- Lazy load heavy components
- Use React.memo for expensive renders
Troubleshooting
Widget Not Registered
Problem: Widget file exists but tool doesn’t appear Solutions:- Ensure file has
.tsxor.tsextension - Export
widgetMetadataobject - Export default React component
- Check server logs for errors during widget loading
Props Not Passed
Problem: Component receives empty props Solutions:- Use
useWidget()hook instead of React props - Ensure
widgetMetadata.inputsis a valid Zod schema - Check Apps SDK is injecting
window.openai.toolInput - Verify tool parameters match schema
HMR Not Working
Problem: Changes don’t reflect without restart Solutions:- Check Vite dev server is running
- Ensure you’re editing the source file in
resources/ - Clear browser cache
- Check console for Vite errors
CSP Errors
Problem: Widget loads but assets fail with CSP errors Solutions:- Set
baseUrlin server config - Add external domains to CSP configuration
- Check browser console for blocked domains
widgetMetadata):
server.uiResource()):
Comparison: Auto vs Manual
Auto Registration (Recommended)
- Zero boilerplate
- Type-safe props
- Automatic tool/resource registration
- Hot module replacement
- Standardized structure
- ⚠️ Less control over registration details
- ⚠️ Must follow naming conventions
Manual Registration
- Full control over configuration
- Custom tool behavior
- Can use raw HTML strings
- ❌ More boilerplate
- ❌ Manual schema duplication
- ❌ Prop mapping is manual
- ❌ No HMR out of the box
Testing Widgets
Using the Inspector
The MCP Inspector provides full support for testing widgets during development:- Start your server:
npm run dev - Open Inspector:
http://localhost:3000/inspector - Test widgets: Execute tools to see widgets render
- Debug interactions: Use console logs and inspector features
- Test API methods: Verify
callTool,setState, etc. work correctly
Testing in ChatGPT
- Configure your MCP server URL in ChatGPT settings
- Invoke tools that return widgets
- Verify widget rendering and interactions
- Test all API methods in production environment
Next Steps
- Apps SDK Resources - Apps SDK primitives and metadata
- Creating Apps SDK Server - Complete guide
- Debugging ChatGPT Apps - Test widgets with Inspector
- Project Templates - Explore available templates