CarlyEmail docs

LangChain and LangGraph

Load CarlyEmail's hosted MCP tools, invoke a LangGraph agent from verified mail, and preserve one graph thread per email thread.

The simplest LangChain MCP integration uses CarlyEmail's hosted server. It supplies the live tool schemas, while a FastAPI webhook invokes the graph whenever a real email arrives.

1. Install and create an inbox

pip install carlyemail langchain langchain-openai langchain-mcp-adapters fastapi uvicorn
npx carlyemail signup --human-email you@example.com --username assistant
npx carlyemail verify 123456
export OPENAI_API_KEY=sk-...
export CARLYEMAIL_API_KEY=ce_us_...
export CARLYEMAIL_INBOX=assistant@carlyemail.com
export CARLYEMAIL_WEBHOOK_SECRET=whsec_...

2. Load CarlyEmail's tools

Python

import asyncio
import os

from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient


async def main():
    client = MultiServerMCPClient({
        "carlyemail": {
            "transport": "http",
            "url": "https://api.carlyemail.com/mcp",
            "headers": {
                "Authorization": f"Bearer {os.environ['CARLYEMAIL_API_KEY']}"
            },
        }
    })
    tools = await client.get_tools()
    agent = create_agent(
        model="openai:gpt-5-mini",
        tools=tools,
        system_prompt=(
            f"You manage {os.environ['CARLYEMAIL_INBOX']}. "
            "Read the message and thread before acting. "
            "Draft rather than send when intent or authority is ambiguous."
        ),
    )
    result = await agent.ainvoke({
        "messages": [{"role": "user", "content": "Summarize new mail."}]
    })
    print(result["messages"][-1].content)


asyncio.run(main())

MCP removes the wrapper-maintenance step. Filter tools before passing them to create_agent when the model needs only a smaller surface, and enforce the allowed operations with an inbox-scoped API key.

3. Make incoming email invoke the graph

This complete FastAPI app keeps a graph checkpoint thread for each CarlyEmail thread_id:

Python

import json
import os
from contextlib import asynccontextmanager

from carlyemail.inbound import create_email_router
from fastapi import FastAPI
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.checkpoint.memory import InMemorySaver


@asynccontextmanager
async def lifespan(app: FastAPI):
    client = MultiServerMCPClient({
        "carlyemail": {
            "transport": "http",
            "url": "https://api.carlyemail.com/mcp",
            "headers": {
                "Authorization": f"Bearer {os.environ['CARLYEMAIL_API_KEY']}"
            },
        }
    })
    app.state.agent = create_agent(
        model="openai:gpt-5-mini",
        tools=await client.get_tools(),
        checkpointer=InMemorySaver(),
        system_prompt=(
            "You manage an email inbox. Read the thread before acting. "
            "Reply in the existing thread, and draft when unsure."
        ),
    )
    yield


app = FastAPI(lifespan=lifespan)


async def on_email(email):
    await app.state.agent.ainvoke(
        {
            "messages": [{
                "role": "user",
                "content": (
                    "Handle this newly received email. Use CarlyEmail's reply "
                    "tool if a reply is appropriate.\n\n" + json.dumps(email.message)
                ),
            }]
        },
        {"configurable": {"thread_id": email.thread_id}},
    )


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 the graph runs — 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.

InMemorySaver makes the example runnable. Replace it with a durable LangGraph checkpointer in production, keeping CarlyEmail thread_id as the graph thread_id.

Run and register the route:

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

Without MCP

carlyemail-toolkit gives you the same tools as native LangChain tools, with no MCP client in the loop, in Python and in JavaScript:

Python

from langchain.agents import create_agent
from carlyemail_toolkit.langchain import CarlyEmailToolkit

agent = create_agent("openai:gpt-5-mini", CarlyEmailToolkit().get_tools())

TypeScript

import { createAgent } from "langchain";
import { CarlyEmailToolkit } from "carlyemail-toolkit/langchain";

const agent = createAgent({ model: "openai:gpt-5-mini", tools: new CarlyEmailToolkit().getTools() });

See Toolkits for naming only some tools and for how a call finds its inbox.

langchain-carlyemail adds a message loader, a search retriever, and a verified inbound router:

pip install "langchain-carlyemail[webhooks]"

Python

from langchain_carlyemail import CarlyEmailLoader, CarlyEmailRetriever

documents = CarlyEmailLoader("assistant@carlyemail.com", max_messages=200).load()
retriever = CarlyEmailRetriever.from_api_key("assistant@carlyemail.com", limit=10)

Its create_email_router wraps the same receiver shown above, and hands your callback a CarlyEmailEvent rather than an InboundEmail.

Warning

Email bodies and attachments are untrusted model input, and a verified delivery says CarlyEmail sent it — not that the sender's instructions are safe to follow. The router handles the transport; the boundary that holds is an inbox-scoped API key. Omit message_send when a person should approve drafts before they leave.

See also