> ## 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.

# Build your first widget

> Start with the Vite CLI and connect a React widget to a Toolgrain tool.

Build a small weather widget from a new React project. A tool returns the weather, Vite builds the widget into one HTML file, and your MCP host displays it.

Use **Node 24.20.0 or later in Node 24**, npm, and matching **Toolgrain 0.3.0 or later** packages. The Vite build recipe is pinned so you can reproduce it.

## 1. Create a Vite project

Run the [Vite CLI](https://vite.dev/guide/#scaffolding-your-first-vite-project) with its React and TypeScript template:

```sh theme={null}
npm create vite@9.3.4 weather-app -- --template react-ts --no-interactive --no-immediate
cd weather-app
npm install
```

Add Toolgrain and the single-file build plugin:

```sh theme={null}
npm install --save-exact @toolgrain/react @toolgrain/mcp @toolgrain/node @toolgrain/protocol zod@^4.6.6
npm install --save-dev --save-exact vite@8.3.4 vite-plugin-singlefile@2.3.4
```

Use matching Toolgrain 0.3.0 or later versions. The commands resolve npm's `latest` tag and save exact versions in your application.

## 2. Define the tool

Create a `server` folder and add this file. The tool's registration key, `weather`, is also the name React will use.

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

export const weatherWidgetUri = "ui://weather/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,
    }),
  }),
};
```

This is deterministic demo data. Toolgrain converts the returned object into structured content and assistant-facing text, so the tool also works in clients that do not display widgets.

## 3. Create the React bindings

Add one small module that connects your components to the tool types:

```ts src/mcp.ts theme={null}
import { createApp } from "@toolgrain/react";
import type { tools } from "../server/tools.ts";

export const { AppProvider, useToolResult } = createApp<typeof tools>();
```

The `import type` is important: it gives React the tool's input/output types without bundling the server code. Create these bindings once, outside your components.

## 4. Replace the starter component

Replace `src/App.tsx`:

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

function Weather() {
  const { result, error, status } = useToolResult();
  if (error) return <p role="alert">{error.message}</p>;
  if (result?.isError) return <p role="alert">Weather could not be loaded.</p>;

  const weather = result?.structuredContent;
  if (!weather) {
    return <p>{status === "cancelled" ? "Request cancelled." : "Waiting for weather…"}</p>;
  }

  return (
    <section>
      <h1>{weather.city}</h1>
      <p>{weather.temperature}°C</p>
    </section>
  );
}

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

**One provider and one hook are enough to display a result.** The provider connects to the host, applies its theme, and reports widget size. `useToolResult()` receives the initiating tool result. The host creates the widget when the tool is called.

Replace the starter's full-page styles with a widget layout:

```css src/index.css theme={null}
:root {
  font-family: var(--font-sans, system-ui, sans-serif);
  color: var(--color-text-primary, #1f2937);
  background: var(--color-background-primary, #fff);
}
body { margin: 0; padding: 16px; }
h1 { margin: 0 0 8px; font-size: 22px; }
p { margin: 8px 0; }
```

## 5. Configure one HTML build

Rename the generated `index.html` to `weather.html` and replace its contents:

```html weather.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>Weather widget</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

The generated `src/main.tsx` renders the `App` you replaced. Now replace `vite.config.ts`:

```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` },
  },
}));
```

In `package.json`, set the `build` script and add `start:mcp`:

```json theme={null}
{
  "scripts": {
    "build": "tsc -b && vite build --mode weather",
    "start:mcp": "node server/index.ts"
  }
}
```

Merge those entries into the existing `scripts` object. In `tsconfig.node.json`, add `server` to `include` so the build also checks your server files:

```json theme={null}
"include": ["vite.config.ts", "server"]
```

`--mode weather` selects `weather.html`. The plugin inlines its JavaScript and CSS into **`dist/widgets/weather.html`**. `publicDir: false` skips the starter's public assets; the HTML above also removes its favicon/logo references.

## 6. Serve the HTML as a resource

Create the MCP server. Keep the widget's presentation and permission settings in the named `widgetUi` object:

```ts server/index.ts theme={null}
import { readFile } from "node:fs/promises";
import { createServer, resource } from "@toolgrain/mcp";
import { stdio } from "@toolgrain/node";
import { APPS_MIME_TYPE, type AppResourceMeta } from "@toolgrain/protocol/apps";
import { tools, weatherWidgetUri } from "./tools.ts";

const widgetUi = {
  prefersBorder: true,
} satisfies AppResourceMeta;

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

const server = createServer({
  name: "weather-app",
  version: "1.0.0",
  tools,
  resources: [weatherWidget],
});

await server.listen(stdio());
```

The tool's `resourceUri` and the resource's `uri` must match exactly. The resource reads your built file; a `ui://` URI is an identifier, not a filesystem location.

The HTML MIME type appears on both the resource definition and the returned content. To request permissions or allow network origins later, edit `widgetUi` using the [permissions and metadata guide](https://toolgrain.mintlify.site/mcp-apps/security.md).

## 7. Build and connect a host

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

You should have `dist/widgets/weather.html`, with its script and styles inside it. The server runs with `npm run start:mcp`. It waits for MCP messages over stdio; it does not open a browser or print a startup message to stdout. [Node 24 runs these TypeScript server files directly](https://nodejs.org/docs/latest-v24.x/api/typescript.html#type-stripping).

In Claude Desktop, open **Settings → Developer → Edit Config** ([host setup guide](https://modelcontextprotocol.io/docs/develop/connect-local-servers)). Add the following entry to its `mcpServers` object, using your Node executable and the absolute server entry path:

```json theme={null}
{
  "mcpServers": {
    "toolgrain-weather": {
      "command": "/absolute/path/to/node",
      "args": ["/absolute/path/to/weather-app/server/index.ts"]
    }
  }
}
```

Find your Node executable with `node -p "process.execPath"`. Replace both paths, save the configuration, and restart Claude Desktop to load it. Ask Claude to call `weather` with `{ "city": "Dubai" }`. An Apps-capable host should load the widget and supply that tool's result.

Opening `weather.html` directly, or visiting Vite's dev server, does not create an MCP host connection. Test the connected widget inside a compatible host. See [host testing and troubleshooting](https://toolgrain.mintlify.site/mcp-apps/compatibility.md) for the recorded environments, setup details, and resource-cache behavior.

## Next steps

* [Add a refresh button](https://toolgrain.mintlify.site/mcp-apps/react.md#add-a-refresh-button) with `useToolCall`.
* [Add a second widget](https://toolgrain.mintlify.site/mcp-apps/build.md) with one HTML file and resource per widget.
* [Request permissions or network access](https://toolgrain.mintlify.site/mcp-apps/security.md) in `widgetUi`.
* [Use host actions](https://toolgrain.mintlify.site/mcp-apps/host-actions.md) when you need links, messages, or display-mode requests.


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