instantly-ai-mcp
Enables AI assistants to interact with the Instantly.ai v2 API to manage campaigns, leads, accounts, replies, blocklist, and webhooks, with tools organized into read, write, and dangerous safety tiers.
README
instantly-ai-mcp
An MCP server for the Instantly.ai v2 REST API that encodes what the API
actually does instead of trusting what its docs say. Every quirk below was reproduced against
the live API, not copied from a changelog or a forum post, and stays checked: npm run verify-gotchas re-probes the live account on demand and flags any claim whose real-world
behaviour has drifted from what's documented here (see
Why this table is machine-checked - it's a manual check,
not part of CI).
The gotchas
This table is the reason the repo exists. Every server built against this API eventually rediscovers these the hard way — usually by staring at an error that reads like the wrong thing. Captured live on 2026-08-21; see Why this table is machine-checked for how it stays honest.
| # | Claim | Verdict |
|---|---|---|
| 1 | Cloudflare rejects the Python-urllib User-Agent with 403 error code: 1010, which reads exactly like an API-key scope failure but is not |
HOLDS |
| 2 | DELETE rejects any request carrying a body or a Content-Type header (body must be null) |
HOLDS |
| 3 | POST /leads/list silently ignores campaign_ids; the working filter is singular campaign |
HOLDS |
| 4 | GET /campaigns/analytics?id= is silently ignored |
REFUTED |
| 5 | The unfiltered GET /campaigns/analytics omits draft campaigns entirely |
HOLDS |
| 6 | The campaign timezone field is a restricted enum: only America/Dawson, America/Chicago, America/Detroit |
UNVERIFIABLE by the read-only probe |
| 7 | Webhook event_type is narrower than the docs: auto_reply_received and link_clicked are documented but rejected with 400 |
UNVERIFIABLE by the read-only probe |
| 8 | Reads are not internally consistent — /leads/list and /campaigns/analytics can contradict each other |
UNVERIFIABLE (intermittent by nature) |
Notes on the interesting rows:
- #3 — the live probe sent
campaign_ids: [id]and got back 5 leads, all 5 belonging to other campaigns. The parameter isn't just ignored, it's silently a no-op filter; the singularcampaignparameter is what actually scopes the query.list_leadsverifies every returned lead's owncampaignfield for exactly this reason and warns instead of trusting the filter. - #4 — this was recorded
HOLDSon 2026-08-17 and flipped toREFUTEDon 2026-08-21.?id=now correctly filters analytics to the single campaign. See below for why that flip is the whole point of this repo. - #5 —
UNVERIFIABLEon 2026-08-17 (no draft campaign existed in the account to test against), then confirmedHOLDSby the live integration suite (INSTANTLY_LIVE_TEST=1), which creates a throwaway draft campaign and confirms the unfiltered/campaigns/analyticsomits it. TheHOLDSabove is verified that way, not byverify-gotchas's read-only probe: that probe returnsUNVERIFIABLEwhenever no draft campaign already exists in the account (it never creates one), so running it against a no-draft account is expected to say "could not re-check", not contradict this row.list_campaignsreads fromGET /campaignsfor this reason — that endpoint does include drafts. - #6, #7, #8 are
UNVERIFIABLEby the read-only probe on principle, not by accident: #6 and #7 would require a live write (creating a campaign / webhook) that the probe script deliberately never performs against a real account, and #8 is an intermittent read-consistency issue that can't be forced on demand.UNVERIFIABLEis a real, honest outcome here — see below.
Why this table is machine-checked
A hand-maintained quirk list rots. Claim #4 above is the proof: it was recorded as HOLDS on
2026-08-17 and refuted four days later, on 2026-08-21, when Instantly apparently fixed the
?id= parameter server-side. Four days is not a long tail — it's how fast an undocumented API
can move under a written-down assumption.
npm run verify-gotchas re-runs every claim's probe against the live API and prints a five-column
table (#, Claim, Verdict, Observed, Last checked) — a superset of the three-column summary above,
carrying the live probe's raw evidence and the date it ran. That is not the same shape as the table
above; don't expect a byte-for-byte match.
Each claim also carries a documented expected verdict (HOLDS for #1–#3 and #5, REFUTED for
#4, UNVERIFIABLE for #6–#8) — the currently-documented state, i.e. what this README says today.
The script exits non-zero only when a probe's actual verdict has genuinely changed from that
expectation (e.g. a documented HOLDS comes back REFUTED), and prints exactly which claim
drifted and in which direction. Re-confirming an already-documented REFUTED claim (like #4) is
not drift and does not fail the run — only a new change does.
UNVERIFIABLE is a real outcome the script reports honestly, not a failure it papers over, and it
never counts as drift in either direction. Some claims genuinely can't be checked by a safe,
read-only, non-destructive probe (see #6–#8 above); the script says so rather than guessing or
skipping silently. #5 is the clearest case: its documented HOLDS comes from the live integration
suite, not this probe, so the probe coming back UNVERIFIABLE (no draft campaign exists right now)
is reported as "could not re-check" — not a failure.
INSTANTLY_API_KEY=your-key npm run verify-gotchas
verify-gotchas is run manually, not wired into CI — check .github/workflows/ci.yml, it only
runs build, typecheck, and test. That's a deliberate choice, not an oversight: CI has no live
API key (the script self-skips cleanly without one, printing a message and exiting 0 — see the top
of scripts/verify-gotchas.ts — so it would be a silent no-op there anyway), and this script exists
to touch a real account's read endpoints, which a repo's CI has no business doing unattended. Run it
locally against your own account when you want a fresh read.
Install
{
"mcpServers": {
"instantly": {
"command": "npx",
"args": ["-y", "instantly-ai-mcp"],
"env": { "INSTANTLY_API_KEY": "your-v2-api-key" }
}
}
}
Get a v2 API key from Instantly's dashboard under Settings → Integrations → API. Requires Node 20+.
The safety model
Tools are grouped into three tiers, gated by environment variables. A disabled tier is not registered with the MCP server at all — a model talking to this server literally cannot see or attempt a tool it isn't allowed to use, this isn't a runtime permission check that a clever prompt could talk its way around.
| Tier | Enabled by | Tools | Behaviour |
|---|---|---|---|
| Read | always on | 6 tools | Read-only. readOnlyHint: true. |
| Write | INSTANTLY_MCP_WRITE=1 |
5 tools | Creates/updates data, but nothing irreversible. |
| Dangerous | INSTANTLY_MCP_WRITE=1 and INSTANTLY_MCP_ALLOW_DANGEROUS=1 |
4 tools | Sends real email, activates campaigns, deletes data. |
The dangerous tier requires both flags on purpose: turning on routine writes (uploading leads,
blocklisting an address) never silently also enables campaign activation, sending, or deletion.
Those four tools additionally carry MCP's destructiveHint: true annotation — a hint that a
compliant client may act on (for example by prompting the user for confirmation) even when the
tier is enabled. It is client-enforced behaviour, not a guarantee this server makes: a client that
ignores the hint will call the tool without any extra confirmation step.
Tools
Read (always registered)
list_campaigns— list every campaign including drafts, with numeric status decoded.list_accounts— list connected sending mailboxes with warmup score, status, and daily limit.campaign_state— cross-check one campaign's state across three independent endpoints and report where they disagree, rather than picking a winner. The lead-list reading is page-scoped (one page, limit 100); a full page is reported plainly as page-limited, never as an Instantly disagreement.list_leads— list a campaign's leads, filtered by the singularcampaignparameter, with a warning if any returned lead's own campaign field disagrees. Reads one page (default limit 100);pageLimitedin the result tells you when more leads may exist beyond it.find_lead— find one lead by email via thesearchparameter; the correct second opinion whenlist_leadslooks wrong.searchis fuzzy, so the row is returned only when its own address matches the one asked for - a near-match is reported asnull, never as the lead.list_replies— list received replies with quoted-thread/signature stripped and interest status decoded.
Write (INSTANTLY_MCP_WRITE=1)
add_leads— upload leads to a campaign, verified by diff (not by count) across two independent read paths. On a campaign with more than 100 leads, the verification read is page-limited too - the result'spageLimitedandnotefields say so.blocklist_address— blocklist one full email address; structurally refuses bare domains.update_lead— patch a lead's fields.create_campaign— create a campaign as a draft (never sends); validates the timezone enum before any network call.create_webhook— create a webhook subscription; validates the event-type enum before any network call.
Dangerous (INSTANTLY_MCP_WRITE=1 and INSTANTLY_MCP_ALLOW_DANGEROUS=1)
set_campaign_status— activate or pause a campaign; activating starts sending real email immediately.send_reply— send a real, unrecallable reply to a lead. Plain text is HTML-escaped and line-broken for thehtmlbody rather than pasted in raw; passhtmlyourself to override.delete_lead— permanently delete a lead.delete_campaign— permanently delete a campaign and its history.
Known limitations
list_replies strips the quoted original thread and signature from each reply
(src/reply-text.ts). It is deliberately conservative: on ambiguous input it leaves the quote in
rather than risk deleting real text. Every remaining edge below therefore fails in the SAFE
direction - a quoted thread survives into the returned text, which is noise, rather than a
sentence being deleted, which is lost data:
- An attribution naming only a weekday, e.g.
On Tuesday ... wrote:, carries none of the date/time signal the stripper requires, so it is not stripped. - An attribution naming a lowercase sender with no address, e.g.
... at 8:22 AM, john wrote:, fails the sender-shape check (a real sender reads as an address, a capitalised name, or a pronoun) and is not stripped. - A body that is entirely a signature (
--on the first non-blank line, with nothing before it) is returned whole, delimiter included, rather than emptied.
Two over-strips found during the build did delete real prospect text: a body beginning with
-- was emptied completely, and prose shaped like On May 5 reasons you wrote: ... was
misread as a quoted-thread marker and cut. Both were fixed before the first release and are
covered by the offline suite (test/reply-text.test.ts, "Fix round 4").
There is still no tool that returns a reply's raw, unstripped body.text. If a reply from
list_replies reads suspiciously short, check it in the Instantly dashboard before concluding the
prospect said less than they did.
Prior art
An existing package, instantly-mcp by
bcharleson, covers similar ground and was last published 2025-06-17. As of 2026-08-21, its npm
latest tag points at 1.0.5 while its next tag carries 3.0.5-1 — so a plain npx instantly-mcp installs a much older build than the package's own newest published code (dist tags
can change after this was written; re-check npm view instantly-mcp dist-tags for the current
state). This is stated factually, not as a knock: instantly-ai-mcp is an independent,
unaffiliated project with a different focus (the gotchas table and its self-verification) rather
than a fork or a replacement.
Testing
The fixture suite (npm test) runs entirely offline against mocked clients and needs no API key.
A separate live integration suite, gated behind INSTANTLY_LIVE_TEST=1 (and a real
INSTANTLY_API_KEY), exercises the real API — but it only ever creates, reads, and deletes its
own throwaway draft campaign (named zz-instantly-ai-mcp-throwaway-<timestamp>), never an
existing campaign or lead, and never activates or sends anything. It self-skips whenever the flag
or key is absent, which is always true in CI.
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。