MCP server
Ask for the work instead of writing the calls. Twenty-six tools over stdio, with your key kept out of the conversation.
All documentation
Getting started
Overview Quickstart SDK packagesFrontend
React Vue Svelte React Native Web Components Headless & theming Real-timeBackend API
Authentication Templates Trigger & status Contacts & preferencesMore
Self-hosting MCP server ReferenceWhat it is
@elaanio/mcp is an MCP server that puts the Elaan management API in front of an AI assistant. Once it is connected, you can ask for the work instead of writing the calls: create a notification type, add a template for a brand, trigger a test send, or find out why yesterday's email never arrived.
It speaks stdio, so it runs locally as a subprocess of your assistant. Nothing is hosted by us, and no traffic goes anywhere except from your machine to the Elaan API.
io.elaan/mcp, and published to npm as @elaanio/mcp.Install
You need a service key (sk_…) from the console. There is nothing to install ahead of time; npx fetches the package on first run.
One command, no file to edit:
claude mcp add elaan --env ELAAN_API_KEY=sk_… -- npx -y @elaanio/mcp
To share it with a team, commit a .mcp.json at the project root with the same block as the "Other clients" tab, and keep the key in each developer's environment rather than in the file.
Edit claude_desktop_config.json, reachable from Settings, Developer, Edit config:
{
"mcpServers": {
"elaan": {
"command": "npx",
"args": ["-y", "@elaanio/mcp"],
"env": { "ELAAN_API_KEY": "sk_…" }
}
}
}
Restart the app afterwards. Claude Desktop reads the file once at launch.
~/.cursor/mcp.json for every project, or .cursor/mcp.json in a project root for just that one:
{
"mcpServers": {
"elaan": {
"command": "npx",
"args": ["-y", "@elaanio/mcp"],
"env": { "ELAAN_API_KEY": "sk_…" }
}
}
}
.vscode/mcp.json in the workspace. Note the root key:
{
"servers": {
"elaan": {
"command": "npx",
"args": ["-y", "@elaanio/mcp"],
"env": { "ELAAN_API_KEY": "sk_…" }
}
}
}
servers, not mcpServers. Everything else on this page uses mcpServers. Pasting the wrong one gives you a config that parses fine and simply never loads the server, with no error to go on.~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"elaan": {
"command": "npx",
"args": ["-y", "@elaanio/mcp"],
"env": { "ELAAN_API_KEY": "sk_…" }
}
}
}
Any client that speaks MCP over stdio. The server is a plain command with one environment variable, so the shape is the same wherever the config lives:
{
"mcpServers": {
"elaan": {
"command": "npx",
"args": ["-y", "@elaanio/mcp"],
"env": { "ELAAN_API_KEY": "sk_…" }
}
}
}
If your client cannot set environment variables per server, export ELAAN_API_KEY in the shell that launches it. The server only ever reads it from the environment.
The server refuses to start without a key, and it tells you so rather than failing on the first call. If your config passes an unexpanded placeholder such as ${ELAAN_API_KEY}, that is treated as missing too, because a literal dollar sign in an auth header is never what anyone meant.
Where the key lives
The API key is read from the environment and is never a tool argument. That is deliberate. A key passed as an argument becomes part of the conversation: it lands in transcripts, in scrollback, and in whatever logging sits between the assistant and you. Keeping it in the process environment means the model never sees it and cannot repeat it back.
.env. Use a key you can rotate, and prefer a separate one from the key your production backend uses, so revoking it costs you nothing.The tools
Twenty-six of them, covering the management surface. You do not need to learn the names; the assistant picks them. They are listed here so you can see the shape of what it can reach.
Notification types
list_notification_types, get_notification_type, create_notification_type, update_notification_type, delete_notification_type, restore_notification_type
Types are soft deleted, which is why restore exists. Other objects reference a type by key, so the record has to survive.
Templates
list_templates, get_template, create_template, update_template, delete_template, list_templates_affected_by, preview_email_render
list_templates_affected_by answers "if I rename this variable, what breaks", and preview_email_render renders a template against a brand and a set of variables without sending anything. Both are far more useful in a conversation than in a dashboard.
Brands
list_brandings, create_branding, update_branding, delete_branding
The tenant default brand cannot be deleted, and the server returns the same error the API does rather than pretending otherwise.
Contacts and preferences
list_contacts, get_contact, create_contact, get_contact_preferences, set_contact_preference
Sending and debugging
trigger_notification, get_notification_event, list_deliveries, get_delivery_stats
This is the group that earns its keep. Trigger a send, take the returned event id, and ask what happened to it per channel, all without leaving the conversation.
What it deliberately cannot do
Transport credentials are absent, not gated. There are no tools for /v1/email-transport or /v1/push-transport, so the server cannot read or write your SMTP, SES, SendGrid, Mailgun, Expo or FCM credentials.
That is a deliberate scope decision rather than a permission check. A tool that exists but refuses is still a tool an assistant will try, describe and reason about. One that was never registered cannot be called by mistake, cannot be talked into running, and cannot leak a secret through an error message. Configure transports in the console.
Things worth asking it
- "Create a notification type for order shipped, with email and in-app on by default."
- "Add an email template for it, and show me how it renders for the Northwind brand."
- "Trigger it for contact crm-12345 and tell me what happened on each channel."
- "Which templates use the
order_idvariable?" - "What is the delivery success rate this week, and which channel is worst?"
The last two are the ones people are surprised by. Both are questions about your configuration rather than a single record, and both are tedious to answer by clicking.