local-mcp 3.0.340 → 3.0.341

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.
Files changed (3) hide show
  1. package/download.js +9 -3
  2. package/index.js +40 -14
  3. package/package.json +1 -1
package/download.js CHANGED
@@ -325,9 +325,15 @@ async function ensureTray() {
325
325
  return _ensureTrayWindows()
326
326
  }
327
327
 
328
- // Respect explicit uninstall — don't resurrect tray if user ran `uninstall`
329
- const uninstalledMarker = path.join(CACHE_DIR, '..', '.uninstalled')
330
- if (fs.existsSync(uninstalledMarker)) return null
328
+ // Respect explicit uninstall — don't resurrect tray if user ran `uninstall`.
329
+ // Checks BOTH the runtime-dir copy and the durable App Support copy: a user who
330
+ // "cleaned up" with `rm -rf ~/.local/share/local-mcp` removes the runtime-dir copy,
331
+ // and only the durable one survives to stop a resurrection loop (FB-D1FA79).
332
+ const uninstalledMarkers = [
333
+ path.join(CACHE_DIR, '..', '.uninstalled'),
334
+ path.join(os.homedir(), 'Library', 'Application Support', 'Local MCP', '.uninstalled'),
335
+ ]
336
+ if (uninstalledMarkers.some((p) => { try { return fs.existsSync(p) } catch { return false } })) return null
331
337
 
332
338
  const info = await getLatestBinary()
333
339
  const version = info.version
package/index.js CHANGED
@@ -18,6 +18,38 @@ const os = require('os')
18
18
  const fs = require('fs')
19
19
  const https = require('https')
20
20
 
21
+ // ── Uninstall sentinels ───────────────────────────────────────────────────────
22
+ // A `.uninstalled` sentinel marks a machine the user removed, so no respawn (an
23
+ // AI client that still has `local-mcp` in its config re-spawning `npx local-mcp`)
24
+ // resurrects it. We keep TWO copies and treat EITHER as "uninstalled":
25
+ // 1. ~/.local/share/local-mcp/.uninstalled (runtime dir — legacy)
26
+ // 2. ~/Library/Application Support/Local MCP/.uninstalled (durable)
27
+ // #2 exists because all three guards (the two below + ensureTray in download.js)
28
+ // used to read ONLY the runtime-dir copy — so a user "cleaning up" with
29
+ // `rm -rf ~/.local/share/local-mcp` blinded every guard at once, and a surviving
30
+ // project-scoped `.mcp.json` (which no uninstaller can reach) kept reinstalling the
31
+ // machine (FB-D1FA79). The App Support copy survives that cleanup. A reinstall
32
+ // clears BOTH; an uninstall writes BOTH.
33
+ function uninstallSentinelPaths() {
34
+ const { CACHE_DIR } = require('./download')
35
+ const list = [path.join(path.dirname(CACHE_DIR), '.uninstalled')]
36
+ if (process.platform === 'darwin') {
37
+ list.push(path.join(os.homedir(), 'Library', 'Application Support', 'Local MCP', '.uninstalled'))
38
+ }
39
+ return list
40
+ }
41
+ function anyUninstallSentinel() {
42
+ return uninstallSentinelPaths().some((p) => { try { return fs.existsSync(p) } catch { return false } })
43
+ }
44
+ function writeUninstallSentinels() {
45
+ for (const p of uninstallSentinelPaths()) {
46
+ try { fs.mkdirSync(path.dirname(p), { recursive: true }) } catch {}
47
+ try { fs.writeFileSync(p, String(Date.now())) } catch {}
48
+ }
49
+ }
50
+ function clearUninstallSentinels() {
51
+ for (const p of uninstallSentinelPaths()) { try { fs.unlinkSync(p) } catch {} }
52
+ }
21
53
 
22
54
  // Platform support: macOS only (Windows/Linux on waitlist at local-mcp.com)
23
55
  const SUPPORTED_PLATFORMS = ['darwin']
@@ -67,8 +99,7 @@ function detectManualUninstall() {
67
99
  async function main() {
68
100
  if (cmd === 'setup') {
69
101
  // Clear uninstall sentinel — user explicitly wants to (re-)install
70
- const { CACHE_DIR } = require('./download')
71
- try { fs.unlinkSync(path.join(path.dirname(CACHE_DIR), '.uninstalled')) } catch {}
102
+ clearUninstallSentinels()
72
103
  const { runSetup } = require('./setup')
73
104
  await runSetup()
74
105
  process.exit(0)
@@ -112,10 +143,9 @@ async function main() {
112
143
  }
113
144
 
114
145
  // Write sentinel BEFORE deleting cache dir, so it survives cache wipes.
115
- // ensureTray() checks this and skips re-installation.
116
- const sentinelDir = path.dirname(CACHE_DIR)
117
- try { fs.mkdirSync(sentinelDir, { recursive: true }) } catch {}
118
- try { fs.writeFileSync(path.join(sentinelDir, '.uninstalled'), String(Date.now())) } catch {}
146
+ // ensureTray() checks this and skips re-installation. Writes BOTH the runtime-dir
147
+ // copy and the durable App Support copy (see uninstallSentinelPaths).
148
+ writeUninstallSentinels()
119
149
 
120
150
  if (fs.existsSync(CACHE_DIR)) {
121
151
  fs.rmSync(CACHE_DIR, { recursive: true, force: true })
@@ -153,11 +183,10 @@ async function main() {
153
183
  // measured 2026-06-12). ensureTray() already honored the sentinel, but the stdio
154
184
  // server path never did. Refuse to start so a respawn can't resurrect the install.
155
185
  // Legit reinstalls clear the sentinel first: `npx local-mcp setup` (above) and
156
- // install.sh (curl/DMG, which rm's it before copying the bundle).
186
+ // install.sh (curl/DMG, which rm's it before copying the bundle). Checks BOTH the
187
+ // runtime-dir and durable App Support copies (see uninstallSentinelPaths).
157
188
  {
158
- const { CACHE_DIR: _cacheDir } = require('./download')
159
- const sentinel = path.join(path.dirname(_cacheDir), '.uninstalled')
160
- if (fs.existsSync(sentinel)) {
189
+ if (anyUninstallSentinel()) {
161
190
  process.stderr.write(
162
191
  '[LMCP] Local MCP was uninstalled — not starting.\n' +
163
192
  ' Reinstall: npx local-mcp setup\n' +
@@ -176,11 +205,8 @@ async function main() {
176
205
  // (FB-A30CDC). The AI client's stale MCP config entry is left alone; it just respawns us
177
206
  // and we short-circuit here, which is harmless.
178
207
  {
179
- const { CACHE_DIR: _cacheDir2 } = require('./download')
180
208
  if (detectManualUninstall()) {
181
- const sentinelDir = path.dirname(_cacheDir2)
182
- try { fs.mkdirSync(sentinelDir, { recursive: true }) } catch {}
183
- try { fs.writeFileSync(path.join(sentinelDir, '.uninstalled'), String(Date.now())) } catch {}
209
+ writeUninstallSentinels()
184
210
  const trayPlist = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.local-mcp.tray.plist')
185
211
  if (fs.existsSync(trayPlist)) {
186
212
  try { execSync(`launchctl unload -w "${trayPlist}" 2>/dev/null`, { stdio: 'pipe' }) } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.340",
3
+ "version": "3.0.341",
4
4
  "description": "Let ChatGPT, Claude, Cursor & any MCP client actually use your Mac — read & reply to email, manage your calendar, text over iMessage, find files, work with Teams, Slack & Office. On your Mac, no API keys, free.",
5
5
  "main": "index.js",
6
6
  "bin": {