Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

Smtp Wrapper

БесплатноНе проверен

A minimal MCP server that provides a send_email tool for sending HTML email through an SMTP relay.

GitHubEmbed

Описание

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_TO hard-limits recipients, so even a misused tool cannot mail outside the allowlist.
  • Setting REQUIRE_POMERIUM_IDENTITY=true makes the app cryptographically verify Pomerium's identity assertion on every /mcp request — 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 Host to the upstream address before forwarding. The route may be https://email-mcp.example.com, but what the container receives is Host: 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) watches requirements.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 to main, on Dependabot PRs, via manual dispatch, and weekly (Mon 06:00 UTC) with no-cache so 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 MCP initialize + tools/list over a non-localhost Host. This is what makes an unattended SDK bump safe to merge: the failures an MCP SDK upgrade actually causes — binding the wrong interface, or a Host allowlist 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

from github.com/JB09/smtp-mcp-wrapper

Установка Smtp Wrapper

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/JB09/smtp-mcp-wrapper

FAQ

Smtp Wrapper MCP бесплатный?

Да, Smtp Wrapper MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Smtp Wrapper?

Нет, Smtp Wrapper работает без API-ключей и переменных окружения.

Smtp Wrapper — hosted или self-hosted?

Доступен hosted-вариант: Unyly запускает сервер в облаке, локальная установка не обязательна.

Как установить Smtp Wrapper в Claude Desktop, Claude Code или Cursor?

Открой Smtp Wrapper на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Smtp Wrapper with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории communication