> ## Documentation Index
> Fetch the complete documentation index at: https://toolgrain.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Add more widgets

> Build one self-contained HTML file per widget and register one resource for each.

Continue from [Build your first widget](https://toolgrain.mintlify.site/mcp-apps/quickstart.md). Add a sales widget alongside weather, using the same Vite project and typed bindings.

| Tool | Vite entry | Built file | Resource URI |
| - | - | - | - |
| `weather` | `weather.html` | `dist/widgets/weather.html` | `ui://weather/widget.html` |
| `sales` | `sales.html` | `dist/widgets/sales.html` | `ui://sales/widget.html` |

Each HTML file is an independent widget. A host creates an instance of the widget associated with the tool it calls.

## 1. Add the sales tool

Replace `server/tools.ts` with the two-tool version:

```ts server/tools.ts theme={null}
import { tool } from "@toolgrain/mcp";
import { z } from "zod";

export const weatherWidgetUri = "ui://weather/widget.html";
export const salesWidgetUri = "ui://sales/widget.html";

export const tools = {
  weather: tool({
    description: "Show demo weather for a city.",
    input: z.object({ city: z.string().default("Dubai") }),
    output: z.object({ city: z.string(), temperature: z.number() }),
    _meta: { ui: { resourceUri: weatherWidgetUri } },
    execute: ({ city }) => ({ city, temperature: city === "London" ? 14 : 29 }),
  }),
  sales: tool({
    description: "Show demo sales for a region.",
    input: z.object({ region: z.string().default("West") }),
    output: z.object({ region: z.string(), total: z.number() }),
    _meta: { ui: { resourceUri: salesWidgetUri } },
    execute: ({ region }) => ({ region, total: region === "West" ? 31000 : 42000 }),
  }),
};
```

The existing `createApp<typeof tools>()` now knows both tool names. In the quickstart's `Weather` component, select its tool explicitly so TypeScript narrows the result:

```tsx theme={null}
const { result, error, status } = useToolResult("weather");
```

If you added the refresh example, it already selects `weather` by name.

## 2. Add the sales component and entry

```tsx src/Sales.tsx theme={null}
import { AppProvider, useToolResult } from "./mcp.ts";

function SalesView() {
  const { result, error } = useToolResult("sales");
  if (error) return <p role="alert">{error.message}</p>;
  if (result?.isError) return <p role="alert">Sales could not be loaded.</p>;
  const sales = result?.structuredContent;
  if (!sales) return <p>Waiting for sales…</p>;

  return (
    <section>
      <h1>{sales.region} sales</h1>
      <p>${sales.total.toLocaleString()}</p>
    </section>
  );
}

export default function Sales() {
  return (
    <AppProvider appInfo={{ name: "Sales widget", version: "1.0.0" }} tool="sales">
      <SalesView />
    </AppProvider>
  );
}
```

Create a React entry for that component:

```tsx src/sales-main.tsx theme={null}
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import Sales from "./Sales.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode><Sales /></StrictMode>,
);
```

Add its HTML entry at the project root:

```html sales.html theme={null}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sales widget</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/sales-main.tsx"></script>
  </body>
</html>
```

## 3. Build each entry separately

The Vite configuration from the quickstart selects one HTML input from `--mode`:

```ts vite.config.ts theme={null}
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig(({ mode }) => ({
  plugins: [react(), viteSingleFile()],
  publicDir: false,
  build: {
    outDir: "dist/widgets",
    emptyOutDir: mode === "weather",
    rolldownOptions: { input: `${mode}.html` },
  },
}));
```

Update the `build` script in `package.json`:

```json theme={null}
"build": "tsc -b && vite build --mode weather && vite build --mode sales"
```

Run it:

```sh theme={null}
npm run build
```

The weather build clears `dist/widgets`; the sales build preserves it. The result contains **both** `weather.html` and `sales.html`, each with its own inlined JavaScript/CSS.

Use one Vite build per HTML input with [vite-plugin-singlefile](https://github.com/richardtallent/vite-plugin-singlefile#readme). The plugin is designed for one HTML entry at a time; separate runs give each resource its own bundled dependencies. For a longer list of widgets, the repository's [build script](https://github.com/keshavkatwe/toolgrain/blob/main/apps/react-apps-example/scripts/build-widgets.mjs) runs the same sequence in a loop.

## 4. Register the second resource

In `server/index.ts`, add `salesWidgetUri` to the import from `./tools.ts`. Before `createServer`, add the second resource:

```ts theme={null}
const salesWidget = resource({
  uri: salesWidgetUri,
  name: "sales-widget",
  mimeType: APPS_MIME_TYPE,
  read: async (uri) => ({
    contents: [{
      uri,
      mimeType: APPS_MIME_TYPE,
      text: await readFile(new URL("../dist/widgets/sales.html", import.meta.url), "utf8"),
      _meta: { ui: widgetUi },
    }],
  }),
});
```

Register both definitions:

```ts theme={null}
resources: [weatherWidget, salesWidget],
```

Both widgets reuse the same `widgetUi` settings here. Give a resource its own [metadata configuration](https://toolgrain.mintlify.site/mcp-apps/security.md) when it needs different permissions or network origins.

Rebuild, reload the host's connector, and call `weather` and `sales`. Each tool points to its own resource URI and HTML entry.

## Assets and development

Import local JavaScript, CSS, and assets from your widget's source so the bundler can include them. This recipe sets `publicDir: false`. If you enable Vite's `public` folder, its copied files are not automatically inlined by the single-file plugin. External scripts, fonts, images, and APIs need the appropriate [CSP origins](https://toolgrain.mintlify.site/mcp-apps/security.md#allow-network-requests-and-external-assets).

`npm run dev` serves `/weather.html` and `/sales.html` during development. A normal browser tab has no MCP host connection. Rebuild the HTML and reload it in the host to test tool results and interactions. The repository example also has a `dev:ui` watch build for its shared entry.

If several tools should intentionally render the same HTML application, use a [shared entry](https://toolgrain.mintlify.site/mcp-apps/shared-entries.md) with explicit routing instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.