local-mcp 3.0.369 → 3.0.371

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,6 +1,6 @@
1
1
  # Local MCP
2
2
 
3
- > **The only Mac MCP you can use from ChatGPT & Claude.ai on the web** — plus 260 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, Safari, Chrome, Word/Excel/PowerPoint and more. 100% local, no API keys, free — your data never leaves your machine.
3
+ > **The only Mac MCP you can use from ChatGPT & Claude.ai on the web** — plus 215+ 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, Safari, Chrome, Word/Excel/PowerPoint and more. 100% local, no API keys, free — your data never leaves your machine.
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)
@@ -74,7 +74,7 @@ LMCP runs a native MCP server that bridges your Mac apps to any AI client:
74
74
 
75
75
   
76
76
 
77
- ## All 260 tools (macOS)
77
+ ## Tool catalog (macOS)
78
78
 
79
79
  Email (12): `list_accounts` `list_email_accounts` `list_emails` `read_email` `send_email` `reply_email` `create_draft` `search_emails` `move_email` `save_attachment` `create_email_folder` `list_email_folders`
80
80
 
@@ -132,6 +132,8 @@ NordVPN (3): `nordvpn_status` `nordvpn_servers` `nordvpn_diagnose`
132
132
 
133
133
  Referral (2): `create_referral_invites` `list_referral_candidates`
134
134
 
135
+ Agent mesh (9): `agent_mesh_status` `agent_mesh_create` `agent_mesh_invite` `agent_mesh_join` `agent_mesh_revoke` `agent_checkin` `agents_list` `agent_send` `agent_inbox`
136
+
135
137
  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
138
 
137
139
  _Coming soon (not included in the 162 count above): **Evernote** (support is being rebuilt)._
package/download.js CHANGED
@@ -18,6 +18,15 @@ function extractTar(tarPath, destDir) {
18
18
  const tarBin = process.platform === 'win32'
19
19
  ? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
20
20
  : 'tar'
21
+ // Windows 10 1803+ ships bsdtar at System32\tar.exe. On an older/stripped Windows
22
+ // it's absent — fail with a clear cause instead of a bare execFileSync ENOENT that
23
+ // the caller would otherwise mask as "not yet available" (#1256).
24
+ if (process.platform === 'win32' && !fs.existsSync(tarBin)) {
25
+ throw new Error(
26
+ `tar.exe not found at ${tarBin} — Windows 10 1803+ (with built-in bsdtar) is ` +
27
+ `required to unpack the LMCP server.`
28
+ )
29
+ }
21
30
  execFileSync(tarBin, ['-xzf', tarPath, '-C', destDir], { stdio: 'pipe' })
22
31
  }
23
32
 
@@ -81,6 +90,19 @@ function getArch() {
81
90
  throw new Error(`Unsupported architecture: ${plat}/${arch}. LMCP requires arm64 or x64.`)
82
91
  }
83
92
 
93
+ // Version of the artifact a given URL actually points at, parsed from the
94
+ // published name lmcp-server-<version>-<os>-<arch>.tar.gz. Returns null when the
95
+ // name doesn't match, so the caller falls back explicitly instead of inventing one.
96
+ //
97
+ // Why this exists: /runtime/latest returns a top-level `version` that is the MAC
98
+ // release, while windows_*/linux_* are built from a separate win-* row. They
99
+ // diverge (2026-08-08: version=3.0.369 next to a 3.0.258 windows artifact), so on
100
+ // non-darwin the Mac number describes neither what gets downloaded nor what runs.
101
+ function versionFromArtifactUrl(url, arch) {
102
+ const m = new RegExp(`/lmcp-server-(.+)-${arch}\\.tar\\.gz$`).exec(String(url || ''))
103
+ return m ? m[1] : null
104
+ }
105
+
84
106
  /**
85
107
  * Obtiene la versión más reciente del binario desde el backend.
86
108
  * Si esta máquina está en beta_machines, el backend retorna la versión beta.
@@ -109,7 +131,7 @@ async function getLatestBinary() {
109
131
  /**
110
132
  * Descarga un archivo con barra de progreso.
111
133
  */
112
- async function downloadFile(url, destPath) {
134
+ function downloadOnce(url, destPath) {
113
135
  return new Promise((resolve, reject) => {
114
136
  const file = fs.createWriteStream(destPath)
115
137
  const proto = url.startsWith('https') ? https : http
@@ -151,6 +173,27 @@ async function downloadFile(url, destPath) {
151
173
  })
152
174
  }
153
175
 
176
+ // Retry transient download failures (timeouts, resets, 5xx). A 4xx is permanent —
177
+ // a missing/forbidden artifact won't appear on a retry, so fail fast and let the
178
+ // caller decide whether that means "not yet published". Backoff grows per attempt.
179
+ async function downloadFile(url, destPath) {
180
+ const maxAttempts = 3
181
+ let lastErr
182
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
183
+ try {
184
+ return await downloadOnce(url, destPath)
185
+ } catch (err) {
186
+ lastErr = err
187
+ if (/HTTP 4\d\d/.test(err.message)) throw err // permanent, don't retry
188
+ if (attempt < maxAttempts) {
189
+ process.stderr.write(`\n Download failed (${err.message}) — retrying (${attempt}/${maxAttempts - 1})...\n`)
190
+ await new Promise(r => setTimeout(r, 1000 * attempt))
191
+ }
192
+ }
193
+ }
194
+ throw lastErr
195
+ }
196
+
154
197
  /**
155
198
  * Asegura que el binario esté descargado y listo.
156
199
  * @returns {Promise<{binPath: string}>}
@@ -167,25 +210,52 @@ async function ensureBinary() {
167
210
  const binPath = path.join(CACHE_DIR, binName)
168
211
  const versionFile = path.join(CACHE_DIR, '.go-server-version')
169
212
 
170
- // Check if already cached at the right version
213
+ // Resolve the download URL BEFORE the cache check: the version this platform
214
+ // actually installs is the one in that URL, not info.version (the Mac release).
215
+ //
216
+ // Each branch knows its own version by construction, so there is no guessing:
217
+ // · published URL → the version is whatever that name says. If the name
218
+ // doesn't parse we FAIL rather than fall back to info.version, which would
219
+ // record a Mac number against a non-Mac artifact — the very bug this path
220
+ // exists to fix, reappearing in the one corner nobody would look at.
221
+ // · composed URL → we built it from info.version, so that IS the version.
222
+ const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
223
+ const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
224
+
225
+ let url, platformVersion
226
+ if (publishedUrl) {
227
+ platformVersion = versionFromArtifactUrl(publishedUrl, arch)
228
+ if (!platformVersion) {
229
+ throw new Error(
230
+ `No pude determinar la versión del artefacto publicado para ${arch}.\n` +
231
+ ` URL: ${publishedUrl}\n` +
232
+ ` Se esperaba un nombre lmcp-server-<versión>-${arch}.tar.gz.\n` +
233
+ `Reportalo: la convención de nombres del backend cambió.`
234
+ )
235
+ }
236
+ url = publishedUrl
237
+ } else {
238
+ platformVersion = version
239
+ url = `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
240
+ }
241
+
242
+ // Check if already cached at the right version. Comparing against
243
+ // info.version here re-downloaded the SAME artifact on every Mac release and
244
+ // then recorded the Mac number, so the machine reported a build it wasn't
245
+ // running. Comparing against platformVersion keeps both directions honest:
246
+ // an unchanged Windows artifact is a cache hit, a new one is a miss.
171
247
  if (fs.existsSync(binPath)) {
172
248
  try {
173
249
  const cached = fs.readFileSync(versionFile, 'utf8').trim()
174
- if (cached === version) return { binPath, versionDir: CACHE_DIR, version }
250
+ if (cached === platformVersion) return { binPath, versionDir: CACHE_DIR, version: platformVersion }
175
251
  } catch {}
176
252
  }
177
253
 
178
- // Download the Go binary tarball from R2
179
- // Use the platform-specific URL from /runtime/latest if available (avoids version mismatch)
180
- const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
181
- const url = (info[archKey] && info[archKey].startsWith('http'))
182
- ? info[archKey]
183
- : `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
184
- process.stderr.write(`\nLMCP v${version} (${arch}) not found in cache.\n`)
254
+ process.stderr.write(`\nLMCP v${platformVersion} (${arch}) not found in cache.\n`)
185
255
  process.stderr.write(`Downloading from ${url}\n`)
186
256
 
187
257
  fs.mkdirSync(CACHE_DIR, { recursive: true })
188
- const tarPath = path.join(CACHE_DIR, `go-server-${version}.tar.gz`)
258
+ const tarPath = path.join(CACHE_DIR, `go-server-${platformVersion}.tar.gz`)
189
259
 
190
260
  try {
191
261
  await downloadFile(url, tarPath)
@@ -193,11 +263,20 @@ async function ensureBinary() {
193
263
  extractTar(tarPath, CACHE_DIR)
194
264
  fs.unlinkSync(tarPath)
195
265
  } catch (err) {
196
- // Go binary not yet published for this version — fall back to message
197
266
  try { fs.unlinkSync(tarPath) } catch {}
267
+ // Only a 404 means the artifact for this version/arch genuinely isn't published.
268
+ // Every other failure (network, missing tar.exe, disk) is an environment problem —
269
+ // surface it verbatim so it's diagnosable. Masking ALL failures as "in preview" sent
270
+ // us chasing a backend version-alignment ghost that didn't exist (#1256): the artifact
271
+ // was published, valid and hash-correct; the client just failed to fetch/unpack it.
272
+ if (/HTTP 404/.test(err.message)) {
273
+ throw new Error(
274
+ `LMCP ${platformVersion} is not yet available for ${arch}.\n` +
275
+ `The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
276
+ )
277
+ }
198
278
  throw new Error(
199
- `LMCP ${version} is not yet available for ${arch}.\n` +
200
- `The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
279
+ `Failed to install LMCP ${platformVersion} (${arch}) from ${url}:\n ${err.message}`
201
280
  )
202
281
  }
203
282
 
@@ -205,10 +284,10 @@ async function ensureBinary() {
205
284
  throw new Error(`Binary not found after extraction: ${binPath}`)
206
285
  }
207
286
  if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
208
- fs.writeFileSync(versionFile, version)
287
+ fs.writeFileSync(versionFile, platformVersion)
209
288
 
210
289
  process.stderr.write(` Ready at ${CACHE_DIR}\n`)
211
- return { binPath, versionDir: CACHE_DIR, version }
290
+ return { binPath, versionDir: CACHE_DIR, version: platformVersion }
212
291
  }
213
292
 
214
293
  // macOS: Swift binary in a nested tarball
@@ -499,4 +578,4 @@ async function ensureSlackProxy() {
499
578
  }
500
579
  }
501
580
 
502
- module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, CACHE_DIR, TRAY_DIR }
581
+ module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl, CACHE_DIR, TRAY_DIR }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.369",
3
+ "version": "3.0.371",
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": {
@@ -20,7 +20,9 @@
20
20
  "node": ">=18"
21
21
  },
22
22
  "os": [
23
- "darwin"
23
+ "darwin",
24
+ "linux",
25
+ "win32"
24
26
  ],
25
27
  "keywords": [
26
28
  "mcp",
package/setup.js CHANGED
@@ -584,7 +584,7 @@ async function runSetup(opts = {}) {
584
584
  if (configured.length > 0) {
585
585
  if (healthOk) {
586
586
  console.log(`✅ LMCP configured for: ${configured.join(', ')}\n`)
587
- console.log(' ✓ Server binary verified — 100 tools ready\n')
587
+ console.log(' ✓ Server binary verified — 215+ tools ready\n')
588
588
  } else {
589
589
  console.log(`⚠ LMCP configured for: ${configured.join(', ')}`)
590
590
  console.log(' Server binary could not be verified — it may still work after restart.\n')
@@ -662,29 +662,50 @@ async function runSetup(opts = {}) {
662
662
  // can attach an email later via the tray or /settings to unlock cross-Mac
663
663
  // sync + paid-license features. Non-fatal on failure — the install still
664
664
  // succeeds and the server can retry at startup.
665
- if (!email) {
665
+ // Provisioning is ALWAYS anonymous, with or without an email — same change made in install.sh
666
+ // (#1083). Two separate ways this used to be skipped, and both left the machine with NO
667
+ // cloud_token, depending on /tunnel/token minting one for that address on first contact — the
668
+ // very TOFU path being removed (a stranger can pre-register someone else's address and end up
669
+ // sharing the token):
670
+ // 1. `if (!email)` — an install that supplied LMCP_EMAIL never registered at all.
671
+ // 2. `alreadyLinked` — a config that already carried license_email was skipped too, even
672
+ // with no token. That one is easier to miss: it fires on the SECOND run of a machine
673
+ // whose email was saved a few lines above.
674
+ // Both fail silently: setup finishes, nothing is printed, the relay simply never comes up.
675
+ {
666
676
  try {
667
677
  const cfg = (_safeReadConfig(cfgFile)).data || {}
668
- const alreadyLinked = (cfg.license_email && cfg.license_email.length > 0)
669
- const alreadyAnon = (cfg.cloud_token && typeof cfg.cloud_token === 'string' && cfg.cloud_token.startsWith('lmcp-'))
670
- if (!alreadyLinked && !alreadyAnon) {
671
- const tok = await _registerAnonToken()
672
- if (tok) {
678
+ const existing = (cfg.cloud_token && typeof cfg.cloud_token === 'string'
679
+ && cfg.cloud_token.startsWith('lmcp-')) ? cfg.cloud_token : ''
680
+ let token = existing
681
+ if (!token) {
682
+ token = await _registerAnonToken()
683
+ if (token) {
673
684
  const cfg2 = (_safeReadConfig(cfgFile)).data || {}
674
- cfg2.cloud_token = tok
685
+ cfg2.cloud_token = token
675
686
  _atomicWriteConfig(cfgFile, cfg2)
676
687
  console.log(' ✓ Cloud relay activated (anonymous)')
677
688
  }
678
689
  }
690
+ // Associate the email with this machine's token — the one just minted OR the one it
691
+ // already had. Outside the "no token yet" branch on purpose: a machine re-running setup
692
+ // (reinstall, upgrade, second attempt) already has a token, and nesting this inside meant
693
+ // its email was dropped without a sound. The cloud_token travels as proof of possession:
694
+ // /install-event has no auth, so that UPDATE refuses to touch a row unless the caller
695
+ // holds that machine's token.
696
+ if (email && token) await _associateEmail(email, token)
679
697
  } catch { /* non-fatal */ }
680
698
 
681
- // Show settings URL with machine_id so user can register email (LMC-887)
682
- try {
683
- const mid = _getMachineId()
684
- if (mid) {
685
- console.log(`\n \x1b[1m📧 Get update notifications:\x1b[0m https://local-mcp.com/settings?m=${mid}`)
686
- }
687
- } catch { /* non-fatal */ }
699
+ // Show settings URL with machine_id so user can register email (LMC-887). Only when no
700
+ // email was supplied — someone who passed LMCP_EMAIL does not need to be asked for it.
701
+ if (!email) {
702
+ try {
703
+ const mid = _getMachineId()
704
+ if (mid) {
705
+ console.log(`\n \x1b[1m📧 Get update notifications:\x1b[0m https://local-mcp.com/settings?m=${mid}`)
706
+ }
707
+ } catch { /* non-fatal */ }
708
+ }
688
709
  }
689
710
 
690
711
  // LMCP_METHOD lets install.sh pass 'curl' so we can distinguish it from
@@ -771,6 +792,48 @@ function _getMachineId() {
771
792
  return ''
772
793
  }
773
794
 
795
+ /// Associate an email with this machine's tunnel token, presenting the token as proof of
796
+ /// possession. Setup used to get this for free by registering WITH the email; now that
797
+ /// provisioning is always anonymous, the association has to happen explicitly or these users
798
+ /// stay anonymous forever on the backend.
799
+ ///
800
+ /// /install-event has no auth, so the UPDATE behind it refuses to touch a row unless the caller
801
+ /// holds that machine's token — that guard is what makes calling this from an unauthenticated
802
+ /// installer safe. Calling it when the email is already set is harmless: the backend also guards
803
+ /// on (email IS NULL OR email = ''), so a no-op is the normal case here, not an error.
804
+ ///
805
+ /// Non-fatal on every failure path: setup must finish regardless.
806
+ async function _associateEmail(email, token) {
807
+ return new Promise((resolve) => {
808
+ try {
809
+ const https = require('https')
810
+ const payload = JSON.stringify({
811
+ stage: 'email_prompt',
812
+ email_prompt_result: 'submitted',
813
+ email,
814
+ machine_id: _getMachineId(),
815
+ cloud_token: token,
816
+ source: 'npx-setup',
817
+ })
818
+ const req = https.request({
819
+ host: BACKEND_HOST,
820
+ port: 443,
821
+ method: 'POST',
822
+ path: '/install-event',
823
+ headers: {
824
+ 'Content-Type': 'application/json',
825
+ 'Content-Length': Buffer.byteLength(payload),
826
+ },
827
+ timeout: 5000,
828
+ }, (res) => { res.on('data', () => {}); res.on('end', () => resolve()) })
829
+ req.on('error', () => resolve())
830
+ req.on('timeout', () => { try { req.destroy() } catch {} resolve() })
831
+ req.write(payload)
832
+ req.end()
833
+ } catch { resolve() }
834
+ })
835
+ }
836
+
774
837
  /// Claim an anonymous tunnel token via /tunnel/register-anon so cloud relay
775
838
  /// activates without asking for an email (2026-04-19 anon-first rollout). The
776
839
  /// token is keyed on the Mac's machine_id (IOPlatformUUID, shared via Keychain