mbag.ai

Documentation

The mailbuttons-langchain package wraps the governed agent surface (/api/v1/mcp/*) as LangChain tools. Every call is scope-checked, capability-gated, and appended to a tamper-evident audit log on the server; the package is a thin, faithful client of that contract.

Install

pip install mailbuttons-langchain

Set the connection from the environment (the token is never inlined):

export MAILBUTTONS_API_KEY="<your scoped agent token>"
export MAILBUTTONS_API_URL="https://mbag.ai"

The agent token comes from provisioning a sandbox inbox with your account API key — see step 1 of the Claude Agent SDK guide; the flow is identical.

Use

from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

from mailbuttons_langchain import get_mailbuttons_tools

tools = get_mailbuttons_tools()  # reads MAILBUTTONS_API_URL / MAILBUTTONS_API_KEY

agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools)
result = agent.invoke(
    {"messages": [("user", "Reply to the latest email in inbox 7 thanking them.")]}
)
print(result["messages"][-1].content)

get_mailbuttons_tools() returns eleven StructuredTools — reading (mailbuttons_list_messages, mailbuttons_get_message, mailbuttons_get_thread, mailbuttons_get_attachment_text), the governed send (mailbuttons_send_email), drafts (mailbuttons_list_drafts, mailbuttons_get_draft), and governance (mailbuttons_extract_code, mailbuttons_propose_sender, mailbuttons_request_promotion, mailbuttons_audit_tail). Creating inboxes and approving drafts are not exposed — those are human, account-key actions, not agent tools.

Governance: blocked/draft are results, not errors

This is the crux of the model. When a send is blocked by policy, or held as a draft for a human to approve, the tool returns that outcome as ordinary data so the agent can read it and reason about what to do next:

{"status": "blocked", "policy": {"decision": "blocked", "matched_rule": "blocklist"}}
{"status": "draft_pending_approval", "policy": {"decision": "draft"}, "reason": "requested_draft", "draft_id": 42}

Only auth / scope / capability / network failures raise MailbuttonsApiError:

from mailbuttons_langchain import MailbuttonsApiError

try:
    ...
except MailbuttonsApiError as e:
    print(e.status, e.code, e.next_step)   # e.g. 403 inbox_out_of_scope "Open a request..."

A token can never widen its own scope. The only escalation path is a human-approved request, which mailbuttons_propose_sender and mailbuttons_request_promotion merely open.