apple-tools-mcp 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -566
- package/lib/mailWrite.js +41 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
# apple-tools-mcp
|
|
2
2
|
|
|
3
|
-
An MCP (Model Context Protocol) server for Apple Mail, Messages, Calendar, and Contacts on macOS. Search them
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Semantic Search**: Find emails, messages, and events using natural language queries
|
|
8
|
-
- **Write Tools (2.0.0)**: Send/reply/forward/draft mail, mark read, archive, trash; send iMessage/SMS; create, edit, remove, and RSVP to calendar events; create, edit, and remove contacts
|
|
9
|
-
- **Safe by Default**: Deletes and multi-recipient sends never run without `confirm`, and every write supports `dry_run`
|
|
10
|
-
- **Vector Indexing**: Uses LanceDB for fast similarity search with local embeddings
|
|
11
|
-
- **Privacy-First**: All processing happens locally on your Mac - no data leaves your machine
|
|
12
|
-
- **Smart Deduplication**: Handles IMAP duplicates, prioritizing INBOX over Junk/Trash
|
|
13
|
-
- **Date Intelligence**: Understands queries like "last week", "yesterday", "March 2024"
|
|
3
|
+
An MCP (Model Context Protocol) server for Apple Mail, Messages, Calendar, and Contacts on macOS. Search them in plain language, send mail and messages, and create, edit, or remove calendar events and contacts. It talks to any MCP client over stdio. Search embeddings and the write bridge stay on your Mac.
|
|
14
4
|
|
|
15
5
|
## Requirements
|
|
16
6
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
7
|
+
- macOS
|
|
8
|
+
- Node.js 18 or later
|
|
9
|
+
- The permissions below, on the Mac that holds the Mail, Messages, Calendar, and Contacts data
|
|
20
10
|
|
|
21
|
-
##
|
|
11
|
+
## Setup
|
|
22
12
|
|
|
23
|
-
### 1. Install
|
|
13
|
+
### 1. Install
|
|
24
14
|
|
|
25
15
|
```bash
|
|
26
16
|
npm install -g apple-tools-mcp
|
|
@@ -34,205 +24,67 @@ cd Apple-Tools-MCP
|
|
|
34
24
|
npm install
|
|
35
25
|
```
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
"command": "node",
|
|
41
|
-
"args": ["/absolute/path/to/Apple-Tools-MCP/index.js"]
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**Mac Mini** stays on a **global npm** install (`npm install -g apple-tools-mcp`) — no git clone on Mini. **MacBook / development** uses the clone above.
|
|
45
|
-
|
|
46
|
-
After **first install** and after **upgrade** (when write tools are present or change), run the permissions command once on the host UI before using write tools — see [step 2b](#2b-grant-automation-for-write-tools-first-run--ship-gate).
|
|
47
|
-
|
|
48
|
-
### 2. Grant Full Disk Access
|
|
49
|
-
|
|
50
|
-
The MCP server needs access to read your Mail, Messages, and Calendar databases.
|
|
51
|
-
|
|
52
|
-
1. First, find your Node.js path by running in Terminal:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
which node
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
This will output something like `/opt/homebrew/bin/node` or `/usr/local/bin/node`
|
|
27
|
+
A global install uses `npx` in the client config below. A clone uses `node` and the full path to `index.js`.
|
|
59
28
|
|
|
60
|
-
|
|
29
|
+
After you install or upgrade, run the permissions command in the next section. `npm install` only prints a reminder. It cannot click Allow for you.
|
|
61
30
|
|
|
62
|
-
|
|
31
|
+
### 2. Permissions
|
|
63
32
|
|
|
64
|
-
|
|
33
|
+
Two grants, and they do different jobs.
|
|
65
34
|
|
|
66
|
-
|
|
35
|
+
**Full Disk Access** lets the server read the Mail, Messages, Calendar, and Contacts databases.
|
|
67
36
|
|
|
68
|
-
|
|
37
|
+
**Automation** lets it send mail, send messages, and change calendar events and contacts through those apps. Mail compose also needs **Accessibility**, because the message body is pasted into Mail's window through System Events.
|
|
69
38
|
|
|
70
|
-
|
|
39
|
+
#### Full Disk Access
|
|
71
40
|
|
|
72
|
-
|
|
41
|
+
1. In Terminal, run `which node` and copy the path it prints.
|
|
42
|
+
2. Open **System Settings → Privacy & Security → Full Disk Access**.
|
|
43
|
+
3. Click **+**.
|
|
44
|
+
4. Press **Cmd+Shift+G**, paste the path, and press Enter.
|
|
45
|
+
5. Select `node` and turn its toggle on.
|
|
73
46
|
|
|
74
|
-
|
|
47
|
+
#### Automation, Accessibility, and System Events
|
|
75
48
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#### First install and upgrade: `apple-tools-mcp permissions`
|
|
79
|
-
|
|
80
|
-
Run this **after first global install** and **after upgrade** when write surfaces are present or change. It probes **`process.execPath`** (the `node` running the command) so macOS can pop **Allow** dialogs for Contacts, Calendar, Mail, Messages, and System Events in one sitting. You click **Allow**; the command cannot grant silently. Already-granted surfaces report OK without another click. Missing grants print a report and the process **exits non-zero** (fail closed).
|
|
81
|
-
|
|
82
|
-
Use the **same `node` the product uses** — not the MCP host app. Always invoke so **`process.execPath`** is that node. A bare `apple-tools-mcp` (or a path like `~/.nvm/versions/node/v22.21.1/bin/apple-tools-mcp`) can have a shebang that starts a **different** node; Allow dialogs attach to `execPath`, not the CLI path you typed.
|
|
49
|
+
Run this from **Terminal.app**, so the prompts attach to node and not to your editor or chat app:
|
|
83
50
|
|
|
84
51
|
```bash
|
|
85
|
-
# Preferred: execPath matches the product node
|
|
86
52
|
$(which node) $(which apple-tools-mcp) permissions
|
|
87
|
-
|
|
88
|
-
# After `npm install -g apple-tools-mcp` on that node (same idea):
|
|
89
|
-
node "$(dirname "$(which node)")/../lib/node_modules/apple-tools-mcp/index.js" permissions
|
|
90
|
-
npx apple-tools-mcp permissions
|
|
91
|
-
|
|
92
|
-
# From a clone:
|
|
93
|
-
npm run permissions
|
|
94
53
|
```
|
|
95
54
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- **Mini:** `/Users/petercoates/.local/node/bin/node` (global npm / indexer LaunchAgent)
|
|
99
|
-
- **MacBook:** `/Users/petercoates/.nvm/versions/node/v22.21.1/bin/node` (Claude’s nvm `node`, **not** Homebrew)
|
|
100
|
-
|
|
101
|
-
**MacBook (default for `permissions`):** run from **Terminal.app**. Click **Allow** for the **printed `process.execPath`**. Confirm **System Settings → Privacy & Security → Automation** for that node → Contacts, Calendar, Mail, Messages, and System Events. Do **not** start `apple-tools-indexer` — the always-on indexer / write bridge is a **Mini** install only.
|
|
102
|
-
|
|
103
|
-
**Mini:** use the [Mini ship-gate](#mini-ship-gate-host-setup) path (LaunchAgent-owned node / `writer.sock`). Missing-grant copy mentions the write bridge only when that socket is present.
|
|
104
|
-
|
|
105
|
-
On the host UI (Mini Screen Sharing or MacBook local), open **System Settings → Privacy & Security → Automation**, then run the command. Click **Allow** for **`node`** → Contacts, Calendar, Mail, Messages, and System Events. npm `postinstall` only **prints a reminder** — it does not run the probes unattended.
|
|
106
|
-
|
|
107
|
-
This is a real Apple Events pass: Mail uses `make new outgoing message` (compose then discard; nothing is sent). Messages enumerates accounts (nothing is sent). `dry_run` of `mail_send` / `messages_send` never talks to those apps and **does not count**. Contacts creates and deletes a throwaway person in-script. Calendar lists calendars only (no leftover events). System Events is probed with **process / GUI scripting** (`tell process` + `unix id` of System Events; nothing is typed) so the keystroke compose path can pop **Allow** and verify a real Automation grant. A soft `tell application "System Events" to get name` is **not** enough — that can succeed while TCC still has node → System Events **denied**. If a prior **Don't Allow** left a sticky Deny, `permissions` reports System Events **missing** and exits non-zero: turn **System Events** on for that node in System Settings → Privacy & Security → **Automation**, then **re-run `apple-tools-mcp permissions` after Allow**.
|
|
108
|
-
|
|
109
|
-
**One-pass first-run:** Allow **`node`** to control **Mail**, **Messages**, **Contacts**, **Calendar**, and **System Events** in the same Automation pass. Contacts or Calendar being allowed does **not** grant Mail, Messages, or System Events — they are separate Apple Events targets. A denied Mail grant hangs `mail_send` / `mail_draft` / `mail_reply` / `mail_forward` until the client disconnects; `dry_run` never talks to Mail, so that deny is invisible until a real compose.
|
|
110
|
-
|
|
111
|
-
**Do not add `node` via the + button in System Settings → Privacy & Security → Contacts or Calendars.** On current macOS those panes often have **no Add button**, and that instruction is not the ship-gate setup — it failed on the Mini.
|
|
55
|
+
From a clone, `npm run permissions` does the same thing. `npx apple-tools-mcp permissions` also works.
|
|
112
56
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
This is the first-run flow for Mail, Messages, Contacts, and Calendar **writes** on the Mac Mini. Do it on the Mini UI (or Screen Sharing to Mini), with the indexer LaunchAgent owning `node`:
|
|
116
|
-
|
|
117
|
-
1. Open **System Settings → Privacy & Security → Automation**.
|
|
118
|
-
2. Run write prove-out with the **indexer LaunchAgent** owning `node` (`~/.apple-tools-mcp/writer.sock` / launchd). Against the tip, with the LaunchAgent up:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
npm run smoke:writes -- --apply
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
That is the ship-gate context. An embedded agent shell, IDE terminal, or MCP host app subprocess is **not** the ship-gate host unless the write bridge is up and the work executes inside launchd-owned `node`.
|
|
125
|
-
3. When prompts appear, click **Allow** for **`node`** to control **Mail**, **Messages**, **Contacts**, **Calendar**, and **System Events** (needed for `mail_send` / `mail_draft` keystrokes under the LaunchAgent). Watch the host — Allow **`node`** (the LaunchAgent binary), not any other app. Use whatever path the LaunchAgent plist / `which node` reports — `/Users/petercoates/.local/node/bin/node` is a Mini *example* only, not a universal path. Do **not** approve the MCP client / host app that launched a short-lived stdio server.
|
|
126
|
-
4. **Keep Contacts, Mail, and Messages running** on the write host (leave the apps open; do not quit them). Cold `tell application "Contacts"` under launchd often fails with `-600` / “application isn't running” instead of auto-launching — that is **not** an Automation deny. The write path launches Contacts.app before CRUD, but write reliability still requires those three apps to stay running. **Calendar does not need to stay open** (Calendar writes go through EventKit).
|
|
127
|
-
5. **Full Disk Access** on `node` is a separate grant and covers **reads** (Mail / Messages / Calendar / AddressBook databases). Write tools need the Automation / Apple Events grants to Mail.app (`mail_send`, `mail_draft`, `mail_reply`, `mail_forward`, and the other mail writes), Messages.app (`messages_send`), Contacts.app, and Calendar.app.
|
|
128
|
-
6. npm Trusted Publisher / publish tokens are unrelated to TCC. Do not confuse them with this setup.
|
|
129
|
-
|
|
130
|
-
If you dismissed a prompt, re-open **Automation** and turn the **node → Mail** / **node → Messages** / **node → Contacts** / **node → Calendar** toggles back on. `tccutil reset AppleEvents` re-arms the Automation prompt so you can Allow **`node`** again. Do not use `tccutil reset AddressBook` / `tccutil reset Calendar`, and do not add `node` via Settings **+** into the Contacts or Calendars privacy lists — those panes often have no Add button, and that is not how this ship gate is granted.
|
|
131
|
-
|
|
132
|
-
#### `dry_run` does not prove Mail Automation
|
|
133
|
-
|
|
134
|
-
`mail_send` / `mail_draft` / `mail_reply` / `mail_forward` with `dry_run=true` return immediately and never send Apple events to Mail — dry_run never talks to Mail. A TCC deny for **node → Mail** is therefore invisible until a real compose (`make new outgoing message`). The hang is **Automation denied**, not “Mail.app could not be reached” / “app not available”.
|
|
135
|
-
|
|
136
|
-
`tell application "Mail" to get name` can succeed while compose still hangs. The smoke test’s Mail step runs that real compose (then discards the outgoing message, or on an older daemon saves a clearly named Draft) so the deny fails **setup**, not a later production `mail_send`. The same Allow-via-prompt applies to **Messages** for `messages_send`; `messages_send` with `dry_run=true` likewise never talks to Messages.app. Smoke also live-enumerates Messages accounts (nothing is sent). **`--apply` fails closed if Mail, Messages, Contacts, or Calendar Automation is missing.**
|
|
137
|
-
|
|
138
|
-
#### Which process macOS is actually asking about
|
|
139
|
-
|
|
140
|
-
This is the part that decides whether writes work on your setup. macOS attributes an Apple event to the **responsible process**, not to whichever binary sent it. When an MCP client launches this server over stdio, the *client app* is responsible for everything node does — so the grant that matters belongs to the host app, not to node.
|
|
141
|
-
|
|
142
|
-
#### Reads and writes are different mechanisms
|
|
143
|
-
|
|
144
|
-
Worth separating, because they fail for different reasons and have different fixes. This applies to **both** Contacts and Calendar:
|
|
145
|
-
|
|
146
|
-
| | **Reads** (`contacts_search`, `contacts_lookup`, `calendar_date`, `calendar_free_time`, indexing) | **Writes** (`contacts_add/edit/remove`, `calendar_add/edit/remove/rsvp`) |
|
|
147
|
-
|---|---|---|
|
|
148
|
-
| How | sqlite query straight against `AddressBook-v22.abcddb` / `Calendar.sqlitedb` | Contacts.app (`CNContactStore`) / Calendar.app (EventKit) |
|
|
149
|
-
| Gated by | **Full Disk Access** on the responsible process | The **AddressBook** / **calendars** privacy classes, which require the host to hold `com.apple.security.personal-information.addressbook` / `….calendars` |
|
|
150
|
-
| Typical failure | `EPERM` / "unable to open database" | denial with **no prompt at all** |
|
|
151
|
-
| Fix | grant FDA to the responsible process | run the write where node is the responsible process |
|
|
152
|
-
|
|
153
|
-
So an `EPERM` reading `~/Library/Application Support/AddressBook/Sources/…` is almost always a Full Disk Access or attribution problem — **not** evidence of the entitlement gap. The server labels it that way in its logs so the two do not get conflated.
|
|
154
|
-
|
|
155
|
-
The entitlement gap bites on the **write** path: **a host app that cannot be granted Contacts or Calendars access blocks that CRUD no matter what node is allowed to do.**
|
|
156
|
-
|
|
157
|
-
Claude Desktop is the documented example. A `codesign` dump of the shipped app shows it carries **neither** personal-information entitlement — the only ones present are location and photos-library:
|
|
57
|
+
The command prints `process.execPath`. That path is the node binary macOS will ask about. Click **Allow** for that binary. If it warns that the command you typed started a different node, run it again with the printed path in front:
|
|
158
58
|
|
|
159
59
|
```bash
|
|
160
|
-
|
|
161
|
-
# no com.apple.security.personal-information.addressbook
|
|
162
|
-
# no com.apple.security.personal-information.calendars
|
|
60
|
+
/absolute/path/to/node $(which apple-tools-mcp) permissions
|
|
163
61
|
```
|
|
164
62
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
**So: Claude Desktop Contacts *and* Calendar CRUD are unsupported host limitations, not package defects.** The ship gate for both is the **Node host — Mini with Full Disk Access** (plus any Automation grants), which is exactly what the write bridge below makes available to every client.
|
|
168
|
-
|
|
169
|
-
#### The fix: let the indexer daemon own the writes
|
|
170
|
-
|
|
171
|
-
The **indexer daemon** is started by launchd, so node is the responsible process for its Apple events and macOS can grant AddressBook and Calendars access to node directly.
|
|
172
|
-
|
|
173
|
-
From 2.0.0, the daemon therefore doubles as a **write bridge**. When it is running, any MCP stdio process hands privacy-gated writes to it over a user-only unix socket at `~/.apple-tools-mcp/writer.sock` (mode 0600, inside your 0700 app directory — local only, nothing on the network). The daemon performs the write under its own TCC identity and returns what happened.
|
|
174
|
-
|
|
175
|
-
So the supported configuration for writes is: **run the indexer daemon** ([LaunchAgent setup below](#always-on-indexer-mac-mini-launchagent)) on the Mac that owns the data. Then:
|
|
176
|
-
|
|
177
|
-
| Host | Reads (incl. Contacts + Calendar) | Mail / Messages writes | Calendar CRUD | Contacts CRUD |
|
|
178
|
-
|------|-----------------------------------|------------------------|---------------|----------------|
|
|
179
|
-
| Node host: indexer daemon or `node index.js` from a terminal | Yes (FDA on node) | Yes | **Yes — ship gate host** | **Yes — ship gate host** |
|
|
180
|
-
| Any stdio client **with the daemon running** | Yes | Yes (via the bridge) | Yes (via the bridge) | Yes (via the bridge) |
|
|
181
|
-
| Claude Desktop, **no daemon running** | Yes — reads are sqlite + FDA, unaffected by the entitlements | Yes, if Claude is granted Automation for Mail/Messages | **No — host limitation** (no calendars entitlement) | **No — host limitation** (no addressbook entitlement) |
|
|
182
|
-
|
|
183
|
-
If a write is denied and no write-bridge socket is listening, the tool uses **Terminal.app + printed `process.execPath` Automation** copy — it does **not** tell MacBook users to start `apple-tools-indexer`. On Mini, when `writer.sock` is up, missing-grant copy may mention the LaunchAgent / write bridge. Contacts and Calendar denials each name their own privacy class and note that reads are unaffected.
|
|
63
|
+
Allow **node** to control Mail, Messages, Contacts, Calendar, and System Events. Each app is its own switch. Allowing Contacts does not allow Mail.
|
|
184
64
|
|
|
185
|
-
|
|
65
|
+
Then open **System Settings → Privacy & Security → Accessibility** and turn on the same node binary. Without that, Mail compose cannot paste the body.
|
|
186
66
|
|
|
187
|
-
The
|
|
67
|
+
The command does real work so the prompts can appear. It opens a Mail compose window and throws it away (nothing is sent), lists Messages accounts (nothing is sent), creates and deletes a throwaway contact, lists calendars without creating events, and checks System Events. It exits with an error until Mail, Messages, Contacts, Calendar, and System Events are all allowed.
|
|
188
68
|
|
|
189
|
-
|
|
69
|
+
`dry_run` on `mail_send` or `messages_send` does not talk to those apps, so a dry run will not pop the prompts and will not tell you whether Automation is allowed. A real send that hangs after you denied Mail is an Automation denial, not "Mail could not be reached."
|
|
190
70
|
|
|
191
|
-
|
|
71
|
+
If you clicked **Don't Allow**, macOS will not ask again. Open **System Settings → Privacy & Security → Automation**, turn the switch on for that node and that app, then run `apple-tools-mcp permissions` again.
|
|
192
72
|
|
|
193
|
-
|
|
194
|
-
pgrep -fl apple-tools-indexer
|
|
195
|
-
ls -l ~/.apple-tools-mcp/writer.sock
|
|
196
|
-
```
|
|
73
|
+
Do not add node with the **+** button under **Privacy & Security → Contacts** or **Calendars**. Those lists are not how these writes are granted, and on current macOS they often have no Add button.
|
|
197
74
|
|
|
198
|
-
|
|
75
|
+
Leave **Mail, Messages, and Contacts** running. If they are quit, writes to them often fail even when Automation is allowed. Calendar does not need to stay open.
|
|
199
76
|
|
|
200
|
-
|
|
77
|
+
#### If your chat app cannot hold the grants
|
|
201
78
|
|
|
202
|
-
|
|
203
|
-
npm run smoke:writes
|
|
204
|
-
```
|
|
79
|
+
macOS treats these actions as coming from the app that launched the server. A chat app that starts a short-lived MCP process is that app. Some of them cannot be granted Contacts or Calendar access, no matter what you allow for node.
|
|
205
80
|
|
|
206
|
-
|
|
81
|
+
Run the indexer in the next section. It is a normal node process, so the Automation grants you gave node apply. Your MCP client sends writes to it through a local socket at `~/.apple-tools-mcp/writer.sock`. When a prompt appears, allow **node**, not the chat app.
|
|
207
82
|
|
|
208
|
-
|
|
209
|
-
node scripts/smoke-writes.js --apply # first writable calendar
|
|
210
|
-
node scripts/smoke-writes.js --apply --calendar=Work # pick the calendar
|
|
211
|
-
node scripts/smoke-writes.js --apply --keep # leave the test items behind
|
|
212
|
-
```
|
|
83
|
+
### 3. Connect your MCP client
|
|
213
84
|
|
|
214
|
-
|
|
85
|
+
Any stdio MCP client can run the server. The command is the same; only the client's settings screen changes.
|
|
215
86
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
node scripts/smoke-writes.js --apply --allow-local # only from Terminal.app / launchd
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Expected results: **PASS on the Node host with the bridge up — this is the ship gate for Mail, Messages, Contacts, and Calendar writes.** `--apply` fails closed if any of those four Automation grants is missing. On Claude Desktop with no daemon running, Contacts and Calendar CRUD are both expected to fail; that is the documented host limitation above, not a regression. Mail and Messages still need their own **node → Mail** / **node → Messages** grants; a Contacts/Calendar grant does not cover them.
|
|
223
|
-
|
|
224
|
-
### 3. Configure your MCP client
|
|
225
|
-
|
|
226
|
-
This server speaks MCP over **stdio**. Any compatible client can run it — Claude Desktop is one example, not the only one. Other stdio MCP clients work the same way: register the command below in that client's MCP settings.
|
|
227
|
-
|
|
228
|
-
**Command**
|
|
229
|
-
|
|
230
|
-
- `npx` with args `["-y", "apple-tools-mcp"]` (npm install)
|
|
231
|
-
- or `node` with args `["/absolute/path/to/Apple-Tools-MCP/index.js"]` (from source)
|
|
232
|
-
|
|
233
|
-
**Example: Claude Desktop**
|
|
234
|
-
|
|
235
|
-
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
87
|
+
Global install:
|
|
236
88
|
|
|
237
89
|
```json
|
|
238
90
|
{
|
|
@@ -245,96 +97,27 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
245
97
|
}
|
|
246
98
|
```
|
|
247
99
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
MCP clients are **short-lived stdio** processes: they exit when the client closes stdin. Always-on indexing belongs on the **indexer daemon**, not a sleep-pipe wrapper around this binary.
|
|
251
|
-
|
|
252
|
-
### 4. Restart your MCP client
|
|
100
|
+
From a clone, use `node` and the absolute path to `index.js` instead of `npx`.
|
|
253
101
|
|
|
254
|
-
|
|
102
|
+
Claude Desktop keeps this in `~/Library/Application Support/Claude/claude_desktop_config.json`. Quit the client completely and reopen it after you save.
|
|
255
103
|
|
|
256
|
-
|
|
104
|
+
The MCP process is short-lived: it exits when the client disconnects. Ongoing indexing belongs on the indexer, not on a wrapper that holds this process open.
|
|
257
105
|
|
|
258
|
-
|
|
106
|
+
### 4. Indexer
|
|
259
107
|
|
|
260
|
-
|
|
108
|
+
On first use the server builds a search index of your mail, messages, and calendar events. That can take a while if you have a lot of mail. The index is stored in `~/.apple-tools-mcp/vector-index/`.
|
|
261
109
|
|
|
262
|
-
|
|
263
|
-
- **No daemon** (default MacBook / Claude Desktop / Cursor): the MCP stdio process indexes **locally on startup**, same as previous versions, then exits when the client disconnects.
|
|
264
|
-
|
|
265
|
-
You can manually rebuild the index (stop the indexer daemon first if it is running, so it is not writing at the same time):
|
|
110
|
+
Run the indexer so the index stays current and so writes go through node:
|
|
266
111
|
|
|
267
112
|
```bash
|
|
268
|
-
|
|
269
|
-
npm run build-index
|
|
270
|
-
|
|
271
|
-
# Optional: cap email lookback (e.g. for a faster test rebuild)
|
|
272
|
-
APPLE_TOOLS_INDEX_DAYS_BACK=30 npm run build-index
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
The index is stored in `~/.apple-tools-mcp/vector-index/`.
|
|
276
|
-
|
|
277
|
-
## Index refresh interval
|
|
278
|
-
|
|
279
|
-
Resolved **once at process start**. Precedence (highest wins):
|
|
280
|
-
|
|
281
|
-
1. `INDEX_INTERVAL_MS` environment variable (milliseconds or human form: `30s`, `1m`, `5m`, `1h`)
|
|
282
|
-
2. `~/.apple-tools-mcp/config.json` keys `indexInterval` or `indexIntervalMs`
|
|
283
|
-
3. Product default: **5 minutes** (`300000` ms) — typical MacBook / MCP local-fallback
|
|
284
|
-
|
|
285
|
-
Values are **clamped** to **15 seconds** minimum and **6 hours** maximum. Invalid JSON, unknown keys, and unparseable intervals are logged and ignored (the process does not crash). The effective interval is logged at start, for example:
|
|
286
|
-
|
|
287
|
-
```text
|
|
288
|
-
Effective index refresh interval: 1m (60000 ms) [source=config]
|
|
113
|
+
apple-tools-indexer
|
|
289
114
|
```
|
|
290
115
|
|
|
291
|
-
A
|
|
116
|
+
That command is `node index.js --mode=indexer`. A global install provides `apple-tools-indexer` (`bin/apple-tools-indexer.js`). From a clone, use `npm run indexer`.
|
|
292
117
|
|
|
293
|
-
|
|
118
|
+
To keep it running across logins, use a LaunchAgent. LaunchAgents do not see your shell `PATH`, so put in the absolute paths from `which node` and `npm root -g`.
|
|
294
119
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
```json
|
|
298
|
-
{
|
|
299
|
-
"indexInterval": "1m"
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Equivalent: `"indexIntervalMs": 60000`, or `INDEX_INTERVAL_MS=60000` (env overrides the file).
|
|
304
|
-
|
|
305
|
-
Missing `config.json` is fine — env then the 5-minute default apply.
|
|
306
|
-
|
|
307
|
-
## Always-on indexer (Mac Mini LaunchAgent)
|
|
308
|
-
|
|
309
|
-
On Mini, run the **indexer daemon**, not a sleep-pipe wrapper around `apple-tools-mcp`. Claude Desktop and other clients still attach via short-lived stdio MCP (`npx -y apple-tools-mcp` or the global `apple-tools-mcp` bin).
|
|
310
|
-
|
|
311
|
-
The daemon does two jobs: it refreshes the vector index, and it serves the **write bridge** at `~/.apple-tools-mcp/writer.sock` so stdio clients can perform Mail / Messages / Contacts / Calendar writes that their host app cannot be granted (see [step 2b](#2b-grant-automation-for-write-tools-first-run--ship-gate)). When macOS prompts, Allow **`node`** (the LaunchAgent binary) to control Mail.app, Messages.app, Contacts.app, Calendar.app, **and System Events** (mail_send / mail_draft fill the body via AX hit-test). Do not approve the MCP client / host app that launched a short-lived stdio server, and do not try to add `node` via **+** in the Contacts or Calendars privacy lists.
|
|
312
|
-
|
|
313
|
-
**Keep Contacts.app, Mail.app, and Messages.app running all the time** on the Mini (and on any MacBook that does local writes). Quitting them makes Apple Events to those apps unreliable (`-600` / “application isn't running”), which is a cold-launch miss — not a missing Automation grant. The Contacts write path launches Contacts.app before add/edit/remove, but do not rely on that instead of leaving the apps open. **Calendar.app does not need to stay open**; Calendar writes use EventKit.
|
|
314
|
-
|
|
315
|
-
The indexer does **not** post product Notification Center / `display notification` / UserNotifications toasts. `Apple Tools MCP indexer running` is a **stderr** line only (the plist redirects it to a file). That does **not** silence macOS Background Items or “running in the background” notices for `node` / the LaunchAgent — those are OS notices, and this package does not suppress them. It also does not suppress OS Allow dialogs. The [permissions command](#2b-grant-automation-for-write-tools-first-run--ship-gate) still needs those dialogs.
|
|
316
|
-
|
|
317
|
-
The bridge is created before the daemon touches the vector index, so writes stay available even when the index is missing, locked, or mid-rebuild. Confirm it after an upgrade with `ls -l ~/.apple-tools-mcp/writer.sock` (it should be a `srw-------` socket); the daemon removes it on shutdown.
|
|
318
|
-
|
|
319
|
-
**Entrypoint:** `node index.js --mode=indexer`
|
|
320
|
-
**Convenience bin:** `apple-tools-indexer` (`bin/apple-tools-indexer.js`; npm global install provides it)
|
|
321
|
-
**npm script (clone only):** `npm run indexer`
|
|
322
|
-
**One-shot rebuild:** `npm run build-index` (stop the indexer daemon first)
|
|
323
|
-
|
|
324
|
-
LaunchAgent should invoke **node + `--mode=indexer`** on the **global** package (Mini has no git clone). LaunchAgent does not inherit your shell `PATH`, so use absolute paths from `which node` and `npm root -g`.
|
|
325
|
-
|
|
326
|
-
```bash
|
|
327
|
-
which node
|
|
328
|
-
# Use this path (and the LaunchAgent plist). Examples only — not universal:
|
|
329
|
-
# Mini ship-gate host: /Users/petercoates/.local/node/bin/node
|
|
330
|
-
# Apple Silicon Homebrew: /opt/homebrew/bin/node
|
|
331
|
-
# Intel Homebrew / usr/local: /usr/local/bin/node
|
|
332
|
-
|
|
333
|
-
npm root -g
|
|
334
|
-
# Example: /opt/homebrew/lib/node_modules
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Example `~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist`:
|
|
120
|
+
Save this as `~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist` and replace both paths:
|
|
338
121
|
|
|
339
122
|
```xml
|
|
340
123
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -345,19 +128,14 @@ Example `~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist`:
|
|
|
345
128
|
<string>com.apple-tools-mcp.indexer</string>
|
|
346
129
|
<key>ProgramArguments</key>
|
|
347
130
|
<array>
|
|
348
|
-
<string>/
|
|
349
|
-
<string>/
|
|
131
|
+
<string>/absolute/path/to/node</string>
|
|
132
|
+
<string>/absolute/path/to/node_modules/apple-tools-mcp/index.js</string>
|
|
350
133
|
<string>--mode=indexer</string>
|
|
351
134
|
</array>
|
|
352
135
|
<key>RunAtLoad</key>
|
|
353
136
|
<true/>
|
|
354
137
|
<key>KeepAlive</key>
|
|
355
138
|
<true/>
|
|
356
|
-
<key>EnvironmentVariables</key>
|
|
357
|
-
<dict>
|
|
358
|
-
<key>PATH</key>
|
|
359
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
360
|
-
</dict>
|
|
361
139
|
<key>StandardOutPath</key>
|
|
362
140
|
<string>/tmp/apple-tools-indexer.out.log</string>
|
|
363
141
|
<key>StandardErrorPath</key>
|
|
@@ -366,54 +144,32 @@ Example `~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist`:
|
|
|
366
144
|
</plist>
|
|
367
145
|
```
|
|
368
146
|
|
|
369
|
-
Replace the node and `node_modules` paths with the values from `which node` and `npm root -g`. Load it with:
|
|
370
|
-
|
|
371
147
|
```bash
|
|
372
148
|
launchctl load ~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist
|
|
373
149
|
```
|
|
374
150
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
## Remote HTTP transport (v3.0.0)
|
|
151
|
+
The refresh interval is chosen once, when the process starts. The default is 5 minutes. Set `INDEX_INTERVAL_MS` (`30s`, `1m`, `1h`, or a number of milliseconds) or add this to `~/.apple-tools-mcp/config.json`:
|
|
378
152
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
Codex Desktop, and ChatGPT Desktop. Claude Desktop uses the included local
|
|
385
|
-
HTTP-to-stdio proxy; Claude Code and Codex connect to the Mini's HTTP
|
|
386
|
-
endpoint directly. ChatGPT Desktop does not currently support this private
|
|
387
|
-
MCP connection; see the client guide for its provider-supported options.
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"indexInterval": "1m"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
388
158
|
|
|
389
|
-
|
|
390
|
-
trust model relies on, **every HTTP request needs a bearer token.** stdio
|
|
391
|
-
has no equivalent check — a locally spawned child process is already
|
|
392
|
-
trusted by whoever spawned it.
|
|
159
|
+
Values are clamped to 15 seconds through 6 hours. A missing config file is fine; the 5-minute default is used.
|
|
393
160
|
|
|
394
|
-
|
|
395
|
-
**Convenience bin:** `apple-tools-http` (`bin/apple-tools-http.js`; npm global install provides it)
|
|
396
|
-
**npm script (clone only):** `npm run http`
|
|
161
|
+
If you do not run the indexer, the MCP process indexes when it starts, then exits when the client disconnects. Writes then run inside that process, and macOS attributes them to the app that launched it.
|
|
397
162
|
|
|
398
|
-
###
|
|
163
|
+
### 5. Another computer (optional)
|
|
399
164
|
|
|
400
|
-
The
|
|
401
|
-
macOS Keychain (service `apple-tools-mcp-http`) — never in a config file,
|
|
402
|
-
the server's config file, or logs after the one-time generation banner.
|
|
403
|
-
There's no external account and no cost; it's a self-issued password,
|
|
404
|
-
like an SSH key.
|
|
165
|
+
The same tools are available over HTTP, with a bearer token on every request. stdio on this Mac does not use the token.
|
|
405
166
|
|
|
406
167
|
```bash
|
|
407
|
-
apple-tools-
|
|
168
|
+
apple-tools-http
|
|
169
|
+
apple-tools-mcp http-token
|
|
408
170
|
```
|
|
409
171
|
|
|
410
|
-
|
|
411
|
-
the Keychain item and restarting the server (it generates a fresh one on
|
|
412
|
-
the next run), then update every client using the old value.
|
|
413
|
-
|
|
414
|
-
### Config
|
|
415
|
-
|
|
416
|
-
`~/.apple-tools-mcp/config.json` (same file the index interval uses):
|
|
172
|
+
`~/.apple-tools-mcp/config.json` (the same file as the index interval):
|
|
417
173
|
|
|
418
174
|
```json
|
|
419
175
|
{
|
|
@@ -422,241 +178,217 @@ the next run), then update every client using the old value.
|
|
|
422
178
|
}
|
|
423
179
|
```
|
|
424
180
|
|
|
425
|
-
|
|
426
|
-
host is `127.0.0.1`, so HTTP is local-only until you explicitly set a network
|
|
427
|
-
address such as `0.0.0.0`. Default port is 8421 (the indexer's stdio process
|
|
428
|
-
needs no port; this is only for `--transport=http`). For remote access, use a
|
|
429
|
-
Tailscale address for clients that connect directly from your tailnet, or
|
|
430
|
-
put the server behind a TLS-terminating reverse proxy. Do not send the
|
|
431
|
-
bearer token over an untrusted LAN. Cloud-hosted connectors need a separate
|
|
432
|
-
way to reach the server; a private Tailscale address alone is insufficient.
|
|
181
|
+
`127.0.0.1` accepts connections only on this Mac. Set `httpHost` to a LAN or Tailscale address when a client on another machine should connect. `APPLE_TOOLS_HTTP_HOST` and `APPLE_TOOLS_HTTP_PORT` override the file. Do not send the token over an open network.
|
|
433
182
|
|
|
434
|
-
|
|
183
|
+
An example LaunchAgent is `examples/com.apple-tools-http.plist`. How to point Claude and other clients at it, including `apple-tools-http-proxy`, is in `examples/mcp-client.md`.
|
|
435
184
|
|
|
436
|
-
|
|
437
|
-
`~/Library/LaunchAgents/`, replace the `REPLACE_ME*` placeholders the same
|
|
438
|
-
way as the indexer plist above, then `launchctl load` it). Runs alongside
|
|
439
|
-
the indexer LaunchAgent, not instead of it — the HTTP server is a second,
|
|
440
|
-
independent process.
|
|
185
|
+
## Index commands
|
|
441
186
|
|
|
442
|
-
|
|
187
|
+
Stop the indexer before a manual rebuild so two processes are not writing the index at once.
|
|
443
188
|
|
|
444
|
-
|
|
189
|
+
```bash
|
|
190
|
+
# Rebuild (all email history)
|
|
191
|
+
npm run build-index
|
|
445
192
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
- **ChatGPT Desktop on the MacBook:** current custom MCP support does not provide a direct private HTTP connection in the desktop app. The client guide describes the documented ChatGPT web route.
|
|
450
|
-
- **Claude / ChatGPT cloud connectors:** a connector does not connect from your device's tailnet interface. [Claude requires a publicly reachable server](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp); [ChatGPT directs private-network servers to Secure MCP Tunnel and currently supports MCP apps on web only](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt). Use the provider's supported network setup before adding this server as a cloud connector.
|
|
193
|
+
# Optional shorter email lookback
|
|
194
|
+
APPLE_TOOLS_INDEX_DAYS_BACK=30 npm run build-index
|
|
195
|
+
```
|
|
451
196
|
|
|
452
|
-
|
|
197
|
+
Your client can also call `rebuild_index`.
|
|
453
198
|
|
|
454
|
-
|
|
199
|
+
Force a clean rebuild when the index is corrupt or out of date:
|
|
455
200
|
|
|
456
|
-
|
|
201
|
+
```bash
|
|
202
|
+
launchctl unload ~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist
|
|
457
203
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
204
|
+
rm -rf ~/.apple-tools-mcp/vector-index
|
|
205
|
+
rm -f ~/.apple-tools-mcp/index-meta.json
|
|
206
|
+
rm -f ~/.apple-tools-mcp/indexer.lock
|
|
207
|
+
```
|
|
462
208
|
|
|
463
|
-
|
|
209
|
+
Start the indexer or your MCP client again so it builds a new index.
|
|
464
210
|
|
|
465
|
-
|
|
466
|
-
|------|-------------|
|
|
467
|
-
| `mail_search` | Semantic search for emails with filters (sender, recipient, attachments, mailbox) |
|
|
468
|
-
| `mail_recent` | Get most recent emails (supports unread filter) |
|
|
469
|
-
| `mail_date` | Get emails from a specific date ("today", "yesterday", "Nov 13") |
|
|
470
|
-
| `mail_read` | Read full email content by file path |
|
|
471
|
-
| `mail_senders` | List most frequent email senders |
|
|
472
|
-
| `mail_thread` | Get all emails in a conversation thread |
|
|
211
|
+
Watch progress. With the LaunchAgent above:
|
|
473
212
|
|
|
474
|
-
|
|
213
|
+
```bash
|
|
214
|
+
tail -f /tmp/apple-tools-indexer.err.log
|
|
215
|
+
```
|
|
475
216
|
|
|
476
|
-
|
|
477
|
-
|------|-------------|
|
|
478
|
-
| `messages_search` | Semantic search for iMessages/SMS with filters |
|
|
479
|
-
| `messages_recent` | Get most recent messages |
|
|
480
|
-
| `messages_conversation` | Get full conversation history with a contact |
|
|
481
|
-
| `messages_contacts` | List all contacts you've messaged |
|
|
217
|
+
Client logs depend on the app. Claude Desktop:
|
|
482
218
|
|
|
483
|
-
|
|
219
|
+
```bash
|
|
220
|
+
tail -f ~/Library/Logs/Claude/mcp-server-apple-tools.log
|
|
221
|
+
```
|
|
484
222
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
| `calendar_free_time` | Find available time slots on a date (live Calendar.app, not the search index) |
|
|
492
|
-
| `calendar_recurring` | List recurring events |
|
|
223
|
+
Audit coverage against the source databases:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npm run audit
|
|
227
|
+
npm run audit -- --reporter=verbose > audit-report.txt
|
|
228
|
+
```
|
|
493
229
|
|
|
494
|
-
|
|
230
|
+
Your client can also call `audit_index`.
|
|
495
231
|
|
|
496
|
-
|
|
497
|
-
|------|-------------|
|
|
498
|
-
| `contacts_search` | Search contacts by name, email, phone, or organization |
|
|
499
|
-
| `contacts_lookup` | Look up a specific contact's full details |
|
|
232
|
+
## Available tools
|
|
500
233
|
|
|
501
|
-
###
|
|
234
|
+
### Universal search
|
|
502
235
|
|
|
503
236
|
| Tool | Description |
|
|
504
237
|
|------|-------------|
|
|
505
|
-
| `
|
|
506
|
-
| `
|
|
507
|
-
|
|
508
|
-
## Write Tools (2.0.0)
|
|
509
|
-
|
|
510
|
-
Write tools change your data. They do not use the vector index, so they keep working while the indexer daemon holds the lock.
|
|
238
|
+
| `smart_search` | Search across mail, messages, and calendar. Picks the sources from the question. |
|
|
239
|
+
| `person_search` | Find communication with one person across Mail, Messages, and Calendar. |
|
|
511
240
|
|
|
512
|
-
|
|
241
|
+
### Email
|
|
513
242
|
|
|
514
|
-
|
|
|
515
|
-
|
|
516
|
-
| `
|
|
517
|
-
| `
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
- **Multi-recipient sends always need `confirm: true`** — `mail_send` / `mail_forward` when `to` + `cc` + `bcc` total more than one address, `mail_reply` with `reply_all: true`, and `messages_send` to multiple handles or to a group chat. A single-recipient send runs on the first call.
|
|
523
|
-
- **`mail_draft` is exempt from the recipient rule** because a draft is never delivered.
|
|
524
|
-
- **`dry_run` and confirm-blocked calls are not deliveries.** Planned writes return `ok: false`, `planned: true`, `delivered: false`, and MCP sets `isError: true`, so a client that only checks `isError` cannot treat a preview as sent. The text is `DRY RUN` / `CONFIRMATION REQUIRED` and never `verified in Sent`. Re-run with `dry_run=false` (and `confirm=true` when required) to actually send.
|
|
525
|
-
- Responses name what happened (tool, ids, recipients, titles) and never echo message bodies — including on the error path.
|
|
526
|
-
- Writes never invent data. A missing or malformed `message_id`, `event_id`, `contact_id`, `chat_id`, or recipient is refused with a message saying so. Calendar times must be explicit local datetimes (`YYYY-MM-DD HH:MM`); natural language such as "next Tuesday" is rejected for writes.
|
|
527
|
-
|
|
528
|
-
### Mail write tools
|
|
243
|
+
| Tool | Description |
|
|
244
|
+
|------|-------------|
|
|
245
|
+
| `mail_search` | Semantic search, with filters for sender, recipient, attachments, and mailbox. |
|
|
246
|
+
| `mail_recent` | Most recent emails. Can limit to unread. |
|
|
247
|
+
| `mail_date` | Emails from a date such as "today", "yesterday", or "Nov 13". |
|
|
248
|
+
| `mail_read` | Full email content by file path. |
|
|
249
|
+
| `mail_senders` | Most frequent senders. |
|
|
250
|
+
| `mail_thread` | Every email in a conversation. |
|
|
529
251
|
|
|
530
|
-
|
|
531
|
-
|------|-----------|--------------|
|
|
532
|
-
| `mail_send` | `to[]` (required), `cc[]`, `bcc[]`, `subject` (required), `body` (required), `body_format` (`plain` \| `html`) | `confirm` when total recipients > 1 |
|
|
533
|
-
| `mail_draft` | `to[]` (required), `cc[]`, `bcc[]`, `subject`, `body`, `body_format` | none (saved to Drafts, not sent) |
|
|
534
|
-
| `mail_reply` | `message_id` or `file_path`, `body` (required), `reply_all`, `save_as_draft` | `confirm` when `reply_all: true` |
|
|
535
|
-
| `mail_forward` | `message_id` or `file_path`, `to[]` (required), `body`, `save_as_draft` | `confirm` when recipients > 1 |
|
|
536
|
-
| `mail_mark` | `message_id` or `file_path`, `status` (`read` \| `unread`, default `read`) | none |
|
|
537
|
-
| `mail_archive` | `message_id` or `file_path` | none |
|
|
538
|
-
| `mail_trash` | `message_id` or `file_path` | **`confirm` required** |
|
|
252
|
+
### Messages
|
|
539
253
|
|
|
540
|
-
|
|
254
|
+
| Tool | Description |
|
|
255
|
+
|------|-------------|
|
|
256
|
+
| `messages_search` | Semantic search for iMessage and SMS, with filters. |
|
|
257
|
+
| `messages_recent` | Most recent messages. |
|
|
258
|
+
| `messages_conversation` | Full conversation with a contact. |
|
|
259
|
+
| `messages_contacts` | Contacts you have messaged. |
|
|
541
260
|
|
|
542
|
-
|
|
261
|
+
### Calendar
|
|
543
262
|
|
|
544
|
-
|
|
263
|
+
| Tool | Description |
|
|
264
|
+
|------|-------------|
|
|
265
|
+
| `calendar_search` | Semantic search for events, with filters. |
|
|
266
|
+
| `calendar_date` | Events on a date, read live from Calendar rather than the search index. |
|
|
267
|
+
| `calendar_upcoming` | Next upcoming events. |
|
|
268
|
+
| `calendar_week` | Events for this week or a future week. |
|
|
269
|
+
| `calendar_free_time` | Open time on a date, read live from Calendar rather than the search index. |
|
|
270
|
+
| `calendar_recurring` | Recurring events. |
|
|
545
271
|
|
|
546
|
-
|
|
272
|
+
### Contacts
|
|
547
273
|
|
|
548
|
-
|
|
274
|
+
| Tool | Description |
|
|
275
|
+
|------|-------------|
|
|
276
|
+
| `contacts_search` | Search by name, email, phone, or organization. |
|
|
277
|
+
| `contacts_lookup` | Full details for one contact. |
|
|
549
278
|
|
|
550
|
-
|
|
279
|
+
### Index
|
|
551
280
|
|
|
552
|
-
|
|
281
|
+
| Tool | Description |
|
|
282
|
+
|------|-------------|
|
|
283
|
+
| `rebuild_index` | Rebuild the search index for one source or all of them. |
|
|
284
|
+
| `audit_index` | Check index health and coverage. |
|
|
553
285
|
|
|
554
|
-
|
|
286
|
+
## Write tools
|
|
555
287
|
|
|
556
|
-
|
|
288
|
+
Write tools change your data. They do not use the search index, so they work while the indexer is rebuilding.
|
|
557
289
|
|
|
558
|
-
|
|
290
|
+
Every write tool accepts:
|
|
559
291
|
|
|
560
|
-
|
|
292
|
+
| Argument | Meaning |
|
|
293
|
+
|----------|---------|
|
|
294
|
+
| `dry_run` | Preview only. Nothing is sent or changed. This wins even if `confirm` is also set. |
|
|
295
|
+
| `confirm` | Approves an action that is otherwise blocked. |
|
|
561
296
|
|
|
562
|
-
|
|
297
|
+
Deletes need `confirm: true`: `mail_trash`, `calendar_remove`, `contacts_remove`, and `contacts_edit` when it clears every email or phone. One id per call. There is no bulk delete.
|
|
563
298
|
|
|
564
|
-
|
|
299
|
+
Sends to more than one person need `confirm: true`: `mail_send` and `mail_forward` when To, Cc, and Bcc add up to more than one address, `mail_reply` with `reply_all: true`, and `messages_send` to several people or to a group chat. A single recipient sends on the first call. `mail_draft` never sends, so it does not need that confirm.
|
|
565
300
|
|
|
566
|
-
|
|
301
|
+
A dry run or a call that still needs confirm does not deliver anything. The result is `ok: false`, `planned: true`, `delivered: false`, and the client may mark it as an error. The text says `DRY RUN` or `CONFIRMATION REQUIRED`. Run it again with `dry_run` false, and with `confirm: true` when the tool asked for it.
|
|
567
302
|
|
|
568
|
-
|
|
303
|
+
Responses name the action, ids, and recipients. They do not echo message bodies.
|
|
569
304
|
|
|
570
|
-
|
|
305
|
+
A missing or malformed id or recipient is refused. Calendar times are local datetimes, `YYYY-MM-DD HH:MM`. Phrases like "next Tuesday" are rejected on writes.
|
|
571
306
|
|
|
572
|
-
###
|
|
307
|
+
### Mail
|
|
573
308
|
|
|
574
|
-
| Tool | Arguments | Confirm
|
|
575
|
-
|
|
576
|
-
| `
|
|
309
|
+
| Tool | Arguments | Confirm |
|
|
310
|
+
|------|-----------|---------|
|
|
311
|
+
| `mail_send` | `to[]` (required), `cc[]`, `bcc[]`, `subject` (required), `body` (required), `body_format` (`plain` or `html`) | when recipients > 1 |
|
|
312
|
+
| `mail_draft` | `to[]` (required), `cc[]`, `bcc[]`, `subject`, `body`, `body_format` | no (saved to Drafts) |
|
|
313
|
+
| `mail_reply` | `message_id` or `file_path`, `body` (required), `reply_all`, `save_as_draft` | when `reply_all` is true |
|
|
314
|
+
| `mail_forward` | `message_id` or `file_path`, `to[]` (required), `body`, `save_as_draft` | when recipients > 1 |
|
|
315
|
+
| `mail_mark` | `message_id` or `file_path`, `status` (`read` or `unread`, default `read`) | no |
|
|
316
|
+
| `mail_archive` | `message_id` or `file_path` | no |
|
|
317
|
+
| `mail_trash` | `message_id` or `file_path` | yes |
|
|
577
318
|
|
|
578
|
-
|
|
319
|
+
Pass the RFC822 Message-ID, or the `file_path` from `mail_search` or `mail_recent` and the server reads the Message-ID from the file. `mail_archive` moves the message to that account's Archive (or All Mail). `mail_trash` moves it to Trash.
|
|
579
320
|
|
|
580
|
-
|
|
581
|
-
- **`chat_id`** — the chat GUID of an existing conversation, for example `iMessage;-;+15551234567` (1:1) or `iMessage;+;chat123456789` (group). The GUID is checked against `~/Library/Messages/chat.db` before anything is sent, so an unknown chat id is refused rather than delivered somewhere unexpected. The same lookup counts participants, which is how group chats are detected for the confirm rule.
|
|
582
|
-
- **`service`** — `auto` (default) tries iMessage and falls back to the SMS relay; `imessage` and `sms` pin the service.
|
|
583
|
-
- **`attachment_path`** — an absolute path to a file that already exists on this Mac. Text and attachment can be sent together.
|
|
321
|
+
`body_format: "html"` pastes the text into Mail's compose window. Mail may still create its own HTML version. New mail is a new message, not a quoted reply. Reply and forward still include the original.
|
|
584
322
|
|
|
585
|
-
|
|
323
|
+
A send is successful only after the message is in **Sent** or still in **Outbox**. If the call times out, check Sent before you try again. Retrying a message that already went out sends a second copy. A timeout is not the same thing as a denied Automation prompt.
|
|
586
324
|
|
|
587
|
-
|
|
588
|
-
|------|-----------|--------------|
|
|
589
|
-
| `calendar_list_calendars` | none | none (read-only helper) |
|
|
590
|
-
| `calendar_add` | `calendar_name` (required), `title` (required), `start` (required), `end`, `all_day`, `location`, `notes`, recurrence args, `alerts_minutes_before[]` | none |
|
|
591
|
-
| `calendar_edit` | `event_id` (required), optional `eventkit_id` from add, plus any of `title`, `start`, `end`, `location`, `notes`, recurrence args, `alerts_minutes_before[]`, `replace_alerts` | none |
|
|
592
|
-
| `calendar_remove` | `event_id` (required), optional `eventkit_id` from add, optional `calendar_name` | **`confirm` required** |
|
|
593
|
-
| `calendar_rsvp` | `event_id` (required), `response` (`accept` \| `decline` \| `tentative`), `attendee_email` | none |
|
|
325
|
+
Compose needs the Accessibility and System Events grants from the permissions section.
|
|
594
326
|
|
|
595
|
-
|
|
327
|
+
### Messages
|
|
596
328
|
|
|
597
|
-
|
|
329
|
+
| Tool | Arguments | Confirm |
|
|
330
|
+
|------|-----------|---------|
|
|
331
|
+
| `messages_send` | `to[]` or `chat_id`, `text`, `attachment_path`, `service` (`auto`, `imessage`, or `sms`) | several handles, or a group chat |
|
|
598
332
|
|
|
599
|
-
|
|
333
|
+
- `to` is a phone number in E.164 form (`+15551234567`) or an Apple ID email.
|
|
334
|
+
- `chat_id` is an existing conversation id, such as `iMessage;-;+15551234567` or `iMessage;+;chat123456789`. It is checked against Messages before anything is sent. That lookup is also how group chats are detected.
|
|
335
|
+
- `service`: `auto` (default) tries iMessage and can fall back to SMS; `imessage` and `sms` pin the service.
|
|
336
|
+
- `attachment_path` is an absolute path to a file that already exists on this Mac. Text and a file can go together.
|
|
600
337
|
|
|
601
|
-
|
|
338
|
+
### Calendar
|
|
602
339
|
|
|
603
|
-
|
|
340
|
+
| Tool | Arguments | Confirm |
|
|
341
|
+
|------|-----------|---------|
|
|
342
|
+
| `calendar_list_calendars` | none | no. Lists calendars so you can pick one. |
|
|
343
|
+
| `calendar_add` | `calendar_name` (required), `title` (required), `start` (required), `end`, `all_day`, `location`, `notes`, recurrence, `alerts_minutes_before[]` | no |
|
|
344
|
+
| `calendar_edit` | `event_id` (required), optional `eventkit_id` from add, plus any of `title`, `start`, `end`, `location`, `notes`, recurrence, `alerts_minutes_before[]`, `replace_alerts` | no |
|
|
345
|
+
| `calendar_remove` | `event_id` (required), optional `eventkit_id` from add, optional `calendar_name` | yes |
|
|
346
|
+
| `calendar_rsvp` | `event_id` (required), `response` (`accept`, `decline`, or `tentative`), `attendee_email` | no |
|
|
604
347
|
|
|
605
|
-
|
|
348
|
+
`calendar_date` and `calendar_add` report the event id. Pass that id to edit, remove, and RSVP. Run `calendar_list_calendars` first so a new event lands on the calendar you mean. `calendar_add` also returns `eventkit_id`; pass it back on edit and remove.
|
|
606
349
|
|
|
607
|
-
|
|
350
|
+
Recurrence is either structured fields or one raw rule:
|
|
608
351
|
|
|
609
352
|
| Argument | Values |
|
|
610
353
|
|----------|--------|
|
|
611
354
|
| `frequency` | `daily`, `weekly`, `monthly`, `yearly` |
|
|
612
|
-
| `interval` | 1
|
|
613
|
-
| `count` | 1
|
|
614
|
-
| `until` |
|
|
615
|
-
| `by_day` | `MO TU WE TH FR SA SU`
|
|
616
|
-
| `recurrence` | raw
|
|
617
|
-
|
|
618
|
-
`{ frequency: "weekly", interval: 2, by_day: ["MO","WE"], count: 10 }` becomes `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;COUNT=10`. Anything outside this grammar is refused.
|
|
355
|
+
| `interval` | 1–366 |
|
|
356
|
+
| `count` | 1–1000 occurrences. Do not combine with `until`. |
|
|
357
|
+
| `until` | local datetime |
|
|
358
|
+
| `by_day` | `MO` `TU` `WE` `TH` `FR` `SA` `SU` |
|
|
359
|
+
| `recurrence` | a raw rule such as `FREQ=WEEKLY;INTERVAL=1;COUNT=10` |
|
|
619
360
|
|
|
620
|
-
|
|
361
|
+
`alerts_minutes_before` takes up to five values, from 0 to 40320 minutes (four weeks). On edit, supplying alerts replaces the existing ones. `replace_alerts: true` removes them without adding new ones.
|
|
621
362
|
|
|
622
|
-
|
|
363
|
+
`calendar_rsvp` sets your response on an invitation. If that macOS version will not write it, the tool says so instead of pretending it worked.
|
|
623
364
|
|
|
624
|
-
### Contacts
|
|
365
|
+
### Contacts
|
|
625
366
|
|
|
626
|
-
| Tool | Arguments | Confirm
|
|
627
|
-
|
|
628
|
-
| `contacts_add` | `first_name`, `last_name`, `organization`, `job_title`, `emails[]`, `email_label`, `phones[]`, `phone_label` |
|
|
629
|
-
| `contacts_edit` | `contact_id` (required), any of the
|
|
630
|
-
| `contacts_remove` | `contact_id` (required) |
|
|
367
|
+
| Tool | Arguments | Confirm |
|
|
368
|
+
|------|-----------|---------|
|
|
369
|
+
| `contacts_add` | `first_name`, `last_name`, `organization`, `job_title`, `emails[]`, `email_label`, `phones[]`, `phone_label` | no |
|
|
370
|
+
| `contacts_edit` | `contact_id` (required), any of the add fields, `replace_emails`, `replace_phones` | yes, when replacing with an empty list |
|
|
371
|
+
| `contacts_remove` | `contact_id` (required) | yes |
|
|
631
372
|
|
|
632
|
-
|
|
373
|
+
Use the contact id from `contacts_search`, `contacts_lookup`, or `contacts_add`. Creating a contact needs at least one of `first_name`, `last_name`, or `organization`. Writes go through Contacts, which keeps iCloud in sync.
|
|
633
374
|
|
|
634
|
-
###
|
|
375
|
+
### Examples
|
|
635
376
|
|
|
636
377
|
```jsonc
|
|
637
|
-
// Preview first - nothing is sent
|
|
638
378
|
{ "name": "mail_send", "arguments": { "to": ["a@example.com"], "subject": "Status", "body": "All good", "dry_run": true } }
|
|
639
379
|
|
|
640
|
-
// Single recipient: sends on the first call
|
|
641
380
|
{ "name": "mail_send", "arguments": { "to": ["a@example.com"], "subject": "Status", "body": "All good" } }
|
|
642
381
|
|
|
643
|
-
// Two recipients: blocked until confirmed
|
|
644
382
|
{ "name": "mail_send", "arguments": { "to": ["a@example.com", "b@example.com"], "subject": "Status", "body": "All good", "confirm": true } }
|
|
645
383
|
|
|
646
|
-
// Recurring event with an alert
|
|
647
384
|
{ "name": "calendar_add", "arguments": { "calendar_name": "Work", "title": "Standup", "start": "2026-09-21 09:00", "end": "2026-09-21 09:15", "frequency": "weekly", "by_day": ["MO","TU","WE","TH","FR"], "alerts_minutes_before": [10] } }
|
|
648
385
|
|
|
649
|
-
// Delete: preview, then confirm
|
|
650
386
|
{ "name": "calendar_remove", "arguments": { "event_id": "EVT-UID", "confirm": true } }
|
|
651
387
|
```
|
|
652
388
|
|
|
653
|
-
|
|
389
|
+
This package does not include Reminders, Notes, FaceTime, or Files.
|
|
654
390
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
## Example Queries
|
|
658
|
-
|
|
659
|
-
Ask your MCP client things like:
|
|
391
|
+
## Example questions
|
|
660
392
|
|
|
661
393
|
- "Find emails from John about the quarterly report"
|
|
662
394
|
- "What messages did I get from Mom last week?"
|
|
@@ -666,135 +398,46 @@ Ask your MCP client things like:
|
|
|
666
398
|
- "What's Sarah's phone number?"
|
|
667
399
|
- "Show me all communication with David from last month"
|
|
668
400
|
|
|
669
|
-
## Privacy
|
|
670
|
-
|
|
671
|
-
- **Local Processing**: All embeddings are generated locally using Hugging Face Transformers.js
|
|
672
|
-
- **No Cloud Services**: No data is sent to external servers; the write bridge is a local unix socket, never a network port
|
|
673
|
-
- **Reads are read-only**: Search and lookup tools never modify your data. The [write tools](#write-tools-200) are the only ones that change anything, and they are opt-in per call, with `confirm` required for deletes and multi-recipient sends
|
|
674
|
-
- **No credentials**: The server holds no tokens or passwords. It uses the Mail, Messages, Calendar, and Contacts apps you are already signed into
|
|
675
|
-
- **Your Data**: The vector index is stored locally in your home directory
|
|
676
|
-
|
|
677
|
-
## Troubleshooting
|
|
678
|
-
|
|
679
|
-
### "Authorization denied" errors
|
|
680
|
-
|
|
681
|
-
Ensure Node.js has Full Disk Access (see Installation step 2).
|
|
682
|
-
|
|
683
|
-
### A write tool reports that macOS denied the automation
|
|
684
|
-
|
|
685
|
-
The message names which process macOS was actually asking about. Work through it in this order:
|
|
401
|
+
## Privacy
|
|
686
402
|
|
|
687
|
-
|
|
688
|
-
2. **Mini only:** `pgrep -fl apple-tools-indexer` / `~/.apple-tools-mcp/writer.sock`. If the LaunchAgent is the product host, start it so writes run under launchd-owned node (see [Mini ship-gate](#mini-ship-gate-host-setup)).
|
|
689
|
-
3. **Did you approve the Automation prompts for `node`?** Re-run `apple-tools-mcp permissions` with the product `node` (it prints `process.execPath`). Open System Settings → Privacy & Security → **Automation** and confirm **that printed node** is allowed to control **Mail**, **Messages**, Contacts, Calendar, and **System Events**. A prior **Don't Allow** for System Events is a sticky Deny — the Allow dialog will not appear again; turn **System Events** on for that node, then re-run `permissions` (it must not report granted from a soft name check). On Mini the LaunchAgent binary is whatever the plist / `which node` reports (`/Users/petercoates/.local/node/bin/node` is a Mini *example* only). MacBook Claude nvm is `/Users/petercoates/.nvm/versions/node/v22.21.1/bin/node`, not Homebrew. Watch the host — Allow **`node`**, not any other app. Contacts or Calendar being allowed does **not** grant Mail. A `mail_send` hang is **Automation denied**, not “Mail.app could not be reached”; `dry_run` never talks to Mail so it cannot detect this. Do **not** approve the MCP client / host app that launched a short-lived stdio server, and do **not** try to add `node` via **+** in the Contacts or Calendars privacy lists — those panes often have no Add button. `tccutil reset AppleEvents` re-arms the Automation prompt so you can Allow **`node`** again. Full Disk Access is a separate read grant; npm Trusted Publisher / tokens are unrelated.
|
|
690
|
-
4. **Mini: is the daemon's node binary the one with Full Disk Access?** LaunchAgents do not inherit your shell `PATH`; confirm the plist points at the same path `which node` reports.
|
|
691
|
-
5. **Is it actually the write path?** Contacts or Calendar *reads* failing with `EPERM` is a Full Disk Access / attribution problem, not the entitlement gap — fix FDA for the responsible process. Contacts or Calendar *CRUD* failing with no prompt under Claude Desktop is the [documented host limitation](#reads-and-writes-are-different-mechanisms): Claude.app carries neither the addressbook nor the calendars entitlement. On Mini, start the daemon so the write goes through the bridge. On MacBook, use Terminal.app + the printed node.
|
|
692
|
-
6. **"Contacts.app / Calendar.app could not be reached"** with the app clearly installed is an **Automation / responsible-process** failure, not a missing app — macOS reports a refused Apple event as `-1728` / "can't get application". On MacBook / Terminal, Allow the printed `process.execPath`. On Mini, the write bridge / LaunchAgent may apply. A Contacts `-600` / “application isn't running” is a **cold launch**, not Automation — keep Contacts, Mail, and Messages running; Calendar does not need to stay open.
|
|
693
|
-
7. **`calendar_remove` FAIL after add/edit PASS** is a delete-path failure, not missing Calendar Automation. Same write-bridge RPC. Read the `osascript kind=/error=` line: `kind=timeout` + `ETIMEDOUT` / `-1712` is an iCloud/CalDAV hang or Calendar confirmation dialog — **not** TCC. `EVENTKIT_NOT_FOUND status=4` is writeOnly EventKit failing to see an AppleScript-created event (add must go through EventKit). `-1743` / `-10004` is a real Automation deny. Paste that line if it still FAILs.
|
|
694
|
-
8. **Prove the Mini host itself works** with `node scripts/smoke-writes.js --apply` on the Node host with the LaunchAgent running; it routes through the bridge and separates the read and write mechanisms for you.
|
|
403
|
+
Search and the index read your mail, messages, calendar, and contacts on this Mac. Write tools send messages and change events and contacts. Deletes and multi-recipient sends wait for `confirm`.
|
|
695
404
|
|
|
696
|
-
|
|
405
|
+
Embeddings are computed locally. Nothing is uploaded. The write bridge is a socket in your home directory, not a network port. The server stores no passwords; it uses the Apple apps you are already signed into. The index stays in `~/.apple-tools-mcp/`.
|
|
697
406
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
### Empty search results
|
|
701
|
-
|
|
702
|
-
1. Check that the index was built: `ls ~/.apple-tools-mcp/vector-index/`
|
|
703
|
-
2. Rebuild the index if needed: `npm run build-index`
|
|
704
|
-
|
|
705
|
-
### Server not appearing in the MCP client
|
|
706
|
-
|
|
707
|
-
1. Verify your client config is valid (JSON files must be valid JSON)
|
|
708
|
-
2. Restart the MCP client completely (for Claude Desktop: Cmd+Q, then reopen)
|
|
709
|
-
3. Check the client's MCP logs for errors
|
|
710
|
-
|
|
711
|
-
### Force rebuild the index
|
|
712
|
-
|
|
713
|
-
If the index becomes corrupted or out of sync:
|
|
714
|
-
|
|
715
|
-
```bash
|
|
716
|
-
# If the Mini indexer LaunchAgent is running, unload it first
|
|
717
|
-
# launchctl unload ~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist
|
|
718
|
-
|
|
719
|
-
# Remove existing index files
|
|
720
|
-
rm -rf ~/.apple-tools-mcp/vector-index
|
|
721
|
-
rm -f ~/.apple-tools-mcp/index-meta.json
|
|
722
|
-
rm -f ~/.apple-tools-mcp/indexer.lock
|
|
723
|
-
|
|
724
|
-
# Restart the indexer daemon or your MCP client to trigger a fresh rebuild
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
### Monitor indexing progress
|
|
728
|
-
|
|
729
|
-
Watch the MCP server logs in your client. Log locations vary by client; Claude Desktop example:
|
|
407
|
+
## Troubleshooting
|
|
730
408
|
|
|
731
|
-
|
|
732
|
-
tail -f ~/Library/Logs/Claude/mcp-server-apple-tools.log
|
|
733
|
-
```
|
|
409
|
+
**"Authorization denied" or the databases will not open.** Full Disk Access is missing or it was granted to a different `node` than the one that is running. Repeat the Full Disk Access steps with `which node`.
|
|
734
410
|
|
|
735
|
-
|
|
411
|
+
**A write is denied, or sending mail hangs.** Automation was not granted for the node that is actually running. Run `apple-tools-mcp permissions` from Terminal.app and allow the printed `process.execPath` for Mail, Messages, Contacts, Calendar, and System Events. Allow node, not the chat app. If you previously clicked Don't Allow, turn the switch on in Automation and run the command again. `dry_run` does not talk to Mail, so it will not catch this.
|
|
736
412
|
|
|
737
|
-
|
|
413
|
+
**"CONFIRMATION REQUIRED".** The safety check worked. Pass `confirm: true`, or use `dry_run: true` if you only wanted a preview.
|
|
738
414
|
|
|
739
|
-
|
|
740
|
-
# Quick audit
|
|
741
|
-
npm run audit
|
|
415
|
+
**Search returns nothing.** Check `ls ~/.apple-tools-mcp/vector-index/`. If it is missing or stale, rebuild with the commands above.
|
|
742
416
|
|
|
743
|
-
|
|
744
|
-
npm run audit -- --reporter=verbose > audit-report.txt
|
|
745
|
-
```
|
|
417
|
+
**The server does not show up in the client.** Confirm the config is valid JSON, then quit the client completely and reopen it.
|
|
746
418
|
|
|
747
419
|
## Development
|
|
748
420
|
|
|
749
421
|
```bash
|
|
750
|
-
# Clone the repo
|
|
751
422
|
git clone https://github.com/sfls1397/Apple-Tools-MCP.git
|
|
752
423
|
cd Apple-Tools-MCP
|
|
753
|
-
|
|
754
|
-
# Install dependencies
|
|
755
424
|
npm install
|
|
756
|
-
|
|
757
|
-
# Install test dependencies
|
|
758
|
-
npm install -D vitest @vitest/coverage-v8 fast-check
|
|
759
|
-
|
|
760
|
-
# Run tests
|
|
761
425
|
npm test
|
|
762
|
-
|
|
763
|
-
# Run the indexer daemon (owns indexer.lock + vector-index refresh)
|
|
764
426
|
npm run indexer
|
|
765
|
-
|
|
766
|
-
# One-shot rebuild (stop the indexer daemon first)
|
|
767
427
|
npm run build-index
|
|
768
|
-
|
|
769
|
-
# Run tests with verbose coverage report
|
|
770
|
-
npx vitest run --coverage --reporter=verbose
|
|
771
|
-
|
|
772
|
-
# Run audit to check index health
|
|
773
428
|
npm run audit
|
|
774
|
-
|
|
775
|
-
# Prove the write path on this host (dry run)
|
|
776
429
|
npm run smoke:writes
|
|
777
|
-
|
|
778
|
-
# Real CRUD — the extra -- is required so npm forwards --apply
|
|
779
430
|
npm run smoke:writes -- --apply
|
|
780
431
|
```
|
|
781
432
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
Contributions are welcome! Please:
|
|
785
|
-
|
|
786
|
-
1. Fork the repository
|
|
787
|
-
2. Create a feature branch
|
|
788
|
-
3. Make your changes
|
|
789
|
-
4. Run the tests: `npm test`
|
|
790
|
-
5. Submit a pull request
|
|
433
|
+
`npm run smoke:writes` checks the write path without creating lasting contacts or events. `npm run smoke:writes -- --apply` performs real creates, edits, and deletes, then removes the test items. The extra `--` is required so npm forwards `--apply`. `--apply` fails if Mail, Messages, Contacts, or Calendar Automation is missing.
|
|
791
434
|
|
|
792
435
|
## License
|
|
793
436
|
|
|
794
|
-
MIT License
|
|
437
|
+
MIT License. See [LICENSE](LICENSE).
|
|
795
438
|
|
|
796
439
|
## Acknowledgments
|
|
797
440
|
|
|
798
|
-
-
|
|
799
|
-
-
|
|
800
|
-
-
|
|
441
|
+
- [Model Context Protocol SDK](https://github.com/modelcontextprotocol/sdk)
|
|
442
|
+
- [LanceDB](https://lancedb.com/)
|
|
443
|
+
- [Hugging Face Transformers.js](https://github.com/huggingface/transformers.js)
|
package/lib/mailWrite.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
isHardTccDenial
|
|
32
32
|
} from "./appleScript.js";
|
|
33
33
|
import { isIndexerMode } from "./processMode.js";
|
|
34
|
+
import { safeSpawn } from "./shell.js";
|
|
34
35
|
import {
|
|
35
36
|
planWrite,
|
|
36
37
|
plannedWriteResult,
|
|
@@ -160,6 +161,21 @@ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
|
160
161
|
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
161
162
|
/** Focus request ran, but the re-read AXFocusedUIElement was not the chosen body. */
|
|
162
163
|
export const BODY_FOCUS_UNPROVEN_SENTINEL = "BODY_FOCUS_UNPROVEN";
|
|
164
|
+
/** System Events sees 0 Mail windows (display asleep) while Mail AppleScript still lists the compose. */
|
|
165
|
+
export const DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL = "DISPLAY_ASLEEP_GUI_UNAVAILABLE";
|
|
166
|
+
export const DISPLAY_WAKE_SECONDS = 15;
|
|
167
|
+
export const DISPLAY_WAKE_POLL_ATTEMPTS = 20;
|
|
168
|
+
|
|
169
|
+
/** Declare user activity so a sleeping display wakes before GUI scripting. Best-effort; never throws. */
|
|
170
|
+
export function wakeDisplay() {
|
|
171
|
+
if (process.platform !== "darwin" || process.env.VITEST) return false;
|
|
172
|
+
try {
|
|
173
|
+
return safeSpawn("/usr/bin/caffeinate", ["-u", "-t", String(DISPLAY_WAKE_SECONDS)], { timeout: 5000 }).status === 0;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
/** Mail To: recipient chips surface as U+FFFC (object replacement), not body text. */
|
|
164
180
|
export const MAIL_RECIPIENT_TOKEN_CHAR_ID = 65532;
|
|
165
181
|
export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
@@ -525,17 +541,27 @@ on atmTopElemBits(atmEl)
|
|
|
525
541
|
return atmBits
|
|
526
542
|
end atmTopElemBits
|
|
527
543
|
|
|
544
|
+
-- Mail titles a compose window with the full subject, while the marker is
|
|
545
|
+
-- cut to ${MAIL_COMPOSE_TITLE_MARKER_MAX} characters for long subjects. A marker that length is a
|
|
546
|
+
-- prefix; a shorter one is the whole subject and must match exactly.
|
|
547
|
+
on atmTitleMatches(atmTitle, atmMarker)
|
|
548
|
+
if atmTitle is atmMarker then return true
|
|
549
|
+
if (length of atmMarker) >= ${MAIL_COMPOSE_TITLE_MARKER_MAX} and atmTitle starts with atmMarker then return true
|
|
550
|
+
return false
|
|
551
|
+
end atmTitleMatches
|
|
552
|
+
|
|
528
553
|
on atmTopElemExactTitle(atmEl, atmMarker)
|
|
529
554
|
try
|
|
530
|
-
if (name of atmEl as string
|
|
555
|
+
if my atmTitleMatches(name of atmEl as string, atmMarker) then return true
|
|
531
556
|
end try
|
|
532
557
|
try
|
|
533
|
-
if (title of atmEl as string
|
|
558
|
+
if my atmTitleMatches(title of atmEl as string, atmMarker) then return true
|
|
534
559
|
end try
|
|
535
560
|
try
|
|
536
561
|
tell application "System Events"
|
|
537
|
-
|
|
562
|
+
set atmAxTitle to (value of attribute "AXTitle" of atmEl as string)
|
|
538
563
|
end tell
|
|
564
|
+
if my atmTitleMatches(atmAxTitle, atmMarker) then return true
|
|
539
565
|
end try
|
|
540
566
|
return false
|
|
541
567
|
end atmTopElemExactTitle
|
|
@@ -620,11 +646,16 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
620
646
|
tell application "System Events"
|
|
621
647
|
tell process "Mail"
|
|
622
648
|
set frontmost to true
|
|
649
|
+
repeat ${DISPLAY_WAKE_POLL_ATTEMPTS} times
|
|
650
|
+
if (count of (every window)) > 0 then exit repeat
|
|
651
|
+
delay 0.5
|
|
652
|
+
end repeat
|
|
653
|
+
set atmSeWindowCount to count of (every window)
|
|
623
654
|
delay 0.3
|
|
624
655
|
set atmWin to missing value
|
|
625
656
|
repeat with atmCand in windows
|
|
626
657
|
try
|
|
627
|
-
if (name of atmCand as string
|
|
658
|
+
if my atmTitleMatches(name of atmCand as string, titleMarker) then
|
|
628
659
|
set atmWin to atmCand
|
|
629
660
|
exit repeat
|
|
630
661
|
end if
|
|
@@ -648,6 +679,7 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
648
679
|
end if
|
|
649
680
|
end repeat
|
|
650
681
|
end if
|
|
682
|
+
if atmWin is missing value and atmSeWindowCount is 0 then error "${DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL}"
|
|
651
683
|
if atmWin is missing value then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
652
684
|
try
|
|
653
685
|
perform action "AXRaise" of atmWin
|
|
@@ -734,6 +766,7 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
734
766
|
on error errMsg number errNum
|
|
735
767
|
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
736
768
|
if errMsg contains "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
769
|
+
if errMsg contains "${DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL}" then error "${DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL}"
|
|
737
770
|
error errMsg number errNum
|
|
738
771
|
end try
|
|
739
772
|
end atmFillMailBody`;
|
|
@@ -1282,6 +1315,9 @@ function failure(action, summary, result, secrets) {
|
|
|
1282
1315
|
if (err.includes("gui_scripting_unavailable")) {
|
|
1283
1316
|
return `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
|
|
1284
1317
|
}
|
|
1318
|
+
if (err.includes("display_asleep_gui_unavailable")) {
|
|
1319
|
+
return `${action} failed — attempted to ${summary}. The display is asleep, so System Events cannot see Mail's compose window (GUI unavailable even after a wake attempt); nothing was sent. Wake the display and retry.`;
|
|
1320
|
+
}
|
|
1285
1321
|
if (err.includes("body_focus_unproven")) {
|
|
1286
1322
|
return `${action} failed — attempted to ${summary}. Keyboard focus was not on the message body (focus proof failed); the draft was discarded and nothing was sent.`;
|
|
1287
1323
|
}
|
|
@@ -1470,6 +1506,7 @@ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerM
|
|
|
1470
1506
|
});
|
|
1471
1507
|
|
|
1472
1508
|
const deadlineMs = sentVerifyClock.now() + MCP_CALL_DEADLINE_MS;
|
|
1509
|
+
wakeDisplay();
|
|
1473
1510
|
const result = runAppleScript(script, { timeout: MAIL_COMPOSE_TIMEOUT_MS, appName: "Mail" });
|
|
1474
1511
|
if (
|
|
1475
1512
|
!result.ok &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-tools-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts. Speaks stdio (local) or bearer-token-authenticated Streamable HTTP (LAN / Tailscale).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|