widgets/<name>/ registers as the MCP tool <name> plus a ui:// resource the host renders. It needs two files: widget.ts, which default-exports defineWidget({ ... }), and ui.tsx, which default-exports a React component.
widgets/select-plan/widget.ts
widgets/select-plan/ui.tsx
Why a widget is two files
widget.ts gets imported by the server and by the browser bundle, so it stays free of React and CSS. It carries one data schema, which serves as the tool’s input schema, its structured output, and the type the component receives:
generateHelpers<AppType>() in a shared file typed against the server, which makes a widget’s type depend on the server’s shape. Here the widget owns its own contract, so the two cannot drift.
data arrives as soon as the host has the tool input, which on most hosts happens before the server responds, so render optimistically and reach for isReady when you need the final value.
Contract fields
string
required
Shown to humans in connector UIs, and used as the
title annotation.string
required
LLM-facing. When to show this widget, and how to frame it. The starter template’s version tells the model what to say before calling and what not to repeat afterwards.
Shape
required
A plain Zod shape. Input schema, structured output and component props, all from this one definition.
(data) => string
Text handed to the model alongside the rendered widget. Use it to say what the model should not repeat, and what it should wait for. Without it the runtime sends a default that tells the model the widget displays all the detail itself and to wait for the user to interact with it.
(input) => data | Promise<data>
Optional server-side loader, for widgets whose data comes from an API rather than from the model. Defaults to echoing the input through. A
load that throws returns an error envelope telling the host something went wrong on your side and to offer a retry.boolean
default:true
Let the host size the widget’s frame to its content. A card whose height depends on its data is cut off or padded out by any fixed frame. Set it to
false for a widget that renders its own scroll area.WidgetCsp
Hooks in ui.tsx
Everything a component needs comes from@waniwani/kit/web.
useWidget().data is the model’s input until the server answers, so an if (!data) guard only covers the first paint. Guard on isReady for anything that must wait for load().
What a request does
Every arrow that leavesApp out is runtime code. Error envelopes, annotation defaults (including the title Claude’s Connectors Directory requires), the “do not narrate the widget” instruction, the CSP block and tracking through withWaniwani all sit in one place, for every app.