# Connecting an AI agent

**Who's this for?** Anyone using an AI chat client that speaks MCP — Claude Desktop, Claude Code, Codex, Copilot, or your own. Connect once and your agent can read, search, summarise, and reply to mail in your inboxy inbox.

**The short setup.** Most chat clients only need one URL: `https://mcp.inboxy.net`. Paste it into your client's MCP config, restart, sign in once via OAuth. Skip to [Connecting Claude Desktop](#connecting-claude-desktop) for the click-through, or read on for the lay of the land.

**What changes after you connect:** your agent gains a set of tools grouped by prefix — `mail_*` for inbox, `feed_*` and `article_*` for RSS, `address_*` for your aliases, `account_*` for your plan. Read tools are unlimited; the four send-mail tools are scoped so a leaked credential can't be turned into a spam cannon (full rules under [What an agent can send](#what-an-agent-can-send)).

## What the agent can do

Tools are grouped by prefix; most chat clients render them under headings.

### `mail_*` — your inbox

| Tool | Purpose |
|---|---|
| `mail_list` | Recent mail, filterable by classification, unread, address, since |
| `mail_count` | `{ total, unread }` matching a filter — single round-trip |
| `mail_get` | One message: metadata + attachment list |
| `mail_get_body` | Parsed text body + headers |
| `mail_show` | Render an email as Markdown with inline images (proxied) |
| `mail_summarise` | One-line summary via your client's LLM (MCP sampling — **temporarily disabled**; degrades to subject + classification) |
| `mail_forward` | Forward to your verified `personal_email` (recipient locked server-side, idempotent). Does **not** consume `mail_send_quota` |
| `mail_reply` | Reply to the **original sender** — recipient + From locked server-side, 5/hour cap, spam-classified messages refused, 30-day age window, every reply audit-logged. Does **not** consume `mail_send_quota` |
| `mail_mark_read` / `mail_mark_unread` | Single-message read state |
| `mail_mark_all_read` | Bulk; optional `address_id` / `classification` / `before` filter |
| `mail_keep` / `mail_unkeep` | Pin/unpin a message so the retention sweep won't delete it. Cap: 10 currently kept |
| `mail_unsubscribe` | Surface the unsubscribe methods for a message — `List-Unsubscribe` header (mailto + URL), RFC 8058 one-click POST flag, and body links matching /unsubscribe\|opt-out/. Read-only: returns the URL, never visits or POSTs it |
| `mail_send` | Send a brand-new message from one of your `@inboxy.net` aliases to any recipient (external or `@inboxy.net`). **Debits one unit of `mail_send_quota` per recipient** — the same counter the web compose UI debits. Refusal `quota_exhausted` when the bucket is empty. Internal `@inboxy.net → @inboxy.net` is DB-only (no SMTP, no footer); external recipients go via the sender worker and get the inboxy attribution footer. Self-loops and reserved `@inboxy.net` local-parts are refused |
| `mail_invite` | Invite a friend to sign up for inboxy. `to` is any external email. **Capped at 10 per rolling 30 days per user** — independent counter from `mail_send_quota`, refusal `invite_cap_reached`. Caller supplies `personal_note` ≤ 500 chars; subject + body template are operator-controlled. Sent from the `mail.` transactional subdomain. If the invitee signs up via the included one-time link, the inviter is recorded for a future reward feature. Refused if the address already belongs to an inboxy user (`recipient_already_user`) |
| `mail_get_attachment` | Attachment bytes (inline for small images) + signed download URL |
| `mail_search` | Semantic search across your messages |
| `mail_delete` | Permanently delete a message and its embedding. Refused for kept messages |

### `outbox_*` — sent replies

| Tool | Purpose |
|---|---|
| `outbox_list` | List replies you have sent via `mail_reply`, newest first. Filter by `status` / `kind` / `in_reply_to_message_id` |
| `outbox_get` | One sent reply — header block + full `body_text` and `body_html` |
| `outbox_count` | `{ total }` for a status / kind filter — single round-trip |
| `outbox_delete` | Permanently delete one sent reply. Refused if the row is kept |

Status reflects the send pipeline: `queued` (in-flight), `sent` (delivered to the recipient's MX), `rejected` (refused by the sender worker — see `status_reason`), `suppressed` (recipient on the hard-bounce list), `failed` (queue exhausted retries).

### `feed_*` — RSS feeds

| Tool | Purpose |
|---|---|
| `feed_list` | List subscriptions with status + per-feed `{ total, unread }` counts |
| `feed_add` | Subscribe to a feed URL |
| `feed_remove` | Unsubscribe |
| `feed_get` | Detail for one subscription |
| `feed_refresh` | Force a re-poll of one feed (benefits every subscriber) |
| `feed_summarise` | One-paragraph digest of one feed via your LLM (**sampling temporarily disabled**) |

### `article_*` — feed articles

| Tool | Purpose |
|---|---|
| `article_list` | Articles across your feeds, newest first |
| `article_count` | `{ total, unread }` across your subscriptions or one feed |
| `article_get` | One article — link-rewritten HTML + plain text |
| `article_mark_read` / `article_mark_unread` | Per-article state |
| `article_mark_all_read` | Bulk; optional `feed_id` / `before` filter |
| `article_summarise` | One-line summary of one article via your LLM (**sampling temporarily disabled**) |

### `address_*` — your aliases

| Tool | Purpose |
|---|---|
| `address_list` | Your `@inboxy.net` aliases with per-alias `{ total, unread }` counts (`include_retired` opt-in) |
| `address_mint` | Mint a new alias (`adjective-noun` or `opaque` scheme) |
| `address_retire` | Stop accepting mail on an alias |

### `account_*` — plan + quota

| Tool | Purpose |
|---|---|
| `account_get` | One blob: user, subscription (name / since / renews_at), quota (used vs limit, including `kept_messages`, `mail_send_remaining`, `invites_remaining`), rate-limits, retention |

### `mark_*` — cross-domain bulk

| Tool | Purpose |
|---|---|
| `mark_all_read` | Zero both unread mail AND unread articles in one call. Optional `before` ISO-8601 scopes the cut-off. Returns `{ mail_marked, articles_marked }` |

## What an agent CAN send

inboxy is a "give your AI an inbox" service, not "give your AI your Gmail". Every outbound surface is scoped server-side so a leaked credential can't be turned into a spam cannon.

| Surface | Recipient rule | Quota | Footer? | Where it ends up |
|---|---|---|---|---|
| `mail_reply` | Locked to the original sender of the message you're replying to. | Own 5/hour cap; no `mail_send_quota` debit. | Yes (external). | The sender's MX, via the outbound queue. |
| `mail_forward` | Locked to your verified `personal_email`. | Idempotent; no `mail_send_quota` debit. | Yes (external). | Your verified inbox. |
| `mail_send` | **Any address**, external or `@inboxy.net`. Refused if it resolves to your own `personal_email` / own alias / reserved local-part. | **Debits `mail_send_quota`** (one per recipient). Shared with the web compose UI. Refusal `quota_exhausted`. | Yes for external; no for `@inboxy.net`. | Recipient's mailbox (DB-only for `@inboxy.net`). |
| `mail_invite` | **Any external** address that isn't already an inboxy user (`recipient_already_user`). | **10 per rolling 30 days**, independent counter. Refusal `invite_cap_reached`. | Yes (the friend should know who's inviting them). | Recipient's mailbox, via the `mail.` transactional subdomain. Signup link is one-time and binds the inviter→invitee relationship in the `invites` table for a future reward. |

**The internal-vs-external rule:** an `@inboxy.net → @inboxy.net` send (whether between two aliases on your account or across users) is delivered **in-network** — a messages row on the recipient with the raw MIME stored in our EU R2 bucket, fed into the same classifier pipeline that handles inbound MX mail. **No SMTP egress, no Cloudflare Email Sending binding involved.** Any other recipient is queued onto `outbound-send` and the sender worker emits SMTP via the apex `inboxy.net` domain — that's where the attribution footer lands.

**Why a footer at all?** When inboxy is acting as your agentic sender to a third party, that third party deserves to know what they're replying to. The footer is one line of plain text plus a small HTML block; the sender worker is the single place it's injected, so all four tools above behave identically.

## Drop-in skill file

If your client supports user-installed skills (e.g. Claude Code's `~/.claude/skills/`), grab the inboxy skill from `https://inboxy.net/docs/skill` (raw markdown) and drop it at `~/.claude/skills/inboxy/SKILL.md`. The rendered version lives at `https://inboxy.net/help/skill`. The skill briefs the agent on every tool plus the workflow patterns we recommend.

## Available prompts

If your chat client supports MCP prompts (e.g. Claude Desktop's `/` menu), inboxy publishes the following templated prompts. Selecting one seeds a complete playbook into the conversation:

| Prompt | What it does |
|---|---|
| `triage_unread` | Walks your unread inbox, marks spam-ish stuff read, proposes drafts |
| `find_receipts_this_month` | Extracts vendor / amount / date from this month's receipts |
| `summarise_feed` | 24-hour digest of one feed (asks you for a `feed_id`) |
| `draft_unsubscribe_list` | Finds dead newsletters, surfaces unsubscribe URLs |
| `daily_brief` | One paragraph: urgent mail, headlines, account warnings |
| `verify_login_codes` | Lists OTPs / login links from the last hour |

REST callers can read the same prompt bodies via the MCP `prompts/get` endpoint or hard-code the equivalent system message in their own client.

## Connecting Claude Desktop

The MCP server speaks OAuth — no API key needed for chat clients.

1. Open Claude Desktop → **Settings → Developer → Edit Config**.
2. Add:

   ```json
   {
     "mcpServers": {
       "inboxy": {
         "url": "https://mcp.inboxy.net"
       }
     }
   }
   ```

3. Restart Claude Desktop. On first use, it will redirect to inboxy.net to sign you in via OAuth.

## Connecting Claude Code / other MCP clients

Same URL: `https://mcp.inboxy.net`. The client handles OAuth itself.

For clients that need a static API key (CI scripts, server-side automations), use the REST API at `https://api.inboxy.net` with a key minted under **Account → API keys**. The MCP server and REST API expose the same data.

## Connecting OpenClaw (and other unattended agents)

OpenClaw — and similar third-party agent runners (Codex, Microsoft Scout, Hermes Agent, …) — should be wired against a **dedicated alias** so the credential, the inbound mailbox, and the outbound `From:` are all scoped to that one agent. If the key leaks, blast radius is one alias.

### 1. Mint an alias for the agent

1. **Account → Addresses → New address.** Pick the `adjective-noun` scheme (memorable) or `opaque` (un-guessable).
2. Label it for the agent ("openclaw", "codex", etc.) so the inbox view stays legible.

### 2. Mint an address-scoped API key

1. **Account → API keys → New key.**
2. Set **Scope = `manage`** and **Address = `<your-alias>@inboxy.net`** (the dropdown lists your active aliases).
3. Copy the token — it's shown once. Store it in OpenClaw's secret manager (never in repo).

A key scoped to an address can only act on that one alias: `mail_list` / `mail_search` only see that alias's messages, and `mail_send` (or `mail_reply` / `mail_forward`) auto-pin `from_local_part` to the bound alias — a caller-supplied value is ignored. Unscoped or wrong-kind keys get a 403.

### 3. Wire the credential into OpenClaw

`mcp.inboxy.net` accepts **either** an OAuth grant (Claude Desktop, Claude Code) **or** an `Authorization: Bearer ik_...` API key. For unattended agents like OpenClaw, the bearer path is the right one — it skips the browser sign-in and carries your key's address scope through every tool call.

One-shot install (from your OpenClaw shell):

```bash
export INBOXY_KEY=ik_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
openclaw mcp add inboxy \
  --url https://mcp.inboxy.net \
  --header "Authorization=Bearer $INBOXY_KEY"
```

A couple of OpenClaw-specific quirks to know:

- `--header` parses as `Name=Value`, **not** `Name: Value`. Using a colon produces `"Authorization: Bearer …" is an invalid header name`.
- Set `INBOXY_KEY` in your shell or secret store first so the token never lands in OpenClaw's `~/.openclaw/openclaw.json` (where `--header` values are persisted) or your shell history. Treat `~/.openclaw/openclaw.json` like a credential file.

If your OpenClaw build prefers a JSON config block instead of the CLI, the equivalent is:

```json
{
  "mcpServers": {
    "inboxy": {
      "url": "https://mcp.inboxy.net",
      "headers": { "Authorization": "Bearer ik_…" }
    }
  }
}
```

### 3b. Wire the credential into Hermes Agent

Hermes (Nous Research) reads its MCP server list from `~/.hermes/config.yaml`. Add an inboxy entry under `mcp_servers` using the bearer-token form:

```yaml
mcp_servers:
  inboxy:
    url: "https://mcp.inboxy.net"
    headers:
      Authorization: "Bearer ${INBOXY_KEY}"
```

Hermes resolves `${VAR}` placeholders against `~/.hermes/.env`, so drop the token there rather than committing it inline:

```
# ~/.hermes/.env
INBOXY_KEY=ik_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Hermes-specific quirks vs OpenClaw:

- Headers use **colon syntax** (`Authorization: "Bearer …"`), not `Name=Value`. That's the opposite of OpenClaw's CLI quirk — if you copy-paste between the two, watch the separator.
- The config is **YAML**, indentation matters; mis-indenting `headers:` puts the token on the wrong server entry.
- Hermes also supports `auth: oauth` instead of static headers, which triggers the same OAuth 2.1 + DCR flow Claude Desktop uses. We recommend the bearer path for unattended setups because address-scoping carries through every tool call.

### 4. Verify

From OpenClaw, ask the agent to call `account_get` — should return your plan + quota (including `mail_send_remaining`). Then `address_list` should return **only the one alias** (the key is scoped — if you see other addresses, the scope didn't take). Finally test outbound:

```
mail_send to=<your-personal-email> subject="hello" text="ping"
```

You'll see a row in **Account → Outbox** with `source = mail_send` and your `mail_send_remaining` decremented by 1.

### Caps and limits to remember

- **`mail_send_quota` is per-user, not per-alias** — the same counter the web compose UI debits. A leaked address-scoped key can still burn through your bucket, but only from one alias and only against the same per-user cap.
- **Reserved local-parts** (`abuse`, `security`, `support`, `privacy`, `dpo`, `dmarc-rua`) are refused as recipients.
- **Self-loops** are refused — the agent can't mail your own `personal_email` or any of your own aliases via `mail_send`. Use `mail_forward` (which is itself locked to your `personal_email`) if you want a copy of an inbox item delivered upstream.
- **Invites** have their own counter: 10 per rolling 30 days, independent of `mail_send_quota`. Use `mail_invite` rather than `mail_send` when introducing a friend to inboxy.

## Why attachment URLs don't need an API key

`mail_get_attachment` returns either inline bytes (small images) or a short-lived signed URL pointing at `api.inboxy.net/v1/attachments/{token}/download`. That URL **does not require a separate Authorization header** — the embedded HMAC token *is* the auth, validated locally by the api worker. The api bearer-auth middleware explicitly bypasses this path for that reason.

So an MCP client that has OAuth-authorised against `mcp.inboxy.net` can follow the returned URL with a plain `fetch` — no extra credentials, no separate API key.

The URL is single-attachment-scoped and expires (TTL 1 hour). If a token leaks, only that one attachment is exposed, and only until it expires.

## Browser extensions

For Chrome / Firefox / Edge extensions that want to read inboxy mail without going through an agent, use the [browser extension](/help/extension) — it does device-bound (DPoP) auth, which doesn't need a long-lived bearer key.

## Revoking access

- Revoke an OAuth grant from **Account → MCP / OAuth clients**.
- Revoke an API key from **Account → API keys**.

Revocation is immediate; the next request returns 401.
