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

> Hoe een app tools declareert en hoe hun handlers resultaten retourneren.

# Tools

Een **tool** is een benoemde actie die de AI kan aanroepen, met getypeerde invoer — bijv. `send_email(to, subject, body)`. Tools worden gedeclareerd in de `tools.ts` van een skill en, voor tools die je app aanbiedt, ondersteund door een HTTP-route in een van je componenten. Deze pagina is de bron van waarheid voor het tool-contract; [Skills](/apps/skills) behandelt waar `tools.ts` zich bevindt.

## Een tool declareren

Elk item in `tools.ts` heeft een provider-veilige `name`, een vriendelijke `displayName`, een `description`, een Zod `input`-schema, en een `target` die aangeeft wat Kazzle aanroept.

```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 het herbruikbare adres voor een tool of vereiste-actie-knop. Een directe AI-tool-aanroep en een knopklik kunnen hetzelfde target-object gebruiken, dus beide voeren dezelfde code uit.

| Target   | Velden                                                              | Wordt uitgevoerd waar                                                                                               |
| -------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `app`    | `component`, `path`, `method`, optioneel `query`, `headers`, `body` | In een van je eigen componenten. Kazzle lost de component-URL op en voegt een ondertekend app-identiteitstoken toe. |
| `url`    | `url`, `method`, optioneel `query`, `headers`, `body`               | Een extern eindpunt. De URL-oorsprong en methode zijn letterlijke auteurwaarden.                                    |
| `kazzle` | `name`                                                              | Een ingebouwde client-side Kazzle-handler.                                                                          |

HTTP-targets gebruiken dezelfde aanvraagvelden:

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

Er zijn geen impliciete aanvraagstandaards. Als je handler de onbewerkte tool-invoer als JSON moet ontvangen, schrijf je `body: '${input}'`.

## Referenties

Kazzle lost referenties op in HTTP-target `query`-, `headers`-, `body`- en `url`-velden vóór verzending:

* `${input}` — de hele tool-invoer, of de hele knop-invoer.
* `${input.path}` — een geneste waarde uit de invoer.
* `${env.NAME}` — een benoemde omgevingsvariabele uit de gedeclareerde `env.collection` + `env.environment` van de eigenaar-app-component.

Resolutie is eenmalig. Als een referentie ontbreekt of onbekend is, mislukt de tool in plaats van een lege waarde in te vullen. Gebruik `$${input.name}` wanneer je letterlijke `${input.name}`-tekst nodig hebt.

## Handler-aanvraag

Voor een `app`-target voeg je de overeenkomende route toe in de target-component. Met `body: '${input}'` stuurt Kazzle de getypeerde invoer als JSON-body:

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

`app`-targets ontvangen ook:

| Header                | Waarde                                                                 |
| --------------------- | ---------------------------------------------------------------------- |
| `Authorization`       | `Bearer <identity token>` die de gebruiker + installatie identificeert |
| `Kazzle-Tool-Context` | `{"source":"thread","threadId":"..."}` of `{"source":"api"}`           |

App-targets kunnen `Authorization` niet instellen via `headers`; Kazzle bezit die header. Lees de context met `toolContext(req)` uit `@kazzle/app/tools` wanneer de handler thread vs Tools API moet onderscheiden.

Een tool gedeclareerd zonder overeenkomende route doet niets nuttigs — voeg beide samen toe. `tools.json` wordt niet ondersteund; de app-compiler mislukt als deze er een vindt.

## Handler-respons

Retourneer platte tekst, of JSON met maximaal drie kanalen:

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

* **`content`** — het platte resultaat dat de AI leest (gevoerd naar het model). Vereist.
* **`markdown`** — optioneel. Een korte rich-text-samenvatting weergegeven in de tool-kaart (react-markdown; onbewerkte HTML wordt ontsnapt). Goed voor een zin, een kleine lijst, een inline-link. Een bloot relatief pad wordt weergegeven als dode voorgeformateerde tekst — links moeten **absoluut** zijn.
* **`embedUrl`** — optioneel. Een **absolute** URL die je app aanbiedt; de kaart geeft deze weer in een sandbox-iframe. Dit is hoe je een echte, volledige-breedte-UI toont — een verbindingsscherm, een dashboard, een grafiek. Je app host en bezit de pagina, dus deze kan volledig interactief zijn tegen je eigen backend, cookies en OAuth. Niets wordt naar de drive geschreven.

`embedUrl` wint van `markdown` wanneer beide zijn ingesteld. Behoud altijd een betekenisvolle `content` — dat is wat de AI leest.

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

## Vereiste actie

Een app-tool kan de thread pauzeren wanneer deze een gebruikers- (of apparaat-)actie nodig heeft. Retourneer `type: 'action_required'` met een kaarttitel en knoppen. Alleen geldig wanneer `toolContext(req).source === 'thread'` — over de Tools API (`source: 'api'`), retourneer in plaats daarvan een normale domeinenfout.

```ts theme={"theme":"material-theme-darker"}
import { toolContext, isThreadToolInvocation } from '@kazzle/app/tools';

const context = toolContext(req);
if (!isThreadToolInvocation(context)) {
  return Response.json(
    { code: 'connect_required', error: 'Gmail must be connected before this tool can run.' },
    { status: 409 },
  );
}

return Response.json({
  type: 'action_required',
  title: 'Connect Gmail',
  description: 'Connect Gmail before Kazzle can search messages.',
  // Optional: only this device may run the gated buttons
  // assignee: { computerId: '<from computer { list: {} }>' },
  elements: [
    {
      type: 'button',
      id: 'connect',
      label: 'Connect Gmail',
      variant: 'primary',
      input: { scope: 'gmail.readonly' },
      target: {
        type: 'app',
        component: 'api',
        path: '/oauth/start',
        method: 'POST',
        body: '${input}',
      },
    },
  ],
});
```

Kazzle slaat de kaart op bij de tool-aanroep en hervat via `/chat/resume` nadat een knop is ingedrukt. Overeenkomende clients zien de auteurknoppen; niet-overeenkomende clients zien nog steeds Overslaan/Annuleren plus "Doorgaan op {apparaat}". Een knop zonder `target` dient zijn `input` in als het tool-resultaat. Een knop met een target voert eerst dat target uit, en gebruikt vervolgens het target-resultaat als het tool-resultaat.

### Rich UI — een app-gehoste pagina insluiten

Wanneer het resultaat van een tool visueel of interactief is (een verbindingsscherm, een grafiek, een samenvattingsdashboard), dien je de pagina vanuit een van je componenten en retourneer je de **absolute** URL als `embedUrl`. Bouw de URL op uit de geïnjecteerde component-URL — nooit een relatief pad.

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

De pagina wordt uitgevoerd in een cross-origin iframe (zijn eigen oorsprong, cookies en scripts). Om de kaart in grootte aan te passen, post je de hoogte naar de parent — de kaart luistert hiernaar en past de grootte aan (begrensd tot 70% van de viewport):

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

Geef de voorkeur aan `embedUrl` boven het uitzenden van HTML-strings: je app dient al pagina's, de UI blijft interactief en versioned met je code, en er worden geen per-aanroep-artefacten ergens geschreven.
