local-mcp 3.0.369 → 3.0.370

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 271 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
+ ## All 271 tools (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
@@ -81,6 +81,19 @@ function getArch() {
81
81
  throw new Error(`Unsupported architecture: ${plat}/${arch}. LMCP requires arm64 or x64.`)
82
82
  }
83
83
 
84
+ // Version of the artifact a given URL actually points at, parsed from the
85
+ // published name lmcp-server-<version>-<os>-<arch>.tar.gz. Returns null when the
86
+ // name doesn't match, so the caller falls back explicitly instead of inventing one.
87
+ //
88
+ // Why this exists: /runtime/latest returns a top-level `version` that is the MAC
89
+ // release, while windows_*/linux_* are built from a separate win-* row. They
90
+ // diverge (2026-08-08: version=3.0.369 next to a 3.0.258 windows artifact), so on
91
+ // non-darwin the Mac number describes neither what gets downloaded nor what runs.
92
+ function versionFromArtifactUrl(url, arch) {
93
+ const m = new RegExp(`/lmcp-server-(.+)-${arch}\\.tar\\.gz$`).exec(String(url || ''))
94
+ return m ? m[1] : null
95
+ }
96
+
84
97
  /**
85
98
  * Obtiene la versión más reciente del binario desde el backend.
86
99
  * Si esta máquina está en beta_machines, el backend retorna la versión beta.
@@ -167,25 +180,52 @@ async function ensureBinary() {
167
180
  const binPath = path.join(CACHE_DIR, binName)
168
181
  const versionFile = path.join(CACHE_DIR, '.go-server-version')
169
182
 
170
- // Check if already cached at the right version
183
+ // Resolve the download URL BEFORE the cache check: the version this platform
184
+ // actually installs is the one in that URL, not info.version (the Mac release).
185
+ //
186
+ // Each branch knows its own version by construction, so there is no guessing:
187
+ // · published URL → the version is whatever that name says. If the name
188
+ // doesn't parse we FAIL rather than fall back to info.version, which would
189
+ // record a Mac number against a non-Mac artifact — the very bug this path
190
+ // exists to fix, reappearing in the one corner nobody would look at.
191
+ // · composed URL → we built it from info.version, so that IS the version.
192
+ const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
193
+ const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
194
+
195
+ let url, platformVersion
196
+ if (publishedUrl) {
197
+ platformVersion = versionFromArtifactUrl(publishedUrl, arch)
198
+ if (!platformVersion) {
199
+ throw new Error(
200
+ `No pude determinar la versión del artefacto publicado para ${arch}.\n` +
201
+ ` URL: ${publishedUrl}\n` +
202
+ ` Se esperaba un nombre lmcp-server-<versión>-${arch}.tar.gz.\n` +
203
+ `Reportalo: la convención de nombres del backend cambió.`
204
+ )
205
+ }
206
+ url = publishedUrl
207
+ } else {
208
+ platformVersion = version
209
+ url = `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
210
+ }
211
+
212
+ // Check if already cached at the right version. Comparing against
213
+ // info.version here re-downloaded the SAME artifact on every Mac release and
214
+ // then recorded the Mac number, so the machine reported a build it wasn't
215
+ // running. Comparing against platformVersion keeps both directions honest:
216
+ // an unchanged Windows artifact is a cache hit, a new one is a miss.
171
217
  if (fs.existsSync(binPath)) {
172
218
  try {
173
219
  const cached = fs.readFileSync(versionFile, 'utf8').trim()
174
- if (cached === version) return { binPath, versionDir: CACHE_DIR, version }
220
+ if (cached === platformVersion) return { binPath, versionDir: CACHE_DIR, version: platformVersion }
175
221
  } catch {}
176
222
  }
177
223
 
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`)
224
+ process.stderr.write(`\nLMCP v${platformVersion} (${arch}) not found in cache.\n`)
185
225
  process.stderr.write(`Downloading from ${url}\n`)
186
226
 
187
227
  fs.mkdirSync(CACHE_DIR, { recursive: true })
188
- const tarPath = path.join(CACHE_DIR, `go-server-${version}.tar.gz`)
228
+ const tarPath = path.join(CACHE_DIR, `go-server-${platformVersion}.tar.gz`)
189
229
 
190
230
  try {
191
231
  await downloadFile(url, tarPath)
@@ -196,7 +236,7 @@ async function ensureBinary() {
196
236
  // Go binary not yet published for this version — fall back to message
197
237
  try { fs.unlinkSync(tarPath) } catch {}
198
238
  throw new Error(
199
- `LMCP ${version} is not yet available for ${arch}.\n` +
239
+ `LMCP ${platformVersion} is not yet available for ${arch}.\n` +
200
240
  `The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
201
241
  )
202
242
  }
@@ -205,10 +245,10 @@ async function ensureBinary() {
205
245
  throw new Error(`Binary not found after extraction: ${binPath}`)
206
246
  }
207
247
  if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
208
- fs.writeFileSync(versionFile, version)
248
+ fs.writeFileSync(versionFile, platformVersion)
209
249
 
210
250
  process.stderr.write(` Ready at ${CACHE_DIR}\n`)
211
- return { binPath, versionDir: CACHE_DIR, version }
251
+ return { binPath, versionDir: CACHE_DIR, version: platformVersion }
212
252
  }
213
253
 
214
254
  // macOS: Swift binary in a nested tarball
@@ -499,4 +539,4 @@ async function ensureSlackProxy() {
499
539
  }
500
540
  }
501
541
 
502
- module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, CACHE_DIR, TRAY_DIR }
542
+ 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.370",
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": {
package/setup.js CHANGED
@@ -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