MCP for Cold Email Infrastructure: Domains and Mailboxes
Give an AI agent your cold email infrastructure through MCP: check domains, connect your own, list mailboxes. Which actions to allow, which to keep manual.
On this page
An AI agent that works on cold email infrastructure needs to know which domains you have, which names are free, which mailboxes exist and where each domain is in its setup. Most of that is reading. A few actions change things, and one of them, buying domains, costs money the moment it runs.
This post sorts the domain and mailbox operations of the Outreach2day API into what an agent may do on its own, what it may do after you approve a plan, and what stays with a person. Then it builds an MCP server for Claude or Cursor that follows those rules. A hosted Outreach2day MCP server is in development (Get early access on the home page); this server runs locally over the public REST API.
Which Actions an Agent Should Get
The four layers of cold email infrastructure are domains, mailboxes, warm-up and the sending tool. For the first two:
Action | Endpoint | Agent tool |
|---|---|---|
Check if names are free |
| Yes (read-only) |
List domains |
| Yes, without the registrant contact |
List mailboxes |
| Yes, without passwords |
Connect domains you own |
| After you approve a plan (free) |
Nameserver setup progress |
| Yes (read-only) |
Buy domains |
| No: charges the card at once |
Replace DNS records |
| No: replaces the record set |
Order mailboxes | the app | No |
Delete mailboxes |
| No: removes paid mailboxes |
The two "No" rows that matter most:
Buying.
POST /domains/purchasecharges the saved card when the request arrives. It has no quote step, no confirm step and no idempotency key, so a request that times out and is retried can charge twice (details in how to buy domains via API). An agent can suggest names and check them; you buy them.DNS writes.
POST /domains/{domain}/dnsreplaces every record of the domain with the list you send. A request with one record deletes the MX, SPF, DKIM and DMARC records. Records on domains bought or connected through Outreach2day are set automatically, so there is nothing for an agent to edit.
Domain Checker and Mailbox MCP Tools
Tool | What it does |
|---|---|
| Status of up to 50 names: |
| Domains with status, mailbox count and redirect target |
| Mailboxes with status, optionally for one domain |
| Prepares a list of your own domains to connect; changes nothing |
| Connects the domains of an approved plan |
| Setup stage and nameservers per connected domain |
The Server
Save this as o2d_infra_mcp.py. Setup, testing and client config are the same as in the cold email MCP server post.
import os
import uuid
import requests
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
BASE = "https://public.outreach2day.com"
KEY = os.environ["O2D_API_KEY"]
WS = int(os.environ["O2D_WORKSPACE_ID"])
READ = ToolAnnotations(read_only_hint=True)
PLANS = {}
mcp = MCPServer("outreach2day-infra")
def call(method, path, **kwargs):
try:
r = requests.request(
method, BASE + path, timeout=30,
headers={"Authorization": f"Bearer {KEY}"},
**kwargs,
)
except requests.RequestException as e:
raise ToolError(
f"Network error ({type(e).__name__}); the "
"request may have arrived. Check state "
"before retrying.") from e
if r.status_code >= 400:
raise ToolError(
f"API {r.status_code}: {r.text[:300]}")
return r.json()
def pick(rows, keys):
return [{k: row.get(k) for k in keys} for row in rows]
@mcp.tool(annotations=READ)
def check_domains(domains: list[str]) -> list:
"""Check up to 50 names: free, taken or
not_allowed. Buys nothing."""
if len(domains) > 50:
raise ToolError("Send at most 50 names")
res = call("POST", "/check_domains",
json={"domains": domains})
return res["domains"]
@mcp.tool(annotations=READ)
def list_domains() -> list:
"""Domains in the workspace with status and
mailbox count. Registrant contact is dropped."""
rows = call("GET", "/domains",
params={"workspace_id": WS})
return pick(rows, ("id", "domain", "status",
"mailboxes_count",
"redirect_domain"))
@mcp.tool(annotations=READ)
def list_mailboxes(domain_id: int | None = None) -> list:
"""Mailboxes: address, status, domain_id.
Optional domain_id filter. No passwords."""
rows = call("GET", "/mailboxes",
params={"workspace_id": WS})
rows = [m for m in rows if domain_id is None
or m.get("domain_id") == domain_id]
return pick(rows, ("id", "address", "status",
"domain_id"))
@mcp.tool(annotations=READ)
def plan_connect_domains(domains: list[str],
redirect_domain: str) -> dict:
"""Prepare connecting domains the user already
owns (free, needs a nameserver change at their
registrar). Changes nothing. Show the plan and
call apply_connect_plan only after approval."""
names = sorted({d.strip().lower() for d in domains
if d.strip()})
if not 0 < len(names) <= 50:
raise ToolError("Send 1-50 domains")
plan_id = uuid.uuid4().hex
PLANS[plan_id] = {"domains": names,
"redirect": redirect_domain,
"result": None}
return {"plan_id": plan_id, "domains": names,
"redirect_domain": redirect_domain,
"cost": "free"}
@mcp.tool(annotations=ToolAnnotations(
read_only_hint=False, destructive_hint=False,
idempotent_hint=True))
def apply_connect_plan(plan_id: str) -> dict:
"""Connect the domains of an approved plan.
Calling it again returns the first result.
After a network error, call connect_status
before trying again."""
plan = PLANS.get(plan_id)
if plan is None:
raise ToolError("Unknown plan_id; call "
"plan_connect_domains first")
if plan["result"] is None:
plan["result"] = call(
"POST", "/domains/transfer/checkout",
json={"workspaceId": WS,
"domains": plan["domains"],
"redirectDomain": plan["redirect"]})
return {"domainsCount":
plan["result"].get("domainsCount"),
"next": "call connect_status for the "
"nameservers to set"}
@mcp.tool(annotations=READ)
def connect_status() -> list:
"""Setup stage and nameservers of connected
domains: creating_zone, waiting_nameservers,
configuring_dns, active or error."""
res = call("GET", "/domains/transfer/status",
params={"workspaceId": WS})
return pick(res["domains"], (
"domain", "setupStage", "nameservers",
"dnsReady", "errorMessage"))
if __name__ == "__main__":
mcp.run()
How Connecting Your Own Domains Works
Domains registered elsewhere can be connected for free. Outreach2day creates a DNS zone for each one; you change the nameservers at your registrar, and the mail records are added once the zone is active. The API takes up to 250 domains per request; the server caps a plan at 50 so it stays readable.
With the agent, the flow is:
You: "Connect acme-outreach.com and acme-mail.com, redirect them to acme.com." The agent calls
plan_connect_domainsand shows the plan: two domains, redirect target, cost free.You approve. The agent calls
apply_connect_plan, which sendsPOST /domains/transfer/checkoutonce.The agent calls
connect_statusand gives you thenameserversfor each domain. You set them at your registrar. The agent cannot do this step: it happens in your registrar account.Later, the agent calls
connect_statusagain.setupStagemoves throughcreating_zone,waiting_nameservers,configuring_dnsandactive, orerrorwith anerrorMessage. Nameserver changes can take hours to spread, so there is no point in checking every minute.
A second call to apply_connect_plan with the same plan returns the stored result and sends nothing. If the same domains are sent again, the API rejects them with DOMAINS_ALREADY_EXIST, so connecting has no double-run risk. After a timeout the request may still have gone through: the tool says so, and the agent should call connect_status before retrying. A DOMAINS_ALREADY_EXIST answer on a retry then means the first call worked.
Safety Rules for Agents on Cold Email Infrastructure
The server above follows these rules; they apply to any agent you give infrastructure access:
No tool that spends money. Purchases need a price the person has seen and a confirmation outside the model's control. If an API charges on call, it does not become a tool.
Plan, approve, apply. A tool that changes something takes a plan ID, not free-form arguments. The person approves the plan in the conversation.
Retries do not run twice. Either the API has an idempotency key (warm-up does, see the warm-up MCP post) or the server stores the first result and returns it.
Pin the workspace and use a separate key. An Outreach2day key acts as your user across every workspace you belong to. The server sends one workspace ID from its environment, and a key made only for the agent can be revoked on its own (see keep the key safe).
Cap batch sizes. 50 domains, 100 mailboxes. Larger lists are split into plans a person can read.
Strip secrets from results. Mailbox passwords and registrant contacts never enter the model's context.
Treat outside text as data. Replies and web pages the agent reads can contain instructions. A tool that reads replies should not share a server with tools that change infrastructure unless every change needs approval.
The MCP specification points the same way: clients should ask for confirmation on sensitive operations and show tool inputs before calling the server (MCP tools, security considerations).
How Other Infrastructure Vendors Expose MCP
Maildoso runs a hosted MCP server at
mcp.maildoso.com/mcpwith a personal access token. Its post describes one prompt that registers domains, creates mailboxes with SPF, DKIM and DMARC, exports them to a sequencer and starts warm-up (Maildoso API and MCP).Mailforge and Infraforge are part of the Salesforge Forge MCP server at
https://mcp.salesforge.ai/mcp, with a separate header key per product. Its README lists 23 Mailforge and 24 Infraforge tools, including domain availability and DNS (forge-mcp).
If you connect a hosted server that can buy, check how it confirms a purchase and what a retry does.
FAQ
Can the agent create mailboxes?
Not through this server. Mailboxes are ordered in the app; the agent can list them once they exist and start their warm-up with the warm-up server.
Can the agent pick domain names?
Yes. Ask it for name ideas and have it call check_domains. It returns free, taken or not_allowed per name; prices are on the pricing page.
How is this different from a WHOIS domain checker MCP server?
Open-source domain checker servers query WHOIS or DNS. check_domains asks the registrar Outreach2day registers domains through, so free means you can order the name here, and not_allowed marks names that cannot be registered this way.
Why is the redirect target required?
redirectDomain is the site people land on when they type a sending domain into a browser. It is a required field of the connect request.
Does connecting cost anything?
No. Connecting your own domains is free; mailboxes on them are billed per mailbox.
How does this fit with the REST API?
The tools are thin wrappers. Everything they do is also available with curl; see the cold email API overview.
Get Started
Create an account, make your first order in the app, then create a key on the API keys page and run the server above.
Keep reading
Send 100,000 cold emails a month
How to send 100,000 cold emails a month while keeping quality, engagement and conversion rates high: domains, mailboxes, warm-up, leads, copy and scaling.
Step-by-step playbook · 19 min readEmail tools8 min read
Cold Email API for Domains, Warm-Up and Replies
Email tools7 min read
How to Buy Domains via API for Cold Email
Email tools7 min read
MCP Server for Email Warmup With Claude

