# Vercel AI SDK

> Give an agent built on the Vercel AI SDK a Pidgeon mailbox in one line: create addresses, send, reply, and wait for a verification code.

An agent built on the [Vercel AI SDK](https://ai-sdk.dev) can hold a Pidgeon
mailbox with one line. `@pidgeon-ai/ai-sdk` turns the [MCP server's](https://pidgeon.ai/docs/mcp.md)
tools into AI SDK tools, so the model gets the same names and the same
descriptions whichever way it reaches Pidgeon.

```sh
npm install @pidgeon-ai/ai-sdk ai zod
```

## Sending, and waiting for the answer

```ts
import { generateText, isStepCount } from 'ai';
import { pidgeonTools } from '@pidgeon-ai/ai-sdk';

const { text } = await generateText({
model: 'anthropic/claude-sonnet-5.5',
tools: pidgeonTools({
apiKey: process.env.PIDGEON_API_KEY,
identityId: process.env.PIDGEON_IDENTITY_ID,
}),
stopWhen: isStepCount(10),
prompt:
'Email ops@yourdomain.com from agent@yourdomain.com and ask for the staging access code. ' +
'Wait for the reply and tell me the code.',
});
```

The model calls `send_message`, then `await_message`, which holds one request
open until mail arrives and returns it with `extracted.codes` and
`extracted.links` already pulled out — see [Verification
codes](https://pidgeon.ai/docs/verification-codes.md). There is no polling loop to write.

`generateText` stops after one step unless told otherwise, so `stopWhen` is
what lets the model call a tool and then read its result.

## Options

- **`apiKey`** — a Pidgeon API key. `PIDGEON_API_KEY` is read when you pass
  none, and `pidgeonTools()` throws when there is neither.
- **`baseUrl`** — `https://pidgeon.ai/api/v1` unless you run Pidgeon yourself.
- **`identityId`** — the address a tool uses when the model names none. With
  it set, `identity_id` is optional on every tool that takes one; without it,
  the model finds an address with `list_addresses` and passes it.
- **`client`** — a client from `createClient` in `@pidgeon-ai/sdk`, used
  instead of one built from the options above.

## Asking before anything is sent

Every tool carries `metadata.readOnly`, and `WRITE_TOOLS` names the ones that
change something. Hand them to `toolApproval` and a person confirms each send,
reply and hand-over before it happens:

```ts
import { WRITE_TOOLS, pidgeonTools } from '@pidgeon-ai/ai-sdk';

await generateText({
model,
tools: pidgeonTools(),
toolApproval: Object.fromEntries(WRITE_TOOLS.map((name) => [name, 'user-approval'])),
prompt,
});
```

## The key is the whole of the access control

This hands an API key to a model. Nothing in the package decides what may be
reached: the key's own ceiling and the addresses it was granted do all of it,
server-side, exactly as for any other caller. Give it the narrowest key the
agent needs — a key created at `read` can never send. [Authentication](https://pidgeon.ai/docs/authentication.md)
says how to narrow one.

A refusal reaches the model as a tool error carrying the API's own sentence,
so it can tell a person why rather than retry.

## What it exposes

All 26 of the MCP server's tools, read from the same list:

| Tool                 | Does                                                                                                                                                                                                           | Endpoint                             |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `list_addresses`     | The addresses this credential can reach.                                                                                                                                                                       | `GET /identities`                    |
| `create_address`     | Create an address for an agent to use.                                                                                                                                                                         | `POST /identities`                   |
| `get_address`        | One address, by id.                                                                                                                                                                                            | `GET /identities/{id}`               |
| `list_threads`       | Conversations in one address, newest first.                                                                                                                                                                    | `GET /threads`                       |
| `get_thread`         | One conversation, by id.                                                                                                                                                                                       | `GET /threads/{id}`                  |
| `list_messages`      | The messages of one conversation, oldest first — the text to read before replying.                                                                                                                             | `GET /messages`                      |
| `search_messages`    | Full-text search across every address this credential can reach, ranked by relevance.                                                                                                                          | `GET /messages/search`               |
| `send_message`       | Send from an address this credential may send as.                                                                                                                                                              | `POST /messages`                     |
| `get_message`        | One message, by id, with its body, the headers we keep, and what the sending domain’s records proved — \`authentication.spf\`, \`.dkim\`, \`.dmarc\`, where null means not reported rather than failed.        | `GET /messages/{id}`                 |
| `await_message`      | Waits until a message arrives at an address — after you submit a sign-up form, say — and returns it with \`extracted.codes\` and \`extracted.links\`: verification codes and confirm links, most likely first. | `GET /messages/await`                |
| `reply_to_message`   | The recipients, the subject and the thread are read off the message being answered, so you bring the words and nothing else.                                                                                   | `POST /messages/{id}/reply`          |
| `update_thread`      | Who owns a conversation and where it has got to.                                                                                                                                                               | `PATCH /threads/{id}`                |
| `update_message`     | Star a message, or move it to \`archive\`, \`spam\` or \`trash\`.                                                                                                                                              | `PATCH /messages/{id}`               |
| `list_send_rules`    | The allow and block list of one address, and the send policy it hangs off.                                                                                                                                     | `GET /identities/{id}/send-rules`    |
| `list_receive_rules` | The rules that decide what reaches one address — reject, spam, trash, archive or inbox, per sender or per domain.                                                                                              | `GET /identities/{id}/receive-rules` |
| `list_folders`       | Every folder of one address with its counts.                                                                                                                                                                   | `GET /mailboxes`                     |
| `list_folder`        | One folder of one address, newest first, a conversation per row — where \`list\_threads\` says which conversations exist, this says where they are filed and what they look like in a list.                    | `GET /mailboxes/{folder}`            |
| `list_changes`       | Everything that moved in one mailbox since a \`sync\_cursor\`, arrivals included, so a long-running agent does not re-read a folder to find one new message.                                                   | `GET /mailboxes/changes`             |
| `list_events`        | Every event across every mailbox this credential reaches — mail received, sent, delivered, bounced, filed.                                                                                                     | `GET /events`                        |
| `list_labels`        | Labels belong to an address, not to the workspace, so the same name on three addresses is three labels with three ids.                                                                                         | `GET /labels`                        |
| `create_label`       | A label on one address.                                                                                                                                                                                        | `POST /labels`                       |
| `apply_label`        | The label has to belong to the same address the conversation is in — \`list\_labels\` for that address gives you the right id.                                                                                 | `POST /threads/{id}/labels`          |
| `remove_label`       | Removes one label from one conversation.                                                                                                                                                                       | `DELETE /threads/{id}/labels`        |
| `list_pods`          | A pod is a container for one of your own customers: it holds addresses and domains, and \*\*mail sent from it lands on its own sending reputation\*\*, so one customer cannot damage another’s delivery.       | `GET /pods`                          |
| `create_pod`         | A container for one of your customers, with its own sending reputation.                                                                                                                                        | `POST /pods`                         |
| `get_metrics`        | Sent, delivered, bounced, rejected, complained, received and spam over a window of \`days\` — counted from the event log, so it includes outcomes the provider reported afterwards.                            | `GET /metrics`                       |

Each calls one operation in [`@pidgeon-ai/sdk`](https://pidgeon.ai/docs/sdks.md) — the endpoint
beside it, as the [reference](https://pidgeon.ai/docs/reference.md) describes it, with the same
refusals.
