---
title: "Let a Claude API agent send email (MCP connector)"
description: "Let a Claude API agent send email: attach Unitpost with the MCP connector (mcp_servers plus an mcp_toolset) or a client tool you approve."
url: https://www.unitpost.com/docs/ai/claude-api
section: MCP
updated: 2026-11-02
---
# Let a Claude API agent send email (MCP connector)

## Connection settings

- **Server URL**: https://mcp.unitpost.com/mcp
- **Transport**: Streamable HTTP
- **Auth (recommended)**: OAuth 2.1 — add the URL, approve in the browser (scopes match API capabilities; tokens refresh automatically)
- **Auth (legacy)**: Authorization: Bearer pk_live_YOUR_KEY (workspace API key from Settings → API keys; for clients that can only paste a key)
- **Tools**: one per API operation (email_send, email_campaigns_send, …) (the default set)
- **Toolsets (optional)**: https://mcp.unitpost.com/mcp?toolsets=default,campaigns (or the X-MCP-Toolsets header; add ?readonly=1 for read-only tools)

## Claude API

> Let a Claude API agent send email: attach Unitpost with the MCP connector (mcp_servers plus an mcp_toolset) or a client tool you approve.

The Messages API can call the Unitpost MCP server itself through the MCP connector, with no MCP client on your side. For a person to approve each send, give Claude a client tool that calls the REST API instead.

### 1. Create a send-only key

Open https://www.unitpost.com → Settings → API keys and create a key with Sending only permissions. On a paid plan, open Advanced and keep only `emails:send`, so the key can send email and do nothing else. Copy the `pk_live_…` value (shown once) into the environment your agent runs in.

```bash
export UNITPOST_API_KEY=pk_live_YOUR_KEY
```

> A Sending only key can also send campaigns and read contacts, segments, templates and domains. Keep the key in your server code: the model never needs to see it.

### 2. Attach the MCP connector

Install `anthropic`. Pass the server in `mcp_servers` with your key as `authorization_token` (sent as a Bearer token), and allowlist `email_send` in the matching `mcp_toolset`. The connector needs the `mcp-client-2025-11-20` beta.

```python
import os

import anthropic

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    betas=["mcp-client-2025-11-20"],
    mcp_servers=[
        {
            "type": "url",
            "url": "https://mcp.unitpost.com/mcp",
            "name": "unitpost",
            "authorization_token": os.environ["UNITPOST_API_KEY"],
        }
    ],
    tools=[
        {
            "type": "mcp_toolset",
            "mcp_server_name": "unitpost",
            "default_config": {"enabled": False},
            "configs": {"email_send": {"enabled": True}},
        }
    ],
    messages=[{"role": "user", "content": "Email ada@example.com from hi@yourdomain.com: the report is ready."}],
)

for block in response.content:
    if block.type == "mcp_tool_result":
        print(block.content)  # the send result, or the error envelope
```

> The connector runs `email_send` inside the request, with no pause for approval. `email_send` takes an `idempotency_key` beside `body`. Tell the agent to pass a new one per message and the same one on a retry: the server's instructions say so too.

### 3. Or add one client tool

Your code runs this tool, so it can check and ask before the POST. `@beta_tool` builds the schema from the signature and docstring, and the tool runner loops until Claude is done.

```python
import json
import os

import anthropic
import httpx
from anthropic import beta_tool

ALLOWED = {"ada@example.com"}  # who this agent may email


def post_email(sender: str, to: str, subject: str, text: str, idempotency_key: str) -> str:
    body = json.dumps({"from": sender, "to": to, "subject": subject, "text": text})
    res = httpx.post(
        "https://www.unitpost.com/api/v1/email",
        content=body,
        headers={
            "Authorization": f"Bearer {os.environ['UNITPOST_API_KEY']}",
            "Content-Type": "application/json",
            "User-Agent": "my-agent/1.0",
            "Idempotency-Key": idempotency_key,  # the model's: a retry reuses it
        },
    )
    return res.text  # {"id": ...} or {"error": {"code": ..., "message": ...}}


@beta_tool
def send_email(sender: str, to: str, subject: str, text: str, idempotency_key: str) -> str:
    """Send one email through Unitpost.

    Args:
        sender: From address on a verified domain, e.g. "Acme <hi@acme.com>".
        to: One recipient address.
        subject: Subject line.
        text: Plain-text body.
        idempotency_key: A new value for each message; the same value when
            retrying this one, so a retry never sends twice.
    """
    if to not in ALLOWED:
        return f"Refused: {to} is not on this agent's allowlist."
    return post_email(sender, to, subject, text, idempotency_key)


client = anthropic.Anthropic()
runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=16000,
    tools=[send_email],
    messages=[{"role": "user", "content": "Email ada@example.com from hi@yourdomain.com: the report is ready."}],
)
print(runner.until_done())
```

> The model passes `idempotency_key`: a new one per message, and the same one when it retries that message, so a lost response never becomes a second email. A key made inside the tool would change on every retry. Never derive it from the content, or two separate identical notifications become one. `User-Agent` is required: a request without one is refused with `missing_user_agent` (403).

### 4. Gate each send

Every send is real mail to a real person. Send only transactional mail or mail people opted in to: the Acceptable Use Policy rules out cold outreach. Check each recipient against an allowlist, or have a person approve the send, before the call goes out. Pass an idempotency key, and the same key on a retry: for 24 hours a retry returns the first result instead of sending twice. With the connector, the allowlist in `mcp_toolset` and a send-only key are the controls. With the client tool, ask inside the function before the POST: the tool runner waits for it to return.

```python
@beta_tool
def send_email(sender: str, to: str, subject: str, text: str, idempotency_key: str) -> str:
    """Send one email through Unitpost. sender must be on a verified domain.

    idempotency_key: a new value for each message, and the same value when
    retrying that message, so a retry never sends twice.
    """
    if to not in ALLOWED:
        return f"Refused: {to} is not on this agent's allowlist."
    if input(f"Send '{subject}' to {to}? [y/N] ").strip().lower() != "y":
        return "Refused: a person declined this send."
    return post_email(sender, to, subject, text, idempotency_key)
```

> Errors come back as `{ "error": { "code", "message", "details" } }`. Branch on `error.code` and fix the call instead of resending it: `domain_not_verified` means the `from` domain isn't verified, `validation_error` names the bad fields in `details`, and `rate_limit_exceeded` comes with a `Retry-After` wait. Over MCP the same error arrives under `data`, with a `hint` for the next move.

### 5. Verify the send

Make the first send to an address you own, from an address on a verified domain. A success returns the email's `id` and a `status` of `queued` (`scheduled` when you set `scheduled_at`); over MCP the same object comes back under `data`. Open Emails in the dashboard to see the message, its recipient and its delivery status, or read it back with `email_get` (MCP) or `GET /api/v1/email/{id}`.

```json
{
  "id": "email_…",
  "status": "queued",
  "scheduled_at": null
}
```

> A retry with the same idempotency key returns this same body (REST adds `Idempotent-Replayed: true`) and sends nothing new.

## Related

- [Send email from Cursor](https://www.unitpost.com/docs/ai/cursor): Send email from Cursor's agent through the Unitpost MCP server. Add the server URL, approve with OAuth in the browser, then send a test.
- [Send email from Claude Code](https://www.unitpost.com/docs/ai/claude-code): Send email from Claude Code with the Unitpost MCP server: add the URL, authenticate from /mcp, approve in the browser, then send a test.
- [Send email from Claude Desktop](https://www.unitpost.com/docs/ai/claude-desktop): Send email from Claude Desktop: add Unitpost as a custom connector, approve with OAuth in the browser, and send a test from a verified domain.
