Skip to main content
The useWidget hook provides a universal, protocol-agnostic interface for building widgets that work with both MCP Apps (SEP-1865) and ChatGPT Apps SDK protocols. It automatically detects the environment and provides a consistent API regardless of which protocol is being used.
Protocol Detection: The hook automatically detects whether it’s running in:
  • MCP Apps environment (JSON-RPC over postMessage)
  • ChatGPT Apps SDK environment (window.openai API)
Your widget code stays identical across both protocols!

Import

import { useWidget } from "mcp-use/react";

Basic Usage

import { useWidget } from "mcp-use/react";

interface MyWidgetProps {
  city: string;
  temperature: number;
}

const MyWidget: React.FC = () => {
  // Works identically in both MCP Apps and ChatGPT
  const { props, theme, callTool } = useWidget<MyWidgetProps>();

  return (
    <div data-theme={theme}>
      <h1>{props.city}</h1>
      <p>{props.temperature}°C</p>
    </div>
  );
};
Cross-Protocol Compatibility: This exact code works without modifications in:
  • ChatGPT (via Apps SDK protocol)
  • Claude Desktop (via MCP Apps protocol)
  • Any MCP Apps-compatible client
The hook handles all protocol-specific communication behind the scenes.

Type Parameters

The hook accepts four optional type parameters:
useWidget<
  TProps, // Props type (from toolInput)
  TOutput, // Output type (from toolOutput/structuredContent)
  TMetadata, // Metadata type (from toolResponseMetadata)
  TState // State type (for widgetState)
>();

Return Values

Props and State

PropertyTypeDescription
propsPartial<TProps>Widget props (mapped from widget-only data). Empty {} when isPending is true
outputTOutput | nullTool output from the last execution
metadataTMetadata | nullResponse metadata from the tool
stateTState | nullPersisted widget state
setState(state: TState | ((prev: TState | null) => TState)) => Promise<void>Update widget state (persisted and shown to model)

Layout and Theme

PropertyTypeDescription
theme"light" | "dark"Current theme (auto-syncs with ChatGPT)
displayMode"inline" | "pip" | "fullscreen"Current display mode
safeAreaSafeAreaSafe area insets for mobile layout
maxHeightnumberMaximum height available (pixels)
userAgentUserAgentDevice capabilities (device, capabilities)
localestringCurrent locale (e.g., "en-US")
mcp_urlstringMCP server base URL for making API requests

Actions

MethodSignatureDescription
callTool(name: string, args: Record<string, unknown>) => Promise<CallToolResponse>Call a tool on the MCP server
sendFollowUpMessage(prompt: string) => Promise<void>Send a follow-up message to the ChatGPT conversation
openExternal(href: string) => voidOpen an external URL in a new tab
requestDisplayMode(mode: DisplayMode) => Promise<{ mode: DisplayMode }>Request a different display mode
notifyIntrinsicHeight(height: number) => Promise<void>Notify OpenAI about intrinsic height changes for auto-sizing

Availability

PropertyTypeDescription
isAvailablebooleanWhether the window.openai API is available
isPendingbooleanWhether the tool is currently executing. When true, props will be empty {}

Complete Example

import { useWidget } from "mcp-use/react";

interface ProductProps {
  productId: string;
  name: string;
  price: number;
}

interface ProductOutput {
  reviews: Array<{ rating: number; comment: string }>;
}

interface ProductState {
  favorites: string[];
}

const ProductWidget: React.FC = () => {
  const {
    // Props and state
    props,
    output,
    state,
    setState,

    // Layout & theme
    theme,
    displayMode,
    safeArea,

    // Actions
    callTool,
    sendFollowUpMessage,
    openExternal,
    requestDisplayMode,
    notifyIntrinsicHeight,

    // Availability
    isAvailable,
  } = useWidget<ProductProps, ProductOutput, {}, ProductState>();

  const handleAddToFavorites = async () => {
    const newFavorites = [...(state?.favorites || []), props.productId];
    await setState({ favorites: newFavorites });
  };

  const handleGetReviews = async () => {
    const result = await callTool("get-product-reviews", {
      productId: props.productId,
    });
    // Handle result
  };

  return (
    <div data-theme={theme}>
      <h1>{props.name}</h1>
      <p>${props.price}</p>
      <button onClick={handleAddToFavorites}>Add to Favorites</button>
      <button onClick={handleGetReviews}>Get Reviews</button>
    </div>
  );
};

Widget Lifecycle

Widgets render before the tool execution completes. This means:
  1. First render (isPending = true):
    • Widget mounts immediately when tool is called
    • props is {} (empty object)
    • output and metadata are null
    • This allows showing loading states
  2. After tool completes (isPending = false):
    • props contains the actual widget data
    • output and metadata are available
    • Widget re-renders with full data
Example:
const MyWidget: React.FC = () => {
  const { props, isPending } = useWidget<MyWidgetProps>();

  if (isPending) {
    return <LoadingSpinner />;
  }

  // Safe to access props now
  return (
    <div>
      {props.city} - {props.temperature}°C
    </div>
  );
};
Alternative: Using optional chaining
const MyWidget: React.FC = () => {
  const { props, isPending } = useWidget<MyWidgetProps>();

  return (
    <div>
      {isPending ? (
        <LoadingSpinner />
      ) : (
        <div>
          {props.city} - {props.temperature}°C
        </div>
      )}
    </div>
  );
};
Testing Widget Lifecycle: The mcp-use Inspector fully supports widget lifecycle testing. You can verify isPending transitions by executing tools in the Inspector and watching console logs. See Debugging Widgets for testing details.

Helper Hooks

For convenience, there are specialized hooks for common use cases:

useWidgetProps

Get only the widget props:
import { useWidgetProps } from "mcp-use/react";

const props = useWidgetProps<{ city: string; temperature: number }>();
// { city: "Paris", temperature: 22 }

useWidgetTheme

Get only the theme:
import { useWidgetTheme } from "mcp-use/react";

const theme = useWidgetTheme(); // 'light' | 'dark'

useWidgetState

Get state management:
import { useWidgetState } from "mcp-use/react";

const [favorites, setFavorites] = useWidgetState<string[]>([]);

// Update state
await setFavorites(["item1", "item2"]);

// Or use functional update
await setFavorites((prev) => [...prev, "newItem"]);

Key Features

1. Props Without Props

Components don’t accept props via React props. Instead, props come from the hook:
// ❌ Don't do this
const MyWidget: React.FC<MyProps> = ({ city, temperature }) => { ... }

// ✅ Do this
const MyWidget: React.FC = () => {
  const { props } = useWidget<MyProps>();
  const { city, temperature } = props;
  // ...
}

2. Automatic Provider Detection

The hook automatically detects whether it’s running in:
  • Apps SDK (ChatGPT): Reads from window.openai
  • MCP-UI: Reads from URL parameters
  • Standalone: Uses default props

3. Reactive Updates

The hook subscribes to all window.openai global changes via the openai:set_globals event, ensuring your component re-renders when:
  • Theme changes
  • Display mode changes
  • Widget state updates
  • Tool input/output changes

4. Auto-sizing Support

Use notifyIntrinsicHeight to notify OpenAI about height changes:
const { notifyIntrinsicHeight } = useWidget();

useEffect(() => {
  const height = containerRef.current?.scrollHeight || 0;
  notifyIntrinsicHeight(height);
}, [content]);
Or use McpUseProvider with autoSize={true} for automatic height notifications.

5. State Management

Widget state persists across widget interactions and is shown to the model:
const { state, setState } = useWidget<{}, {}, {}, { favorites: string[] }>();

// Update state
await setState({ favorites: ["item1", "item2"] });

// Functional update
await setState((prev) => ({
  favorites: [...(prev?.favorites || []), "newItem"],
}));

6. Tool Calls

Call other MCP tools from your widget:
const { callTool } = useWidget();

const handleSearch = async () => {
  const result = await callTool("search-products", {
    query: "laptop",
  });
  // Handle result
};

7. Follow-up Messages

Send messages to the ChatGPT conversation:
const { sendFollowUpMessage } = useWidget();

const handleRequestInfo = async () => {
  await sendFollowUpMessage("Show me more details about this product");
};

8. Display Mode Control

Request display mode changes:
const { requestDisplayMode } = useWidget();

const handleFullscreen = async () => {
  const result = await requestDisplayMode("fullscreen");
  // result.mode is the granted mode (may differ from requested)
};

Default Values

The hook provides safe defaults when values are not available:
  • theme: "light"
  • displayMode: "inline"
  • safeArea: { insets: { top: 0, bottom: 0, left: 0, right: 0 } }
  • maxHeight: 600
  • userAgent: { device: { type: "desktop" }, capabilities: { hover: true, touch: false } }
  • locale: "en"
  • props: {} (or defaultProps if provided)
  • output: null
  • metadata: null
  • state: null

Error Handling

The hook throws errors when methods are called but the API is not available:
const { callTool, isAvailable } = useWidget();

const handleAction = async () => {
  if (!isAvailable) {
    console.warn("Widget API not available");
    return;
  }

  try {
    await callTool("my-tool", {});
  } catch (error) {
    console.error("Tool call failed:", error);
  }
};