# Technical Design Spec (Phased): `mail-smtp-mcp`
## 1) Summary
Build an MCP server that provides a small set of outcome-oriented tools (target 3–7, hard cap 15) to
send email via SMTP for pre-configured accounts. Tools must use flat, strongly-typed JSON Schemas;
return token-efficient summaries by default; and implement security guardrails (no secrets in
outputs, least privilege, scrubbed audit logging, and bounded payload sizes).
This TDS is phased so the server becomes useful early (configuration validation, connectivity checks,
and safe sending) then adds attachments, hardening, and policy controls (allowlists, quotas).
## 2) Goals
- Enable an LLM to send a well-formed email in **one tool call**.
- Provide safe, policy-controlled outbound email sending for configured accounts.
- Keep outputs concise and structured for LLM consumption (no raw MIME by default).
- Enforce strict validation to prevent header injection and accidental exfiltration.
## 3) Non-Goals (for this server)
- IMAP browsing/searching/reading email (separate bounded context: `mail-imap-mcp`).
- Full MUA features (threading, drafts folders, scheduled send, contact management).
- Provider-specific workflows unless required (e.g., Gmail API specifics).
- Rich templating systems (can be layered outside the server).
## 4) Assumptions
- This server runs in a trusted local or controlled environment where credentials can be provisioned
securely.
- SMTP accounts are configured outside the LLM prompt (recommended); tools refer to them by
`account_id`.
- Outbound email is inherently irreversible; sending must be guarded by explicit enablement and
policy controls.
## 4.1) Implementation Stack (decided)
- **MCP:** `@modelcontextprotocol/sdk` over **stdio**
- **SMTP:** `nodemailer` (SMTP transport)
- **Schemas:** `zod` for inputs + outputs; generate JSON Schema from Zod when registering MCP tool
`inputSchema`
- **Package manager:** `pnpm`
## 5) Constraints & Design Rules
- **Tool count:** recommended 3–7; hard cap 15.
- **Outcome-oriented tools:** each tool maps to a complete user capability.
- **Input schemas:** flat inputs; constrain strings/arrays with bounds and enums where possible.
- **Outputs:** concise summaries; avoid raw RFC822/MIME outputs by default.
- **Security:** never return secrets; scrub secrets from logs; least privilege; strict size limits.
- **Sending gate:** sending must be disabled by default and explicitly enabled via environment.
- **Testing:** validate schema violations, send gate behavior, recipient policy enforcement, and size
limits.
## 6) Proposed Bounded Context
**“Send outbound email messages over SMTP for configured accounts.”**
Primary user outcomes:
- “Email Alice a short status update from my work account.”
- “Send an email with a small attachment (size-limited) to a known recipient.”
- “Verify my SMTP account is configured and can connect/authenticate.”
## 7) Proposed Tool Surface (v1)
Target: 3 tools (keeps the surface small and safe).
### 7.1 Tool list (names are stable API)
1. `mail_smtp_list_accounts`
2. `mail_smtp_verify_account`
3. `mail_smtp_send_message`
### 7.2 Common conventions
- All tools accept `account_id: string` (default `"default"` if omitted).
- All tools return:
- A single `content` item of `type: "text"` whose text is a JSON object
- `summary`: concise human-readable summary
- `data`: structured payload aligned to the tool contract
- `_meta`: optional metadata (e.g., capabilities, policy summary)
- Never echo credentials; never include SMTP AUTH details in outputs.
- Tools should return actionable, non-sensitive errors (missing env vars, auth/connectivity failures,
blocked recipients, size limit violations).
### 7.3 Tool contracts (indicative; finalize in Phase 0)
#### `mail_smtp_list_accounts`
Purpose: enumerate configured SMTP accounts (non-secret metadata only).
Inputs:
- `account_id` (optional; if present, validate it exists and return only that account)
Output `data` (indicative):
- `accounts[]`: `{ account_id, default_from?, host?, port?, secure? }`
#### `mail_smtp_verify_account`
Purpose: check config + connectivity/auth (no email sent).
Inputs:
- `account_id` (default `"default"`)
Output `data` (indicative):
- `account_id`
- `status`: `"ok" | "failed"`
- `details?`: library-dependent diagnostics (keep minimal and non-sensitive)
- `error?`: actionable error message (no secrets)
#### `mail_smtp_send_message`
Purpose: send an outbound email (text and/or HTML), optionally with small attachments.
Sending behavior:
- `dry_run=true` validates inputs, policy, and size estimates without sending.
- `dry_run=false` sends the message if (and only if) `MAIL_SMTP_SEND_ENABLED=true`.
- No `confirm` field is used for sending; safety relies on the send gate + policy/limits.
Inputs (flat; bounds enforced):
- `account_id` (default `"default"`)
- `from` (optional; if omitted, use configured default; otherwise must pass policy)
- `to` (required; string or string[]; max N)
- `cc` (optional; string or string[]; max N)
- `bcc` (optional; string or string[]; max N)
- `reply_to` (optional)
- `subject` (required; max 256; reject CR/LF)
- `text_body` (optional; max chars)
- `html_body` (optional; max chars)
- `attachments` (optional; max count; each has strict bounds):
- `filename` (required; max 256; reject path traversal patterns)
- `content_base64` (required; bounded total bytes after decode)
- `content_type` (optional; max 128)
- `dry_run` (optional boolean; default false)
Output `data` (indicative):
- `account_id`
- `dry_run` boolean
- `envelope`: `{ from, to[], cc[]?, bcc[]? }`
- `message_id?` (if available from transport)
- `accepted[]`, `rejected[]` (if available from transport)
- `size_bytes_estimate?` (especially useful for `dry_run`)
## 8) Data Model (conceptual)
Use strongly-typed internal models (TypeScript types/Zod schemas) for:
- `AccountConfig` (host/port/secure/user/pass/default_from + policy) stored in env, never emitted
- `RecipientList` (normalized and validated addresses)
- `SendRequest` (validated subject/bodies, attachment metadata)
- `SendResult` (accepted/rejected, message id, policy decisions)
## 9) Security & Privacy
Non-negotiables:
- Never include passwords, OAuth tokens, cookies, or other credentials in tool outputs.
- Scrub secrets from audit logs (redact keys like `password`, `pass`, `token`, `secret`,
`authorization`, `cookie`, `key`).
- Enforce sending enablement:
- Default `MAIL_SMTP_SEND_ENABLED=false`
- `mail_smtp_send_message` rejects when disabled (even for valid payloads)
- `dry_run=true` is permitted when send is disabled
- Prevent header injection:
- Reject CR/LF in `subject`, recipient fields, and any header-like inputs
- Do not accept arbitrary raw headers in v1
- Treat HTML as untrusted input:
- Do not execute/sanitize HTML for sending purposes; pass through as content
- Ensure logs and outputs never include full body/HTML unless explicitly needed (prefer size
summaries)
Recipient policy:
- If no allowlist is configured, allow all outbound (subject to limits).
- If configured, enforce optional allowlists:
- `MAIL_SMTP_ALLOWLIST_DOMAINS` (comma-separated)
- `MAIL_SMTP_ALLOWLIST_ADDRESSES` (comma-separated; optional)
Limits (defaults decided in Phase 0; enforce hard maximums in schema):
- Maximum recipients per message (combined `to+cc+bcc`)
- Maximum message size / body size
- Maximum attachments count and size (and decoded bytes)
- Optional rate limiting (messages per minute) if running beyond a single-user local environment
## 10) Observability & Operations
- Structured logs for each tool invocation (tool name, duration, success/failure).
- Audit log (scrubbed args) for every tool call; include policy decisions (e.g., “blocked by
allowlist”).
- Optional capabilities surface in `_meta` (e.g., `send_enabled`, policy summary, limits) without
secrets.
## 11) Error Handling
Return specific, user-actionable errors:
- Account not configured (list missing env vars with the right prefix).
- Authentication failed / connection timeout / TLS negotiation failure.
- Recipient blocked by policy (include which recipient or domain was blocked).
- Invalid address format.
- Attachments too large / invalid base64 / too many attachments.
Avoid generic “500” failures; include enough context to remediate without leaking sensitive data.
## 12) Testing Strategy
Minimum coverage per tool:
- Valid input → expected result shape and summary text.
- Invalid input (schema violations) → `isError: true` with actionable message.
- Send-gate enforcement (`MAIL_SMTP_SEND_ENABLED`).
- `dry_run` behavior (validates without sending).
- Recipient allowlist policy (allowed vs blocked).
- Attachment limits (count, total bytes, per-attachment bytes).
- Token efficiency checks (responses remain concise under configured thresholds).
Optional integration testing:
- Use a local test SMTP server (e.g., MailHog/Mailpit) in CI to verify end-to-end send and `message_id`
behavior without hitting real providers.
## 13) Versioning & Compatibility
- Use semantic versioning `MAJOR.MINOR.PATCH`.
- Tool names and required fields are the contract.
- Deprecate before removal; document migrations for schema changes.
## Appendix A) Environment Variables (proposed)
Account configuration (per account `DEFAULT`/`WORK`/etc):
- `MAIL_SMTP_DEFAULT_HOST`
- `MAIL_SMTP_DEFAULT_PORT` (default `587` or `465` depending on `SECURE`)
- `MAIL_SMTP_DEFAULT_SECURE` (default `false` for 587 STARTTLS; `true` for 465 implicit TLS)
- `MAIL_SMTP_DEFAULT_USER`
- `MAIL_SMTP_DEFAULT_PASS`
- `MAIL_SMTP_DEFAULT_FROM` (recommended)
Multiple accounts are supported by replacing `DEFAULT` with an uppercase account ID:
- `MAIL_SMTP_WORK_HOST`
- `MAIL_SMTP_WORK_USER`
- `MAIL_SMTP_WORK_PASS`
- `MAIL_SMTP_WORK_FROM`
Global controls:
- `MAIL_SMTP_SEND_ENABLED` (default `false`)
- `MAIL_SMTP_ALLOWLIST_DOMAINS` (comma-separated; optional)
- `MAIL_SMTP_ALLOWLIST_ADDRESSES` (comma-separated; optional)
- `MAIL_SMTP_MAX_RECIPIENTS` (default e.g., `10`)
- `MAIL_SMTP_MAX_MESSAGE_BYTES` (default e.g., `2_500_000`)
- `MAIL_SMTP_MAX_ATTACHMENT_BYTES` (default e.g., `2_000_000`)
- `MAIL_SMTP_MAX_ATTACHMENTS` (default e.g., `5`)
- `MAIL_SMTP_RATE_LIMIT_PER_MIN` (optional)
Timeouts:
- `MAIL_SMTP_CONNECT_TIMEOUT_MS`
- `MAIL_SMTP_SOCKET_TIMEOUT_MS`