Browse documentation

MCP server

ReplyLayer ships a Model Context Protocol (MCP) server so an agent host can call the platform as tools instead of raw HTTP. The same tool registry backs two transports, so the tool set, names, and behavior are identical either way:

  • Hosted — a streamable-HTTP endpoint at POST /v1/mcp on the API. No install; authenticate with your API key as a Bearer token. Claude, ChatGPT and Meta Muse instead sign in (OAuth) at POST /v1/mcp/oauth, the same handler listing every tool except the four an agent key can never use (see below).
  • stdio — the replylayer-mcp package (npm), run locally as a subprocess by your MCP client. Reads the API key from the environment or stored credentials.

Both expose the same 44 tools; a sign-in connection lists 40 of them. Every tool call returns an MCP CallToolResult; on failure the result carries isError: true and a structured JSON envelope you branch on (see The result envelope).

Connect over hosted HTTP

The hosted server is stateless: every POST /v1/mcp request is independent — there is no session id, and GET/DELETE are not supported (with a valid key they return 405 with an Allow: POST header; without one, 401). Authenticate every request with Authorization: Bearer <api-key>; a missing or malformed Bearer token returns 401. Authentication on /v1/mcp is a static Bearer key; this URL has no OAuth flow and no OAuth discovery metadata.

Sign-in endpoint (/v1/mcp/oauth). The same stateless handler and tool set, authenticated by OAuth 2.1 for three clients only: Claude, ChatGPT and Meta Muse. Without a token it answers 401 with WWW-Authenticate: Bearer resource_metadata="https://api.replylayer.ai/.well-known/oauth-protected-resource/v1/mcp/oauth", scope="mailboxes", which starts the client's sign-in. Each connection acts as an agent key bound to the mailboxes the user approved, so every agent-key limit applies, and tools/list leaves out the four tools an agent key can never use: approve_review, deny_review, get_account_usage and remove_suppression. Every tool listed there carries securitySchemes (oauth2, scope mailboxes), top-level and mirrored in _meta, and a tool error caused by the token being revoked or expiring mid-request carries _meta["mcp/www_authenticate"]: the same error="invalid_token" challenge plus error_description="The ReplyLayer connection is no longer valid; sign in again", so a client that signs in per tool can send the user back through sign-in; /v1/mcp carries neither. The two URLs never share credentials: an API key is refused on /v1/mcp/oauth, and an OAuth token is refused on /v1/mcp and every REST route. Access tokens last one hour; a connection ends after 30 days without use and at the latest 90 days after consent, and the user can revoke it from the dashboard at any time. The human-facing setup is in MCP server.

A representative MCP client config:

{
  "mcpServers": {
    "replylayer": {
      "type": "streamable-http",
      "url": "https://api.replylayer.ai/v1/mcp",
      "headers": { "Authorization": "Bearer rly_live_..." }
    }
  }
}

The hosted server does no implicit retry. If the API returns a rate-limit or transient error, the structured error is surfaced to your tool result immediately rather than the server sleeping through a Retry-After. Your agent owns the retry decision — read details.retry_after / details.reset_at and decide (see Errors).

Connect over stdio

Run the published replylayer-mcp package as a subprocess. It resolves the API key from REPLYLAYER_API_KEY, or from credentials stored by replylayer-mcp init (written under ~/.replylayer/). REPLYLAYER_API_URL optionally overrides the API base (defaults to the public API).

{
  "mcpServers": {
    "replylayer": {
      "command": "npx",
      "args": ["-y", "replylayer-mcp"],
      "env": { "REPLYLAYER_API_KEY": "rly_live_..." }
    }
  }
}

Like the hosted transport, the stdio server does no implicit retry — a tool call fails fast and returns the structured error so the agent decides retry-vs-abort.

The result envelope

Every tool returns an MCP CallToolResult. On success, content[0].text is the tool's JSON result (pretty-printed). On failure, isError: true and content[0].text is a JSON string with this shape:

{
  "error": "human-readable message",
  "code": "STABLE_ERROR_CODE",
  "details": { "...": "structured fields, when present" },
  "hint": ["next-step line", "..."]
}

Branch on code, not on the prose in error:

  • code is the stable machine-readable error code. The full catalog lives in Errors.
  • details carries structured fields when the API provides them — e.g. a rate-limit error carries daily_limit, sends_remaining, and reset_at; a capability/denial error carries reason_axis and a cheapest_next_step.
  • hint is an array of plain-text next-step lines (the same guidance the CLI prints). It is advisory; the machine contract is code + details.

Tool-side precondition failures (for example a capability probe that shows the server cannot honor a has_attachment filter → SEARCH_OPERATOR_UNSUPPORTED) use the same { error, code, details? } envelope, so you branch on code uniformly whether the failure came from the API or from the tool itself.

Send tools return isError on a resolved send

send_email, reply_to_message, and send_draft can return isError: true even when the HTTP call succeeded — because a send the scanner refused, or one held by an infrastructure fault, is a failed tool call your host must not mark done. The flag is derived from the governed email-effect on the send result:

email_effect.effect_statusisErrorMeaning
blockedtrueContent refused, terminal — edit or escalate; do not retry as-is.
held_infrastructuretrueTransient infrastructure hold — retryable. Retry the same send later; do not rewrite the content.
held_for_reviewfalseAccepted into governance / review — releasable, not a failure.
sentfalseDispatched.

A held_for_review result leads with a HELD FOR REVIEW — not sent line (HELD — not sent for a quarantined send) that tells the agent to tell the account owner (the person it acts for) the email is waiting for their approval, with the direct link from hold_context.review_url and the deadline from hold_context.review_expires_at when the server has them, and not to send it again. An infrastructure hold never gets that line or the link: it is a failed call whose advice is to retry later. send_draft shapes its result the same way as send_email and reply_to_message.

An unrecognized or future effect_status fails closed to isError: true — an older server must not report an outcome it cannot interpret as success. For what each effect status means and how holds resolve, see Send gates. send_draft additionally surfaces its send-time conflicts (rescan rejection, already-sent) as isError JSON keyed on code, with the API details flattened to the JSON root.

Structured output

From replylayer-mcp 0.14.0, every tool except remove_suppression declares an outputSchema and returns structuredContent (a machine-parseable object) alongside the text block. The hosted endpoints (/v1/mcp, /v1/mcp/oauth) serve it once the server is updated; a local server has it from 0.14.0. remove_suppression always returns isError, so it has no success shape, and it is not listed on /v1/mcp/oauth. Earlier versions declare an output schema only on send_email, reply_to_message, send_draft, list_messages, read_message, get_thread, list_threads, list_mailboxes, wait_for_message and get_attachment_preview. Check the tool's outputSchema in tools/list; when a tool has none, parse the text block.

  • structuredContent is the same JSON as the text block (for a held send, the text block after the leading HELD line).
  • Each schema follows the API route's response, after the fields the tool leaves out (below). Objects accept fields the API adds later, enum-like values are declared as strings (known values are in the field description), and only the top-level fields the route always returns are required.
  • A tool error (isError: true) carries no structuredContent, except a blocked or infrastructure-held send, which keeps its send result so you can read email_effect.
  • Send results and message reads describe hold_context.review_url and hold_context.review_expires_at on a held send.

Fields MCP results leave out

MCP results carry what an assistant needs to act, not internal bookkeeping. On every transport (stdio, /v1/mcp and /v1/mcp/oauth) the tools remove these fields from the API response before building both the text block and structuredContent. The REST API, the SDKs and the CLI still return them.

ToolLeft outStill there
read_message, get_threadreviewed_by_id; on each attachments[] entry hash, av_signature, av_scanned_at, content_disposition, raw_retention_reason, raw_deleted_atreviewed_by_type, review_outcome; attachment filename, content_type, size, policy_action, sniffed_mime_type, declared_mime_type, mime_mismatch, content_id, av_verdict, stored, inline, raw_retention and every preview_* field
delete_messager2_objects_failed (storage keys)r2_objects_failed_count in its place
list_suppressions, add_suppression, list_inbound_blocklist, add_inbound_blocklist, list_allowlist, list_inbound_allowlist, add_inbound_allowlist_entryadded_by_actor_idadded_by_actor_type
list_allowlist_blocked_attemptsactor_idactor_type; aggregated rows are still one per recipient and sending key, so the same recipient can appear twice
get_agent_quotawarmup.velocity_gate_modethe rest of warmup
get_attachment_previewattachment.hash, preview.extractoreverything else
list_mailboxesscanner_policy, pii_redaction_config, last_applied_policy_mode, instruction_trust_mode, instruction_trust_strict_recipient, and the dashboard consent bookkeeping (*_accepted_at, *_accepted_version, attachment_reauth_at, attachment_policy_version, current_*_version, legacy_wildcard_active), which the API already omits for API keys and connectionsid, name, address, display_name, effective_from_display, status, created_at, pii_mode, recipient_policy_mode, sender_policy_mode, hitl_mode, agent_send_policy, restricted_by, agent_send_containment, allow_thread_replies, agent_authoring_mode, default_subaddress_mode, attachment_exposure_mode, attachment_allowed_file_families, outbound_attachments.enabled

The do-not-contact and inbound-blocklist source field names no vendor on MCP: a bounce or complaint delivered by the sending provider reads provider_webhook, an address found on the provider's own suppression list at send time reads provider_sync, and any value the server does not recognise reads system. customer, one_click_https and mailto_inbound are unchanged, and reason still says why the entry exists.

A 400 VALIDATION_ERROR keeps details.validation, but each item carries only instancePath (the field) and message. The top-level error already names the first failing field and, for an enum, its allowed values. A 500 SELF_HOSTED_DECRYPT_FAILED keeps details.endpoint (your mail host) and leaves out details.domain_id.

next_cursor on the list tools is an opaque paging token: pass it back unchanged as cursor and do not parse it or show it to a user.

list_drafts returns has_more beside drafts; when it is true, pass the last draft's id as before for the next page.

Server instructions

Both transports send the same server-level instructions primer: the stdio server and the hosted /v1/mcp server. It is a short workflow guide (branch on message status, recovery paths for held mail, how to read a rate-limit error) that a host can inject into the model context at connect time. The sign-in endpoint /v1/mcp/oauth sends the same primer with its API-key wording reworded for a connection: there is no API key, a revoked or expired connection means asking the user to reconnect, and the note on held-for-review mail says the connection (not an agent key) cannot approve. For held mail the primer tells the model to tell the account owner the send is waiting for them, give them hold_context.review_url, and not send it again.

Some hosts ignore server instructions (Hermes does), so do not depend on the primer being in context. The per-tool description fields are self-contained and carry what a model needs to call each tool correctly.

Tool registry (44 tools)

From replylayer-mcp 0.14.0, every tool except remove_suppression declares an outputSchema and emits structuredContent (see Structured output). Every tool declares a Title and explicit boolean readOnlyHint, destructiveHint and openWorldHint annotations, plus idempotentHint on every tool that changes something; the Annotations column lists the hints that are true. One tools/list serves every client, so where client guidelines differ the stricter reading wins: a tool that modifies account data is destructive even when it can be undone (the list and marker writes), and a tool that can email someone outside the account is openWorld. Annotations are advisory metadata for the host — the authoritative permission gates are enforced server-side.

Messages & threads

ToolTitlePurposeAnnotations
send_emailSend emailSend a new email from a mailbox, or continue a thread with thread_id; returns status, scan verdict, and the governed email-effect.destructive, openWorld
reply_to_messageReply to messageReply to an inbound message from the mailbox that received it, threading correctly.destructive, openWorld
list_messagesList messagesList / search messages in a mailbox (search, sender, date, direction, status, starred, has_attachment); page older via before.readOnly
read_messageRead messageRead one message by ID: plaintext body projection, attachment metadata/status, scan results. Side-effect-free (does not advance read state).readOnly
wait_for_messageWait for messageLong-poll a mailbox; returns when a new message arrives or the timeout expires.readOnly
get_threadGet threadRead a full thread as one ordered conversation (oldest → newest, safe view).readOnly
list_threadsList threadsList conversation threads (most-recent first) with participants, unread_count, last_message_at.readOnly
mark_message_readMark message readMark one inbound message read (outbound / deleted / firewall-blocked rows are a 200 no-op).destructive, idempotent
mark_thread_readMark thread readBulk-mark every visible inbound unread message in a thread read; returns marked_count.destructive, idempotent
star_messageStar or unstar messageStar or unstar a message.destructive, idempotent
star_threadStar or unstar threadStar or unstar a thread.destructive, idempotent
delete_messageDelete messagePermanently delete a message (any direction, deletable state); purges raw bytes + derivatives. Refuses draft / scheduled / dispatching rows; agent keys require the account's agent-deletion policy.destructive, idempotent
get_attachment_previewGet attachment previewGet a derived text preview of an inbound attachment (text, CSV, PDF, Office); capped in length. Raw bytes are not available via MCP.readOnly

Mailboxes & account

ToolTitlePurposeAnnotations
list_mailboxesList mailboxesList account mailboxes (name, full address, status).readOnly
get_account_usageGet account usageGet account usage, limits, and storage-quota breakdown.readOnly
get_agent_quotaGet send quotaGet the send-budget quota (sends today, effective daily limit, remaining, reset time, bound mailboxes); works with agent-scoped keys.readOnly
get_link_scanning_statusGet link scanning statusRead malicious link scanning (URL reputation) activation status. Enabling it is a human/admin action, not an agent one.readOnly

Drafts & scheduled send

ToolTitlePurposeAnnotations
create_draftCreate draftCreate a draft (the scanner runs synchronously and attaches a verdict); pass send_at to schedule dispatch.destructive, openWorld
update_draftUpdate draftUpdate a draft and re-scan; set/clear sub-address and send_at fields; clear_send_at cancels a scheduled send.destructive, openWorld
get_draftGet draftRead one draft by ID (body, scan results, schedule + sub-address fields).readOnly
list_draftsList draftsList drafts in a mailbox.readOnly
delete_draftDelete draftSoft-delete a draft (hides it from list_drafts; cancels any schedule).destructive, openWorld
send_draftSend draftSend a draft; the scanner re-runs authoritatively at send time. Distinct send-time conflicts surface as isError JSON.destructive, openWorld

Recipients

ToolTitlePurposeAnnotations
list_recipientsList recipientsList recipients with confirmation status.readOnly
add_recipientAdd recipientSandbox only. Add a person to the account's recipient list: they are emailed a confirmation link and become sendable when they click it. An attest argument adds the person without confirmation, using one of the trial's limited attestations; when none are left the call is refused with TIER_LIMIT, and when agent recipient writes are unavailable with INSUFFICIENT_SCOPE. A sign-in connection that isn't allowed to add recipients should ask the user to add the person.destructive, openWorld

Do-not-contact list

ToolTitlePurposeAnnotations
list_suppressionsList do-not-contact entriesList do-not-contact entries (system-added + customer-added; exact email or @domain).readOnly
add_suppressionAdd do-not-contact entryAdd an exact email or @domain to the do-not-contact list. One-way on MCP — removal is not exposed.destructive, idempotent
add_suppressions_bulkAdd do-not-contact entries in bulkBulk-add up to 1000 do-not-contact entries; returns a partial-success added / already_existed / invalid response.destructive, idempotent
remove_suppressionRemove do-not-contact entry (unavailable)Always returns isError — removal is intentionally not available via MCP; use the CLI, an SDK admin key, or the dashboard.readOnly

Suppression removal is a per-human admin decision by design. See the Do-not-contact guide for the full add / list / remove contract.

Recipient allowlist (outbound)

ToolTitlePurposeAnnotations
list_allowlistList recipient allowlistList a mailbox's recipient allowlist (only consulted in allowlist mode). The response's always_allowed array lists the four ReplyLayer test addresses, which are allowed without an entry unless the do-not-contact list suppresses them. Agents cannot mutate the allowlist via MCP — mutation is admin-only.readOnly
list_allowlist_blocked_attemptsList allowlist-blocked sendsInspect sends rejected by the recipient allowlist (aggregated summary by default; aggregate=false for raw history).readOnly

Inbound firewall

ToolTitlePurposeAnnotations
list_inbound_blocklistList inbound blocklistList the account-wide inbound sender blocklist.readOnly
add_inbound_blocklistAdd to inbound blocklistAdd an exact email or @domain to the inbound blocklist (one-way on MCP).destructive, idempotent
add_inbound_blocklist_bulkAdd to inbound blocklist in bulkBulk-add up to 1000 inbound blocklist entries; partial-success response.destructive, idempotent
list_inbound_allowlistList inbound allowlistList a mailbox's inbound sender allowlist.readOnly
add_inbound_allowlist_entryAdd to inbound allowlistAdd a sender (email or @domain) to a mailbox's inbound allowlist.destructive, idempotent
add_inbound_allowlist_bulkAdd to inbound allowlist in bulkBulk-add up to 1000 inbound allowlist entries; partial-success response.destructive, idempotent
list_inbound_firewall_blocked_attemptsList firewall-blocked sendersList incoming senders the inbound firewall rejected (aggregated by default; aggregate=false for raw history).readOnly
release_firewall_blocked_messageRelease firewall-blocked messageRelease a firewall-blocked message back into scanner processing (returns 202; re-read the message for the verdict). Mailbox-bound keys are limited to their mailbox.destructive

Triage & review

ToolTitlePurposeAnnotations
release_quarantined_messageRelease quarantined messageRelease a quarantined inbound message back to available. Refuses outbound rows. Mailbox-bound keys are limited to their mailbox.destructive
block_quarantined_messageBlock quarantined messageBlock a quarantined inbound message (terminal, no delivery). Refuses outbound rows.destructive
report_and_blockReport and block senderBlock a held inbound message and add its resolved sender to the account-wide inbound blocklist (idempotent). Refuses outbound rows.destructive, idempotent
approve_reviewApprove message held for reviewApprove a pending_review message and dispatch it. Admin keys + dashboard sessions only — agent keys get 403.destructive, openWorld
deny_reviewDeny message held for reviewDeny a pending_review message (terminal block, no dispatch). Admin keys + dashboard sessions only — agent keys get 403.destructive

Where contracts live

This page owns the MCP transport, tool registry, and result-envelope contract. For the platform contracts a tool references, see the owning page:

  • Message states, read semantics, and verdict vocabulary → Messages
  • What happens when a send is held, blocked, or contained → Send gates
  • Full error-code catalog → Errors
  • Attachment staging and preview lifecycle → Attachments
  • Tier and sandbox send limits → Limits
  • Per-field trust and untrusted-content handling → Security model
  • Webhook event catalog (dispatch, review, schedule signals) → Webhooks
  • CLI equivalents and exit codes → CLI

The replylayer-mcp package is published on npm.