---
title: MCP setup
description: Configure the Postfleet MCP server with a bearer key, the full tool list, and how to handle the 202 pending-approval outcome.
---

# MCP setup

Postfleet ships a Model Context Protocol (MCP) server so an agent can send and read mail as
tools. It is thin-over-REST: every tool calls the same `/api/v1/` endpoints as your key, so
auth, scoping, and quotas are enforced exactly once.

## Configure the server

The server speaks streamable HTTP at `/api/mcp` and authenticates with a bearer `pf_` key. Pass
the key in the `Authorization` header:

```json
{
  "mcpServers": {
    "postfleet": {
      "url": "https://your-app.example.com/api/mcp",
      "headers": { "Authorization": "Bearer pf_..." }
    }
  }
}
```

A request without a `Bearer` token is rejected `401`:

```json
{ "error": "missing bearer token",
  "hint": "Configure this MCP server with an Authorization: Bearer pf_... header." }
```

The key's scope and `can_read`/`can_send`/mailbox binding apply to tool calls exactly as they
do to the REST API — see [Authentication](./auth). Note that `wait_for_email` can hold a request
open up to ~2 minutes, so a deployment must allow a long request duration.

## Tools

| Tool | Arguments | Purpose |
|---|---|---|
| `create_mailbox` | `slug?`, `display_name?`, `extraction_schema_id?` | Create a mailbox this agent owns. |
| `send_email` | `mailbox_id`, `to`, `subject`, `text` | Send a **new** email. |
| `reply_email` | `mailbox_id`, `reply_to_message_id`, `text` | Reply to a received message (threads automatically). |
| `list_inbox` | `mailbox_id`, `limit?` | List recent messages, newest first. |
| `read_email` | `message_id` | Read one message in full (cleaned body, sanitization report, classification, extraction). |
| `wait_for_email` | `mailbox_id`, `from_contains?`, `subject_contains?`, `timeout_seconds?` | Block until a matching email arrives, or time out (`{timed_out:true}`). |
| `create_draft` | `mailbox_id`, `to?`, `subject?`, `text`, `reply_to_message_id?` | Stage a draft without sending. Returns `{id, status:"draft"}`. |
| `list_drafts` | `mailbox_id` | List open drafts (status `draft`, `pending_approval`, `sending`). |
| `send_draft` | `id` | Send a staged draft by id. |

`read_email` never returns the pre-sanitization raw body — only the cleaned, screened content
reaches the agent.

## The 202 pending-approval outcome

If a mailbox requires human approval, `send_email`, `reply_email`, and `send_draft` do **not**
send immediately. They return the queued-draft shape instead of a message id:

```json
{ "draft_id": "d_123...", "status": "pending_approval" }
```

**This is a success, not an error, and it must not be retried.** The email is delivered once a
human approves the draft in the dashboard. To follow up, call `list_drafts` — a `pending_approval`
entry is still waiting on a human; once approved it is sent and tombstoned (drops off the list).

Distinguish the two success shapes:

- `{ "id": "...", "thread_id": "..." }` → sent.
- `{ "draft_id": "...", "status": "pending_approval" }` → queued for human approval.

See [Drafts & human-in-the-loop](./drafts) for the full approval model.

## Errors

Tool errors come back as a short message plus a recovery hint (never a stack trace), for
example an invalid key (`401`), a scope/capability rejection (`403`), quota exhaustion (`402`),
a not-found id (`404`), a suppressed recipient (`422`), or a transient provider issue (`502`,
retry in a few seconds). The underlying status and shapes match the REST API documented
throughout these pages.
