@jencryzthers/envoy
v0.1.0
Published
Envoy — Apple Mail MCP server. Gives an LLM complete, safety-gated control of Apple Mail on macOS over the Model Context Protocol.
Maintainers
Readme
Envoy — the Apple Mail MCP
Apple Mail MCP server. Envoy gives an LLM complete, safety-gated control of Apple Mail on macOS over the Model Context Protocol.
22 mail tools plus 3 self-introspection analytics tools, exposed over stdio JSON-RPC. Every interaction with Mail.app goes through JXA via osascript — no AppleScript files, no AppleMail database access.
Built with a safety-first model: confirm-gated mutators, dry-run previews, bulk-operation caps, and a server-wide safe mode for smoke-testing without touching real mail data.
Requirements
- macOS (tested on Sequoia / Tahoe)
- Node.js ≥ 20
- Mail.app configured with at least one account
- Automation permission granted to the process invoking the MCP (System Settings → Privacy & Security → Automation)
Install
From npm (recommended):
npm install -g @jencryzthers/envoy
# or run on demand without installing:
npx @jencryzthers/envoyFrom source:
git clone https://github.com/jencryzthers/envoy.git
cd envoy
npm install
npm run buildConfigure in Claude Code / Claude Desktop
Add to ~/.claude.json (Claude Code) or ~/Library/Application Support/Claude/claude_desktop_config.json (Claude Desktop):
{
"mcpServers": {
"envoy": {
"type": "stdio",
"command": "npx",
"args": ["-y", "tsx", "/absolute/path/to/envoy/src/index.ts"]
}
}
}For a built install, point at dist/index.js instead and drop tsx.
Tools
Read
| Tool | Purpose |
|---|---|
| list_accounts | List configured Mail accounts |
| list_mailboxes | List mailboxes for an account |
| list_messages | List messages in a mailbox (paginated, filterable) |
| get_message | Fetch full message body + optional headers/HTML |
| get_thread | Reconstruct a conversation thread |
| search_messages | Spotlight-first search with JXA fallback |
| list_attachments | List attachments on a message |
| list_rules | View Mail rules |
| list_signatures | View signatures |
Write (confirm-gated)
| Tool | Purpose |
|---|---|
| send_draft | Send a prepared draft |
| reply | Reply to a message |
| forward | Forward a message |
| move_messages | Move messages between mailboxes |
| copy_messages | Copy messages |
| delete_messages | Delete (Trash or permanent) |
| delete_mailbox | Delete a mailbox |
Write (non-gated)
| Tool | Purpose |
|---|---|
| create_draft / update_draft | Create or edit a draft |
| set_flags | Mark read/unread, flagged |
| create_mailbox / rename_mailbox | Manage folders |
| save_attachment | Save attachment to disk |
Analytics (introspection)
| Tool | Purpose |
|---|---|
| analytics_summary | Aggregate stats on tool usage |
| analytics_errors | Recent failed calls |
| analytics_sequences | Common call sequences |
Safety Model
Confirm-gated tools return a DryRunResult preview unless confirm: true is passed:
{ dryRun: true, action, summary, affectedIds, details }Bulk cap: operations affecting more than 200 messages require acknowledgeBulk: N where N matches affectedIds.length.
Permanent delete: delete_messages({ permanent: true }) additionally requires confirm: true.
Safe mode: set ENVOY_SAFE_MODE=1 in the MCP env to force every mutating tool into dry-run. Read tools pass through unchanged. Useful for first-time testing against a real mailbox.
Stable IDs
Format: acct:<account>/mbox:<path>/msg:<message-id-header>
acct:iCloud
acct:iCloud/mbox:INBOX
acct:iCloud/mbox:INBOX/msg:<[email protected]>Environment Variables
| Variable | Default | Purpose |
|---|---|---|
| ENVOY_SAFE_MODE | unset | Set to 1 to force all mutators to dry-run |
| ENVOY_ATTACHMENT_ROOT | $TMPDIR | Restricts save_attachment destination |
| ENVOY_ANALYTICS_DB | ~/.envoy/analytics.db | SQLite path for tool-call analytics |
| ENVOY_ANALYTICS_DISABLED | unset | Set to 1 to disable analytics writes |
| ENVOY_ANALYTICS_MAX_ROWS | 100000 | Row cap before pruning |
Development
npm test # Run unit tests (vitest)
npm run typecheck # tsc --noEmit
npm run build # Build to dist/
npm run scenarios # Run 200-scenario dry-run testerSee CLAUDE.md for architecture, file layout, and how to add a new tool.
Known Runtime Gotchas
- macOS Sequoia/Tahoe Spotlight doesn't index Mail messages for
mdfind, even though it does for the Spotlight UI.search_messagesautomatically falls back to a JXA scan of Mail when Spotlight returns empty. m.size()throws-1728 Can't get objecton some messages (encrypted, partial download, certain IMAP server quirks). All JXA scripts that touch message size wrap it in try/catch and return0on failure.- Mail.app must be running for any JXA call to succeed.
update_draftcan't change recipients or rewrite the body of an existing draft. It only patchessubject/content, theto/cc/bccfields are ignored, and Mail rejects edits to a saved draft's body with-10003 "Access not allowed"(saved messages are immutable via AppleScript — not account-specific). To change a draft, delete it and recreate withcreate_draft.- The id returned by
create_draftcan go stale. Its mailbox segment is hardcodedDrafts(a localized Mail UI uses e.g.Brouillons), and the message-id is reassigned once the account syncs to the server (Exchange/EWS). Re-resolve the draft vialist_messages/search_messagesinstead of reusing the returned id after a sync. - The account's default signature is auto-appended to a created draft's body (signature images make
hasAttachmentstrue). If you pass a complete body, expect a trailing signature on top of it.
License
MIT
