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.
npm install @pidgeon-ai/ai-sdk ai zodSending, and waiting for the answer
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_KEYis read when you pass none, andpidgeonTools()throws when there is neither.baseUrl—https://pidgeon.ai/api/v1unless you run Pidgeon yourself.identityId— the address a tool uses when the model names none. With it set,identity_idis optional on every tool that takes one; without it, the model finds an address withlist_addressesand passes it.client— a client fromcreateClientin@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:
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:
| 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 — the endpoint
beside it, as the reference describes it, with the same
refusals.