Skip to main content
Glama
paulo-amaral

applemail-mcp-server

by paulo-amaral
README.md
# applemail-mcp-server

A local MCP server that gives Claude access to **macOS Mail.app** through JXA (JavaScript for
Automation). Every account already configured in Mail — iCloud, Gmail, Outlook/Exchange, IMAP —
becomes visible, with no OAuth, no API keys, and no data leaving the machine.

**Read-only by default. There is no `send` tool, by design.**

## Tools

| Tool | Writes? | What it does |
|---|---|---|
| `apple_mail_list_accounts` | no | Account names, types, addresses. Start here. |
| `apple_mail_list_mailboxes` | no | Mailboxes + unread counts (names are case-sensitive and localised). |
| `apple_mail_unread_summary` | no | Unread counts per account/mailbox. Reads counters only — fast on huge mailboxes. |
| `apple_mail_search_messages` | no | Header search with filters; returns opaque per-message handles. |
| `apple_mail_get_message` | no | Full headers, recipients, attachment names and body for one handle. |
| `apple_mail_set_message_status` | yes | Mark read/unread/flagged. **Disabled unless you opt in.** |
| `apple_mail_compose_draft` | yes | Opens a pre-filled compose window. Never sends — you click Send. |
| `apple_calendar_list_calendars` | no | Calendar names + writable flag. Call before creating an event. |
| `apple_calendar_create_event` | yes | Create one event in **Apple Calendar only**. **Disabled unless you opt in.** Skips duplicates. |

## Requirements

- macOS with Mail.app configured (at least one account)
- Node.js 18+

## Build

```bash
npm install
npm run build
node dist/index.js --doctor   # verifies Mail.app is reachable and permissions are granted
```

## Configure Claude Desktop and/or Claude Code

Both hosts need an **absolute** path to `node` and to `dist/index.js` — Claude Desktop launches
the server with a stripped environment, so a bare `node` on `PATH` will not resolve.

```bash
npm run configure
```

This detects the exact node binary running the script (`process.execPath` — correct regardless
of nvm/fnm/volta/homebrew, and survives a node version switch by just re-running the command),
writes/updates the `apple-mail` entry in `claude_desktop_config.json`, and re-registers it with
`claude mcp add` for Claude Code. Flags:

| Flag | Effect |
|---|---|
| `--calendar-writes` | sets `APPLE_MAIL_MCP_ALLOW_CALENDAR_WRITES=1` on the registered entry |
| `--status-writes` | sets `APPLE_MAIL_MCP_ALLOW_STATUS_WRITES=1` |
| `--skip-desktop` | only touch Claude Code |
| `--skip-code` | only touch Claude Desktop |

Run once per target if you want different write policy on each (e.g.
`npm run configure -- --calendar-writes --skip-code` then `npm run configure -- --skip-desktop`).

After running it, **quit Claude Desktop with Cmd+Q** — closing the window is not enough, the
config is only read at startup — and reopen it. If the server does not appear, check
`~/Library/Logs/Claude/mcp*.log`.

**Manual alternative**, if you'd rather not run the script — edit
`~/Library/Application Support/Claude/claude_desktop_config.json` directly:

```json
{
  "mcpServers": {
    "apple-mail": {
      "command": "/ABSOLUTE/PATH/TO/node",
      "args": ["/ABSOLUTE/PATH/TO/applemail-mcp-server/dist/index.js"]
    }
  }
}
```

and register Claude Code with:

```bash
claude mcp add --transport stdio --scope user apple-mail -- node /ABSOLUTE/PATH/TO/dist/index.js
```

## macOS permissions

The first call triggers a system prompt to let the host app control Mail. If you dismissed it, go
to **System Settings → Privacy & Security → Automation** and enable **Mail** under Claude (or
Claude Code / Terminal, whichever launched the server). Re-launch the host app afterwards.

## Environment variables

| Variable | Default | Effect |
|---|---|---|
| `APPLE_MAIL_MCP_ALLOW_STATUS_WRITES` | off | Set to `1` to register the read/flag tool. |
| `APPLE_MAIL_MCP_ALLOW_COMPOSE` | on | Set to `0` to remove the compose-window tool. |
| `APPLE_MAIL_MCP_ALLOW_CALENDAR_WRITES` | off | Set to `1` to register `apple_calendar_create_event`. |
| `APPLE_MAIL_MCP_TIMEOUT_MS` | `90000` | Per-operation timeout. |

For the strictest posture, leave status and calendar writes off and set `APPLE_MAIL_MCP_ALLOW_COMPOSE=0`.
The server is then incapable of modifying anything.

## Design notes

**Apple Events are the bottleneck.** Each property access is an IPC round-trip, so the server never
loops over messages individually — it uses bulk getters (`spec.subject()` returns the whole column
in one event) and pushes filters down into Mail.app via `whose`. `since_days` defaults to 30 and
`search_messages` *refuses* rather than hangs when more than `max_scan` (default 400) messages
match, returning an error that tells the agent how to narrow the query.

**Handles, not IDs.** Search returns an opaque base64 handle encoding account + mailbox + Mail's
internal row id, so a follow-up read is a direct lookup rather than a re-scan. Handles go stale if
the message is moved or deleted; the error says so and tells you to search again.

**No string interpolation into script source.** Parameters are passed to `osascript` as a single
JSON argv entry and parsed inside the script, so mailbox names and search terms cannot inject code.

**Body search is not supported.** `query` matches subject and sender only — searching bodies over
Apple Events on a large mailbox is pathologically slow. Use Mail's own search for that.

**Calendar writes go to Apple Calendar only.** `apple_calendar_create_event` never touches Google
Calendar, Outlook, or any other service — it calls Calendar.app the same way `compose_draft` calls
Mail.app. It skips (rather than duplicates) an event when one with the same title already exists
on the same day in the same calendar, so a daily scan can be re-run safely. Date/time extraction
from message text is left to the calling agent, not done with regex inside this server — Apple
Events give no reliable way to validate a guessed date, so a wrong guess would silently create a
bad event.

## Daily mail-to-calendar automation

This server never runs on its own — an agent decides *when* to scan mail and *whether* a message
describes something calendar-worthy, then calls `apple_calendar_create_event`. To get a daily
scan, schedule a Claude Code routine (see the `schedule` skill) that runs once a day with a prompt
along these lines:

```text
Call apple_mail_search_messages (since_days: 1) across all accounts, read anything that looks
like a meeting, appointment, or deadline with apple_mail_get_message, then call
apple_calendar_list_calendars and apple_calendar_create_event to add each one to the right Apple
Calendar. Never invent a date. Report what was created and what was skipped.
```

Requires `APPLE_MAIL_MCP_ALLOW_CALENDAR_WRITES=1`, and Mail.app / Calendar.app must both be
reachable when the routine fires (the host machine needs to be on and unlocked).

## Security

This server reads your entire mailbox. Two things worth keeping in mind:

1. **Email bodies are untrusted input.** A message can contain text aimed at the model rather than
   at you. The `get_message` description tells the model to treat message contents as data and
   never as instructions, but a read-only configuration is what actually bounds the blast radius —
   keep the write tools off unless you need them.
2. **Scope matters more than trust.** If the mailbox holds client, government or UN correspondence,
   consider pointing searches at a specific account or archive mailbox rather than letting the
   server roam every account.

## Limitations (v0.1)

- No send, move, delete, or attachment extraction
- No true threading — search by subject or sender to group a conversation
- Plain-text bodies only (Mail returns the text part; HTML markup is not preserved)
- macOS only

## License

MIT

TDQS

A3.9/5.0

Scored across 7 tools

Disambiguation2/5

The mail tools are mostly distinct, but apple_mail_list_mailboxes and apple_mail_unread_summary both expose mailbox lists with unread counts, creating overlap. More critically, apple_calendar_list_calendars is unrelated to the mail domain and sits awkwardly in the same server, making tool selection confusing.

Naming Consistency2/5

Most tools follow the apple_mail_<verb>_<noun> pattern (list_accounts, search_messages), but apple_mail_unread_summary is adjective-noun and apple_calendar_list_calendars breaks the prefix entirely. This mixed convention reduces predictability.

Tool Count4/5

Seven tools is within the desirable range, and each mail tool has a clear job. However, including a calendar tool in a mail server muddies the scope, making the count feel less purposeful.

Completeness2/5

The set covers read-oriented operations (list, search, fetch) and draft creation, but lacks essential mail lifecycle actions like send, delete, move, or mark as read/unread. The calendar tool doesn't fill these gaps and appears arbitrary.

Maintenance

ActivitySlowing
ResponsivenessNo issues