apple-tools-mcp 2.0.1 → 2.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 +54 -13
- package/bin/apple-tools-indexer.js +15 -0
- package/bin/apple-tools-mcp.js +7 -0
- package/index.js +26 -7
- package/indexer.js +4 -1
- package/lib/appleScript.js +90 -26
- package/lib/calendarWrite.js +39 -0
- package/lib/contactsWrite.js +191 -4
- package/lib/mailWrite.js +296 -26
- package/lib/messagesWrite.js +6 -2
- package/lib/permissions.js +360 -0
- package/lib/processMode.js +50 -2
- package/lib/shell.js +27 -0
- package/lib/writeRouting.js +76 -6
- package/lib/writeTools.js +9 -6
- package/package.json +7 -3
- package/scripts/postinstall.js +32 -0
- package/scripts/smoke-writes.js +4 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ If you installed from source, point your MCP client at the local `index.js` inst
|
|
|
43
43
|
|
|
44
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
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
|
+
|
|
46
48
|
### 2. Grant Full Disk Access
|
|
47
49
|
|
|
48
50
|
The MCP server needs access to read your Mail, Messages, and Calendar databases.
|
|
@@ -73,6 +75,37 @@ Full Disk Access covers the **read** tools. Write tools need the automation perm
|
|
|
73
75
|
|
|
74
76
|
Write tools drive Mail, Messages, Calendar, and Contacts through AppleScript. macOS gates those Apple events behind **Automation**, not by adding `node` to the Contacts or Calendars privacy lists.
|
|
75
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, and Messages 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.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Preferred: execPath matches the product node
|
|
86
|
+
$(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
|
+
```
|
|
95
|
+
|
|
96
|
+
If the invoked CLI path and `process.execPath` differ, the command prints a **WARN** with both paths and tells you to re-run as `execPath …/apple-tools-mcp permissions`. The command also prints the binary it is probing. Host examples (not universal paths):
|
|
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, and Messages. 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, and Messages. 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).
|
|
108
|
+
|
|
76
109
|
**One-pass first-run:** Allow **`node`** to control **Mail**, **Messages**, **Contacts**, and **Calendar** in the same Automation pass. Contacts or Calendar being allowed does **not** grant Mail or Messages — 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.
|
|
77
110
|
|
|
78
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.
|
|
@@ -90,8 +123,9 @@ This is the first-run flow for Mail, Messages, Contacts, and Calendar **writes**
|
|
|
90
123
|
|
|
91
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`.
|
|
92
125
|
3. When prompts appear, click **Allow** for **`node`** to control **Mail**, **Messages**, **Contacts**, and **Calendar**. 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.
|
|
93
|
-
4. **
|
|
94
|
-
5.
|
|
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.
|
|
95
129
|
|
|
96
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.
|
|
97
131
|
|
|
@@ -146,7 +180,7 @@ So the supported configuration for writes is: **run the indexer daemon** ([Launc
|
|
|
146
180
|
| Any stdio client **with the daemon running** | Yes | Yes (via the bridge) | Yes (via the bridge) | Yes (via the bridge) |
|
|
147
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) |
|
|
148
182
|
|
|
149
|
-
If a write is denied and no
|
|
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.
|
|
150
184
|
|
|
151
185
|
#### Verifying Mail Automation plus Contacts and Calendar CRUD on a Node host
|
|
152
186
|
|
|
@@ -177,7 +211,7 @@ The smoke test drives writes through **the same dispatcher the MCP tools use**,
|
|
|
177
211
|
node scripts/smoke-writes.js --apply --keep # leave the test items behind
|
|
178
212
|
```
|
|
179
213
|
|
|
180
|
-
The header prints the write path it chose (`indexer daemon via write bridge` or `in this process`), and the read path (sqlite + FDA) is reported separately from the write paths (Mail.app compose, Contacts.app, Calendar.app), so a failure tells you which mechanism refused. A Mail hang
|
|
214
|
+
The header prints the write path it chose (`indexer daemon via write bridge` or `in this process`), and the read path (sqlite + FDA) is reported separately from the write paths (Mail.app compose, Contacts.app, Calendar.app), so a failure tells you which mechanism refused. A Mail **compose-probe** hang is **TCC / Automation denied**, not “app not available”. A `mail_send` / `mail_reply` / `mail_forward` hang after `send` is a **timeout** (`ETIMEDOUT` / `-1712`) unless Mail reports `-1743` / `-10004`; check Sent before retrying.
|
|
181
215
|
|
|
182
216
|
**The parent process matters.** With `--apply` and **no bridge listening**, the smoke test **refuses to run** rather than executing in-process and calling the result a package failure. Running it from an embedded agent shell, an IDE terminal, or a host app's subprocess is *not* the ship-gate context on its own, because macOS attributes the Apple events to that parent. Either start the LaunchAgent (preferred, and what production clients use), or run it from **Terminal.app**, where node is the responsible process, and pass `--allow-local` to acknowledge that:
|
|
183
217
|
|
|
@@ -276,10 +310,14 @@ On Mini, run the **indexer daemon**, not a sleep-pipe wrapper around `apple-tool
|
|
|
276
310
|
|
|
277
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, and Calendar.app. 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.
|
|
278
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
|
+
|
|
279
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.
|
|
280
318
|
|
|
281
319
|
**Entrypoint:** `node index.js --mode=indexer`
|
|
282
|
-
**Convenience bin:** `apple-tools-indexer` (
|
|
320
|
+
**Convenience bin:** `apple-tools-indexer` (`bin/apple-tools-indexer.js`; npm global install provides it)
|
|
283
321
|
**npm script (clone only):** `npm run indexer`
|
|
284
322
|
**One-shot rebuild:** `npm run build-index` (stop the indexer daemon first)
|
|
285
323
|
|
|
@@ -427,6 +465,8 @@ Two arguments are available on **every** write tool:
|
|
|
427
465
|
|
|
428
466
|
Emails are addressed by their RFC822 **Message-ID**. Pass `message_id`, or pass the `file_path` from `mail_search` / `mail_recent` and the server reads the Message-ID out of the `.emlx` headers for you. `mail_archive` moves the message to its account's Archive (or All Mail) mailbox; `mail_trash` moves it to that account's Trash.
|
|
429
467
|
|
|
468
|
+
**Send timeout vs TCC — check Sent before retrying.** `mail_send` / `mail_reply` / `mail_forward` can hang after Mail has already put the message in Sent. That hang is a **timeout** (`ETIMEDOUT` / `-1712` / AppleEvent timed out), not a TCC deny. The same hang-vs-TCC split applies to **find / reply / open before send** — an Allowed `ETIMEDOUT` there is never `MAIL_TCC_GUIDANCE`. Real Mail Automation denials report **`-1743`**, **`-10004`**, or “not authorized to send Apple events”. After a send hang the tool looks in Sent (and Outbox) and returns **success** if the message is there — never label a delivered send as TCC fail or timeout. Replies match `In-Reply-To` or an exact recent `Re:` + original subject (the fallback compose does not set reply headers). Forwards match the intended recipient plus an exact `Fwd:` subject or the original Message-ID in a forwarded body — not `In-Reply-To`, and not an unrelated `Fwd:` that only shares a To. Neither scan treats the original itself as this send. If Sent-verify misses, the error tells you it was a hang and to check Sent. **Clients must Sent-check before retrying a timed-out send**; retrying a message that already landed sends a second copy. This is not a silent TCC grant.
|
|
469
|
+
|
|
430
470
|
### Messages write tool
|
|
431
471
|
|
|
432
472
|
| Tool | Arguments | Confirm rule |
|
|
@@ -487,7 +527,7 @@ Like Contacts, calendar writes go through Calendar.app rather than writing `Cale
|
|
|
487
527
|
| `contacts_edit` | `contact_id` (required), any of the above, `replace_emails`, `replace_phones` | **`confirm` required** when replacing with an empty list |
|
|
488
528
|
| `contacts_remove` | `contact_id` (required) | **`confirm` required** |
|
|
489
529
|
|
|
490
|
-
Contacts are addressed by their Contacts.app person id (for example `ABCD1234-...:ABPerson`), reported as `Contact ID` by `contacts_search` and `contacts_lookup` and returned by `contacts_add`. At least one of `first_name`, `last_name`, or `organization` is required to create a contact. Writes go through Contacts.app, never by writing the AddressBook database directly (that breaks iCloud sync).
|
|
530
|
+
Contacts are addressed by their Contacts.app person id (for example `ABCD1234-...:ABPerson`), reported as `Contact ID` by `contacts_search` and `contacts_lookup` and returned by `contacts_add`. At least one of `first_name`, `last_name`, or `organization` is required to create a contact. Writes go through Contacts.app, never by writing the AddressBook database directly (that breaks iCloud sync). Keep **Contacts, Mail, and Messages** running for write reliability; the Contacts write path launches Contacts.app before CRUD if it was quit. Calendar does not need to stay open (EventKit).
|
|
491
531
|
|
|
492
532
|
### Example write calls
|
|
493
533
|
|
|
@@ -542,13 +582,14 @@ Ensure Node.js has Full Disk Access (see Installation step 2).
|
|
|
542
582
|
|
|
543
583
|
The message names which process macOS was actually asking about. Work through it in this order:
|
|
544
584
|
|
|
545
|
-
1. **
|
|
546
|
-
2. **
|
|
547
|
-
3. **
|
|
548
|
-
4. **
|
|
549
|
-
5. **
|
|
550
|
-
6.
|
|
551
|
-
7. **
|
|
585
|
+
1. **MacBook / Terminal (`permissions` CLI):** there is **no** always-on indexer. Re-run `apple-tools-mcp permissions` from **Terminal.app**, Allow the **printed `process.execPath`**, and check System Settings → Privacy & Security → **Automation** for that node → Contacts, Calendar, Mail, and Messages. Do **not** start `apple-tools-indexer`.
|
|
586
|
+
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)).
|
|
587
|
+
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, and Calendar. 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.
|
|
588
|
+
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.
|
|
589
|
+
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.
|
|
590
|
+
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.
|
|
591
|
+
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.
|
|
592
|
+
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.
|
|
552
593
|
|
|
553
594
|
### A write returned "CONFIRMATION REQUIRED"
|
|
554
595
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm bin for the indexer daemon.
|
|
4
|
+
* Dedicated wrapper so npm 12 pack/publish keeps the CLI — `index.js` as a
|
|
5
|
+
* bin target is rewritten with "script name index.js was invalid and removed".
|
|
6
|
+
* Injects `--mode=indexer` so a realpath to this file still starts the daemon.
|
|
7
|
+
* `permissions` still wins (same contract as the apple-tools-indexer bin name).
|
|
8
|
+
*/
|
|
9
|
+
if (!process.argv.includes('--mode=indexer')) {
|
|
10
|
+
const modeIdx = process.argv.indexOf('--mode');
|
|
11
|
+
if (modeIdx === -1 || process.argv[modeIdx + 1] !== 'indexer') {
|
|
12
|
+
process.argv.splice(2, 0, '--mode=indexer');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
await import('../index.js');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm bin for the MCP stdio server (and `permissions` subcommand).
|
|
4
|
+
* Dedicated wrapper so npm 12 pack/publish keeps the CLI — `index.js` as a
|
|
5
|
+
* bin target is rewritten with "script name index.js was invalid and removed".
|
|
6
|
+
*/
|
|
7
|
+
import '../index.js';
|
package/index.js
CHANGED
|
@@ -10,7 +10,8 @@ import fs from "fs";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { validateEmailPath, stripHtmlTags, unfoldRfc822Headers, validateLimit, validateDaysBack, validateWeekOffset, toUnixMillis } from "./lib/validators.js";
|
|
12
12
|
import { cycleEndFlags, indexUnavailableMessage, indexQueryGate } from "./lib/indexGate.js";
|
|
13
|
-
import { isIndexerMode } from "./lib/processMode.js";
|
|
13
|
+
import { isIndexerMode, isPermissionsMode } from "./lib/processMode.js";
|
|
14
|
+
import { runPermissionsCommand } from "./lib/permissions.js";
|
|
14
15
|
import { loadResolvedIndexInterval, logResolvedInterval } from "./lib/config.js";
|
|
15
16
|
import { createIndexerLock, DEFAULT_LOCK_HEARTBEAT_MS } from "./lib/indexerLock.js";
|
|
16
17
|
import {
|
|
@@ -37,6 +38,8 @@ const PACKAGE_VERSION = JSON.parse(
|
|
|
37
38
|
).version;
|
|
38
39
|
|
|
39
40
|
// Canonical indexer entrypoint: `node index.js --mode=indexer` or `apple-tools-indexer`.
|
|
41
|
+
// Permissions CLI: `apple-tools-mcp permissions` — short-lived, no MCP / indexer.
|
|
42
|
+
const PERMISSIONS_MODE = isPermissionsMode();
|
|
40
43
|
const INDEXER_MODE = isIndexerMode();
|
|
41
44
|
const resolvedIndexInterval = loadResolvedIndexInterval();
|
|
42
45
|
const INDEX_INTERVAL = resolvedIndexInterval.ms;
|
|
@@ -151,7 +154,7 @@ process.on("unhandledRejection", (reason, promise) => {
|
|
|
151
154
|
|
|
152
155
|
// MCP stdio clients exit when the host closes stdin. The indexer daemon must
|
|
153
156
|
// not — LaunchAgent / KeepAlive often attaches stdin to /dev/null.
|
|
154
|
-
bindStdinCloseExit(process.stdin, INDEXER_MODE, () => {
|
|
157
|
+
bindStdinCloseExit(process.stdin, INDEXER_MODE || PERMISSIONS_MODE, () => {
|
|
155
158
|
console.error("Client disconnected. Exiting.");
|
|
156
159
|
shutdownIndexing(0);
|
|
157
160
|
});
|
|
@@ -318,6 +321,7 @@ function startBackgroundIndexing() {
|
|
|
318
321
|
|
|
319
322
|
// Stop background indexing and clean up timers
|
|
320
323
|
function stopBackgroundIndexing() {
|
|
324
|
+
const wasRunning = Boolean(indexTimer || progressCheckTimer);
|
|
321
325
|
if (indexTimer) {
|
|
322
326
|
clearInterval(indexTimer);
|
|
323
327
|
indexTimer = null;
|
|
@@ -326,7 +330,9 @@ function stopBackgroundIndexing() {
|
|
|
326
330
|
clearInterval(progressCheckTimer);
|
|
327
331
|
progressCheckTimer = null;
|
|
328
332
|
}
|
|
329
|
-
|
|
333
|
+
if (wasRunning) {
|
|
334
|
+
console.error("Background indexing stopped");
|
|
335
|
+
}
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
// Unblock searches after a cycle ends. Must run on failure as well as success
|
|
@@ -440,9 +446,22 @@ async function initializeIndexing() {
|
|
|
440
446
|
// Start indexing immediately on server startup. A startup failure is logged
|
|
441
447
|
// rather than rejected: an unhandled rejection would tear down the daemon,
|
|
442
448
|
// taking the write bridge with it.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
449
|
+
if (PERMISSIONS_MODE) {
|
|
450
|
+
runPermissionsCommand({
|
|
451
|
+
execPath: process.execPath,
|
|
452
|
+
argv: process.argv,
|
|
453
|
+
version: PACKAGE_VERSION
|
|
454
|
+
}).then((code) => {
|
|
455
|
+
process.exit(code);
|
|
456
|
+
}).catch((e) => {
|
|
457
|
+
console.error(`Permissions command error: ${e.message}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
});
|
|
460
|
+
} else {
|
|
461
|
+
initializeIndexing().catch((e) => {
|
|
462
|
+
console.error(`Indexing startup failed: ${e.message}`);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
446
465
|
|
|
447
466
|
// ============ SEMANTIC SEARCH FUNCTIONS ============
|
|
448
467
|
|
|
@@ -1584,6 +1603,6 @@ async function main() {
|
|
|
1584
1603
|
// fallback on this stdio process only if indexer.lock is free.
|
|
1585
1604
|
}
|
|
1586
1605
|
|
|
1587
|
-
if (shouldConnectMcpStdio(INDEXER_MODE)) {
|
|
1606
|
+
if (!PERMISSIONS_MODE && shouldConnectMcpStdio(INDEXER_MODE)) {
|
|
1588
1607
|
main().catch(console.error);
|
|
1589
1608
|
}
|
package/indexer.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import * as lancedb from "@lancedb/lancedb";
|
|
4
|
-
import { pipeline } from "@xenova/transformers";
|
|
5
4
|
import {
|
|
6
5
|
validateEmailPath,
|
|
7
6
|
validateMailboxName,
|
|
@@ -86,6 +85,10 @@ let db = null;
|
|
|
86
85
|
async function getEmbedder() {
|
|
87
86
|
if (!embeddingPipeline) {
|
|
88
87
|
console.error("Loading embedding model (first time may take a minute)...");
|
|
88
|
+
// Lazy import: @xenova/transformers loads sharp at import time. A static
|
|
89
|
+
// import crashes Linux (and any host without the platform sharp native)
|
|
90
|
+
// before search.js formatters / pronoun helpers can load.
|
|
91
|
+
const { pipeline } = await import("@xenova/transformers");
|
|
89
92
|
embeddingPipeline = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
90
93
|
console.error("Embedding model loaded.");
|
|
91
94
|
}
|
package/lib/appleScript.js
CHANGED
|
@@ -52,18 +52,21 @@ const TCC_SIGNATURES = [
|
|
|
52
52
|
];
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* failures, not as a missing app.
|
|
55
|
+
* Contacts (and sometimes Mail) report these when the app is installed
|
|
56
|
+
* but not launched. That is a cold-start, not an Automation deny.
|
|
57
|
+
* "-1728" / "can't get application" stays on APP_MISSING_SIGNATURES —
|
|
58
|
+
* when the bundle is on disk that is still attribution.
|
|
60
59
|
*/
|
|
61
|
-
const
|
|
60
|
+
const APP_NOT_RUNNING_SIGNATURES = [
|
|
62
61
|
"application isn't running",
|
|
63
|
-
"can't get application",
|
|
64
|
-
"can't get every application",
|
|
65
62
|
"application is not running",
|
|
66
63
|
"-600",
|
|
64
|
+
"contacts_not_running"
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const APP_MISSING_SIGNATURES = [
|
|
68
|
+
"can't get application",
|
|
69
|
+
"can't get every application",
|
|
67
70
|
"-1728",
|
|
68
71
|
"-10810"
|
|
69
72
|
];
|
|
@@ -115,7 +118,7 @@ export function appBundleInstalled(appName, existsFn = fs.existsSync) {
|
|
|
115
118
|
* @param {string} message
|
|
116
119
|
* @param {object} [context]
|
|
117
120
|
* @param {boolean|null} [context.appInstalled] - whether the target app exists
|
|
118
|
-
* @returns {"tcc"|"timeout"|"not_found"|"attribution"|"app_unavailable"|"unknown"}
|
|
121
|
+
* @returns {"tcc"|"timeout"|"not_found"|"attribution"|"app_not_running"|"app_unavailable"|"unknown"}
|
|
119
122
|
*/
|
|
120
123
|
export function classifyAppleScriptError(message, context = {}) {
|
|
121
124
|
const text = String(message || "").toLowerCase();
|
|
@@ -123,8 +126,8 @@ export function classifyAppleScriptError(message, context = {}) {
|
|
|
123
126
|
if (text.includes(sentinel.toLowerCase())) return "not_found";
|
|
124
127
|
}
|
|
125
128
|
if (OSASCRIPT_MISSING_SIGNATURES.some((sig) => text.includes(sig))) return "app_unavailable";
|
|
126
|
-
// ETIMEDOUT / -1712 is a hang, not a TCC deny. Mail
|
|
127
|
-
//
|
|
129
|
+
// ETIMEDOUT / -1712 is a hang, not a TCC deny. Mail maps this kind to
|
|
130
|
+
// MAIL_SEND_TIMEOUT_GUIDANCE (find/reply/open/send), never MAIL_TCC_GUIDANCE.
|
|
128
131
|
// calendar_remove must never print Calendar-denied copy for ETIMEDOUT alone.
|
|
129
132
|
if (TIMEOUT_SIGNATURES.some((sig) => text.includes(sig))) return "timeout";
|
|
130
133
|
if (TCC_SIGNATURES.some((sig) => text.includes(sig))) return "tcc";
|
|
@@ -138,6 +141,10 @@ export function classifyAppleScriptError(message, context = {}) {
|
|
|
138
141
|
) {
|
|
139
142
|
return "not_found";
|
|
140
143
|
}
|
|
144
|
+
// Installed + not running (-600) is a cold launch, not Automation.
|
|
145
|
+
if (APP_NOT_RUNNING_SIGNATURES.some((sig) => text.includes(sig))) {
|
|
146
|
+
return context.appInstalled === true ? "app_not_running" : "app_unavailable";
|
|
147
|
+
}
|
|
141
148
|
if (APP_MISSING_SIGNATURES.some((sig) => text.includes(sig))) {
|
|
142
149
|
// The app is on disk, so "can't get application" means macOS refused to
|
|
143
150
|
// let the responsible process drive it - an Automation / attribution
|
|
@@ -151,6 +158,35 @@ export function isTccDenial(message) {
|
|
|
151
158
|
return classifyAppleScriptError(message) === "tcc";
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Whether dispatchWriteTool should append MacBook/Mini host recovery copy.
|
|
163
|
+
*
|
|
164
|
+
* Handlers rewrite raw Apple events as CONTACTS_/CALENDAR_/MESSAGES_/MAIL
|
|
165
|
+
* TCC guidance or ATTRIBUTION_GUIDANCE, which do not contain the -1743 /
|
|
166
|
+
* -10004 signatures `isTccDenial` looks for (except MAIL_TCC_GUIDANCE).
|
|
167
|
+
* MAIL_SEND_TIMEOUT_GUIDANCE must never match — that hang is not a deny.
|
|
168
|
+
*/
|
|
169
|
+
export function needsHostTccAdvice(message) {
|
|
170
|
+
const text = String(message || "");
|
|
171
|
+
if (!text) return false;
|
|
172
|
+
if (text.includes(MAIL_SEND_TIMEOUT_GUIDANCE)) return false;
|
|
173
|
+
if (text.includes(CONTACTS_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
174
|
+
if (text.includes(MAIL_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
175
|
+
if (text.includes(MESSAGES_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
176
|
+
if (text.includes(CALENDAR_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
177
|
+
if (
|
|
178
|
+
text.includes(CONTACTS_TCC_GUIDANCE) ||
|
|
179
|
+
text.includes(CALENDAR_TCC_GUIDANCE) ||
|
|
180
|
+
text.includes(MESSAGES_TCC_GUIDANCE) ||
|
|
181
|
+
text.includes(MAIL_TCC_GUIDANCE) ||
|
|
182
|
+
text.includes(ATTRIBUTION_GUIDANCE) ||
|
|
183
|
+
text.includes(TCC_GUIDANCE)
|
|
184
|
+
) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return isTccDenial(text);
|
|
188
|
+
}
|
|
189
|
+
|
|
154
190
|
/**
|
|
155
191
|
* Real Automation / privacy deny codes — not a hung Calendar.app delete.
|
|
156
192
|
*
|
|
@@ -226,8 +262,7 @@ export function formatOsascriptDiagnostic(result, source = "osascript") {
|
|
|
226
262
|
*/
|
|
227
263
|
export const TCC_GUIDANCE =
|
|
228
264
|
"macOS denied this automation. TCC attributes Apple events to the process responsible for the MCP server, " +
|
|
229
|
-
"so a host app without the matching automation grant blocks the write even when node has Full Disk Access.
|
|
230
|
-
"Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so writes execute under node, or run this server from a host that can be granted Automation access.";
|
|
265
|
+
"so a host app without the matching automation grant blocks the write even when node has Full Disk Access.";
|
|
231
266
|
|
|
232
267
|
/**
|
|
233
268
|
* Contacts writes are the sharpest case, and worth separating from contacts
|
|
@@ -241,8 +276,28 @@ export const CONTACTS_TCC_GUIDANCE =
|
|
|
241
276
|
"macOS denied Contacts access for this write. Contacts writes go through Contacts.app (CNContactStore), which is gated by the AddressBook privacy class " +
|
|
242
277
|
"and attributed to the process responsible for this MCP server - not to node. A host app that was built without the AddressBook entitlement " +
|
|
243
278
|
"(com.apple.security.personal-information.addressbook) is denied with no prompt, and that cannot be fixed with Full Disk Access or tccutil. " +
|
|
244
|
-
"Contacts *reads* are unaffected: they query the AddressBook database directly and only need Full Disk Access.
|
|
245
|
-
|
|
279
|
+
"Contacts *reads* are unaffected: they query the AddressBook database directly and only need Full Disk Access.";
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Contacts.app was installed but not running. Cold `tell application
|
|
283
|
+
* "Contacts"` under launchd often returns -600 instead of auto-launching.
|
|
284
|
+
* That is not -1743 / -10004 and not attribution.
|
|
285
|
+
*/
|
|
286
|
+
export const CONTACTS_APP_NOT_RUNNING_GUIDANCE =
|
|
287
|
+
"Contacts.app was not running and did not become ready after launch. This is not a TCC / Automation deny (-1743 / -10004) " +
|
|
288
|
+
"and not an attribution / responsible-process failure. The write path launches Contacts.app; if this persists, open Contacts.app and retry.";
|
|
289
|
+
|
|
290
|
+
export const MAIL_APP_NOT_RUNNING_GUIDANCE =
|
|
291
|
+
"Mail.app was not running. Keep Mail, Messages, and Contacts running for write reliability. " +
|
|
292
|
+
"This is not a TCC / Automation deny (-1743 / -10004) and not an attribution / responsible-process failure.";
|
|
293
|
+
|
|
294
|
+
export const MESSAGES_APP_NOT_RUNNING_GUIDANCE =
|
|
295
|
+
"Messages.app was not running. Keep Mail, Messages, and Contacts running for write reliability. " +
|
|
296
|
+
"This is not a TCC / Automation deny (-1743 / -10004) and not an attribution / responsible-process failure.";
|
|
297
|
+
|
|
298
|
+
export const CALENDAR_APP_NOT_RUNNING_GUIDANCE =
|
|
299
|
+
"Calendar.app was not running. Calendar writes use EventKit and do not require Calendar.app to stay open. " +
|
|
300
|
+
"This is not a TCC / Automation deny (-1743 / -10004).";
|
|
246
301
|
|
|
247
302
|
/**
|
|
248
303
|
* Calendar has the same split as Contacts: reads in this package are sqlite
|
|
@@ -253,8 +308,7 @@ export const CALENDAR_TCC_GUIDANCE =
|
|
|
253
308
|
"macOS denied Calendar access for this write. Calendar writes go through Calendar.app (EventKit), which is gated by the calendars privacy class " +
|
|
254
309
|
"(com.apple.security.personal-information.calendars) and attributed to the process responsible for this MCP server - not to node. " +
|
|
255
310
|
"A host app built without that entitlement is denied with no prompt, and Full Disk Access or tccutil cannot change it. " +
|
|
256
|
-
"Calendar *reads* are unaffected: they query Calendar.sqlitedb directly and only need Full Disk Access.
|
|
257
|
-
"Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so Calendar writes execute under node, which macOS can grant Calendars access to.";
|
|
311
|
+
"Calendar *reads* are unaffected: they query Calendar.sqlitedb directly and only need Full Disk Access.";
|
|
258
312
|
|
|
259
313
|
/**
|
|
260
314
|
* Shown when the target app is installed but macOS still refused the Apple
|
|
@@ -264,20 +318,31 @@ export const CALENDAR_TCC_GUIDANCE =
|
|
|
264
318
|
*/
|
|
265
319
|
export const ATTRIBUTION_GUIDANCE =
|
|
266
320
|
"The app is installed, so this is an Automation / responsible-process problem rather than a missing app: macOS refused to let the process " +
|
|
267
|
-
"responsible for this server drive it.
|
|
268
|
-
"under launchd-owned node, or run this server from a parent that can hold Automation access (Terminal.app) and approve the prompt.";
|
|
321
|
+
"responsible for this server drive it.";
|
|
269
322
|
|
|
270
323
|
/**
|
|
271
324
|
* Mail writes are gated by Automation → Mail for the responsible process.
|
|
272
|
-
*
|
|
273
|
-
*
|
|
325
|
+
* Shown only for a hard deny (-1743 / -10004 / "not authorized…").
|
|
326
|
+
* ETIMEDOUT / -1712 is `MAIL_SEND_TIMEOUT_GUIDANCE`, including find/reply/open
|
|
327
|
+
* before send. dry_run never sends Apple events to Mail.
|
|
274
328
|
*/
|
|
275
329
|
export const MAIL_TCC_GUIDANCE =
|
|
276
|
-
"macOS denied Mail automation (Apple Events to Mail.app).
|
|
330
|
+
"macOS denied Mail automation (Apple Events to Mail.app). Real denials report -1743, -10004, or \"not authorized to send Apple events\" — a TCC / Automation deny for node → Mail, " +
|
|
277
331
|
"not Mail.app missing or unavailable. dry_run never talks to Mail, so it cannot detect this grant. " +
|
|
278
332
|
"Allow node in System Settings → Privacy & Security → Automation for Mail (same Allow-via-prompt as Contacts and Calendar — do not add node via + in a privacy list). " +
|
|
279
|
-
"A Contacts or Calendar grant does not include Mail.
|
|
280
|
-
|
|
333
|
+
"A Contacts or Calendar grant does not include Mail.";
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
|
|
337
|
+
* or sending. That is not -1743 / -10004, including before send.
|
|
338
|
+
* Clients must Sent-check before retrying a send — a retry of a delivered
|
|
339
|
+
* send creates a second copy. This is not a silent TCC grant.
|
|
340
|
+
*/
|
|
341
|
+
export const MAIL_SEND_TIMEOUT_GUIDANCE =
|
|
342
|
+
"Mail AppleScript timed out (ETIMEDOUT / -1712 / AppleEvent timed out) — this is a find/reply/send hang (including find/reply/open before send), not a TCC / Automation deny. " +
|
|
343
|
+
"Real Mail Automation denials report -1743, -10004, or \"not authorized to send Apple events\". " +
|
|
344
|
+
"A timeout can happen after Mail already delivered the message. Check Sent (and Outbox) for this send before retrying; a retry after a successful delivery sends a second copy. " +
|
|
345
|
+
"This tool verifies Sent when a send hangs; if nothing is there yet, wait and look again rather than immediately resending.";
|
|
281
346
|
|
|
282
347
|
/**
|
|
283
348
|
* Messages writes are a separate Automation target from Mail / Contacts / Calendar.
|
|
@@ -286,8 +351,7 @@ export const MESSAGES_TCC_GUIDANCE =
|
|
|
286
351
|
"macOS denied Messages automation (Apple Events to Messages.app). A hang or timeout on send is a TCC / Automation deny for node → Messages, " +
|
|
287
352
|
"not Messages.app missing or unavailable. dry_run never talks to Messages, so it cannot detect this grant. " +
|
|
288
353
|
"Allow node in System Settings → Privacy & Security → Automation for Messages (same Allow-via-prompt as Mail, Contacts, and Calendar — do not add node via + in a privacy list). " +
|
|
289
|
-
"A Contacts or Calendar grant does not include Messages.
|
|
290
|
-
"Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so Messages writes execute under node.";
|
|
354
|
+
"A Contacts or Calendar grant does not include Messages.";
|
|
291
355
|
|
|
292
356
|
/**
|
|
293
357
|
* @param {"contacts"|"calendar"|"mail"|"messages"|string} source
|
package/lib/calendarWrite.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
parseWriteDateTime,
|
|
16
16
|
DATE_HANDLER,
|
|
17
17
|
CALENDAR_TCC_GUIDANCE,
|
|
18
|
+
CALENDAR_APP_NOT_RUNNING_GUIDANCE,
|
|
18
19
|
ATTRIBUTION_GUIDANCE,
|
|
19
20
|
formatOsascriptDiagnostic,
|
|
20
21
|
isHardTccDenial
|
|
@@ -194,6 +195,9 @@ function failure(action, summary, result, secrets = []) {
|
|
|
194
195
|
if (result.kind === "not_found" || raw.includes("EVENT_NOT_FOUND")) {
|
|
195
196
|
return `${action} failed — attempted to ${summary}. No event with that id was found; use the Event ID from calendar_date.`;
|
|
196
197
|
}
|
|
198
|
+
if (result.kind === "app_not_running") {
|
|
199
|
+
return `${action} failed — attempted to ${summary}. ${CALENDAR_APP_NOT_RUNNING_GUIDANCE}`;
|
|
200
|
+
}
|
|
197
201
|
if (result.kind === "attribution") {
|
|
198
202
|
return `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
|
|
199
203
|
}
|
|
@@ -231,6 +235,8 @@ export function describeCalendarRemoveFailure(action, summary, appleResult, even
|
|
|
231
235
|
head = `${action} failed — attempted to ${summary}. Calendar.app delete timed out. That is not a TCC / Automation deny when calendar_add/calendar_edit succeed on this host — Calendar.app has no remove or move-to-trash; iCloud/CalDAV delete can hang or wait on a confirmation dialog.`;
|
|
232
236
|
} else if (appleResult.kind === "not_found" || raw.includes("EVENT_NOT_FOUND")) {
|
|
233
237
|
head = `${action} failed — attempted to ${summary}. No event with that id was found; use the Event ID from calendar_date.`;
|
|
238
|
+
} else if (appleResult.kind === "app_not_running") {
|
|
239
|
+
head = `${action} failed — attempted to ${summary}. ${CALENDAR_APP_NOT_RUNNING_GUIDANCE}`;
|
|
234
240
|
} else if (appleResult.kind === "attribution") {
|
|
235
241
|
head = `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
|
|
236
242
|
} else if (appleResult.kind === "app_unavailable") {
|
|
@@ -1365,3 +1371,36 @@ export function calendarRsvp(args = {}) {
|
|
|
1365
1371
|
})
|
|
1366
1372
|
};
|
|
1367
1373
|
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* First-run / upgrade probe: a live Calendar.app Apple Event that pops
|
|
1377
|
+
* Automation → Calendar. Listing only — no events are created, so nothing
|
|
1378
|
+
* is left behind.
|
|
1379
|
+
*/
|
|
1380
|
+
export function buildCalendarAutomationProbeScript() {
|
|
1381
|
+
return `tell application "Calendar"
|
|
1382
|
+
get name of every calendar
|
|
1383
|
+
end tell
|
|
1384
|
+
return "OK"`;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Live Calendar Apple Events check. Not a dry_run. Does not create events.
|
|
1389
|
+
* @returns {{ ok: boolean, message: string, kind: string|null }}
|
|
1390
|
+
*/
|
|
1391
|
+
export function probeCalendarAutomation() {
|
|
1392
|
+
const action = "calendar_automation_probe";
|
|
1393
|
+
const summary = "list calendars in Calendar.app (Automation / Apple Events check; no events created)";
|
|
1394
|
+
const result = runAppleScript(buildCalendarAutomationProbeScript(), { timeout: 30000, appName: "Calendar" });
|
|
1395
|
+
if (!result.ok) {
|
|
1396
|
+
return { ok: false, message: failure(action, summary, result), kind: result.kind };
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
ok: true,
|
|
1400
|
+
kind: null,
|
|
1401
|
+
message: writeSuccessMessage(
|
|
1402
|
+
action,
|
|
1403
|
+
"Calendar Automation allowed (listed calendars; no events created)"
|
|
1404
|
+
)
|
|
1405
|
+
};
|
|
1406
|
+
}
|