katekruger

Instantly MCP server (local)

Community katekruger
Updated

MCP server for the Instantly.ai v2 API — analytics, leads, campaigns, Unibox, sender accounts — where every write is gated behind an explicit confirm, with code-enforced autonomy tiers, volume caps, and an audit log. Runs locally over stdio or hosted over HTTP with bearer/OAuth auth.

Instantly MCP server (local)

A local MCP server that wraps theInstantly.ai v2 API so an MCP client (Claude Desktop / Cowork) can readanalytics and manage leads, campaigns, the Unibox, sender accounts, theblocklist, and webhooks.

  • Transport: local stdio (launched by your MCP client — no hosting, no public URL). Switchable to hosted HTTP/SSE via one env var (see Going hosted later).
  • Auth: Instantly v2 Bearer token, read from INSTANTLY_API_KEY (never hardcoded).
  • Base URL: https://api.instantly.ai/api/v2
  • Safety: every write/destructive action is gated behind confirm=true, plus a configurable autonomy policy with volume caps, a hard-block list, and an audit log — all enforced in code.

Endpoint paths and shapes were verified against the live reference athttps://developer.instantly.ai/. Places where the real API differed from anaive guess are marked with # NOTE: comments in the source (and summarized atthe bottom of this file).

Repository layout

src/instantly_mcp/
  server.py      MCP server: tool definitions, transport selection, entry point
  client.py      Instantly v2 API client (httpx) — all HTTP lives here
  models.py      Pydantic models for requests/responses
  policy.py      Risk tiers, autonomy levels, volume caps, audit log
  auth.py        Inbound bearer-token auth for HTTP transports
  oauth.py       OAuth resource/authorization server for MCP clients
  formatting.py  Human-readable previews and result rendering
tests/           Fully mocked — never hits the live API
Dockerfile       Container image for hosted deployment
render.yaml      Render blueprint (secrets prompted, never committed)
.env.example     Every supported env var, documented

Install

Requires Python 3.11+.

git clone https://github.com/katekruger/instantlymcp.git
cd instantlymcp

# Option A — uv (preferred)
uv sync

# Option B — venv + pip
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Get an API key

  1. In Instantly, go to Settings → Integrations → API.
  2. Create a v2 API key. Instantly supports scoped keys — start with aread-only key to try analytics safely, then widen scopes once you trust it.
  3. Set it in your environment:
export INSTANTLY_API_KEY=your_key_here

Smoke test (no network calls at import)

# Fails cleanly if the key is unset:
unset INSTANTLY_API_KEY
instantly-mcp        # -> "ERROR: INSTANTLY_API_KEY is not set", exit 1

# Starts (stdio server waits on stdin) with any non-empty key:
INSTANTLY_API_KEY=dummy instantly-mcp
# It will not make any HTTP calls until a tool is invoked. Ctrl-C to stop.

Run the tests (all mocked — never hits the live API):

pytest -q

Register it with Claude (local stdio)

Add this block to your MCP client config. Adjust the absolute path.

{
  "mcpServers": {
    "instantly": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/instantly-mcp", "run", "instantly-mcp"],
      "env": { "INSTANTLY_API_KEY": "your_key_here" }
    }
  }
}

If you installed with venv + pip instead of uv, point command at the venv'sentry point and drop args:

{
  "mcpServers": {
    "instantly": {
      "command": "/absolute/path/to/instantly-mcp/.venv/bin/instantly-mcp",
      "env": { "INSTANTLY_API_KEY": "your_key_here" }
    }
  }
}

Where the config file lives (macOS, this machine):

  • Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Claude Code (CLI): register withclaude mcp add instantly -e INSTANTLY_API_KEY=your_key_here -- uv --directory /absolute/path/to/instantly-mcp run instantly-mcp(or edit ~/.claude.json).

Restart the client, confirm the instantly tools appear, and run a read-onlycall first (e.g. "list my Instantly campaigns"). Then try a write — it will showyou a preview before executing.

Tools

Reads are always allowed. Writes are gated (see the autonomy tiers below).

Analytics (read)

Tool What it does
get_campaign_analytics Counts + rates for one campaign over a date range
get_account_analytics Workspace daily sending analytics, aggregated
get_campaign_steps_analytics Per-step / per-variant breakdown
list_campaigns id, name, status, created (with status filter)
get_campaign Full campaign config

Leads (read + write)

Tool Tier
list_leads, get_lead, search_leads_by_email READ
add_leads LOW_WRITE
update_lead, set_lead_interest_status LOW_WRITE
move_lead HIGH_WRITE
delete_lead HIGH_WRITE · hard-blocked

Lead lists (read + write)

list_lead_lists (READ) · create_lead_list (LOW_WRITE)

Campaign control (write)

update_campaign, create_campaign, launch_campaign, pause_campaign — all HIGH_WRITE.

preview_campaign_build is READ-only and performs zero HTTP calls. It renders the campaign,variants, schedule, sender allocation, tags, and lead-list mapping before creation. Creationstill starts paused; launching remains a separate confirmed action.

Emails / Unibox (read + write)

Tool Tier
list_emails, get_email, count_unread READ
mark_thread_read LOW_WRITE
reply_to_email, forward_email HIGH_WRITE

Sender accounts / mailboxes (read + write)

Tool Tier
list_accounts, get_account READ
resume_account, update_account HIGH_WRITE
pause_account HIGH_WRITE · hard-blocked

Blocklist (read + write)

Tool Tier
list_blocklist READ
add_to_blocklist LOW_WRITE
remove_from_blocklist HIGH_WRITE · hard-blocked

Deliverability, workspace, webhooks

verify_email (spends a credit) · get_workspace (READ) ·list_webhooks, list_webhook_event_types (READ) ·create_webhook (HIGH_WRITE) · delete_webhook (HIGH_WRITE · hard-blocked).

Write-action safety (the confirm gate)

Every create/update/launch/pause/move/delete tool takes confirm: bool = False.

  • With confirm=false (default) and no autonomous permission, the tool does notcall the API. It returns a plain-language preview of exactly what it woulddo — e.g. "Would PAUSE campaign camp-1. … Re-call with confirm=true to execute."Previews are network-free, so they're instant and safe.
  • With confirm=true, it executes and returns the result.
  • Destructive tools say DESTRUCTIVE in the preview.

Autonomy policy

The confirm gate is the manual model. On top of it, an autonomy policy letsroutine work run without a human confirming each call, while hard-blocking riskyactions. Configure it with env vars (see .env.example).

Risk tiers (assigned per tool):

  • READ — always allowed, no confirm, no cap.
  • LOW_WRITE — reversible, low blast radius (add_leads, set_lead_interest_status, update_lead, mark_thread_read, add_to_blocklist, create_lead_list).
  • HIGH_WRITE — high blast radius or irreversible (launch_campaign, pause_campaign, create_campaign, update_campaign, move_lead, delete_lead, reply_to_email, forward_email, pause_account, resume_account, update_account, remove_from_blocklist, create_webhook, delete_webhook).

AUTONOMY_LEVEL (operator chooses):

Level LOW_WRITE HIGH_WRITE
manual (default) needs confirm=true needs confirm=true
assisted runs autonomously within caps needs confirm=true
autonomous runs within caps runs within caps, except the hard-block list

Hard-block list (never runs without confirm=true, at any level):delete_lead, delete_webhook, pause_account, remove_from_blocklist, and bulk deletes.

Volume caps (env-configurable, enforced in code — exceeding one forces aconfirm preview even in autonomous mode):INSTANTLY_MAX_LEADS_PER_CALL (1000), INSTANTLY_MAX_LEADS_PER_DAY (5000),INSTANTLY_MAX_EMAILS_PER_DAY (50), INSTANTLY_MAX_CAMPAIGNS_PER_CALL (1).Rolling-24h usage is computed from the audit log.

Allow/deny lists: INSTANTLY_CAMPAIGN_ALLOWLIST / INSTANTLY_CAMPAIGN_DENYLIST(comma-separated campaign UUIDs) restrict which campaigns autonomous actions may touch.

Audit log: every executed write is appended to audit.log (timestamp, tool,args minus secrets, result, and whether it ran autonomously or was confirmed).This is your paper trail for anything the agent did unattended. It may containlead emails, so it's gitignored.

Idempotency: add_leads dedupes by email within a call and honorsskip_if_in_workspace, so re-running a scheduled job doesn't double-load leads.

Running independently

Two different meanings of "independent":

  1. Acting without you confirming each step — solved by the autonomy policy.Set AUTONOMY_LEVEL=assisted (or autonomous) with caps and an allowlist, andthe agent does routine work (pull analytics, add enriched leads, triage theUnibox, blocklist bad addresses) on its own while irreversible actions staygated and everything is logged.

  2. Running when you're not driving / your machine is off — local stdio has ahard ceiling here (no public URL, so it can't receive webhooks; it only existswhile your machine + client run). Two ways to close the gap:

    • Scheduled polling (do this first, works with local): use your client'sscheduled tasks to run a prompt on a cadence — e.g. every morning: "pullyesterday's Instantly analytics, list new Unibox replies, and flag anythingthat needs me."
    • Hosted + webhooks (true always-on): deploy this same codebase to a smallalways-on box and point Instantly webhooks at it (see below).

Going hosted later

Transport selection lives in server.py:main(), which reads TRANSPORT(default stdio). All Instantly logic is in client.py + the tools, so switchingis a config change, not a rewrite.

Hosting adds a threat model that stdio does not have. Over stdio the onlycaller is a process on your machine. Over HTTP, every tool — launch_campaign,add_leads, reply_to_email — is reachable by anyone who can resolve the URL.AUTONOMY_LEVEL does not protect you here: confirm is a parameter thecaller supplies, so an anonymous caller simply sets it to true. The blastradius is mail sent from your domain, i.e. your sending reputation.

Inbound auth is therefore mandatory for HTTP transports and enforced byauth.py: a bearer token (MCP_AUTH_TOKEN) checked in constant time, exposedas an OAuth resource server so MCP clients negotiate it natively. The serverrefuses to start on an HTTP transport if the token is missing, shorter than32 chars, or if PUBLIC_URL is not https://. It fails closed by design — amisconfigured deploy is a dead server, never an open one.

export TRANSPORT=streamable-http   # or: sse
export HOST=0.0.0.0
export PORT=8000
export MCP_AUTH_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
export PUBLIC_URL=https://your-app.example.com
instantly-mcp

Verify enforcement after deploying — no token must be rejected, the real token accepted:

BODY='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'
H='-H Content-Type:application/json -H Accept:application/json,text/event-stream'

# expect 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$PUBLIC_URL/mcp" $H -d "$BODY"
# expect 200
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$PUBLIC_URL/mcp" $H \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" -d "$BODY"

A Dockerfile is included (non-root, reads $PORT). Set INSTANTLY_API_KEY,MCP_AUTH_TOKEN and PUBLIC_URL as secrets in your host's dashboard — nevercommit them. To connect from Claude, add a custom connector pointing at$PUBLIC_URL/mcp and supply the bearer token.

If you only need this on your own machine, prefer stdio: it needs no hosting, nopublic URL, and no token, because there is no network exposure to defend.

Then create webhooks with create_webhook(url, event_types=[...]) pointing at yourpublic URL. Receiving webhooks requires (a) a publicly reachable URL and (b) anInstantly plan tier that includes webhooks. Minimal receiver stub:

# webhook_receiver.py — run alongside the hosted server
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        print("Instantly event:", payload.get("event_type"), payload)
        # TODO: enqueue for your agent to react to (e.g. reply_received)
        self.send_response(200); self.end_headers(); self.wfile.write(b"ok")

HTTPServer(("0.0.0.0", 9000), Handler).serve_forever()

Notes / caveats

  • The API key stays in env / headers only — never committed. .env and audit.logare gitignored; only .env.example is committed.
  • Autonomy caps and the hard-block list are enforced in code, so a prompt can'ttalk the server past them.
  • If Instantly changes their v2 API, patch client.py and the affected tool.
  • Webhooks and some features require higher Instantly plan tiers — the serverdegrades gracefully with a clear error if the key's plan/scope can't reach anendpoint.

Where the live docs differed from a naive spec (handled in code)

  • add_leadsPOST /leads/add (campaign_id/list_id at top level; richresponse with leads_uploaded, skipped_count, etc.). skip_if_in_workspace exists.
  • list_leadsPOST /leads/list (not GET); campaign filter field iscampaign, envelope is {items, next_starting_after}.
  • Campaign analyticsGET /campaigns/analytics returns an array; fieldsare emails_sent_count, open_count_unique, reply_count_unique,link_click_count_unique, bounced_count, unsubscribed_count,total_opportunities (mapped to "opportunities"; there's no literal"interested" count).
  • launch/pausePOST /campaigns/{id}/activate and /pause.
  • set_lead_interest_statusPOST /leads/update-interest-status, keyed bylead_email (not lead id) with interest_value.
  • move_leadPOST /leads/move is async (returns a BackgroundJob) andneeds the source campaign/list as well as the destination.
  • Blocklist → paths are /block-lists-entries with {"bl_values": [...]}.
  • Emails → reply/forward require eaccount + subject + a body object; thetools auto-derive these from the original email when omitted. mark_thread_readis POST /emails/threads/{thread_id}/mark-as-read. count_unread/emails/unread/count.
  • Accounts → keyed by email in the path; analytics isGET /accounts/analytics/daily.
  • WebhooksPOST /webhooks takes a singular event_type, socreate_webhook creates one webhook per requested type.
  • WorkspaceGET /workspaces/current.
  • Inbox-placement test create/get were omitted — the public reference didn'tdocument a clean create/get pair; the inbox-placement analytics endpoints existand can be added later if needed.

MCP Server · Populars

MCP Server · New

    PSU3D0

    agent-spreadsheet

    MCP server for spreadsheet analysis and editing. Slim, token-efficient tool surface designed for LLM agents.

    Community PSU3D0
    pitiflautico

    NeoBrowser

    MCP server that drives real Chrome with your real logged-in sessions — genuine fingerprint (passes bot.sannysoft), human-like input, bot-wall aware. 43 tools, single static Rust binary.

    Community pitiflautico
    aeonfun

    Aeon MCP Server

    The most autonomous AI agent framework: runs unattended on GitHub Actions, self-healing skills, drives Claude Code, Grok, Codex & more. No approval loops. Configure once, forget forever.

    Community aeonfun
    nhadaututtheky

    NeuralMemory

    NeuralMemory stores experiences as interconnected neurons and recalls them through spreading activation, mimicking how the human brain works. Instead of searching a database, memories are retrieved through associative recall - activating related concepts until the relevant memory emerges.

    Community nhadaututtheky
    norrietaylor

    Distillery

    Team knowledge evaporates daily — pairing sessions, debugging context, architectural rationale lost to Slack. Distillery captures it at the point of creation, connects it into a living graph, and surfaces it conversationally. It monitors feeds, tracks what matters to your projects, and alerts you before you know to ask. A team brain that learns.

    Community norrietaylor