Skip to main content
UI widgets allow you to create rich, interactive user interfaces that MCP clients can display. mcp-use supports multiple widget formats including OpenAI Apps SDK (this page) and MCP-UI.

Quick Start

Start with the Apps SDK template which includes automatic widget registration:
npx create-mcp-use-app my-mcp-server --template apps-sdk
cd my-mcp-server
npm install
npm run dev
This creates a project with:
my-mcp-server/
├── resources/                      # 👈 Put your UI widgets here
│   ├── product-search-result/      # Folder-based widget example
│   │   ├── widget.tsx              # Widget entry point
│   │   ├── components/             # Reusable components
│   │   ├── hooks/                  # Custom hooks
│   │   ├── constants.ts
│   │   └── types.ts
│   └── styles.css                  # Global widget styles
├── public/                         # Static assets
│   └── fruits/                     # Example images
├── index.ts                        # Server entry point
├── package.json
├── tsconfig.json
└── README.md

Folder Structure

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. For simple widgets, a single file is sufficient:
resources/
├── user-card.tsx          # Widget file
├── weather-display.tsx    # Another widget
└── product-card.tsx       # Yet another widget
Each .tsx file in the resources/ folder becomes a widget. The widget name is derived from the filename (without extension). For complex widgets with multiple components, hooks, or utilities, organize them in folders:
resources/
├── widget-name.tsx                    # Single-file widget
└── product-search-result/             # Folder-based widget
    ├── widget.tsx                     # Entry point (required)
    ├── components/                    # Sub-components
    │   ├── Accordion.tsx
    │   ├── AccordionItem.tsx
    │   ├── Carousel.tsx
    │   └── CarouselItem.tsx
    ├── hooks/                         # Custom hooks
    │   └── useCarouselAnimation.ts
    ├── constants.ts                   # Constants
    └── types.ts                       # Type definitions
Key Points:
  • The folder name becomes the widget name (e.g., product-search-result)
  • The entry point must be named widget.tsx (not index.tsx)
  • You can organize sub-components, hooks, utilities, and types within the folder
  • The widget.tsx file should export widgetMetadata and the default component
Example Widget:
// resources/product-search-result/widget.tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from 'mcp-use/react';
import { Accordion } from './components/Accordion';
import { Carousel } from './components/Carousel';
import type { ProductSearchResultProps } from './types';
import { propSchema } from './types';

export const widgetMetadata: WidgetMetadata = {
  description: 'Display product search results with filtering',
  props: propSchema,
};

const ProductSearchResult: React.FC = () => {
  const { props } = useWidget<ProductSearchResultProps>();
  
  return (
    <McpUseProvider autoSize>
      <div>
        <Carousel />
        <Accordion items={props.items} />
      </div>
    </McpUseProvider>
  );
};

export default ProductSearchResult;

Widget Metadata

Contains the information that the MCP resource (and the tool that exposes it) will use when are automatically built by mcp-use.
import type { WidgetMetadata } from 'mcp-use/react';

export const widgetMetadata: WidgetMetadata = {
  // Required: Human-readable description
  description: string,
  
  // Required: Zod schema defining the widget's input parameters
  props: z.ZodObject<...>,
  
  // Optional: Control automatic tool registration (defaults to true)
  exposeAsTool?: boolean,
    
  // Optional: Apps SDK metadata (CSP, widget description, etc.)
  appsSdkMetadata?: {
    'openai/widgetDescription'?: string,
    'openai/widgetCSP'?: {
      connect_domains?: string[],
      resource_domains?: string[],
    },
    'openai/toolInvocation/invoking'?: string,
    'openai/toolInvocation/invoked'?: string,
    'openai/widgetAccessible'?: boolean,
    'openai/resultCanProduceWidget'?: boolean,
  },
}
The inputs field expects a Zod schema that defines the shape of the parameters passed to your widget. This schema is used to generate the tool’s input validation and to type-check the props in your widget component via useWidget<T>().
Widget Exposure Modes: By default, widgets are automatically registered as both MCP tools and resources (exposeAsTool: true). You can control this behavior:
  • Auto-registered (default): Widget is callable as a tool directly by the model
    export const widgetMetadata: WidgetMetadata = {
      description: 'Display weather',
      props: propSchema,
      // exposeAsTool: true (default, can be omitted)
    };
    
  • Custom tool only: Widget is only accessible through custom tools you define
    export const widgetMetadata: WidgetMetadata = {
      description: 'Display weather',
      props: propSchema,
      exposeAsTool: false  // Don't auto-register as tool
    };
    
Note: The CSP domains you specify will be merged with default trusted domains (like *.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.

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

ComponentDescriptionLink
McpUseProviderUnified provider that combines all common React setup (StrictMode, ThemeProvider, BrowserRouter, WidgetControls, ErrorBoundary)McpUseProvider →
WidgetControlsDebug button and view controls (fullscreen/pip) with customizable positioningWidgetControls →
ErrorBoundaryError boundary component for graceful error handling in widgetsErrorBoundary →
ImageImage component that handles both data URLs and public file pathsImage →
ThemeProviderTheme provider for consistent theme management across widgetsThemeProvider →

Hooks

HookDescriptionLink
useWidgetMain hook providing type-safe access to all widget capabilities (props, state, theme, actions)useWidget →

Static Assets

Widgets can use static assets from a public/ folder. The framework automatically serves these assets and copies them during build. Folder Structure
my-mcp-server/
├── resources/
│   └── product-widget.tsx
├── public/                    # Static assets
│   ├── fruits/
│   │   ├── apple.png
│   │   ├── banana.png
│   │   └── orange.png
│   └── logo.svg
└── index.ts
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:
import { Image } from 'mcp-use/react';

function ProductWidget() {
  return (
    <div>
      {/* Paths are relative to public/ folder */}
      <Image src="/fruits/apple.png" alt="Apple" />
      <Image src="/logo.svg" alt="Logo" />
    </div>
  );
}
Direct URL Access: The framework provides utilities for accessing public files:
  • window.__mcpPublicUrl: Base URL for public assets (e.g., http://localhost:3000/mcp-use/public)
  • window.__getFile: Helper function to get file URLs
// Get public URL
const publicUrl = window.__mcpPublicUrl; // "http://localhost:3000/mcp-use/public"
const imageUrl = `${publicUrl}/fruits/apple.png`;

// Or use the helper
const imageUrl = window.__getFile?.('fruits/apple.png');

Patterns

Accessing Tool Input

Widgets are called by the model in the same way as tools. The useWidget hook provides access to the tool input, typed based on widget metadata schmea you defined.
const { props } = useWidget();
Example:
export const widgetMetadata: WidgetMetadata = {
  description: 'Display product search results with filtering',
  props: z.object({
    query: z.string(),
  }),
};

const ProductSearchResult: React.FC = () => {
  const { props } = useWidget();
  return <div>Query: {props.query}</div>;
};

Widget State

Widgets can maintain state across interactions. State is persisted by the host, for example in ChatGPT:
const { state, setState } = useWidget();

// Save state (persists to localStorage)
await setState({ favoriteCity: 'Tokyo', filters: { price: '$$' } });

// State persists across widget re-renders and page reloads
console.log(state?.favoriteCity); // 'Tokyo'

// Update state with function (like React useState)
await setState(prev => ({
  ...prev,
  favoriteCity: 'Paris'
}));

Calling Other Tools

Widgets can call other MCP tools directly using callTool:
const { callTool } = useWidget();

const handleSearch = async () => {
  try {
    const result = await callTool('search_cities', { 
      query: 'tokyo' 
    });
    // result.content contains the tool response
    console.log(result.content);
  } catch (error) {
    console.error('Tool call failed:', error);
  }
};
Return Format:
const result: CallToolResponse = {
  content: [
    { type: 'text', text: '...' },
    // ... other content items
  ],
  isError?: boolean
};

Display Mode Control

Request different display modes (inline, pip, or fullscreen):
const { requestDisplayMode, displayMode } = useWidget();

const handleExpand = async () => {
  const result = await requestDisplayMode('fullscreen');
  // result.mode = 'fullscreen' (may be different if request denied)
  console.log('Display mode:', result.mode);
};

// Current display mode
console.log(displayMode); // 'inline' | 'pip' | 'fullscreen'
Display Modes:
  • 'inline' - Default embedded view in conversation
  • 'pip' - Picture-in-Picture floating window
  • 'fullscreen' - Full browser window (on mobile, PiP coerces to fullscreen)
You can user the <WidgetControls /> to automatically add controls to your widget.
import { WidgetControls } from 'mcp-use/react';

function MyWidget() {
  return (
    <McpUseProvider viewControls>
      <div>My widget content</div>
    </McpUseProvider>
  );
}
or
import { WidgetControls } from 'mcp-use/react';

function MyWidget() {
  return (
    <WidgetControls>
      <div>My widget content</div>
    </WidgetControls>
  );
}

Custom Tools with Widgets

You can create custom tools that return widgets instead of relying solely on automatic widget registration. This is useful when you need to:
  • Fetch data before displaying the widget
  • Use different tool parameters than widget props
  • Have multiple tools use the same widget
  • Add custom logic or validation

Using the widget() Helper

The widget() helper returns runtime data for a widget. You must combine it with the widget config on the tool definition to set up all registration-time metadata. Important: The widget configuration is split between two places:
  1. Tool definition (widget: { name, invoking, ... }) - Registration-time metadata
  2. Helper return (widget({ props, output, ... })) - Runtime data
import { widget, text } from 'mcp-use/server';
import { z } from 'zod';

server.tool({
  name: 'get-weather',
  description: 'Get current weather for a city',
  schema: z.object({
    city: z.string().describe('City name')
  }),
  // Widget config sets all registration-time metadata
  widget: {
    name: 'weather-display',      // Must match a widget in resources/
    invoking: 'Fetching weather data...',
    invoked: 'Weather data loaded'
  }
}, async ({ city }) => {
  // Fetch weather data from API
  const weatherData = await fetchWeatherAPI(city);
  
  // Return widget with runtime data only
  return widget({
    props: {
      city,
      temperature: weatherData.temp,
      conditions: weatherData.conditions,
      humidity: weatherData.humidity,
      windSpeed: weatherData.windSpeed
    },
    output: text(`Current weather in ${city}: ${weatherData.temp}°C, ${weatherData.conditions}`),
    message: `Current weather in ${city}`
  });
});
Key Points:
  • widget: { name, invoking, invoked, ... } on tool definition - Configures all widget metadata at registration time
  • widget({ props, output, message }) helper - Returns runtime data only:
    • props - Widget-only data passed to useWidget().props (hidden from model)
    • output - Optional response helper result (text(), object(), etc.) that the model sees
    • message - Optional text message override
  • Widget must exist - The widget name must match a .tsx file or folder in resources/
  • Disable auto-registration - Set exposeAsTool: false in the widget’s metadata if you only want it accessible through custom tools:
// resources/weather-display/widget.tsx
export const widgetMetadata: WidgetMetadata = {
  description: 'Display weather information',
  props: propSchema,
  exposeAsTool: false  // Only accessible via custom tools
};
See Tools Guide for more information about the widget config option.

Configuration

Base URL for Production

Set the MCP_URL environment variable or pass baseUrl:
const server = new MCPServer({
  name: 'my-server',
  version: '1.0.0',
  baseUrl: process.env.MCP_URL || 'https://myserver.com'
});
This ensures:
  • Widget URLs use the correct domain
  • Apps SDK CSP automatically includes your server
Preferably set the variable at build time to have statically generated widget assets paths. If you don’t set it at build time you must set it at runtime either by passing the baseUrl option to the MCPServer constructor or by setting the MCP_URL environment variable.

Environment Variables

# Server Configuration
MCP_URL=https://myserver.com

# For Static Deployments (e.g., Supabase)
MCP_SERVER_URL=https://myserver.com/functions/v1/my-function
CSP_URLS=https://myserver.com,...other domains
Environment Variable Details:
  • MCP_URL: Base URL for widget assets and public files. Used by Vite’s base option 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).

Testing

Using the Inspector

The MCP Inspector provides full support for testing widgets during development:
  1. Start your server: npm run dev
  2. Open Inspector: http://localhost:3000/inspector
  3. Test widgets: Execute tools to see widgets render
  4. Debug interactions: Use console logs and inspector features
  5. Test API methods: Verify callTool, setState, etc. work correctly
See Debugging ChatGPT Apps for complete testing guide.

Testing in ChatGPT

You need to enable the Developer Mode in ChatGPT to test widgets.
  • Enable developer mode: Go to Settings → Connectors → Advanced → Developer mode.
  • Import MCPs: In the Connectors tab, add your remote MCP server. It will appear in the composer’s “Developer Mode” tool later during conversations.
  • Use connectors in conversations: Choose Developer mode from the Plus menu and select connectors. You may need to explore different prompting techniques to call the correct tools. For example:
    • Be explicit: “Use the “Acme CRM” connector’s “update_record” tool to …”. When needed, include the server label and tool name.
    • Disallow alternatives to avoid ambiguity: “Do not use built-in browsing or other tools; only use the Acme CRM connector.”
    • Disambiguate similar tools: “Prefer Calendar.create_event for meetings; do not use Reminders.create_task for scheduling.”
    • Specify input shape and sequencing: “First call Repo.read_file with { path: ”…” }. Then call Repo.write_file with the modified content. Do not call other tools.”
    • If multiple connectors overlap, state preferences up front (e.g., “Use CompanyDB for authoritative data; use other sources only if CompanyDB returns no results”).
    • Developer mode does not require search/fetch tools. Any tools your connector exposes (including write actions) are available, subject to confirmation settings.
    • See more guidance in Using tools and Prompting.
    • Improve tool selection with better tool descriptions: In your MCP server, write action-oriented tool names and descriptions that include “Use this when…” guidance, note disallowed/edge cases, and add parameter descriptions (and enums) to help the model choose the right tool among similar ones and avoid built-in tools when inappropriate.

Next Steps