使用说明
Resend MCP Server
Streamable HTTP MCP server for email and newsletter management via Resend.
Protocol status: candidate
2026-07-28, using exact MCP server/client2.0.0-beta.5. This is not a claim of final protocol conformance.
Author: overment
[!WARNING] This server gives an AI agent access to your email sending capabilities. Language models can make mistakes, misinterpret instructions, or send unintended emails. Always:
- MAKE SURE that the client you use with this MCP requires you to REVIEW and CONFIRM the action before use.
- Test with a small segment first
- Broadcasts are always scheduled (minimum 5 minutes ahead) so you can cancel if needed (default is 5 minutes)
- Set
RESEND_DEFAULT_FROMto a verified sender address
Motivation
Traditional email APIs require knowing exact IDs, segment structures, and API quirks. This server is designed so AI agents can:
- Send newsletters — broadcast to segments with proper formatting
- Manage subscribers — add, update, remove contacts in bulk
- Use human identifiers — segment names and emails, not UUIDs
- Handle formatting — automatic multipart emails (HTML + plain text)
- Track campaigns — view delivery status, cancel scheduled sends
The result: an agent that can reliably manage your newsletter without you touching the Resend dashboard.
Features
- ✅ Contacts — Add, update, remove, search contacts (bulk-capable, up to 100 per call)
- ✅ Segments — Create, list, delete segments and manage membership
- ✅ Send — Individual emails or broadcast to segments
- ✅ Campaigns — View history, check delivery status, cancel scheduled
- ✅ Templates — List available templates with variables
- ✅ Subscriptions — Topic opt-in/opt-out management
- ✅ Multipart Emails — Auto-generates HTML + plain text for best deliverability
- ✅ Dual Runtime — Fetch-native Bun or Cloudflare Workers
- ✅ Modern + Legacy — Candidate
2026-07-28plus SDK stateless legacy fallback - ✅ Credential Separation — MCP access tokens are never forwarded to Resend
Design Principles
- LLM-friendly: Task-oriented tools, not 1:1 API mirrors
- Bulk-first: Contact operations accept arrays by default
- Human identifiers: Use emails and segment names, not UUIDs
- Multipart by default: Plain text auto-generates HTML for proper rendering
- Clear feedback: Results include summaries and next-step suggestions
Server Instructions (What the Model Sees)
Use these tools to manage email contacts, segments, and send emails via Resend.
WORKFLOW:
1. find_contacts or segments(list) → discover existing data
2. upsert_contacts → add/update subscribers
3. send → individual email or broadcast to segment
4. campaigns(status) → track delivery
FORMATTING (for body):
- Use \n\n between paragraphs
- Use \n between list items
- Plain text is auto-converted to multipart (HTML + text) for proper rendering
IMPORTANT:
- Segment names are case-insensitive
- Templates must be published in Resend dashboard to use
- Broadcasts use segment name, not ID
Quick Start
Prerequisites
- Bun or Node.js 20+
- Resend Account with verified domain
1. Install
cd resend-mcp
bun install
cp env.example .env
2. Configure
Get your Resend API key from resend.com/api-keys.
Edit .env:
PORT=3000
MCP_AUTH_MODE=static-bearer
# Generate with: openssl rand -hex 32
MCP_BEARER_TOKEN=your-random-auth-token
# Resend credentials
RESEND_API_KEY=re_your_resend_api_key
RESEND_DEFAULT_FROM=newsletter@yourdomain.com
3. Run
bun dev
# MCP: http://127.0.0.1:3000/mcp
4. Connect Client
Alice App:
- URL:
http://127.0.0.1:3000/mcp - Type:
streamable-http - Header:
Authorization: Bearer <your-MCP_BEARER_TOKEN>
Claude Desktop / Cursor:
{
"mcpServers": {
"resend": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:3000/mcp", "--transport", "http-only"],
"env": { "NO_PROXY": "127.0.0.1,localhost" }
}
}
}
Tools
upsert_contacts
Add or update contacts in bulk. Creates if email doesn't exist, updates if it does.
// Input
{
contacts: [
{ email: "john@example.com", first_name: "John", last_name: "Doe" },
{ email: "jane@example.com", properties: { plan: "pro", company: "Acme" } }
],
segments?: ["Newsletter", "Premium"], // Add to these segments
unsubscribed?: false // Set subscription status
}
// Output
{
results: [{ email, ok, id, action: "created" | "updated", error? }],
summary: { created: 1, updated: 1, failed: 0 }
}
remove_contacts
Permanently delete contacts from your list.
// Input
{ emails: ["user@example.com", "other@example.com"] }
// Output
{ results: [{ email, ok, error? }], summary: { deleted: 2, failed: 0 } }
find_contacts
Search and filter contacts with pagination.
// Input
{
segment?: "Newsletter", // Filter by segment name
email?: "user@example.com", // Find specific contact
unsubscribed?: false, // Filter by status
limit?: 50, // Max 100
cursor?: "..." // From previous response
}
// Output
{
items: [{ id, email, first_name, last_name, unsubscribed, created_at, properties }],
has_more: boolean,
cursor?: string
}
segments
Manage segments and their members.
// List all segments
{ action: "list" }
→ { items: [{ id, name, contact_count, created_at }] }
// Create segment
{ action: "create", name: "VIP Customers" }
→ { id, name, ok: true }
// Delete segment (contacts remain in system)
{ action: "delete", name: "Old Segment" }
// Add contacts to segment
{ action: "add_contacts", name: "Newsletter", contacts: ["a@x.com", "b@x.com"] }
// Remove contacts from segment
{ action: "remove_contacts", name: "Newsletter", contacts: ["a@x.com"] }
send
Send individual emails or broadcast to segments.
// Individual email
{
to: "user@example.com", // or array up to 50
subject: "Welcome!",
body: "Hello {{{FIRST_NAME}}}!\n\nWelcome to our newsletter.",
from_name?: "Alice Newsletter",
reply_to?: "support@example.com",
schedule_for?: "2024-12-25T10:00:00Z"
}
// Broadcast to segment
{
segment: "Newsletter", // Segment name (case-insensitive)
subject: "Weekly Update",
name?: "Week 42 Newsletter", // Dashboard display name
body: "HEADLINE\n\nFirst paragraph.\n\nSecond paragraph.",
schedule_for?: "in 2 hours" // Natural language supported
}
// Using template
{
to: "user@example.com",
template: "welcome-email", // Template alias or ID
variables: { CTA_TEXT: "Get Started", DISCOUNT: "20%" }
}
Formatting tips:
- Use
\n\nbetween paragraphs - Use
\nbetween list items - Plain text is auto-converted to HTML with proper line breaks
- Personalization:
{{{FIRST_NAME}}}or{{{FIRST_NAME|Friend}}}
campaigns
View and manage broadcast campaigns.
// List recent campaigns
{ action: "list", limit?: 20 }
→ { items: [{ id, name, subject, segment_name, status, sent_at, recipients_count }] }
// Get detailed status
{ action: "status", campaign_id: "bc123..." }
→ { id, status, sent_count, delivered_count, opened_count, failed_count }
// Cancel scheduled campaign
{ action: "cancel", campaign_id: "bc123..." }
→ { id, cancelled: true }
subscriptions
Manage topic preferences for contacts.
// Subscribe to topic
{ emails: "user@example.com", action: "subscribe", topic: "Product Updates" }
// Unsubscribe from topic
{ emails: ["a@x.com", "b@x.com"], action: "unsubscribe", topic: "Newsletter" }
// Global unsubscribe (from all emails)
{ emails: "user@example.com", action: "unsubscribe_all" }
templates
List available email templates.
// Input
{ limit?: 50, cursor?: "..." }
// Output
{
items: [{ id, alias, name, subject, from, variables: [{ key, type, fallback }] }],
has_more: boolean
}
Examples
1. Import subscribers and send welcome email
// Add contacts to Newsletter segment
{
"name": "upsert_contacts",
"arguments": {
"contacts": [
{ "email": "alice@example.com", "first_name": "Alice" },
{ "email": "bob@example.com", "first_name": "Bob" }
],
"segments": ["Newsletter"]
}
}
Response:
Added 2 contact(s). Created: 2, Updated: 0, Failed: 0.
Next: Use 'find_contacts' to verify or 'send' to email them.
2. Send a broadcast newsletter
{
"name": "send",
"arguments": {
"segment": "Newsletter",
"subject": "This Week in AI",
"name": "Week 42 Newsletter",
"body": "HIGHLIGHTS\n\nClaude 4 was released this week with impressive new capabilities.\n\nKEY FEATURES\n\n• Extended context window\n• Improved reasoning\n• Better code generation\n\nREAD MORE\n\nCheck out the full announcement on our blog."
}
}
Response:
Broadcast to segment "Newsletter" queued for sending (Campaign ID: abc123).
Use 'campaigns' tool to track delivery.
3. Check campaign delivery
{
"name": "campaigns",
"arguments": { "action": "status", "campaign_id": "abc123" }
}
Response:
Campaign abc123: sent
- Sent: 1,234
- Delivered: 1,198
- Opened: 456 (38%)
- Failed: 36
4. Unsubscribe a user
{
"name": "subscriptions",
"arguments": {
"emails": "unhappy@example.com",
"action": "unsubscribe_all"
}
}
Response:
Updated 1 contact(s). Success: 1, Failed: 0.
Authentication Flow
┌─────────────────────────────────────────────────────────────────┐
│ Client (Alice App, Claude Desktop) │
│ │ │
│ │ Authorization: Bearer <MCP_BEARER_TOKEN> │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ MCP Server (Node.js / Cloudflare Worker) ││
│ │ ││
│ │ 1. Validate MCP_BEARER_TOKEN or OAuth RS token ││
│ │ 2. Use RESEND_API_KEY internally ││
│ │ ││
│ │ RESEND_API_KEY ──────────► Resend API ││
│ │ RESEND_DEFAULT_FROM ─────► (api.resend.com) ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
Key points:
MCP_BEARER_TOKEN: Optional random MCP client token; legacyBEARER_TOKENremains a compatibility aliasRESEND_API_KEY: Server-side Resend key — never exposed to clients and never sourced from an MCP bearerRESEND_DEFAULT_FROM: Verified sender address — required
Deployment Options
Local Development (Node.js/Bun)
bun dev
# Endpoint: http://127.0.0.1:3000/mcp
Cloudflare Worker (Local Dev)
# Create .dev.vars with secrets
echo "MCP_BEARER_TOKEN=your_token" >> .dev.vars
echo "RESEND_API_KEY=re_xxx" >> .dev.vars
echo "RESEND_DEFAULT_FROM=newsletter@yourdomain.com" >> .dev.vars
bun x wrangler dev --local | cat
# Endpoint: http://127.0.0.1:8787/mcp
Cloudflare Worker (Production)
# Set secrets
bun x wrangler secret put MCP_BEARER_TOKEN
bun x wrangler secret put RESEND_API_KEY
bun x wrangler secret put RESEND_DEFAULT_FROM
# Deploy
bun x wrangler deploy
# Endpoint: https://<worker>.<account>.workers.dev/mcp
HTTP Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/mcp |
POST | Modern or stateless-legacy MCP exchange |
/mcp |
GET, DELETE | 405 Method Not Allowed (no server-created sessions) |
/health |
GET | Candidate protocol/runtime status |
Environment Variables
| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
✓ | Resend API key from resend.com/api-keys |
RESEND_DEFAULT_FROM |
✓ | Verified sender address (e.g., newsletter@yourdomain.com) |
MCP_AUTH_MODE |
none, static-bearer, or oauth (default: none) |
|
MCP_BEARER_TOKEN |
For static auth | MCP client token (legacy BEARER_TOKEN is accepted) |
MCP_PUBLIC_URL |
Production | Canonical MCP resource URL |
MCP_ALLOWED_HOSTS |
Strict Host allowlist | |
MCP_ALLOWED_ORIGIN_HOSTNAMES |
Strict browser Origin hostname allowlist | |
PORT |
Server port (default: 3000) | |
HOST |
Server host (default: 127.0.0.1) | |
LOG_LEVEL |
debug, info, warn, error (default: info) |
Architecture
src/
├── index.ts # Bun fetch entry
├── worker.ts # Cloudflare Workers entry
├── config/ # Validated runtime/provider config + metadata
├── core/ # Fresh McpServer factory + isolate handler
├── http/ # Auth, body bounds, Host/Origin/CORS boundary
├── services/resend/client.ts # Preserved Resend provider client
├── schemas/outputs.ts # Complete Zod v4 output schemas
└── shared/tools/resend/ # Preserved email/contact/campaign/template tools
Development
bun dev # Start with hot reload
bun start # Run the stateless HTTP server
bun run typecheck # TypeScript check
bun run lint # Lint code
bun run build # Production build
Testing with MCP Inspector:
bunx @modelcontextprotocol/inspector
# Connect to: http://localhost:3000/mcp
# For static auth: Authorization: Bearer <your-MCP_BEARER_TOKEN>
Troubleshooting
| Issue | Solution |
|---|---|
| 401 Unauthorized | Check MCP_AUTH_MODE and MCP_BEARER_TOKEN, or the configured OAuth RS token |
| "RESEND_API_KEY not configured" | Set RESEND_API_KEY in .env or secrets |
| "RESEND_DEFAULT_FROM not configured" | Set RESEND_DEFAULT_FROM to a verified sender |
| "Segment not found" | Use segments(action='list') to see available segments |
| "Template not found" | Ensure template is published in Resend dashboard |
| No newlines in email | Already fixed — emails are multipart (HTML + text) |
| Rate limit (429) | Resend default is 2 req/s. Wait for retry-after header |
| Tools not showing | Reconnect the client and verify it negotiated modern 2026-07-28 or the stateless legacy fallback |
License
MIT