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 — and import old mail from elsewhere so it can search that too.
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 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, digest_* for your daily digest. 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 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 of one message, generated server-side and cached so later reads are free. mail_summarize is an identical alias for the American spelling |
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_attach_inline |
Upload one attachment for a later mail_reply by passing its bytes inline as base64 — no separate HTTP round-trip. For files too large to inline, use mail_create_upload |
mail_attach_url |
Upload one attachment for a later mail_reply by giving a URL that we fetch server-side. Accepts https://, http://, and data:. SSRF-blocked: loopback, link-local, RFC1918, and Cloudflare-internal hosts are refused |
mail_create_upload |
Reserve a short-lived signed PUT URL for one large attachment; the PUT returns an id for mail_reply.attachment_ids |
mail_get_attachment |
Attachment bytes (inline for small images) + signed download URL |
mail_search |
Semantic search across your messages |
mail_import |
Import one historic .eml (base64, ≤6 MB) onto one of your aliases so search and reads cover it. Archive write only: never forwarded, never digested, stored as read, retained from the import date. Mail you sent yourself is accepted and flagged sent_by_user (mail_reply refuses it). Dedup on Message-ID — re-sending is a no-op. 500 per calendar month on Early Adopter (5000 paid); bulk backfills are easier over REST POST /v1/mail/import (10 MB, raw bytes). See Importing old mail |
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, generated server-side |
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, generated server-side |
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 |
address_record_attribution |
Record that an alias was given to a particular site, so you can see which alias you handed to whom. Upserts per (alias, site) — calling again just refreshes the timestamp |
account_* — plan + quota
| Tool | Purpose |
|---|---|
account_get |
One blob: user, subscription (name / since / renews_at), quota (used vs limit, including kept_messages, mail_send, mail_import, invites_remaining), rate-limits, retention |
digest_* — daily digest
| Tool | Purpose |
|---|---|
digest_get |
Your digest schedule: whether it's on, the local delivery time, its timezone, and the next send instant. null if no schedule exists |
digest_set_enabled |
Pause or resume the daily digest. While paused nothing is sent |
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. (mail_import is the one write that isn't a send: it only stores mail you hand it, and inboxy never reads your other mailboxes itself.)
| 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.
Open Claude Desktop → Settings → Developer → Edit Config.
Add:
{ "mcpServers": { "inboxy": { "url": "https://mcp.inboxy.net" } } }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.
Two things to get right whatever the client:
- The endpoint is the bare origin —
https://mcp.inboxy.net, with no/mcppath. Lots of MCP examples in the wild end in/mcp; ours returns404 {"error":"not_found"}if you copy that habit. - If your client can't do the browser sign-in (Codex, CI scripts, server-side automations), you don't have to drop down to REST.
mcp.inboxy.netaccepts a static API key asAuthorization: Bearer ik_…on the same endpoint — mint one under Account → API keys and see Connecting Codex below. The REST API athttps://api.inboxy.netis there if you'd rather not speak MCP at all; it exposes 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
- Account → Addresses → New address. Pick the
adjective-nounscheme (memorable) oropaque(un-guessable). - Label it for the agent ("openclaw", "codex", etc.) so the inbox view stays legible.
2. Mint an address-scoped API key
- Account → API keys → New key.
- Set Scope =
manageand Address =<your-alias>@inboxy.net(the dropdown lists your active aliases). - 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):
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:
--headerparses asName=Value, notName: Value. Using a colon produces"Authorization: Bearer …" is an invalid header name.- Set
INBOXY_KEYin your shell or secret store first so the token never lands in OpenClaw's~/.openclaw/openclaw.json(where--headervalues are persisted) or your shell history. Treat~/.openclaw/openclaw.jsonlike a credential file.
If your OpenClaw build prefers a JSON config block instead of the CLI, the equivalent is:
{
"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:
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 …"), notName=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: oauthinstead 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.
3c. Wire the credential into Codex (OpenAI)
Codex reads its MCP servers from ~/.codex/config.toml (%USERPROFILE%\.codex\config.toml on Windows). Two paths work; pick by whether anyone is at the keyboard.
OAuth — attended, nothing to store. Add the server, then sign in once:
[mcp_servers.inboxy]
url = "https://mcp.inboxy.net"
codex mcp login inboxy
That runs the same OAuth 2.1 + PKCE + dynamic-registration flow Claude Desktop uses, and the grant shows up under Account → MCP / OAuth clients.
Bearer key — unattended, and the only path that carries address scoping. Codex's bearer_token_env_var takes the name of an environment variable, not the key itself. Put the key in the environment, and name the variable in the config:
[mcp_servers.inboxy]
enabled = true
url = "https://mcp.inboxy.net"
bearer_token_env_var = "INBOXY_API_KEY"
# macOS / Linux — in your shell profile or secret manager
export INBOXY_API_KEY=ik_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Windows — a *user* variable, so it survives reboots
setx INBOXY_API_KEY "ik_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
codex mcp add writes the same TOML for you:
codex mcp add inboxy \
--url https://mcp.inboxy.net \
--bearer-token-env-var INBOXY_API_KEY
Codex-specific quirks, in the order people hit them:
bearer_token_env_varis a variable name, never the key. Pastingik_…there makes Codex look for an environment variable literally calledik_…; it finds nothing, and the connection ends up unauthenticated. The 401 you get back reads like inboxy rejected your key, when nothing ever reached us. Codex adds theBearerprefix itself, so the variable holds the bareik_…value.- Restart Codex fully after setting the variable. A running Codex — and the VS Code extension host — keeps the environment it started with. On Windows a
setx/ System Properties variable only reaches processes launched afterwards. Quit the app; opening a new chat is not enough. - No
/mcpsuffix.https://mcp.inboxy.net/mcpis a 404. Use the bare origin. - The key must be a live inboxy key. We recognise the
ik_prefix; anything else falls through to the OAuth path and comes back as401 invalid_tokenrather than "bad key". A revoked key gives the same 401 — mint a fresh one rather than debugging the config. - If you'd rather use raw headers, Codex also has
http_headers(literal values) andenv_http_headers(values pulled from named environment variables — that variable must then hold the wholeBearer ik_…string, prefix included).bearer_token_env_varis the simpler of the three. - Back up
config.tomlbefore editing. Codex rewrites the file when you usecodex mcp add/remove, and a malformed TOML table takes out every other server in it, not just inboxy.
The same alias-plus-scoped-key pattern from steps 1 and 2 applies: give Codex its own alias and a manage key bound to it, and a leak costs you one alias. If Codex only needs to read, mint the key with manage unticked — the send tools then refuse with a 403.
Credential syntax by client
Three clients, three separators — this is the most common copy-paste failure:
| Client | Config file | Credential form |
|---|---|---|
| OpenClaw | ~/.openclaw/openclaw.json |
--header "Authorization=Bearer $INBOXY_KEY" — Name=Value, no colon |
| Hermes | ~/.hermes/config.yaml |
Authorization: "Bearer ${INBOXY_KEY}" — colon, ${VAR} read from ~/.hermes/.env |
| Codex | ~/.codex/config.toml |
bearer_token_env_var = "INBOXY_API_KEY" — a variable name only: no colon, no Bearer, no key |
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_quotais 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_emailor any of your own aliases viamail_send. Usemail_forward(which is itself locked to yourpersonal_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. Usemail_inviterather thanmail_sendwhen introducing a friend to inboxy. - Imports have their own counter too: 500 per calendar month (5000 paid), shown as
quota.mail_importinaccount_get. An address-scoped key can only import onto its bound alias. Nothing is sent, somail_send_quotais untouched.
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 — 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.
Still need help? support@inboxy.net