apple-tools-mcp 1.1.3 → 1.2.0
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 +134 -21
- package/index.js +119 -110
- package/lib/config.js +312 -0
- package/lib/indexerLock.js +376 -0
- package/lib/indexerRuntime.js +142 -0
- package/lib/processMode.js +28 -0
- package/package.json +4 -4
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,13 +32,15 @@ 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",
|
|
40
39
|
"args": ["/absolute/path/to/Apple-Tools-MCP/index.js"]
|
|
41
40
|
```
|
|
42
41
|
|
|
42
|
+
**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.
|
|
43
|
+
|
|
43
44
|
### 2. Grant Full Disk Access
|
|
44
45
|
|
|
45
46
|
The MCP server needs access to read your Mail, Messages, and Calendar databases.
|
|
@@ -64,11 +65,18 @@ The MCP server needs access to read your Mail, Messages, and Calendar databases.
|
|
|
64
65
|
|
|
65
66
|
7. Ensure the toggle for Node.js is enabled
|
|
66
67
|
|
|
67
|
-
### 3. Configure
|
|
68
|
+
### 3. Configure your MCP client
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
**Command**
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
- `npx` with args `["-y", "apple-tools-mcp"]` (npm install)
|
|
75
|
+
- or `node` with args `["/absolute/path/to/Apple-Tools-MCP/index.js"]` (from source)
|
|
70
76
|
|
|
71
|
-
**
|
|
77
|
+
**Example: Claude Desktop**
|
|
78
|
+
|
|
79
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
72
80
|
|
|
73
81
|
```json
|
|
74
82
|
{
|
|
@@ -81,15 +89,24 @@ Add to your Claude Desktop config file:
|
|
|
81
89
|
}
|
|
82
90
|
```
|
|
83
91
|
|
|
84
|
-
|
|
92
|
+
Other clients use their own settings UI or config file. Use the same `command` and `args`; only the file path or UI differs.
|
|
93
|
+
|
|
94
|
+
MCP clients are **short-lived stdio** processes: they exit when the client closes stdin. Always-on indexing belongs on the **indexer daemon**, not a sleep-pipe wrapper around this binary.
|
|
95
|
+
|
|
96
|
+
### 4. Restart your MCP client
|
|
85
97
|
|
|
86
|
-
Quit and reopen Claude Desktop
|
|
98
|
+
Quit and reopen the client so it loads the server. For Claude Desktop, fully quit (Cmd+Q) and reopen.
|
|
87
99
|
|
|
88
100
|
## Building the Index
|
|
89
101
|
|
|
90
|
-
On first use,
|
|
102
|
+
On first use, a vector index of your emails, messages, and calendar events is built automatically. Email history is unlimited by default. This may take a while depending on the volume of data.
|
|
91
103
|
|
|
92
|
-
|
|
104
|
+
**Who indexes**
|
|
105
|
+
|
|
106
|
+
- **Indexer daemon running** (recommended on Mac Mini): the daemon owns `~/.apple-tools-mcp/indexer.lock` and refreshes `~/.apple-tools-mcp/vector-index/`. MCP stdio clients only search; they do not start background refresh.
|
|
107
|
+
- **No daemon** (default MacBook / Claude Desktop / Cursor): the MCP stdio process indexes **locally on startup**, same as previous versions, then exits when the client disconnects.
|
|
108
|
+
|
|
109
|
+
You can manually rebuild the index (stop the indexer daemon first if it is running, so it is not writing at the same time):
|
|
93
110
|
|
|
94
111
|
```bash
|
|
95
112
|
# Index all email history (default)
|
|
@@ -101,9 +118,99 @@ APPLE_TOOLS_INDEX_DAYS_BACK=30 npm run build-index
|
|
|
101
118
|
|
|
102
119
|
The index is stored in `~/.apple-tools-mcp/vector-index/`.
|
|
103
120
|
|
|
121
|
+
## Index refresh interval
|
|
122
|
+
|
|
123
|
+
Resolved **once at process start**. Precedence (highest wins):
|
|
124
|
+
|
|
125
|
+
1. `INDEX_INTERVAL_MS` environment variable (milliseconds or human form: `30s`, `1m`, `5m`, `1h`)
|
|
126
|
+
2. `~/.apple-tools-mcp/config.json` keys `indexInterval` or `indexIntervalMs`
|
|
127
|
+
3. Product default: **5 minutes** (`300000` ms) — typical MacBook / MCP local-fallback
|
|
128
|
+
|
|
129
|
+
Values are **clamped** to **15 seconds** minimum and **6 hours** maximum. Invalid JSON, unknown keys, and unparseable intervals are logged and ignored (the process does not crash). The effective interval is logged at start, for example:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
Effective index refresh interval: 1m (60000 ms) [source=config]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A warn line is also logged when clamping occurs.
|
|
136
|
+
|
|
137
|
+
### Example `~/.apple-tools-mcp/config.json` (Mac Mini)
|
|
138
|
+
|
|
139
|
+
Recommended Mini always-on interval is **1 minute**. 30 seconds is allowed (at or above the 15s floor).
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"indexInterval": "1m"
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Equivalent: `"indexIntervalMs": 60000`, or `INDEX_INTERVAL_MS=60000` (env overrides the file).
|
|
148
|
+
|
|
149
|
+
Missing `config.json` is fine — env then the 5-minute default apply.
|
|
150
|
+
|
|
151
|
+
## Always-on indexer (Mac Mini LaunchAgent)
|
|
152
|
+
|
|
153
|
+
On Mini, run the **indexer daemon**, not a sleep-pipe wrapper around `apple-tools-mcp`. Grok Bot, Claude Desktop, and other clients still attach via short-lived stdio MCP (`npx -y apple-tools-mcp` or the global `apple-tools-mcp` bin).
|
|
154
|
+
|
|
155
|
+
**Entrypoint:** `node index.js --mode=indexer`
|
|
156
|
+
**Convenience bin:** `apple-tools-indexer` (same file; npm global install provides it)
|
|
157
|
+
**npm script (clone only):** `npm run indexer`
|
|
158
|
+
**One-shot rebuild:** `npm run build-index` (stop the indexer daemon first)
|
|
159
|
+
|
|
160
|
+
LaunchAgent should invoke **node + `--mode=indexer`** on the **global** package (Mini has no git clone). LaunchAgent does not inherit your shell `PATH`, so use absolute paths from `which node` and `npm root -g`.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
which node
|
|
164
|
+
# Apple Silicon Homebrew example: /opt/homebrew/bin/node
|
|
165
|
+
# Intel Homebrew / usr/local example: /usr/local/bin/node
|
|
166
|
+
|
|
167
|
+
npm root -g
|
|
168
|
+
# Example: /opt/homebrew/lib/node_modules
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Example `~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist`:
|
|
172
|
+
|
|
173
|
+
```xml
|
|
174
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
175
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
176
|
+
<plist version="1.0">
|
|
177
|
+
<dict>
|
|
178
|
+
<key>Label</key>
|
|
179
|
+
<string>com.apple-tools-mcp.indexer</string>
|
|
180
|
+
<key>ProgramArguments</key>
|
|
181
|
+
<array>
|
|
182
|
+
<string>/opt/homebrew/bin/node</string>
|
|
183
|
+
<string>/opt/homebrew/lib/node_modules/apple-tools-mcp/index.js</string>
|
|
184
|
+
<string>--mode=indexer</string>
|
|
185
|
+
</array>
|
|
186
|
+
<key>RunAtLoad</key>
|
|
187
|
+
<true/>
|
|
188
|
+
<key>KeepAlive</key>
|
|
189
|
+
<true/>
|
|
190
|
+
<key>EnvironmentVariables</key>
|
|
191
|
+
<dict>
|
|
192
|
+
<key>PATH</key>
|
|
193
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
194
|
+
</dict>
|
|
195
|
+
<key>StandardOutPath</key>
|
|
196
|
+
<string>/tmp/apple-tools-indexer.out.log</string>
|
|
197
|
+
<key>StandardErrorPath</key>
|
|
198
|
+
<string>/tmp/apple-tools-indexer.err.log</string>
|
|
199
|
+
</dict>
|
|
200
|
+
</plist>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Replace the node and `node_modules` paths with the values from `which node` and `npm root -g`. Load it with:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
launchctl load ~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
KeepAlive belongs on this indexer job only — not on the MCP stdio process.
|
|
210
|
+
|
|
104
211
|
## Available Tools
|
|
105
212
|
|
|
106
|
-
Once configured,
|
|
213
|
+
Once configured, your MCP client can use these tools:
|
|
107
214
|
|
|
108
215
|
### Universal Search
|
|
109
216
|
|
|
@@ -159,7 +266,7 @@ Once configured, Claude can use these tools:
|
|
|
159
266
|
|
|
160
267
|
## Example Queries
|
|
161
268
|
|
|
162
|
-
Ask
|
|
269
|
+
Ask your MCP client things like:
|
|
163
270
|
|
|
164
271
|
- "Find emails from John about the quarterly report"
|
|
165
272
|
- "What messages did I get from Mom last week?"
|
|
@@ -187,28 +294,31 @@ Ensure Node.js has Full Disk Access (see Installation step 2).
|
|
|
187
294
|
1. Check that the index was built: `ls ~/.apple-tools-mcp/vector-index/`
|
|
188
295
|
2. Rebuild the index if needed: `npm run build-index`
|
|
189
296
|
|
|
190
|
-
### Server not appearing in
|
|
297
|
+
### Server not appearing in the MCP client
|
|
191
298
|
|
|
192
|
-
1. Verify your config
|
|
193
|
-
2. Restart
|
|
194
|
-
3. Check
|
|
299
|
+
1. Verify your client config is valid (JSON files must be valid JSON)
|
|
300
|
+
2. Restart the MCP client completely (for Claude Desktop: Cmd+Q, then reopen)
|
|
301
|
+
3. Check the client's MCP logs for errors
|
|
195
302
|
|
|
196
303
|
### Force rebuild the index
|
|
197
304
|
|
|
198
305
|
If the index becomes corrupted or out of sync:
|
|
199
306
|
|
|
200
307
|
```bash
|
|
308
|
+
# If the Mini indexer LaunchAgent is running, unload it first
|
|
309
|
+
# launchctl unload ~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist
|
|
310
|
+
|
|
201
311
|
# Remove existing index files
|
|
202
312
|
rm -rf ~/.apple-tools-mcp/vector-index
|
|
203
313
|
rm -f ~/.apple-tools-mcp/index-meta.json
|
|
204
314
|
rm -f ~/.apple-tools-mcp/indexer.lock
|
|
205
315
|
|
|
206
|
-
# Restart
|
|
316
|
+
# Restart the indexer daemon or your MCP client to trigger a fresh rebuild
|
|
207
317
|
```
|
|
208
318
|
|
|
209
319
|
### Monitor indexing progress
|
|
210
320
|
|
|
211
|
-
Watch the MCP server logs in
|
|
321
|
+
Watch the MCP server logs in your client. Log locations vary by client; Claude Desktop example:
|
|
212
322
|
|
|
213
323
|
```bash
|
|
214
324
|
tail -f ~/Library/Logs/Claude/mcp-server-apple-tools.log
|
|
@@ -242,12 +352,15 @@ npm install -D vitest @vitest/coverage-v8 fast-check
|
|
|
242
352
|
# Run tests
|
|
243
353
|
npm test
|
|
244
354
|
|
|
245
|
-
# Run
|
|
246
|
-
|
|
355
|
+
# Run the indexer daemon (owns indexer.lock + vector-index refresh)
|
|
356
|
+
npm run indexer
|
|
247
357
|
|
|
248
|
-
#
|
|
358
|
+
# One-shot rebuild (stop the indexer daemon first)
|
|
249
359
|
npm run build-index
|
|
250
360
|
|
|
361
|
+
# Run tests with verbose coverage report
|
|
362
|
+
npx vitest run --coverage --reporter=verbose
|
|
363
|
+
|
|
251
364
|
# Run audit to check index health
|
|
252
365
|
npm run audit
|
|
253
366
|
```
|
package/index.js
CHANGED
|
@@ -10,142 +10,110 @@ 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 { isSearchBlockedByIndexing, cycleEndFlags, indexUnavailableMessage } 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() {
|
|
@@ -223,13 +190,15 @@ function getIndexingMessage() {
|
|
|
223
190
|
|
|
224
191
|
// Run a single indexing cycle (called by background timer)
|
|
225
192
|
function runIndexCycle() {
|
|
226
|
-
|
|
227
|
-
|
|
193
|
+
const cycle = beginIndexCycle(indexingInProgress);
|
|
194
|
+
if (!cycle.started) {
|
|
228
195
|
return;
|
|
229
196
|
}
|
|
197
|
+
indexingInProgress = cycle.indexingInProgress;
|
|
230
198
|
|
|
231
199
|
// Safety net: check lock before indexing
|
|
232
200
|
if (!acquireLock()) {
|
|
201
|
+
indexingInProgress = false;
|
|
233
202
|
console.error("Another instance is indexing. Skipping.");
|
|
234
203
|
return;
|
|
235
204
|
}
|
|
@@ -299,6 +268,11 @@ function triggerIndexIfNeeded() {
|
|
|
299
268
|
|
|
300
269
|
// Start continuous background indexing
|
|
301
270
|
function startBackgroundIndexing() {
|
|
271
|
+
if (!loggedIndexInterval) {
|
|
272
|
+
logResolvedInterval(resolvedIndexInterval);
|
|
273
|
+
loggedIndexInterval = true;
|
|
274
|
+
}
|
|
275
|
+
|
|
302
276
|
// Run indexing immediately on startup
|
|
303
277
|
runIndexCycle();
|
|
304
278
|
|
|
@@ -307,7 +281,7 @@ function startBackgroundIndexing() {
|
|
|
307
281
|
runIndexCycle();
|
|
308
282
|
}, INDEX_INTERVAL);
|
|
309
283
|
|
|
310
|
-
console.error(`Background indexing started (interval: ${
|
|
284
|
+
console.error(`Background indexing started (interval: ${resolvedIndexInterval.human} / ${INDEX_INTERVAL} ms)`);
|
|
311
285
|
}
|
|
312
286
|
|
|
313
287
|
// Stop background indexing and clean up timers
|
|
@@ -323,17 +297,22 @@ function stopBackgroundIndexing() {
|
|
|
323
297
|
console.error("Background indexing stopped");
|
|
324
298
|
}
|
|
325
299
|
|
|
326
|
-
// Unblock searches
|
|
327
|
-
//
|
|
300
|
+
// Unblock searches after a cycle ends. Must run on failure as well as success
|
|
301
|
+
// so tools are not stuck forever. The indexer daemon keeps indexer.lock for
|
|
302
|
+
// the process lifetime; MCP local-fallback still releases between cycles.
|
|
328
303
|
function applyCycleEnd(success) {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
304
|
+
const result = applyIndexerCycleEnd({
|
|
305
|
+
success,
|
|
306
|
+
indexerMode: INDEXER_MODE,
|
|
307
|
+
cycleEndFlags,
|
|
308
|
+
releaseLock
|
|
309
|
+
});
|
|
310
|
+
indexingInProgress = result.indexingInProgress;
|
|
311
|
+
sessionIndexComplete = result.sessionIndexComplete;
|
|
312
|
+
ownsIndexLock = result.ownsIndexLock;
|
|
313
|
+
if (result.isFirstEverRun === false) {
|
|
334
314
|
isFirstEverRun = false;
|
|
335
315
|
}
|
|
336
|
-
releaseLock();
|
|
337
316
|
}
|
|
338
317
|
|
|
339
318
|
// Index-backed tools wait only while THIS process owns the lock and has not
|
|
@@ -345,13 +324,35 @@ function stillIndexingMessage() {
|
|
|
345
324
|
return null;
|
|
346
325
|
}
|
|
347
326
|
|
|
327
|
+
function waitForLockAndStartDaemon() {
|
|
328
|
+
waitForIndexerLock(acquireLock, {
|
|
329
|
+
retryMs: LOCK_RETRY_MS,
|
|
330
|
+
onAcquired: () => {
|
|
331
|
+
beginOwnedIndexing({
|
|
332
|
+
startHeartbeat: startLockHeartbeat,
|
|
333
|
+
startBackground: startBackgroundIndexing
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
348
339
|
// Initialize and start indexing
|
|
349
340
|
async function initializeIndexing() {
|
|
350
341
|
isFirstEverRun = await checkIfFirstRun();
|
|
351
342
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
343
|
+
if (INDEXER_MODE) {
|
|
344
|
+
console.error(`Apple Tools MCP indexer running (v${PACKAGE_VERSION})`);
|
|
345
|
+
logResolvedInterval(resolvedIndexInterval);
|
|
346
|
+
loggedIndexInterval = true;
|
|
347
|
+
waitForLockAndStartDaemon();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// MCP stdio: if the indexer daemon (or another instance) holds the lock,
|
|
352
|
+
// skip background refresh and use the shared index. If nothing holds the
|
|
353
|
+
// lock, index locally as before so the stdio happy path still works.
|
|
354
|
+
const startup = mcpIndexingStartup(() => acquireLock());
|
|
355
|
+
if (!startup.startBackground) {
|
|
355
356
|
console.error("Another apple-tools-mcp instance is indexing. Server will run without background indexing.");
|
|
356
357
|
// Lost lock is not "still indexing": this process will never complete a
|
|
357
358
|
// local cycle. Searches proceed whenever isIndexReady() is true.
|
|
@@ -359,8 +360,13 @@ async function initializeIndexing() {
|
|
|
359
360
|
return;
|
|
360
361
|
}
|
|
361
362
|
|
|
362
|
-
// Start background indexing
|
|
363
|
-
|
|
363
|
+
// Start background indexing (local fallback when no daemon is running)
|
|
364
|
+
// using the same heartbeat path as the daemon so a long first-index cycle
|
|
365
|
+
// cannot look like a stale lock to another waiter.
|
|
366
|
+
beginOwnedIndexing({
|
|
367
|
+
startHeartbeat: startLockHeartbeat,
|
|
368
|
+
startBackground: startBackgroundIndexing
|
|
369
|
+
});
|
|
364
370
|
}
|
|
365
371
|
|
|
366
372
|
// Start indexing immediately on server startup
|
|
@@ -1524,7 +1530,10 @@ async function main() {
|
|
|
1524
1530
|
const transport = new StdioServerTransport();
|
|
1525
1531
|
await server.connect(transport);
|
|
1526
1532
|
console.error(`Apple Tools MCP server running (v${PACKAGE_VERSION})`);
|
|
1527
|
-
// Background indexing
|
|
1533
|
+
// Background indexing: indexer daemon when --mode=indexer; otherwise local
|
|
1534
|
+
// fallback on this stdio process only if indexer.lock is free.
|
|
1528
1535
|
}
|
|
1529
1536
|
|
|
1530
|
-
|
|
1537
|
+
if (shouldConnectMcpStdio(INDEXER_MODE)) {
|
|
1538
|
+
main().catch(console.error);
|
|
1539
|
+
}
|