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 carlyemail claude-agent-sdk fastapi 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

from carlyemail.inbound import create_email_router
from claude_agent_sdk import query
from fastapi import FastAPI

app = FastAPI()


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


app.include_router(
    create_email_router(
        on_email,
        path="/hooks/carlyemail",
        allow_from=["you@example.com"],
    )
)

create_email_router verifies the signature over the raw bytes, admits message.received and nothing else, drops mail the inbox sent itself, checks the sender, ignores redeliveries, and answers the request before Claude starts — so a run that outlasts the delivery timeout is not retried into a second reply. See receiving mail for the options and for doing it without FastAPI.

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. The router handles the transport; the boundary that holds is an inbox-scoped API key. Keep it 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