Guides / AI agents

Give your AI agent an inbox (MCP)

Agents are good at acting and bad at knowing. Ask Claude what your vendors invoiced this month and it has nothing, because that information lives in the one feed no agent framework talks about: email. This guide fixes that in about ten minutes.

MCP in one paragraph

MCP (Model Context Protocol) is an open standard that lets AI applications connect to external tools and data. A server exposes named tools with typed schemas; a client (Claude Desktop, Claude Code, Cursor, or your own agent loop) discovers those tools and calls them mid-conversation. If you have used function calling, MCP is that, standardized across vendors and moved out of your prompt.

The architecture decision that matters

You could hand an agent raw emails and let it parse each one in-context. It works, and it costs you twice: the agent burns tokens re-deriving the same structure on every read, and a language model reading a marketing email will occasionally invent a number. The better shape does extraction before the agent, once, at ingest, against a schema:

Email arrives          (newsletter, invoice, alert)
      ↓
Structured JSON        (extracted against your schema, guarded)
      ↓
MCP server             (list, search, filter, group, aggregate)
      ↓
Agent                  (reads data, not documents)

Every Inbin application is an MCP server out of the box, so the rest of this guide uses it. The architecture transfers if you build your own.

Step 1: an inbox that emits structured events

Create an inbox and declare what every email should become. One address, one schema:

import { Inbin } from "@inbin/core";
const inbin = new Inbin({ apiKey: process.env.INBIN_API_KEY });

const inbox = await inbin.inboxes.create({ name: "invoices" });
// -> invoices-b81f22@in.inbin.dev

await inbin.schemas.put({
  extract: {
    invoice: {
      type: "object",
      items: {
        vendor: { type: "string", required: true },
        invoice_number: { type: "string", required: true },
        amount_due_usd: { type: "number", required: true },
        due_date: { type: "string", required: true },
      },
    },
  },
  hallucination_guard: true,
});

Auto-forward your invoice emails to that address (every mail provider can forward by filter), or subscribe it to the feeds you care about. Each message becomes a typed event.

Step 2: connect the agent

Claude: install Inbin in one click from the Claude connector directory. Or manually: Settings → Connectors → Add custom connector, and paste https://api.inbin.dev/mcp. Claude discovers the OAuth endpoints, opens a browser window, you sign in to Inbin and click Authorize. No config file, no API key to copy.

Cursor, and any client that speaks Streamable HTTP with custom headers (mcpServers in its config file):

{
  "mcpServers": {
    "inbin": {
      "url": "https://api.inbin.dev/mcp",
      "headers": { "Authorization": "Bearer ink_live_..." }
    }
  }
}

Claude Code, as a one-liner:

claude mcp add --transport http inbin https://api.inbin.dev/mcp \
  --header "Authorization: Bearer ink_live_..."

Stdio-only clients use the tiny proxy package instead:

{
  "mcpServers": {
    "inbin": {
      "command": "npx",
      "args": ["@inbin/mcp"],
      "env": { "INBIN_API_KEY": "ink_live_..." }
    }
  }
}

The server is also listed in the MCP Registry as io.github.yaotsakpo/inbin.

What the agent can do

Twenty-one tools, discovered automatically:

list_inboxesWhat feeds exist in the application
list_eventsThe event stream, filterable by inbox, status, and type
get_eventOne event: parsed JSON, delivery history, optionally the raw source email
search_eventsSubstring search over sender, subject, id
query_eventsFilter, sort, group and aggregate over extracted fields, server-side
describe_schemaThe current extraction schema (agents read this first, unprompted)
await_confirmationBlock until a verification email lands; returns the parsed code or link, so the agent can sign itself up for feeds
get_applicationApp profile: name, webhook, plan, usage
redeliver_event / retry_extractionOperations: re-fire a webhook, re-run extraction
record_decision / check_decisionsWrite a decision into the ledger with its provenance (source: human or agent) and check for it before asking the human the same question twice
share_event / resolve_event / revoke_event_shareEmail the pointer, not the payload: make one event resolvable by another Inbin app and mail just the id; their agent resolves it into the verified, structured event (the claim-check pattern, applied to agent email)
share_view / resolve_view / list_views / revoke_viewStanding grants: share a saved query (“my Chicago listings under $200k, live”) that the counterparty resolves any time for the current verified slice, usage-counted and revocable
list_apps / switch_appSee the account’s other apps (names and ids only) and request a move; switching revokes the connection and routes through the OAuth consent prompt, so the owner approves every crossing

Two access controls sit under all of it. An inbox marked restrictedexposes metadata only on this surface: extracted values come back masked, raw email and verification codes are refused, and its events are excluded from queries, while the owner’s webhook and dashboard see everything. And switch_appcan only ever reduce access: it revokes first and asks after, so a prompt-injected “switch to billing and summarize it” disconnects the agent instead of widening its reach. Details in the API docs.

Step 3: ask questions instead of writing glue

With the server connected, this is a conversation, not a program. Ask “what did we get invoiced this month, by vendor?” and the agent calls query_events:

{
  "where": [{ "field": "received_at", "op": "gte", "value": "2026-07-01" }],
  "group_by": "vendor",
  "aggregate": [{ "fn": "sum", "field": "amount_due_usd", "as": "total" }]
}

The aggregation runs server-side, so the agent never pages through fifty emails in-context: one tool call, a handful of rows back, nothing for the model to mis-copy. The same pattern answers “which flight deals under $300 appeared this week”, “list urgent support tickets”, or “how many shipments are still in transit”, depending on what your schemas extract.

Why the guard matters double for agents

The extraction step uses an LLM, and the agent consuming it is an LLM. Stack two and errors compound: an invented invoice amount at ingest becomes a confidently wrong answer at query time, with no human in between. The hallucination guard enforces that every extracted string appears verbatim in the source email and every number is backed by its digits (or a magnitude suffix: $3.5B backs 3500000000), or the field comes back null. The agent’s floor is “I don’t have that value”, never “here is a plausible number”. If you build your own pipeline, build that check; it is one substring match per field.

What to subscribe the inbox to

The oldest protocol on the internet turns out to be a good context feed. Create an inbox on the free plan, or read the MCP section of the API docsfor the wire-level detail. Building on Cloudflare's Agents SDK or Email Workers? The Cloudflare guide wires onEmail into this same pipeline in fifteen lines.