tiny MCP to let Claude send mail over JMAP
  • JavaScript 97.3%
  • Nix 2.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Kate Meerburg f475a6bc85 add attachment sending: paths, inline base64, blob reuse, one-per-email fan-out
send_email now takes an attachments array ({path} read server-side,
{data,name} inline base64, or {blobId,...} to re-send an existing blob
without re-uploading) plus oneEmailPerAttachment, which duplicates the
message once per attachment for one-document-per-email processing queues.

Path reads can be confined with FASTMAIL_ATTACHMENT_ROOTS; uploads are
capped by the session's maxSizeUpload. Also fixed the stdin-close handler
to let in-flight requests flush their replies before exiting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:14:33 +02:00
fastmail-send-mcp.mjs add attachment sending: paths, inline base64, blob reuse, one-per-email fan-out 2026-07-13 18:14:33 +02:00
flake.lock initial commit 2026-07-12 18:40:38 +02:00
flake.nix initial commit 2026-07-12 18:40:38 +02:00
README.md add attachment sending: paths, inline base64, blob reuse, one-per-email fan-out 2026-07-13 18:14:33 +02:00

fastmail-send-mcp

A tiny stdio MCP server that fills the gaps in the official Fastmail MCP: send_email sends mail immediately (the official one deliberately only drafts), and list_attachments/get_attachment retrieve attachment payloads (the official read_email stops at body text).

It also carries its own search_email and read_email: email ids from the official Fastmail MCP are not usable here (they don't resolve against this server's JMAP session), so attachment retrieval starts from this server's own search results instead.

Zero dependencies — plain Node (18+, needs global fetch), newline-delimited JSON-RPC over stdin/stdout.

Auth

The server reads FASTMAIL_API_TOKEN from its environment; it never handles secrets itself, on the assumption that an MCP proxy injects the token. Create the token at Fastmail → Settings → Privacy & Security → API tokens, with the Email and Email Submission scopes.

Optional: FASTMAIL_SESSION_URL overrides the JMAP session endpoint (defaults to https://api.fastmail.com/jmap/session), FASTMAIL_MAX_ATTACHMENT_BYTES caps how large an attachment may be returned inline (default 10 MiB), and FASTMAIL_ATTACHMENT_ROOTS (colon-separated directories) confines which paths send_email may attach from. Leaving the latter unset allows any file the server process can read — note that anything readable then becomes mailable, so set it on machines with other tenants.

Running

FASTMAIL_API_TOKEN=fmu1-... node fastmail-send-mcp.mjs

Example proxy/client config:

{
  "mcpServers": {
    "fastmail-send": {
      "command": "node",
      "args": ["/home/deprekated/Projects/home/fastmail-send-extension/fastmail-send-mcp.mjs"],
      "env": { "FASTMAIL_API_TOKEN": "<injected by proxy>" }
    }
  }
}

The tools

send_email arguments:

field required notes
to yes array of a@b.com or Name <a@b.com>
subject yes
body one of plain-text body
html one of HTML body; may be combined with body for multipart/alternative
cc, bcc no same format as to
from no must match one of the account's identities; defaults to the primary identity
inReplyTo no Message-ID being replied to, for threading
attachments no array of attachment specs, see below
oneEmailPerAttachment no fan a batch of attachments out as individual emails

On success the sent message is filed into the Sent mailbox with $draft cleared. Failures (bad identity, rejected submission, API errors) come back as tool errors with the JMAP error type and description.

Attachments

Each entry in attachments is one of:

  • { "path": "/abs/path/file.pdf" } — read from the filesystem of the machine running this server (not the MCP client's). name defaults to the basename and type is guessed from the extension; both can be overridden. Subject to FASTMAIL_ATTACHMENT_ROOTS if set.
  • { "data": "<base64>", "name": "file.pdf" } — inline payload; name is required, type guessed from it if omitted.
  • { "blobId": "...", "name": "file.pdf", "type": "application/pdf" } — an existing JMAP blob from this server's list_attachments/get_attachment, re-sent without re-uploading (i.e. forwarding an attachment).

Uploads are capped by the JMAP server's advertised maxSizeUpload (~50 MB on Fastmail).

With oneEmailPerAttachment: true, one copy of the email is sent per attachment — identical recipients, subject, and body, each carrying exactly one attachment. This suits processing queues that expect one document per message. Copies go out sequentially; on failure the error reports which copies were already sent, so retries can resend just the remainder.

search_email filters by any combination of query (full-text), from, to, subject, mailbox (role or name, e.g. inbox), after/before (UTC dates), and hasAttachment, returning up to limit (default 20, max 100) results sorted newest-first, each with its JMAP id, addresses, subject, date, preview, and hasAttachment flag.

read_email takes an emailId from search_email and returns full headers (including messageId, handy as inReplyTo for threaded replies via send_email), the body (plain text preferred, raw HTML with bodyType: "html" for HTML-only mail, truncated past 256 KiB per part), and attachment metadata.

list_attachments takes an emailId (from search_email) and returns each attachment's name, MIME type, size, index, and blobId.

get_attachment takes an emailId plus — when the email has more than one attachment — a selector (name, index, or blobId), downloads the blob server-side, and returns {name, type, size, blobId, encoding: "base64", data}. Payloads are inlined as base64 because raw JMAP blob URLs require the bearer token, which clients behind the auth proxy don't hold. Attachments larger than the configured cap are refused rather than truncated.