Documentation Index
Fetch the complete documentation index at: https://docs.mcp-use.com/llms.txt
Use this file to discover all available pages before exploring further.
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.
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
| 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.
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
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>
);
};
Widgets render before the tool execution completes. This means:
-
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
-
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 below.
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>
);
};
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
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>
);
};
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:
Get only the widget props:
import { useWidgetProps } from "mcp-use/react";
const props = useWidgetProps<{ city: string; temperature: number }>();
// { city: "Paris", temperature: 22 }
Get only the theme:
import { useWidgetTheme } from "mcp-use/react";
const theme = useWidgetTheme(); // 'light' | 'dark'
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"],
}));
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 conversation. Accepts a plain string shorthand or a full content block array per the SEP-1865 ui/message spec:
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:
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);
}
};