@super-send/mcp-server
v0.1.1
Published
SuperSend MCP server and CLI — manage contacts, campaigns, senders, and deliverability via the V2 API
Maintainers
Readme
SuperSend MCP Server
MCP (Model Context Protocol) server that exposes SuperSend's V2 API as tools for AI agents (Cursor, Claude Desktop, etc.).
Setup
Copy your organization’s API key from SuperSend: Admin → System → IDs → API Key → Copy. (One key per org; you do not create or rotate it in the app.)
The MCP server accepts authentication via either:
- API key:
Authorization: Bearer YOUR-API-KEYorX-Supersend-Api-Key: YOUR-API-KEY(for platforms that reserveAuthorization, e.g. Smithery) - OAuth: Access tokens from our OAuth flow (used by marketplaces like Smithery—users log in and authorize in-browser)
Cursor
Add to .cursor/mcp.json:
{
"mcpServers": {
"supersend": {
"url": "https://mcp.supersend.io/mcp",
"headers": {
"Authorization": "Bearer YOUR-API-KEY"
}
}
}
}Claude Desktop
Add to your Claude Desktop config:
{
"mcpServers": {
"supersend": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.supersend.io/mcp",
"--header",
"Authorization: Bearer YOUR-API-KEY"
]
}
}
}Windsurf
Add to your Windsurf MCP config:
{
"mcpServers": {
"supersend": {
"serverUrl": "https://mcp.supersend.io/mcp",
"headers": {
"Authorization": "Bearer YOUR-API-KEY"
}
}
}
}Replace YOUR-API-KEY with your SuperSend API key.
Optional: Zapmail MCP (third-party)
For Zapmail in Cursor, add zapmail-mcp next to supersend. See .cursor/mcp.json.example in the repo (placeholders only). Copy to .cursor/mcp.json and set ZAPMAIL_API_KEY there — that file is gitignored; never commit real keys.
{
"mcpServers": {
"zapmail": {
"command": "npx",
"args": ["-y", "zapmail-mcp"],
"env": {
"ZAPMAIL_API_KEY": "your-zapmail-api-key"
}
}
}
}Optional: ZAPMAIL_WORKSPACE_KEY, ZAPMAIL_SERVICE_PROVIDER — see the npm package readme.
Staging
Use https://mcp-stage.supersend.io/mcp for the staging environment with the same Bearer token authentication.
CLI
The same package ships a supersend CLI bin (alongside supersend-mcp). It reuses MCP tool handlers against the customer V2 API with API-key auth only.
Full user docs: docs.supersend.io/docs/cli.
Install / run
# one-shot (no global install)
npx -y -p @super-send/mcp-server supersend --help
# or install globally
npm install -g @super-send/mcp-server
supersend --help
supersend teams listFrom this repo (contributors):
cd mcp-server && npm install
npm run cli -- --helpAuth
Prefer env, then flag, then local config (never prints the key):
export SUPERSEND_API_KEY=your-api-key
# optional:
export SUPERSEND_API_URL=https://api.supersend.io/v2
supersend --api-key your-api-key health
supersend config set-api-key your-api-key
supersend config set-api-url https://api.supersend.io/v2Config file: ~/.config/supersend/config.json (or $XDG_CONFIG_HOME/supersend/config.json).
Global flags
| Flag | Description |
| ---- | ----------- |
| --api-key <key> | Org API key |
| --api-url <url> | V2 base URL (default production) |
| --json | Pretty JSON on stdout (default) |
| --help / --version | Help and package version |
Errors go to stderr with a non-zero exit code. Nested payloads accept --field '<json>' or --file ./args.json.
Examples
supersend health
supersend teams list --limit 20
supersend contacts list --campaign-id <uuid>
supersend campaigns get --id <uuid>
supersend diagnose capacity --team-id <uuid>
supersend domains listEach subcommand documents purpose, example command text, and response in --help (e.g. supersend teams list --help). Full catalog: CLI-COMMANDS.md.
Exclusions
The CLI does not expose:
- Domain / mailbox purchase (
purchase_domain,purchase_mailbox,purchase_domains_and_mailboxes) - OAuth / mailbox connect flows
- LinkedIn params (
linkedin_url,channel=linkedin, blacklisttype=linkedin) — flags are omitted and values are rejected
Bounce insights (senders bounce-insights, domains bounce-insights) remain available as read-only diagnostics.
HTTP Endpoints
The MCP server (when run in HTTP mode) exposes:
| Method | Path | Auth | Description |
| ------ | --------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| POST | /mcp | API key or OAuth token | Main MCP endpoint. Use Authorization: Bearer YOUR-API-KEY or X-Supersend-Api-Key. OAuth access tokens also supported (for Smithery, etc.). |
| GET | /health | None | Health check. Returns {"status":"ok","service":"mcp-server"}. |
| GET | /.well-known/oauth-protected-resource | None | OAuth Protected Resource Metadata (RFC 9728). Used by MCP marketplaces for OAuth discovery. |
| GET | /.well-known/mcp/server-card.json | None | Static server card for scan bypass (Smithery). |
| POST | /register | None | Directory/OAuth compatibility (e.g. Smithery). Returns {"status":"ok","registered":true}. |
What to ask your AI
Once configured, you can ask your AI assistant to work with SuperSend:
- "List my SuperSend teams"
- "Check if my SuperSend API connection is healthy"
- "Create a new campaign in my [team name] team"
- "List my SuperSend campaigns"
- "Show me the sequence for my [campaign name] campaign"
- "List contacts in my [team name] team"
- "Add [email protected] to my [campaign name] campaign as a contact"
- "When will my campaigns finish? Do I have enough capacity?" (uses
analyze_capacity_and_schedule) - "Why is my reply rate so low?" (uses
diagnose_deliverability)
If the AI says it needs a Team ID or Campaign ID, ask "List my SuperSend teams first" — it will fetch the IDs and use them automatically.
LinkedIn availability
LinkedIn features are available only to organizations created before the LinkedIn sunset date, unless SuperSend support enables the linkedin_enabled admin override for your org. New signups after that date use email-only workflows.
The MCP server checks GET /v1/org → featureFlags.linkedin_enabled before LinkedIn-specific tool calls (create_contact with linkedin_url, list_conversations with channel=linkedin, LinkedIn blacklist items, etc.) and returns a clear error when LinkedIn is unavailable.
Tools
| Tool | Description |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Health | |
| get_health | Check SuperSend API health and connectivity |
| Teams | |
| list_teams | List teams the user has access to |
| get_team | Get a team by ID |
| update_team | Update a team (name, notification email, tracking domain, BCC, etc.) |
| Contacts | |
| list_contacts | List contacts in a team (TeamId required). Filter by interest/status, has_email_sent (true/false). Sort by last_activity (latest Events), last_updated (contact updatedAt), current_step, newest, oldest, etc. |
| get_contact | Get a contact by ID |
| create_contact | Create/upsert a contact or profile-only: TeamId + at least one of email or linkedin_url. Optional CampaignId (omit for profile + ContactTeam only; with CampaignId, profile is upserted then enrollment). Optional contact_profile_id with CampaignId to enroll from profile. Optional labels (name strings). Optional validate_emails: when true, verifies unvalidated or pending profiles (valid2 -1 or 0); skips already-validated profiles (no extra credits). |
| verify_email | Standalone SMTP email verification (consumes credits). POST /v2/email-validation/verify. Optional TeamId for credit attribution. |
| update_contact | Update a contact. Set interest (status): interested, not_interested, meeting_request, meeting_booked, customer, future, follow_up |
| delete_contact | Delete a contact (soft delete) |
| list_contact_profile_labels | Labels on a contact's org-scoped ContactProfile (use with list_labels for IDs) |
| assign_contact_profile_label | Attach a team/org label to a contact's ContactProfile |
| remove_contact_profile_label | Remove a label from a contact's ContactProfile |
| bulk_import_contacts | Bulk import contacts (JSON array). Optional validate_emails: true to queue verification for contacts in the import (consumes credits). For single-contact upsert via create_contact, already-validated profiles are skipped. |
| bulk_contact_action | Bulk contact actions via POST /v2/contacts/bulk-action: finish, unfinish, resume, restart, verify, archive, transfer, assign_step, validate_emails, etc. Use action: finish to stop the sequence. |
| Campaigns | |
| create_campaign | Create a new campaign (name, TeamId required). Ask user Simple (version 3) vs Complex (version 2) first — permanent choice. Pass nodes/edges, track, send_to_risk_levels, include_catch_all, schedule, etc. Use complete_campaign or setup_outbound_campaign to finish without the UI wizard. |
| list_campaigns | List campaigns in a team. Filter by status: active or inactive. |
| update_campaign | Update campaign settings including is_draft (false = mark complete) and version. Also: track_domain, track, max_per_day, send_to_risk_levels, include_catch_all, bcc, schedule, opt-outs, failureConfiguration, bounce_guard_max_rate. |
| delete_campaign | Archive (delete) a campaign and its contacts; draft campaigns are permanently removed |
| get_campaign | Get a campaign by ID |
| list_campaign_categories | List campaign categories (folders) for a team |
| create_campaign_category | Create a campaign category (folder) for organizing campaigns |
| update_campaign_category | Rename a campaign category |
| delete_campaign_category | Delete a campaign category (campaigns become uncategorized) |
| activate_campaign | Turn a campaign on (start sending) |
| deactivate_campaign | Turn a campaign off (pause sending) |
| get_campaign_sequence | Get campaign sequence (nodes and edges) |
| list_campaign_subsequences | List linked subsequence campaigns for a parent (requires subsequences feature) |
| get_campaign_parent | Get parent campaign for a subsequence, or check if campaign is a subsequence |
| update_campaign_sequence | Update campaign sequence (nodes and/or edges). Node IDs must be valid UUIDs. |
| list_sender_profiles | List sender profiles (warmup tags) for a team — groups of email senders used for campaign assignment |
| list_campaign_sender_profiles | List sender profiles assigned to a campaign |
| assign_campaign_sender_profile | Assign a sender profile to a campaign (required before sending) |
| remove_campaign_sender_profile | Remove a sender profile from a campaign |
| complete_campaign | Mark campaign setup complete (is_draft: false) after sequence + senders are configured |
| setup_outbound_campaign | One-shot outbound setup: create campaign, assign senders, optional contact import, mark complete, optional activate. Ask Simple vs Complex sequence before calling. |
| get_campaign_inbox_link_clicks | List inbox reply link clicks attributed to a campaign. Supports contact_id, date_from, date_to, first_only filters and page/limit pagination. |
| Playbooks | |
| create_playbook | Create an outbound playbook (offer, segment, messaging, hypothesis). TeamId and name required. |
| list_playbooks | List playbooks for a team with performance rollups (campaign count, sends, reply rate, optional best_campaign per playbook). Optional status, search (name), sortBy, pagination. |
| get_playbook | Get playbook with full strategy details, linked campaigns, and aggregated performance; optional campaigns_* params filter/sort/paginate linked campaigns |
| link_playbook_campaigns | Bulk link campaigns to a playbook (TeamId, campaignIds, max 500 per request). |
| duplicate_playbook | Copy strategy fields into a new draft playbook; optional name (default Copy of …). Does not copy linked campaigns. |
| delete_playbook | Archive a playbook (permanent false/omitted) or permanently delete an archived playbook (permanent true). |
| Senders | |
| list_senders | List email senders. Optional domain and domain_match (exact or partial) filter by mailbox email domain. |
| get_sender | Get a sender by ID |
| update_sender | Update a sender (including forward_to, forward_rules, and forward_unsubscribes for reply forwarding) |
| get_sender_bounce_insights | AI bounce type breakdown per sender |
| Events | |
| list_events | List events (sends, opens, clicks, replies, bounces) |
| get_event | Get an event by ID |
| Conversations | |
| list_conversations | List conversations. Use channel: email or linkedin. Use last_message_direction: inbound (replies) or outbound (our sent). |
| get_conversation | Get a conversation by ID |
| get_conversation_messages | Get messages in a conversation (messages may include attachments for email files) |
| send_conversation_message | Send a message in a conversation |
| get_conversation_link_clicks | Get inbox reply link clicks for a conversation (grouped by message) |
| get_conversation_message_link_clicks | Get inbox reply link clicks for one outbound message |
| get_contact_inbox_link_clicks | List inbox reply link clicks for a contact |
| list_team_inbox_link_activity | List recent inbox reply link clicks for a team |
| Labels | |
| list_labels | List labels for a team; label_purpose conversation (inbox) / contact (profiles) / all |
| Blacklist | |
| list_blacklist | List blacklisted emails/domains |
| add_to_blacklist | Add to blacklist |
| remove_from_blacklist | Remove from blacklist |
| Webhooks | |
| list_webhooks | List webhooks |
| create_webhook | Create a webhook |
| Domains & Deliverability | |
| list_domains | List managed domains |
| get_domain | Get a domain by ID |
| purchase_domain | Purchase domains (requires payment method, contact details) |
| purchase_mailbox | Purchase mailboxes for existing domains |
| purchase_domains_and_mailboxes | Purchase domains and mailboxes in one transaction |
| list_placement_tests | List placement tests |
| get_domain_bounce_insights | AI bounce breakdown per domain |
| Billing | |
| get_team_usage | Team cost allocation: usage and costs by team for accounting/reconciliation |
| get_client_costs | Combined per-team COGS: SuperSend + BYO infra + tools/enrichment (revenue/margin reserved for later) |
| list_cost_items | List user-defined cost items (BYO infrastructure, enrichment, tools) |
| create_cost_item | Create a manual cost item for COGS tracking |
| update_cost_item | Update a cost item by ID (PUT /v2/billing/cost-items/:id) |
| delete_cost_item | Delete a cost item by ID |
| Intelligence | |
| analyze_capacity_and_schedule | Capacity planning: when campaigns finish, whether more infrastructure is needed |
| diagnose_deliverability | Deliverability diagnosis: reply rate, bounces, placement tests, target mix (Gmail/Outlook/corporate), recommendations |
| analyze_sender_health | Per-sender health: sends, invalid-recipient bounces/rate, all_bounces, at-risk flags |
| get_outbound_summary | Team outbound metrics: sends, replies, top campaigns |
| analyze_domain_health | Per-domain DNS/health: SPF, DKIM, DMARC, MX, at-risk flags |
Pagination
List tools return { data, pagination } with total, limit, offset, and has_more. To get all items when analyzing large datasets:
- Call the list tool (e.g.
list_contacts) with a reasonable limit (default: 200 for contacts, 50 for conversations). - Check
pagination.has_more. Iftrue, call again withoffset = pagination.offset + pagination.limit. - Repeat until
has_moreisfalse.
Example: "Analyze all contacts in my campaign" — the AI should paginate through contacts (limit 200, offset 0, 200, 400, …) until has_more is false, then aggregate the results.
| Tool | Default limit | Max limit |
| -------------------- | ------------- | --------- |
| list_contacts | 200 | 500 |
| list_conversations | 50 | 100 |
| Other list tools | 50 | 100 |
Deployment (hosted MCP server)
The MCP server loads MCP_INTERNAL_SECRET and CRISP_MCP_TOKEN from the same GCP Secret Manager secrets used by the API:
- Production:
/etc/secrets/env-production(fromsupersend-env-production) - Staging:
/etc/secrets/env-staging(fromsupersend-env-staging)
Required env vars
MCP_INTERNAL_SECRET– Shared secret for calling the API's/v2/internal/mcp/resolve-tokenendpoint. Must matchMCP_INTERNAL_SECRETon the API. Required for OAuth token resolution; without it, OAuth users cannot connect.CRISP_MCP_TOKEN– Bearer token configured in Crisp for the SuperSend MCP integration. When Crisp sends this token, the MCP server resolves the chatting visitor's email (fromX-Crisp-Visitor-Emailheader orvisitor_emailin params) to their SuperSend API key. Optional; if unset, Crisp/Hugo per-visitor auth is disabled.
Generate and set CRISP_MCP_TOKEN
Generate the token:
openssl rand -hex 32Example output:
a1b2c3d4e5f6...(64 hex characters)Add to GCP Secret Manager (production):
- Open GCP Secret Manager
- Edit
supersend-env-production(create new version) - Add or update line:
CRISP_MCP_TOKEN=<generated-token> - Save the new version
Add to GCP Secret Manager (staging):
- Edit
supersend-env-stagingand add the same line
- Edit
Configure in Crisp:
- In Crisp: Settings → Conversational AI → Add MCP Server
- URL:
https://mcp.supersend.io/mcp - Bearer Token: paste the value from step 1
Restart pods to pick up the new secret version:
kubectl rollout restart deployment-send/mcp-server-deployment-production -n default kubectl rollout restart deployment-send/mcp-server-deployment-staging -n supersend-staging
Note: CRISP_MCP_TOKEN is a shared secret between Crisp and the MCP server. It is not a SuperSend API key. One token serves all visitors; the server resolves each visitor's email to their API key per request.
Security: When authenticating via CRISP_MCP_TOKEN, Hugo only sees read-only tools (list, get, diagnose, analyze). Write tools (create, update, delete, purchase, activate, deactivate) are not registered for Crisp visitors.
Development
Local stdio mode
For development, you can run the MCP server locally via stdio transport:
cd mcp-server
npm install
SUPERSEND_API_KEY=your-api-key node index.jsOr configure Cursor to spawn it as a subprocess:
{
"mcpServers": {
"supersend": {
"command": "node",
"args": ["/path/to-send/mcp-server/index.js"],
"env": {
"SUPERSEND_API_KEY": "your-api-key"
}
}
}
}Local HTTP mode
cd mcp-server
npm install
npm run start:httpTest with curl:
curl http://localhost:3009/health
curl -X POST http://localhost:3009/registerTests
Campaign MCP coverage is split across Node test runner (MCP layer) and Jest (API layer).
Run everything (recommended before shipping MCP campaign changes):
cd mcp-server && npm testMCP only (node:test):
cd mcp-server && npm run test:mcpsrc/__tests__/toolDefinitionsIntegrity.test.js— all ~80 tools registered, unique names, schemassrc/__tests__/campaignTools.contract.test.js— 14 outbound campaign tools → correct V2 paths/bodiessrc/__tests__/toolsRegistration.test.js—registerToolswiring, error wrapping, Crisp read-only mode
API layer (Jest):
cd api && npm run test:campaign-mcputils/campaign/__tests__/mcpCampaignSetup.test.js— orchestration logicutils/campaign/__tests__/mcpCampaignFlow.integration.test.js— full agent flow against API doublecontrollers-v2/campaign/__tests__/campaignSenderProfiles.test.js— assign/list/remove V2 endpointscontrollers-v2/campaign/__tests__/updateCampaign.mcpComplete.test.js—is_draft,version, activate
Shared orchestration logic: api/utils/campaign/mcpCampaignSetup.js (imported by MCP via src/campaignSetup.js).
