Advanced widget patterns and examples for ChatGPT apps.
Sign in to save downloads to your library and vote.
Preview
Widget Patterns
Advanced widget patterns and examples for ChatGPT apps.
Table of Contents
- Complex Widget (Folder Structure)
- Data Fetching Widget
- Stateful Widget
- Themed Widget
- Widget with Tool Calls
Complex Widget (Folder Structure)
For widgets with multiple components:
resources/
└── product-search/
├── widget.tsx # Entry point (required name)
├── components/
│ ├── ProductCard.tsx
│ └── FilterBar.tsx
├── hooks/
│ └── useFilter.ts
├── types.ts
└── constants.ts
Entry point (widget.tsx):
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
import { ProductCard } from "./components/ProductCard";
import { FilterBar } from "./components/FilterBar";
export const widgetMetadata: WidgetMetadata = {
description: "Display product search results with filtering",
props: z.object({
products: z.array(
z.object({
id: z.string(),
name: z.string(),
price: z.number(),
image: z.string(),
}),
),
query: z.string(),
}),
};
const ProductSearch: React.FC = () => {
const { props, isPending, state, setState } = useWidget();
if (isPending) {
return (
<McpUseProvider autoSize>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div>
<h1>Search: {props.query}</h1>
<FilterBar onFilter={(filters) => setState({ filters })} />
<div className="grid grid-cols-3 gap-4">
{props.products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</div>
</McpUseProvider>
);
};
export default ProductSearch;
Data Fetching Widget
Widget that refreshes data via tool calls:
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Display data with refresh capability",
props: z.object({
id: z.string(),
title: z.string(),
data: z.array(z.any()),
}),
};
const DataWidget: React.FC = () => {
const { props, isPending, callTool } = useWidget();
if (isPending) return <div>Loading...</div>;
const refresh = async () => {
try {
await callTool("fetch-data", { id: props.id });
} catch (error) {
console.error("Refresh failed:", error);
}
};
return (
<McpUseProvider autoSize>
<div>
<h1>{props.title}</h1>
<button onClick={refresh}>Refresh</button>
<ul>
{props.data.map((item, i) => (
<li key={i}>{JSON.stringify(item)}</li>
))}
</ul>
</div>
</McpUseProvider>
);
};
export default DataWidget;
Stateful Widget
Widget with persistent state across interactions:
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Counter with persistent state",
props: z.object({
initialValue: z.number().optional(),
}),
};
const CounterWidget: React.FC = () => {
const { props, isPending, state, setState } = useWidget();
if (isPending) return <div>Loading...</div>;
const count = state?.count ?? props.initialValue ?? 0;
const increment = async () => {
await setState({ count: count + 1 });
};
const decrement = async () => {
await setState({ count: count - 1 });
};
return (
<McpUseProvider autoSize>
<div className="flex items-center gap-4">
<button onClick={decrement}>-</button>
<span className="text-2xl">{count}</span>
<button onClick={increment}>+</button>
</div>
</McpUseProvider>
);
};
export default CounterWidget;
Themed Widget
Widget that respects light/dark theme:
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Theme-aware content display",
props: z.object({
title: z.string(),
content: z.string(),
}),
};
const ThemedWidget: React.FC = () => {
const { props, isPending, theme } = useWidget();
if (isPending) return <div>Loading...</div>;
const isDark = theme === "dark";
return (
<McpUseProvider autoSize>
<div
className={`p-4 rounded-lg ${
isDark ? "bg-gray-900 text-white" : "bg-white text-gray-900"
}`}
>
<h1
className={`text-xl font-bold ${isDark ? "text-blue-400" : "text-blue-600"}`}
>
{props.title}
</h1>
<p className={isDark ? "text-gray-300" : "text-gray-700"}>
{props.content}
</p>
</div>
</McpUseProvider>
);
};
export default ThemedWidget;
Widget with Tool Calls
Widget that calls other MCP tools:
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
import { useState } from "react";
export const widgetMetadata: WidgetMetadata = {
description: "City weather lookup with search",
props: z.object({
defaultCity: z.string().optional(),
}),
};
const WeatherSearch: React.FC = () => {
const { props, isPending, callTool, state, setState } = useWidget();
const [loading, setLoading] = useState(false);
const [city, setCity] = useState(props.defaultCity || "");
if (isPending) return <div>Loading...</div>;
const searchWeather = async () => {
if (!city.trim()) return;
setLoading(true);
try {
const result = await callTool("get-weather", { city });
await setState({ lastResult: result.content, lastCity: city });
} catch (error) {
console.error("Search failed:", error);
} finally {
setLoading(false);
}
};
return (
<McpUseProvider autoSize>
<div className="p-4">
<div className="flex gap-2 mb-4">
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Enter city name"
className="border rounded px-2 py-1"
/>
<button onClick={searchWeather} disabled={loading}>
{loading ? "Searching..." : "Search"}
</button>
</div>
{state?.lastResult && (
<div>
<h3>Weather in {state.lastCity}:</h3>
<pre>{JSON.stringify(state.lastResult, null, 2)}</pre>
</div>
)}
</div>
</McpUseProvider>
);
};
export default WeatherSearch;
Ingestion metadata
- Source catalog
- awesome-llm-apps
- Repository
- Shubhamsaboo/awesome-llm-apps · main
- File path
- generative_ui_agents/ai-mcp-app-builder/apps/mcp-use-server/.agent/skills/chatgpt-app-builder/references/widget-patterns.md
- Last refreshed
- 10/24/2026, 3:00:14 AM (5h ago)
- Refresh schedule
- Daily · 03:00 UTC
- Dedupe status
- Unique · deduped by (source, url)