# Widget components

> Building a widget as a React file: how FrontMCP bundles it, the props and bridge hooks it gets from @frontmcp/ui/react, the MUI-based components, theme and renderers in @frontmcp/ui, and the React-free helpers in @frontmcp/uipack.

Source: https://frontmcp.dev/reference/ui/components

A widget is a [template function](https://frontmcp.dev/reference/ui#templates) or a React component in a file of its own. For a file, FrontMCP bundles it with esbuild, wraps it in `McpBridgeProvider`, and mounts it into the page with the call's data as props. The React side comes from `@frontmcp/ui`: hooks that read the call and call tools through the host, eleven components built on MUI, a theme, and content renderers. `@frontmcp/uipack` is the React-free core FrontMCP builds every page with; its markup helpers and `renderToolTemplate()` work outside the server too. [Building Widgets with React](https://frontmcp.dev/learn/building-widgets-with-react) teaches it step by step.

```tsx
@Tool({ ..., ui: { template: { file: join(__dirname, "weather.widget.tsx") }, resourceMode?: "cdn" | "inline" } })

// weather.widget.tsx
export default function WeatherWidget({ output, input, loading }) { … }

import { useStructuredContent, useToolOutput, useToolInput, useCallTool, … } from "@frontmcp/ui/react";
import { Card, Badge, Button, Table, … } from "@frontmcp/ui/components";
import { FrontMcpThemeProvider } from "@frontmcp/ui/theme";
import { html, trustedHtml, renderToolTemplate } from "@frontmcp/uipack";
```

---

## Reference

The Playground can't import `@frontmcp/ui` or `@frontmcp/uipack`, or bundle a widget file, so only [one example](#splitting-a-template-into-functions) on this page runs in it. Every other example was run for real with FrontMCP 1.9.2, React 19 and MUI 7: widgets bundled by a server in Node, in a CommonJS project and in an ES module one, and rendered in Chromium. With 1.9.3, widgets that read the call and call tools with the hooks below were bundled and rendered again, in both kinds of project; the MUI components weren't. The hooks and components were also rendered in jsdom and with React's server renderer on 1.8.7; `@frontmcp/ui` 1.9.2 is the same code.

### Building a widget file

Install `@frontmcp/ui` in the server's project, even for a widget that doesn't import it: the code FrontMCP adds to mount your component imports `McpBridgeProvider` from `@frontmcp/ui/react`. npm 7 and later install its peers, `react`, `react-dom`, `@mui/material`, `@emotion/react` and `@emotion/styled`, along with it.

```bash
npm install @frontmcp/ui
```

Point `ui.template` at the file with an absolute path, and export the component as the default:

```ts get-weather.tool.ts
import { join } from "node:path";
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "get_weather",
  description: "Current weather for a city",
  inputSchema: { city: z.string() },
  outputSchema: { city: z.string(), tempC: z.number(), conditions: z.string() },
  ui: { template: { file: join(__dirname, "weather.widget.tsx") } },
})
export class GetWeather extends ToolContext {
  async execute({ city }: { city: string }) {
    return { city, tempC: 21, conditions: "Sunny" };
  }
}
```

```tsx weather.widget.tsx
type Weather = { city: string; tempC: number; conditions: string };

export default function WeatherWidget({ output }: { output: Weather | null }) {
  if (!output?.city) return <p>Loading…</p>;
  return (
    <div className="weather">
      <h2>{output.city}</h2>
      <p>{output.tempC}°C, {output.conditions}</p>
    </div>
  );
}
```

In Chromium the page shows `Oslo`, `21°C, Sunny`, opened on its own and in an MCP Apps host. It checks for the field it needs, `output?.city`, and not for `output`: a failed result's error text is truthy too ([The props](#the-props)). [See more examples below.](#usage)

The server can be CommonJS, which is what `frontmcp create` makes, or an ES module (`"type": "module"`), where the path is `join(import.meta.dirname, "weather.widget.tsx")`. Changed in 1.9: an ES module server couldn't bundle widget files. Every call logged `applyUI: UI rendering failed` with `Dynamic require of "path" is not supported`, and the result had no page.

- A relative `file` is resolved from the working directory, not from the tool's file: use `join(__dirname, …)`, or `import.meta.dirname` in an ES module. With a plain `tsc` build, `__dirname` is a folder under `dist/`, so copy the widget files there too. Otherwise every call logs `applyUI: UI rendering failed` with `FileSource widget not found (ENOENT): no file at "…"`, and when the same path exists under `src/`, the message names that file.
- `frontmcp build` copies the `*.widget.tsx` and `*.widget.jsx` files into its output, next to the bundle for `--target node`, and prints `copied 1 widget source file (*.widget.tsx/jsx) to dist/node`. The `install-<name>.sh` script it writes copies the bundle, the manifest and the runner only, so an installed build has no widgets, and the staging folder of `--target mcpb` has none either. Other local files a widget imports aren't copied: a widget with `import { greet } from "./format"` fails in the built server with `Failed to bundle FileSource "…/dist/node/hello.widget.tsx"` and `Could not resolve "./format"`, so keep a widget in one file.
- FrontMCP bundles the file with `esbuild`, which `@frontmcp/uipack` lists as an optional peer dependency, so nothing installs it for you. A project from `frontmcp create` has it, through the `frontmcp` package; anywhere else, install it as a dependency, not a dev dependency, since it's loaded when the tool is called. Without it every call logs `applyUI: UI rendering failed` with `FileSource widget "…" needs esbuild, which is not installed`.
- The file is bundled when the tool is called, or at startup for a [static](https://frontmcp.dev/reference/ui#serving-modes) tool. Imports come from the server's `node_modules`. A page with MUI in it is about 460 KB.
- Name the file `*.widget.tsx` and leave it out of the server's own `tsc` build, which would otherwise need `"jsx"` settings and React's types.

#### The props

FrontMCP renders your default export inside `McpBridgeProvider`, with three props:

| Prop | Value |
| --- | --- |
| `output` | The call's output. In an inline page, what `execute()` returned. In a static page, `null` until a host sends a result. After an [MCP Apps host's result](https://frontmcp.dev/reference/ui/hosts#mcp-apps-hosts), its `structuredContent`, or the error's message when the tool failed. With the [OpenAI Apps SDK](https://frontmcp.dev/reference/ui/hosts#the-openai-apps-sdk), `window.openai.toolOutput`: in a static page from the start, and in any page each new one the host assigns. |
| `input` | The call's arguments, read once from the page: `null` in a static page. `useToolInput()` has the arguments the host sends. |
| `loading` | `true` while there's no output, which in a static page is until the host sends a result; `false` from then on. |

Check for the fields you need rather than for `output` itself: a failed result's error text is truthy.

Changed in 1.8.7: after an MCP Apps host's result, `output` used to be the result's `content` array, `[{ type: "text", text: "{…}" }]`, and widgets read `useStructuredContent()` first to get the object. It's the object now, and `useStructuredContent()` still returns it.

Changed in 1.9: a static page used to start with `output` and `input` set to `{}` and `loading` `false`, so a widget that checked `if (!output)` drew the empty object. With the OpenAI Apps SDK, `output` stayed `{}` until the host assigned a new `toolOutput`.

#### `resourceMode`: `"cdn"` or `"inline"`

| `resourceMode` | The page | Chosen by default for |
| --- | --- | --- |
| `"cdn"` | About 59 KB. React 19.2.4, `react/jsx-runtime` and `react-dom/client` come from `esm.sh` through an import map. | Every client except `"claude"`, and every static widget, which is built before any client connects. |
| `"inline"` | About 730 KB, React included. Nothing is loaded from outside, MUI widgets included. | Clients detected as `"claude"`, when the widget is served inline. |

Where `esm.sh` can't be reached, a `"cdn"` page stays on "Loading widget...". Set `resourceMode: "inline"` for hosts that block outside scripts, and for static widgets meant for them.

### Hooks

`@frontmcp/ui/react` (also exported from `@frontmcp/ui`) reads the call and talks to the host through the page's [bridge](https://frontmcp.dev/reference/ui/hosts#the-bridge). A widget file is already inside `McpBridgeProvider`.

| Hook | Returns |
| --- | --- |
| `useToolOutput<T>()` | The call's output: the embedded output, then whatever the host sends as a result (its `structuredContent`, or a failed result's error text). `null` outside a provider. |
| `useStructuredContent<T>()` | The `structuredContent` of the result an MCP Apps host sent, or the `window.openai.toolOutput` of the OpenAI Apps SDK. `undefined` before one arrives, and in an inline page no host has sent a result to. |
| `useToolInput<T>()` | The call's arguments: the page's, then the `arguments` of each `tool-input` the host sends. |
| `useCallTool<In, Out>(name, options?)` | `[call, { data, loading, error, called }, reset]`. `call(args)` calls the tool through the host and resolves with the host's whole result, or `null` on failure. The call is marked as the widget's own, so a tool with a widget answers it without its page ([Calling a tool from a widget](https://frontmcp.dev/reference/ui/hosts#calling-a-tool-from-a-widget)). `options` are `onSuccess`, `onError` and `resetOnToolChange` (default `true`). |
| `useToolCalls({ key: toolName, … })` | `{ key: { call, data, loading, error, reset }, … }`: `useCallTool` for several tools. |
| `useSendMessage()` | `[send(text), { loading, error, sent }]`: sends a message to the conversation, where the host allows it. |
| `useOpenLink()` | `open(url)`, through the host. |
| `useTheme()`, `useDisplayMode()` | `"light"` or `"dark"`, and `"inline"`, `"fullscreen"` or `"pip"`. They follow what the host says: its answer to the handshake, then each `host-context-changed`. |
| `useHostContext()` | What the host said about itself: `{ theme, displayMode, locale, userAgent, safeArea, viewport, availableDisplayModes? }`, from its answer to the handshake, then each `host-context-changed`. |
| `useCapability(name)` | Whether the bridge's adapter can do `name`, like `"canCallTools"`. It's `false` until an MCP Apps host answers the handshake, and then `true` for what the host advertised. |
| `useMcpBridgeContext()`, `useMcpBridge()` | `{ bridge, loading, error, ready, adapterId, capabilities, revision }` (`revision` goes up when the handshake finishes), and the bridge itself. |

`McpBridgeProvider` takes `config`, `onReady` and `onError`. In a FrontMCP page it uses the page's `window.FrontMcpBridge`, and `ready` is `true` from the first render, before an MCP Apps host has answered the handshake. Outside a provider the hooks return empty values, and `call()` fails with `Bridge not initialized`.

The widget renders before an MCP Apps host answers `ui/initialize`, so its first render has `useCapability("canCallTools")` `false`, `useTheme()` `"light"` and `useHostContext()` as the page's own defaults. The answer makes the hooks render again, so a button that waits for `useCapability("canCallTools")` appears once the host can call tools, and stays hidden in a host that never answers. `useSendMessage()` and `useOpenLink()` don't wait: they ask the host, and `error` has its refusal, like `ui/message isn't supported (code: -32601)`.

Changed in 1.8.7: `useCapability()` stayed `false` and `useHostContext()` stayed `null` for good in an MCP Apps host, so a button behind `useCapability("canCallTools")` never showed, and widgets read `bridge.getHostContext()` themselves.

Changed in 1.8.6: `useTheme()` used to stay `"light"` when the handshake said `"dark"` until the host sent a change. The handshake's `hostContext` now reaches the bridge's `onContextChange` listeners, so it's the theme the host answered, and the bridge also sets the page's `color-scheme` from it ([Hosts and platforms](https://frontmcp.dev/reference/ui/hosts#the-theme)).

A tool that fails in the host usually comes back as a result with `isError: true`, in `data`, not in `error`. `error` is for calls that couldn't be made, like `Tool calls not supported on this platform (ext-apps)` when the host didn't answer the handshake.

> **Pitfall: useCallTool returns a tuple**
`useCallTool()` returns an array, not an object, so `const { call, loading } = useCallTool("get_weather")` gives `undefined` for both. Destructure an array: `const [call, { loading }] = useCallTool("get_weather")`.

### Components

`@frontmcp/ui/components`, and one entry point per component (`@frontmcp/ui/components/Card`), are React components built on MUI. Put them inside `FrontMcpThemeProvider`.

| Component | Props | Renders |
| --- | --- | --- |
| `Button` | `variant` (`"primary"`, `"secondary"`, `"danger"`, `"ghost"`), `size`, `loading`, `disabled`, `startIcon`, `endIcon`, `fullWidth`, `type`, `onClick`, `children` | An MUI `Button`; `loading` adds a spinner. |
| `Card` | `title`, `subtitle`, `headerActions`, `footer`, `elevation`, `clickable`, `onClick`, `slotProps`, `children` | An MUI `Card` with a header when there's a title. |
| `Alert` | `severity` (`"success"`, `"info"`, `"warning"`, `"error"`), `title`, `dismissible`, `onDismiss`, `icon`, `children` | An MUI `Alert`. |
| `Badge` | `label` (required), `variant` (`"default"`, `"primary"`, `"success"`, `"warning"`, `"error"`, `"info"`), `size`, `dot`, `removable`, `onRemove`, `icon` | An MUI `Chip`. |
| `Avatar` | `src`, `alt`, `size`, `variant`, `children` | An MUI `Avatar`; without `src` or `children`, a person icon. |
| `Modal` | `open`, `onClose` (both required), `title`, `actions`, `maxWidth`, `fullWidth`, `slotProps`, `children` | An MUI `Dialog`; nothing while closed. |
| `Table` | `columns` (`{ key, label, align? }[]`), `rows`, `size`, `stickyHeader`, `maxHeight` | An MUI `Table` in a container. |
| `TextField` | `label`, `value`, `defaultValue`, `onChange(value)`, `type`, `multiline`, `rows`, `error`, `helperText`, `placeholder`, `disabled`, `required`, `fullWidth`, `size` | An MUI `TextField`. `onChange` gets the value, not the event. |
| `Select` | `options` (`{ value, label }[]`), `label`, `value`, `defaultValue`, `onChange(value)`, `placeholder`, `error`, `helperText`, `disabled`, `required`, `fullWidth`, `size` | An MUI select `TextField`. |
| `List` | `items` (`{ id, primary, secondary?, icon?, onClick?, divider? }[]`), `dense` | An MUI `List`. |
| `Loader` | `variant` (`"spinner"`, `"bar"`, `"skeleton"`, `"overlay"`), `label`, `determinate`, `value`, `size`, `color`, `skeletonShape`, `skeletonWidth`, `skeletonHeight`, `skeletonLines`, `open`, `contained`, `custom` | A spinner, a progress bar, skeleton lines or a backdrop. |

`LoaderProvider custom={({ variant, label, value }) => …}` replaces every `Loader` under it with your own; `useLoaderContext()` reads it.

These are React components. The HTML-string functions the upstream guides show, like `card()`, `badge()` and `descriptionList()` from `@frontmcp/ui`, don't exist in 1.9.3 ([Troubleshooting](#module-frontmcpui-has-no-exported-member-card)).

### Theme

`@frontmcp/ui/theme`:

| Export | Description |
| --- | --- |
| `FrontMcpThemeProvider` | `theme?` and `children`. Applies the MUI theme and a baseline. |
| `createFrontMcpTheme(config?)` | An MUI theme from `{ palette?: { primary, secondary, success, warning, error, info, background, surface, text }, typography?: { fontFamily, monoFontFamily, fontSize }, shape?: { borderRadius }, mode?: "light" \| "dark" }`. |
| `defaultTheme`, `darkTheme` | Ready-made MUI themes; `darkTheme.palette.mode` is `"dark"`. |
| `useFrontMcpTheme()` | The current theme. |
| `SYSTEM_FONT_STACK`, `MONO_FONT_STACK` | The font stacks the themes use. |

### Renderers

`@frontmcp/ui/renderer` shows content of a detected kind. `detectContentType(value)` guesses the kind:

| Value | Kind |
| --- | --- |
| `"a,b\n1,2"` | `"csv"` |
| `"graph TD; A-->B"` | `"mermaid"` |
| `"$x^2$"` | `"math"` |
| `"https://x.com/a.png"`, a `data:image/…` URI | `"image"` |
| `'{"type":"bar","data":[]}'` | `"chart"` |
| anything else, `"# Title"` included | `"html"` |

`registerAllRenderers()` registers the renderers, which `renderContent(value, options?)` needs: pdf, chart, flow, map, mermaid, math, image, video, audio, csv, jsx, mdx and html, in that order of priority. The chart, map, PDF and diagram libraries are optional peers of `@frontmcp/ui`, not installed with it. Each renderer is also its own entry point, like `@frontmcp/ui/renderer/csv`.

### `@frontmcp/uipack`

The package FrontMCP builds pages with, installed with `@frontmcp/sdk`. Its root works from CommonJS and from ES modules.

| Export | Description |
| --- | --- |
| `` html`…` ``, `trustedHtml(markup)`, `isTrustedHtml(value)` | The same markup builder as `ctx.helpers`, for templates split into functions in other files. `html` returns a `TrustedHtml` object; `String()` gives the markup. |
| `escapeHtml(value)`, `safeJsonForScript(value)` | `escapeHtml` is `ctx.helpers.escapeHtml`. `safeJsonForScript` is JSON with `<` and `>` escaped, for a `<script>`. |
| `createTemplateHelpers()` | A new `ctx.helpers`. |
| `renderToolTemplate({ toolName, input, output, template, platformType?, resourceMode?, sizing?, escapeStringResults?, csp? })` | Renders a template the way a call does, without a server: `{ html, uiType, hash, size, meta }`, where `meta` has `ui/html`, `ui/type` and `ui/mimeType`. `csp` is the tool's [`ui.csp`](https://frontmcp.dev/reference/ui#content-security-policy). Use it to test a template. |
| `buildShell(content, { toolName, csp?, input?, output?, includeBridge?, title?, sizing? })` | Wraps markup in a page: `{ html, hash, size }`. `csp` makes the same policy as on a call: your origins are added to the CDNs. Without the bridge, a page is about 1.3 KB. |
| `MCP_APPS_MIME_TYPE` | `"text/html;profile=mcp-app"`. |

### Other entry points

- `@frontmcp/ui/bridge`: the bridge as a class, `FrontMcpBridge`, with its adapters and `generateBridgeIIFE()`, for pages FrontMCP doesn't build.
- `@frontmcp/ui/runtime`: `detectContentType`, and `transpileJsx()` with the Babel loader it uses.
- `@frontmcp/ui/auth`: components for the sign-in pages, on [Custom login UI](https://frontmcp.dev/reference/auth/login-ui).

---

## Usage

### Using the components in a widget

`card.widget.tsx` puts the result in a `Card` with a `Badge`, inside the theme. The tool is the one [above](#building-a-widget-file), with this file as `template`.

```tsx card.widget.tsx
import { Badge, Card } from "@frontmcp/ui/components";
import { FrontMcpThemeProvider } from "@frontmcp/ui/theme";

type Weather = { city: string; tempC: number; conditions: string };

export default function WeatherCard({ output }: { output: Weather | null }) {
  if (!output?.city) return null;
  return (
    <FrontMcpThemeProvider>
      <Card title={output.city} subtitle="Current weather">
        <p>{output.tempC}°C</p>
        <Badge label={output.conditions} variant="success" />
      </Card>
    </FrontMcpThemeProvider>
  );
}
```

The page, about 460 KB with `resourceMode: "cdn"` and 1.1 MB with `"inline"`, shows `Oslo`, `Current weather`, `21°C` and a `Sunny` chip in Chromium, with `"inline"` even with `esm.sh` blocked.

### Calling a tool from a React widget

The button calls `get_weather` through the host, and `data` is the host's whole result. Until a call has answered, the widget shows `output`.

```tsx refresh.widget.tsx
import { useCallTool } from "@frontmcp/ui/react";

type Weather = { city: string; tempC: number };

export default function Refreshable({ output }: { output: Weather | null }) {
  const [refresh, { data, loading, error }] = useCallTool<{ city: string }, Weather>("get_weather");

  const weather = data?.structuredContent ?? (output?.city ? output : null);
  return (
    <div>
      <p>{weather ? `${weather.city}: ${weather.tempC}°C` : "No data yet"}</p>
      <button disabled={loading} onClick={() => refresh({ city: "Bergen" })}>Bergen</button>
      {error && <p role="alert">{error.message}</p>}
      {data?.isError && <p role="alert">The tool failed.</p>}
    </div>
  );
}
```

In an MCP Apps host, the paragraph shows `Oslo: 21°C`, from the embedded output or the host's result, and clicking sends `tools/call` for `get_weather` to the host, which calls the server as itself ([Calling a tool from a widget](https://frontmcp.dev/reference/ui/hosts#calling-a-tool-from-a-widget)); the paragraph then shows `Bergen: 12°C`. In a frame whose parent doesn't answer, clicking shows `Tool calls not supported on this platform (ext-apps)`. Both were run in Chromium.

### Splitting a template into functions

Template functions don't need React. `html` from `@frontmcp/uipack` builds the same trusted markup as `ctx.helpers.html`, so pieces of a widget can live in their own file and be tested on their own:

```ts weather-parts.ts
import { html } from "@frontmcp/uipack";

export const badge = (text: string) => html`<span class="badge">${text}</span>`;

export const forecast = (days: { day: string; tempC: number }[]) =>
  html`<ul>${days.map((d) => html`<li>${d.day}: ${d.tempC}°C ${d.tempC > 20 ? badge("warm") : null}</li>`)}</ul>`;
```

```ts forecast.tool.ts
import { Tool, ToolContext, z, type TemplateContext } from "@frontmcp/sdk";
import { forecast } from "./weather-parts";

type Forecast = { city: string; days: { day: string; tempC: number }[] };

@Tool({
  name: "get_forecast",
  description: "Three-day forecast",
  inputSchema: { city: z.string() },
  ui: { template: (ctx: TemplateContext<{ city: string }, Forecast>) => ctx.helpers.html`<h2>${ctx.output.city}</h2>${forecast(ctx.output.days)}` },
})
export class GetForecast extends ToolContext {
  async execute({ city }: { city: string }): Promise<Forecast> {
    return { city, days: [{ day: "Mon", tempC: 19 }, { day: "Tue", tempC: 23 }] };
  }
}
```

The page's body is `<h2>Oslo</h2><ul><li>Mon: 19°C </li><li>Tue: 23°C <span class="badge">warm</span></li></ul>`: markup from `@frontmcp/uipack`'s `html` is trusted by `ctx.helpers.html`, so it isn't escaped twice.

Without `@frontmcp/uipack` in your imports, pass `ctx.helpers` to the pieces instead. The Playground can run this one:

```ts weather-parts.ts active
import type { TemplateContext } from "@frontmcp/sdk";

type Helpers = TemplateContext["helpers"];

export const badge = (h: Helpers, text: string) => h.html`<span class="badge">${text}</span>`;

export const forecast = (h: Helpers, days: { day: string; tempC: number }[]) =>
  h.html`<ul>${days.map((d) => h.html`<li>${d.day}: ${d.tempC}°C ${d.tempC > 20 ? badge(h, "warm") : null}</li>`)}</ul>`;
```

```ts forecast.tool.ts
import { Tool, ToolContext, z, type TemplateContext } from "@frontmcp/sdk";
import { forecast } from "./weather-parts";

type Forecast = { city: string; days: { day: string; tempC: number }[] };

@Tool({
  name: "get_forecast",
  description: "Three-day forecast",
  inputSchema: { city: z.string() },
  ui: { template: (ctx: TemplateContext<{ city: string }, Forecast>) => ctx.helpers.html`<h2>${ctx.output.city}</h2>${forecast(ctx.helpers, ctx.output.days)}` },
})
export class GetForecast extends ToolContext {
  async execute({ city }: { city: string }): Promise<Forecast> {
    return { city, days: [{ day: "Mon", tempC: 19 }, { day: "Tue", tempC: 23 }] };
  }
}
```

```ts parts.test.ts
import { test, expect } from "@frontmcp/testing";

test("the pieces nest without double escaping", async ({ mcp }) => {
  const html: string = (await mcp.tools.call("get_forecast", { city: "Oslo" })).raw._meta["ui/html"];
  expect(html.slice(html.indexOf("<body>") + 6, html.indexOf("</body>")).trim()).toBe(
    '<h2>Oslo</h2><ul><li>Mon: 19°C </li><li>Tue: 23°C <span class="badge">warm</span></li></ul>',
  );
});

test("values are still escaped", async ({ mcp }) => {
  const html: string = (await mcp.tools.call("get_forecast", { city: "<script>" })).raw._meta["ui/html"];
  expect(html).toContain("<h2>&lt;script&gt;</h2>");
});
```

### Testing a template without a server

`renderToolTemplate()` renders a template as a call would, so a unit test can check the markup:

```ts forecast.template.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { renderToolTemplate } from "@frontmcp/uipack";
import { forecast } from "./weather-parts";

test("warm days get a badge", () => {
  const { html, uiType, meta } = renderToolTemplate({
    toolName: "get_forecast",
    input: { city: "Oslo" },
    output: { city: "Oslo", days: [{ day: "Tue", tempC: 23 }] },
    template: (ctx: any) => ctx.helpers.html`${forecast(ctx.output.days)}`,
  });
  assert.equal(uiType, "html");
  assert.equal(meta["ui/mimeType"], "text/html;profile=mcp-app");
  assert.match(html, /<li>Tue: 23°C <span class="badge">warm<\/span><\/li>/);
});
```

---

## Troubleshooting

### `Dynamic require of "path" is not supported`

Logged with `applyUI: UI rendering failed` for a `.tsx` template in the Playground, which runs FrontMCP in the browser. Widget files are bundled from the file system, by a server on Node. Before 1.9, an ES module server on Node failed with the same message ([Building a widget file](#building-a-widget-file)); since 1.9 it bundles them.

### `FileSource widget "…" needs esbuild, which is not installed`

Logged with `applyUI: UI rendering failed`, and the result has no page. FrontMCP loads `esbuild` when the tool is called, and it's an optional peer of `@frontmcp/uipack`, so `npm install esbuild` in the server's project, as a dependency and not a dev dependency.

### `FileSource widget not found (ENOENT): no file at "…"`

The widget file isn't where the compiled tool looks: `join(__dirname, …)` points into the build output, where `tsc` doesn't copy `*.widget.tsx` files. The message says so, and names the file under `src/` when the same path exists there. Copy the widgets next to the output, or build with `frontmcp build`.

### `FileSource widget "…" requires the @frontmcp/ui package`

The full message goes on: `which provides the React bridge mount that's injected at bundle time. Install it (e.g. npm install @frontmcp/ui or yarn add @frontmcp/ui) and try again.` Install `@frontmcp/ui` in the server's project.

### `Module '"@frontmcp/ui"' has no exported member 'card'`

Or, at runtime, `The requested module '@frontmcp/ui' does not provide an export named 'card'`. `@frontmcp/ui` 1.9.3 has no HTML-string components: `card`, `badge`, `button`, `descriptionList`, `form` and `input` are gone. Use the React [components](#components) in a widget file, or write the markup with `html`.

### My widget renders with empty data

- It checks `if (!output)`, which passes for the error text of a failed result, so the widget draws it. Check for a field, like `output?.city`.
- It reads a field that `outputSchema` doesn't declare, which FrontMCP removed from the result ([Fields the schema doesn't declare](https://frontmcp.dev/reference/ui#fields-the-schema-doesnt-declare)).

### The page stays on "Loading widget..."

React didn't load from `esm.sh`. Set `resourceMode: "inline"` ([resourceMode](#resourcemode-cdn-or-inline)).

### `Bridge not initialized`

A hook's `call()` ran outside `McpBridgeProvider`: in a component rendered outside the widget file's tree, or in a test. Wrap it in `McpBridgeProvider`.
