Search the docs

Find a page, a section, or an endpoint.

MCP

The hosted server, and the one that runs beside your agent.

An agent can hold a Pidgeon inbox directly: read it, answer from it, create addresses in it. Pidgeon speaks Model Context Protocol over the same API this documentation describes — a protocol adapter, and nothing more.

There are two ways in and they are the same server. The hosted one needs no install; the package runs it on your machine.

Hosted, which is a URL

sh
claude mcp add --transport http pidgeon https://pidgeon.ai/api/mcp

The first call answers 401, and the client sends you to Pidgeon to sign in. You choose what it can do — read, read and send, or full access — and what it can reach: one address, or every address if you own the account. That choice becomes an API key, and the client holds a token for it. It shows up under Connected apps, and Disconnect there ends it on the client's next call.

Claude's custom connectors, ChatGPT, Grok, Cursor, Codex and VS Code take the same URL and sign in the same way. Each request is answered on its own: the server holds no session, because it has no state of its own to keep.

For an agent with nobody there to sign in, send a key instead:

sh
claude mcp add --transport http pidgeon https://pidgeon.ai/api/mcp \
--header "Authorization: Bearer $PIDGEON_API_KEY"

x-api-key works too. Cursor, Codex (which reads TOML, not JSON), VS Code, Windsurf and Gemini CLI take the same URL and header; the integrations list has each one.

In Grok

In Grok, Connectors → Custom, paste https://pidgeon.ai/api/mcp, and sign in at the consent screen; no key is needed. Meta's Muse is in review with Meta.

In Cursor

Add Pidgeon to Cursor from its cursor.directory listing, which carries the hosted server and the skill.

Or add the server by hand, in ~/.cursor/mcp.json (or .cursor/mcp.json for one project). The URL is all it needs; Cursor signs you in on the first call:

json
{
"mcpServers": {
  "pidgeon": {
    "url": "https://pidgeon.ai/api/mcp"
  }
}
}

For an agent with nobody there to sign in, add "headers": { "Authorization": "Bearer ${env:PIDGEON_API_KEY}" } beside the URL, and Cursor fills it from your environment.

Codex, VS Code and Gemini CLI

For Codex, VS Code and Gemini CLI, one command writes the server into the client's own file:

sh
npx -y @pidgeon-ai/mcp init --client codex    # ~/.codex/config.toml
npx -y @pidgeon-ai/mcp init --client vscode   # .vscode/mcp.json
npx -y @pidgeon-ai/mcp init --client gemini   # .gemini/settings.json

With no key set it writes the URL alone, and the client signs you in. With PIDGEON_API_KEY set it checks the key first, then has the client read it from that variable (Codex, Gemini CLI) or ask for it once (VS Code), so the key never lands in the file. It adds to what is there, comments included, copies a file it changes to <file>.bak, does nothing the second time, and replaces a different pidgeon entry only with --force. --scope user or --scope project picks the other file; --print writes nothing.

Or installed, which is a package

sh
PIDGEON_API_KEY=pgn_live_… npx -y @pidgeon-ai/mcp init

That checks the key against the API before it writes anything, and tells you which addresses it reaches — the one question a block of JSON cannot answer, and the one that otherwise surfaces as a model quietly getting an error you never see. Then it writes .mcp.json in the current directory and prints what it wrote. It never replaces an entry that is already there: --force does that, and --print shows the config without writing anything, which is what you want if your client keeps its own somewhere else.

The key is referenced from the environment rather than written into the file, so the config can be committed and the credential cannot:

json
{
"mcpServers": {
  "pidgeon": {
    "command": "npx",
    "args": ["-y", "@pidgeon-ai/mcp"],
    "env": { "PIDGEON_API_KEY": "${PIDGEON_API_KEY}" }
  }
}
}

Claude Desktop, or any other MCP client, takes the same block. To run the server directly, which is mostly useful for watching it start:

sh
PIDGEON_API_KEY=pgn_live_… npx -y @pidgeon-ai/mcp

PIDGEON_BASE_URL points it somewhere other than production, which is only useful if you are running Pidgeon yourself.

The key is the whole of the access control

This hands an API key to a model — one you minted, or one you granted by signing in. 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. There is no second set of rules here to get wrong.

So give it the narrowest key the agent needs. A key created at read can never send, however generously it is granted an inbox — see Authentication for what a key's reach means and how to narrow it. An agent that only triages should hold a key that only reads, and then the question of whether it might send something embarrassing is not a question about the prompt.

A key's reach is a snapshot of the addresses it was granted, so an address created after the key is not in it.

What it exposes

All 61 of them, read from the server's own list so this page cannot be behind it:

ToolDoesEndpoint
get_setupCall this first.GET /agent/setup
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
check_messageAsks whether `send_message` with these arguments would go, and sends nothing.POST /messages/check
verify_senderChecks a raw message, or its headers, and answers whether an AI agent sent it, on whose behalf, and whether the signatures on it hold: the signed agent declaration (`Agent-of`) and the `Post-Envelope`, from Pidgeon or any other signer using the same formats.POST /verify
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, and `.signature: "pass"` when another Pidgeon address sent it, signed and checked before it was stored.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
forward_messageSends one message on to somebody it was not addressed to, with its attachments, from the address that received it.POST /messages/{id}/forward
create_draftWrites a message without sending it, for a human to read and send — use it when you are asked to prepare replies for sign-off.POST /drafts
get_draft_approvalWhere a held send stands, by its `draft_id`: `pending` until a person approves or rejects it in the dashboard, then `approved` (it was sent, and `message_id` is the message), `rejected`, or `expired` after 24 hours with nobody deciding.GET /drafts/{id}/approval
list_draftsEvery draft at one address, newest first, with its recipients and body — so you can come back to one and finish it.GET /drafts
get_draftOne draft by id: its sender, recipients, subject, text and attachments.GET /drafts/{id}
update_draftChanges the fields you pass and leaves the rest; `null` clears the subject or text.PATCH /drafts/{id}
delete_draftDeletes one draft, and cancels its send if it was scheduled.DELETE /drafts/{id}
schedule_draftSends one draft later, from Pidgeon’s side, with no further call: it goes even if you and the person are offline.POST /drafts/{id}/schedule
unschedule_draftTakes a scheduled draft off the schedule.DELETE /drafts/{id}/schedule
read_attachmentThe text of one attachment — a CSV, a log, JSON, a calendar file, or a PDF, a Word `.docx` or an Excel `.xlsx`.GET /attachments/{id}/text
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 — and its `receive_policy`.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 inbox 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 address 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
list_domainsThe domains this account sends from, with `status` (`verified` or still waiting on DNS) and `pod_id`, the pod each one sends from.GET /domains
add_domainAdds a domain to send from, such as `team.example.com`, and answers the DNS `dns_records` to publish where its DNS is kept.POST /domains
get_domainOne domain, by id, with every DNS record it needs and whether each is found yet.GET /domains/{id}
verify_domainLooks the domain’s DNS up now and answers whether it is verified and which records are still missing or wrong.POST /domains/{id}/verify
list_api_keysThis account’s keys: name, prefix, what each may reach (`pod_id`, `identity_ids` or every address), its `access`, when it was last used, and whether it is revoked.GET /api-keys
create_api_keyA new key for a server or another agent.POST /api-keys
revoke_api_keyStops a key working, at once and for good, with every key it made.DELETE /api-keys/{id}
list_webhooksThe URLs this account posts events to, which address and events each one hears, and whether deliveries are failing.GET /webhooks
create_webhookPosts events to `url` as they happen.POST /webhooks
delete_webhookStops posting to that URL.DELETE /webhooks/{id}
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
get_calendarThe calendar of one address, by the address’s `id`: its default time zone and how many events it holds.GET /identities/{id}/calendar
update_calendarThe time zone an event created without one gets, as an IANA name like `Europe/Lisbon`.PATCH /identities/{id}/calendar
get_agendaWhat is on an address’s calendar between `after` and `before` (ISO 8601 date-times), in start order, with each date of a recurring event listed on its own.GET /identities/{id}/calendar/agenda
find_free_timeWhen every address in `identity_ids` (comma-separated address ids, up to 10) is free between `after` and `before` (ISO 8601; default the next 7 days, 31 at most), in stretches at least `duration_minutes` long (default 30).GET /calendar/free-time
get_calendar_eventOne event by `eventId` on the calendar of address `id`: its times, description, location, recurrence, and its attendees with their answers.GET /identities/{id}/calendar/events/{eventId}
create_calendar_eventPuts an event on the calendar of address `id`.POST /identities/{id}/calendar/events
update_calendar_eventChanges one calendar event: the fields you pass, leaving the rest.PATCH /identities/{id}/calendar/events/{eventId}
delete_calendar_eventRemoves an event from the calendar of address `id`.DELETE /identities/{id}/calendar/events/{eventId}
respond_to_invitationAnswers a meeting invitation that arrived as mail, by the message’s `id`: `accepted`, `declined` or `tentative`, with an optional `comment`.POST /messages/{id}/invitation/respond
find_contactTurns a name into an address: “email Priya” → `search: "priya"`.GET /contacts
update_contactChanges one contact by its `id` from `find_contact`: the fields you pass, leaving the rest.PATCH /contacts/{id}
block_contactFrom now on, mail from this contact to the address they are a contact of goes to spam, by the contact’s `id`.POST /contacts/{id}/block

Each is the endpoint beside it in the reference, with the same refusals. A tool that is forbidden to the key fails the way the API fails, with the sentence the API would have used.

Search covers every address the key reaches unless you name one, and each result says which address it was found in.

Send later

To send at a time, pass send_at to send_message or reply_to_message (ISO 8601 with an offset, such as 2026-11-04T09:00:00+01:00, a minute to 90 days ahead), or schedule_draft for a draft already written. Pidgeon sends it then, from its side, even if you and your agent are offline, so prefer it to a task your agent schedules for itself. The answer is status: scheduled with a cancel call, and unschedule_draft takes it back. Each scheduled message goes on its own: one refused never stops the rest. On an address that asks a person first, it waits for their approval and goes at its time once they give it.

What it is not

It is not an agent runtime, and it does not decide anything. Pidgeon has no opinion about which model you run or what it is allowed to conclude — the same position Threads and hand-off takes about the rest of the product. If the agent gets stuck, handing the conversation to a person is a thread status, not a feature of this package.

Further reading: email MCP servers for sending and receiving, compared.