Guides

Agents and MCP

Give an AI agent an email address of its own. It reads and writes through its own key, and every message it sends is held to rules you set: anything outside them waits for a person to approve.

How it works

  • You create the agent in the console under Agents. It gets a real mailbox on your domain and a key of its own.
  • The key holds agent:mail alone and is bound to that one agent. It reaches that mailbox and nothing else in your account.
  • The agent may always write to colleagues at its own domain. Beyond that you choose: anyone, only people who wrote to it first, or an allow list, within a daily limit. Or hold every message for approval.
  • A message outside the rules is not refused, it waits. A person approves it (and may edit it first) or turns it down. Approving twice sends once.
  • Every read and every send is on the agent's record.

Connect with MCP

The Rykon Mail MCP server is one file with no dependencies. It runs on Node 18 or later, speaks MCP over stdio, and calls only the agent inbox API below with the agent's key. It is short enough to read before you trust it.

Claude Code
curl -fsSLo rykon-mail-mcp.mjs https://rykonmail.com/assets/agents/rykon-mail-mcp.mjs
claude mcp add rykon-mail \
  -e RYKON_MAIL_AGENT_KEY=rmk_live_... \
  -- node "$PWD/rykon-mail-mcp.mjs"

Any other MCP client takes the same command and environment, usually as JSON:

mcp.json
{
  "mcpServers": {
    "rykon-mail": {
      "command": "node",
      "args": ["/path/to/rykon-mail-mcp.mjs"],
      "env": { "RYKON_MAIL_AGENT_KEY": "rmk_live_..." }
    }
  }
}

RYKON_MAIL_API overrides the API address, which defaults to https://app.rykonmail.com/api/agent/v1.

ToolDoes
whoamiThe agent’s name, address and the rules its sending is held to.
inboxMessages in the inbox, newest first; unread only by default.
read_messageOne message in full. Reading marks it read and is recorded.
replyReply in the same thread, to whoever sent the message.
send_messageWrite a new message. Anything outside the rules waits for a person.
message_statusWhether a held message went out, is still waiting, or was turned down.

Held means held

When a send is waiting for approval, the tools tell the model so in words, with the id to check later, and tell it not to send the message again.

The agent inbox API

The same five calls the MCP server makes, for an agent you run yourself. Base URL https://app.rykonmail.com/api/agent/v1, authenticated with the agent's key as Authorization: Bearer. Errors have the usual shape, and each agent key has its own allowance.

GET/me

The agent itself

The agent’s name and address, and the rules its sending is held to, with a sentence that explains them which you can hand to the model as it is.

Needsagent:mailReturns200 OK
GET/me
curl https://app.rykonmail.com/api/agent/v1/me \
  -H "Authorization: Bearer $RYKON_MAIL_AGENT_KEY"
Response
200 OK
{
  "name": "Support agent",
  "address": "support-agent@yourcompany.com",
  "state": "active",
  "rules": {
    "sendTo": "repliers",
    "allowList": [],
    "dailyLimit": 50,
    "approveAll": false,
    "explained": "You may write to people at yourcompany.com, and to people who have written to you, up to 50 messages a day. Anything else waits for a person."
  }
}
GET/messages

List the inbox

Messages in the agent’s inbox, newest first.

Needsagent:mailReturns200 OK

Query parameters

unseenbooleanoptional
true for only the messages not yet read. Default false.
limitintegeroptional
How many, 1 to 100. Default 20.
GET/messages
curl "https://app.rykonmail.com/api/agent/v1/messages?unseen=true&limit=20" \
  -H "Authorization: Bearer $RYKON_MAIL_AGENT_KEY"
Response
200 OK
{
  "messages": [
    {
      "uid": 41,
      "subject": "Question about my order",
      "from": {
        "name": "Jordan Lee",
        "address": "jordan@example.com"
      },
      "date": "2026-10-30T09:12:44.000Z",
      "seen": false
    }
  ]
}
GET/messages/{uid}

Read a message

One message in full, as text. Reading it marks it seen and puts it on the agent’s record. A very long message is cut, and says so with truncated.

Needsagent:mailReturns200 OK

Path parameters

uidintegerrequired
The message’s uid from the inbox list.
GET/messages/{uid}
curl https://app.rykonmail.com/api/agent/v1/messages/41 \
  -H "Authorization: Bearer $RYKON_MAIL_AGENT_KEY"
Response
200 OK
{
  "uid": 41,
  "from": {
    "name": "Jordan Lee",
    "address": "jordan@example.com"
  },
  "to": [
    {
      "name": "Support agent",
      "address": "support-agent@yourcompany.com"
    }
  ],
  "cc": [],
  "date": "2026-10-30T09:12:44.000Z",
  "subject": "Question about my order",
  "text": "Hi, my order 1042 has not arrived yet. Can you check where it is?",
  "truncated": false
}
POST/messages

Send or reply

Write a new message, or reply to one in the inbox by its uid: the recipient, subject and threading are then taken from that message unless you give them. A message inside the rules is sent and answered 200; anything else waits for a person and is answered 202 with state pending and the reason.

Needsagent:mailReturns202 Accepted

Body

tostring[]optional
Up to 20 addresses. Required unless replyTo is given.
ccstring[]optional
Up to 20 addresses.
subjectstringoptional
Up to 300 characters. Required unless replyTo is given.
textstringrequired
The plain text body.
replyTointegeroptional
The uid of the message this answers.
POST/messages
curl -X POST https://app.rykonmail.com/api/agent/v1/messages \
  -H "Authorization: Bearer $RYKON_MAIL_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [
      "sam@example.org"
    ],
    "subject": "Your quote from Your Company",
    "text": "Hi Sam, as promised on the phone, your quote is below."
  }'
Response
202 Accepted
{
  "id": "3b5f0c2e-8d41-4a8e-b7a2-61f0d9e4c7aa",
  "state": "pending",
  "heldBecause": "sam@example.org has not written to it first"
}
GET/outbox/{id}

Where a message stands

Whether a message the agent asked to send is pending, sent or rejected, and whether a person edited it before it went.

Needsagent:mailReturns200 OK

Path parameters

idstringrequired
The id a send returned.
GET/outbox/{id}
curl https://app.rykonmail.com/api/agent/v1/outbox/3b5f0c2e-8d41-4a8e-b7a2-61f0d9e4c7aa \
  -H "Authorization: Bearer $RYKON_MAIL_AGENT_KEY"
Response
200 OK
{
  "id": "3b5f0c2e-8d41-4a8e-b7a2-61f0d9e4c7aa",
  "state": "sent",
  "recipients": [
    "sam@example.org"
  ],
  "subject": "Your quote from Your Company",
  "held_because": "sam@example.org has not written to it first",
  "edited": false,
  "decided_at": "2026-10-30T09:20:03.118Z",
  "error": null,
  "created_at": "2026-10-30T09:14:51.902Z",
  "sent_at": "2026-10-30T09:20:04.511Z"
}

New mail notifications

Instead of polling, an agent can be set in the console to notify your endpoint when mail arrives. Each new message is posted once as JSON, signed in the x-rykonmail-signature header with the agent's notification secret: t= the time in Unix seconds, v1= the hex HMAC-SHA256 of the time, a full stop and the raw body.

POST to your endpoint
{
  "type": "message.received",
  "agent": { "id": "e2a7c9d4-...", "address": "support-agent@yourcompany.com" },
  "message": {
    "uid": 41,
    "messageId": "<CAF1x9@mail.example.com>",
    "from": "jordan@example.com",
    "subject": "Question about my order",
    "date": "2026-10-30T09:12:44.000Z"
  }
}
Verify the signature
import { createHmac, timingSafeEqual } from 'node:crypto';

// header: the x-rykonmail-signature value, "t=<seconds>,v1=<hex>"
export function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
}

Managing agents from your account

With an account key holding mailboxes:read or mailboxes:write, you can create agents, set their sending rules, and approve or reject the messages waiting for a person, from your own tools. Approving sends once, however many times it is called.