What is a cold email MCP server? The cold-email MCP server for coding agents
A hosted, streamable-HTTP MCP server exposing 32 intent-level cold-email infrastructure tools, scoped by per-tenant bearer token and live in production. This page documents the exact tool surface — verified against the live tools/list response, not aspirational.
What this MCP server does
Most cold-email MCP servers in the wild re-export a vendor's full API surface — a well-known example ships 100+ tools. This one is a curated, high-level facade: 32 intents that hide domain-registrar, mailbox-vendor, and sequencing-engine plumbing behind calls an agent can reason about in one context window. Content generation — the offer, the subject lines, the sequence body — stays your agent's job; this server owns infrastructure provisioning, sequencing, isolation, and compliance guardrails. Five of the 32 (get_dashboard, configure_dashboard, label_thread, list_campaigns, activity) are the same facade the optional human dashboard at /app uses — parity by design, never dashboard-only state. For how that tool count compares to larger vendor-wrapper MCP servers, see Smartlead vs Instantly MCP tool coverage.
How to add it
Streamable HTTP, JSON-RPC 2.0 (initialize, tools/list, tools/call), one config line, no OAuth dance. Get a token first via POST /signup (see the setup guide), then add:
{
"mcpServers": {
"agent-cold-email": {
"url": "https://api.coldrig.dev/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}
The endpoint resolves your token fresh on every call — there is no session or cache that could leak another tenant's data. The same tool names and same per-tenant auth are also reachable as plain REST (see openapi.yaml) if your agent talks HTTP directly instead of MCP.
The 32 tools — exact schemas
Verified directly against the live tools/list response and openapi.yaml. Two additional helpers described in the design spec (write_sequence, suggest_domains) are not implemented — don't assume they exist.
setup_infrastructure
Buy branded lookalike domains, provision mailboxes, and start the warmup ramp. Domain purchase, DNS, provisioning, and warmup run before the response, not in the background — a `provisioning` field in the response names any work still owed; poll infrastructure_status or re-call this tool to progress it. Resend the same idempotencyKey (or omit it identically) to retry safely — the call converges onto what's already provisioned rather than buying again; a distinct idempotencyKey provisions additional infrastructure instead.
{
brand: string (1-200 chars),
primaryDomain: string (3-253 chars),
domains: integer (1-20),
inboxesEach: integer (1-10),
persona: string (1-200 chars),
physicalAddress: string (1-500 chars), // CAN-SPAM footer address
senderIdentity: string (1-200 chars), // legal sender identity, captured at setup
quoteOnly?: boolean, // true previews the new count + price, provisions nothing
registerDomains?: boolean, // omit = leave prior consent unchanged; true = opt in (requires registrant); false = revoke
registrant?: { firstName, lastName, email, phone, addressLine1, city, state, country, postalCode, organization? }
}
// the first 7 fields are required; quoteOnly/registerDomains/registrant are optional
infrastructure_status
Provisioning + warmup progress, per-mailbox health, and send-readiness. No parameters.
launch_campaign
Create and activate a campaign against a lead list. The caller supplies the offer and sequence step content — this platform does not generate outreach copy.
{
name: string (1-200 chars),
offer: string (1-2000 chars),
leads: [ { email: string, firstName?: string (up to 200 chars; needed by every lead when a step uses {{firstName}}), company?: string, vars?: { [key: word]: string (one line) } } ], // 1-5000 items; a {{key}} in a subject/body needs that key, non-empty, in every lead's vars
sequence: [ { step: integer (>=1),
subject: string (1-300 chars; required on step 1 — a later step may omit it to stay in step 1's thread, resolving to "Re: " + step 1's subject),
body: string (1-20000 chars), delayDays: integer (0-60) } ], // 1-10 items
timezone: string, // default "UTC"
sendWindow: { startHour: integer (0-23), endHour: integer (0-23, inclusive), days?: integer[] (0=Sun..6=Sat) }, // default {startHour:8, endHour:16, days:[1,2,3,4,5]}
stopOnReply: boolean, // default true
previewOnly?: boolean, // true = dry run: checks run, the first three leads' rendered steps come back, nothing is created or sent
includeAlreadyEmailed?: boolean // true also emails people who already got this campaign's first email; default false, they are skipped and counted
}
People who already got this campaign's first email from another campaign of this account are skipped and counted in alreadyEmailed. To include them, send includeAlreadyEmailed: true on the launch itself (previewOnly: true shows the count first), or after a launch add them to that same campaign with add_leads and includeAlreadyEmailed: true. Never launch the same list again to include them: that emails everyone who was not skipped a second time.
campaign_results
Sends, replies, bounces, and complaints for one campaign, plus unique contacts (contacts, contacted, replied), replyRate, positiveReplies, and a perStep split.
{ campaignId: string }
list_campaigns
List every campaign for the tenant with id, name, status, and event counts (sent/reply/bounce/...) — no per-campaign lookup needed. No parameters.
metrics
Account-wide outcome totals across all campaigns (sent, reply, bounce, complaint, unsubscribe, failed, soft_bounce, auto_reply). auto_reply counts automatic replies such as out-of-office, which are never counted as replies. No parameters. For warmup/deliverability health, use infrastructure_status.
activity
Unified, chronological activity feed merging campaign events (sent/reply/bounce/...) with deliverability control-loop actions (pause/throttle/...).
{
limit?: integer (1-200), // default 50
cursor?: string,
kind?: "event" | "deliverability" // omit for every kind
}
inbox
Unified reply inbox across all mailboxes for the tenant. No parameters.
thread
Full message history for one thread.
{ threadId: string }
reply
Send a reply on an existing thread.
{ threadId: string, body: string (1-20000 chars) }
mark
Mark a thread read, unread, or archived.
{ threadId: string, status: "read" | "unread" | "archived" }
label_thread
Set (or, with label: null, clear) a triage label on an inbox thread — the same labels the dashboard UI shows as chips.
{ threadId: string, label?: string (1-100 chars) | null } // default null
pause
Pause one campaign.
{ campaignId: string }
pause_all
Pause every campaign for the tenant. No parameters.
account
Usage, billing state, quota, and what the AI deliverability control loop has done (paused/throttled mailboxes, burning domains, auto-replacements, recent actions). No parameters.
remove_mailboxes
Downgrade: release your N newest live mailboxes now and lower the billed quantity. acknowledged must be true — the release is immediate for provisioning, but there is no mid-cycle credit; the lower price takes effect at your next renewal (minimum 5 mailboxes / $99). To add mailboxes instead, use setup_infrastructure or configure_byo_domain's request_managed_mailboxes.
{
count: integer (1-60),
acknowledged: true
}
get_dashboard
List every saved dashboard view (id, name, isDefault, rev, editedBy) or, with id, fetch one view's full layout + rev. The agent-controlled counterpart to the optional human dashboard at /app.
{ id?: string (1-200 chars) } // omit to list every view
configure_dashboard
Create, update, promote-to-default, or delete a dashboard saved view. update requires the rev you last read; a stale rev returns a structured conflict (currentRev + currentLayout) so you can rebase and retry.
{
action: "create" | "update" | "promote" | "delete",
id?: string, // required for update/promote/delete
name?: string (1-200 chars), // required for create; optional rename on update
rev?: integer, // required for update (rev-CAS)
layout?: { schemaVersion: 1, widgets: [...] }, // required for create/update
note?: string (0-2000 chars)
}
get_webhooks
List every outbound webhook subscription, or, with id, fetch one subscription plus its recent delivery and attempt log. Secrets are never returned on reads — shown once at create/rotate.
{ id?: string (1-200 chars) } // omit to list every subscription
configure_webhook
Create, update, or delete an outbound webhook subscription — push reply, bounce, soft_bounce, complaint, and unsubscribe events (HMAC-SHA256 signed, header X-Coldrig-Signature) to your own HTTPS endpoint. URLs must be https and resolve to a public host.
{
action: "create" | "update" | "delete",
id?: string, // required for update/delete
url?: string, // required for create; optional on update
eventTypes?: ("reply" | "bounce" | "soft_bounce" | "complaint" | "unsubscribe")[], // required for create; 1-5 items
secret?: string (16-200 chars), // omit on create to auto-generate; set to rotate
active?: boolean // default true
}
get_byo_domains
List your bring-your-own domains, or, with id, fetch one domain's full intake detail (pre-flight scan, abuse verdict, consent status). byoStatus progresses pending_kyc|pending_consent|pending_dns → active (or rejected/abandoned).
{ id?: string (1-200 chars) } // omit to list every domain
configure_byo_domain
Register or advance a BYO domain intake. Registration, the pre-flight scan, and consent tracking are live today; real DNS delegation, live reputation checks, and platform-provisioned mailboxes on your own domain are self-serve and automatic once you've gone live, with mailbox send-authorization completing on our side.
{
action: "register" | "poll_dns" | "acknowledge_consent" | "request_managed_mailboxes" | "connect_mailbox",
domain?: string (3-253 chars), // required for register
domainRelationship?: "fresh_standalone" | "subdomain_of_primary" | "is_primary", // required for register
id?: string, // required for poll_dns/acknowledge_consent/request_managed_mailboxes/connect_mailbox
acknowledged?: true, // required (must be true) for acknowledge_consent
count?: integer (1-10), // required for request_managed_mailboxes
personaSlug?: string (1-50 chars), // optional for request_managed_mailboxes
email?: string, // required for connect_mailbox
transport?: { kind: "smtp", host, port, secure, user, pass }
| { kind: "gmail_api", clientId, clientSecret, refreshToken }
| { kind: "ms_graph", mode: "delegated" | "app_only", tenantId, clientId, clientSecret, refreshToken? }
// required for connect_mailbox — declares a mailbox connection YOU already hold; not this platform's own send transport
}
No pricing is published yet for BYO-connected mailboxes specifically, and a burned BYO domain hard-pauses rather than auto-replacing (unlike a burned lookalike domain) — see the BYO domain page for the full risk model.
suppress_lead
Permanently suppress an email address tenant-wide — every current and future campaign — the manual/free-text "stop emailing me" path for opt-outs the strict typed-unsubscribe matcher misses. Cancels every pending send and marks every campaign-lead row for this email suppressed. There is no un-suppress tool.
{
email: string (email),
reason?: "manual", // the only value this tool honestly claims — bounce/complaint/unsubscribe are recorded automatically elsewhere
note?: string (0-2000 chars) // accepted for schema symmetry, not persisted
}
update_lead
Record what you learned about a contact — their reply, your triage — as a durable, contact-level disposition keyed by email, visible across every campaign that lists them. A partial patch: only the fields you pass are changed.
{
email: string (email),
interestStatus?: "none" | "interested" | "meeting_booked" | "not_now" | "not_interested" | "bad_fit" | "out_of_office" | "wrong_person",
notes?: string (0-5000 chars),
tags?: string[] (0-50 items, each 1-100 chars)
}
// at least one of interestStatus, notes, tags is required
list_leads
List/export leads with their contact-level disposition, cursor-paginated. This is the export surface — paginate to dump the full book of business as JSON (no separate CSV endpoint).
{
campaign?: string (1-200 chars),
interestStatus?: "none" | "interested" | "meeting_booked" | "not_now" | "not_interested" | "bad_fit" | "out_of_office" | "wrong_person",
suppressed?: boolean,
replied?: boolean,
cursor?: string (1-500 chars),
limit?: integer (1-200) // default 50
}
Returns { leads[], nextCursor }; each row: leadId, email, firstName, company, campaignId, campaignName, globalStatus, interestStatus, notes, tags, suppressed, lastEventType, lastEventTs, createdAt.
list_messages
List this tenant's system + operator messages (a retryable setup step, a credential going live, an operator notice), cursor-paginated. Unacked messages sort first (newest first within that group), then acked ones (also newest first). infrastructure_status also inlines the newest 5 unacked messages for a quick glance — this is the full paginated surface.
{
cursor?: string (1-500 chars),
limit?: integer (1-200) // default 50
}
Returns { messages[], nextCursor }; each message: id, kind, severity, body, actionHint, source (system|operator), createdAt, readAt (null until acked).
ack_message
Acknowledge a message by id (from list_messages or infrastructure_status's messages[]) — sets it read so it stops surfacing as unacked. Idempotent: acking an already-acked id returns success with no second effect, never an error. 404 if the id doesn't exist for this tenant.
{ messageId: string }
contact_operator
Reach a human operator — files a support ticket and notifies the operator. Works in every account state a tenant token still authenticates in, including dunning-suspended, canceling and canceled (an admin-terminated abuse account is the one exception — its token 401s at auth). The reply arrives as a message on this account (list_messages / infrastructure_status's messages[] — there is no separate reply-fetch call). Retrying with the identical body and urgency within an hour is safe: same ticketId, no second alert — re-sending the same text at a higher urgency is an escalation and files a new ticket. needs_human also bypasses the ~10-minute alert throttle. Rate-limited to 5 calls/hour per tenant.
{
body: string (1-2000 chars),
urgency?: "normal" | "needs_human" // default "normal"
}
Returns { ticketId, note }.
submit_feedback
Send private feedback to the Coldrig founders about how the product is working for you. It is stored privately and emailed to the founders, never published; it opens no support ticket and changes nothing on your account (use contact_operator for a problem that needs an answer). Each call is a separate submission.
{
score?: integer (0-10),
text?: string (1-2000 chars) // at least one of score / text is required
}
Returns { feedbackId, received }.
add_leads
Add contacts to a campaign that is already running (active or paused). Each new contact starts at step 1 on the campaign's own schedule, under every launch rule. An address already in the campaign, a suppressed address, an address repeated in the request, and a person who already got this email are skipped and counted, never an error.
{
campaignId: string,
leads: [ { email: string, firstName?: string, company?: string, vars?: { [key: word]: string } } ], // 1-5000 items; 5000 per campaign in total
listSource?: string, // required on a crowdfunding account
includeAlreadyEmailed?: boolean // true also adds people who already got this campaign's first email from another campaign
}
People who already got this campaign's first email from another campaign of this account are skipped and counted in alreadyEmailed. To include them, send includeAlreadyEmailed: true on the launch itself (previewOnly: true shows the count first), or after a launch add them to that same campaign with add_leads and includeAlreadyEmailed: true. Never launch the same list again to include them: that emails everyone who was not skipped a second time.
Returns { campaignId, added, suppressed, alreadyInCampaign, alreadyEmailed, duplicatesRemoved, contacts }.
resume
Resume one paused campaign. On a paid account, resuming sends nothing by itself: the held steps continue through the normal schedule, one step at a time per lead, and leads stopped on their own (replied, unsubscribed, bounced, suppressed, or stopped with pause leadEmails) stay stopped. Refused if the campaign is not paused, is past its end date, or is a trial campaign on an account that has gone live.
{ campaignId: string }
Returns { resumed: true }.
plan_campaign
Plan a dated outreach campaign (for example a fundraising drive) and get its price. Read-only: nothing is booked, charged or changed. safeReach is how many contacts the warm-up ramp can safely reach if you book on bookingDate; the price is for that many. A bookingDate after the last campaign day is refused 400. Campaign checkout and booking are not available yet: to book, contact support (contact_operator).
{
contacts: integer (>= 1),
campaignStart: "YYYY-MM-DD", // first campaign day, UTC
campaignDays: integer (1-366),
bookingDate: "YYYY-MM-DD", // the day you would book, normally today
domainsAlreadyHeld?: integer, // default 0
listIsOwnOptIn?: boolean // default false
}
Returns { mailboxes, domainsToBuy, bookBy, advanceStartOn, safeReach, priceCents, warnings[] }. Warnings: over_60_mailboxes, late_booking, own_list_cheaper_on_esp.
The no-signup CLI demo
npx agent-cold-email demo mints a demo tenant, provisions sample infrastructure, and runs the accelerated sandbox pipeline end to end (warmup, sends, replies, bounces, stop-on-reply) in one command — no signup or token-wrangling required. The npm CLI is live at v0.2.2. The same 32 tools are available over the hosted MCP endpoint or directly over HTTP (quickstart). The demo path stays sandboxed; real sending is live in production, and going live is self-serve — call POST /checkout, pay, and mailbox send-authorization completes on our side.
Auth model
One bearer token per tenant, minted by the unauthenticated POST /signup bootstrap call. Every other tool call requires Authorization: Bearer <token> and resolves server-side to exactly one tenant's isolated state. The one exception is the crowdfunding shared bounce list, which holds only hashed addresses that hard-bounced or complained, with no customer identity attached.
Related
- Run your entire cold email operation with Claude Code — the flagship end-to-end lifecycle guide, plus a reusable Claude Code Skill.
- Run your entire cold email operation with Cursor — exact mcp.json config and Cursor Rules.
- Run your entire cold email operation with Codex — exact config.toml and native AGENTS.md context.
- How to run cold email with your AI coding agent — the full step-by-step walkthrough.
- 32 intent-level tools vs larger vendor MCP surfaces — lifecycle coverage and control tradeoffs.
- Smartlead vs Instantly vs agent-cold-email.
- Docs — quickstart, CLI usage, auth model.
- openapi.yaml — the same 32 intents as REST.
- MCP server card — machine-readable transport + tool list for registry scans.