Mail MCP

Mail MCP

MCP server for any IMAP mailbox that lets AI models read, search, send, and manage email, including processing attachments as images.

Category
Visit Server

README

Mail MCP

An MCP server for any IMAP mailbox — Gmail, Outlook, iCloud, Fastmail, Yandex, your company's own server. Not tied to a provider, an account or a project: it is configured with the host, the address and the password, and that is the whole binding.

20 tools covering reading, searching, threading, sending, replying, forwarding, drafts, flags, folders and attachments.

The headline feature: the server reads attachments. A screenshot mailed to you is downloaded and handed to the model as an image — not as a filename it cannot open.


Two ways to run it

Mode Transport Configuration comes from Use it when
Hosted Streamable HTTP on /mcp request headers, per call clients should connect to a URL with nothing installed
Local stdio environment variables the mailbox is private, or credentials must not leave the machine

The public deployment lives at https://mail-mcp.webapace.ink — the landing page on /, the MCP endpoint on /mcp. Connecting to it needs no install:

claude mcp add --transport http mail \
  https://mail-mcp.webapace.ink/mcp \
  --header "X-Mail-User: you@fastmail.com" \
  --header "X-Mail-Password: your-app-password"

Or write the configuration yourself. The same object goes into .mcp.json in a repository, ~/.claude.json, claude_desktop_config.json or .cursor/mcp.json — fill in the blanks:

{
  "mcpServers": {
    "mail": {
      "type": "http",
      "url": "https://mail-mcp.webapace.ink/mcp",
      "headers": {
        "X-Mail-User": "",
        "X-Mail-Password": "",
        "X-Mail-Imap-Host": "",
        "X-Mail-Smtp-Host": ""
      }
    }
  }
}
Header Fill in with If left empty
X-Mail-User the address you sign in with the request is refused — this one is required
X-Mail-Password the app-specific password from your provider the request is refused — this one is required too
X-Mail-Imap-Host imap.example.com, for a mailbox with no preset derived from your address, when its domain is one of the known providers
X-Mail-Smtp-Host smtp.example.com, likewise derived from your address; without either, sending is unavailable

X-Mail-Read-Only, X-Mail-Allowed-Recipients and X-Mail-Allowed-Folders can be added the same way; see Hosted mode for the full header list.

Keep the file out of version control, or write "X-Mail-Password": "${MAIL_PASSWORD}" — Claude Code and Cursor substitute environment variables, so the secret stays in your shell.

One thing to weigh. Unlike a project-scoped API token, a mailbox password opens the whole mailbox, and on the hosted endpoint it passes through the host on every request. Nothing is stored there, and the guard rails below narrow what can be done with it — but if that trade is not acceptable, run it locally; it is a few commands and everything behaves identically.

Several mailboxes at once — several entries with different names:

{
  "mcpServers": {
    "mail-work":     { "type": "http", "url": "https://mail-mcp.webapace.ink/mcp", "headers": { "X-Mail-User": "me@company.com",  "X-Mail-Password": "…" } },
    "mail-personal": { "type": "http", "url": "https://mail-mcp.webapace.ink/mcp", "headers": { "X-Mail-User": "me@fastmail.com", "X-Mail-Password": "…" } }
  }
}

Two things worth knowing when a change appears to do nothing:

  • In Claude Code an entry added to the local scope (kept in ~/.claude.json) takes precedence over the project's .mcp.json. claude mcp list shows what is actually in use, and claude mcp remove <name> -s local drops a stale one.
  • Client configuration is read at startup, so restart the app after editing the file.

The rest of this document is for running your own copy in either mode.


Running it locally

git clone git@github.com:amalychev/mail-mcp.git
cd mail-mcp
npm install
npm run build

Requires Node.js 20 or newer. Then point your assistant at the built file, with the configuration as environment variables instead of headers:

claude mcp add mail \
  --env MAIL_USER=you@fastmail.com \
  --env MAIL_PASSWORD=your-app-password \
  -- node /absolute/path/mail-mcp/dist/index.js

That is the whole setup for a known provider: the IMAP and SMTP hosts are derived from the address. Nothing listens on the network in this mode, and the credentials never leave the machine. Ask for mail_whoami to confirm — it logs in to both servers and reports what it found.

Or in a file, which travels with the project:

{
  "mcpServers": {
    "mail": {
      "command": "node",
      "args": ["/absolute/path/mail-mcp/dist/index.js"],
      "env": {
        "MAIL_USER": "you@example.com",
        "MAIL_PASSWORD": "${MAIL_PASSWORD}",
        "IMAP_HOST": "imap.example.com",
        "SMTP_HOST": "smtp.example.com"
      }
    }
  }
}

The same block works in Claude Desktop (claude_desktop_config.json) and Cursor (.cursor/mcp.json).


The password

Nearly every provider refuses your account password over IMAP and wants an app-specific password — a separate value you generate once, use here, and can revoke without touching the account itself.

Provider Where to get it
Gmail / Workspace Turn on 2-Step Verification, then App passwords. Workspace accounts also need IMAP enabled by an administrator
iCloud appleid.apple.com → Sign-In and Security → App-Specific Passwords. The login is often the part of the address before @
Yahoo / AOL Account security → Generate app password
Fastmail Settings → Privacy & Security → App passwords
Yandex / Mail.ru Security → app passwords, and IMAP switched on under mail-client settings
Zoho Security → App passwords, plus IMAP access in Mail Settings
Outlook.com / Microsoft 365 Usually OAuth only — see below
Proton Install Proton Bridge and use the credentials it shows, not your Proton password

Microsoft has switched personal accounts and most tenants to OAuth, so a password often fails outright. Supply an access token as MAIL_PASSWORD together with MAIL_AUTH_TYPE=oauth2; the same setting works for Gmail's OAuth if you prefer it to an app password.

When authentication fails, the error names your provider and says what it wants — the message is not a generic "login failed".


Configuration

In stdio mode everything is configured through environment variables.

Variable Required Default Description
MAIL_USER yes — The address (or login name) you sign in with
MAIL_PASSWORD yes — App-specific password, or an OAuth token with MAIL_AUTH_TYPE=oauth2
MAIL_PROVIDER no detected Preset that fills in all four host settings
IMAP_HOST / IMAP_PORT no from preset Incoming server. A host pasted with :993 or a scheme is cleaned up
IMAP_SECURE no from port true for implicit TLS, false for STARTTLS
SMTP_HOST / SMTP_PORT / SMTP_SECURE no from preset Outgoing server. Omit entirely for a read-only mailbox
SMTP_USER / SMTP_PASSWORD no the IMAP ones When the outgoing server wants separate credentials
MAIL_FROM / MAIL_FROM_NAME no MAIL_USER The From header. Required when the login is not an address
MAIL_AUTH_TYPE no password oauth2 — treat the password as an access token
MAIL_DEFAULT_FOLDER no INBOX Folder used when a tool does not name one
MAIL_READ_ONLY no false true — every tool that would change anything is refused
MAIL_ALLOWED_RECIPIENTS no — Comma-separated addresses or @domains; mail elsewhere is refused
MAIL_ALLOWED_FOLDERS no — Comma-separated folders; everything else is out of scope
MAIL_APPEND_TO_SENT no true false — do not file a copy of sent mail
MAIL_TLS_REJECT_UNAUTHORIZED no true false — accept a self-signed certificate
MAIL_TIMEOUT_MS no 60000 Per-connection timeout
MAIL_MAX_ATTACHMENT_BYTES no 8388608 Largest attachment read or sent
MAIL_MAX_BODY_CHARS no 40000 Bodies longer than this are truncated
MAIL_ALLOW_LOCAL_FILES no true false — refuse to attach files from disk

MAIL_ADDRESS, MAIL_PASS, IMAP_USER, IMAP_PASSWORD, MAIL_IMAP_HOST and MAIL_SMTP_HOST are accepted as aliases.

Providers with a preset

gmail, outlook, yahoo, icloud, fastmail, zoho, yandex, mailru, gmx, aol, migadu, mailbox, proton — each fills in the IMAP and SMTP host, port and transport. Aliases such as google, hotmail, office365 and protonmail resolve to the right one, and an address ending in a domain the preset owns selects it with no configuration at all.

Anything else is a normal IMAP server: give the two hosts and it works.


What the tools do

Messages are addressed by UID within a folder. A UID is only meaningful together with the folder it came from, and a message that moves gets a new one — every tool that returns messages returns the folder alongside them.

Folders accept aliases: inbox, sent, drafts, trash, junk, archive resolve through the server's own special-use flags, so they work whether the account calls it Sent, [Gmail]/Sent Mail or Отправленные.

Reading

Tool Description
mail_list_messages Newest messages in a folder, with an optional preview of each
mail_search Sender, recipient, subject, body, date range, read state, size, arbitrary headers
mail_read_message One message: headers, readable body, attachment list
mail_read_thread The whole conversation, oldest first, including your own replies from Sent
mail_get_raw_message The raw source — Received chains, SPF/DKIM, list headers

mail_read_message fetches only the body part, so a message carrying a 20 MB attachment still costs a few kilobytes to read. HTML-only mail is flattened to prose with the links kept. Reading never marks anything as read unless you pass mark_read: true.

Attachments

Tool Description
mail_list_attachments What is attached, with the part ids needed to fetch it
mail_read_attachment Downloads one: images come back as images, text as text, the rest as a base64 resource

Writing

Tool Description
mail_send A new message, with attachments from disk or from base64
mail_reply Threads properly: In-Reply-To and References are carried over, reply_all optional
mail_forward With the usual header block and, unless disabled, the original attachments
mail_save_draft Writes into Drafts without sending — for text a person should review first

Every sent message is filed in the Sent folder, and the copy is the exact bytes that were sent rather than a re-composed lookalike. The From address comes from the deployment and cannot be overridden by a tool argument.

Organising

Tool Description
mail_set_flags Read/unread, flagged, answered, custom keywords, several messages at once
mail_mark_search_read Marks everything a search matches, with a ceiling so a broad query cannot run away
mail_move_messages / mail_copy_messages File mail into folders
mail_delete_messages Moves to Trash; permanent: true needs confirm: true as well
mail_list_folders / mail_folder_status The folder tree, with counts on request
mail_folder_action Create, rename, delete, subscribe, unsubscribe
mail_whoami Logs in to both servers and reports account, quota and restrictions

Keeping it inside bounds

An assistant with a mailbox password can, in principle, read everything and write to anyone. Three independent limits narrow that, and they stack:

Read-only. MAIL_READ_ONLY=true refuses every tool that sends, moves, flags or deletes. Reading still works completely.

A recipient allowlist. MAIL_ALLOWED_RECIPIENTS=@yourcompany.com,partner@client.com checks every recipient — To, Cc and Bcc together — before anything reaches the SMTP server. This is the one worth setting even when everything else is open: it turns "the assistant could email anyone" into "the assistant could email these people".

A folder allowlist. MAIL_ALLOWED_FOLDERS=INBOX,INBOX/Clients puts every other folder out of reach, for reading as well as writing. A listed folder covers its subfolders. The "no such folder" error is filtered to the allowed set, so it cannot be used to map the account.

Beyond that: deleting is a move to Trash unless permanent: true and confirm: true are both given, deleting a folder needs confirm: true, and an app-specific password can be revoked in the provider's interface without touching the account.


Hosted mode

Start the HTTP transport with MCP_TRANSPORT=http (or --http). It serves:

Route Purpose
GET / the landing page (landing.html, or LANDING_PATH)
POST /mcp the MCP endpoint, stateless — one server instance per request
GET /health liveness probe
GET /robots.txt, /sitemap.xml generated per request from the Host header
GET /favicon.ico, /favicon.svg, /apple-touch-icon.png, /icon-192.png, /icon-512.png, /og-image.png, /site.webmanifest static files from assets/ (or ASSETS_PATH), cached for a week

Every request carries its own credentials, so one deployment serves many mailboxes without holding state:

Header Maps to
X-Mail-User / X-Mail-Password MAIL_USER / MAIL_PASSWORD — Authorization: Basic is accepted instead
X-Mail-Provider MAIL_PROVIDER
X-Mail-Imap-Host, X-Mail-Imap-Port, X-Mail-Imap-Secure the incoming server
X-Mail-Smtp-Host, X-Mail-Smtp-Port, X-Mail-Smtp-Secure the outgoing server
X-Mail-From, X-Mail-From-Name the From header
X-Mail-Default-Folder MAIL_DEFAULT_FOLDER
X-Mail-Read-Only, X-Mail-Allowed-Recipients, X-Mail-Allowed-Folders the guard rails above
X-Mail-Auth-Type oauth2 to send a token instead of a password

TLS verification, timeouts and the size caps are deliberately not header-controlled: they are process-wide and belong to whoever runs the deployment. Attaching files from disk is forced off, because in hosted mode "the local disk" is the server's, not the caller's.

Without an allowlist the server refuses private addresses (localhost, RFC 1918 ranges, 169.254.*, *.internal, *.local) so a public deployment cannot be used to probe the network it runs in. MAIL_ALLOWED_HOSTS=imap.example.com,smtp.example.com narrows it to named hosts.

docker build -t mail-mcp .
docker run -p 8080:8080 -e MAIL_ALLOWED_HOSTS=imap.example.com,smtp.example.com mail-mcp

What hosting costs you. The mailbox password passes through the host on every request. The server keeps nothing — no accounts, no database, nothing written to disk, one throwaway server instance per request — but a mailbox password is not a scoped token, and no amount of "we store nothing" changes that. Run it yourself, or run it locally over stdio, whenever that matters.

Connection reuse

Logging in to IMAP costs a TLS handshake and a round trip, and providers rate-limit repeated logins hard. Authenticated connections are therefore pooled per mailbox and kept warm for five idle minutes, shared across requests in hosted mode. Work on one connection is serialised, because a single IMAP connection has one selected folder at a time.

Deploying

deploy.sh does the whole cycle on the server — pull, build, swap the container, verify, purge the CDN cache:

./deploy.sh                # the usual deploy
./deploy.sh --page-only    # only replace landing.html in the running container, no rebuild
./deploy.sh --no-pull      # deploy the working tree as it is
./deploy.sh --logs         # follow the container log afterwards

Copy deploy.env.example to deploy.env on the server and set the port, container name, PUBLIC_URL and, if the site sits behind Cloudflare, a zone id and an API token with the Cache Purge permission. deploy.env is git-ignored, so server-specific values stay there.

The previous image is tagged :previous before every build, and a failed health check restores it automatically and exits non-zero, so a broken build never stays deployed.

Landing page assets

assets/ holds everything the page references: favicon.svg (the source of every raster icon), favicon.ico, the touch and PWA icons, site.webmanifest, and og-image.png — the 1200×630 preview, rendered from assets/og-card.html.

The PNG and ICO files are committed, so a normal build needs nothing extra. Regenerate them only after editing favicon.svg or og-card.html:

npm run assets      # headless Chrome does the rasterising; set CHROME=… if it is not found

Development

npm run build       # compile
npm run dev         # tsc --watch
npm run typecheck   # types only
npm run start       # stdio mode
npm run start:http  # hosted mode on PORT (default 8080)

Layout:

src/
  index.ts          entry point, picks stdio or HTTP
  server.ts         builds a configured MCP server with every tool registered
  http.ts           hosted mode: landing page, stateless /mcp, per-request credentials
  config.ts         configuration from environment or headers
  providers.ts      host presets for the common mail services
  mail-client.ts    IMAP connection pool, folder resolution, bodies, attachments, SMTP
  mime.ts           charsets, HTML to text, addresses, quoting, subjects
  dates.ts          date arguments: ISO, `7d`, `yesterday`
  tools/
    helpers.ts      shared argument schemas, result formatting, the read-only guard
    core.ts         mail_whoami
    folders.ts      folder tree and management
    messages.ts     listing, search, reading, threads, raw source
    attachments.ts  attachment listing and download
    compose.ts      send, reply, forward, drafts
    actions.ts      flags, moving, copying, deleting

To add a tool, call defineTool in the appropriate module: error handling and the read-only guard are wired in automatically.


Troubleshooting

Symptom Cause
IMAP rejected the credentials Nearly always the account password where an app-specific one is needed, or IMAP not enabled in the provider's web interface. The message names your provider's requirement
Cannot reach the SMTP server Submission is port 587 with STARTTLS or 465 with TLS; port 25 is blocked by most networks
The server refused the message (550/553) The account is not allowed to send as the configured From address
Connect hangs, then times out A TLS port configured as STARTTLS or the reverse. Set IMAP_SECURE / SMTP_SECURE explicitly for a non-standard port
No folder X in this account The error lists the real folder names — providers localise them, and Gmail nests them under [Gmail]
No message with UID N UIDs belong to one folder. A message that was moved has a different UID there
certificate errors For an internal server with its own CA, set MAIL_TLS_REJECT_UNAUTHORIZED=false
Refused: … MAIL_READ_ONLY / MAIL_ALLOWED_* Working as intended: a guard rail stopped it
Server will not start Logs go to stderr with the [mail-mcp] prefix; usually MAIL_USER or MAIL_PASSWORD is missing

A visual walkthrough with examples lives in landing.html at the repository root.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured