replylayer-mcp
The MCP server for ReplyLayer — safe email for AI agents. It exposes ReplyLayer's
email surface (send, reply, read, monitor, release) as MCP tools so an MCP client
(Claude Desktop, Cursor, …) can drive transactional and operational email with built-in
security scanning. Not a bulk or marketing tool.
Server identity: replylayer v0.12.0. 44 tools. The hosted server
(POST /v1/mcp on api.replylayer.ai) reports the same identity and injects the
same server-level workflow primer as this binary — both read the version from this
package, so neither can drift from it.
Install
Published to npm as replylayer-mcp. Most MCP clients launch it via npx (no
global install needed) — see Client config. To run or
install it directly:
npx -y replylayer-mcp
Credentials
Auth is a single ReplyLayer API key. The server resolves it from the
REPLYLAYER_API_KEY environment variable first, then from the stored credentials file
at ~/.replylayer/credentials. Production (https://api.replylayer.ai) is the default
endpoint; REPLYLAYER_API_URL is a testing-only override.
Set up the stored credential interactively (writes ~/.replylayer/credentials,
mode 0600):
replylayer-mcp init
For CI / automation, skip init and pass REPLYLAYER_API_KEY in the environment.
A new key is inert until a human completes a one-time email-verification bootstrap
(invite-gated signup + a 6-digit code). The server runs with a key a human has already
provisioned and verified.
Client config (copy-paste)
Claude Desktop (claude_desktop_config.json) or Cursor (mcpServers) — launch via
npx so there's no global install or absolute path to manage:
{
"mcpServers": {
"replylayer": {
"command": "npx",
"args": ["-y", "replylayer-mcp"],
"env": {
"REPLYLAYER_API_KEY": "rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7z"
}
}
}
}
If you ran replylayer-mcp init to store the key, the env block is optional. With a
global install (npm i -g replylayer-mcp), use "command": "replylayer-mcp" and drop
the args.
The directional safety model
Every message is security-scanned, and scanning is directional:
- Outbound text that resembles prompt-injection is your agent's own authored content —
delivered clean. The secrets scanner is outbound-only.
- Inbound prompt-injection is quarantined.
The send tools (send_email, reply_to_message, send_draft) return an outcome —
branch on the outcome, not on success/failure. The discriminator is
email_effect.effect_status: sent | held_for_review | held_infrastructure | blocked
(the Governed Email Effect contract). Treat an
effect_status you do not recognize as held, never as sent. Where email_effect is
absent, fall back to the top-level status: sent | quarantined | pending_review | blocked. sent means ReplyLayer accepted the message for sending — it is not proof
the recipient received it.
Those send responses also carry the why inline: scan (the scanner
verdict — inspect scan.verdict, scan.findings, and scan.findings[].agent_instructions)
and hold_context (the hold reason plus agent_instructions on held sends — a typed
policy cause, a policy change to the scanner's decision, or a genuine scan-explained
hold; null on sent/terminal outcomes and normally on infrastructure holds). Call
read_message for full detail. A finding with
failure_class: "inference_error" is an infrastructure error, not a content judgment —
retry later, do not rewrite.
Recovery: inbound quarantined → release_quarantined_message; firewall_blocked →
release_firewall_blocked_message; pending_review needs human approval (agent keys
cannot approve — escalate). RATE_LIMITED → if details.reset_at is present it is the
daily send quota (back off until then); otherwise it is a short-window limit — wait for
details.retry_after (or the time in the error message), then retry.
RECIPIENT_SUPPRESSED / BILLING_* / EMAIL_NOT_VERIFIED → escalate, do not retry.
This server does no implicit retry (neither on 5xx nor on a 429 Retry-After):
every tool call fails fast and returns the structured error, so the agent owns retry
timing and a tool call is never held open by a silent server-side wait.
Idempotent sends
send_email and reply_to_message take an optional idempotency_key. Pick one stable
key per send or reply intent and reuse that exact key for every retry of that intent: a
replay returns the original outcome instead of sending a second message. The record is
permanent. On IDEMPOTENT_REQUEST_IN_FLIGHT, retry the same key after the indicated
delay. On IDEMPOTENT_REQUEST_NOT_PROVEN_SENT, stop and report it for investigation —
the message was not re-sent, and minting a new key could duplicate it.
create_draft's idempotency_key is a different thing: it is valid only alongside
send_at (scheduled sends) and replays for 24 h, not permanently.
Trusted instruction sources
By default every inbound read carries the standing untrusted-content contract:
agent_safety_context.untrusted_content: true plus a guidance string telling the
agent not to follow instructions embedded in the message body. Relaxing that on a given
read is entirely server-side and operator-configured — this server has no client-side
opt-in or env var for it. The server relaxes a read only when the customer has (a) put
the mailbox into instruction_trust_mode: 'enabled', (b) enabled this API key's
instruction-trust capability, and (c) designated that exact sender address (address-grain
only — no domain-wide trust) as a trusted instruction source, and the message itself
is verified_aligned (Mailgun-verified domain authenticity), clean-scanning, and
available from that sender. When all of that lines up, guidance is replaced with
trusted-instruction guidance (the agent may act on
that verified sender's own explicit requests in this message) and a thin
instruction_trust basis object is attached — untrusted_content stays true. This is
read-relaxation only: it never changes how an outbound send is gated.
Default is off end to end: no operator configuration → no relaxation → byte-identical
legacy reads. There is currently no MCP tool to grant, list, or revoke trusted sources —
see the note at the end of Tool surface.
Tool surface (44 tools)
Reads (no side effects): list_messages (search + filters + paging), read_message,
get_thread, list_threads, wait_for_message,
list_mailboxes, list_recipients, list_suppressions, list_allowlist,
list_allowlist_blocked_attempts, list_drafts, get_draft, list_inbound_blocklist,
list_inbound_allowlist, list_inbound_firewall_blocked_attempts, get_account_usage,
get_agent_quota, get_attachment_preview, get_link_scanning_status.
get_link_scanning_status reports whether malicious link scanning (URL reputation, backed by
Google Web Risk) is active for the account. Turning it on is an admin action (it enables an
account-wide sub-processor data flow), so the MCP surface is read-only here — enable it from the
dashboard, the SDK, or rly account link-scanning enable.
Sends (dispatch outbound mail): send_email, reply_to_message, send_draft.
The high-traffic tools — the 6 reads (list_messages, read_message, get_thread,
list_threads, list_mailboxes, wait_for_message) and the 3 sends (send_email,
reply_to_message, send_draft) — now return structuredContent (a typed object you can
consume without re-parsing the text blob) alongside the text block, with declared
outputSchemas. get_attachment_preview also returns structuredContent.
Drafts: create_draft, update_draft, delete_draft (plus send_draft above).
Inbound triage: release_quarantined_message, block_quarantined_message,
report_and_block (block + blocklist the sender, inbound-only),
release_firewall_blocked_message. Human-in-the-loop (admin-only; agent keys 403):
approve_review, deny_review.
Delete: delete_message — soft-deletes a message (any direction) and purges its
raw MIME; agent keys require the account delete policy to be enabled (else 403).
Policy / contacts: add_recipient, add_suppression, add_inbound_blocklist,
add_inbound_allowlist_entry, mark_message_read, mark_thread_read.
Star / unstar: star_message (toggle a message's starred flag),
star_thread (toggle a thread's starred flag).
Bulk policy mutations: add_inbound_allowlist_bulk (up to 1000 sender addresses for a
mailbox's inbound allowlist in one call), add_suppressions_bulk (up to 1000 addresses to
the account-wide do-not-contact list), add_inbound_blocklist_bulk (up to 1000 addresses
to the account-wide inbound blocklist).
Policy stub (intentionally unavailable): remove_suppression — always returns an
isError result explaining that suppression removal is a per-human admin decision; use
rly suppressions remove, an SDK admin key, or the dashboard instead.
Not yet a tool: trusted-instruction-source management (grant / list / revoke, plus the
mailbox mode and per-key capability toggles — see above)
has no MCP wrapper today. That includes the revoke endpoint, which — like the inbound-triage
release tools — is unprivileged and agent-key-callable server-side; it just isn't exposed
here yet. Manage trusted sources from the ReplyLayer dashboard (or the REST API directly)
in the meantime.
Tool notes
star_message — wraps PATCH /v1/messages/:id/star. Input: message_id,
starred: boolean. Use one tool call to star or unstar (matches the SDK/REST {starred}
shape — there are no separate star/unstar verbs).
star_thread — wraps PATCH /v1/mailboxes/:id/threads/:thread_id/star. Input:
mailbox, thread_id, starred: boolean.
get_attachment_preview — wraps GET /v1/messages/:id/attachments/:idx/preview.
Returns derived text preview only (PDF, Office, text/plain, text/csv). Surfaces
404 ATTACHMENT_PREVIEW_NOT_AVAILABLE as an isError result when the derivative is not
ready, failed, or unavailable. Returns structuredContent alongside the text block.
add_inbound_allowlist_bulk — wraps POST /v1/mailboxes/:id/inbound-allowlist/bulk.
Input: mailbox, emails: string[] (max 1000). Returns the full partial-success shape:
added, already_existed, invalid (with per-entry reason), and counts.
add_suppressions_bulk — wraps POST /v1/suppressions/bulk. Input:
emails: string[] (max 1000). Same partial-success shape.
add_inbound_blocklist_bulk — wraps POST /v1/inbound-blocklist/bulk. Input:
emails: string[] (max 1000). Same partial-success shape.
remove_suppression — policy-explanation stub. Always returns isError with code
SUPPRESSION_REMOVE_NOT_AVAILABLE. Improves agent UX by explaining why removal is not
available via MCP rather than returning a generic unknown-tool error.
wait_for_message timeout — the tool reflects the real server cap: max 30 s per
call (not 120 s). For longer waits, call the tool again.
Browse & search
Inbox search and full thread reads are first-class on MCP: list_messages filters by
keyword search, sender, date range, direction, status (incl. the held-mail states
quarantined / pending_review for triage) and starred, and pages older via
before=<oldest message id from the previous page>. list_threads lists conversations
(with starred / has_inbound), and get_thread reads a full thread — pass mailbox
to disambiguate a thread key that resolves in more than one mailbox. list_messages also
supports has_attachment (true/false) — it probes /v1/health for the
messages.has_attachment_filter capability and returns a structured
SEARCH_OPERATOR_UNSUPPORTED on older servers that don't advertise it.
Hosted vs local attachments
send_email, reply_to_message, create_draft and update_draft take an
attachments array. What that array means depends on which server you are talking to.
Local server (this package, npx -y replylayer-mcp) — attachments are paths on
your own machine. The server reads each file, uploads it, waits for its content scan,
and references the resulting handles on the send. This is the normal way to attach files.
Hosted server (POST /v1/mcp on api.replylayer.ai) — the hosted server never
reads files. It runs in ReplyLayer's own infrastructure, so a path in attachments
would name a file on our server, not yours. Supplying a non-empty attachments array
is refused before any file is opened, with a stable code:
{
"error": "The hosted MCP server cannot read files from your machine. To attach files, run the local MCP server (npx -y replylayer-mcp) or upload through the REST API / SDK and send from there; hosted attachment handles are not yet available on MCP.",
"code": "HOSTED_LOCAL_ATTACHMENTS_UNSUPPORTED",
"details": { "attachments": 1 }
}
details reports only how many paths you sent; the paths themselves are never echoed back.
To attach a file on the hosted server, pick one of:
- Run the local MCP server (
npx -y replylayer-mcp) and use attachments as
documented above; or
- Upload through the REST API or an SDK —
POST /v1/attachments returns a handle id,
and POST /v1/messages accepts attachment_ids. Send from there.
Two details worth knowing:
- An empty
attachments: [] is not a rejection — it behaves exactly like omitting the
field.
- A same-key idempotent retry still replays. If an earlier send from the local
server carried real paths and you retry it against the hosted server with the same
idempotency_key, the replay probe runs first and returns the original result. The
attachments parameter therefore stays in the hosted schema (so such a retry still
parses) — only its description changes, and it is refused on a probe miss.
Hosted attachment handles on MCP are not available yet; when they land, they will be an
explicit handle-id parameter rather than a path.
Changelog
- unreleased — the hosted MCP server now injects the same server-level workflow
primer as this binary and advertises the real package version; both transports read
the version from
package.json. The primer branches on
email_effect.effect_status (with the legacy top-level status as a fallback for
older servers) and carries the immediate-send idempotency rules. Additive public
API: replylayer-mcp/tools also exports SERVER_INSTRUCTIONS and
MCP_PACKAGE_VERSION.
- 0.12.0 — the hosted MCP server now refuses local
attachments paths before any
filesystem access (HOSTED_LOCAL_ATTACHMENTS_UNSUPPORTED); see
Hosted vs local attachments. Additive public API: the
tool registrars exported from replylayer-mcp/tools accept an optional third
RegisterToolOptions argument ({ transport?: 'local' | 'hosted' }, default
'local'), so every existing two-argument call is unchanged.
See also
- Public CLI repo (install & package trust): https://github.com/replylayer/rly
- The agent
SKILL.md, the CLI ↔ MCP ↔ REST verb-parity map, and ENDPOINTS.md
(the REST API contract) will be published with the hosted docs site when it is
available.