> For the complete documentation index, see [llms.txt](https://docs.flowcp.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.flowcp.ai/guides/build-widgets.md).

# Build MCP widgets

Tools let an AI agent *call* your API; **widgets** let it *show* the result as an interactive UI. A widget is an HTML document the server publishes as a `ui://` resource. When you link a widget to a tool, the AI host renders the widget inline in the chat surface instead of just printing the tool's text — a contact picker, an order summary card, a chart, and so on.

Widgets are additive and degrade gracefully: the linked tool still returns its normal JSON/text, so clients that don't support widgets keep working.

## How clients consume a widget

When a widget is enabled and linked to a tool, the runtime:

* Publishes the HTML as a resource at `ui://<slug>` (and a parallel `ui://<slug>/skybridge` variant), so both Anthropic MCP Apps hosts (Claude) and OpenAI Apps SDK hosts (ChatGPT) can render it.
* Decorates the linked tool's metadata so the host knows which resource to render when the tool runs (`_meta.ui.resourceUri` for Claude, `openai/outputTemplate` for ChatGPT).

The host renders the widget in a sandboxed iframe and brokers messages between the widget and your server.

## Managing widgets from an agent

Widgets can be managed two ways: from the dashboard (below) or by an AI agent connected to the [FlowCP MCP server](/guides/agent-driven-onboarding.md). The agent surface exposes widgets as tools — `list_widgets`, `create_widget`, `update_widget`, and `delete_widget` — so an agent can author and toggle widgets for a server from the chat. Like the dashboard, new widgets start **disabled**.

## Opening the widgets list

From your app's dashboard, click the **Widgets** tab. The page lists every widget on the selected server, each with an on/off switch. If your app has multiple environments (branches), use the branch buttons to switch between them.

## Starting from the library

Don't want to write a widget from scratch? Click **Browse library** to open the **Widget library** — a gallery of prebuilt Generative UI templates (an order summary card, a data table, stat cards, an image gallery, a key/value list, and a contact form). Filter by category, then click **Use template** on the one you want.

The template opens in the widget editor pre-filled with a working name, description, and HTML. Customize it however you like — the HTML is yours to edit — and click **Create widget** to save it. Library templates are just a starting point: once saved, a widget behaves exactly like one you authored by hand, including the default-deny rule (it starts **disabled** until you turn it on).

## Creating a widget

1. Click **New widget** (or start from a template — see above).
2. Fill in the fields:
   * **Name** — a `snake_case` identifier, e.g. `order_summary`. The resource is served at `ui://order-summary`.
   * **Description** — a one-line summary of what the widget shows.
   * **Widget HTML** — the HTML/CSS/JS for the component. A starter template is pre-filled.
   * **Allowed connect domains** (optional) — comma-separated origins the widget may call. The host blocks all others by default.
3. Click **Create widget**.

New widgets are created **disabled**, following the platform's default-deny model — see [Default-deny model](/security/default-deny-model.md).

### The ext-apps runtime

Widgets talk to the host through the `App` class from `@modelcontextprotocol/ext-apps`. Because host security policy blocks fetching scripts from a CDN, the runtime **inlines** that library for you: put the placeholder `/*__EXT_APPS_BUNDLE__*/` inside a `<script type="module">` tag and the server replaces it with the library at serve time, exposing `globalThis.ExtApps`:

```html
<script type="module">
  /*__EXT_APPS_BUNDLE__*/
  const { App } = globalThis.ExtApps;
  const app = new App({ name: 'OrderSummary', version: '1.0.0' }, {});
  app.ontoolresult = ({ content }) => {
    const data = JSON.parse(content[0].text);
    render(data);
  };
  await app.connect();
</script>
```

`app.ontoolresult` receives the linked tool's return value. Other `App` methods let the widget send a message into the conversation (`sendMessage`), call another tool (`callServerTool`), or open a link (`openLink`).

## Linking a widget to a tool

A widget only renders when a tool it's linked to runs. On the [Tools](/guides/review-and-customize-tools.md) page, each tool row has a **widget selector** (shown once the server has at least one widget). Pick your widget to link it, or choose **No widget** to unlink. Keep the linked tool's text output meaningful so the experience degrades cleanly on clients without widget support.

## Enabling a widget

Toggle the switch on a widget's row to **On** to publish it. Changes take effect immediately — no re-publish required. Disabled widgets are never exposed as a resource, and tools linked to a disabled widget carry no widget metadata.

## Editing and deleting

* Click the **pencil** icon to edit a widget's name, description, HTML, or allowed domains. Renaming re-derives the `ui://` slug.
* Click the **trash** icon to delete a widget. This cannot be undone. Any tool linked to it is automatically unlinked.

## Tips for effective widgets

* **One tool, one focused widget.** Small, single-purpose widgets render and reason better than a catch-all UI.
* **Mention the widget in the tool description.** It helps the agent decide when to invoke the tool.
* **Return a useful text summary too.** The tool's text content is what users on non-widget clients see.
* **Declare only the domains you need.** Leave *Allowed connect domains* empty unless the widget genuinely calls an external origin.
