As of 2026-07-13

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.

Live in production The endpoint below is reachable today and live for real sending (Gmail API, HTTPS/443) on activated accounts. New public signups default to an isolated sandbox tenant — no real domains, mailboxes, or sends — until activation. See FAQ for the full status disclosure.

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