MCP Server cho Email (IMAP/SMTP). TypeScript, Node.js >= 24, bun, ESM. 5 composite tools, 21 actions (messages, folders, attachments, send, config) plus config__open_relay + help. Multi-account, App Passwords, auto-discovery.
# Setup
bun install
# Lint & Type check
bun run check # biome check + tsc --noEmit
bun run type-check # tsc --noEmit only
# Fix
bun run check:fix # biome fix + type check
# Test
bun run test # vitest (--passWithNoTests)
bun run test:watch # vitest watch
bun run test:coverage # vitest --coverage
bun vitest run src/tools/helpers/errors.test.ts # single file
# Build & Dev
bun run build # tsc --build tsconfig.build.json + esbuild CLI
bun run dev # tsx watch dev server
# Docker
bun run docker:build
bun run docker:run
# Mise shortcuts
mise run setup # full dev setup
mise run lint # bun run check
mise run test # bun run test
mise run fix # bun run check:fixsrc/
init-server.ts # Entry point, env validation
relay-setup.ts # formatCredentials() helper (relay form fields -> EMAIL_CREDENTIALS string)
relay-schema.ts # Relay form schema (email credential fields)
credential-state.ts # Single-user / stdio credential resolution from env
auth/ # Per-user credential store + Outlook OAuth (HTTP mode)
in-memory-cred-store.ts # LIVE per-user store: ephemeral in-memory, keyed by JWT sub
outlook-device-code.ts # Microsoft device-code OAuth flow
subject-context.ts # Per-request JWT-sub scope (AsyncLocalStorage)
transports/
http.ts # Multi-user HTTP transport with OAuth 2.1
docs/ # Markdown docs phuc vu qua MCP resources
tools/
registry.ts # Tool registration + routing
composite/ # 1 file per domain: messages, folders, attachments, send, config
helpers/ # errors, config, html-utils, imap-client, smtp-client
- stdio mode (default):
EMAIL_CREDENTIALS(bat buoc). Format:user@gmail.com:app-password- Multi-account:
user1@gmail.com:pass1,user2@outlook.com:pass2 - Custom IMAP host:
user@custom.com:password:imap.custom.com - Custom IMAP host + port:
user@custom.com:password:imap.custom.com:1993 - Local IMAP proxy:
user@custom.com:password:localhost:1993(localhostaccepted as host; per-account port)
- Multi-account:
- http mode (opt-in via
--http,MCP_TRANSPORT=http, orTRANSPORT_MODE=http):PUBLIC_URL(for relay/OAuth redirect URLs). Per-user credentials are held in an in-memory store (auth/in-memory-cred-store.ts, keyed by JWTsub, cleared on restart).MCP_AUTH_DISABLE=1skips Bearer JWT verification (for deploys behind an external auth gateway). PORT(default0= OS-assigned random port),HOST(optional bind address)OUTLOOK_CLIENT_ID-- tu chon, cho self-hosted OAuth2 client. CLIauth --client-id=<id>override env var (flag thang env, xemauth-cli.ts:parseArgs)OUTLOOK_TENANT-- tu chon, tenant cho CA device-code LAN token refresh (oauth2.ts:getOutlookTenant). Defaultconsumers(stdio/CLI) vacommon(http delegated,auth/outlook-device-code.ts). Work/school (Entra ID) cancommonhoac tenant GUID -- refresh token cua work/school danh vao/consumersse loiAADSTS7000012OUTLOOK_SCOPES-- tu chon, danh sach scope cach nhau bang space (oauth2.ts:getOutlookScopes). Dung khi grant duoc consent hep hon default (vd IMAP-only, khong SMTP)OUTLOOK_EXTRA_DOMAINS-- tu chon, domain cach nhau bang dau phay, gop them vaoOUTLOOK_DOMAINSde mailbox M365 tren custom domain di duong OAuth thay vi doi password- CF deploy: 3 bien tren PHAI nam trong
worker.ts:CONTAINER_ENV_KEYS, khong forward = set o Worker nhung container khong nhan
- Biome: 2 spaces, 120 line width, single quotes, semicolons as needed, trailing commas none
- Import:
import typerieng,.jsextension bat buoc,node:prefix cho builtins - tsconfig:
strict: true, target es2021, module es2022, moduleResolution Bundler - Error:
EmailMCPError+withErrorHandling()HOF.enhanceError()+suggestFixes(). - Error details duoc sanitize de tranh lo secrets/passwords.
- Test files co-located:
errors.test.tscanherrors.ts noExplicitAny: off (email API responses dungany)
PSR v10 (workflow_dispatch) -> npm + Docker (amd64+arm64) + GHCR + MCP Registry.
- Outlook/Hotmail/Live dung OAuth2 tu dong (Device Code flow). Token luu tai
~/.better-email-mcp/tokens.json. - Gmail, Yahoo, iCloud: dung App Passwords, KHONG phai password thuong.
- Account resolution: filter theo email, id, hoac partial match.
- Composite tool signature:
async function toolName(accounts: AccountConfig[], input: TypedInput): Promise<any> - 3-tier token optimization: Tier 1 (compact), Tier 2 (help tool), Tier 3 (MCP Resources).
- Pre-commit: biome check --write, tsc --noEmit, bun run test.
- Secrets: skret SSM namespace
/better-email-mcp/prod(regionap-southeast-1)
Two transports, selected in init-server.ts:52-53. There is no MCP_MODE env var; the old remote-relay / local-relay distinction was removed (see transports/http.ts:5).
- stdio (default): MCP SDK
StdioServerTransportdirectly. Reads credentials fromEMAIL_CREDENTIALSOREMAIL_USER+EMAIL_APP_PASSWORD. Outlook accounts use an App Password in this mode. - http (opt-in): enabled via
--http,MCP_TRANSPORT=http, orTRANSPORT_MODE=http. Single multi-user relay:/authorizeform for App-Password providers (pasteemail:app-password) plus bundled Outlook device-code OAuth. Per-user credentials keyed by JWTsub. Outlook token file:~/.better-email-mcp/tokens.json. Deploy athttps://email.n24q02m.com.
-
(Obsolete) Outlook Device Code "2 tab" duplicate auth — chỉ affect
local-relaymode, đã bị gỡ cùng vớiMCP_MODE(xem mục Modes). HTTP mode hiện dùng mcp-core delegated device_code, không duplicate. -
(Obsolete) Browser UI stuck "Waiting for server..." — chỉ affect
MCP_MODE=local-relay(ECDHrelay/client.ts:sendMessage('complete')paste-form flow), đã bị gỡ. Không còn relay-client path nào live trong HTTP mode. -
Config storage path: stdio/single-user config ghi qua mcp-core
config-file.js->config.enctại platformdirsmcpconfig dir ($APPDATA\mcp\Config\config.enctrên Windows; khac Python servers$LOCALAPPDATA\mcp\config.enc). Khi debug/test, clean ca 2 paths +~/.better-email-mcp/tokens.jsonde reset state. -
Outlook token email key:
saveOutlookTokensfallback toOUTLOOK_EMAILenv hoac'outlook-device-code'khi Microsoft token response khong include email field (device code mặc định không trả email). Workaround: setOUTLOOK_EMAILenv var khi self-host. Long-term fix: requestopenid email profilescopes + decode id_token trong onTokenReceived.
Driven by mcp-core/scripts/e2e/ (matrix-locked, 15 configs). Run a single config from this repo via make e2e (proxy) or directly:
cd ../mcp-core && uv run --project scripts/e2e python -m e2e.driver <config-id>
Configs for this repo: email-gmail, email-outlook.
email-outlook is t2-interaction (Microsoft device-code, 900s timeout); user clicks microsoft.com/devicelogin.
Tier policy:
- T0 (precommit + CI on PR / main push) - runs without upstream identity. Skret keys not required.
- T2 non-interaction (
make e2e-config CONFIG=<id>locally) - driver pre-fills relay form from skret AWS SSM/better-email-mcp/prod(ap-southeast-1). No user gate. - T2 interaction - driver fills relay form, then prints upstream user-gate URL; user signs in / types OTP at provider. Driver enforces per-flow timeouts (device-code 900s, oauth-redirect 300s, browser-form 600s) and emits
[poll] elapsed=Xs remaining=Ys status=<body>every 30s. On timeout, container logs + lastsetup-statusare saved to<tmp>/e2e-diag/BEFORE teardown for post-mortem.
Multi-user remote mode (deployment property; not a separate config) keys per-user credentials by JWT sub in an in-memory store (auth/in-memory-cred-store.ts, TC-NearZK), cleared on restart — users re-submit after a restart.
References: mcp-core/scripts/e2e/matrix.yaml, ~/.claude/skills/mcp-dev/references/e2e-full-matrix.md (harness-readiness gate), ~/.claude/skills/mcp-dev/references/secrets-skret.md (per-server credential layout), ~/.claude/skills/mcp-dev/references/multi-user-pattern.md (per-JWT-sub isolation).