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

# Tools

> How an app declares tools and how their handlers return results.

# Tools

A **tool** is a named action the AI can call, with typed inputs — e.g. `send_email(to, subject, body)`. Tools are declared in a skill's `tools.ts` and, for tools your app serves, backed by an HTTP route in one of your components. This page is the source of truth for the tool contract; [Skills](/apps/skills) covers where `tools.ts` lives.

## Declaring a tool

Each entry in `tools.ts` has a provider-safe `name`, a friendly `displayName`, a `description`, a Zod `input` schema, and a `target` that says what Kazzle invokes.

```ts theme={"theme":"material-theme-darker"}
import { z } from 'zod';
import type { KazzleTool } from '@kazzle/app/tools';

export const SaveBookmarkInput = z.object({
  url: z.string().url().describe('URL to save.'),
  title: z.string().optional().describe('Optional human-readable title.'),
}).strict();

const saveBookmarkTarget = {
  type: 'app',
  component: 'api',
  path: '/tools/save-bookmark',
  method: 'POST',
  body: '${input}',
} as const;

export const tools = [
  {
    name: 'save_bookmark',
    displayName: 'Save bookmark',
    description: 'Save a bookmark URL and return a confirmation.',
    input: SaveBookmarkInput,
    target: saveBookmarkTarget,
  },
] as const satisfies readonly KazzleTool[];
```

## Targets

`target` is the reusable address for a tool or pending-input button. A direct AI tool call and a button click can use the same target object, so both run the same code.

| Target   | Fields                                                             | Runs where                                                                                          |
| -------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `app`    | `component`, `path`, `method`, optional `query`, `headers`, `body` | One of your own components. Kazzle resolves the component URL and adds a signed app identity token. |
| `url`    | `url`, `method`, optional `query`, `headers`, `body`               | An external endpoint. The URL origin and method are literal author values.                          |
| `kazzle` | `name`                                                             | A built-in client-side Kazzle handler.                                                              |

HTTP targets use the same request fields:

```ts theme={"theme":"material-theme-darker"}
type RequestSpec = {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  query?: Record<string, string>;
  headers?: Record<string, string>;
  body?: unknown;
};
```

There are no implicit request defaults. If your handler should receive the raw tool input as JSON, write `body: '${input}'`.

## References

Kazzle resolves references in HTTP target `query`, `headers`, `body`, and `url` fields before dispatch:

* `${input}` — the whole tool input, or the whole button input.
* `${input.path}` — a nested value from the input.
* `${env.NAME}` — a named environment variable from the owning app component's declared `env.collection` + `env.environment`.

Resolution is single-pass. If a reference is missing or unknown, the tool fails instead of substituting an empty value. Use `$${input.name}` when you need literal `${input.name}` text.

## Handler request

For an `app` target, add the matching route in the target component. With `body: '${input}'`, Kazzle sends the typed input as the JSON body:

```json theme={"theme":"material-theme-darker"}
{ "...": "the typed input" }
```

`app` targets also receive an `Authorization: Bearer <identity token>` header identifying the user + install. App targets cannot set `Authorization` through `headers`; Kazzle owns that header.

A tool declared with no matching route does nothing useful — add both together. `tools.json` is not supported; the app compiler fails if it finds one.

## Handler response

Return plain text, or JSON with up to three channels:

```json theme={"theme":"material-theme-darker"}
{ "content": "...", "markdown": "...", "embedUrl": "https://..." }
```

* **`content`** — the plain result the AI reads (fed to the model). Required.
* **`markdown`** — optional. A short rich-text summary rendered in the tool card (react-markdown; raw HTML is escaped). Good for a sentence, a small list, an inline link. A bare relative path renders as dead preformatted text — links must be **absolute**.
* **`embedUrl`** — optional. An **absolute** URL your app serves; the card renders it in a sandboxed iframe. This is how you show a real, full-width UI — a connect screen, a dashboard, a chart. Your app hosts and owns the page, so it can be fully interactive against your own backend, cookies, and OAuth. Nothing is written to the drive.

`embedUrl` wins over `markdown` when both are set. Always keep a meaningful `content` — that's what the AI reads.

```ts theme={"theme":"material-theme-darker"}
if (req.method === 'POST' && new URL(req.url).pathname === '/tools/save-bookmark') {
  const input = await req.json();
  // ...do the work...
  return Response.json({ content: `Saved ${input.title}` });
}
```

## Pending input

An app tool can pause the thread when it needs the user to act. Return `type: 'pending_input'` with a card title and buttons:

```ts theme={"theme":"material-theme-darker"}
return Response.json({
  type: 'pending_input',
  title: 'Connect Gmail',
  description: 'Connect Gmail before Kazzle can search messages.',
  elements: [
    {
      type: 'button',
      id: 'connect',
      label: 'Connect Gmail',
      input: { scope: 'gmail.readonly' },
      target: {
        type: 'app',
        component: 'api',
        path: '/oauth/start',
        method: 'POST',
        body: '${input}',
      },
    },
  ],
});
```

Kazzle saves the card on the tool call, shows it on every device, and resumes the thread through `/chat/resume` after the user clicks a button. A button without `target` submits its `input` as the tool result. A button with a target runs that target first, then uses the target result as the tool result.

### Rich UI — embed an app-hosted page

When a tool's result is visual or interactive (a connect screen, a chart, a summary dashboard), serve the page from one of your components and return its **absolute** URL as `embedUrl`. Build the URL from the injected component URL — never a relative path.

```ts theme={"theme":"material-theme-darker"}
// A tool that needs the user to connect their account:
return Response.json({
  content: "Gmail isn't connected yet — the user needs to connect it.",
  embedUrl: `${process.env.KAZZLE_APP_COMPONENT_URL}/connect?token=${identity}`,
});
```

The page runs in a cross-origin iframe (its own origin, cookies, and scripts). To size the card, post its height to the parent — the card listens for this and resizes (capped at 70% of the viewport):

```ts theme={"theme":"material-theme-darker"}
// inside the embedded page
parent.postMessage({ __kazzle_height: document.body.scrollHeight }, '*');
```

Prefer `embedUrl` over emitting HTML strings: your app already serves pages, the UI stays interactive and versioned with your code, and no per-call artifacts are written anywhere.
