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_logswith the sametenant_id,actor_type, andactor_idas the rest of the agent's actions. The existingemail.sent/email.receivedaudit 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:
| Column | Purpose |
|---|---|
id | rmc_… connection identifier |
tenant_id | Owning tenant |
mode | bearer (pasted API key) or oauth (one-click Resend OAuth) |
resend_account_id | Resend account id (optional, set after OAuth) |
api_key_encrypted | AES-256-GCM encrypted Resend API key |
api_key_prefix | Last 6 chars of the plaintext key, for UI display only |
scopes_json | Method allowlist the tenant has enabled |
status | active or revoked |
last_used_at | Timestamp of the most recent proxied call |
last_used_method | MCP method name of the most recent proxied call |
created_at | Creation timestamp |
revoked_at | Revocation 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_keyis required, must start withre_, and is 32+ chars.scopesis required, non-empty, and every entry must be in the curated allowlist. Unknown scopes return400 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
Authorizationheader is replaced with the tenant's encrypted Resend bearer. - The
User-Agentis rewritten toReverbin-MCP-Proxy/0.1 (+https://reverbin.com). - An
X-Reverbin-Tenant-Idheader is added (for Resend-side audit). - The JSON-RPC
methodfield is checked against the connection'sscopes_jsonallowlist. Denied methods return403 scope_deniedand never reach Resend.
The upstream response is streamed back to the agent. Reverbin's proxy:
- Logs the call to
audit_logsafter the response completes (so the audit row can include HTTP status, response size, and the Resend request id fromx-request-id). - Charges one unit against the tenant's
mcp_callquota. - 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_createdresend_mcp.connection_rotatedresend_mcp.connection_revokedresend_mcp.connection_test(withstatus_codeandlatency_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).
| Plan | mcp_call per month |
|---|---|
| Free | 2,000 |
| Developer | 10,000 |
| Startup Beta | 100,000 |
| Enterprise | custom |
The proxy 429 quota_exceededs when the tenant has spent its budget. The dashboard displays usage next to the connection card.
Failure modes
| Symptom | Cause |
|---|---|
401 unauthorized | Resend API key rejected by Resend |
403 scope_denied | Method not in connection scopes_json |
403 forbidden_method | Method in explicit deny list |
413 response_too_large | Upstream response body > 4 MB |
429 quota_exceeded | Monthly mcp_call budget exhausted |
502 upstream_error | Resend returned 5xx |
504 upstream_timeout | Resend 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_atandlast_used_methodTest connectionbutton (callsPOST /v1/integrations/resend-mcp/connection/test)Rotate secretbutton (regenerates the Resend API key in Resend, then re-encrypts locally)Revoke connectionbutton (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/testcalls Resend/v1/domainsand surfaces the result