reverbin
Menu

Resend MCP integration

How Reverbin proxies the Resend hosted MCP server so agents can publish templates, send broadcasts, and manage contacts with a single encrypted connection and one audit row per call.

Reverbin speaks the Resend hosted MCP server on behalf of tenant agents so that every agent action against Resend is scoped, auditable, and quota-enforced.

This integration is proxied, tenant-scoped, and allowlisted. Reverbin never gives the agent a direct Resend API key. Instead, the tenant stores a single Resend API key (encrypted) on a resend_mcp_connections row, and the agent calls Reverbin's /v1/integrations/resend-mcp/proxy endpoint to talk to Resend. Reverbin rewrites the request, attaches the tenant's bearer token, enforces an allowlist of MCP methods, and writes an audit_logs row per call.

Why a proxy

  • Auditability. Every agent-initiated Resend call must be visible in audit_logs with the same tenant_id, actor_type, and actor_id as the rest of the agent's actions. The existing email.sent / email.received audit chain only covers mail that flows through Reverbin's pipeline. Templates, broadcasts, contacts, audiences, and domains actions do not. The proxy fills that gap.
  • Quota enforcement. Direct MCP calls bypass Reverbin's plan quotas. The proxy charges a single unit per method invocation, so Free/Developer/Startup tenants cannot burn their monthly Resend budget through an unattended agent.
  • Scope narrowing. Reverbin treats the Resend API key as a privileged operator credential. The proxy exposes a curated method allowlist and refuses destructive actions (domains.delete, audiences.delete, contacts.batch_delete) at the proxy layer. Agents can create a contact but cannot wipe an audience.
  • OAuth compatibility. A tenant can connect Reverbin to Resend with a one-click OAuth flow or with a pasted Resend API key. Either way Reverbin stores only the encrypted API key; the agent never sees Resend credentials.

Connection model

A tenant may have at most one active resend_mcp_connections row. The row stores:

ColumnPurpose
idrmc_… connection identifier
tenant_idOwning tenant
modebearer (pasted API key) or oauth (one-click Resend OAuth)
resend_account_idResend account id (optional, set after OAuth)
api_key_encryptedAES-256-GCM encrypted Resend API key
api_key_prefixLast 6 chars of the plaintext key, for UI display only
scopes_jsonMethod allowlist the tenant has enabled
statusactive or revoked
last_used_atTimestamp of the most recent proxied call
last_used_methodMCP method name of the most recent proxied call
created_atCreation timestamp
revoked_atRevocation timestamp

The plaintext API key is returned exactly once on POST /v1/integrations/resend-mcp/connections and never again. Listing endpoints show the prefix only.

Endpoints

All endpoints are tenant-scoped (requireApiKey + requireAccountAccess + requirePrivateRouteScope).

GET    /v1/integrations/resend-mcp/connection       # current connection (no key)
POST   /v1/integrations/resend-mcp/connection       # create / replace (returns key once)
POST   /v1/integrations/resend-mcp/connection/test  # Resend /v1/domains call to verify key
DELETE /v1/integrations/resend-mcp/connection       # revoke (soft delete, audit)
POST   /v1/integrations/resend-mcp/connection/rotate-secret
POST   /v1/integrations/resend-mcp/proxy            # Streamable HTTP → mcp.resend.com/mcp

POST /v1/integrations/resend-mcp/connection

Request body:

{
  "api_key": "re_…",
  "scopes": ["templates.read", "templates.publish", "broadcasts.create", "contacts.read", "contacts.create"]
}
  • api_key is required, must start with re_, and is 32+ chars.
  • scopes is required, non-empty, and every entry must be in the curated allowlist. Unknown scopes return 400 invalid_scope.
  • Replaces the existing connection (re-encrypts the new key and resets last_used_at).
  • Returns { id, mode, api_key, prefix, scopes, status, created_at, secret_returned_once: true }.

POST /v1/integrations/resend-mcp/proxy

The proxy is Streamable-HTTP only (MCP 2025-03-26 transport). The agent must send an MCP initialize request first, then a stream of JSON-RPC messages.

The request body is forwarded as-is to https://mcp.resend.com/mcp with these modifications:

  • The Authorization header is replaced with the tenant's encrypted Resend bearer.
  • The User-Agent is rewritten to Reverbin-MCP-Proxy/0.1 (+https://reverbin.com).
  • An X-Reverbin-Tenant-Id header is added (for Resend-side audit).
  • The JSON-RPC method field is checked against the connection's scopes_json allowlist. Denied methods return 403 scope_denied and never reach Resend.

The upstream response is streamed back to the agent. Reverbin's proxy:

  • Logs the call to audit_logs after the response completes (so the audit row can include HTTP status, response size, and the Resend request id from x-request-id).
  • Charges one unit against the tenant's mcp_call quota.
  • Rejects calls with response bodies > 4 MB (413 response_too_large).
  • Times out at 30 s (504 upstream_timeout).

Allowlist

The default curated allowlist (RESEND_MCP_ALLOWED_SCOPES in src/resend-mcp.ts):

# Templates
templates.read
templates.publish
templates.delete

# Broadcasts
broadcasts.read
broadcasts.create
broadcasts.send

# Contacts
contacts.read
contacts.create
contacts.update
contacts.delete

# Audiences
audiences.read
audiences.create
audiences.update

# Domains
domains.read
domains.verify

Explicitly denied regardless of scope state:

domains.delete
audiences.delete
api-keys.create
api-keys.rotate
api-keys.delete
account.delete

These are the methods that would let an agent wipe the tenant's Resend account, revoke its own credentials, or rotate keys out from under Reverbin. Reverbin never forwards them.

Audit

Every successful or failed proxy call writes one audit_logs row:

{
  "action": "resend_mcp.method",
  "target_type": "resend_mcp_connection",
  "target_id": "rmc_…",
  "metadata_json": {
    "method": "broadcasts.create",
    "resend_request_id": "abc123",
    "status_code": 200,
    "duration_ms": 412,
    "response_bytes": 1024,
    "scope_check": "allowed"
  }
}

Connection lifecycle actions also write audit rows:

  • resend_mcp.connection_created
  • resend_mcp.connection_rotated
  • resend_mcp.connection_revoked
  • resend_mcp.connection_test (with status_code and latency_ms)

Quotas

A new plan resource mcp_call is added to plan_quotas. Defaults match the email volume budget per plan (so the agent cannot burn more Resend budget than its mail quota).

Planmcp_call per month
Free2,000
Developer10,000
Startup Beta100,000
Enterprisecustom

The proxy 429 quota_exceededs when the tenant has spent its budget. The dashboard displays usage next to the connection card.

Failure modes

SymptomCause
401 unauthorizedResend API key rejected by Resend
403 scope_deniedMethod not in connection scopes_json
403 forbidden_methodMethod in explicit deny list
413 response_too_largeUpstream response body > 4 MB
429 quota_exceededMonthly mcp_call budget exhausted
502 upstream_errorResend returned 5xx
504 upstream_timeoutResend did not respond within 30 s
connection.status === 'revoked'DELETE was called; recreate to use again

UI surface

/mail/settings gains a Resend MCP card under the existing Advanced integrations disclosure. The card shows:

  • connection status (active / not connected / revoked)
  • prefix of the stored key (last 6 chars)
  • enabled scopes
  • last_used_at and last_used_method
  • Test connection button (calls POST /v1/integrations/resend-mcp/connection/test)
  • Rotate secret button (regenerates the Resend API key in Resend, then re-encrypts locally)
  • Revoke connection button (soft delete)

The card is hidden when the connection is revoked for > 30 days.

OAuth vs bearer

Reverbin prefers bearer by default: agents paste a Resend API key with the minimum scopes their workflow needs (e.g. templates:write, broadcasts:write, contacts:write). Bearer is simple, headless-safe, and works in CI.

OAuth is supported for Dustin's own Reverbin install via the Resend connector directory, but Reverbin does not host a Resend OAuth callback URL. Tenants who want one-click OAuth instead run a Reverbin self-hosted build on their own domain and configure RESEND_OAUTH_CLIENT_ID / RESEND_OAUTH_CLIENT_SECRET / RESEND_OAUTH_REDIRECT_URI. Self-hosted Reverbin exchanges the authorization code for a Resend API key, encrypts it, and stores it in resend_mcp_connections.mode = 'oauth'. Reverbin Cloud (the managed offering) only accepts bearer connections.

Migration

migrations.ts migration 013_resend_mcp_connections creates the table and indexes idempotently. sql/schema.sql is updated for fresh-bootstrap parity.

Tests

tests/resend-mcp.test.ts covers:

  • connection create / list / revoke lifecycle
  • secret encryption round-trip
  • scope allowlist enforcement (allowed / denied / forbidden)
  • method deny list enforcement
  • audit row written for every proxy call
  • quota exhaustion returns 429
  • upstream 5xx / timeout / oversize response handling
  • signature tampering is rejected
  • connection/test calls Resend /v1/domains and surfaces the result