apple-tools-mcp 1.1.2 → 1.1.4
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 +24 -16
- package/index.js +105 -47
- package/indexer.js +2 -1
- package/lib/indexGate.js +59 -0
- package/package.json +1 -3
- package/search.js +12 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# apple-tools-mcp
|
|
2
2
|
|
|
3
|
-
An MCP (Model Context Protocol) server that provides semantic search across Apple Mail, Messages, Calendar, and Contacts on macOS. Use natural language to search your emails, iMessages, calendar events, and contacts
|
|
3
|
+
An MCP (Model Context Protocol) server that provides semantic search across Apple Mail, Messages, Calendar, and Contacts on macOS. Use natural language to search your emails, iMessages, calendar events, and contacts from any compatible MCP client over stdio.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -14,7 +14,6 @@ An MCP (Model Context Protocol) server that provides semantic search across Appl
|
|
|
14
14
|
|
|
15
15
|
- **macOS** (Ventura 13.0 or later recommended)
|
|
16
16
|
- **Node.js** 18.0 or later
|
|
17
|
-
- **Claude Desktop** app
|
|
18
17
|
- **Full Disk Access** permission for the Node.js binary
|
|
19
18
|
|
|
20
19
|
## Installation
|
|
@@ -33,7 +32,7 @@ cd Apple-Tools-MCP
|
|
|
33
32
|
npm install
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
If you installed from source, point
|
|
35
|
+
If you installed from source, point your MCP client at the local `index.js` instead of `npx` in step 3:
|
|
37
36
|
|
|
38
37
|
```json
|
|
39
38
|
"command": "node",
|
|
@@ -64,11 +63,18 @@ The MCP server needs access to read your Mail, Messages, and Calendar databases.
|
|
|
64
63
|
|
|
65
64
|
7. Ensure the toggle for Node.js is enabled
|
|
66
65
|
|
|
67
|
-
### 3. Configure
|
|
66
|
+
### 3. Configure your MCP client
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
This server speaks MCP over **stdio**. Any compatible client can run it — Claude Desktop is one example, not the only one. Cursor, Grok Bot, and other stdio MCP clients work the same way: register the command below in that client's MCP settings.
|
|
70
69
|
|
|
71
|
-
**
|
|
70
|
+
**Command**
|
|
71
|
+
|
|
72
|
+
- `npx` with args `["-y", "apple-tools-mcp"]` (npm install)
|
|
73
|
+
- or `node` with args `["/absolute/path/to/Apple-Tools-MCP/index.js"]` (from source)
|
|
74
|
+
|
|
75
|
+
**Example: Claude Desktop**
|
|
76
|
+
|
|
77
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
72
78
|
|
|
73
79
|
```json
|
|
74
80
|
{
|
|
@@ -81,9 +87,11 @@ Add to your Claude Desktop config file:
|
|
|
81
87
|
}
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
Other clients use their own settings UI or config file. Use the same `command` and `args`; only the file path or UI differs.
|
|
91
|
+
|
|
92
|
+
### 4. Restart your MCP client
|
|
85
93
|
|
|
86
|
-
Quit and reopen Claude Desktop
|
|
94
|
+
Quit and reopen the client so it loads the server. For Claude Desktop, fully quit (Cmd+Q) and reopen.
|
|
87
95
|
|
|
88
96
|
## Building the Index
|
|
89
97
|
|
|
@@ -103,7 +111,7 @@ The index is stored in `~/.apple-tools-mcp/vector-index/`.
|
|
|
103
111
|
|
|
104
112
|
## Available Tools
|
|
105
113
|
|
|
106
|
-
Once configured,
|
|
114
|
+
Once configured, your MCP client can use these tools:
|
|
107
115
|
|
|
108
116
|
### Universal Search
|
|
109
117
|
|
|
@@ -159,7 +167,7 @@ Once configured, Claude can use these tools:
|
|
|
159
167
|
|
|
160
168
|
## Example Queries
|
|
161
169
|
|
|
162
|
-
Ask
|
|
170
|
+
Ask your MCP client things like:
|
|
163
171
|
|
|
164
172
|
- "Find emails from John about the quarterly report"
|
|
165
173
|
- "What messages did I get from Mom last week?"
|
|
@@ -187,11 +195,11 @@ Ensure Node.js has Full Disk Access (see Installation step 2).
|
|
|
187
195
|
1. Check that the index was built: `ls ~/.apple-tools-mcp/vector-index/`
|
|
188
196
|
2. Rebuild the index if needed: `npm run build-index`
|
|
189
197
|
|
|
190
|
-
### Server not appearing in
|
|
198
|
+
### Server not appearing in the MCP client
|
|
191
199
|
|
|
192
|
-
1. Verify your config
|
|
193
|
-
2. Restart
|
|
194
|
-
3. Check
|
|
200
|
+
1. Verify your client config is valid (JSON files must be valid JSON)
|
|
201
|
+
2. Restart the MCP client completely (for Claude Desktop: Cmd+Q, then reopen)
|
|
202
|
+
3. Check the client's MCP logs for errors
|
|
195
203
|
|
|
196
204
|
### Force rebuild the index
|
|
197
205
|
|
|
@@ -203,12 +211,12 @@ rm -rf ~/.apple-tools-mcp/vector-index
|
|
|
203
211
|
rm -f ~/.apple-tools-mcp/index-meta.json
|
|
204
212
|
rm -f ~/.apple-tools-mcp/indexer.lock
|
|
205
213
|
|
|
206
|
-
# Restart
|
|
214
|
+
# Restart your MCP client to trigger a fresh rebuild
|
|
207
215
|
```
|
|
208
216
|
|
|
209
217
|
### Monitor indexing progress
|
|
210
218
|
|
|
211
|
-
Watch the MCP server logs in
|
|
219
|
+
Watch the MCP server logs in your client. Log locations vary by client; Claude Desktop example:
|
|
212
220
|
|
|
213
221
|
```bash
|
|
214
222
|
tail -f ~/Library/Logs/Claude/mcp-server-apple-tools.log
|
package/index.js
CHANGED
|
@@ -9,10 +9,19 @@ import {
|
|
|
9
9
|
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
|
+
import { isSearchBlockedByIndexing, cycleEndFlags, indexUnavailableMessage } from "./lib/indexGate.js";
|
|
13
|
+
|
|
14
|
+
const PACKAGE_VERSION = JSON.parse(
|
|
15
|
+
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
|
16
|
+
).version;
|
|
12
17
|
|
|
13
18
|
// Lock file to prevent duplicate indexing processes
|
|
14
19
|
const LOCK_FILE = path.join(process.env.HOME, ".apple-tools-mcp", "indexer.lock");
|
|
15
20
|
const LOCK_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes - if lock is older, assume hung process
|
|
21
|
+
// True only while this process won the indexer lock. Distinct from
|
|
22
|
+
// sessionIndexComplete: a secondary instance that lost the lock never
|
|
23
|
+
// completes a local cycle and must not stay on "still indexing" forever.
|
|
24
|
+
let ownsIndexLock = false;
|
|
16
25
|
|
|
17
26
|
function acquireLock() {
|
|
18
27
|
try {
|
|
@@ -32,6 +41,7 @@ function acquireLock() {
|
|
|
32
41
|
|
|
33
42
|
// If we already hold the lock, return true
|
|
34
43
|
if (pid === process.pid) {
|
|
44
|
+
ownsIndexLock = true;
|
|
35
45
|
return true;
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -44,6 +54,7 @@ function acquireLock() {
|
|
|
44
54
|
fs.unlinkSync(LOCK_FILE);
|
|
45
55
|
} else {
|
|
46
56
|
console.error(`Another indexing instance running (PID ${pid}). Skipping indexing.`);
|
|
57
|
+
ownsIndexLock = false;
|
|
47
58
|
return false;
|
|
48
59
|
}
|
|
49
60
|
} catch {
|
|
@@ -57,17 +68,20 @@ function acquireLock() {
|
|
|
57
68
|
// This prevents TOCTOU race condition - will throw EEXIST if file was created between check and write
|
|
58
69
|
try {
|
|
59
70
|
fs.writeFileSync(LOCK_FILE, `${process.pid}:${Date.now()}`, { flag: 'wx' });
|
|
71
|
+
ownsIndexLock = true;
|
|
60
72
|
return true;
|
|
61
73
|
} catch (err) {
|
|
62
74
|
if (err.code === 'EEXIST') {
|
|
63
75
|
// Another process won the race
|
|
64
76
|
console.error("Another process acquired lock during race. Skipping indexing.");
|
|
77
|
+
ownsIndexLock = false;
|
|
65
78
|
return false;
|
|
66
79
|
}
|
|
67
80
|
throw err; // Re-throw unexpected errors
|
|
68
81
|
}
|
|
69
82
|
} catch (e) {
|
|
70
83
|
console.error("Lock file error:", e.message);
|
|
84
|
+
ownsIndexLock = false;
|
|
71
85
|
return false; // On error, fail safe - don't proceed
|
|
72
86
|
}
|
|
73
87
|
}
|
|
@@ -80,6 +94,7 @@ function releaseLock() {
|
|
|
80
94
|
const pid = parseInt(pidStr);
|
|
81
95
|
if (pid === process.pid) {
|
|
82
96
|
fs.unlinkSync(LOCK_FILE);
|
|
97
|
+
ownsIndexLock = false;
|
|
83
98
|
console.error(`Released lock file (PID ${process.pid})`);
|
|
84
99
|
}
|
|
85
100
|
}
|
|
@@ -125,7 +140,7 @@ process.on("unhandledRejection", (reason, promise) => {
|
|
|
125
140
|
process.exit(1);
|
|
126
141
|
});
|
|
127
142
|
|
|
128
|
-
// Exit when stdin closes (
|
|
143
|
+
// Exit when stdin closes (MCP client disconnected)
|
|
129
144
|
process.stdin.on("close", () => {
|
|
130
145
|
console.error("Client disconnected. Exiting.");
|
|
131
146
|
stopBackgroundIndexing();
|
|
@@ -259,11 +274,8 @@ function runIndexCycle() {
|
|
|
259
274
|
|
|
260
275
|
lastIndexTime = Date.now();
|
|
261
276
|
lastProgressTime = Date.now();
|
|
262
|
-
|
|
263
|
-
sessionIndexComplete = true;
|
|
264
|
-
isFirstEverRun = false; // After successful index, no longer first run
|
|
277
|
+
applyCycleEnd(true);
|
|
265
278
|
console.error("Indexing complete.");
|
|
266
|
-
releaseLock(); // Allow other instances to index
|
|
267
279
|
// Pre-warm tables to eliminate first-query latency
|
|
268
280
|
await prewarmTables();
|
|
269
281
|
}).catch(e => {
|
|
@@ -274,9 +286,7 @@ function runIndexCycle() {
|
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
console.error("Indexing error:", e.message);
|
|
277
|
-
|
|
278
|
-
sessionIndexComplete = true; // Mark complete even on error so queries can proceed
|
|
279
|
-
releaseLock(); // Allow other instances to index
|
|
289
|
+
applyCycleEnd(false);
|
|
280
290
|
});
|
|
281
291
|
}
|
|
282
292
|
|
|
@@ -313,6 +323,28 @@ function stopBackgroundIndexing() {
|
|
|
313
323
|
console.error("Background indexing stopped");
|
|
314
324
|
}
|
|
315
325
|
|
|
326
|
+
// Unblock searches and drop the indexer lock after a cycle ends.
|
|
327
|
+
// Must run on failure as well as success so tools are not stuck forever.
|
|
328
|
+
function applyCycleEnd(success) {
|
|
329
|
+
const flags = cycleEndFlags(success);
|
|
330
|
+
indexingInProgress = flags.indexingInProgress;
|
|
331
|
+
sessionIndexComplete = flags.sessionIndexComplete;
|
|
332
|
+
ownsIndexLock = flags.ownsIndexLock;
|
|
333
|
+
if (flags.isFirstEverRun === false) {
|
|
334
|
+
isFirstEverRun = false;
|
|
335
|
+
}
|
|
336
|
+
releaseLock();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Index-backed tools wait only while THIS process owns the lock and has not
|
|
340
|
+
// finished its cycle. Lost-lock secondaries fall through to isIndexReady().
|
|
341
|
+
function stillIndexingMessage() {
|
|
342
|
+
if (isSearchBlockedByIndexing(sessionIndexComplete, ownsIndexLock)) {
|
|
343
|
+
return getIndexingMessage();
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
316
348
|
// Initialize and start indexing
|
|
317
349
|
async function initializeIndexing() {
|
|
318
350
|
isFirstEverRun = await checkIfFirstRun();
|
|
@@ -321,6 +353,9 @@ async function initializeIndexing() {
|
|
|
321
353
|
// indexing but keep the MCP server running so search still works.
|
|
322
354
|
if (!acquireLock()) {
|
|
323
355
|
console.error("Another apple-tools-mcp instance is indexing. Server will run without background indexing.");
|
|
356
|
+
// Lost lock is not "still indexing": this process will never complete a
|
|
357
|
+
// local cycle. Searches proceed whenever isIndexReady() is true.
|
|
358
|
+
ownsIndexLock = false;
|
|
324
359
|
return;
|
|
325
360
|
}
|
|
326
361
|
|
|
@@ -338,13 +373,14 @@ async function mailSearch(query, options = {}) {
|
|
|
338
373
|
return "Error: query parameter is required for mail_search";
|
|
339
374
|
}
|
|
340
375
|
|
|
341
|
-
|
|
342
|
-
|
|
376
|
+
const indexing = stillIndexingMessage();
|
|
377
|
+
if (indexing) {
|
|
378
|
+
return indexing;
|
|
343
379
|
}
|
|
344
380
|
|
|
345
381
|
const ready = await isIndexReady("emails");
|
|
346
382
|
if (!ready) {
|
|
347
|
-
return "
|
|
383
|
+
return indexUnavailableMessage("emails");
|
|
348
384
|
}
|
|
349
385
|
|
|
350
386
|
const result = await searchEmails(query, options);
|
|
@@ -352,13 +388,14 @@ async function mailSearch(query, options = {}) {
|
|
|
352
388
|
}
|
|
353
389
|
|
|
354
390
|
async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJunk = false) {
|
|
355
|
-
|
|
356
|
-
|
|
391
|
+
const indexing = stillIndexingMessage();
|
|
392
|
+
if (indexing) {
|
|
393
|
+
return indexing;
|
|
357
394
|
}
|
|
358
395
|
|
|
359
396
|
const ready = await isIndexReady("emails");
|
|
360
397
|
if (!ready) {
|
|
361
|
-
return "
|
|
398
|
+
return indexUnavailableMessage("emails");
|
|
362
399
|
}
|
|
363
400
|
|
|
364
401
|
const result = await getRecentEmailResults(limit, daysBack, unreadOnly, includeJunk);
|
|
@@ -366,13 +403,14 @@ async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJ
|
|
|
366
403
|
}
|
|
367
404
|
|
|
368
405
|
async function mailDate(date, includeJunk = false) {
|
|
369
|
-
|
|
370
|
-
|
|
406
|
+
const indexing = stillIndexingMessage();
|
|
407
|
+
if (indexing) {
|
|
408
|
+
return indexing;
|
|
371
409
|
}
|
|
372
410
|
|
|
373
411
|
const ready = await isIndexReady("emails");
|
|
374
412
|
if (!ready) {
|
|
375
|
-
return "
|
|
413
|
+
return indexUnavailableMessage("emails");
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
const result = await getEmailDateResults(date, includeJunk);
|
|
@@ -380,13 +418,14 @@ async function mailDate(date, includeJunk = false) {
|
|
|
380
418
|
}
|
|
381
419
|
|
|
382
420
|
async function messagesSearch(query, options = {}) {
|
|
383
|
-
|
|
384
|
-
|
|
421
|
+
const indexing = stillIndexingMessage();
|
|
422
|
+
if (indexing) {
|
|
423
|
+
return indexing;
|
|
385
424
|
}
|
|
386
425
|
|
|
387
426
|
const ready = await isIndexReady("messages");
|
|
388
427
|
if (!ready) {
|
|
389
|
-
return "
|
|
428
|
+
return indexUnavailableMessage("messages");
|
|
390
429
|
}
|
|
391
430
|
|
|
392
431
|
const result = await searchMessages(query, options);
|
|
@@ -394,13 +433,14 @@ async function messagesSearch(query, options = {}) {
|
|
|
394
433
|
}
|
|
395
434
|
|
|
396
435
|
async function messagesRecent(limit = 10, daysBack = 1) {
|
|
397
|
-
|
|
398
|
-
|
|
436
|
+
const indexing = stillIndexingMessage();
|
|
437
|
+
if (indexing) {
|
|
438
|
+
return indexing;
|
|
399
439
|
}
|
|
400
440
|
|
|
401
441
|
const ready = await isIndexReady("messages");
|
|
402
442
|
if (!ready) {
|
|
403
|
-
return "
|
|
443
|
+
return indexUnavailableMessage("messages");
|
|
404
444
|
}
|
|
405
445
|
|
|
406
446
|
const result = await getRecentMessageResults(limit, daysBack);
|
|
@@ -408,13 +448,14 @@ async function messagesRecent(limit = 10, daysBack = 1) {
|
|
|
408
448
|
}
|
|
409
449
|
|
|
410
450
|
async function messagesConversation(contact, limit = 50) {
|
|
411
|
-
|
|
412
|
-
|
|
451
|
+
const indexing = stillIndexingMessage();
|
|
452
|
+
if (indexing) {
|
|
453
|
+
return indexing;
|
|
413
454
|
}
|
|
414
455
|
|
|
415
456
|
const ready = await isIndexReady("messages");
|
|
416
457
|
if (!ready) {
|
|
417
|
-
return "
|
|
458
|
+
return indexUnavailableMessage("messages");
|
|
418
459
|
}
|
|
419
460
|
|
|
420
461
|
const result = await getConversationResults(contact, limit);
|
|
@@ -422,13 +463,14 @@ async function messagesConversation(contact, limit = 50) {
|
|
|
422
463
|
}
|
|
423
464
|
|
|
424
465
|
async function calendarSearch(query, options = {}) {
|
|
425
|
-
|
|
426
|
-
|
|
466
|
+
const indexing = stillIndexingMessage();
|
|
467
|
+
if (indexing) {
|
|
468
|
+
return indexing;
|
|
427
469
|
}
|
|
428
470
|
|
|
429
471
|
const ready = await isIndexReady("calendar");
|
|
430
472
|
if (!ready) {
|
|
431
|
-
return "
|
|
473
|
+
return indexUnavailableMessage("calendar");
|
|
432
474
|
}
|
|
433
475
|
|
|
434
476
|
const result = await searchCalendar(query, options);
|
|
@@ -591,8 +633,9 @@ function formatSmartSearchResults(results, synthesizedGroups = null) {
|
|
|
591
633
|
|
|
592
634
|
// Smart search - routes to appropriate sources and optionally synthesizes results
|
|
593
635
|
async function smartSearch(query, options = {}) {
|
|
594
|
-
|
|
595
|
-
|
|
636
|
+
const indexing = stillIndexingMessage();
|
|
637
|
+
if (indexing) {
|
|
638
|
+
return indexing;
|
|
596
639
|
}
|
|
597
640
|
|
|
598
641
|
const { limit = 5, synthesize = true } = options;
|
|
@@ -638,6 +681,10 @@ async function smartSearch(query, options = {}) {
|
|
|
638
681
|
|
|
639
682
|
await Promise.all(searches);
|
|
640
683
|
|
|
684
|
+
if (Object.keys(results).length === 0) {
|
|
685
|
+
return indexUnavailableMessage();
|
|
686
|
+
}
|
|
687
|
+
|
|
641
688
|
// Synthesize results into timeline if multiple sources returned data
|
|
642
689
|
let synthesizedGroups = null;
|
|
643
690
|
if (synthesize) {
|
|
@@ -763,8 +810,9 @@ function formatContactLookupResult(contact) {
|
|
|
763
810
|
// ============ PERSON SEARCH (CROSS-SOURCE) ============
|
|
764
811
|
|
|
765
812
|
async function personSearch(name, limit = 10) {
|
|
766
|
-
|
|
767
|
-
|
|
813
|
+
const indexing = stillIndexingMessage();
|
|
814
|
+
if (indexing) {
|
|
815
|
+
return indexing;
|
|
768
816
|
}
|
|
769
817
|
|
|
770
818
|
// First, try to find the contact to get all their identifiers
|
|
@@ -924,7 +972,7 @@ function formatPersonSearchResults(results) {
|
|
|
924
972
|
// ============ MCP SERVER SETUP ============
|
|
925
973
|
|
|
926
974
|
const server = new Server(
|
|
927
|
-
{ name: "apple-tools-mcp", version:
|
|
975
|
+
{ name: "apple-tools-mcp", version: PACKAGE_VERSION },
|
|
928
976
|
{ capabilities: { tools: {} } }
|
|
929
977
|
);
|
|
930
978
|
|
|
@@ -1342,8 +1390,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1342
1390
|
|
|
1343
1391
|
// Mail tools
|
|
1344
1392
|
case "mail_senders":
|
|
1345
|
-
|
|
1346
|
-
|
|
1393
|
+
{
|
|
1394
|
+
const indexing = stillIndexingMessage();
|
|
1395
|
+
if (indexing) {
|
|
1396
|
+
result = indexing;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (!(await isIndexReady("emails"))) {
|
|
1401
|
+
result = indexUnavailableMessage("emails");
|
|
1347
1402
|
break;
|
|
1348
1403
|
}
|
|
1349
1404
|
result = formatSendersResults(await getFrequentSenders(
|
|
@@ -1356,13 +1411,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1356
1411
|
case "rebuild_index":
|
|
1357
1412
|
// Check if indexing is already in progress in this session
|
|
1358
1413
|
if (indexingInProgress) {
|
|
1359
|
-
result = "
|
|
1414
|
+
result = "Indexing is already in progress. Please wait for it to complete before starting a rebuild.";
|
|
1360
1415
|
break;
|
|
1361
1416
|
}
|
|
1362
1417
|
|
|
1363
1418
|
// Acquire lock to prevent parallel rebuilds across multiple MCP instances
|
|
1364
1419
|
if (!acquireLock()) {
|
|
1365
|
-
result = "
|
|
1420
|
+
result = "Indexing is already in progress in a different session. Please wait for it to complete before starting a rebuild.";
|
|
1366
1421
|
break;
|
|
1367
1422
|
}
|
|
1368
1423
|
|
|
@@ -1373,10 +1428,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1373
1428
|
|
|
1374
1429
|
// Fire and forget - don't await
|
|
1375
1430
|
rebuildIndex(rebuildSources).then((rebuildResult) => {
|
|
1376
|
-
|
|
1377
|
-
isFirstEverRun = false;
|
|
1378
|
-
indexingInProgress = false;
|
|
1379
|
-
releaseLock();
|
|
1431
|
+
applyCycleEnd(true);
|
|
1380
1432
|
console.error("Index rebuild completed:", JSON.stringify({
|
|
1381
1433
|
cleared: rebuildResult.cleared,
|
|
1382
1434
|
indexed: Object.fromEntries(
|
|
@@ -1386,8 +1438,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1386
1438
|
}));
|
|
1387
1439
|
}).catch(e => {
|
|
1388
1440
|
console.error("Index rebuild error:", e.message);
|
|
1389
|
-
|
|
1390
|
-
releaseLock();
|
|
1441
|
+
applyCycleEnd(false);
|
|
1391
1442
|
});
|
|
1392
1443
|
|
|
1393
1444
|
result = `🔄 Index rebuild started for: ${rebuildSources.join(", ")}.\n\nThis runs in the background and may take several minutes for large mailboxes. You can continue using other tools - searches will use the new index once complete.`;
|
|
@@ -1423,8 +1474,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1423
1474
|
// ============ NEW TOOLS - PHASE 3 ============
|
|
1424
1475
|
|
|
1425
1476
|
case "mail_thread":
|
|
1426
|
-
|
|
1427
|
-
|
|
1477
|
+
{
|
|
1478
|
+
const indexing = stillIndexingMessage();
|
|
1479
|
+
if (indexing) {
|
|
1480
|
+
result = indexing;
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (!(await isIndexReady("emails"))) {
|
|
1485
|
+
result = indexUnavailableMessage("emails");
|
|
1428
1486
|
break;
|
|
1429
1487
|
}
|
|
1430
1488
|
result = formatEmailThreadResults(await getEmailThread(args.file_path, validateLimit(args?.limit, 30)));
|
|
@@ -1465,7 +1523,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1465
1523
|
async function main() {
|
|
1466
1524
|
const transport = new StdioServerTransport();
|
|
1467
1525
|
await server.connect(transport);
|
|
1468
|
-
console.error(
|
|
1526
|
+
console.error(`Apple Tools MCP server running (v${PACKAGE_VERSION})`);
|
|
1469
1527
|
// Background indexing runs automatically on startup and every INDEX_INTERVAL
|
|
1470
1528
|
}
|
|
1471
1529
|
|
package/indexer.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
stripSubjectPrefixes
|
|
16
16
|
} from "./lib/validators.js";
|
|
17
17
|
import { safeSqlite3Json, safeOsascript, safeFind } from "./lib/shell.js";
|
|
18
|
+
import { indexUnavailableMessage } from "./lib/indexGate.js";
|
|
18
19
|
|
|
19
20
|
// Re-export contact functions for use by other modules
|
|
20
21
|
export {
|
|
@@ -1936,7 +1937,7 @@ export function getWeekEvents(weekOffset = 0) {
|
|
|
1936
1937
|
// Uses subject-based matching since Message-ID isn't indexed
|
|
1937
1938
|
export async function getEmailThread(filePath, limit = 20) {
|
|
1938
1939
|
await initDB();
|
|
1939
|
-
if (!tables.emails) return { error: "
|
|
1940
|
+
if (!tables.emails) return { error: indexUnavailableMessage("emails"), emails: [] };
|
|
1940
1941
|
|
|
1941
1942
|
try {
|
|
1942
1943
|
// Validate file path to prevent path traversal attacks
|
package/lib/indexGate.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index-session search gating.
|
|
3
|
+
*
|
|
4
|
+
* sessionIndexComplete means "this process finished (or abandoned) its own
|
|
5
|
+
* index cycle." A second MCP instance that lost the indexer lock never runs
|
|
6
|
+
* a cycle, so that flag stays false. Lost-lock must not be treated as
|
|
7
|
+
* "still indexing" — callers still check isIndexReady() for a missing index.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether index-backed tools should return the still-indexing message.
|
|
12
|
+
*
|
|
13
|
+
* @param {boolean} sessionIndexComplete
|
|
14
|
+
* @param {boolean} ownsIndexLock true if this process won/holds the indexer lock
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function isSearchBlockedByIndexing(sessionIndexComplete, ownsIndexLock) {
|
|
18
|
+
return Boolean(ownsIndexLock) && !sessionIndexComplete;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* In-memory flags after an index or rebuild cycle ends.
|
|
23
|
+
* Searches must be unblocked on both success and failure.
|
|
24
|
+
*
|
|
25
|
+
* @param {boolean} success
|
|
26
|
+
* @returns {{ indexingInProgress: false, sessionIndexComplete: true, ownsIndexLock: false, isFirstEverRun?: false }}
|
|
27
|
+
*/
|
|
28
|
+
export function cycleEndFlags(success) {
|
|
29
|
+
const flags = {
|
|
30
|
+
indexingInProgress: false,
|
|
31
|
+
sessionIndexComplete: true,
|
|
32
|
+
ownsIndexLock: false
|
|
33
|
+
};
|
|
34
|
+
if (success) {
|
|
35
|
+
flags.isFirstEverRun = false;
|
|
36
|
+
}
|
|
37
|
+
return flags;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* User-facing message when a source table is missing.
|
|
42
|
+
* Distinct from still-indexing: retry because the index is not there yet,
|
|
43
|
+
* not because this process is mid-cycle.
|
|
44
|
+
*
|
|
45
|
+
* @param {"emails"|"messages"|"calendar"|undefined} type
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function indexUnavailableMessage(type) {
|
|
49
|
+
if (type === "messages") {
|
|
50
|
+
return "Messages index not available. Please try again shortly.";
|
|
51
|
+
}
|
|
52
|
+
if (type === "calendar") {
|
|
53
|
+
return "Calendar index not available. Please try again shortly.";
|
|
54
|
+
}
|
|
55
|
+
if (type === "emails") {
|
|
56
|
+
return "Email index not available. Please try again shortly.";
|
|
57
|
+
}
|
|
58
|
+
return "Index not available. Please try again shortly.";
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-tools-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "MCP server for semantic search across Apple Mail, Messages, and Calendar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -27,8 +27,6 @@
|
|
|
27
27
|
"messages",
|
|
28
28
|
"imessage",
|
|
29
29
|
"semantic-search",
|
|
30
|
-
"claude",
|
|
31
|
-
"anthropic",
|
|
32
30
|
"macos"
|
|
33
31
|
],
|
|
34
32
|
"engines": {
|
package/search.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as chrono from "chrono-node";
|
|
|
3
3
|
import { safeOsascript } from "./lib/shell.js";
|
|
4
4
|
import { safeMatch, validateSearchQuery, toUnixMillis } from "./lib/validators.js";
|
|
5
5
|
import { embed, INDEX_DIR, getRecentEmails, getEmailsByDateRange, getRecentMessages, getConversation, getEventsOnDate, resolveEmail, resolvePhone, formatContact } from "./indexer.js";
|
|
6
|
+
import { indexUnavailableMessage } from "./lib/indexGate.js";
|
|
6
7
|
|
|
7
8
|
let db = null;
|
|
8
9
|
let tables = {};
|
|
@@ -106,12 +107,11 @@ function resolvePronouns(query) {
|
|
|
106
107
|
return query;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
//
|
|
110
|
-
// first (or only) pronoun
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
110
|
+
// Never use RegExp#test with a /g regex — lastIndex is stateful and can
|
|
111
|
+
// skip the first (or only) pronoun on this or a later call. A fresh
|
|
112
|
+
// regex plus replace() is lastIndex-safe; do not hoist or test() it.
|
|
113
|
+
const pronounRe = new RegExp('\\b(they|them|their|he|him|his|she|her|hers)\\b', 'gi');
|
|
114
|
+
return query.replace(pronounRe, queryContext.lastPerson);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// Extract entities (people, dates) from natural language query and convert to filters
|
|
@@ -627,7 +627,7 @@ export async function searchEmails(query, options = {}) {
|
|
|
627
627
|
if (!tbl) {
|
|
628
628
|
return {
|
|
629
629
|
success: false,
|
|
630
|
-
error: "
|
|
630
|
+
error: indexUnavailableMessage("emails")
|
|
631
631
|
};
|
|
632
632
|
}
|
|
633
633
|
|
|
@@ -1022,7 +1022,7 @@ export async function searchMessages(query, options = {}) {
|
|
|
1022
1022
|
if (!tbl) {
|
|
1023
1023
|
return {
|
|
1024
1024
|
success: false,
|
|
1025
|
-
error: "
|
|
1025
|
+
error: indexUnavailableMessage("messages")
|
|
1026
1026
|
};
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
@@ -1252,7 +1252,7 @@ export async function searchCalendar(query, options = {}) {
|
|
|
1252
1252
|
if (!tbl) {
|
|
1253
1253
|
return {
|
|
1254
1254
|
success: false,
|
|
1255
|
-
error: "
|
|
1255
|
+
error: indexUnavailableMessage("calendar")
|
|
1256
1256
|
};
|
|
1257
1257
|
}
|
|
1258
1258
|
|
|
@@ -1673,6 +1673,9 @@ export function formatWeekEventsResults(result) {
|
|
|
1673
1673
|
// Format mail_thread results
|
|
1674
1674
|
export function formatEmailThreadResults(result) {
|
|
1675
1675
|
if (result.error) {
|
|
1676
|
+
if (result.error === indexUnavailableMessage("emails")) {
|
|
1677
|
+
return result.error;
|
|
1678
|
+
}
|
|
1676
1679
|
return `Error: ${result.error}`;
|
|
1677
1680
|
}
|
|
1678
1681
|
|