Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
DEPRECATED: Replaced by mcp-app-builder. Previously used to build MCP servers with tools, resources, and prompts via mcp-use.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/widgets/basics.md
1# Widget Basics23Widgets are React components that provide visual UI for MCP tools. They let users browse, compare, and interact with data visually.45**Use widgets for:** Product lists, calendars, dashboards, search results, file browsers, any visual data representation67---89## When to Use Widgets1011**Use a widget when:**12- ✅ Browsing or comparing multiple items13- ✅ Visual representation improves understanding (charts, images, layouts)14- ✅ Interactive selection is easier visually than through text15- ✅ User needs to see data structure at a glance1617**Use plain tool (no widget) when:**18- ❌ Output is simple text or a single value19- ❌ No visual representation adds value20- ❌ Quick conversational response is sufficient2122**When in doubt:** Use a widget. It makes the experience better.2324---2526## Minimal Widget2728### 1. Create Tool with Widget Config2930```typescript31// index.ts32import { MCPServer, widget, text } from "mcp-use/server";33import { z } from "zod";3435const server = new MCPServer({36name: "my-server",37version: "1.0.0"38});3940server.tool(41{42name: "show-weather",43description: "Display weather for a city",44schema: z.object({45city: z.string().describe("City name")46}),47widget: {48name: "weather-display", // Must match filename: resources/weather-display.tsx49invoking: "Fetching weather...", // Optional: shown while loading50invoked: "Weather loaded" // Optional: shown when complete51}52},53async ({ city }) => {54const data = await getWeather(city);5556return widget({57props: {58city: data.city,59temp: data.temperature,60conditions: data.conditions,61icon: data.icon62},63output: text(`Weather in ${city}: ${data.temperature}°C, ${data.conditions}`)64});65}66);67```6869### 2. Create Widget Component7071```tsx72// resources/weather-display.tsx73import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";74import { z } from "zod";7576const propsSchema = z.object({77city: z.string(),78temp: z.number(),79conditions: z.string(),80icon: z.string()81});8283export const widgetMetadata: WidgetMetadata = {84description: "Display weather information for a city",85props: propsSchema,86exposeAsTool: false // ← Critical: prevents duplicate tool registration87};8889type Props = z.infer<typeof propsSchema>;9091export default function WeatherDisplay() {92const { props, isPending } = useWidget<Props>();9394if (isPending) {95return (96<McpUseProvider autoSize>97<div>Loading weather...</div>98</McpUseProvider>99);100}101102return (103<McpUseProvider autoSize>104<div style={{ padding: 20 }}>105<h2>{props.city}</h2>106<img src={props.icon} alt={props.conditions} width={64} />107<div style={{ fontSize: 48 }}>{props.temp}°C</div>108<p>{props.conditions}</p>109</div>110</McpUseProvider>111);112}113```114115**Key requirements:**1161. Export `widgetMetadata` with props schema1172. Infer type from schema and pass to `useWidget<Props>()`1183. `exposeAsTool` defaults to `false` — correct when pairing with a custom tool1194. Wrap root in `<McpUseProvider autoSize>`1205. **Always check `isPending` before accessing `props`**121122**Production builds (`mcp-use build`):** Never use bare `useWidget()` without a props generic — fields default to `unknown` and TypeScript will fail (e.g. TS2322). If you use `callTool` from `useWidget()`, treat `structuredContent` and nested values as `unknown` until you parse with Zod, narrow with `typeof`/`Array.isArray`, or assign to typed variables; do not pass `unknown` directly as JSX children or string props.123124---125126## Widget Metadata127128The `widgetMetadata` export defines your widget's contract:129130```typescript131export const widgetMetadata: WidgetMetadata = {132description: "Brief description of what this widget displays",133props: z.object({134// Define all props the widget expects135id: z.string(),136title: z.string(),137count: z.number(),138items: z.array(z.object({139name: z.string(),140value: z.number()141}))142}),143exposeAsTool: false // Default; omit or set explicitly when pairing with a custom tool144};145```146147**Fields:**148- `description` - What the widget displays/does149- `props` - Zod schema defining expected props shape150- `exposeAsTool` - Set to `true` to auto-register as a tool (default: `false`)151- `metadata.invoking` - Status text shown in inspector while tool runs (auto-default: `"Loading {name}..."`)152- `metadata.invoked` - Status text shown in inspector after tool completes (auto-default: `"{name} ready"`)153154```typescript155export const widgetMetadata: WidgetMetadata = {156description: "Display weather information for a city",157props: propsSchema,158metadata: {159invoking: "Fetching weather...", // Shimmer text while tool runs160invoked: "Weather loaded", // Static text when complete161csp: { connectDomains: ["https://api.weather.com"] },162},163};164```165166These status texts appear as animated shimmer text (pending) and static text (complete) in the MCP Inspector and ChatGPT. The values also flow to `openai/toolInvocation/invoking`/`invoked` in tool metadata automatically.167168---169170## useWidget() Hook171172The `useWidget()` hook provides access to props and widget state:173174```typescript175const {176props, // Widget props from tool response177isPending, // True while props are loading178setState, // Update widget state179state, // Current widget state180} = useWidget();181```182183**To call tools from a widget**, use the dedicated `useCallTool()` hook — see [interactivity.md](interactivity.md).184185### props186Data passed from tool's `widget({ props })` response:187188```typescript189const { props } = useWidget();190191// Access props after isPending check192if (!isPending) {193console.log(props.city); // "Tokyo"194console.log(props.temp); // 28195}196```197198**Always check `isPending` before accessing `props`:**199```typescript200❌ const { props } = useWidget();201return <div>{props.city}</div>; // Error! props undefined while loading202203✅ const { props, isPending } = useWidget();204if (isPending) return <div>Loading...</div>;205return <div>{props.city}</div>; // Safe206```207208### isPending209Boolean indicating if props are still loading.210211**CRITICAL:** Widgets render **before** the tool completes execution. On first render:212- `isPending` is `true`213- `props` is an empty object `{}`214- Accessing `props` fields will cause errors215216**Widget Lifecycle:**2171. Widget mounts immediately when tool is called → `isPending = true`, `props = {}`2182. Tool executes and returns `widget({ props })`2193. Widget re-renders → `isPending = false`, `props` contains data220221```typescript222const { isPending } = useWidget();223224if (isPending) {225return (226<McpUseProvider autoSize>227<div>Loading...</div>228</McpUseProvider>229);230}231232// Now safe to access props - guaranteed to have data233```234235**Multiple patterns for handling isPending:**236237```typescript238// ✅ Pattern 1: Early return (recommended)239if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;240return <McpUseProvider autoSize><div>{props.data}</div></McpUseProvider>;241242// ✅ Pattern 2: Conditional rendering243return (244<McpUseProvider autoSize>245{isPending ? <div>Loading...</div> : <div>{props.data}</div>}246</McpUseProvider>247);248249// ✅ Pattern 3: Optional chaining (when props might be undefined)250return <McpUseProvider autoSize><div>{props?.data ?? "Loading..."}</div></McpUseProvider>;251```252253---254255## McpUseProvider256257**Required wrapper** for all widgets. Provides context and handles iframe sizing.258259```typescript260import { McpUseProvider } from "mcp-use/react";261262export default function MyWidget() {263const { props, isPending } = useWidget();264265if (isPending) {266return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;267}268269return (270<McpUseProvider autoSize>271<div>272{/* Your widget content */}273</div>274</McpUseProvider>275);276}277```278279**Props:**280- `autoSize={true}` - Automatically resize iframe to content (recommended)281- `autoSize={false}` - Fixed height, widget handles scrolling282283**Must wrap:**284- ✅ Every return path (including loading states)285- ✅ Root element of component286287---288289## Props Handling Patterns290291### Simple Props292```typescript293export const widgetMetadata: WidgetMetadata = {294props: z.object({295message: z.string(),296count: z.number()297}),298exposeAsTool: false299};300301export default function SimpleWidget() {302const { props, isPending } = useWidget();303304if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;305306return (307<McpUseProvider autoSize>308<div>309<p>{props.message}</p>310<p>Count: {props.count}</p>311</div>312</McpUseProvider>313);314}315```316317### Array Props318```typescript319export const widgetMetadata: WidgetMetadata = {320props: z.object({321items: z.array(z.object({322id: z.string(),323name: z.string()324}))325}),326exposeAsTool: false327};328329export default function ListWidget() {330const { props, isPending } = useWidget();331332if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;333334return (335<McpUseProvider autoSize>336<ul>337{props.items.map(item => (338<li key={item.id}>{item.name}</li>339))}340</ul>341</McpUseProvider>342);343}344```345346### Nested Props347```typescript348export const widgetMetadata: WidgetMetadata = {349props: z.object({350user: z.object({351name: z.string(),352profile: z.object({353bio: z.string(),354avatar: z.string()355})356})357}),358exposeAsTool: false359};360361export default function ProfileWidget() {362const { props, isPending } = useWidget();363364if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;365366const { user } = props;367368return (369<McpUseProvider autoSize>370<div>371<img src={user.profile.avatar} alt={user.name} />372<h2>{user.name}</h2>373<p>{user.profile.bio}</p>374</div>375</McpUseProvider>376);377}378```379380### Optional Props381```typescript382export const widgetMetadata: WidgetMetadata = {383props: z.object({384title: z.string(),385subtitle: z.string().optional(), // May be undefined386items: z.array(z.string())387}),388exposeAsTool: false389};390391export default function FlexibleWidget() {392const { props, isPending } = useWidget();393394if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;395396return (397<McpUseProvider autoSize>398<div>399<h1>{props.title}</h1>400{props.subtitle && <h2>{props.subtitle}</h2>}401<ul>402{props.items.map((item, i) => <li key={i}>{item}</li>)}403</ul>404</div>405</McpUseProvider>406);407}408```409410---411412## File Location413414Widgets live in `resources/` directory:415416```417my-server/418├── index.ts # Server code419├── resources/420│ ├── weather-display.tsx # Widget component421│ ├── product-list.tsx422│ └── calendar-view.tsx423└── package.json424```425426**Naming convention:**427- Use kebab-case for widget names428- Tool config: `widget: { name: "weather-display" }`429- File: `resources/weather-display.tsx`430431---432433## TypeScript Types434435For type safety, infer props type from schema:436437⚠️ **CRITICAL:** Always define your Zod schema in a separate constant before `widgetMetadata`. Never infer types from `widgetMetadata.props` - TypeScript will lose type information and the result will be `unknown`.438439```typescript440import { z } from "zod";441import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";442443const propsSchema = z.object({444city: z.string(),445temp: z.number(),446conditions: z.string()447});448449export const widgetMetadata: WidgetMetadata = {450description: "Display weather",451props: propsSchema,452exposeAsTool: false453};454455type Props = z.infer<typeof propsSchema>;456457export default function WeatherWidget() {458const { props, isPending } = useWidget<Props>();459460if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;461462// Now props is fully typed!463return (464<McpUseProvider autoSize>465<div>466<h2>{props.city}</h2> {/* ✓ TypeScript knows this is string */}467<p>{props.temp}°C</p> {/* ✓ TypeScript knows this is number */}468</div>469</McpUseProvider>470);471}472```473474---475476## Common Mistakes477478### ❌ Missing isPending Check479```typescript480// ❌ Bad - props undefined during loading481export default function BadWidget() {482const { props } = useWidget();483484return (485<McpUseProvider autoSize>486<div>{props.title}</div> {/* Error! */}487</McpUseProvider>488);489}490491// ✅ Good492export default function GoodWidget() {493const { props, isPending } = useWidget();494495if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;496497return (498<McpUseProvider autoSize>499<div>{props.title}</div>500</McpUseProvider>501);502}503```504505### ❌ Missing McpUseProvider506```typescript507// ❌ Bad - Missing provider508export default function BadWidget() {509const { props, isPending } = useWidget();510511if (isPending) return <div>Loading...</div>;512513return <div>{props.title}</div>; {/* Won't render correctly */}514}515516// ✅ Good517export default function GoodWidget() {518const { props, isPending } = useWidget();519520if (isPending) return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;521522return (523<McpUseProvider autoSize>524<div>{props.title}</div>525</McpUseProvider>526);527}528```529530### `exposeAsTool` — default is `false`531```typescript532// ✅ Default — widget is a resource only, exposed via a custom tool533export const widgetMetadata: WidgetMetadata = {534description: "...",535props: z.object({ ... })536// exposeAsTool defaults to false537};538539// ✅ Explicit opt-in to auto-registration540export const widgetMetadata: WidgetMetadata = {541description: "...",542props: z.object({ ... }),543exposeAsTool: true // Auto-registers widget as a tool544};545```546547### ❌ Missing Type Parameter on useWidget548```typescript549// ❌ Bad - props is UnknownObject, no autocomplete or type safety550const propsSchema = z.object({551title: z.string(),552count: z.number()553});554555export default function BadWidget() {556const { props } = useWidget(); // props is UnknownObject557return <div>{props.title}</div>; // No IDE support, runtime errors possible558}559560// ✅ Good - props is fully typed with IDE support561const propsSchema = z.object({562title: z.string(),563count: z.number()564});565566type Props = z.infer<typeof propsSchema>;567568export default function GoodWidget() {569const { props } = useWidget<Props>(); // props is properly typed570return <div>{props.title}</div>; // Full autocomplete and type checking571}572```573574### ❌ Inferring Type from widgetMetadata.props575```typescript576// ❌ Bad - Type inference fails, Props is unknown577export const widgetMetadata: WidgetMetadata = {578description: "...",579props: z.object({580title: z.string(),581count: z.number()582}) // Inline schema definition583};584585type Props = z.infer<typeof widgetMetadata.props>; // Props is unknown!586587export default function BadWidget() {588const { props } = useWidget<Props>();589return <div>{props.title}</div>; // No autocomplete, no type safety590}591592// ✅ Good - Extract schema first for proper type inference593const propsSchema = z.object({594title: z.string(),595count: z.number()596});597598export const widgetMetadata: WidgetMetadata = {599description: "...",600props: propsSchema // Reference the schema variable601};602603type Props = z.infer<typeof propsSchema>; // Props is properly typed!604605export default function GoodWidget() {606const { props } = useWidget<Props>();607return <div>{props.title}</div>; // Full autocomplete and type checking608}609```610611**Why this happens:** The `WidgetMetadata` type is generic, so TypeScript can't preserve the specific Zod schema type when defined inline. Always extract your schema to a separate constant before using it in `widgetMetadata`.612613---614615## Testing Widgets616617### Option 1: Inspector (interactive)6186191. Start dev server: `npm run dev`6202. Open inspector: `http://localhost:3000/inspector`6213. Click "List Tools" → Find your tool6224. Click "Call Tool" → Enter test input6235. Widget renders in inspector624625**Quick iteration:**626- Change widget code → Auto-reload627- Adjust props schema → Update tool call input628- Test edge cases (empty lists, missing optional props)629630### Option 2: Headless screenshot (agent-friendly)631632For visual feedback loops where you want to verify a widget change without leaving the terminal — call the tool, save a PNG, eyeball it, edit, repeat:633634```bash635# Saved-server form (assumes you ran `mcp-use client connect dev <url>` once)636npx mcp-use client dev screenshot --tool get-weather city=Tokyo \637--width 800 --height 600 --theme light \638--output ./weather.png639640# Ad-hoc form — no saved server, pass auth headers inline if needed641npx mcp-use client screenshot --mcp http://localhost:3000/mcp \642--tool get-weather city=Tokyo643```644645- Args are `key=value` pairs, `key:='<json>'` for nested values, or one full JSON object646- The saved-server form reuses the auth from `mcp-use client connect` (OAuth or `--auth <token>`); the ad-hoc form accepts `-H "Header: value"` (repeatable) for authenticated servers647- Add `--device-scale-factor 2` for Retina output648- For sandboxed environments without a local Chrome, point `--cdp-url <ws>` at a hosted Chromium (e.g. Notte) and `--inspector <publicly-reachable-url>` at a deployed inspector649650Equivalently, `mcp-use client <name> tools call <tool> ... --screenshot` calls the tool *and* saves a widget PNG in one step — useful for one-shot verification.651652---653654## Complete Example655656```typescript657// index.ts658import { MCPServer, widget, text } from "mcp-use/server";659import { z } from "zod";660661const server = new MCPServer({662name: "product-server",663version: "1.0.0"664});665666server.tool(667{668name: "search-products",669description: "Search products by keyword",670schema: z.object({671query: z.string().describe("Search query")672}),673widget: {674name: "product-list",675invoking: "Searching products...",676invoked: "Products loaded"677}678},679async ({ query }) => {680const products = await searchProducts(query);681682return widget({683props: {684products,685query,686totalCount: products.length687},688output: text(`Found ${products.length} products matching "${query}"`)689});690}691);692693server.listen();694```695696```tsx697// resources/product-list.tsx698import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";699import { z } from "zod";700701export const widgetMetadata: WidgetMetadata = {702description: "Display product search results",703props: z.object({704products: z.array(z.object({705id: z.string(),706name: z.string(),707price: z.number(),708image: z.string()709})),710query: z.string(),711totalCount: z.number()712}),713exposeAsTool: false714};715716export default function ProductList() {717const { props, isPending } = useWidget();718719if (isPending) {720return (721<McpUseProvider autoSize>722<div style={{ padding: 20 }}>Loading products...</div>723</McpUseProvider>724);725}726727return (728<McpUseProvider autoSize>729<div style={{ padding: 20 }}>730<h2>Search: "{props.query}"</h2>731<p>Found {props.totalCount} products</p>732733<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 16 }}>734{props.products.map(product => (735<div key={product.id} style={{ border: "1px solid #ddd", padding: 12, borderRadius: 8 }}>736<img src={product.image} alt={product.name} style={{ width: "100%", height: 150, objectFit: "cover" }} />737<h3 style={{ fontSize: 16, margin: "8px 0" }}>{product.name}</h3>738<p style={{ fontSize: 18, fontWeight: "bold" }}>${product.price}</p>739</div>740))}741</div>742</div>743</McpUseProvider>744);745}746```747748---749750## Next Steps751752- **Manage widget state** → [state.md](state.md)753- **Add interactivity** → [interactivity.md](interactivity.md)754- **Style with themes** → [ui-guidelines.md](ui-guidelines.md)755- **Advanced patterns** → [advanced.md](advanced.md)756