hypermail-mcp
v0.7.8
Published
Unified email MCP server — operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.
Maintainers
Readme
hypermail-mcp
A Model Context Protocol server that lets an agent operate any of the user's inboxes through a single, unified tool surface.
v0.7.8 — Default Gmail loopback redirect URI changed from random-port
/oauth2callbackto fixedhttp://127.0.0.1:33333/callback(still overridable viaHYPERMAIL_GMAIL_REDIRECT_URI)..data/encryption key directory added to.gitignore.v0.7.7 — Env-only configuration. Runtime config now comes from flat
HYPERMAIL_*environment variables plus selected CLI overrides. Config files and legacy provider env names are no longer read. Hosted Gmail OAuth callbacks are supported viaHYPERMAIL_GMAIL_REDIRECT_URI; local loopback and manual completion still work.v0.7.6 — Gmail setup uses OAuth authorization URLs instead of Google's rejected device-code flow for Gmail API scopes.
complete_add_accountaccepts a final redirected URL or rawcode/state, and provider credentials use dedicatedHYPERMAIL_GMAIL_*/HYPERMAIL_OUTLOOK_*env vars.v0.7.5 — Attachments via file path on
send_email/draft_email(attachmentsparam).edit_draftgainsnew_attachmentsandremove_attachments—add_attachment_to_draftis removed (23 tools now). Draft editing uses multi-strategy thread boundary detection for more reliable quoted-thread preservation. Watcher now supports shell-command notification alongside webhook delivery. Published CLI installs the MCP SDK dependency so global/npx runs do not fail on a missing SDK module.v0.7.4 —
inReplyTois now a required parameter onsend_emailanddraft_email(was optional). Set it tofalsefor a new email, or pass a message ID to thread a reply. This forces the agent to make an explicit choice instead of silently treating replies as new conversations.v0.7.3 —
edit_draftnow preserves the quoted thread history when editing Outlook reply/forward drafts. Previously, editing a draft body would overwrite the entire content — including the quoted thread. Now only the answer part (above the spacer delimiter) is replaced.v0.7.1 — Every config field is now settable via a dedicated
HYPERMAIL_*env var. Legacy provider env vars are no longer accepted. See Environment Variables for the full reference.v0.7.0 — Email watch mode: background poll loop detects new inbox messages and POSTs them to a configurable webhook URL (e.g. Mastra). Opt-in — disabled by default, enabled via
HYPERMAIL_WATCH_ENABLED=true. Works in both stdio and HTTP transport modes.v0.6.3 — Unify stdio and HTTP modes into a single feature set. Removed email watch (inbox polling, SSE push, notification buffer), agent multi-tenancy (
agents.yaml,x-api-keyauth, per-agent allowlists), and thecheck_notificationstool. Droppedjs-yamldependency. Dockerfile simplified to a singleinstall → build → prunestep.v0.6.2 — Version source-of-truth fix:
version.tsnow imports directly frompackage.jsoninstead of hardcoding, preventing version drift between the two files.v0.6.1 — Docker deployment (standalone Dockerfile with HEALTHCHECK), email notification bug fixes (ID-based dedup, pagination cap, dynamic re-scan), Node 22 base image, dropped docker-compose.
v0.5.0 — Replaced optional
isHtmlboolean with requiredformatparameter ("html"|"markdown") onsend_email,draft_email, andedit_draft. Markdown bodies are converted to HTML viamarkedso recipients always see clean HTML.v0.4.3 — Upgraded Zod to v4.4.3. Fixed MCP SDK v1.29.0 compatibility by wrapping all tool schemas in
z.object()and replacing discriminated union output schemas that causedvalidateToolOutputcrashes.
The agent doesn't care whether an address is a work Outlook account, a personal
Microsoft account, a personal IMAP mailbox, or Gmail — it just calls
list_emails, search_emails, read_email, send_email and passes the email
address as the account argument. The server routes to the right backend.
v1 status: Outlook / Microsoft 365 (personal + work) fully supported via
Microsoft Graph. IMAP (any IMAP server) supported via imapflow + nodemailer.
Gmail supported via Google OAuth authorization-code flow with local loopback or
hosted callbacks plus remote-safe manual completion.
Why
- Existing Outlook/M365 MCP servers (e.g.
@softeria/ms-365-mcp-server) expose ~200 raw Graph endpoints and are tied to a single signed-in user. - This project wraps the same proven stack (
@azure/msal-nodefor auth,@microsoft/microsoft-graph-clientfor HTTP) but exposes only a small, provider-agnostic email API and supports multiple accounts at once, keyed by email address.
Install / run
npm install -g hypermail-mcp # or pnpm / npx
hypermail-mcp --helpRun as a stdio MCP server (the default) — wire it into your MCP host:
Claude Desktop / Claude Code
{
"mcpServers": {
"hyper-email": {
"command": "npx",
"args": ["-y", "hypermail-mcp"]
}
}
}Or via the CLI:
claude mcp add hypermail -- npx -y hypermail-mcpAs a hosted HTTP server
hypermail-mcp --http --port 3000 --host 0.0.0.0
# endpoint: http://<host>:3000/mcp (Streamable HTTP transport, session-aware)When hosted, set HYPERMAIL_KEY so the account file is reproducibly
decryptable across restarts and redeploys.
Docker
# Build
docker build -t hypermail-mcp .
# Run
# Pass secret values from your shell or deployment environment; do not commit them.
docker run -d \
--name hypermail-mcp \
-p 3000:3000 \
-e HYPERMAIL_KEY \
-e HYPERMAIL_OUTLOOK_CLIENT_ID \
-e HYPERMAIL_OUTLOOK_TENANT_ID \
-v hypermail-data:/var/lib/mcp \
hypermail-mcpThe image runs the server in HTTP mode on port 3000 with a 30-second
HEALTHCHECK against /mcp. Data is persisted via a Docker volume at
/var/lib/mcp.
Development
To test the HTTP server locally:
# Terminal 1: auto-rebuild TypeScript on save
pnpm dev
# Terminal 2: start HTTP server with env/CLI config
pnpm dev:httpThe server listens on http://127.0.0.1:3000/mcp.
Runtime and provider configuration
Hypermail uses flat HYPERMAIL_* environment variables as the source of truth.
There is no runtime config file. CLI flags only override transport, host, port,
and data directory for a single invocation.
CLI flags: --http, --port, --host, --data-dir, --help.
Subcommands: hypermail-mcp generate-key — generate a base64 32-byte key for
HYPERMAIL_KEY.
Local CLI / env example
export HYPERMAIL_KEY="$(hypermail-mcp generate-key)"
export HYPERMAIL_DATA_DIR="$HOME/.local/share/hypermail-mcp"
export HYPERMAIL_OUTLOOK_CLIENT_ID="<your-client-id>"
hypermail-mcpGeneric MCP client JSON example
{
"mcpServers": {
"hypermail": {
"command": "npx",
"args": ["-y", "hypermail-mcp"],
"env": {
"HYPERMAIL_KEY": "${HYPERMAIL_KEY}",
"HYPERMAIL_DATA_DIR": "${HYPERMAIL_DATA_DIR}",
"HYPERMAIL_OUTLOOK_CLIENT_ID": "${HYPERMAIL_OUTLOOK_CLIENT_ID}"
}
}
}
}Environment Variables
| Env var | Purpose | Default / behavior |
| --- | --- | --- |
| HYPERMAIL_DATA_DIR | Account/token store location | ${XDG_DATA_HOME:-~/.local/share}/hypermail-mcp |
| HYPERMAIL_KEY | 32-byte AES-256-GCM key as hex/base64, or any passphrase derived via SHA-256 | If unset, generates and persists a local key and prints a startup warning |
| HYPERMAIL_TRANSPORT | Runtime transport: stdio or http | stdio; --http overrides to http |
| HYPERMAIL_HTTP_PORT | HTTP bind port | 3000; invalid HTTP-mode values warn and fall back |
| HYPERMAIL_HTTP_HOST | HTTP bind host | 127.0.0.1; invalid HTTP-mode values warn and fall back |
| HYPERMAIL_OUTLOOK_CLIENT_ID | Optional custom Azure/Entra public client ID | Built-in public client |
| HYPERMAIL_OUTLOOK_TENANT_ID | Optional Outlook tenant/authority selector | common |
| HYPERMAIL_GMAIL_CLIENT_ID | Google OAuth client ID | Required when adding a Gmail account |
| HYPERMAIL_GMAIL_CLIENT_SECRET | Google OAuth client secret, when issued by the client type | unset |
| HYPERMAIL_GMAIL_REDIRECT_URI | Hosted Gmail OAuth callback URI | Local loopback callback when unset |
| HYPERMAIL_TOOLS_ENABLED | Comma-separated tool allowlist | Empty/unset means no filtering |
| HYPERMAIL_TOOLS_DISABLED | Comma-separated tool blocklist | Empty/unset means no filtering |
| HYPERMAIL_WATCH_ENABLED | Enable inbox polling: true or false | false |
| HYPERMAIL_WATCH_POLL_SECONDS | Watcher polling cadence | 10 |
| HYPERMAIL_WATCH_WEBHOOK_URL | Webhook delivery target | Required if watch is enabled and no notify command is set |
| HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS | Webhook retry attempts | 5 |
| HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS | Webhook exponential-backoff base delay | 1000 |
| HYPERMAIL_WATCH_NOTIFY_COMMAND | Shell command run with EmailFull JSON on stdin | Required if watch is enabled and no webhook is set |
| HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS | Notify-command execution timeout | 30000 |
| HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS | Notify-command retry attempts | 5 |
| HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS | Notify-command exponential-backoff base delay | 1000 |
Priority order: selected CLI flags > HYPERMAIL_* env vars > hardcoded defaults.
Per-tool filtering (HYPERMAIL_TOOLS_ENABLED / HYPERMAIL_TOOLS_DISABLED) lets
operators ship minimal agent-facing surfaces. If both non-empty lists are set,
or either list contains an unknown tool name, startup fails.
Tools
All "email" tools take an account argument — the email address of the inbox
to operate on. The server resolves the right provider from the encrypted
account store.
| Tool | Inputs | Notes |
| --- | --- | --- |
| list_accounts | — | Returns registered emails + provider, no secrets. |
| add_account | provider, email?, config? | Starts the provider add flow. Outlook returns a device code; Gmail returns an OAuth URL. Returns {handle, verification:{type, userCode, verificationUri, expiresAt, message}}. |
| complete_add_account | provider, handle, authorizationResponse?, code?, state? | Returns pending / ready / expired / error. Gmail accepts a pasted final redirected URL or raw code/state for remote-safe completion. |
| get_account_settings | account | Get signature (HTML) and style preferences for an account. |
| set_account_settings | account, signature?, signaturePath?, style? | Set signature HTML (inline or via file path) and font preferences. |
| remove_account | email | Deletes tokens for the account. |
| list_emails | account, folder?, limit?, unreadOnly?, skip? | Defaults: folder=inbox, limit=25. Supports pagination via skip — response includes hasMore. |
| search_emails | account, query, limit? | KQL on Outlook. |
| read_email | account, id, format? | Returns full body + recipients + attachment metadata. format: markdown (default), html, or text. |
| read_attachment | account, messageId, attachmentId | Download an attachment to a temporary file and return its path. |
| archive_email | account, id | Move a message to the Archive folder. |
| trash_email | account, id | Move a message to Deleted Items (trash). |
| move_email | account, id, destination | Move to any folder by well-known name (inbox, drafts, etc.) or custom folder ID. |
| send_email | account, to[], cc?, bcc?, subject, body, format, include_signature, inReplyTo, replyAll?, forwardMessageId?, attachments? | Send an email. format ("html" or "markdown") controls body format — Markdown is converted to HTML via marked. Appends signature when include_signature is true. inReplyTo sends as threaded reply; forwardMessageId sends as forward. inReplyTo is required — set to false for new emails. attachments is an optional array of {filePath, name?} — files are read from disk and encoded automatically. |
| draft_email | account, to[], cc?, bcc?, subject, body, format, include_signature, inReplyTo, replyAll?, forwardMessageId?, attachments? | Save as draft instead of sending. Same params as send_email including attachments. Returns the draft message ID and HTML body (draftHtml). inReplyTo is required — set to false for new emails. |
| edit_draft | account, id, to?, cc?, bcc?, subject?, body?, format?, include_signature?, new_attachments?, remove_attachments? | Edit an existing draft by ID. Only provided fields are updated. new_attachments adds files ({filePath, name?}[]); remove_attachments removes by attachment ID (string[]). Returns the updated draft ID, HTML body (draftHtml), and attachment metadata. |
| send_draft | account, id | Send an existing draft email by ID. Use with draft IDs returned by draft_email or edit_draft. |
| list_folders | account, parentFolderId? | List available mail folders. Returns top-level folders by default, or children of parentFolderId. |
| create_folder | account, displayName, parentFolderId? | Create a new mail folder under root (default) or the given parent. |
| delete_folder | account, folderId | Delete a mail folder by ID. |
| rename_folder | account, folderId, newName | Rename an existing mail folder. |
| mark_read | account, id | Mark a message as read. |
| mark_unread | account, id | Mark a message as unread. |
Email Watch
When enabled, hypermail-mcp runs a background poll loop that scans inboxes for new messages and delivers each one via webhook POST and/or shell command. Intended for push-based email triage — downstream agents receive full email content without polling.
HYPERMAIL_WATCH_ENABLED=true \
HYPERMAIL_WATCH_POLL_SECONDS=10 \
HYPERMAIL_WATCH_WEBHOOK_URL=http://localhost:3000/api/email-webhook \
HYPERMAIL_WATCH_NOTIFY_COMMAND='node /path/to/email-handler.js' \
hypermail-mcpValidation: If HYPERMAIL_WATCH_ENABLED=true, startup requires at least one
of HYPERMAIL_WATCH_WEBHOOK_URL or HYPERMAIL_WATCH_NOTIFY_COMMAND. Webhook
URLs are syntax-validated, and notify commands must be non-empty. Startup does
not test network reachability or execute the command.
Behavior:
- Polls all accounts in the store, inbox only.
- Detects new emails via
lastSeenIds(capped at 200) stored in the encrypted account file — no duplicate emits across restarts. - Two delivery modes (can be used together):
- Webhook: One
POSTper email (full body asEmailFullJSON). - Notify command: Executes
HYPERMAIL_WATCH_NOTIFY_COMMANDthrough the platform shell with theEmailFullJSON piped to stdin.
- Webhook: One
- Both modes use exponential backoff (
baseDelay × 2^attempt). Retries on failures (non-2xx for webhook, non-zero exit for command). Logs and moves on aftermaxAttemptsexhausted — never blocks the poll loop. - Command delivery is fire-and-forget — the poll loop continues while delivery runs in the background.
- Works in both stdio and HTTP transport modes — the poll interval fires normally alongside MCP message handling.
Rate limits: Polling every 10s on a single inbox = 6 req/min = 0.6% of Microsoft Graph's 10,000 req/10min per-user limit. Safe for personal inboxes.
Add-account flows
Outlook
- Agent calls
add_account({ provider: "outlook" }). - Server returns:
{ "status": "pending", "handle": "…uuid…", "verification": { "type": "device_code", "userCode": "ABCD-EFGH", "verificationUri": "https://microsoft.com/devicelogin", "expiresAt": "2025-…", "message": "To sign in, use a web browser to open …" } } - The user opens the URL and enters the code.
- Agent polls
complete_add_account({ provider: "outlook", handle })until it returns{ "status": "ready", "account": {...} }. - From then on, any tool can be called with
account: "<that-email>".
Gmail
Gmail uses Google OAuth 2.0, matching the official Gmail MCP model. Google's
device-code endpoint rejects Gmail API scopes, so Hypermail uses an authorization
URL with a real callback. Service accounts are only suitable for Google
Workspace domain-wide delegation; they don't grant server-to-server access to
consumer @gmail.com inboxes.
For local stdio/Desktop OAuth clients, Hypermail starts a temporary
127.0.0.1 loopback callback server automatically. For hosted HTTP deployments,
set HYPERMAIL_GMAIL_REDIRECT_URI and register the exact URI in Google Auth
Platform, for example:
HYPERMAIL_TRANSPORT=http
HYPERMAIL_GMAIL_REDIRECT_URI=https://mail.example.com/oauth/gmail/callback- Configure
HYPERMAIL_GMAIL_CLIENT_IDand, when issued by your Google client type,HYPERMAIL_GMAIL_CLIENT_SECRET. Use a Desktop client for local loopback, or a Web client for hosted HTTP callbacks. - Agent calls
add_account({ provider: "gmail" }). - Server returns an OAuth URL:
{ "status": "pending", "handle": "…uuid…", "verification": { "type": "oauth_url", "userCode": "", "verificationUri": "https://accounts.google.com/o/oauth2/v2/auth?...", "expiresAt": "2025-…", "message": "Open this URL in a browser to authorize Gmail access..." } } - The user opens
verificationUriand grants access. If the configured callback is reachable, the browser shows a small success page and the agent can pollcomplete_add_account({ provider: "gmail", handle })until ready. - If the browser cannot reach the callback, the manual fallback still works:
copy the final redirected URL from the browser address bar and call:
{ "provider": "gmail", "handle": "…uuid…", "authorizationResponse": "http://127.0.0.1:54321/oauth2callback?code=...&state=..." } complete_add_accountvalidates state, exchanges the code for tokens, stores the account, and returns{ "status": "ready", "account": {...} }.
Roadmap
- Threading / conversations.
- Calendar integration.
Project layout
src/
cli.ts # arg parsing + entry
server.ts # MCP server, stdio + HTTP transports, session management
version.ts # version constant
config.ts # env-only config types + resolution
store/
account-store.ts # encrypted multi-account store (AES-256-GCM)
crypto.ts # AES-256-GCM encrypt/decrypt, key resolution, atomic writes
providers/
types.ts # EmailProvider interface + shared DTOs
registry.ts # routes account email → provider
outlook/
auth.ts # msal-node device-code flow
client.ts # @microsoft/microsoft-graph-client factory
index.ts # OutlookProvider implementation
imap/index.ts # IMAP provider (imapflow + nodemailer)
gmail/
auth.ts # Google OAuth authorization-code flow
client.ts # Gmail API (googleapis)
index.ts # GmailProvider implementation
shared/ # shared utilities across providers
watcher/
manager.ts # WatcherManager — inbox poll loop + dedup
webhook.ts # HTTP POST with exponential backoff retry
script.ts # shell-command delivery with retry/timeout
index.ts # barrel export
tools/
index.ts # MCP tool registrations
accounts.ts # list/add/remove/complete-add account tools
browse.ts # list/search/read email tools
compose.ts # send/draft/edit/send-draft tools
folders.ts # list/create/delete/rename folder tools
organize.ts # archive/trash/move/mark-read/mark-unread tools
shared.ts # shared tool helpersLicense
MIT
