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

# Tools

> One file under tools/ is one MCP tool. defineTool takes a title, a description, Zod shapes for input and output, hints that become annotations, and a run function.

A file at `tools/<name>.ts` that default-exports `defineTool({ ... })` registers as the MCP tool `<name>`. Rename the file and the tool renames with it. `.ts`, `.tsx` and `.mts` are picked up.

```ts tools/check-eligibility.ts theme={null}
import { defineTool } from "@waniwani/kit";
import { z } from "zod";
import { buildPlans } from "../lib/plans.js";

export default defineTool({
  title: "Check instalment eligibility",
  description:
    "Work out which instalment plans a basket qualifies for. Call this before showing any plans, and before quoting any figure.",
  input: {
    amount: z.number().positive().describe("Basket total in euros, e.g. 249.90"),
    country: z.enum(["FR", "ES", "PT"]).default("FR"),
  },
  output: {
    eligible: z.boolean(),
    plans: z.array(z.object({ id: z.string(), monthly: z.number(), fee: z.number() })),
  },
  hints: { readOnly: true },
  run: ({ amount, country }) =>
    amount < 50
      ? { eligible: false, plans: [] }
      : { eligible: true, plans: buildPlans(amount, country) },
});
```

## Fields

<ResponseField name="title" type="string" required>
  Shown to humans in connector UIs.
</ResponseField>

<ResponseField name="description" type="string" required>
  LLM-facing. The only thing the model reads before deciding to call this, so it says when to call it and what not to do instead. A tool with no description fails the build check.
</ResponseField>

<ResponseField name="input" type="Shape">
  A Zod shape written as a plain object, `{ amount: z.number() }` rather than `z.object({ ... })`. `.describe()` on a field reaches the model as that argument's description. `run` receives the parsed value, typed off this shape.
</ResponseField>

<ResponseField name="output" type="Shape">
  The structured output schema, also a plain Zod shape. The runtime advertises it in `tools/list` and validates what `run` returns against it.
</ResponseField>

<ResponseField name="hints" type="ToolHints">
  Behavioural hints for the host LLM. The runtime translates them into MCP `annotations` and always fills in the `title` annotation that Claude's Connectors Directory requires.

  <Expandable title="hints">
    <ResponseField name="readOnly" type="boolean" default={false}>
      The tool only reads. Defaults to `true` for widgets.
    </ResponseField>

    <ResponseField name="destructive" type="boolean">
      The tool can destroy data.
    </ResponseField>

    <ResponseField name="openWorld" type="boolean">
      The tool reaches out to the open internet.
    </ResponseField>

    <ResponseField name="idempotent" type="boolean">
      Calling twice with the same input has the same effect as calling once.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="run" type="(input) => ToolResult | Promise<ToolResult>" required>
  The handler. It may be async.
</ResponseField>

## What run can return

| Return                  | Becomes                                         |
| ----------------------- | ----------------------------------------------- |
| a string                | the model-facing text                           |
| a plain object          | `structuredContent`, plus a JSON text fallback  |
| a full `CallToolResult` | passed through, for the rare tool that needs it |

A `run` that throws returns an error envelope telling the host to offer a retry rather than invent a result, so exceptions are safe to let propagate.

## Writing the description

The `description` is what the model sees at the moment it is choosing a tool, so the starter template's version tells the model when to call it and what to avoid:

```ts theme={null}
description:
  "Find products matching what the shopper asked for. Call this before naming any product or quoting any price, and never answer either from memory. Pass the shopper's own words as the query.",
```

How the tools fit together across a conversation goes in the app's [`overview`](/kit/app-config) instead, which the host reads once at connect.

## Tool or endpoint

A tool appears in `tools/list` and costs a turn, and the model can reason about what came back. An [endpoint](/kit/endpoints) under `api/` is invisible to the model and is called by the widget itself from the browser. Reach for a tool when the model is the caller.
