local-mcp 3.0.413 → 3.0.414

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 191+ 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
@@ -543,17 +543,86 @@ function _isUsableTrayBundle(trayApp) {
543
543
  catch { return false }
544
544
  }
545
545
 
546
+ // Version compare mirroring mac-swift/Sources/LocalMCPServer/Version.swift's isNewer —
547
+ // #3599 criterio 6: un mismo criterio "más nuevo gana" en los dos lados, cada uno probado
548
+ // por separado (no hay código compartido entre Swift y Node). Incluye el mismo desempate
549
+ // que Version.swift: en un empate numérico, un release gana a un pre-release
550
+ // ("3.0.297" > "3.0.297-test").
551
+ function _versionIsNewer(a, b) {
552
+ const parse = (v) => {
553
+ const s = String(v || '')
554
+ const nums = s.split(/[-+]/, 1)[0].split('.').map((n) => parseInt(n, 10) || 0)
555
+ return { nums, isPrerelease: s.includes('-') || s.includes('+') }
556
+ }
557
+ const pa = parse(a), pb = parse(b)
558
+ for (let i = 0; i < Math.max(pa.nums.length, pb.nums.length); i++) {
559
+ const x = pa.nums[i] || 0, y = pb.nums[i] || 0
560
+ if (x !== y) return x > y
561
+ }
562
+ if (pa.isPrerelease !== pb.isPrerelease) return !pa.isPrerelease && pb.isPrerelease
563
+ return false
564
+ }
565
+
566
+ const _CFBUNDLE_VERSION_RE = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/
567
+
568
+ // CFBundleShortVersionString of an installed .app, or null when unreadable. Tries a plain
569
+ // UTF8 text read FIRST — an xml1 plist (what every test fixture here writes, and what some
570
+ // build toolchains leave on disk) needs no external tool and reads on ANY platform, unlike
571
+ // `plutil` (macOS-only, absent on the ubuntu-latest runner these tests also run on, #3599
572
+ // review). Only reaches for `plutil -convert xml1` when the direct read finds no match —
573
+ // the real fallback for a genuinely binary-encoded Info.plist.
574
+ function _bundleVersion(appPath, exec = execFileSync) {
575
+ const infoPath = path.join(appPath, 'Contents', 'Info.plist')
576
+ try {
577
+ const m = _CFBUNDLE_VERSION_RE.exec(fs.readFileSync(infoPath, 'utf8'))
578
+ if (m) return m[1]
579
+ } catch { /* missing, unreadable, or not text — fall through to plutil */ }
580
+ try {
581
+ const xml = exec('/usr/bin/plutil', ['-convert', 'xml1', '-o', '-', infoPath], { stdio: 'pipe' }).toString()
582
+ const m = _CFBUNDLE_VERSION_RE.exec(xml)
583
+ return m ? m[1] : null
584
+ } catch {
585
+ return null
586
+ }
587
+ }
588
+
589
+ // Whether a LocalMCPTray process is running FROM this exact bundle path — matched on the
590
+ // full executable path so a sibling copy elsewhere never counts. Unreadable (pgrep
591
+ // missing/blocked) fails CLOSED to "running": a guard that cannot see must not say "safe
592
+ // to delete" (same rule as index.js's countRunningServers, #1394 §C).
593
+ function _isTrayRunningFrom(appPath, exec = execFileSync) {
594
+ const bin = path.join(appPath, 'Contents', 'MacOS', 'LocalMCPTray')
595
+ try {
596
+ exec('pgrep', ['-f', bin], { stdio: 'pipe' })
597
+ return true // exit 0: matched — a tray IS running from this bundle
598
+ } catch (err) {
599
+ if (err && err.status === 1) return false // ran fine, matched nothing
600
+ return true // could not tell — fail closed
601
+ }
602
+ }
603
+
546
604
  // Retira las copias que `trayApp` supersede — SOLO si `trayApp` ya tiene un bundle usable.
547
605
  // Sin esa guarda, la migración borra la única copia que el Mac tiene antes de que exista su
548
606
  // 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) {
607
+ // #3599 AC5: tampoco retira una copia cuya versión no se pueda leer, ni una LEÍBLEMENTE más
608
+ // nueva que `trayApp`, ni una que un proceso vivo esté sirviendo — mismo criterio fail-closed
609
+ // que el lado Swift (TrayInstallHealth.shouldRemoveOtherCopy): no haber podido medir no es
610
+ // haber medido que es seguro borrar. Devuelve lo retirado para que los tests puedan afirmar
611
+ // sobre el efecto, no sobre el orden.
612
+ function _retireSupersededTrays(trayApp, supersededPaths, rm = _rmrf,
613
+ bundleVersion = _bundleVersion,
614
+ isTrayRunningFrom = _isTrayRunningFrom) {
551
615
  if (!trayApp || !_isUsableTrayBundle(trayApp)) return []
616
+ const trayVersion = bundleVersion(trayApp)
552
617
  const retired = []
553
618
  for (const p of supersededPaths) {
554
619
  if (!p) continue
555
620
  if (_sameLocation(p, trayApp)) continue
556
621
  if (!fs.existsSync(p)) continue
622
+ const otherVersion = bundleVersion(p)
623
+ if (!trayVersion || !otherVersion) continue // unreadable — fail closed, keep it
624
+ if (_versionIsNewer(otherVersion, trayVersion)) continue
625
+ if (isTrayRunningFrom(p)) continue
557
626
  try { rm(p); retired.push(p) } catch { /* ignorar */ }
558
627
  }
559
628
  return retired
@@ -588,6 +657,8 @@ async function ensureTray(deps = {}) {
588
657
  extract = extractTar,
589
658
  writeLaunchAgent = _writeTrayLaunchAgent,
590
659
  isTTY = () => process.stdin.isTTY,
660
+ bundleVersion = _bundleVersion,
661
+ isTrayRunningFrom = _isTrayRunningFrom,
591
662
  } = deps
592
663
 
593
664
  // Windows/Linux: the tray + background daemon are installed by the native
@@ -608,36 +679,28 @@ async function ensureTray(deps = {}) {
608
679
  ]
609
680
  if (markers.some((p) => { try { return fs.existsSync(p) } catch { return false } })) return null
610
681
 
682
+ // #3627: a usable tray ANYWHERE a user would look (/Applications or ~/Applications) is this
683
+ // wrapper's stop condition, at ANY version — keeping it current from here on is the tray's
684
+ // own job (UpdateManager: staged, verified, self-update). This used to fall through to a
685
+ // download+"migrate" that moved a same-version /Applications copy to ~/Applications and
686
+ // deleted the original; on the published 3.0.413 wrapper (no version/process guard on the
687
+ // delete) that reproduced with a fresh DMG install too, not just the QA prerelease that
688
+ // surfaced it (#3627, #3599). A bundle that exists but fails `_isUsableTrayBundle` (a
689
+ // partial extraction, #1338) is NOT covered — that still needs repairing below.
690
+ if (_isUsableTrayBundle(trayApp)) return trayApp
691
+ if (_isUsableTrayBundle(systemTrayApp)) return systemTrayApp
692
+
611
693
  const info = await getLatest()
612
694
  const version = info.version
613
695
  const verFile = path.join(trayDir, '.version')
614
696
 
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)
697
+ // La migración legacy (~/.local/share → ~/Applications) retira una copia que puede ser la
698
+ // ÚNICA del Mac — nunca en el camino que descarga: ahí una falla dejaba la máquina sin
699
+ // cliente. (#2654) Reached only when neither main location has a usable bundle (guard
700
+ // above), so `systemTrayApp` in this list is a stale/broken leftover, never a live one.
620
701
  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
- }
702
+ const retireSuperseded = () =>
703
+ _retireSupersededTrays(trayApp, superseded, _rmrf, bundleVersion, isTrayRunningFrom)
641
704
 
642
705
  const url = `https://download.local-mcp.com/local-mcp-tray-${version}-darwin-universal.tar.gz`
643
706
  process.stderr.write(`\nDescargando tray v${version}...\n`)
@@ -668,8 +731,8 @@ async function ensureTray(deps = {}) {
668
731
  extract(tarPath, userAppsDir)
669
732
  fs.unlinkSync(tarPath)
670
733
 
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.
734
+ // Mismo criterio que usa el guard de arriba y el retiro — un directorio sin ejecutable no
735
+ // cuenta como instalado, o `verFile` (abajo) queda apuntando a un bundle que no sirve.
673
736
  if (!_isUsableTrayBundle(trayApp)) {
674
737
  throw new Error(`Tray no usable tras extraer: ${trayApp}`)
675
738
  }
@@ -794,4 +857,5 @@ module.exports = {
794
857
  _probeWinHardwareId, _resetWinHardwareIdCacheForTests,
795
858
  _readWinMachineIdCache, _writeWinMachineIdCache, WIN_MACHINE_ID_CACHE_NAME,
796
859
  _retireSupersededTrays, _isUsableTrayBundle, SYSTEM_TRAY_APP,
860
+ _bundleVersion, _isTrayRunningFrom, _versionIsNewer,
797
861
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.413",
3
+ "version": "3.0.414",
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": {
@@ -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 — 191+ 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')