Search the docs

Find a page, a section, or an endpoint.

Vercel AI SDK

The same tools as the MCP server, for generateText and agents.

An agent built on the Vercel AI SDK can hold a Pidgeon mailbox with one line. @pidgeon-ai/ai-sdk turns the MCP server's 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. 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 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:

ToolDoesEndpoint
list_addressesThe addresses this credential can reach.GET /identities
create_addressCreate an address for an agent to use.POST /identities
get_addressOne address, by id.GET /identities/{id}
list_threadsConversations in one address, newest first.GET /threads
get_threadOne conversation, by id.GET /threads/{id}
list_messagesThe messages of one conversation, oldest first — the text to read before replying.GET /messages
search_messagesFull-text search across every address this credential can reach, ranked by relevance.GET /messages/search
send_messageSend from an address this credential may send as.POST /messages
get_messageOne 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_messageWaits 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_messageThe 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_threadWho owns a conversation and where it has got to.PATCH /threads/{id}
update_messageStar a message, or move it to `archive`, `spam` or `trash`.PATCH /messages/{id}
list_send_rulesThe allow and block list of one address, and the send policy it hangs off.GET /identities/{id}/send-rules
list_receive_rulesThe rules that decide what reaches one address — reject, spam, trash, archive or inbox, per sender or per domain.GET /identities/{id}/receive-rules
list_foldersEvery folder of one address with its counts.GET /mailboxes
list_folderOne 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_changesEverything 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_eventsEvery event across every mailbox this credential reaches — mail received, sent, delivered, bounced, filed.GET /events
list_labelsLabels belong to an address, not to the workspace, so the same name on three addresses is three labels with three ids.GET /labels
create_labelA label on one address.POST /labels
apply_labelThe 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_labelRemoves one label from one conversation.DELETE /threads/{id}/labels
list_podsA 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_podA container for one of your customers, with its own sending reputation.POST /pods
get_metricsSent, 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 — the endpoint beside it, as the reference describes it, with the same refusals.