# MCP vs SDK vs API for AI agent email

> Should an AI agent get email through an MCP server, an SDK, the REST API or a CLI, and what does each cost?

## The short answer

Use MCP when a model in somebody else's client, such as Claude Code, Cursor or ChatGPT, should decide when to read and send. Use an SDK when your own code runs the loop and calls the model, the REST API when you want no dependency or another language, and the CLI when the agent has a shell. With Pidgeon all four are the same API underneath, so the same task meets the same keys, limits and refusals whichever way in you choose.

## The task

Every section below does the same small job: wait for the next email at the agent's address, then reply to it. Two calls, and both are the ones an email agent makes most.

## MCP: the model holds the tools

Add the hosted server to your client. The first call sends you to Pidgeon to sign in and choose what the agent can do and which address it can reach:

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

From then on the model makes the calls. Asked to answer the next email, it calls `await_message`, then `reply_to_message` with the id it got back:

```json
[
  {
    "tool": "await_message",
    "arguments": { "identity_id": "0c939bd7-2d69-4150-a2ac-de994f0e4887", "timeout": 50 }
  },
  {
    "tool": "reply_to_message",
    "arguments": {
      "id": "af08b0a1-284f-464a-a36e-8e15bb7c2eb8",
      "text": "Thanks, the access code worked."
    }
  }
]
```

**Choose it when** the agent already lives in an MCP client, or when you want the model to decide which of many actions to take. Three things come with it that you would otherwise write. Message bodies arrive as the Firewall's clean text unless the model asks for the original. A send with no `idempotency_key` gets one derived from the call, so a model retrying the same reply in the same hour does not send it twice. And each tool's description tells the model what to do when it is refused, including never re-sending after a wait times out.

**The cost** is control. The model chooses the calls, so the key is the access control: give it the narrowest one the job needs. A key created at `read` cannot send, however the prompt is phrased. Every tool description also takes room in the model's context.

If you build on the Vercel AI SDK, `@pidgeon-ai/ai-sdk` turns the same tools into AI SDK tools with the same names. There are guides for [LangChain](https://pidgeon.ai/docs/langchain.md), the [OpenAI Agents SDK](https://pidgeon.ai/docs/openai-agents.md), the [Claude Agent SDK](https://pidgeon.ai/docs/claude-agent-sdk.md) and others under [integrations](https://pidgeon.ai/docs/integrations.md).

## The SDK: your code runs the loop

```ts
import { client, messagesAwait, messagesReply } from '@pidgeon-ai/sdk';

client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });

const { data } = await messagesAwait({
  query: { identity_id: identityId, timeout: 50 },
  throwOnError: true,
});

const message = data.data.message;
if (message) {
  await messagesReply({
    path: { id: message.id },
    headers: { 'Idempotency-Key': `reply-${message.id}` },
    body: { text: await draftReply(message) },
    throwOnError: true,
  });
}
```

**Choose it when** your program decides what happens and calls the model for the words. The calls are typed from the contract, and the TypeScript package adds the two things a generator cannot: `verifyWebhook`, for the signature on incoming events, and `streamEvents`, a held-open read of the event log. The Python package, `pip install pidgeon`, names every operation the same way (`messages_await`, `messages_reply`).

**The cost** is that you write what MCP gives you. The SDK makes no idempotency key of its own, because a random one protects nothing and one derived from content would swallow a send you meant. So you choose the key, as above.

## The REST API: no dependency

```sh
curl -G https://pidgeon.ai/api/v1/messages/await \
  -H "Authorization: Bearer $PIDGEON_API_KEY" \
  -d identity_id=$IDENTITY_ID \
  -d timeout=50

curl https://pidgeon.ai/api/v1/messages/$MESSAGE_ID/reply \
  -H "Authorization: Bearer $PIDGEON_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-$MESSAGE_ID" \
  -d '{ "text": "Thanks, the access code worked." }'
```

**Choose it when** you work in a language without an SDK, in a workflow tool that makes HTTP requests, or when you want to see every byte. The contract is published at [pidgeon.ai/openapi.json](https://pidgeon.ai/openapi.json), and every error is `{ error: { code, message, field? } }` with a `code` to branch on. For a server, a [webhook](https://pidgeon.ai/docs/webhooks.md) on `message.received` replaces the wait.

**The cost** is the plumbing: retries, signatures and timeouts are yours. A long wait also needs an HTTP client that allows it, up to 280 seconds.

## The CLI: an agent with a shell

```sh
pidgeon messages await --identity_id $IDENTITY_ID --timeout 50
pidgeon messages reply --id $MESSAGE_ID --text "Thanks, the access code worked." \
  --idempotency-key reply-$MESSAGE_ID
```

**Choose it when** the agent is a coding agent that already runs commands, in a script, or in CI. The output is the API's JSON on stdout, so `jq` works, and a refusal exits `1` with the API's code and sentence on stderr. Every operation is a command, generated from the same contract, so the CLI cannot offer one the API does not have.

**The cost**: a command is a process, so it is slower than a call from inside a program. Without `--idempotency-key` each run makes up its own key, and prints it when a send may have gone out without an answer, so a script can retry with it. A second run that does not reuse it is a second send, so pass the key yourself when a script might run twice.

## Choosing

| You have                                         | Use          |
| ------------------------------------------------ | ------------ |
| A model in Claude Code, Cursor, ChatGPT or Codex | MCP          |
| Your own TypeScript or Python agent loop         | The SDK      |
| Another language, or a workflow tool             | The REST API |
| An agent or script with a terminal               | The CLI      |

They mix. A common shape is a webhook into your own code with the SDK, and MCP for a person asking Claude about the same inbox. All four use the same API keys, so the key decides what each can reach.

## Start

[Sign up](https://pidgeon.ai/signup) and create an address. Then [MCP](https://pidgeon.ai/docs/mcp.md), [the SDKs](https://pidgeon.ai/docs/sdks.md), [the CLI](https://pidgeon.ai/docs/cli.md), or the [API reference](https://pidgeon.ai/docs/reference.md). Or paste this into your coding agent and let it set itself up:

```text
Read pidgeon.ai/skill.md and get yourself an email address.
```

## Questions

### When should an agent use MCP for email?

When the agent is a model in an MCP client such as Claude Code, Cursor or ChatGPT and should choose the calls itself. The key it holds is the access control, so make it the narrowest the job needs.

### Do the MCP server and the SDKs expose the same operations?

Not all of them. The SDKs and the CLI are generated from the API contract and cover every operation; the MCP server offers a chosen set of tools, each naming the endpoint it calls and refused the way that endpoint refuses.

### Which one makes an idempotency key for me?

The MCP and AI SDK tools derive one from the call when none is passed, and the CLI makes one per run. The SDKs make none; pass your own.
