Smtp Wrapper
FreeNot checkedA minimal MCP server that provides a send_email tool for sending HTML email through an SMTP relay.
About
A minimal MCP server that provides a send_email tool for sending HTML email through an SMTP relay.
README
A minimal, self-hosted MCP server that exposes a
single send_email tool. It sends real HTML email through an SMTP relay (e.g. Gmail).
The tool is served over the streamable-HTTP MCP transport at /mcp, with an
unauthenticated /healthz liveness route. The implementation is intentionally tiny
(stdlib smtplib) to keep the audit/attack surface small.
⚠️ Security requirement: this server MUST be gated by an authorization service
This server implements no authentication of its own, by design. Anyone who can reach
/mcp can send email. Do not expose it directly to the internet or bind it to a public
port.
It must sit behind an identity-aware authorization proxy — such as
Pomerium in MCP mode, or an
equivalent like Cloudflare Access
or oauth2-proxy — that authenticates and
authorizes every request before it reaches /mcp.
Reference topology:
edge tunnel → reverse proxy (TLS) → Pomerium (SSO + allowlist to a single identity) → smtp-mcp-wrapper
(internal network only)
The provided docker-compose.yml deliberately publishes no host ports and attaches
the container only to the proxy's internal Docker network, so the server is unreachable
except through the authorization proxy.
Defense in depth already built in (these complement, they do not replace, the proxy):
ALLOWED_TOhard-limits recipients, so even a misused tool cannot mail outside the allowlist.- Setting
REQUIRE_POMERIUM_IDENTITY=truemakes the app cryptographically verify Pomerium's identity assertion on every/mcprequest — signature (against Pomerium's JWKS), expiry, and audience. This blocks anything on the shared Docker network from reaching the app directly and bypassing Pomerium. See Enabling app-layer verification.
Configuration
All configuration is via environment variables. Copy .env.example to .env and fill in
real values. .env is git-ignored and must stay that way — it holds the SMTP password.
Nothing secret is baked into the image (credentials are injected at runtime), which is why
the published container image can safely be public.
| Variable | Default | Description |
|---|---|---|
SMTP_HOST |
smtp.gmail.com |
SMTP relay host. |
SMTP_PORT |
587 |
SMTP relay port (STARTTLS). |
SMTP_USER |
— | SMTP username. |
SMTP_PASS |
— | SMTP password. For Gmail, use an App Password. |
MAIL_FROM |
SMTP_USER |
From address. |
MAIL_FROM_NAME |
— | Optional display name for the From header. |
DEFAULT_TO |
— | Recipient used when the tool's to argument is omitted. |
ALLOWED_TO |
— | Comma-separated recipient allowlist. Empty = any recipient allowed. |
STARTUP_TEST_EMAIL |
false |
Send a test email to DEFAULT_TO on startup to verify SMTP. On failure, logs the SMTP error reason (auth/connection); the server keeps running either way. |
REQUIRE_POMERIUM_IDENTITY |
false |
Verify Pomerium's identity assertion on every /mcp request (see below). Requires POMERIUM_JWKS_URL. |
POMERIUM_JWKS_URL |
— | Pomerium's JWKS endpoint, e.g. https://<host>/.well-known/pomerium/jwks.json. Required when the gate is on. |
POMERIUM_AUDIENCE |
— | Expected aud claim (the route host/URL). Verified when set — strongly recommended. |
POMERIUM_ISSUER |
— | Expected iss claim. Verified only when set. |
POMERIUM_IDENTITY_HEADER |
x-pomerium-assertion,x-pomerium-jwt-assertion |
Comma-separated header(s) carrying the assertion JWT. |
MCP_ALLOWED_HOSTS |
— | Comma-separated Host allowlist for /mcp (DNS-rebinding guard). Empty = guard off. See below. |
MCP_ALLOWED_ORIGINS |
https://<each allowed host> |
Comma-separated Origin allowlist for browser-originated requests. |
HOST / PORT |
0.0.0.0 / 8080 |
Server bind address/port. |
The send_email tool
send_email(subject: str, html: str, to?: str, text?: str) -> str
Sends an HTML email. to falls back to DEFAULT_TO and must be within ALLOWED_TO when
that allowlist is set. text is an optional plain-text alternative for non-HTML clients.
DNS-rebinding guard (Host allowlist)
MCP SDK 2.x checks the Host header on every /mcp request and answers 421 Misdirected
Request when it is not allowlisted (CVE-2025-66416 made this on by default). This
server leaves it off unless MCP_ALLOWED_HOSTS is set, so an SDK upgrade alone can
never take a working deployment offline — you opt in.
⚠️ The allowlist is not your public hostname. Pomerium — like most reverse proxies by default — rewrites
Hostto the upstream address before forwarding. The route may behttps://email-mcp.example.com, but what the container receives isHost: email-mcp:8080. Allowlisting the public name still 421s.
Find what actually arrives rather than guessing: in Pomerium's access log, the authority
field on the http-request line is the Host the upstream sees (the host field on the
authorize check line is the public route). This varies per route in the same Pomerium
instance, so check this one.
MCP_ALLOWED_HOSTS=email-mcp:8080
Then redeploy and confirm the startup line names it:
DNS-rebinding guard enabled — allowed hosts: email-mcp:8080; ...
INFO: Uvicorn running on http://0.0.0.0:8080
Matching is literal — a bare example.com will not match a Host carrying a port; use
example.com:* for any port. Alternatively set preserve_host_header: true on the
Pomerium route and allowlist the public name instead.
Verify with a real tool call, not the healthcheck. /healthz is not behind the guard,
so a container answering healthy proves nothing — a misconfigured allowlist shows up only
as a 421 on POST /mcp. scripts/smoke_test.sh automates exactly this check and runs in
CI before any image is pushed.
Enabling app-layer verification
This step is optional — Pomerium already gates all access. Enable it only if you also
want the app to reject any request that reaches it without a valid Pomerium identity
(e.g. a compromised neighbor on the shared Docker network hitting email-mcp:8080
directly). When on, the app verifies the assertion JWT's signature, expiry, and audience.
1. Pomerium — set these on the email-mcp route. The critical addition is
pass_identity_headers: true; without it Pomerium forwards no identity header and the app
rejects every request. Pomerium must also have a signing key configured (it serves the
matching public keys at /.well-known/pomerium/jwks.json).
routes:
- from: https://email-mcp.example.com
to: http://email-mcp:8080 # pathless — the /mcp path passes through
name: email-mcp
mcp:
server: {}
pass_identity_headers: true # <-- REQUIRED: sends X-Pomerium-Assertion to the app
policy:
- allow:
and:
- email:
is: [email protected]
2. App — set these in .env:
REQUIRE_POMERIUM_IDENTITY=true
POMERIUM_JWKS_URL=https://email-mcp.example.com/.well-known/pomerium/jwks.json
POMERIUM_AUDIENCE=email-mcp.example.com
Then docker compose up -d. If REQUIRE_POMERIUM_IDENTITY=true but POMERIUM_JWKS_URL is
unset, the server refuses to start (a security gate must not run unable to verify). To turn
the feature off again, set REQUIRE_POMERIUM_IDENTITY=false.
Run
cp .env.example .env # then edit .env with real values
docker compose up -d
Health check:
docker compose exec email-mcp \
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8080/healthz').read())"
# -> b'ok'
Then add the email-mcp route to your authorization proxy (pathless upstream, e.g.
to: http://email-mcp:8080, so the /mcp path passes through) and connect your MCP
client to https://<your-host>/mcp.
Maintenance
Patches flow with near-zero manual effort:
- Dependabot (
.github/dependabot.yml) watchesrequirements.txt, the Dockerfile base image, and the workflow's actions, opening upgrade PRs weekly. Also enable Dependabot security updates in the repo's Settings → Code security. - CI (
.github/workflows/build.yml) builds and pushes the image to GHCR on push tomain, on Dependabot PRs, via manual dispatch, and weekly (Mon 06:00 UTC) withno-cacheso the OS and Python patches are genuinely refreshed even without code changes. - Smoke test (
scripts/smoke_test.sh) runs against the built image before the push step, driving a real MCPinitialize+tools/listover a non-localhostHost. This is what makes an unattended SDK bump safe to merge: the failures an MCP SDK upgrade actually causes — binding the wrong interface, or aHostallowlist that rejects the proxy — produce an image that builds and reports healthy while every tool call fails, so a build-only gate would wave them straight through. Run it locally with./scripts/smoke_test.sh <image>. - On the host, pull the rebuilt image with Watchtower
(the compose file already sets the opt-in label) or a cron running
docker compose pull && docker compose up -d.
Links
- Pomerium — MCP support
- Pomerium — Protect an MCP server
- Dependabot configuration options
Installing Smtp Wrapper
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/JB09/smtp-mcp-wrapperFAQ
Is Smtp Wrapper MCP free?
Yes, Smtp Wrapper MCP is free — one-click install via Unyly at no cost.
Does Smtp Wrapper need an API key?
No, Smtp Wrapper runs without API keys or environment variables.
Is Smtp Wrapper hosted or self-hosted?
A hosted option is available: Unyly runs the server in the cloud, no local setup required.
How do I install Smtp Wrapper in Claude Desktop, Claude Code or Cursor?
Open Smtp Wrapper on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Gmail
Read, send and search emails from Claude
by GoogleSlack
Send, search and summarize Slack messages
by SlackRunbear
No-code MCP client for team chat platforms, such as Slack, Microsoft Teams, and Discord.
Discord Server
A community discord server dedicated to MCP by [Frank Fiegel](https://github.com/punkpeye)
Klavis AI
Open Source MCP Infra. Hosted MCP servers and MCP clients on Slack and Discord.
Work90210/APIFold
Turn any REST API into a hosted MCP server. 18 free public servers (GitHub, Stripe, Slack, OpenAI, Notion, and more) — no setup required, bring your own API key
by Work90210arikusi/deepseek-mcp-server
MCP server for DeepSeek AI with chat, reasoning, multi-turn sessions, function calling, thinking mode, and cost tracking.
by arikusihashgraph-online/hashnet-mcp-js
MCP server for the Registry Broker. Discover, register, and chat with AI agents on the Hashgraph network.
by hashgraph-onlineprofullstack/mcp-server
A comprehensive MCP server aggregating 20+ tools including SEO optimization, document conversion, domain lookup, email validation, QR generation, weather data,
by profullstackWayStation-ai/mcp
Seamlessly and securely connect Claude Desktop and other MCP hosts to your favorite apps (Notion, Slack, Monday, Airtable, etc.). Takes less than 90 secs.
by waystation-aiCompare Smtp Wrapper with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All communication MCPs
