---
title: "useWidget()"
description: "Universal React hook for MCP Apps and ChatGPT Apps SDK widgets"
icon: "webhook"
---

The `useWidget` hook provides a **universal, protocol-agnostic** interface for building widgets that work with **both** MCP Apps and ChatGPT Apps SDK protocols. It automatically detects the environment and provides a consistent API regardless of which protocol is being used.

<Info>
**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!

</Info>

## Import

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

## Basic Usage

```tsx
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>
  );
};
```

<Note>
**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.

</Note>

## Type Parameters

The hook accepts four optional type parameters:

```typescript
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

| Property   | Type                                                                     | Description                                                                      |
| ---------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `props`    | `Partial<TProps>`                                                        | Widget props (mapped from widget-only data). Empty `{}` when `isPending` is true |
| `output`   | `TOutput \| null`                                                        | Tool output from the last execution                                              |
| `metadata` | `TMetadata \| null`                                                      | Response metadata from the tool                                                  |
| `state`    | `TState \| null`                                                         | Persisted widget state                                                           |
| `setState` | `(state: TState \| ((prev: TState \| null) => TState)) => Promise<void>` | Update widget state (persisted and shown to model)                               |

### Layout and Theme

| Property      | Type                                | Description                                    |
| ------------- | ----------------------------------- | ---------------------------------------------- |
| `theme`       | `"light" \| "dark"`                 | Current theme (auto-syncs with ChatGPT)        |
| `displayMode` | `"inline" \| "pip" \| "fullscreen"` | Current display mode                           |
| `safeArea`    | `SafeArea`                          | Safe area insets for mobile layout             |
| `maxHeight`   | `number`                            | Maximum height available (pixels)              |
| `userAgent`   | `UserAgent`                         | Device capabilities (`device`, `capabilities`) |
| `locale`      | `string`                            | Current locale (e.g., `"en-US"`)               |
| `mcp_url`     | `string`                            | MCP server base URL for making API requests    |

### Actions

| Method                  | Signature                                                                    | Description                                                  |
| ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `callTool`              | `(name: string, args: Record<string, unknown>) => Promise<CallToolResponse>` | Call a tool on the MCP server                                |
| `sendFollowUpMessage`   | `(content: string \| MessageContentBlock[]) => Promise<void>`                | Send a follow-up message; string shorthand or full content block array (SEP-1865) |
| `openExternal`          | `(href: string) => void`                                                     | Open 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

| Property            | Type                 | Description                                                                                                                                 |
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `isAvailable`       | `boolean`            | Whether the `window.openai` API is available                                                                                                |
| `isPending`         | `boolean`            | Whether the tool is currently executing. When `true`, props will be empty `{}`                                                             |
| `partialToolInput`  | `Partial<TProps> \| null` | Partial/streaming tool arguments, updated in real time as the LLM generates them. `null` when not streaming. Only set when the host sends `tool-input-partial` (e.g. MCP Inspector / MCP Apps); in ChatGPT Apps SDK or URL params this remains `null`. |
| `isStreaming`       | `boolean`            | Whether tool arguments are currently being streamed (partial input received but complete input not yet available). `false` in ChatGPT Apps SDK or when the host does not stream. |

### Host Identity (MCP Apps only)

| Property           | Type                                          | Description                                                                                                     |
| ------------------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `hostInfo`         | `{ name: string; version: string } \| undefined` | Name and version of the MCP Apps host, from the `ui/initialize` handshake. `undefined` in ChatGPT Apps SDK.  |
| `hostCapabilities` | `Record<string, unknown> \| undefined`        | Capabilities advertised by the MCP Apps host (SEP-1865 `HostCapabilities`). `undefined` in ChatGPT Apps SDK.   |

These fields are only populated when the widget is running inside an MCP Apps host (Claude, Goose, or any client implementing SEP-1865). They are `undefined` in the ChatGPT Apps SDK environment.

```tsx
const { hostInfo, hostCapabilities } = useWidget();

// Identify the host
if (hostInfo) {
  console.log(`Running inside ${hostInfo.name} ${hostInfo.version}`);
  // e.g. "claude-desktop 1.2.0"
}

// Check for optional host features
if (hostCapabilities?.openLinks) {
  // Host supports opening external URLs via ui/open-link
}
```

## Complete Example

```tsx
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

When the host supports **streaming tool arguments** (e.g. MCP Inspector, MCP Apps clients that send `tool-input-partial`), an optional phase occurs between (1) and (2): the widget receives partial tool input in real time via `partialToolInput` while `isStreaming` is `true`, then transitions to full `props` when the tool input is complete. See [Streaming tool arguments](#streaming-tool-arguments) below.

**Example:**

```tsx
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**

```tsx
const MyWidget: React.FC = () => {
  const { props, isPending } = useWidget<MyWidgetProps>();

  return (
    <div>
      {isPending ? (
        <LoadingSpinner />
      ) : (
        <div>
          {props.city} - {props.temperature}°C
        </div>
      )}
    </div>
  );
};
```

### Streaming tool arguments

When the host streams tool arguments (e.g. MCP Inspector or MCP Apps clients that send `ui/notifications/tool-input-partial`), the widget can show a **live preview** of the incoming data. Use `partialToolInput` and `isStreaming` from `useWidget`:

- **`isStreaming`** is `true` while the LLM is still generating the tool call arguments.
- **`partialToolInput`** contains the partial arguments parsed so far (may be incomplete or invalid JSON until streaming finishes).
- When streaming ends, the host sends the final tool input and `props` is populated; `isStreaming` becomes `false` and `partialToolInput` is cleared.

This is only available when the host supports it; in ChatGPT Apps SDK or when using URL params, `partialToolInput` stays `null` and `isStreaming` stays `false`.

**Example: live preview while streaming**

```tsx
const CodePreviewWidget: React.FC = () => {
  const { props, isPending, isStreaming, partialToolInput } = useWidget<{ code: string; language: string }>();

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

  // Show live preview during streaming, then final props when complete
  const displayCode = isStreaming && partialToolInput?.code != null
    ? partialToolInput.code
    : props.code ?? "";
  const displayLang = (isStreaming ? partialToolInput?.language : props.language) ?? "text";

  return (
    <pre data-language={displayLang}>
      {displayCode || "Waiting for input..."}
    </pre>
  );
};
```

<Tip>
**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](/inspector/debugging-chatgpt-apps#widget-lifecycle-testing) for testing details.
</Tip>

## Helper Hooks

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

### useWidgetProps

Get only the widget props:

```tsx
import { useWidgetProps } from "mcp-use/react";

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

### useWidgetTheme

Get only the theme:

```tsx
import { useWidgetTheme } from "mcp-use/react";

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

### useWidgetState

Get state management:

```tsx
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:

```tsx
// ❌ 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:

```tsx
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:

```tsx
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:

```tsx
const { callTool } = useWidget();

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

### 7. Follow-up Messages

Send messages to the conversation. Accepts a plain string shorthand or a full content block array per the SEP-1865 `ui/message` spec:

```tsx
const { sendFollowUpMessage } = useWidget();

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

// Full content array (MCP Apps  -  supports text, image, resource blocks)
const handleRichMessage = async () => {
  await sendFollowUpMessage([
    { type: "text", text: "Show me more details about this product" },
  ]);
};
```

### 8. Display Mode Control

Request display mode changes:

```tsx
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:

```tsx
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);
  }
};
```

## Related Components

- [`McpUseProvider`](./mcpuseprovider) - Unified provider that includes necessary setup
- [`WidgetControls`](./widgetcontrols) - Debug and view controls
- [`ThemeProvider`](./themeprovider) - Theme management
- [`ErrorBoundary`](./errorboundary) - Error handling
