mbag.ai

Documentation

This guide wires a Claude Agent SDK (TypeScript) app to a governed inbox. Everything below is sandbox-first: nothing here enables external send, promotes a token to production, or widens its scope — those are human actions.

1. Get an agent token

Your account API key (from signup) is a human credential. The agent gets its own scoped token by provisioning a sandbox inbox:

curl -X POST https://mbag.ai/api/v1/mcp/sandbox-inboxes \
  -H "Authorization: Bearer $YOUR_ACCOUNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "My Agent"}'

The response includes sandbox_token — shown exactly once; only its hash is stored. That token is scoped to exactly that inbox, with sandbox capabilities.

2. The key comes from the environment, always

const API_KEY = process.env.MAILBUTTONS_API_KEY;
if (!API_KEY) throw new Error("Set MAILBUTTONS_API_KEY (a scoped sandbox token).");

Never inline the token into source, a tool definition, or a prompt. Set MAILBUTTONS_API_URL=https://mbag.ai alongside it.

3. Attach the MCP server

Point the SDK at the Mailbuttons MCP server so the model calls mailbuttons_* tools directly, launched over stdio:

import { query } from "@anthropic-ai/claude-agent-sdk";

const result = query({
  prompt: userTurn,
  options: {
    mcpServers: {
      mailbuttons: {
        command: "npx",
        args: ["-y", "@mailbuttons/mcp-server"],
        // The token is inherited from the parent env — not passed inline.
        env: { MAILBUTTONS_API_KEY: process.env.MAILBUTTONS_API_KEY! },
      },
    },
    // Let the agent read and send, but route sends through the governed tool.
    allowedTools: [
      "mcp__mailbuttons__mailbuttons_list_messages",
      "mcp__mailbuttons__mailbuttons_get_message",
      "mcp__mailbuttons__mailbuttons_get_thread",
      "mcp__mailbuttons__mailbuttons_send_email",
    ],
  },
});

4. Handle governed send outcomes

A send never "just sends." blocked and draft_pending_approval are normal results, not errors — do not retry to force delivery:

status meaning what to do
sent / queued delivered done
blocked recipient fails policy; policy.matched_rule says which surface the rule; don't retry
draft_pending_approval external recipient, token lacks send_external a human must approve; parked as a draft

If the agent (or a malicious inbound message) tries to email an outside address, this is exactly the wall it hits. That is the product working as designed.

5. Going live

When the integration is ready, the agent calls mailbuttons_request_promotion with the inbox id and the capabilities it wants (e.g. send_external). That returns an approval_url and changes nothing on its own — a human approves it. The agent cannot approve its own request.

Reference implementation

A complete scheduling agent built this way: github.com/mailbuttons/claude-scheduling-agent-ts (Python variant: claude-scheduling-agent-py).