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 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 directly from Claude.
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 Claude Desktop at the local `index.js` instead of `npx` in step 3:
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 Claude Desktop
66
+ ### 3. Configure your MCP client
68
67
 
69
- Add to your Claude Desktop config file:
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
- **Location:** `~/Library/Application Support/Claude/claude_desktop_config.json`
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
- ### 4. Restart Claude Desktop
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 to load the MCP server.
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, Claude can use these tools:
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 Claude things like:
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 Claude
198
+ ### Server not appearing in the MCP client
191
199
 
192
- 1. Verify your config file syntax is valid JSON
193
- 2. Restart Claude Desktop completely (Cmd+Q, then reopen)
194
- 3. Check Claude's MCP logs for errors
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 Claude Desktop to trigger a fresh rebuild
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 real-time:
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 (Claude client disconnected)
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
- indexingInProgress = false;
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
- indexingInProgress = false;
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
- if (!sessionIndexComplete) {
342
- return getIndexingMessage();
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 "Email index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
356
- return getIndexingMessage();
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 "Email index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
370
- return getIndexingMessage();
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 "Email index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
384
- return getIndexingMessage();
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 "Messages index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
398
- return getIndexingMessage();
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 "Messages index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
412
- return getIndexingMessage();
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 "Messages index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
426
- return getIndexingMessage();
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 "Calendar index not available. Please try again shortly.";
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
- if (!sessionIndexComplete) {
595
- return getIndexingMessage();
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
- if (!sessionIndexComplete) {
767
- return getIndexingMessage();
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: "2.0.0" },
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
- if (!sessionIndexComplete) {
1346
- result = getIndexingMessage();
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 = "⏳ Indexing is already in progress. Please wait for it to complete before starting a rebuild.";
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 = "Another indexing operation is already in progress in a different session. Please wait for it to complete.";
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
- sessionIndexComplete = true;
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
- indexingInProgress = false;
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
- if (!sessionIndexComplete) {
1427
- result = getIndexingMessage();
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("Apple Tools MCP server running (v1.1.2)");
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: "Email index not ready", emails: [] };
1940
+ if (!tables.emails) return { error: indexUnavailableMessage("emails"), emails: [] };
1940
1941
 
1941
1942
  try {
1942
1943
  // Validate file path to prevent path traversal attacks
@@ -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.2",
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
- // Do not use RegExp#test with /g — lastIndex is stateful and can skip the
110
- // first (or only) pronoun. replace() with a fresh regex is sufficient.
111
- return query.replace(
112
- /\b(they|them|their|he|him|his|she|her|hers)\b/gi,
113
- queryContext.lastPerson
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: "Email index not ready. Please wait for indexing to complete."
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: "Messages index not ready. Please wait for indexing to complete."
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: "Calendar index not ready. Please wait for indexing to complete."
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