apple-tools-mcp 1.1.4 → 1.2.1
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 +111 -6
- package/index.js +172 -193
- package/indexer.js +26 -21
- package/lib/audit.js +22 -14
- package/lib/config.js +312 -0
- package/lib/indexGate.js +49 -0
- package/lib/indexerLock.js +376 -0
- package/lib/indexerRuntime.js +142 -0
- package/lib/lancedbTables.js +307 -0
- package/lib/processMode.js +28 -0
- package/package.json +4 -2
- package/search.js +4 -18
package/index.js
CHANGED
|
@@ -9,143 +9,111 @@ 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 {
|
|
12
|
+
import { cycleEndFlags, indexUnavailableMessage, indexQueryGate } from "./lib/indexGate.js";
|
|
13
|
+
import { isIndexerMode } from "./lib/processMode.js";
|
|
14
|
+
import { loadResolvedIndexInterval, logResolvedInterval } from "./lib/config.js";
|
|
15
|
+
import { createIndexerLock, DEFAULT_LOCK_HEARTBEAT_MS } from "./lib/indexerLock.js";
|
|
16
|
+
import {
|
|
17
|
+
shouldConnectMcpStdio,
|
|
18
|
+
bindStdinCloseExit,
|
|
19
|
+
beginIndexCycle,
|
|
20
|
+
applyIndexerCycleEnd,
|
|
21
|
+
mcpIndexingStartup,
|
|
22
|
+
waitForIndexerLock,
|
|
23
|
+
beginOwnedIndexing
|
|
24
|
+
} from "./lib/indexerRuntime.js";
|
|
13
25
|
|
|
14
26
|
const PACKAGE_VERSION = JSON.parse(
|
|
15
27
|
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
|
16
28
|
).version;
|
|
17
29
|
|
|
30
|
+
// Canonical indexer entrypoint: `node index.js --mode=indexer` or `apple-tools-indexer`.
|
|
31
|
+
const INDEXER_MODE = isIndexerMode();
|
|
32
|
+
const resolvedIndexInterval = loadResolvedIndexInterval();
|
|
33
|
+
const INDEX_INTERVAL = resolvedIndexInterval.ms;
|
|
34
|
+
const LOCK_HEARTBEAT_MS = DEFAULT_LOCK_HEARTBEAT_MS;
|
|
35
|
+
const LOCK_RETRY_MS = 5 * 1000;
|
|
36
|
+
|
|
18
37
|
// Lock file to prevent duplicate indexing processes
|
|
19
38
|
const LOCK_FILE = path.join(process.env.HOME, ".apple-tools-mcp", "indexer.lock");
|
|
20
|
-
const
|
|
39
|
+
const indexerLock = createIndexerLock({
|
|
40
|
+
lockFile: LOCK_FILE,
|
|
41
|
+
log: (msg) => console.error(msg)
|
|
42
|
+
});
|
|
21
43
|
// True only while this process won the indexer lock. Distinct from
|
|
22
44
|
// sessionIndexComplete: a secondary instance that lost the lock never
|
|
23
45
|
// completes a local cycle and must not stay on "still indexing" forever.
|
|
24
46
|
let ownsIndexLock = false;
|
|
25
47
|
|
|
26
48
|
function acquireLock() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
const ok = indexerLock.acquire();
|
|
50
|
+
ownsIndexLock = indexerLock.ownsLock;
|
|
51
|
+
if (ok) {
|
|
52
|
+
startLockHeartbeat();
|
|
53
|
+
}
|
|
54
|
+
return ok;
|
|
55
|
+
}
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// If we already hold the lock, return true
|
|
43
|
-
if (pid === process.pid) {
|
|
44
|
-
ownsIndexLock = true;
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
57
|
+
function releaseLock() {
|
|
58
|
+
indexerLock.release();
|
|
59
|
+
ownsIndexLock = indexerLock.ownsLock;
|
|
60
|
+
if (!ownsIndexLock) {
|
|
61
|
+
stopLockHeartbeat();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Process exists - check if lock is stale (hung process)
|
|
52
|
-
if (lockAge > LOCK_TIMEOUT_MS) {
|
|
53
|
-
console.error(`Lock file is ${Math.round(lockAge / 60000)} minutes old. Assuming hung process (PID ${pid}). Removing stale lock.`);
|
|
54
|
-
fs.unlinkSync(LOCK_FILE);
|
|
55
|
-
} else {
|
|
56
|
-
console.error(`Another indexing instance running (PID ${pid}). Skipping indexing.`);
|
|
57
|
-
ownsIndexLock = false;
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Process doesn't exist, stale lock file - remove it
|
|
62
|
-
console.error(`Removing stale lock file (PID ${pid} not running)`);
|
|
63
|
-
fs.unlinkSync(LOCK_FILE);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
65
|
+
function startLockHeartbeat() {
|
|
66
|
+
indexerLock.startHeartbeat(LOCK_HEARTBEAT_MS);
|
|
67
|
+
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
fs.writeFileSync(LOCK_FILE, `${process.pid}:${Date.now()}`, { flag: 'wx' });
|
|
71
|
-
ownsIndexLock = true;
|
|
72
|
-
return true;
|
|
73
|
-
} catch (err) {
|
|
74
|
-
if (err.code === 'EEXIST') {
|
|
75
|
-
// Another process won the race
|
|
76
|
-
console.error("Another process acquired lock during race. Skipping indexing.");
|
|
77
|
-
ownsIndexLock = false;
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
throw err; // Re-throw unexpected errors
|
|
81
|
-
}
|
|
82
|
-
} catch (e) {
|
|
83
|
-
console.error("Lock file error:", e.message);
|
|
84
|
-
ownsIndexLock = false;
|
|
85
|
-
return false; // On error, fail safe - don't proceed
|
|
86
|
-
}
|
|
69
|
+
function stopLockHeartbeat() {
|
|
70
|
+
indexerLock.stopHeartbeat();
|
|
87
71
|
}
|
|
88
72
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (pid === process.pid) {
|
|
96
|
-
fs.unlinkSync(LOCK_FILE);
|
|
97
|
-
ownsIndexLock = false;
|
|
98
|
-
console.error(`Released lock file (PID ${process.pid})`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
// Log error but don't throw - we're likely shutting down
|
|
103
|
-
console.error(`Error releasing lock: ${err.message}`);
|
|
73
|
+
function shutdownIndexing(exitCode) {
|
|
74
|
+
stopBackgroundIndexing();
|
|
75
|
+
stopLockHeartbeat();
|
|
76
|
+
releaseLock();
|
|
77
|
+
if (exitCode !== undefined) {
|
|
78
|
+
process.exit(exitCode);
|
|
104
79
|
}
|
|
105
80
|
}
|
|
106
81
|
|
|
107
82
|
// Clean up lock and timer on exit
|
|
108
83
|
process.on("exit", () => {
|
|
109
84
|
stopBackgroundIndexing();
|
|
85
|
+
stopLockHeartbeat();
|
|
110
86
|
releaseLock();
|
|
111
87
|
});
|
|
112
88
|
process.on("SIGINT", () => {
|
|
113
|
-
|
|
114
|
-
releaseLock();
|
|
89
|
+
shutdownIndexing();
|
|
115
90
|
process.exit();
|
|
116
91
|
});
|
|
117
92
|
process.on("SIGTERM", () => {
|
|
118
|
-
|
|
119
|
-
releaseLock();
|
|
93
|
+
shutdownIndexing();
|
|
120
94
|
process.exit();
|
|
121
95
|
});
|
|
122
96
|
process.on("SIGHUP", () => {
|
|
123
|
-
|
|
124
|
-
releaseLock();
|
|
97
|
+
shutdownIndexing();
|
|
125
98
|
process.exit();
|
|
126
99
|
});
|
|
127
100
|
|
|
128
101
|
// Handle uncaught errors - cleanup before crashing
|
|
129
102
|
process.on("uncaughtException", (err) => {
|
|
130
103
|
console.error("Uncaught exception:", err);
|
|
131
|
-
|
|
132
|
-
releaseLock();
|
|
133
|
-
process.exit(1);
|
|
104
|
+
shutdownIndexing(1);
|
|
134
105
|
});
|
|
135
106
|
|
|
136
107
|
process.on("unhandledRejection", (reason, promise) => {
|
|
137
108
|
console.error("Unhandled rejection at:", promise, "reason:", reason);
|
|
138
|
-
|
|
139
|
-
releaseLock();
|
|
140
|
-
process.exit(1);
|
|
109
|
+
shutdownIndexing(1);
|
|
141
110
|
});
|
|
142
111
|
|
|
143
|
-
//
|
|
144
|
-
|
|
112
|
+
// MCP stdio clients exit when the host closes stdin. The indexer daemon must
|
|
113
|
+
// not — LaunchAgent / KeepAlive often attaches stdin to /dev/null.
|
|
114
|
+
bindStdinCloseExit(process.stdin, INDEXER_MODE, () => {
|
|
145
115
|
console.error("Client disconnected. Exiting.");
|
|
146
|
-
|
|
147
|
-
releaseLock();
|
|
148
|
-
process.exit(0);
|
|
116
|
+
shutdownIndexing(0);
|
|
149
117
|
});
|
|
150
118
|
|
|
151
119
|
// Vector search imports
|
|
@@ -199,10 +167,9 @@ let sessionIndexComplete = false; // Track if this session's indexing is done
|
|
|
199
167
|
let isFirstEverRun = true; // True if no index exists yet
|
|
200
168
|
let lastIndexTime = 0;
|
|
201
169
|
let lastProgressTime = 0; // Track when we last made progress (for hung detection)
|
|
202
|
-
// Allow environment variable to override default 5-minute interval
|
|
203
|
-
const INDEX_INTERVAL = parseInt(process.env.INDEX_INTERVAL_MS || (5 * 60 * 1000));
|
|
204
170
|
let indexTimer = null;
|
|
205
171
|
let progressCheckTimer = null;
|
|
172
|
+
let loggedIndexInterval = false;
|
|
206
173
|
|
|
207
174
|
// Check if this is the first ever run (no index exists)
|
|
208
175
|
async function checkIfFirstRun() {
|
|
@@ -213,23 +180,17 @@ async function checkIfFirstRun() {
|
|
|
213
180
|
return !(emailsReady || messagesReady || calendarReady);
|
|
214
181
|
}
|
|
215
182
|
|
|
216
|
-
// Get appropriate status message based on indexing state
|
|
217
|
-
function getIndexingMessage() {
|
|
218
|
-
if (isFirstEverRun) {
|
|
219
|
-
return "Building initial index. This may take several minutes on first run. Please try again shortly.";
|
|
220
|
-
}
|
|
221
|
-
return "Indexing new data. Please try again in a moment.";
|
|
222
|
-
}
|
|
223
|
-
|
|
224
183
|
// Run a single indexing cycle (called by background timer)
|
|
225
184
|
function runIndexCycle() {
|
|
226
|
-
|
|
227
|
-
|
|
185
|
+
const cycle = beginIndexCycle(indexingInProgress);
|
|
186
|
+
if (!cycle.started) {
|
|
228
187
|
return;
|
|
229
188
|
}
|
|
189
|
+
indexingInProgress = cycle.indexingInProgress;
|
|
230
190
|
|
|
231
191
|
// Safety net: check lock before indexing
|
|
232
192
|
if (!acquireLock()) {
|
|
193
|
+
indexingInProgress = false;
|
|
233
194
|
console.error("Another instance is indexing. Skipping.");
|
|
234
195
|
return;
|
|
235
196
|
}
|
|
@@ -299,6 +260,11 @@ function triggerIndexIfNeeded() {
|
|
|
299
260
|
|
|
300
261
|
// Start continuous background indexing
|
|
301
262
|
function startBackgroundIndexing() {
|
|
263
|
+
if (!loggedIndexInterval) {
|
|
264
|
+
logResolvedInterval(resolvedIndexInterval);
|
|
265
|
+
loggedIndexInterval = true;
|
|
266
|
+
}
|
|
267
|
+
|
|
302
268
|
// Run indexing immediately on startup
|
|
303
269
|
runIndexCycle();
|
|
304
270
|
|
|
@@ -307,7 +273,7 @@ function startBackgroundIndexing() {
|
|
|
307
273
|
runIndexCycle();
|
|
308
274
|
}, INDEX_INTERVAL);
|
|
309
275
|
|
|
310
|
-
console.error(`Background indexing started (interval: ${
|
|
276
|
+
console.error(`Background indexing started (interval: ${resolvedIndexInterval.human} / ${INDEX_INTERVAL} ms)`);
|
|
311
277
|
}
|
|
312
278
|
|
|
313
279
|
// Stop background indexing and clean up timers
|
|
@@ -323,44 +289,97 @@ function stopBackgroundIndexing() {
|
|
|
323
289
|
console.error("Background indexing stopped");
|
|
324
290
|
}
|
|
325
291
|
|
|
326
|
-
// Unblock searches
|
|
327
|
-
//
|
|
292
|
+
// Unblock searches after a cycle ends. Must run on failure as well as success
|
|
293
|
+
// so tools are not stuck forever. The indexer daemon keeps indexer.lock for
|
|
294
|
+
// the process lifetime; MCP local-fallback still releases between cycles.
|
|
328
295
|
function applyCycleEnd(success) {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
296
|
+
const result = applyIndexerCycleEnd({
|
|
297
|
+
success,
|
|
298
|
+
indexerMode: INDEXER_MODE,
|
|
299
|
+
cycleEndFlags,
|
|
300
|
+
releaseLock
|
|
301
|
+
});
|
|
302
|
+
indexingInProgress = result.indexingInProgress;
|
|
303
|
+
sessionIndexComplete = result.sessionIndexComplete;
|
|
304
|
+
ownsIndexLock = result.ownsIndexLock;
|
|
305
|
+
if (result.isFirstEverRun === false) {
|
|
334
306
|
isFirstEverRun = false;
|
|
335
307
|
}
|
|
336
|
-
releaseLock();
|
|
337
308
|
}
|
|
338
309
|
|
|
339
310
|
// Index-backed tools wait only while THIS process owns the lock and has not
|
|
340
311
|
// finished its cycle. Lost-lock secondaries fall through to isIndexReady().
|
|
341
312
|
function stillIndexingMessage() {
|
|
342
|
-
|
|
343
|
-
|
|
313
|
+
const gate = indexQueryGate({
|
|
314
|
+
sessionIndexComplete,
|
|
315
|
+
ownsIndexLock,
|
|
316
|
+
indexReady: true,
|
|
317
|
+
isFirstEverRun
|
|
318
|
+
});
|
|
319
|
+
return gate.ok ? null : gate.message;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Shared preflight for index-backed query tools (on-disk tables, not local cycle). */
|
|
323
|
+
async function requireIndex(type) {
|
|
324
|
+
const indexing = stillIndexingMessage();
|
|
325
|
+
if (indexing) {
|
|
326
|
+
return indexing;
|
|
344
327
|
}
|
|
345
|
-
|
|
328
|
+
const ready = await isIndexReady(type);
|
|
329
|
+
const gate = indexQueryGate({
|
|
330
|
+
sessionIndexComplete,
|
|
331
|
+
ownsIndexLock,
|
|
332
|
+
indexReady: ready,
|
|
333
|
+
type,
|
|
334
|
+
isFirstEverRun
|
|
335
|
+
});
|
|
336
|
+
return gate.ok ? null : gate.message;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function waitForLockAndStartDaemon() {
|
|
340
|
+
waitForIndexerLock(acquireLock, {
|
|
341
|
+
retryMs: LOCK_RETRY_MS,
|
|
342
|
+
onAcquired: () => {
|
|
343
|
+
beginOwnedIndexing({
|
|
344
|
+
startHeartbeat: startLockHeartbeat,
|
|
345
|
+
startBackground: startBackgroundIndexing
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
346
349
|
}
|
|
347
350
|
|
|
348
351
|
// Initialize and start indexing
|
|
349
352
|
async function initializeIndexing() {
|
|
350
353
|
isFirstEverRun = await checkIfFirstRun();
|
|
351
354
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
if (INDEXER_MODE) {
|
|
356
|
+
console.error(`Apple Tools MCP indexer running (v${PACKAGE_VERSION})`);
|
|
357
|
+
logResolvedInterval(resolvedIndexInterval);
|
|
358
|
+
loggedIndexInterval = true;
|
|
359
|
+
waitForLockAndStartDaemon();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// MCP stdio: if the indexer daemon (or another instance) holds the lock,
|
|
364
|
+
// skip background refresh and use the shared index. If nothing holds the
|
|
365
|
+
// lock, index locally as before so the stdio happy path still works.
|
|
366
|
+
const startup = mcpIndexingStartup(() => acquireLock());
|
|
367
|
+
if (!startup.startBackground) {
|
|
355
368
|
console.error("Another apple-tools-mcp instance is indexing. Server will run without background indexing.");
|
|
356
369
|
// Lost lock is not "still indexing": this process will never complete a
|
|
357
|
-
// local cycle. Searches proceed whenever isIndexReady() is true
|
|
370
|
+
// local cycle. Searches proceed whenever isIndexReady() is true (initDB
|
|
371
|
+
// re-lists shared on-disk tables; it must not cache an empty first connect).
|
|
358
372
|
ownsIndexLock = false;
|
|
359
373
|
return;
|
|
360
374
|
}
|
|
361
375
|
|
|
362
|
-
// Start background indexing
|
|
363
|
-
|
|
376
|
+
// Start background indexing (local fallback when no daemon is running)
|
|
377
|
+
// using the same heartbeat path as the daemon so a long first-index cycle
|
|
378
|
+
// cannot look like a stale lock to another waiter.
|
|
379
|
+
beginOwnedIndexing({
|
|
380
|
+
startHeartbeat: startLockHeartbeat,
|
|
381
|
+
startBackground: startBackgroundIndexing
|
|
382
|
+
});
|
|
364
383
|
}
|
|
365
384
|
|
|
366
385
|
// Start indexing immediately on server startup
|
|
@@ -373,14 +392,9 @@ async function mailSearch(query, options = {}) {
|
|
|
373
392
|
return "Error: query parameter is required for mail_search";
|
|
374
393
|
}
|
|
375
394
|
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const ready = await isIndexReady("emails");
|
|
382
|
-
if (!ready) {
|
|
383
|
-
return indexUnavailableMessage("emails");
|
|
395
|
+
const blocked = await requireIndex("emails");
|
|
396
|
+
if (blocked) {
|
|
397
|
+
return blocked;
|
|
384
398
|
}
|
|
385
399
|
|
|
386
400
|
const result = await searchEmails(query, options);
|
|
@@ -388,14 +402,9 @@ async function mailSearch(query, options = {}) {
|
|
|
388
402
|
}
|
|
389
403
|
|
|
390
404
|
async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJunk = false) {
|
|
391
|
-
const
|
|
392
|
-
if (
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const ready = await isIndexReady("emails");
|
|
397
|
-
if (!ready) {
|
|
398
|
-
return indexUnavailableMessage("emails");
|
|
405
|
+
const blocked = await requireIndex("emails");
|
|
406
|
+
if (blocked) {
|
|
407
|
+
return blocked;
|
|
399
408
|
}
|
|
400
409
|
|
|
401
410
|
const result = await getRecentEmailResults(limit, daysBack, unreadOnly, includeJunk);
|
|
@@ -403,14 +412,9 @@ async function mailRecent(limit = 30, daysBack = 7, unreadOnly = false, includeJ
|
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
async function mailDate(date, includeJunk = false) {
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
return
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const ready = await isIndexReady("emails");
|
|
412
|
-
if (!ready) {
|
|
413
|
-
return indexUnavailableMessage("emails");
|
|
415
|
+
const blocked = await requireIndex("emails");
|
|
416
|
+
if (blocked) {
|
|
417
|
+
return blocked;
|
|
414
418
|
}
|
|
415
419
|
|
|
416
420
|
const result = await getEmailDateResults(date, includeJunk);
|
|
@@ -418,14 +422,9 @@ async function mailDate(date, includeJunk = false) {
|
|
|
418
422
|
}
|
|
419
423
|
|
|
420
424
|
async function messagesSearch(query, options = {}) {
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const ready = await isIndexReady("messages");
|
|
427
|
-
if (!ready) {
|
|
428
|
-
return indexUnavailableMessage("messages");
|
|
425
|
+
const blocked = await requireIndex("messages");
|
|
426
|
+
if (blocked) {
|
|
427
|
+
return blocked;
|
|
429
428
|
}
|
|
430
429
|
|
|
431
430
|
const result = await searchMessages(query, options);
|
|
@@ -433,14 +432,9 @@ async function messagesSearch(query, options = {}) {
|
|
|
433
432
|
}
|
|
434
433
|
|
|
435
434
|
async function messagesRecent(limit = 10, daysBack = 1) {
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const ready = await isIndexReady("messages");
|
|
442
|
-
if (!ready) {
|
|
443
|
-
return indexUnavailableMessage("messages");
|
|
435
|
+
const blocked = await requireIndex("messages");
|
|
436
|
+
if (blocked) {
|
|
437
|
+
return blocked;
|
|
444
438
|
}
|
|
445
439
|
|
|
446
440
|
const result = await getRecentMessageResults(limit, daysBack);
|
|
@@ -448,14 +442,9 @@ async function messagesRecent(limit = 10, daysBack = 1) {
|
|
|
448
442
|
}
|
|
449
443
|
|
|
450
444
|
async function messagesConversation(contact, limit = 50) {
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const ready = await isIndexReady("messages");
|
|
457
|
-
if (!ready) {
|
|
458
|
-
return indexUnavailableMessage("messages");
|
|
445
|
+
const blocked = await requireIndex("messages");
|
|
446
|
+
if (blocked) {
|
|
447
|
+
return blocked;
|
|
459
448
|
}
|
|
460
449
|
|
|
461
450
|
const result = await getConversationResults(contact, limit);
|
|
@@ -463,14 +452,9 @@ async function messagesConversation(contact, limit = 50) {
|
|
|
463
452
|
}
|
|
464
453
|
|
|
465
454
|
async function calendarSearch(query, options = {}) {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const ready = await isIndexReady("calendar");
|
|
472
|
-
if (!ready) {
|
|
473
|
-
return indexUnavailableMessage("calendar");
|
|
455
|
+
const blocked = await requireIndex("calendar");
|
|
456
|
+
if (blocked) {
|
|
457
|
+
return blocked;
|
|
474
458
|
}
|
|
475
459
|
|
|
476
460
|
const result = await searchCalendar(query, options);
|
|
@@ -1391,16 +1375,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1391
1375
|
// Mail tools
|
|
1392
1376
|
case "mail_senders":
|
|
1393
1377
|
{
|
|
1394
|
-
const
|
|
1395
|
-
if (
|
|
1396
|
-
result =
|
|
1378
|
+
const blocked = await requireIndex("emails");
|
|
1379
|
+
if (blocked) {
|
|
1380
|
+
result = blocked;
|
|
1397
1381
|
break;
|
|
1398
1382
|
}
|
|
1399
1383
|
}
|
|
1400
|
-
if (!(await isIndexReady("emails"))) {
|
|
1401
|
-
result = indexUnavailableMessage("emails");
|
|
1402
|
-
break;
|
|
1403
|
-
}
|
|
1404
1384
|
result = formatSendersResults(await getFrequentSenders(
|
|
1405
1385
|
validateLimit(args?.limit, 30),
|
|
1406
1386
|
validateDaysBack(args?.days_back),
|
|
@@ -1475,16 +1455,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1475
1455
|
|
|
1476
1456
|
case "mail_thread":
|
|
1477
1457
|
{
|
|
1478
|
-
const
|
|
1479
|
-
if (
|
|
1480
|
-
result =
|
|
1458
|
+
const blocked = await requireIndex("emails");
|
|
1459
|
+
if (blocked) {
|
|
1460
|
+
result = blocked;
|
|
1481
1461
|
break;
|
|
1482
1462
|
}
|
|
1483
1463
|
}
|
|
1484
|
-
if (!(await isIndexReady("emails"))) {
|
|
1485
|
-
result = indexUnavailableMessage("emails");
|
|
1486
|
-
break;
|
|
1487
|
-
}
|
|
1488
1464
|
result = formatEmailThreadResults(await getEmailThread(args.file_path, validateLimit(args?.limit, 30)));
|
|
1489
1465
|
break;
|
|
1490
1466
|
|
|
@@ -1524,7 +1500,10 @@ async function main() {
|
|
|
1524
1500
|
const transport = new StdioServerTransport();
|
|
1525
1501
|
await server.connect(transport);
|
|
1526
1502
|
console.error(`Apple Tools MCP server running (v${PACKAGE_VERSION})`);
|
|
1527
|
-
// Background indexing
|
|
1503
|
+
// Background indexing: indexer daemon when --mode=indexer; otherwise local
|
|
1504
|
+
// fallback on this stdio process only if indexer.lock is free.
|
|
1528
1505
|
}
|
|
1529
1506
|
|
|
1530
|
-
|
|
1507
|
+
if (shouldConnectMcpStdio(INDEXER_MODE)) {
|
|
1508
|
+
main().catch(console.error);
|
|
1509
|
+
}
|
package/indexer.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./lib/validators.js";
|
|
17
17
|
import { safeSqlite3Json, safeOsascript, safeFind } from "./lib/shell.js";
|
|
18
18
|
import { indexUnavailableMessage } from "./lib/indexGate.js";
|
|
19
|
+
import { createLanceTableCache, LANCE_CONNECT_OPTIONS } from "./lib/lancedbTables.js";
|
|
19
20
|
|
|
20
21
|
// Re-export contact functions for use by other modules
|
|
21
22
|
export {
|
|
@@ -70,8 +71,17 @@ const MAC_ABSOLUTE_EPOCH = 978307200;
|
|
|
70
71
|
export const CALENDAR_TMP_PREFIX = "apple-tools-cal-";
|
|
71
72
|
|
|
72
73
|
let embeddingPipeline = null;
|
|
74
|
+
|
|
75
|
+
// One connection per process. Do not early-return a first connect that saw an
|
|
76
|
+
// empty catalog — MCP stdio must pick up tables the indexer daemon already wrote.
|
|
77
|
+
const lanceCache = createLanceTableCache({
|
|
78
|
+
connect: (uri, options) => lancedb.connect(uri, options),
|
|
79
|
+
indexDir: INDEX_DIR,
|
|
80
|
+
mkdirSync: (p, o) => fs.mkdirSync(p, o),
|
|
81
|
+
connectOptions: LANCE_CONNECT_OPTIONS
|
|
82
|
+
});
|
|
83
|
+
const tables = lanceCache.tables;
|
|
73
84
|
let db = null;
|
|
74
|
-
let tables = {};
|
|
75
85
|
|
|
76
86
|
async function getEmbedder() {
|
|
77
87
|
if (!embeddingPipeline) {
|
|
@@ -751,23 +761,19 @@ function getCalendarEvents() {
|
|
|
751
761
|
// ============ DATABASE FUNCTIONS ============
|
|
752
762
|
|
|
753
763
|
export async function initDB() {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
764
|
+
const result = await lanceCache.initDB();
|
|
765
|
+
db = result.db;
|
|
766
|
+
return { db, tables };
|
|
767
|
+
}
|
|
758
768
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (tableNames.includes("messages")) {
|
|
764
|
-
tables.messages = await db.openTable("messages");
|
|
765
|
-
}
|
|
766
|
-
if (tableNames.includes("calendar")) {
|
|
767
|
-
tables.calendar = await db.openTable("calendar");
|
|
768
|
-
}
|
|
769
|
+
export async function getOpenTable(type) {
|
|
770
|
+
await initDB();
|
|
771
|
+
return tables[type] || null;
|
|
772
|
+
}
|
|
769
773
|
|
|
770
|
-
|
|
774
|
+
function resetLanceConnection() {
|
|
775
|
+
lanceCache.reset();
|
|
776
|
+
db = null;
|
|
771
777
|
}
|
|
772
778
|
|
|
773
779
|
export async function clearEmailsTable() {
|
|
@@ -828,10 +834,9 @@ export async function rebuildIndex(sources = ["emails", "messages", "calendar"],
|
|
|
828
834
|
}
|
|
829
835
|
}
|
|
830
836
|
|
|
831
|
-
// Reset
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
tables = {};
|
|
837
|
+
// Reset connection cache after dropping tables so initDB() re-opens
|
|
838
|
+
// (or recreates) them instead of holding stale/empty handles.
|
|
839
|
+
resetLanceConnection();
|
|
835
840
|
|
|
836
841
|
// Re-index requested sources
|
|
837
842
|
for (const source of sources) {
|
|
@@ -1451,7 +1456,7 @@ export async function indexAll(progressCallback = null) {
|
|
|
1451
1456
|
|
|
1452
1457
|
export async function isIndexReady(type = "emails") {
|
|
1453
1458
|
await initDB();
|
|
1454
|
-
return tables[type]
|
|
1459
|
+
return tables[type] != null;
|
|
1455
1460
|
}
|
|
1456
1461
|
|
|
1457
1462
|
// ============ DIRECT QUERIES (for recent items) ============
|
package/lib/audit.js
CHANGED
|
@@ -15,6 +15,7 @@ import fs from "fs";
|
|
|
15
15
|
import path from "path";
|
|
16
16
|
import { safeSqlite3Json, safeFind } from "./shell.js";
|
|
17
17
|
import * as lancedb from "@lancedb/lancedb";
|
|
18
|
+
import { LANCE_CONNECT_OPTIONS, openMissingIndexTables } from "./lancedbTables.js";
|
|
18
19
|
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// CONSTANTS AND PATHS
|
|
@@ -55,22 +56,29 @@ let tables = {};
|
|
|
55
56
|
// ============================================================================
|
|
56
57
|
|
|
57
58
|
async function initDB() {
|
|
58
|
-
if (db)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (tableNames.includes("emails")) {
|
|
65
|
-
tables.emails = await db.openTable("emails");
|
|
66
|
-
}
|
|
67
|
-
if (tableNames.includes("messages")) {
|
|
68
|
-
tables.messages = await db.openTable("messages");
|
|
69
|
-
}
|
|
70
|
-
if (tableNames.includes("calendar")) {
|
|
71
|
-
tables.calendar = await db.openTable("calendar");
|
|
59
|
+
if (!db) {
|
|
60
|
+
try {
|
|
61
|
+
db = await lancedb.connect(INDEX_DIR, LANCE_CONNECT_OPTIONS);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("Error initializing database:", e.message);
|
|
64
|
+
return { db: null, tables: {} };
|
|
72
65
|
}
|
|
66
|
+
}
|
|
73
67
|
|
|
68
|
+
try {
|
|
69
|
+
const result = await openMissingIndexTables({
|
|
70
|
+
db,
|
|
71
|
+
tables,
|
|
72
|
+
indexDir: INDEX_DIR,
|
|
73
|
+
reconnect: async () => {
|
|
74
|
+
try {
|
|
75
|
+
db?.close?.();
|
|
76
|
+
} catch {}
|
|
77
|
+
db = await lancedb.connect(INDEX_DIR, LANCE_CONNECT_OPTIONS);
|
|
78
|
+
return db;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
db = result.db;
|
|
74
82
|
return { db, tables };
|
|
75
83
|
} catch (e) {
|
|
76
84
|
console.error("Error initializing database:", e.message);
|