# MCP

> Give an agent a Pidgeon mailbox over Model Context Protocol: a hosted server that needs no install, and a package for clients that cannot send a header.

An agent can hold a Pidgeon mailbox directly: read it, answer from it, create
addresses in it. Pidgeon speaks [Model Context
Protocol](https://modelcontextprotocol.io) 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 **Developers → API keys → Connected apps**, and Disconnect there ends
it on the client's next call.

Claude's custom connectors, ChatGPT, 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. Step by step for [Cursor](https://pidgeon.ai/integrations/cursor),
[Codex](https://pidgeon.ai/integrations/codex) (which reads TOML, not JSON) and
[VS Code](https://pidgeon.ai/integrations/vscode); Windsurf and Gemini CLI take the same URL
and header.

## 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 a mailbox — see
[Authentication](https://pidgeon.ai/docs/authentication.md) 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 28 of them, read from the server's own list so this page
cannot be behind it:

| 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`          |
| `create_draft`       | Writes 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`                       |
| `read_attachment`    | The text of one attachment — a CSV, a log, JSON, a calendar file.                                                                                                                                              | `GET /attachments/{id}/text`         |
| `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 is the endpoint beside it in the [reference](https://pidgeon.ai/docs/reference.md), 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.

## 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](https://pidgeon.ai/docs/threads.md) 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.
