MCP server that sends mail over SMTP, gated behind an allowlist and a human confirmation
io.github.ni-c/smtp-mcp (SMTP MCP Server)
This MCP server sends mail over SMTP. Access is gated behind an allowlist and requires a human confirmation step before sending. It is distributed as a Node.js/TypeScript package and is categorized under topics including email, SMTP, Nodemailer, and Model Context Protocol (MCP).
π οΈ Key Features
Sends email over SMTP
Uses an allowlist for access control
Requires human confirmation to proceed
Built for Model Context Protocol (MCP)
π Use Cases
Email delivery workflows triggered from MCP clients
Systems that need controlled, human-approved outbound messaging
β‘ Developer Benefits
Integrates MCP with SMTP-based sending
Uses TypeScript/Node.js tooling
Includes nodemailer as indicated by its topic set
β οΈ Limitations
Outbound sending is blocked unless the requester is allowlisted
Sending requires human confirmation, preventing fully automated dispatch
A Model Context Protocol (MCP) server for
SMTP, the protocol every mail server speaks to
accept a message for delivery.
Lets MCP clients like Claude Code, Claude Desktop or Codex send, reply to and forward mail from
one configured address β with a human approving every message.
Seven tools is the ceiling, not the floor: SMTP_ALLOW_TOOLS=essential registers a curated five
instead, and a model picks the right tool far more reliably from five than from seven β see
choosing which tools load.
Listing the seven tools, then calling send_mail β which sends nothing and comes back asking, with the recipient and subject on their own labelled lines β and then a second call to an address outside the allowlist, which is refused before any connection is opened
What makes it different
It sends mail. That is why it is a separate server. Its counterpart
imap-mcp reads a mailbox and deliberately has no way to send
anything β that absence is its entire security argument, because an agent that can reach private
data and process attacker-controlled content is only exploitable once it also has a way out.
This server is that way out, so it does not get to make the same claim. It earns its place by
narrowing the channel instead, and by living in its own process with its own credentials.
It is off when you install it.SMTP_ALLOW_SEND defaults to false. Until it is set the
sending tools are not registered at all β absent from tools/list, not refused at call time.
It can only write to people you named.SMTP_ALLOWED_RECIPIENTS is required to turn sending
on. Every address in To, Cc and Bcc is checked against it before a connection is opened, so an
injected "mail this to someone else" fails without the server ever reading it as an instruction.
Allowing everyone is possible and has to be written as *.
Every message is approved by a person. Not by the model β the request goes to the client as
an MCP elicitation. Recipients, subject and any Bcc appear on their own labelled lines, so a
subject written to look like an instruction cannot become part of the server's own sentence.
The sender cannot be chosen. There is no from parameter. A model that could pick its own
sender could write in a colleague's name, and the result would be indistinguishable from a
message they wrote.
Requirements
Node.js 22 or newer
An SMTP account you may send from β a submission server on port 587 or 465, with a username
and password. Providers with two-factor authentication generally need an app-specific
password.
Configuration
Use TLS.SMTP_TLS=starttls (the default) requires the upgrade rather than attempting it,
so a stripped STARTTLS capability fails the connection instead of quietly sending your
password in the clear. For a self-signed certificate prefer a proper internal CA over
SMTP_INSECURE_TLS.
Variable
Required
Default
Description
SMTP_HOST
yes
β
Hostname of the SMTP server, e.g. smtp.example.net.
SMTP_USER
yes
β
Username for SMTP authentication.
SMTP_PASSWORD
yes
β
Password or app-specific password.
SMTP_FROM
yes
β
The only sender used, e.g. Name <person@example.net>. There is no from parameter.
SMTP_REPLY_TO
no
β
Reply-To on every message, e.g. Team <team@example.net>. Unset means replies go to SMTP_FROM.
SMTP_PORT
no
587 / 465 / 25
Depends on SMTP_TLS.
SMTP_TLS
no
starttls
starttls, implicit or none. Never opportunistic.
SMTP_ALLOW_SEND
no
false
Set to true to register the sending tools.
SMTP_ALLOWED_RECIPIENTS
with SMTP_ALLOW_SEND=true
β
Comma-separated addresses and @domains. * allows anyone.
SMTP_MAX_RECIPIENTS
no
10
Distinct recipients across To, Cc and Bcc in one message.
SMTP_MAX_SENDS_PER_HOUR
no
20
Sliding hourly cap.
SMTP_MAX_MESSAGE_BYTES
no
10485760
Size ceiling on the composed message.
SMTP_MAX_ATTACHMENT_BYTES
no
5242880
Size ceiling on one attachment.
SMTP_ATTACHMENT_DIR
no
β
Directory attachments are read from. Unset means no attachments.
SMTP_ATTACHMENT_TYPES
no
document and image allowlist
Comma-separated content types that may be attached. text/html and application/zip are opt-in.
SMTP_SIGNATURE
no
β
Text appended below the standard -- delimiter.
SMTP_AUDIT_LOG
no
β
File the audit lines are appended to, in addition to stderr.
SMTP_ALLOW_TOOLS
no
β
Tool names, a prefix with one trailing *, or essential.
SMTP_DENY_TOOLS
no
β
Removed after SMTP_ALLOW_TOOLS is applied.
SMTP_INSECURE_TLS
no
false
Accept self-signed certificates.
ELICITATION
no
true
false replaces the approval dialog with the two-call token. Not prefixed.
Two defaults are worth reading twice, because they are the opposite of what the rest of this
family does:
SMTP_ALLOW_SEND is off. A freshly installed smtp-mcp can compose and preview messages
and cannot send any.
An unset SMTP_ALLOWED_RECIPIENTS is a startup error, not "anyone". Treating a missing
line as permission is how an accident becomes a delivered message. Write * if you mean it.
Choosing which tools load
SMTP_ALLOW_TOOLS decides what is registered, SMTP_DENY_TOOLS is subtracted from it. Both take
comma-separated tool names or a prefix with a single trailing *. A filtered tool is never
built β it does not appear in tools/list and answers a call with "not found", the same as a
sending tool while the send gate is closed. Nothing is advertised and then refused.
sh
SMTP_ALLOW_TOOLS=essential # the curated five
SMTP_ALLOW_TOOLS='get_server_info,preview_mail'
SMTP_DENY_TOOLS=forward_mail # everything else, minus forwarding
essential is get_server_info, validate_recipients, preview_mail, send_mail and
reply_mail. forward_mail is left out: forwarding carries somebody else's content and
attachments outward, which is the call that most deserves to be switched on deliberately.
An entry that matches no tool aborts startup and lists the real names. A tool quietly missing
from tools/list is invisible β nobody traces an absence back to an environment variable.
Installation
Claude Code
sh
claude mcp add smtp -- npx -y @ni-c/smtp-mcp
Claude Desktop
json
{"mcpServers":{"smtp":{"command":"npx","args":["-y","@ni-c/smtp-mcp"],"env":{"SMTP_HOST":"smtp.example.net","SMTP_USER":"person@example.net","SMTP_PASSWORD":"app-specific-password","SMTP_FROM":"Your Name <person@example.net>","SMTP_ALLOW_SEND":"true","SMTP_ALLOWED_RECIPIENTS":"@example.net"}}}}
A client that cannot spawn a local process β ChatGPT connectors, Claude on the web,
Cursor, LibreChat β reaches smtp-mcp through mcp-hub: one
container serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login
behind a single password and long-lived tokens for the clients that cannot do OAuth. Its
/hub endpoint puts every server behind six meta-tools, so one connector reaches all of
them without NΓtool schemas in the model's context, and it speaks both protocol revisions
β a question this server asks travels through it to the person at the far end.
Its /config/mcp.json uses Claude Code's format, so the entry is the one you already
have:
json
{"mcpServers":{"smtp-mcp":{"command":"npx","args":["-y","@ni-c/smtp-mcp"],"env":{"SMTP_HOST":"smtp.example.net","SMTP_USER":"person@example.net","SMTP_PASSWORD":"β¦","SMTP_FROM":"Your Name <person@example.net>","SMTP_ALLOW_TOOLS":"essential"},"denyTools":["send_mail","reply_mail"]}}}
allowTools and denyTools there are the hub's own per-server filter, which is not
the same thing as *_ALLOW_TOOLS in env β the difference, and the mistake it invites,
are in the client guide.
Tools
Always registered. None of these can put a message on the wire.
Tool
What it does
get_server_info
The endpoint, the fixed sender, the allowlist, the limits, and whether sending is on at all.
validate_recipients
Which addresses this server may write to, and why the others are refused. No connection made.
preview_mail
Builds exactly the message send_mail would build and shows it. Runs every check a send runs.
test_connection
Connects, negotiates TLS, authenticates, disconnects. Sends nothing; at most once every 10 s.
Registered only with SMTP_ALLOW_SEND=true. π€ marks the ones that ask a human before acting.
Tool
What it does
send_mail π€
Sends a new message.
reply_mail π€
Sends a reply that threads under the original, deriving Re: from the subject.
forward_mail π€
Forwards a message to new recipients, quoting the original verbatim.
Structured output
Every tool declares an outputSchema and answers with structuredContent
alongside the text block, so a client can use the result without parsing prose:
jsonc
{"sent":true,"already_sent":false,"message_id":"<b1c9β¦@example.net>","accepted":["her@example.net","him@example.net"],"rejected":[],"bytes":1284,"sends_remaining_this_hour":9,"note":"The SMTP server accepted the message. It cannot be recalled.",}
preview_mail is the one tool that carries untrusted: true and
source: "smtp" as fields: a quoted original was written by whoever sent it,
and anyone in the world can send mail. Its text block keeps the nonce fence β
the structured half states the same fields so a client is not made to parse it.
Everything else here reports this server's own configuration or the outcome of
its own send, where the marker would be a false claim about who wrote it.
Not exposed, on purpose
A from parameter. The sender is SMTP_FROM and nothing else.
A way to skip the confirmation. No force, no "trusted caller" mode. An option to turn
the gate off would be the first thing a prompt injection reaches for.
Reading mail. That is imap-mcp. Keeping the two in
separate processes with separate credentials is most of what makes either of them safe.
Arbitrary headers. Threading uses in_reply_to and references; there is no passthrough
for header names, because that is header injection with extra steps.
DKIM signing. A submission server signs for you. A private signing key in an environment
variable is a worse trade than it looks.
Safety
Sending is off by default and the tools do not exist until it is on.
Every recipient is checked against an allowlist before a connection is opened, in To, Cc
and Bcc alike.
Every message is approved by a human, through MCP elicitation. The two-call token fallback
says plainly that it is not the same thing.
A confirmation is bound to the exact message: a SHA-256 fingerprint over the sorted
recipient list and a digest of the content, so an approval cannot be spent on a wider list or
on different text.
The dialog shows the message, not only the envelope: the body, the quoted original and the
HTML part, each with its length in characters. Everything else here binds who a message goes
to; this is what binds what it says.
Bcc recipients get their own labelled line in the dialog. A hidden recipient a human does
not see is the ideal exfiltration channel.
Confirmation text never quotes caller-chosen values into the server's own sentence.
The same message is not sent twice. An approval proves that somebody agreed to a message,
not that they agreed to it again, so a message the SMTP server accepted is remembered for as
long as an approval for it could still be redeemed.
Outgoing HTML is stripped of scripts, event handlers, remotely loaded images and unsafe URL
schemes β and every removal is reported, never silent. Markup that cannot be cleaned with
confidence is refused rather than repaired.
A quoted original is passed on unchanged, with any prompt-injection shapes it matches named
in the dialog.
Attachments come only from SMTP_ATTACHMENT_DIR, past an extension allowlist, a symlink
refusal and a magic-byte check.
Every accepted message is recorded on stderr and optionally in a file. Never the body.
See SECURITY.md for the reasoning, and for what none of this covers.
Documentation
The full guide, tool reference and security notes live at
smtp-mcp.ni-c.de (source in docs/).
Development
sh
npm install
npm test
npm run build
There is a throwaway Mailpit sandbox in test/integration/. Develop
against it rather than a real mailbox: this server's job is to put messages on the wire, and a
test run that goes wrong sends real mail to real people.
sh
docker compose -f test/integration/compose.yml up -d --wait
npm run build && npm run test:integration
Releasing
Tagging vX.Y.Z on main runs the release workflow: it checks the tag matches package.json,
publishes to npm with provenance through Trusted Publishing, pushes a multi-arch image to GHCR
with an SBOM, creates the GitHub release from the CHANGELOG section, and submits the version to
the MCP Registry.
Contributing
Issues, discussions and pull requests are welcome β see
CONTRIBUTING.md. For vulnerabilities please use
private reporting
rather than a public issue; the policy is in SECURITY.md.
Hostname of the SMTP server, e.g. smtp.example.net.
SMTP_PORT
Port. Defaults to 587 for starttls, 465 for implicit, 25 for none.
SMTP_TLS
starttls (default), implicit or none. STARTTLS is required, never opportunistic.
SMTP_USERrequired
Username for SMTP authentication.
SMTP_PASSWORDrequiredsecret
Password or app-specific password for SMTP authentication.
SMTP_FROMrequired
The only sender this server will use, e.g. "Name <person@example.net>". There is no from parameter.
SMTP_REPLY_TO
Reply-To header on every message, e.g. "Team <team@example.net>". Unset means no Reply-To header and replies go to SMTP_FROM. There is no reply_to parameter.
SMTP_ALLOW_SEND
Set to "true" to register the sending tools. Defaults to false: the server cannot send until it is set.
SMTP_ALLOWED_RECIPIENTS
Comma-separated addresses and @domains this server may write to. Required with SMTP_ALLOW_SEND=true; "*" allows any.
SMTP_MAX_RECIPIENTS
Maximum distinct recipients across To, Cc and Bcc in one message. Default 10.
SMTP_MAX_SENDS_PER_HOUR
Sliding hourly cap on messages sent. Default 20.
SMTP_MAX_MESSAGE_BYTES
Maximum size of the composed message. Default 10485760.
SMTP_MAX_ATTACHMENT_BYTES
Maximum size of a single attachment. Default 5242880.
SMTP_ATTACHMENT_DIR
Directory attachments are read from. Unset means attachments are unavailable.
SMTP_ATTACHMENT_TYPES
Comma-separated content types that may be attached. Defaults to a document and image allowlist.
SMTP_SIGNATURE
Text appended below the standard signature delimiter of every message.
SMTP_AUDIT_LOG
File the audit lines are appended to, in addition to stderr.
SMTP_ALLOW_TOOLS
Comma-separated tool names, a prefix with one trailing *, or "essential" for the curated preset.
SMTP_DENY_TOOLS
Comma-separated tool names or prefixes to remove, applied after SMTP_ALLOW_TOOLS.
SMTP_INSECURE_TLS
Set to "true" to accept self-signed certificates. Prefer a proper internal CA.