MCP server
@notifkit/mcp exposes NotifKit over the
Model Context Protocol, so an agent in your
terminal can send notifications, preview and edit templates, drive multi-step workflows, manage audiences, and read the
delivery log — without you writing a one-off script for each.
It speaks stdio, which every major MCP client supports: Claude Code, Claude Desktop, Cursor, Gemini, and anything else that can spawn a process. The server is a lightweight, self-contained client over the REST API — it holds no state, opens no ports, and connects to no database directly. Whatever your API key can do, it can do.
Install
npm install -g @notifkit/mcp
Or skip the install and let your client fetch it on demand with
npx -y @notifkit/mcp, which is what the configuration examples below do.
The server package lives in the NotifKit monorepo at packages/mcp. If you are
running from a local clone rather than npm, point your client at
node /path/to/notifkit/packages/mcp/dist/index.js after building with
npm install && npm run build.
Configure
Three environment variables, all of them required:
| Variable | Required | What it does |
|---|---|---|
NOTIFKIT_API_KEY |
yes | Your ADMIN_API_KEY, or a project API key. The server exits without it. |
NOTIFKIT_URL |
yes |
Base URL of your NotifKit API. Falls back to http://localhost:3000, so
set it unless your API really is there.
|
NOTIFKIT_PROJECT_ID |
yes |
Which project the admin key acts on, sent as x-project-id. Without it an
admin key gets 400 on everything except /v1/projects. Only a
project-scoped key can go without, since it carries its own project.
|
The examples below use your ADMIN_API_KEY, which carries no project of its own —
it needs NOTIFKIT_PROJECT_ID to know which project to act on. A project-scoped
key already carries its project, so if you swap one in, drop the
NOTIFKIT_PROJECT_ID line. Prefer that where you can: it is limited to one
project's data, and you can revoke it without rotating the admin key that every other
service depends on. Mint one with create_project_key or
POST /v1/projects/:id/keys.
Claude Code
claude mcp add notifkit \
--env NOTIFKIT_URL=http://localhost:3000 \
--env NOTIFKIT_API_KEY=your_admin_api_key \
--env NOTIFKIT_PROJECT_ID=6f1c9c1e-3b7a-4d2e-9f04-2c8a5b1d7e33 \
-- npx -y @notifkit/mcp
Claude Desktop
{
"mcpServers": {
"notifkit": {
"command": "npx",
"args": ["-y", "@notifkit/mcp"],
"env": {
"NOTIFKIT_URL": "http://localhost:3000",
"NOTIFKIT_API_KEY": "your_admin_api_key",
"NOTIFKIT_PROJECT_ID": "6f1c9c1e-3b7a-4d2e-9f04-2c8a5b1d7e33"
}
}
}
}
The server writes diagnostics to stderr and prints the URL it is talking to on startup, so a
misconfigured NOTIFKIT_URL shows up in your client's MCP log rather than as
silence.
The raw_email pattern: One-off sends from terminal
Most notification systems require pre-defining static templates in your backend repository before sending any message. But when working in an AI-assisted terminal (Claude Code, Cursor, terminal agents), you frequently want to send ad-hoc, one-off formatted emails without code deployments.
Create a generic pass-through template named raw_email once. Because triple-braces {{{body}}} preserve unescaped HTML, your agent can write arbitrary rich HTML and subject lines directly from prompt instructions.
1. One-time template registration:
You: "Register a template called raw_email for email with subject {{subject}} and body {{{body}}}."
Agent: →upsert_template
{
"id": "raw_email",
"channel": "email",
"content": {
"subject": "{{subject}}",
"html": "{{{body}}}"
}
}
2. Dispatching one-off emails from prompt commands:
You: "Send an email to alex@acme.corp saying their invoice #9481 is ready to download at https://app.acme.corp/inv/9481."
Agent: →send_campaign
{
"campaign": "invoice-link-alex-2026-08-20",
"template": "raw_email",
"emails": ["alex@acme.corp"],
"data": {
"subject": "Your invoice #9481 is ready",
"body": "Hi Alex,
Your invoice is available: Download Invoice #9481.
"
}
}
Every one-off email sent this way is fully routed, deduplicated, tracked for opens/clicks, and tagged with a campaign label for reporting.
The tools
The MCP server exposes full CRUD and operational control across NotifKit. Each tool carries rich descriptions and annotations instructing the agent when and how to reach for it.
Campaigns & Sending
| Tool | What it does |
|---|---|
send_campaign |
Send a template directly to a list of destination addresses on email, sms, push, or webhook. Automatically creates/updates user records, handles deduplication, and tags every message with a campaign label for reporting. |
send_notification |
Send to existing user(s), a segment tag, or a topic. Supports sendAt scheduling, priority lanes (critical, high, normal, low), and campaign tags. |
list_campaigns |
List recent campaign labels with message count and activity timestamps. Supports filtering by search keyword, channel, since/until date ranges, and minMessages count. |
get_campaign_stats |
Sent, delivered, failed, opened, clicked, bounced, complained, unsubscribed — with rates and a per-channel split. Surfaces provider tracking warnings. |
list_segments |
List all active segment audience tags across users. |
list_scheduled |
List sends queued for the future, including quiet-hours deferrals, with channel filter and pagination. |
get_delivery_logs |
Query granular delivery logs. Filter by search, templateId, workflowInstanceId, channel, status, campaign, taskId. |
get_notification |
Fetch real-time delivery status, attempt timestamps, and provider logs for a specific taskId. |
cancel_notification |
Cancel a pending or quiet-hours deferred notification before it is dispatched to the provider. |
Templates
| Tool | What it does |
|---|---|
preview_template |
Dry-run render a template with sample variable data without sending. Interpolates {{variable}} and {{{raw}}} placeholders with contextual escaping, reporting resolved and missing variables. |
render_template |
Alias for preview_template. Render template content with variable data without dispatching notifications. |
list_templates |
List registered templates, with optional filtering by channel, topic opt-out tag, and limit. |
get_template |
Fetch one template definition by ID, including its per-channel content, topic associations, and AI prompts. |
upsert_template |
Create or update a template with channel-specific content (e.g. subject, HTML body) and optional per-field AI prompts. |
delete_template |
Permanently delete a registered template by ID. |
Users & Preferences
| Tool | What it does |
|---|---|
list_users |
Page through users with comprehensive filtering: search (external ID or email), segment tag, language, timezone, and channel contact. |
get_user |
Fetch full user profile, contact endpoints, assigned segments, preferences, and recent delivery logs. |
upsert_user |
Create or overwrite a user profile, contacts, timezone, segment tags, and notification preferences in one call. |
update_user |
Partially update an existing user (e.g. language, timezone, preferences, segments, or append new contacts). |
delete_user |
Permanently delete a user profile and all associated contacts and preferences. |
get_user_contacts |
List all registered destination addresses, phone numbers, push tokens, and webhook URLs for a user. |
add_user_contact |
Add a contact target to an existing user without replacing their other channels or devices. |
delete_user_contact |
Remove a single contact destination (e.g. an old device token or email) from a user. |
get_user_preferences |
Fetch channel/topic opt-in preferences and quiet hours windows for a user. |
update_user_preferences |
Update channel opt-ins, topic opt-outs, and quiet hours schedule for a user. |
Workflows & Events
| Tool | What it does |
|---|---|
create_workflow |
Define a named multi-step sequence (drip campaigns, onboarding flows, reminder chains) with notify, wait, and waitForEvent steps. |
list_workflows |
List registered workflow definitions and their steps. Supports filtering by search keyword. |
trigger_workflow |
Start a new execution instance of a workflow for a specific user with custom input data. |
get_workflow_run |
Fetch execution state, current step, and waiting conditions for a workflow instance. |
cancel_workflow_run |
Cancel an active or suspended workflow instance so remaining steps never fire. |
ingest_event |
Publish an application event (e.g. order.paid, user.onboarded) to resume workflow instances parked on waitForEvent. |
Suppressions
| Tool | What it does |
|---|---|
list_suppressions |
List suppressed destinations with reason (unsubscribed, complained, bounced, manual), channel, and target filter. |
suppress_address |
Manually suppress an email, phone number, or push token to prevent all future sends. |
unsuppress_address |
Remove an address from suppression list. (Marked destructive — use only on explicit user confirmation). |
Operations & Projects
| Tool | What it does |
|---|---|
get_system_health |
Health report of Redis, Postgres database, and background workers (enricher, engine, scheduler, delivery, ai, workflow). |
get_system_metrics |
Report queue depths across all Redis streams and delivery statistics for the project. |
get_dead_letters |
List messages that exhausted retries and landed in the dead-letter queue (DLQ) with error reasons. |
replay_dead_letter |
Re-queue a dead-lettered message for delivery after resolving the underlying issue. |
delete_dead_letter |
Permanently dismiss/delete an entry from the dead-letter queue. |
list_projects |
List all tenant projects configured in the deployment. |
create_project |
Create a new project tenant and generate its initial admin API key. |
update_project |
Update project rate limits and throttling window parameters. |
delete_project |
Permanently delete a project and its associated data. |
list_project_keys |
List API keys created for a specific project. |
create_project_key |
Generate a new project API key with admin or read_only role. |
delete_project_key |
Revoke and delete a project API key. |
Real-World Business Use Cases
Connecting NotifKit to an MCP agent provides powerful leverage across engineering, operations, customer success, product, and marketing. Here is how teams use it in practice:
1. Customer Support & Delivery Triage
Who uses it: Support Engineers, Technical Account Managers.
Problem: A user opens a ticket stating they never received an urgent password reset link or invoice receipt. Support agents usually do not have direct database access.
You: "User
usr_9182(emailbuyer@example.com) says they didn't receive their invoice receipt. Check why."
Agent:
1. →get_user({ id: "usr_9182" })to inspect registered contacts and quiet hours.
2. →list_suppressions({ target: "buyer@example.com" })to check if the address bounced or was marked as spam.
3. →get_delivery_logs({ search: "usr_9182", limit: 10 })to view dispatch attempts and provider status codes.
Agent response: "The user has quiet hours configured from 22:00 to 08:00 inAmerica/Chicago. The notification was deferred and is scheduled to dispatch at 08:01 AM tomorrow."
2. Safe Template Development with Dry-Run Previews
Who uses it: Product Managers, Template Designers.
Problem: Before broadcasting a campaign, you want to inspect rendered output and verify that all variable placeholders resolve correctly without sending live messages.
You: "Preview the
order_shippedtemplate for user Sarah with order #8172 and FedEx tracking."
Agent: →preview_template({ id: "order_shipped", data: { name: "Sarah", orderId: "8172", carrier: "FedEx", trackingNumber: "FX-817293" } })
{
"templateId": "order_shipped",
"channel": "email",
"rendered": {
"subject": "Order #8172 has shipped!",
"body": "Hi Sarah, your package is on its way via FedEx. Tracking: FX-817293"
},
"resolvedVariables": ["name", "orderId", "carrier", "trackingNumber"],
"unresolvedVariables": []
}
3. E-Commerce Flash Sales & Product Drops (Multichannel SMS & Push)
Who uses it: E-Commerce Operators, Growth Marketers.
Problem: You are launching a 4-hour flash sale. You need to blast 1,500 VIP customers over SMS and Push with a discount code, respecting quiet-hours boundaries.
You: "Send an SMS blast with template
flash_sale_smsto these phone numbers with code FLASH50: +15551234567, +15559876543, +15558765432."
Agent: →send_campaign({ campaign: "flash-sale-aug20", channel: "sms", template: "flash_sale_sms", recipients: ["+15551234567", "+15559876543", "+15558765432"], data: { code: "FLASH50" } })
4. SaaS Product Growth & Onboarding Automations
Who uses it: Product Managers, Growth Engineers.
Problem: You want to design and deploy an automated onboarding drip sequence for new trial users that stops automatically once they activate a core feature.
You: "Create an onboarding workflow called
saas-onboarding-v2: Sendwelcome_email, wait 2 days, sendpro_tips_email, then wait for eventproject.createdfor up to 5 days. If they don't create a project, sendtrial_help_reminder."
Agent: →create_workflowwithnotify,wait, andwaitForEventsteps →list_workflows({ search: "saas-onboarding" })
5. DevOps & Incident Response (Outage Alerts & Priority Lanes)
Who uses it: SRE, On-Call Engineers.
Problem: A regional database cluster is degraded. You need to alert all active users in the region-eu-central segment immediately, bypassing routine throttle queues.
You: "Notify all users in segment
region-eu-centralthat emergency maintenance is starting in 15 minutes. Use critical priority."
Agent: →send_notification({ segment: "region-eu-central", template: "incident_alert", priority: "critical", data: { eta: "15m", service: "Database Cluster A" } })
Result: Critical priority bypasses non-essential quiet-hours deferrals and dispatches immediately.
6. Marketing Performance & Executive Reporting
Who uses it: CMO, Growth Lead.
Problem: You need an aggregated delivery and engagement report for yesterday's product announcement without logging into individual provider dashboards (Resend, Twilio, Firebase).
You: "How did the
v2-launch-broadcastcampaign perform yesterday?"
Agent: →get_campaign_stats({ campaign: "v2-launch-broadcast" })
{
"campaign": "v2-launch-broadcast",
"totals": {
"sent": 12450,
"delivered": 12250,
"failed": 200,
"opened": 5129,
"clicked": 1085,
"bounced": 140,
"complained": 2,
"deliveryRate": 98.4,
"openRate": 41.2,
"clickRate": 8.7,
"complaintRate": 0.02
}
}
7. GDPR / Compliance & Channel Suppressions
Who uses it: Legal/Compliance Officers, Operations.
Problem: A customer exercises their "Right to be Forgotten" or requests an immediate global opt-out across SMS, email, and push.
You: "Customer with phone
+447700900123requested to be suppressed from all future communications."
Agent: →suppress_address({ channel: "sms", target: "+447700900123", reason: "manual" })→list_users({ search: "+447700900123" })to identify and suppress linked email addresses.
Reading the numbers
Audience size is not delivery count. Every send passes the same gates as any other
notification — suppressions, dedupe, opt-out, channel, quiet hours, throttle, template,
contact — so a list of 200 delivers to fewer than 200, by design.
send_campaign reports what was queued;
get_campaign_stats reports what actually happened.
An untracked open rate is not a zero open rate. Opens and clicks arrive from
provider webhooks. SMS and push report nothing at all; on email, nothing arrives unless the
webhook is configured. get_campaign_stats returns a warnings array
saying which of those applies, and the tool description instructs the agent to repeat it
rather than presenting a flat 0%.
Watch the complaint rate. Above roughly 0.3%, mailbox providers begin throttling or junking your mail — and because it is the same sending domain, that reaches your transactional email too. It is reported as its own figure for that reason.
Suppressions
An unsubscribe, a spam complaint, or a hard bounce arriving from a provider webhook adds the
address to a suppression list, and the engine drops it before dispatch on every later send.
This outranks every other setting, priority: "critical" included — a suppression
records that somebody asked to stop hearing from you, or that the address no longer exists,
and neither is overridable by an urgent send.
suppress_address covers the routes a provider cannot see: someone replying to
ask for removal, or telling you over the phone. list_suppressions explains why a
particular person did not receive something.
unsuppress_address exists, is marked destructive, and should be used only on
an explicit request. Removing suppressions to improve a delivery figure is the single most
reliable way to get a sending domain blocked. If an agent proposes it unprompted, that is a
bug in the prompt, not a good idea.
Safety
send_campaign, send_notification, trigger_workflow,
and replay_dead_letter reach real people and cannot be recalled. Every tool
carries MCP annotations describing what it does — read-only tools are marked as such, and
cancel_workflow_run and unsuppress_address are marked destructive —
so a client can auto-approve reads and prompt on everything that writes.
Where to go next
create_workflow is building for you.sendAt — the mechanics behind a scheduled broadcast.