Skip to main content
Glama
amalychev

Mail MCP

by amalychev

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

Local

stdio

environment variables

the default for mail — credentials never leave the machine

Hosted

Streamable HTTP on /mcp

request headers, per call

clients should connect to a URL with nothing installed

Mail is not a scoped API token: the password usually opens the whole mailbox, and often the account behind it. Running locally is therefore the recommended mode, and the hosted endpoint exists for the cases where that is not practical — see Hosted mode for what that choice actually costs.

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

Requires Node.js 20 or newer.

Connecting from Claude Code

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. Ask for mail_whoami to confirm — it logs in to both servers and reports what it found.

Or drop a .mcp.json into the root of your working repository:

{
  "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). Write "${MAIL_PASSWORD}" rather than the password itself — Claude Code and Cursor substitute environment variables, so the file stays shareable and the secret stays in your shell.

Several mailboxes at once — several entries with different names:

{
  "mcpServers": {
    "mail-work":     { "command": "node", "args": ["…/dist/index.js"], "env": { "MAIL_USER": "me@company.com", "MAIL_PASSWORD": "…" } },
    "mail-personal": { "command": "node", "args": ["…/dist/index.js"], "env": { "MAIL_USER": "me@fastmail.com", "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.


Related MCP server: io.github.p-w-4-z/inbox-mcp

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_PASSWORDAuthorization: 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.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    D
    maintenance
    An MCP server that enables AI models to read, search, and send emails via IMAP and SMTP protocols. It supports various providers like Gmail and Outlook, allowing for tasks such as retrieving unread messages, searching by sender, and managing mailbox folders.
  • A
    license
    A
    quality
    D
    maintenance
    Provider-agnostic email MCP server that connects any IMAP mailbox to AI assistants, enabling email management through natural language.
    8
    AGPL 3.0
  • A
    license
    -
    quality
    B
    maintenance
    An MCP server that receives emails on your domain and allows AI assistants to search, read, and manage them via natural language queries.
    1,200
    MIT
  • A
    license
    -
    quality
    D
    maintenance
    An MCP server that gives AI assistants comprehensive access to Apple Mail accounts, enabling email discovery, reading, flag management, and server-side message retrieval.
    MIT

View all related MCP servers

Related MCP Connectors

  • Shipmail MCP server for AI agent custom-domain email inboxes with REST API and webhooks.

  • Hosted email MCP for AI agents with inboxes, send/receive, memory, recovery, and credits.

  • Read, search, send, organize, draft and schedule email across your inboxes from any MCP client.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/amalychev/mail-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server