local-mcp 3.0.413 → 3.0.416

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,14 +1,14 @@
1
1
  # Local MCP
2
2
 
3
- > **The only Mac MCP you can use from ChatGPT & Claude.ai on the web** — plus 188+ local tools for Claude Desktop, Cursor, Windsurf, VS Code & Zed. Connect any AI to Mail, Calendar, Contacts, iMessage, Teams, Slack, WhatsApp, Signal, OneDrive, Google Drive, Microsoft 365, Notes, Reminders, OmniFocus, Word/Excel/PowerPoint and more. 100% local, no API keys, free — your data never leaves your machine.
3
+ > **Use your Mac from ChatGPT & Claude.ai on the web** — plus 192+ local tools for Claude Desktop, Cursor, Windsurf, VS Code & Zed. Connect any AI to Mail, Calendar, Contacts, iMessage, Teams, Slack, WhatsApp, Signal, OneDrive, Google Drive, Microsoft 365, Notes, Reminders, OmniFocus, Word/Excel/PowerPoint and more. Runs on your computer, no API keys, free — local tools read your data on your machine, and your AI provider receives the content needed for each request.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/local-mcp?style=flat-square&label=npm)](https://www.npmjs.com/package/local-mcp)
6
6
  [![macOS](https://img.shields.io/badge/macOS-13%2B-111111?style=flat-square)](https://local-mcp.com?ref=npm)
7
7
  [![Signed](https://img.shields.io/badge/Developer%20ID-notarized-111111?style=flat-square)](https://local-mcp.com?ref=npm)
8
- [![Privacy](https://img.shields.io/badge/data-stays%20on%20device-2e7d32?style=flat-square)](https://local-mcp.com/en/privacy)
8
+ [![Privacy](https://img.shields.io/badge/tools-run%20locally-2e7d32?style=flat-square)](https://local-mcp.com/en/privacy)
9
9
  [![Glama](https://glama.ai/mcp/servers/lanchuske/local-mcp-releases/badge)](https://glama.ai/mcp/servers/lanchuske/local-mcp-releases?ref=npm)
10
10
 
11
- **No cloud. No tokens. No API keys. 100% free.**
11
+ **Runs on your computer. No tokens. No API keys. Free.**
12
12
 
13
13
   
14
14
 
@@ -71,6 +71,7 @@ LMCP runs a native MCP server that bridges your Mac apps to any AI client:
71
71
  | **NordVPN** | Status, server recommendations, diagnostics |
72
72
  | **Image Playground** | Generate images on-device from a text prompt — Apple Intelligence, no cloud, no API key (requires macOS 15.4+) |
73
73
  | **Skills / Recipes** | Save a working multi-tool flow as a reusable skill, rerun it, **export to share with others & import theirs**, see run history, manage in Settings → Skills |
74
+ | **Shortcuts** | List, run (preview → confirm), and inspect the user's own macOS Shortcuts — HomeKit, Focus modes, and any personal automation |
74
75
 
75
76
   
76
77
 
@@ -134,6 +135,8 @@ Agent mesh (9): `agent_mesh_status` `agent_mesh_create` `agent_mesh_invite` `age
134
135
 
135
136
  System (12): `lmcp_state` `get_config` `get_datetime` `daily_brief` `run_diagnostics` `get_audit_log` `lmcp_install_upgrade` `lmcp_upgrade_diagnostics` `report_problem` `request_feature` `report_friction` `run_terminal_command`
136
137
 
138
+ Shortcuts (3): `shortcuts_list` `shortcuts_run` `shortcuts_view`
139
+
137
140
  _Coming soon (not included in the 162 count above): **Evernote** (support is being rebuilt)._
138
141
 
139
142
   
package/download.js CHANGED
@@ -198,8 +198,8 @@ function _getMachineId(deps = {}) {
198
198
  return crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16)
199
199
  }
200
200
 
201
- function getArch() {
202
- const plat = process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : 'darwin'
201
+ function getArch(platform = process.platform) {
202
+ const plat = platform === 'win32' ? 'windows' : platform === 'linux' ? 'linux' : 'darwin'
203
203
  const arch = process.arch
204
204
  if (arch === 'arm64') return `${plat}-arm64`
205
205
  if (arch === 'x64') return `${plat}-amd64`
@@ -330,15 +330,22 @@ async function downloadFile(url, destPath) {
330
330
  * Asegura que el binario esté descargado y listo.
331
331
  * @returns {Promise<{binPath: string}>}
332
332
  */
333
- async function ensureBinary() {
334
- const arch = getArch()
335
- const info = await getLatestBinary()
333
+ async function ensureBinary(deps = {}) {
334
+ // Seam for tests (same idiom as ensureTray's `deps`, #2654): lets a test pin the
335
+ // platform and stub the network without touching process.platform or real HOME.
336
+ const {
337
+ platform = process.platform,
338
+ getLatest = getLatestBinary,
339
+ download = downloadFile,
340
+ } = deps
341
+ const arch = getArch(platform)
342
+ const info = await getLatest()
336
343
  const version = info.version
337
344
 
338
345
  // On non-darwin, use the Go server binary (cross-platform).
339
346
  // The Go binary is a single file, not a nested tarball.
340
- if (process.platform !== 'darwin') {
341
- const binName = process.platform === 'win32' ? 'lmcp-server.exe' : 'lmcp-server'
347
+ if (platform !== 'darwin') {
348
+ const binName = platform === 'win32' ? 'lmcp-server.exe' : 'lmcp-server'
342
349
  const binPath = path.join(CACHE_DIR, binName)
343
350
  const versionFile = path.join(CACHE_DIR, '.go-server-version')
344
351
 
@@ -354,6 +361,17 @@ async function ensureBinary() {
354
361
  const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
355
362
  const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
356
363
 
364
+ // Linux has no published artifact today — unlike Windows, where windows_amd64
365
+ // IS published — so composing the fallback URL below only ever 404s: 817
366
+ // requests/week of dead traffic from MCP sandboxes testing the npm package
367
+ // (#3794). Fail before any network call instead of catching the 404 after.
368
+ if (platform === 'linux' && !publishedUrl) {
369
+ throw new Error(
370
+ 'Local MCP does not support Linux yet.\n' +
371
+ ' Check https://local-mcp.com for updates.'
372
+ )
373
+ }
374
+
357
375
  let url, platformVersion
358
376
  if (publishedUrl) {
359
377
  platformVersion = versionFromArtifactUrl(publishedUrl, arch)
@@ -390,7 +408,7 @@ async function ensureBinary() {
390
408
  const tarPath = path.join(CACHE_DIR, `go-server-${platformVersion}.tar.gz`)
391
409
 
392
410
  try {
393
- await downloadFile(url, tarPath)
411
+ await download(url, tarPath)
394
412
  process.stderr.write(` Extracting...\n`)
395
413
  extractTar(tarPath, CACHE_DIR)
396
414
  fs.unlinkSync(tarPath)
@@ -415,7 +433,7 @@ async function ensureBinary() {
415
433
  if (!fs.existsSync(binPath)) {
416
434
  throw new Error(`Binary not found after extraction: ${binPath}`)
417
435
  }
418
- if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
436
+ if (platform !== 'win32') fs.chmodSync(binPath, 0o755)
419
437
  fs.writeFileSync(versionFile, platformVersion)
420
438
 
421
439
  process.stderr.write(` Ready at ${CACHE_DIR}\n`)
@@ -543,17 +561,86 @@ function _isUsableTrayBundle(trayApp) {
543
561
  catch { return false }
544
562
  }
545
563
 
564
+ // Version compare mirroring mac-swift/Sources/LocalMCPServer/Version.swift's isNewer —
565
+ // #3599 criterio 6: un mismo criterio "más nuevo gana" en los dos lados, cada uno probado
566
+ // por separado (no hay código compartido entre Swift y Node). Incluye el mismo desempate
567
+ // que Version.swift: en un empate numérico, un release gana a un pre-release
568
+ // ("3.0.297" > "3.0.297-test").
569
+ function _versionIsNewer(a, b) {
570
+ const parse = (v) => {
571
+ const s = String(v || '')
572
+ const nums = s.split(/[-+]/, 1)[0].split('.').map((n) => parseInt(n, 10) || 0)
573
+ return { nums, isPrerelease: s.includes('-') || s.includes('+') }
574
+ }
575
+ const pa = parse(a), pb = parse(b)
576
+ for (let i = 0; i < Math.max(pa.nums.length, pb.nums.length); i++) {
577
+ const x = pa.nums[i] || 0, y = pb.nums[i] || 0
578
+ if (x !== y) return x > y
579
+ }
580
+ if (pa.isPrerelease !== pb.isPrerelease) return !pa.isPrerelease && pb.isPrerelease
581
+ return false
582
+ }
583
+
584
+ const _CFBUNDLE_VERSION_RE = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/
585
+
586
+ // CFBundleShortVersionString of an installed .app, or null when unreadable. Tries a plain
587
+ // UTF8 text read FIRST — an xml1 plist (what every test fixture here writes, and what some
588
+ // build toolchains leave on disk) needs no external tool and reads on ANY platform, unlike
589
+ // `plutil` (macOS-only, absent on the ubuntu-latest runner these tests also run on, #3599
590
+ // review). Only reaches for `plutil -convert xml1` when the direct read finds no match —
591
+ // the real fallback for a genuinely binary-encoded Info.plist.
592
+ function _bundleVersion(appPath, exec = execFileSync) {
593
+ const infoPath = path.join(appPath, 'Contents', 'Info.plist')
594
+ try {
595
+ const m = _CFBUNDLE_VERSION_RE.exec(fs.readFileSync(infoPath, 'utf8'))
596
+ if (m) return m[1]
597
+ } catch { /* missing, unreadable, or not text — fall through to plutil */ }
598
+ try {
599
+ const xml = exec('/usr/bin/plutil', ['-convert', 'xml1', '-o', '-', infoPath], { stdio: 'pipe' }).toString()
600
+ const m = _CFBUNDLE_VERSION_RE.exec(xml)
601
+ return m ? m[1] : null
602
+ } catch {
603
+ return null
604
+ }
605
+ }
606
+
607
+ // Whether a LocalMCPTray process is running FROM this exact bundle path — matched on the
608
+ // full executable path so a sibling copy elsewhere never counts. Unreadable (pgrep
609
+ // missing/blocked) fails CLOSED to "running": a guard that cannot see must not say "safe
610
+ // to delete" (same rule as index.js's countRunningServers, #1394 §C).
611
+ function _isTrayRunningFrom(appPath, exec = execFileSync) {
612
+ const bin = path.join(appPath, 'Contents', 'MacOS', 'LocalMCPTray')
613
+ try {
614
+ exec('pgrep', ['-f', bin], { stdio: 'pipe' })
615
+ return true // exit 0: matched — a tray IS running from this bundle
616
+ } catch (err) {
617
+ if (err && err.status === 1) return false // ran fine, matched nothing
618
+ return true // could not tell — fail closed
619
+ }
620
+ }
621
+
546
622
  // Retira las copias que `trayApp` supersede — SOLO si `trayApp` ya tiene un bundle usable.
547
623
  // Sin esa guarda, la migración borra la única copia que el Mac tiene antes de que exista su
548
624
  // reemplazo, y una descarga fallida deja la máquina sin cliente (#2654, hermano de #2643).
549
- // Devuelve lo retirado para que los tests puedan afirmar sobre el efecto, no sobre el orden.
550
- function _retireSupersededTrays(trayApp, supersededPaths, rm = _rmrf) {
625
+ // #3599 AC5: tampoco retira una copia cuya versión no se pueda leer, ni una LEÍBLEMENTE más
626
+ // nueva que `trayApp`, ni una que un proceso vivo esté sirviendo — mismo criterio fail-closed
627
+ // que el lado Swift (TrayInstallHealth.shouldRemoveOtherCopy): no haber podido medir no es
628
+ // haber medido que es seguro borrar. Devuelve lo retirado para que los tests puedan afirmar
629
+ // sobre el efecto, no sobre el orden.
630
+ function _retireSupersededTrays(trayApp, supersededPaths, rm = _rmrf,
631
+ bundleVersion = _bundleVersion,
632
+ isTrayRunningFrom = _isTrayRunningFrom) {
551
633
  if (!trayApp || !_isUsableTrayBundle(trayApp)) return []
634
+ const trayVersion = bundleVersion(trayApp)
552
635
  const retired = []
553
636
  for (const p of supersededPaths) {
554
637
  if (!p) continue
555
638
  if (_sameLocation(p, trayApp)) continue
556
639
  if (!fs.existsSync(p)) continue
640
+ const otherVersion = bundleVersion(p)
641
+ if (!trayVersion || !otherVersion) continue // unreadable — fail closed, keep it
642
+ if (_versionIsNewer(otherVersion, trayVersion)) continue
643
+ if (isTrayRunningFrom(p)) continue
557
644
  try { rm(p); retired.push(p) } catch { /* ignorar */ }
558
645
  }
559
646
  return retired
@@ -588,6 +675,8 @@ async function ensureTray(deps = {}) {
588
675
  extract = extractTar,
589
676
  writeLaunchAgent = _writeTrayLaunchAgent,
590
677
  isTTY = () => process.stdin.isTTY,
678
+ bundleVersion = _bundleVersion,
679
+ isTrayRunningFrom = _isTrayRunningFrom,
591
680
  } = deps
592
681
 
593
682
  // Windows/Linux: the tray + background daemon are installed by the native
@@ -608,36 +697,28 @@ async function ensureTray(deps = {}) {
608
697
  ]
609
698
  if (markers.some((p) => { try { return fs.existsSync(p) } catch { return false } })) return null
610
699
 
700
+ // #3627: a usable tray ANYWHERE a user would look (/Applications or ~/Applications) is this
701
+ // wrapper's stop condition, at ANY version — keeping it current from here on is the tray's
702
+ // own job (UpdateManager: staged, verified, self-update). This used to fall through to a
703
+ // download+"migrate" that moved a same-version /Applications copy to ~/Applications and
704
+ // deleted the original; on the published 3.0.413 wrapper (no version/process guard on the
705
+ // delete) that reproduced with a fresh DMG install too, not just the QA prerelease that
706
+ // surfaced it (#3627, #3599). A bundle that exists but fails `_isUsableTrayBundle` (a
707
+ // partial extraction, #1338) is NOT covered — that still needs repairing below.
708
+ if (_isUsableTrayBundle(trayApp)) return trayApp
709
+ if (_isUsableTrayBundle(systemTrayApp)) return systemTrayApp
710
+
611
711
  const info = await getLatest()
612
712
  const version = info.version
613
713
  const verFile = path.join(trayDir, '.version')
614
714
 
615
- // Las dos migraciones —~/.local/share (legacy) y /Applications → ~/Applications (evita el
616
- // diálogo App Management)— retiran una copia que puede ser la ÚNICA del Mac. Por eso el
617
- // retiro corre en exactamente dos lugares: pegado a cada early-return (no se instala nada,
618
- // el tray de destino ya está verificado) y después de extraer y verificar. Nunca en el
619
- // camino que descarga: ahí una falla dejaba la máquina sin cliente. (#2654)
715
+ // La migración legacy (~/.local/share → ~/Applications) retira una copia que puede ser la
716
+ // ÚNICA del Mac — nunca en el camino que descarga: ahí una falla dejaba la máquina sin
717
+ // cliente. (#2654) Reached only when neither main location has a usable bundle (guard
718
+ // above), so `systemTrayApp` in this list is a stale/broken leftover, never a live one.
620
719
  const superseded = [path.join(trayDir, 'LocalMCPTray.app'), systemTrayApp]
621
- const retireSuperseded = () => _retireSupersededTrays(trayApp, superseded)
622
-
623
- // Ya instalado y actualizado. "Instalado" es el bundle USABLE, no el directorio: si no,
624
- // una extracción a medias se early-returnea para siempre y nadie la repara (#1338).
625
- if (_isUsableTrayBundle(trayApp) && fs.existsSync(verFile)) {
626
- if (fs.readFileSync(verFile, 'utf8').trim() === version) {
627
- retireSuperseded()
628
- return trayApp
629
- }
630
- }
631
-
632
- // In an active MCP session (non-TTY) the tray is running and self-updates via UpdateManager.
633
- // Downloading a new tray bundle and rm-rf'ing the old one mid-session kills the Unix socket
634
- // that local-mcp-server uses, crashing the session. Skip the update; it'll apply next login.
635
- // Un bundle sin ejecutable no tiene tray corriendo que esta descarga pueda romper, así que
636
- // ese caso NO se early-returnea: se repara.
637
- if (!isTTY() && _isUsableTrayBundle(trayApp)) {
638
- retireSuperseded()
639
- return trayApp
640
- }
720
+ const retireSuperseded = () =>
721
+ _retireSupersededTrays(trayApp, superseded, _rmrf, bundleVersion, isTrayRunningFrom)
641
722
 
642
723
  const url = `https://download.local-mcp.com/local-mcp-tray-${version}-darwin-universal.tar.gz`
643
724
  process.stderr.write(`\nDescargando tray v${version}...\n`)
@@ -668,8 +749,8 @@ async function ensureTray(deps = {}) {
668
749
  extract(tarPath, userAppsDir)
669
750
  fs.unlinkSync(tarPath)
670
751
 
671
- // Mismo criterio que usa el retiro, o se estampa .version sobre un bundle roto y el
672
- // early-return de arriba lo da por bueno en cada corrida siguiente.
752
+ // Mismo criterio que usa el guard de arriba y el retiro — un directorio sin ejecutable no
753
+ // cuenta como instalado, o `verFile` (abajo) queda apuntando a un bundle que no sirve.
673
754
  if (!_isUsableTrayBundle(trayApp)) {
674
755
  throw new Error(`Tray no usable tras extraer: ${trayApp}`)
675
756
  }
@@ -794,4 +875,5 @@ module.exports = {
794
875
  _probeWinHardwareId, _resetWinHardwareIdCacheForTests,
795
876
  _readWinMachineIdCache, _writeWinMachineIdCache, WIN_MACHINE_ID_CACHE_NAME,
796
877
  _retireSupersededTrays, _isUsableTrayBundle, SYSTEM_TRAY_APP,
878
+ _bundleVersion, _isTrayRunningFrom, _versionIsNewer,
797
879
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.413",
3
+ "version": "3.0.416",
4
4
  "description": "Let ChatGPT, Claude, Cursor & any MCP client actually use your Mac or Windows PC — read & reply to email, manage your calendar, text over iMessage, find files, work with Teams, Slack & Office. Runs locally, no API keys, free.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "engines": {
21
- "node": ">=18"
21
+ "node": ">=20"
22
22
  },
23
23
  "os": [
24
24
  "darwin",
@@ -64,14 +64,14 @@
64
64
  "automation"
65
65
  ],
66
66
  "author": "LMCP <hello@local-mcp.com>",
67
- "license": "SEE LICENSE IN LICENSE",
67
+ "license": "UNLICENSED",
68
68
  "homepage": "https://local-mcp.com",
69
69
  "repository": {
70
70
  "type": "git",
71
- "url": "https://github.com/colibird-ai/local-mcp.git"
71
+ "url": "https://github.com/lanchuske/local-mcp-releases.git"
72
72
  },
73
73
  "bugs": {
74
- "url": "https://github.com/colibird-ai/local-mcp/issues"
74
+ "url": "https://github.com/lanchuske/local-mcp-releases/issues"
75
75
  },
76
76
  "mcpName": "com.local-mcp/local-mcp"
77
77
  }
package/setup.js CHANGED
@@ -530,7 +530,7 @@ async function runSetup(opts = {}) {
530
530
  if (configured.length > 0) {
531
531
  if (healthOk) {
532
532
  console.log(`✅ LMCP configured for: ${configured.join(', ')}\n`)
533
- console.log(' ✓ Server binary verified — 188+ tools ready\n')
533
+ console.log(' ✓ Server binary verified — 192+ tools ready\n')
534
534
  } else {
535
535
  console.log(`⚠ LMCP configured for: ${configured.join(', ')}`)
536
536
  console.log(' Server binary could not be verified — it may still work after restart.\n')