CarlyEmail docs

Claude Agent SDK

Connect CarlyEmail as a remote MCP server, allow its tools, and wake Claude from signed incoming-email events.

The Claude Agent SDK speaks MCP natively, so CarlyEmail does not need a tool wrapper. Point the SDK at the hosted server and explicitly allow the tools in headless runs.

1. Install and create an inbox

pip install claude-agent-sdk fastapi svix uvicorn
npx carlyemail signup --human-email you@example.com --username assistant
npx carlyemail verify 123456
export CARLYEMAIL_API_KEY=ce_us_...
export CARLYEMAIL_INBOX=assistant@carlyemail.com
export CARLYEMAIL_WEBHOOK_SECRET=whsec_...

2. Add the email tools

Python

import os
from claude_agent_sdk import ClaudeAgentOptions

options = ClaudeAgentOptions(
    mcp_servers={
        "carlyemail": {
            "type": "http",
            "url": "https://api.carlyemail.com/mcp",
            "headers": {
                "Authorization": f"Bearer {os.environ['CARLYEMAIL_API_KEY']}"
            },
        }
    },
    allowed_tools=["mcp__carlyemail__*"],
    system_prompt=(
        f"You manage {os.environ['CARLYEMAIL_INBOX']}. "
        "Read the message and thread before acting. Reply in the existing thread. "
        "Draft rather than send when intent, recipient, or authority is ambiguous."
    ),
)

allowed_tools matters for a non-interactive agent: it permits CarlyEmail's MCP tools without an approval prompt that no terminal user is present to answer. Use specific names instead of the wildcard when the model needs only a few tools, and enforce the same limit with an inbox-scoped API key.

3. Wake Claude from incoming mail

Python

import json
import os

from claude_agent_sdk import query
from fastapi import FastAPI, Request, Response
from svix.webhooks import Webhook, WebhookVerificationError

app = FastAPI()


@app.post("/hooks/carlyemail")
async def on_email(request: Request):
    raw = await request.body()
    try:
        event = Webhook(os.environ["CARLYEMAIL_WEBHOOK_SECRET"]).verify(
            raw, request.headers
        )
    except WebhookVerificationError:
        return Response("Invalid signature", status_code=400)

    if event.get("event_type") != "message.received" or not event.get("message"):
        return Response(status_code=204)

    prompt = (
        "Handle this newly received email. Use CarlyEmail's reply tool if a "
        "reply is appropriate.\n\n" + json.dumps(event["message"])
    )
    async for _ in query(prompt=prompt, options=options):
        pass

    return Response(status_code=204)

Run and register it:

uvicorn app:app --host 0.0.0.0 --port 8000
npx carlyemail webhook https://your-agent.example/hooks/carlyemail \
  --events message.received

Use the event's thread_id as the session or memory key if your Claude runtime stores private working context between emails. CarlyEmail uses it independently to keep replies in the real email conversation.

What the MCP annotations buy you

Every tool carries a title and behavior hints. Read tools are marked read-only; send operations are marked as reaching the outside world; delete operations are marked destructive. These annotations help compatible clients decide when an extra approval is appropriate, but they do not replace API-key permissions.

Warning

A valid signature proves CarlyEmail delivered the event, not that the sender's instructions are safe. Deduplicate by event_id, keep the key server-side, and omit message_send when replies require human approval.

Claude Code and Claude Desktop

The same server also works as an interactive MCP connector with OAuth. See MCP for the no-code configuration.

See also