hypermail-mcp
v0.7.32
Published
Open-source MCP server for AI agents to manage Gmail, Outlook, Microsoft 365, and IMAP mailboxes through one unified interface.
Maintainers
Readme
Hypermail MCP is an open-source email server that lets AI agents manage Gmail, Outlook, Microsoft 365, and IMAP mailboxes through one unified Model Context Protocol interface. Connect multiple personal and work accounts, then use the same tools for each mailbox. Pass an email address; Hypermail routes the request to the right provider.
Run it on your machine or your own infrastructure. Choose the MCP-compatible assistant that accesses your email. The code is open under the MIT license.
Hypermail MCP is available today. The separate Hypermail app is still in development. Learn about both projects on the Hypermail website.
What can your agent do?
- Read and find email — browse inboxes, search one or all connected accounts, read messages, and download attachments.
- Write and reply — compose Markdown emails, reply to threads, forward messages, and create or edit drafts before sending.
- Organize mailboxes — archive, trash, move messages, mark them read or unread, and manage folders.
- Work across accounts — use one tool surface for different providers, with per-account signatures and style preferences.
- Check for new mail — poll for new inbox messages on your agent's schedule, without a separate MCP server for each account.
Supported mailboxes
| Mailbox | Connection | Setup | | --- | --- | --- | | Gmail / Google Workspace | Gmail API with Google OAuth | Authorize with the built-in Desktop client; custom OAuth clients remain optional. Gmail setup | | Outlook / Microsoft 365 | Microsoft Graph with device-code sign-in | Sign in with a personal or work Microsoft account. Outlook setup | | IMAP mailboxes | IMAP for reading and managing mail; SMTP for sending | Provide your provider's IMAP/SMTP settings and password or app password. IMAP setup |
Generic IMAP support covers providers that expose IMAP/SMTP access; it does not mean every email service has a native API integration.
Quick start
Requires Node.js 20 or newer and an MCP-compatible client.
1. Install
npm install -g hypermail-mcp
hypermail-mcp --help2. Connect your agent
For Claude Desktop or another client using mcpServers, add this to its MCP
configuration. npx runs the package without requiring a global install.
{
"mcpServers": {
"hypermail": {
"command": "npx",
"args": ["-y", "hypermail-mcp"]
}
}
}For Claude Code, use:
claude mcp add hypermail -- npx -y hypermail-mcp3. Add a mailbox
Restart or reload your MCP client, then ask your agent:
Connect my Outlook account using Hypermail.
The agent calls add_account and guides you through sign-in. Gmail needs no
OAuth environment variables with the built-in Desktop client; for IMAP,
provide the mailbox's connection settings. See add-account flows
for each provider. Add more accounts to the same server as needed.
4. Put it to work
Try requests like these after connecting your accounts:
Search all my connected mailboxes for invoices from last month.
Draft a reply to this email from my work account. Save it for review; do not send it.
Move the selected messages to my Receipts folder and mark them as read.
Your MCP client decides how to invoke tools and request approval. Configure that client to require confirmation for sending or destructive actions if needed.
More documentation: Configuration · Tools · New-email polling · Account setup · Security · Release notes
Self-hosting
As a hosted HTTP server
hypermail-mcp --http --port 3000 --host 0.0.0.0
# endpoint: http://<host>:3000/mcp (Streamable HTTP transport, session-aware)HTTP has no built-in authentication. Keep it on a local/private network or
behind an authenticated, authorized reverse proxy; do not expose the backend
port directly to the public internet. Set a persistent HYPERMAIL_KEY and data
directory for restarts and redeploys. See the
hosting guide.
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 127.0.0.1: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, --version, --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 | Optional Google OAuth client ID override | Built-in Hypermail MCP client when unset, empty, or whitespace-only |
| HYPERMAIL_GMAIL_CLIENT_SECRET | Optional Google OAuth secret override | Built-in Desktop secret only when the selected ID is the Hypermail client |
| 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_DEBUG | Enable structured debug logs to stderr (1, true, yes, on, or debug) | Disabled by default |
Priority order: selected CLI flags > HYPERMAIL_* env vars > hardcoded defaults.
Gmail OAuth client
New Gmail authorization flows use the built-in Hypermail MCP Desktop client
402846182885-uucmvo5gbgd0u2m49hk2k3ng6vtemuql.apps.googleusercontent.com
and its matching credential pair, without OAuth environment variables.
These are intentionally distributed public-client credentials, not confidential
server credentials. Access and refresh tokens remain private on your installation.
A nonblank HYPERMAIL_GMAIL_CLIENT_ID overrides the client. A custom client ID
never receives the built-in secret. A nonblank HYPERMAIL_GMAIL_CLIENT_SECRET
always overrides secret selection, including for the built-in ID; whitespace is
trimmed and blank values behave as absent. Remove incorrect explicit secret
overrides to use the built-in pair.
Run the MCP on your own computer or your own server. Hypermail's publisher
operates no MCP hosting, OAuth relay, token-exchange backend, or token storage
for this onboarding flow. Local authorization uses
http://127.0.0.1:33333/callback. For a remote MCP, the browser's localhost
refers to the browser machine, not the server; use the manual URL handoff below.
For a public callback on your own hosted HTTP installation, configure your own
Google OAuth Web application client with the exact callback URI and set its ID,
secret, and HYPERMAIL_GMAIL_REDIRECT_URI.
In Google Cloud, check the app's consent-screen name, publishing status, audience,
and test users. Choosing the correct client does not bypass Google's access
restrictions.
When presenting the Gmail authorization link, agents must also ask the user to
copy the full final URL from the browser address bar after approval and paste
it back into the chat, including code and state. This applies even if the
callback page cannot load; a reply such as “done” is not a substitute for the URL.
Pass it as authorizationResponse to complete_add_account with provider
gmail and the original handle. Keep the MCP process alive between these calls.
Automatic callback capture remains supported. Treat the callback URL as sensitive
and do not share it publicly; handing it through chat is a usability/security
tradeoff, not equivalent to a same-device callback.
If token exchange reports an invalid client secret, verify that
HYPERMAIL_GMAIL_CLIENT_SECRET belongs to the selected HYPERMAIL_GMAIL_CLIENT_ID
in the server's effective environment, then restart and generate a fresh
authorization link. Changing the client ID does not update its secret.
Never print the secret in logs or paste it into chat.
Restart the MCP server after changing its environment and call add_account
again to generate a fresh link. Existing accounts retain the OAuth credentials
stored when they were authorized. Validate a new connection by checking the
expected app in Google consent, completing authorization, confirming
complete_add_account returns ready and list_accounts includes the account,
then calling list_emails with limit: 1 to prove Gmail access.
Stored accounts are not migrated to the built-in pair. Accounts with stale or
missing OAuth credentials must reauthorize with add_account and
complete_add_account; changing environment settings does not repair their
stored credential pair.
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
Most 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. Omit account on search_emails or get_new_emails to work across all
connected accounts.
Tools that return or modify an identifiable email include a user-shareable
webUrl for opening that message in the provider's native web client. The URL
contains no credentials; the person opening it must already have mailbox
access. When a URL cannot be supplied, the same email/result instead includes
webUrlUnavailableReason (including on every item in collection responses).
Outlook uses Microsoft Graph's native link and refreshes moved items before
returning their post-operation ID/link. Gmail links are account-aware but use a
best-effort, unofficial web route. Generic IMAP has no universal webmail URL, so
it returns an unsupported reason and never fabricates an imap:// URL.
| Tool | Inputs | Notes |
| --- | --- | --- |
| list_accounts | — | Returns registered emails + provider, no secrets. hasSignature reports locally stored custom-signature presence only; use get_account_settings for the effective signature. |
| add_account | provider, email?, config? | Starts the provider add flow. Outlook returns a device code; Gmail returns an OAuth URL. Ready account responses include public metadata only and never return stored tokens/passwords. Returns {handle, verification:{type, userCode, verificationUri, expiresAt, message}} for pending flows. |
| complete_add_account | provider, handle, authorizationResponse?, code?, state? | Returns pending / ready / expired / error. Ready account responses include public metadata only and never return stored tokens/passwords. Gmail accepts a pasted final redirected URL or raw code/state for remote-safe completion. |
| get_account_settings | account | Get the effective HTML signature (Gmail first, local fallback only on successful empty lookup) and local style preferences. Native lookup errors fail rather than returning local fallback. |
| set_account_settings | account, signature?, signaturePath?, style? | Set local signature HTML (inline or via file path) and font preferences. Returns saved local settings; Gmail's signature is never changed. Clearing local does not clear Gmail. |
| 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. Outlook accepts well-known folder names, folder IDs, and falls back from localized display names to matching folder IDs. |
| get_new_emails | account?, limit? | Pull new inbox emails not previously returned by this tool. limit defaults to 10 and is global when account is omitted. Returns full markdown bodies with attachment metadata; bodies may be truncated. |
| search_emails | account?, query?, from?, to?, cc?, limit? | Provide at least one of query, from, to, or cc; distinct criteria combine with AND. cc searches CC or BCC. Search one account when account is provided, or omit it to search all registered accounts in parallel. Returns account-annotated summaries and partial per-account errors. Address filters are provider-agnostic; Outlook search results with malformed IDs are normalized to readable immutable IDs when possible. |
| read_email | account, id, format? | Returns full body + recipients + attachment metadata. format: markdown (default) or explicit html; text is rejected before provider reads. Messages without HTML retain their literal text body. Outlook retries malformed/stale IDs through Graph ID translation before failing. |
| read_attachment | account, messageId, attachmentId | Download an attachment to a temporary file and return its path plus the parent message's web link or unavailable reason. |
| archive_email | account, id | Archive natively and return the post-operation ID/link. Gmail removes only the Inbox label, retaining other labels and keeping the message in All Mail; other providers use their archive folder. |
| trash_email | account, id | Move a message to Deleted Items (trash) and return its post-operation ID/link. |
| move_email | account, id, destination | Move to any folder by well-known name (inbox, drafts, etc.) or custom folder ID, returning the post-operation ID/link. |
| send_email | account, to[], cc?, bcc?, subject, body, include_signature, inReplyTo, replyAll?, forwardMessageId?, attachments? | Send an email. body is Markdown only, converted to HTML via marked; raw HTML is rejected. Use a blank line between paragraphs. Account styles apply independently of include_signature; the saved HTML signature is appended when 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, include_signature, inReplyTo, replyAll?, forwardMessageId?, attachments? | Save as draft without sending, with the same Markdown-only params as send_email. Returns final draft ID/link and authoritative draftMarkdown after readback. Readback failure returns warning and draftReadbackError. Explicit HTML is available via read_email(format: "html"). Replies retain quoted history and threading. Set required inReplyTo to false for new emails. |
| edit_draft | account, id, to?, cc?, bcc?, subject?, old_text?, new_text?, body?, include_signature?, new_attachments?, remove_attachments? | Copy exact, unique old_text from complete current draftMarkdown or read_email Markdown, not HTML or truncated previews. Words, phrases and complete blocks are supported; unsafe structural selections are rejected before any mutation. Provide Markdown-only new_text; inline selections cannot accept multiple blocks or signatures. Unselected HTML remains byte-for-byte intact. Empty replacement deletes the selection. body remains an alias for new_text with old_text, never full replacement alone. Complete-body persistence is checked after saving. Returns final draft ID/link, draftMarkdown and attachment metadata. new_attachments adds {filePath, name?}[]; remove_attachments removes string[] IDs. |
| 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. |
When include_signature is true, Gmail reads the HTML signature for the account's
From address via SendAs.signature. Nonempty native HTML takes precedence over
the locally stored fallback; only a successful empty result permits fallback.
Lookup errors fail before sending or mutating a draft, even if a local signature
exists. Other providers continue using the local signature. Inclusion fails if
neither source is available; do not also type a signature into the body.
include_signature: false omits both sources without fetching Gmail settings.
Draft replacement resolves only when replacement content requests inclusion;
unselected HTML/history stays unchanged. Metadata/attachment-only edits do not
fetch signatures, and send_draft sends the stored draft without re-appending.
set_account_settings manages only local fallback/style; account lifecycle
responses also describe stored local settings, not native availability.
This uses the existing gmail.modify scope and does not write Gmail settings.
Gmail exposes one signature per sending address through this API, not its named
signature collection or separate reply defaults.
Gmail archiving never creates or adds an Archive / Archivé label. This
applies to both archive_email and move_email(destination: "archive").
The Gmail API removes INBOX; Gmail over IMAP removes \Inbox using
X-GM-LABELS and resolves the resulting message ID in the server-advertised
\All mailbox, including localized mailbox names. Ordinary IMAP servers use
their advertised \Archive / \All mailbox, falling back to Archive when
neither is advertised. Existing custom archive labels are left untouched.
Pull new emails
get_new_emails is the replacement for server-side watch/push delivery. The
server does not run background cron jobs; agents or their harnesses call this
tool on their own schedule, for example every 30–60 seconds.
Behavior:
- Polls inbox only.
accountis optional. When omitted, the tool checks all registered accounts.limitdefaults to10. In all-account mode, the limit is a global total across accounts, selected by oldestreceivedAtfirst.- All-account polling performs per-account candidate collection and hydration in parallel, then returns the combined batch in oldest-first order.
- First use for an account initializes its checkpoint to the newest inbox email and returns no emails for that account.
- Later calls return emails not previously returned by this tool, oldest first.
- Returned bodies are markdown and may be truncated around 20k characters; call
read_emailfor the full body when needed. - Attachments are returned as metadata only; call
read_attachmentfor content. - The tool does not mark emails as read.
limit: 0can initialize/check state without fetching message bodies.
All-account calls return partial failures as errors: [{ account, message }]
and still return successful accounts' emails. Pollers should continue processing
returned emails when errors is non-empty, and use those entries to notify or
log which accounts are failing.
See the Hermes scheduler example for an integration that polls this tool and hands new-email payloads to an agent.
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>".
IMAP
IMAP accounts are added synchronously with host/user/password settings. If the server accepts TCP/TLS but closes during login, Hypermail reports an IMAP authentication failure. Check the mailbox password or app-password, confirm IMAP access is enabled by the provider, then re-add or update the account.
Gmail
For Google Cloud prerequisites, see the website's Google OAuth checklist.
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": {...} }.
Security and control
- Your deployment, your accounts. Run locally over stdio or self-host over Streamable HTTP. Email content is made available to the MCP client and AI agent you connect; local hosting does not mean it stays out of your AI provider.
- Encrypted credentials at rest. Account tokens and passwords are stored with AES-256-GCM. Protect the encryption key and data directory; encryption at rest is not caller authentication.
- Choose the available tools. Use
HYPERMAIL_TOOLS_ENABLEDorHYPERMAIL_TOOLS_DISABLEDto limit the tool surface. These filters are server-wide, not per-user authorization. - Protect HTTP access. HTTP has no built-in authentication. Use private networking or an authenticated, authorized reverse proxy. See the hosting guide.
- Inspect the code. Hypermail MCP is MIT-licensed open source. Review it, contribute, or host it on infrastructure you control.
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
tools/
index.ts # MCP tool registrations
accounts.ts # list/add/remove/complete-add account tools
browse.ts # list/search/read email tools
new-emails.ts # get_new_emails pull/checkpoint tool
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 helpersRelease notes
v0.7.32 — Gmail-native signatures
- Explicit signature inclusion and settings inspection prefer Gmail's native HTML signature, with local fallback only after a successful empty lookup. Lookup failures prevent composition mutations; local settings writes remain offline.
- Signature omission, stored-draft sending, and metadata/attachment-only edits perform no native lookup. Existing unselected draft HTML remains intact.
v0.7.31 — Zero-config Gmail Desktop authorization
- New Gmail authorization flows use the matching public Desktop client ID and secret without OAuth environment variables. Custom client IDs never inherit the built-in secret; explicit secret overrides remain supported.
- Agents request the full final callback URL after Google consent, preserving
code,state, and the original handle for manual completion on remote MCPs. Keep the MCP process alive between authorization calls. - Stored accounts retain their original credentials; stale or incomplete pairs require reauthorization. Public hosted callbacks still require a custom Web application client.
- Verified fresh manual authorization, account listing, bounded Gmail reads, real refresh-token exchange, and custom-client isolation.
v0.7.30 — Gmail authentication and archiving fixes
- Gmail OAuth defaults to the built-in Hypermail MCP client when no custom client ID is configured. Hosted deployments still require a Web application client with their registered callback URL.
- Gmail accounts connected over IMAP use native Gmail archiving.
v0.7.29 — Package documentation
- Clarified the open-source, multi-provider email server positioning and its distinction from the Hypermail app in development.
- Put supported mailboxes, setup, and agent workflows before the technical reference and release history.
- Updated npm metadata and linked the project website and Google OAuth guide.
v0.7.28 — Correctness fixes
Gmail OAuth includes the account address as a login hint to help select the intended Google account.
Composition is Markdown-only:
send_email,draft_email, and replacement content inedit_draftreject raw HTML before provider mutations.draft_emailandedit_draftreturn authoritativedraftMarkdown, replacingdraftHtml.read_emailsupports onlymarkdown(default) and explicithtml; plain-text-only messages retain their literal body.Draft selections are exact, unique Markdown words, phrases or complete blocks copied from
draftMarkdownor a complete currentread_email. Re-read truncated previews first. Source-position edits preserve unselected HTML bytes, signatures and thread history. Unsafe syntax/structure selections are rejected before attachment changes. Inline replacements require inline Markdown without a signature; select a complete block for multiple paragraphs.Persistence checks compare the complete HTML tree, not replacement substrings or Markdown equality. Outlook envelopes, encoding declarations, CRLF and safe block-separator whitespace are tolerated; missing history or changes to inline whitespace, styles, links, images and comments remain failures.
Verification: 367 full-suite tests passed, plus typecheck and build. Local-build MCP stdio smoke passed on Outlook (
[email protected]) and Gmail ([email protected]): draft creation, multi-paragraph replacement, word replacement, provider readback, witness preservation, raw-HTML rejection without mutation andtextformat rejection. Test drafts were retained; no emails were sent. Visual rendering was not reviewed.Attachment downloads use distinct private temporary directories and files; email-controlled filenames cannot overwrite files or escape the download directory. Consumers may remove their downloaded file's directory when finished.
Provider-read timeouts cannot claim new emails or initialize polling checkpoints after the caller has timed out. Successful reads commit under the existing store lock; this is not network-level exactly-once delivery. Gmail polling follows remaining API pages.
Gmail draft references remain message IDs; draft-container lookup is internal. Draft edits and attachment mutations preserve parsed MIME content and return current provider IDs. Bulk removal validates original attachment IDs before rewriting; IMAP replacements are readable before the old draft is deleted. A post-replacement deletion failure reports both IDs.
Replies preserve RFC threading and decoded quoted history; reply-all excludes inherited self/Bcc addresses. Forwards retain decoded content and attachments. Gmail API MIME retains Bcc recipients; IMAP SMTP uses an explicit To/Cc/Bcc envelope without a wire Bcc header.
Settings are patched against the latest locked account state, preserving refreshed tokens and checkpoints.
style: nullclears style; omitted settings remain unchanged. Closed IMAP connections reconnect on the next operation without replaying failed writes.HTTP accepts only
/mcp(query strings allowed), caps POST bodies at 16 MiB, and reports malformed JSON, missing sessions and expired sessions with recoverable protocol errors. HTTP is for local/private use or an authenticated reverse proxy, not public unauthenticated exposure; see hosting. Tool allow/deny lists and encryption-at-rest are not authentication.
Verification: 326 tests passed, TypeScript checks and the Node 20-targeted build passed, and isolated actual-MCP HTTP and MIME/SMTP serialization smoke checks passed. Live no-send draft checks passed on Outlook for combined body edits, bulk attachment removal/addition, downloads, and move/trash. Gmail draft edits, Bcc preservation, additions, reply/forward content, downloads, and move/trash were exercised. No email was sent; test drafts were trashed and local fixtures removed.
Known live gaps: Gmail attachment tokens change between reads of an unchanged draft, causing bulk removal to reject a previously returned ID and download metadata lookup to fall back to attachment. Gmail readback also omits a text attachment when it is mistaken for a body and misparses quoted recipient names containing commas. These defects remain unresolved. Live IMAP checks were blocked by account authentication; live SMTP sending was not attempted.
v0.7.27 — Outlook message links are now normalized to canonical web URLs, preventing malformed or inconsistently encoded links in email results.
v0.7.26 — Email results now include a provider-native
webUrlwhen available, or a per-messagewebUrlUnavailableReason. Links are returned by browse, new-email, attachment, compose, draft, and organization tools. Outlook refreshes post-move IDs and links with an OWA fallback, Gmail returns account-aware links, and generic IMAP reports that web links are unsupported.v0.7.25 — Gmail reauthorization now replaces any cached client after persisting fresh OAuth credentials, so subsequent operations do not reuse stale credentials and fail with
invalid_grant.v0.7.24 —
search_emailsnow accepts provider-neutralfrom,to, andccfilters in addition to optional free text. Criteria combine with AND, whileccsearches both CC and BCC recipients consistently across Outlook, Gmail, and IMAP.v0.7.23 — IMAP reply drafts now retain the referenced message as quoted history and canonicalize generated MIME line endings before IMAP APPEND, so servers that reject bare newlines can save reply drafts successfully.
v0.7.22 — Fixed IMAP
edit_draftupdates so body-only edits preserve the draft's existing recipients, subject, reply context, and attachments.v0.7.21 —
send_emailanddraft_emailnow treat the exact string"false"forinReplyToas the booleanfalse, so agents that serialize that explicit new-message marker as text can forward withforwardMessageIdinstead of incorrectly triggering the reply/forward exclusivity check.v0.7.20 — Hardened long-running IMAP and new-email polling paths: IMAP connections now use explicit connection/greeting/socket/operation timeouts, reset stuck connections, and report post-TLS authentication failures with account-specific remediation guidance.
get_new_emailsnow applies per-account timeouts during candidate collection and hydration so one hanging account does not block all-account polling; partial timeout failures are returned alongside successful accounts' emails.v0.7.19 — Hardened Outlook ID handling when Microsoft Graph
$searchreturns IDs that/me/messages/{id}rejects as malformed:search_emailsnow returns a translated readable immutable ID when available, andread_emailretries malformed/stale IDs throughtranslateExchangeIdsbefore failing.list_emailsalso falls back from localized folder display names such asÉléments envoyésto the matching Graph folder ID.v0.7.18 —
search_emailscan now omitaccountto search all registered accounts in parallel, returning account-annotated results plus per-account errors without discarding successful matches. All-accountget_new_emailspolling also collects and hydrates per-account batches in parallel while preserving the global oldest-first limit.v0.7.17 — Hardened Outlook message IDs returned by
list_emailsandsearch_emails: Outlook reads now request Microsoft Graph immutable IDs,search_emailsprobes results and marks stale/not-found messages withstale: true, andread_email/read_attachmentkeep a legacy mutable-ID fallback for older IDs. Graph errors now include structured diagnostic details when available.v0.7.16 — Hardened
draft_emailafter successful draft creation: if post-save readback fails, the tool now returns the created draft ID withwarninganddraftReadbackErrorinstead of losing the draft behind an error. Compose operations also emit sanitizedHYPERMAIL_DEBUGstage logs, IMAP drafts now use the server-advertised\\Draftsmailbox when present, and the CLI supports--version.v0.7.15 — Hardened IMAP draft creation: drafts now append directly to the Drafts mailbox, retry without the
\\Draftflag when an IMAP server rejects flagged APPEND, and preserve IMAP reply/forward context in drafts the same waysend_emaildoes. IMAP operations for a single account are serialized to avoid shared-connection races, and IMAP write failures now include safe server response details instead of onlyCommand failed. Account setup responses no longer expose stored provider tokens/passwords. Also rejects multiline plain text passed asformat: "html", and adds the Hermesget_new_emailspoller example.v0.7.14 — Added opt-in
HYPERMAIL_DEBUGstructured stderr logs with redaction for server startup, account-store locking/checkpoint writes, andget_new_emailscandidate/claim flow. Hardened Outlookedit_draftbody updates by requesting Graph's updated representation, re-reading with retries, and returning an error instead ofedited: trueif the requested body change is not observable; Outlook body edits also get one safe replay after attachment handling when the final read is stale.v0.7.13 — Hardened
get_new_emailsagainst duplicate delivery when multiple MCP processes share the same encrypted account store. Store writes now reload and merge under a cross-process lock, checkpoints are monotonic, and new-email batches are atomically claimed before being returned.trash_emailalso now uses provider-native trash operations for Gmail and IMAP trash aliases.v0.7.12 — Hardened Outlook reply/forward draft formatting when Microsoft Graph labels generated thread history as HTML but returns plain/unstructured text. Such histories are now defensively normalized with escaped content and line breaks so quoted messages remain readable.
v0.7.11 — Fixed Outlook reply/forward drafts whose Graph-generated quoted bodies are plain text by escaping them and patching the draft as HTML, so newly composed HTML replies no longer render as raw text.
v0.7.10 — Fixed an account-store race where Gmail/Outlook token refreshes could overwrite
get_new_emailscheckpoints. Checkpoint updates now preserve token data and merge delivered IDs at the same timestamp.edit_draftbody edits now require exact selected-section replacement (old_text+new_text) so reply/forward history is preserved instead of overwritten.v0.7.9 — Replaced server-side email watch/webhook/script delivery with the pull-based
get_new_emailstool. Agents schedule their own repeated calls and fetch bounded batches of new inbox email.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. 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.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.
License
MIT.
Built by Hypersimple.
