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 +6 -3
- package/download.js +120 -38
- package/package.json +5 -5
- package/setup.js +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Local MCP
|
|
2
2
|
|
|
3
|
-
> **
|
|
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
|
[](https://www.npmjs.com/package/local-mcp)
|
|
6
6
|
[](https://local-mcp.com?ref=npm)
|
|
7
7
|
[](https://local-mcp.com?ref=npm)
|
|
8
|
-
[](https://local-mcp.com/en/privacy)
|
|
9
9
|
[](https://glama.ai/mcp/servers/lanchuske/local-mcp-releases?ref=npm)
|
|
10
10
|
|
|
11
|
-
**
|
|
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 =
|
|
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
|
-
|
|
335
|
-
|
|
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 (
|
|
341
|
-
const binName =
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
550
|
-
|
|
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
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
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 = () =>
|
|
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
|
|
672
|
-
//
|
|
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.
|
|
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": ">=
|
|
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": "
|
|
67
|
+
"license": "UNLICENSED",
|
|
68
68
|
"homepage": "https://local-mcp.com",
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
71
|
-
"url": "https://github.com/
|
|
71
|
+
"url": "https://github.com/lanchuske/local-mcp-releases.git"
|
|
72
72
|
},
|
|
73
73
|
"bugs": {
|
|
74
|
-
"url": "https://github.com/
|
|
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 —
|
|
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')
|