A small, secure command-line tool that lets AI assistants (via "skills") send email over SMTP.
Configuration lives in appsettings.json; the SMTP password is kept in the .NET secret manager
(out of source control). A recipient allowlist and per-message limits prevent abuse.
Built on .NET 10. The only non-Microsoft dependency is MailKit
(the library Microsoft recommends in place of the legacy System.Net.Mail.SmtpClient).
dotnet build -c Release
# or a self-contained executable you can copy anywhere:
dotnet publish SmtpForAI/SmtpForAI.csproj -c Release -r win-x64 # or linux-x64, osx-arm64, ...The published
SmtpForAIexecutable readsappsettings.jsonfrom next to the executable. The SMTP password is stored separately in the OS user-secrets store, never inappsettings.json.
Interactive setup (prompts for host, port, SSL, username, From address, allowlist, and a masked password):
SmtpForAI configOr non-interactively (handy for scripts/CI/AI):
SmtpForAI config set \
--host smtp.example.com --port 587 --use-ssl true \
--username you@example.com --from you@example.com --display "Your Name" \
--allow-domain example.com --allow-recipient vip@partner.com \
--max-recipients 10 --max-attachment-bytes 10485760 \
--password "your-app-password"Check status at any time (the password is never printed):
SmtpForAI config showWhere things are stored:
appsettings.json— next to the executable. Holds theSmtp(host/port/auth/from) andSecurity(allowlist/limits) sections.secrets.json— in your user profile (e.g. on Windows%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json), holding onlySmtp:Password. This is plaintext but kept out of the repository — treat your profile accordingly.
# Validate against the allowlist/limits without sending:
SmtpForAI send --to alice@example.com --subject "Hello" --body "Hi there" --dry-run --json
# Actually send:
SmtpForAI send --to alice@example.com --subject "Hello" --body "Hi there" --jsonsend options
| Option | Description |
|---|---|
--to / --cc / --bcc |
Recipients. Repeatable, or comma-separated in one value. |
--subject |
Subject line. |
--body |
Body text. |
--body-file <path> |
Read the body from a file (instead of --body). |
| (stdin) | If neither --body nor --body-file is given, the body is read from stdin. |
--html |
Treat the body as HTML. |
--from <addr> |
Override the configured From address. |
--attach <path> |
Attach a file. Repeatable. |
--json |
Emit a machine-readable result: {"ok":true,...} / {"ok":false,"error":"..."}. |
--dry-run |
Validate (allowlist + limits) without sending. |
Every recipient (To/Cc/Bcc) must either match an entry in Security:AllowedRecipients
(exact, case-insensitive) or have its domain listed in Security:AllowedDomains. The total
recipient count must be within MaxRecipients, and each attachment within MaxAttachmentBytes.
Fail-closed: if both allowlists are empty, every send is blocked. Add at least one allowed domain or recipient via
config.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Usage error (unknown/incomplete command line) |
2 |
Config or policy error (not configured, recipient not on allowlist, attachment too large…) |
3 |
SMTP send failure |
SmtpForAI --version # or: SmtpForAI -v, SmtpForAI versionPrints something like SmtpForAI 1.0.12-pre+abc1234 (the suffix is the git commit; on a
release build it is omitted, e.g. SmtpForAI 1.0.12).
2"not configured" — runSmtpForAI config showto see which fields are missing.2"not on the allowlist" — add the recipient/domain viaconfig set --allow-domain ….3authentication failed — many providers require an app password, not your login password. Check the username and that the account allows SMTP.3TLS/connection errors — verify host/port and--use-ssl. Port465uses implicit TLS; port587uses STARTTLS;--use-ssl falsedisables both.
SmtpForAI mcp starts a Model Context Protocol server over
stdio so MCP-aware clients (Claude Desktop, Cursor, …) can call the tool directly.
Prerequisites
-
Publish a self-contained executable (see Build or publish above) and place it somewhere stable, e.g.
C:\Tools\SmtpForAI\SmtpForAI.exe. Claude Desktop will launch this exe on every start, so don't put it in a temp/build directory. -
Configure SmtpForAI before wiring it into Claude Desktop. The MCP server reads the same
appsettings.json+ user-secrets as the CLI, and refuses to send if any required field is missing:SmtpForAI config # interactive setup SmtpForAI config show # confirm "Configured: True"
-
Install Claude Desktop (the desktop app — the web version at claude.ai does not support local MCP servers).
Step 1 — locate claude_desktop_config.json
The easiest way is from inside Claude Desktop: open Settings → Developer → Edit Config. That creates the file if it doesn't exist and opens its folder. By default it lives at:
| OS | Path |
|---|---|
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
Step 2 — add the smtpforai server
Open the file in any text editor and add an mcpServers entry. If the file is empty, paste
this whole snippet; if it already has other servers, just add the "smtpforai" block inside
the existing mcpServers object.
Windows (note the doubled backslashes — JSON requires \\):
{
"mcpServers": {
"smtpforai": {
"command": "C:\\Tools\\SmtpForAI\\SmtpForAI.exe",
"args": ["mcp"]
}
}
}macOS / Linux:
{
"mcpServers": {
"smtpforai": {
"command": "/usr/local/bin/SmtpForAI",
"args": ["mcp"]
}
}
}The command must be the absolute path to the published executable — Claude Desktop does
not inherit your shell's PATH. No env, cwd, or extra args are needed; SmtpForAI reads
appsettings.json from next to the exe and the password from your user profile's secrets
store.
Step 3 — restart Claude Desktop
Fully quit and relaunch Claude Desktop (on Windows, also check the system tray; on macOS, use ⌘Q — closing the window only). Connections are established at startup, so a reload isn't enough.
Step 4 — verify it's connected
In a new chat, open Settings → Developer and confirm smtpforai shows as running.
Then ask Claude something like:
Use the
get_config_statustool from smtpforai and show me the result.
It should return JSON with "configured": true and your configured host/port. If you instead
see "I don't have access to that tool" or the server is marked failed, see Troubleshooting
below.
| Tool | What it does |
|---|---|
send_email |
Sends an email. Supports to/cc/bcc, subject, body, isHtml, from, attachments (absolute paths), and dryRun (validate without sending). |
validate_recipient |
Pure check: is an address well-formed and on the allowlist? No SMTP traffic. |
get_config_status |
Read-only status: configured, missing[], hasPassword (the password value is never returned), plus host/port and allowlist counts. |
The MCP path uses the same allowlist, per-message limits, and fail-closed behavior as the
CLI — there is no MCP-only "trusted" mode. There is intentionally no set_config tool, so an
AI prompt cannot relax the policy or change credentials.
- Server status shows "failed" — open Claude Desktop's MCP log. On Windows it is
%APPDATA%\Claude\logs\mcp-server-smtpforai.log; on macOS~/Library/Logs/Claude/mcp-server-smtpforai.log. SmtpForAI routes all its own logging to stderr, so any startup error (missing file, bad JSON, .NET runtime missing) appears there. send_emailreturns "not configured" — Claude Desktop launched the exe, butappsettings.json/ user-secrets are missing values. RunSmtpForAI config showfrom a terminal as the same OS user that runs Claude Desktop and fix any reported gaps.send_emailreturns "not on the allowlist" — expected: add the recipient or domain viaSmtpForAI config set --allow-domain example.com(or--allow-recipient). The policy is intentionally CLI-only.- Tool list is empty after restart — confirm the JSON is valid (a trailing comma or
single backslash on Windows is the usual cause) and that the
commandpath actually exists.claude_desktop_config.jsonerrors are silent on the UI; they only show up in the log.
SmtpForAI/ # the CLI application
Cli/ # ArgParser, ExitCodes
Commands/ # ConfigCommand, SendCommand
Configuration/ # SmtpSettings, AppConfiguration, AppSettingsWriter, UserSecretsStore
Mcp/ # McpCommand (stdio server), EmailTool ([McpServerTool] methods)
Security/ # MailValidation (allowlist + limits)
Services/ # MailRequest, SendResult, MailSender (shared CLI + MCP core)
appsettings.json # committed template (no secrets)
SmtpForAI.Tests/ # MSTest unit tests + MCP stdio smoke tests
skill/ # SKILL.md (AI skill manifest) + catalog.json
.github/workflows/ci.yml # CI
- .NET 10 SDK (pinned via
global.json).
dotnet build SmtpForAI.slnx -c Release
dotnet test SmtpForAI.slnx -c Release
dotnet run --project SmtpForAI -- config show- Microsoft-only, with two exceptions. The project otherwise uses only Microsoft/BCL
packages. The two deliberate exceptions are MailKit/MimeKit (the library Microsoft
recommends in place of
System.Net.Mail.SmtpClient) and ModelContextProtocol (the Microsoft-+-Anthropic-maintained C# SDK for MCP). - AOT/trim. MailKit/MimeKit and the MCP SDK are both reflection-heavy and not
trim/AOT safe, so
PublishAotis not enabled. Distribute via a self-containeddotnet publishinstead. - CLI + MCP share one core. Both
send(CLI) andsend_email(MCP) build aMailRequest, hand it toServices/MailSender, which runsMailValidation(allowlist + limits + address syntax) before any SMTP traffic. The security policy cannot diverge between the two surfaces. - MCP stdio gotcha.
Mcp/McpCommandroutes all logging to stderr because stdout is the MCP JSON-RPC channel. Any strayConsole.Writethere would corrupt the protocol. - Reflection-free config.
SmtpSettings.Loadreads values via theIConfigurationindexer andGetChildren()(no binder), keeping the door open to trimming. - Secrets.
UserSecretsStorereads/writessecrets.jsonwithJsonNode, keyed"Smtp:Password"exactly as the .NET config system stores it — so the file stays compatible with thedotnet user-secretsCLI.
.github/workflows/ci.yml builds and tests the solution:
- Linux (
ubuntu-latest) on every push and pull request — the cheap default. - Windows (
windows-latest) only onmain, to exercise Windows without paying the higher Windows-minute cost on every PR. verify-release-versionruns onrelease/*branches and PRs targeting them, and fails the build ifversion.jsonstill has a-presuffix. The check is apwshstep so the same script is portable across Windows, macOS, and Linux runners.
The repo uses Nerdbank.GitVersioning (NBGV),
driven by version.json at the repo root. NBGV stamps AssemblyVersion, FileVersion, and
AssemblyInformationalVersion on every build — there are no version literals in any .csproj.
- Day-to-day
version.jsonreads"version": "1.0-pre", so feature-branch builds produce e.g.1.0.12-pre+abc1234(height + short commit hash). - Releasing Cut a
release/<x.y>branch, editversion.jsonto drop the-presuffix (e.g."version": "1.0"), and push. Theverify-release-versionCI job will fail the branch if you forget. Merge the PR, then tag the merge commit. - The
publicReleaseRefSpecinversion.jsonlistsmainandrelease/*, so builds on those refs omit the+gitHashbuild-metadata suffix.
The skill/ folder contains SKILL.md (the AI skill manifest describing how to invoke the
SmtpForAI executable) and catalog.json (a catalog manifest registering the skill for
discovery/install). An assistant configured with this skill calls the CLI with --json and
honors the exit codes above.
- HTTP / SSE MCP transport (currently stdio only)
- OAuth2 / XOAUTH2 SMTP authentication
- Optional DPAPI-encrypted secret storage on Windows