MCP Server for Email (IMAP/SMTP). TypeScript, Node.js >= 24, bun, ESM.
bun install # Install dependencies
bun run build # tsc --build && esbuild CLI bundle
bun run check # Biome check + tsc --noEmit (CI command)
bun run check:fix # Auto-fix Biome + type check
bun run test # vitest --passWithNoTests
bun run test:watch # vitest watch
bun run test:coverage # vitest --coverage
bun run lint # biome check .
bun run format # biome format --write .
bun run type-check # tsc --noEmit
bun run dev # tsx watch dev server
# Run a single test file
bun vitest run src/tools/helpers/errors.test.ts
# Run a single test by name
bun vitest run -t "test name pattern"
# Mise shortcuts
mise run setup # Full dev environment setup
mise run lint # bun run check
mise run test # bun run test
mise run fix # bun run check:fix# Required: email credentials (App Passwords, NOT regular passwords)
EMAIL_CREDENTIALS=user@gmail.com:abcd-efgh-ijkl-mnop
# Multiple accounts
EMAIL_CREDENTIALS=user1@gmail.com:pass1,user2@outlook.com:pass2
# Custom IMAP host (optional :port, default 993)
EMAIL_CREDENTIALS=user@custom.com:password:imap.custom.com:1993
# Local IMAP proxy ("localhost" is accepted as a host)
EMAIL_CREDENTIALS=user@custom.com:password:localhost:1993- Indent: 2 spaces
- Line width: 120
- Quotes: Single quotes
- Semicolons: As needed (omit when possible)
- Trailing commas: None
- Arrow parens: Always
- Bracket spacing: true
- Line endings: LF
- Type imports use
import type(separate statement) - External packages first, then internal imports (relative paths)
- Node builtins with
node:prefix (node:fs,node:path,node:url) - Always use
.jsextension in import paths (ESM requirement)
import type { AccountConfig } from '../helpers/config.js'
import { EmailMCPError, withErrorHandling } from '../helpers/errors.js'
import { searchEmails, readEmail } from '../helpers/imap-client.js'strict: true, target: es2021, module: es2022, moduleResolution: Bundlercomposite: truefor incremental buildsisolatedModules: true,forceConsistentCasingInFileNames: true
| Element | Convention | Example |
|---|---|---|
| Functions/variables | camelCase | registerTools, loadConfig |
| Interfaces | PascalCase | MessagesInput, AccountConfig |
| Classes | PascalCase | EmailMCPError |
| Constants | UPPER_SNAKE_CASE | TOOLS, RESOURCES, DOCS_DIR |
| Files (helpers) | kebab-case | init-server.ts, imap-client.ts |
| Test files | Co-located | errors.test.ts next to errors.ts |
- Custom
EmailMCPErrorclass:message,code,suggestion,details withErrorHandling()HOF wrapper for all composite tool functionsenhanceError()converts raw errors to AI-friendly EmailMCPErrorsuggestFixes()for AI-readable error recovery hints- Error details sanitized to prevent secret/password leakage
noExplicitAny: off (email API responses useany)noNonNullAssertion: off (!assertion allowed)noUnusedVariables: warnnoUnusedImports: error (via organizeImports)
- Composite/Mega Tool: Each domain (messages, folders, attachments, send, config) is one function
- Input:
{ action, ...params }, dispatch viaswitch(input.action) - Every composite tool exports: 1 async function + 1 interface
- Signature:
async function toolName(accounts: AccountConfig[], input: TypedInput): Promise<any> - Account resolution: filter by email, id, or partial match
src/
init-server.ts # Server entry point, env validation
docs/ # Markdown docs served as MCP resources
tools/
registry.ts # Tool registration + routing
composite/ # One file per domain (messages, folders, attachments, send, config)
helpers/ # errors, config, html-utils, imap-client, smtp-client
/** */JSDoc on every function- File-level block comment describing module purpose
- No
@param/@returns-- rely on TypeScript types
Conventional Commits: type(scope): message. Enforced via git hooks.
biome check --write(lint + format)tsc --noEmit(type check)bun run test(run tests)