local-mcp 3.0.92 → 3.0.96

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
  # LMCP
2
2
 
3
- > Give your AI assistant native access to Mac apps — 113 tools for Mail, Calendar, Contacts, iMessage, Teams, Slack, WhatsApp, OneDrive, Notes, OmniFocus, Safari, Word, Excel, PowerPoint, Stocks, NordVPN, and more. Everything runs locally. Your data never leaves your machine.
3
+ > Give your AI assistant native access to Mac apps — 107 tools for Mail, Calendar, Contacts, iMessage, Teams, Slack, WhatsApp, OneDrive, Notes, OmniFocus, Safari, Word, Excel, PowerPoint, Stocks, NordVPN, and more. Everything runs locally. Your data never leaves your machine.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/local-mcp)](https://www.npmjs.com/package/local-mcp)
6
6
  [![macOS 12+](https://img.shields.io/badge/macOS-12%2B-blue)](https://local-mcp.com?ref=npm)
@@ -60,9 +60,8 @@ LMCP runs a native MCP server that bridges your Mac apps to any AI client:
60
60
  | **Finder** | Search files via Spotlight, list directory contents |
61
61
  | **Safari** | Bookmarks, open tabs, click/type/fill forms, run JavaScript |
62
62
  | **NordVPN** | Status, server recommendations, diagnostics |
63
- | **M365 Directory** | Search org directory, people insights — requires Microsoft 365 account |
64
63
 
65
- ## 113 MCP Tools
64
+ ## 107 MCP Tools
66
65
 
67
66
  Email (9): `list_accounts` `list_emails` `read_email` `send_email` `reply_email` `search_emails` `move_email` `save_attachment` `create_email_folder`
68
67
 
@@ -100,8 +99,6 @@ Safari (13): `list_safari_bookmarks` `safari_list_tabs` `safari_read_tab` `safar
100
99
 
101
100
  NordVPN (3): `nordvpn_status` `nordvpn_servers` `nordvpn_diagnose`
102
101
 
103
- M365 Directory (5): `connect_m365_account` `disconnect_m365_account` `search_m365_directory` `get_m365_person` `list_m365_people_insights`
104
-
105
102
  System (7): `run_diagnostics` `update_local_mcp` `update_self_diagnosis` `daily_brief` `report_problem` `request_feature` `request_integration`
106
103
 
107
104
  ## Safety
package/download.js CHANGED
@@ -15,7 +15,10 @@ const { execFileSync } = require('child_process')
15
15
  const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
16
16
  const CACHE_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin')
17
17
  const TRAY_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'tray') // metadata dir
18
- const TRAY_APP = '/Applications/LocalMCPTray.app'
18
+ // Install to ~/Applications (user-owned) to avoid macOS App Management TCC prompt.
19
+ // /Applications requires kTCCServiceAppManagement permission which shows "node would like
20
+ // to access data from other apps" on every tray update — confusing and unnecessary.
21
+ const TRAY_APP = path.join(os.homedir(), 'Applications', 'LocalMCPTray.app')
19
22
 
20
23
  function _getMachineId() {
21
24
  const { execSync } = require('child_process')
@@ -227,6 +230,12 @@ async function ensureTray() {
227
230
  try { execFileSync('rm', ['-rf', oldTrayApp], { stdio: 'pipe' }) } catch { /* ignorar */ }
228
231
  }
229
232
 
233
+ // Migrar instalación vieja de /Applications → ~/Applications (evita diálogo App Management)
234
+ const systemTrayApp = '/Applications/LocalMCPTray.app'
235
+ if (fs.existsSync(systemTrayApp) && trayApp !== systemTrayApp) {
236
+ try { execFileSync('rm', ['-rf', systemTrayApp], { stdio: 'pipe' }) } catch { /* ignorar */ }
237
+ }
238
+
230
239
  // Ya instalado y actualizado
231
240
  if (fs.existsSync(trayApp) && fs.existsSync(verFile)) {
232
241
  if (fs.readFileSync(verFile, 'utf8').trim() === version) return trayApp
@@ -253,8 +262,12 @@ async function ensureTray() {
253
262
  try { execFileSync('rm', ['-rf', trayApp], { stdio: 'pipe' }) } catch { /* ignorar */ }
254
263
  }
255
264
 
256
- process.stderr.write(` Instalando tray en /Applications...\n`)
257
- execFileSync('tar', ['-xzf', tarPath, '-C', '/Applications'], { stdio: 'pipe' })
265
+ // Crear ~/Applications si no existe (ubicación estándar para apps de usuario)
266
+ const userAppsDir = path.join(os.homedir(), 'Applications')
267
+ fs.mkdirSync(userAppsDir, { recursive: true })
268
+
269
+ process.stderr.write(` Instalando tray en ~/Applications...\n`)
270
+ execFileSync('tar', ['-xzf', tarPath, '-C', userAppsDir], { stdio: 'pipe' })
258
271
  fs.unlinkSync(tarPath)
259
272
 
260
273
  if (!fs.existsSync(trayApp)) {
@@ -311,72 +324,71 @@ function _writeTrayLaunchAgent(trayApp) {
311
324
  }
312
325
 
313
326
  /**
314
- * Descarga el co-proceso m365-proxy si no está instalado o es una versión diferente.
315
- * El binary se instala en ~/.local/share/local-mcp/bin/m365-proxy
316
- * @returns {Promise<string|null>} Ruta al binary, o null si falla
327
+ * Downloads the slack-proxy co-process if not installed or on a different version.
328
+ * Binary is installed at ~/.local/share/local-mcp/bin/slack-proxy
329
+ * @returns {Promise<string|null>} Path to the binary, or null if it fails
317
330
  */
318
- async function ensureM365Proxy() {
331
+ async function ensureSlackProxy() {
319
332
  const info = await getLatestBinary()
320
333
  const version = info.version
321
334
  const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
322
- const binPath = path.join(CACHE_DIR, 'm365-proxy')
323
- const verFile = path.join(CACHE_DIR, '.m365-proxy-version')
335
+ const binPath = path.join(CACHE_DIR, 'slack-proxy')
336
+ const verFile = path.join(CACHE_DIR, '.slack-proxy-version')
324
337
 
325
- // Ya instalado y actualizado
338
+ // Already installed and up to date
326
339
  if (fs.existsSync(binPath) && fs.existsSync(verFile)) {
327
340
  if (fs.readFileSync(verFile, 'utf8').trim() === version) return binPath
328
341
  }
329
342
 
330
- const url = `https://download.local-mcp.com/m365-proxy-${version}-${arch}.tar.gz`
331
- process.stderr.write(`\nDescargando m365-proxy v${version}...\n`)
343
+ const url = `https://download.local-mcp.com/slack-proxy-${version}-${arch}.tar.gz`
344
+ process.stderr.write(`\nDownloading slack-proxy v${version}...\n`)
332
345
 
333
346
  fs.mkdirSync(CACHE_DIR, { recursive: true })
334
- const tarPath = path.join(CACHE_DIR, `m365-proxy-${version}.tar.gz`)
347
+ const tarPath = path.join(CACHE_DIR, `slack-proxy-${version}.tar.gz`)
335
348
 
336
349
  try {
337
350
  await downloadFile(url, tarPath)
338
351
  const stat = fs.statSync(tarPath)
339
- if (stat.size < 1000) { fs.unlinkSync(tarPath); throw new Error(`Descarga incompleta (${stat.size} bytes)`) }
352
+ if (stat.size < 1000) { fs.unlinkSync(tarPath); throw new Error(`Incomplete download (${stat.size} bytes)`) }
340
353
 
341
354
  execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
342
355
  fs.unlinkSync(tarPath)
343
356
 
344
- if (!fs.existsSync(binPath)) throw new Error('m365-proxy no encontrado tras extraer')
357
+ if (!fs.existsSync(binPath)) throw new Error('slack-proxy not found after extraction')
345
358
  fs.chmodSync(binPath, 0o755)
346
- try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.m365-proxy', binPath], { stdio: 'pipe' }) } catch { /* no crítico */ }
359
+ try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.slack-proxy', binPath], { stdio: 'pipe' }) } catch { /* non-critical */ }
347
360
 
348
361
  fs.writeFileSync(verFile, version, 'utf8')
349
- process.stderr.write(` m365-proxy instalado en ${binPath}\n`)
362
+ process.stderr.write(` slack-proxy installed at ${binPath}\n`)
350
363
  return binPath
351
364
  } catch (err) {
352
365
  try { if (fs.existsSync(tarPath)) fs.unlinkSync(tarPath) } catch {}
353
- process.stderr.write(` (m365-proxy no disponible: ${err.message})\n`)
366
+ process.stderr.write(` (slack-proxy not available: ${err.message})\n`)
354
367
  return null
355
368
  }
356
369
  }
357
370
 
358
371
  /**
359
- * Downloads the slack-proxy co-process if not installed or on a different version.
360
- * Binary is installed at ~/.local/share/local-mcp/bin/slack-proxy
372
+ * Downloads local-mcp-helper on first install only.
373
+ * CRITICAL: never replaces an existing helper binary — the helper owns
374
+ * Calendar/Contacts/Reminders TCC grants via its cdhash. Replacing the
375
+ * binary silently resets those grants and users would have to re-authorize.
361
376
  * @returns {Promise<string|null>} Path to the binary, or null if it fails
362
377
  */
363
- async function ensureSlackProxy() {
378
+ async function ensureHelper() {
379
+ const binPath = path.join(CACHE_DIR, 'local-mcp-helper')
380
+
381
+ // Already installed — never replace. Stable cdhash = stable TCC grants.
382
+ if (fs.existsSync(binPath)) return binPath
383
+
364
384
  const info = await getLatestBinary()
365
385
  const version = info.version
366
386
  const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
367
- const binPath = path.join(CACHE_DIR, 'slack-proxy')
368
- const verFile = path.join(CACHE_DIR, '.slack-proxy-version')
369
-
370
- // Already installed and up to date
371
- if (fs.existsSync(binPath) && fs.existsSync(verFile)) {
372
- if (fs.readFileSync(verFile, 'utf8').trim() === version) return binPath
373
- }
374
-
375
- const url = `https://download.local-mcp.com/slack-proxy-${version}-${arch}.tar.gz`
376
- process.stderr.write(`\nDownloading slack-proxy v${version}...\n`)
387
+ const url = `https://download.local-mcp.com/local-mcp-helper-${version}-${arch}.tar.gz`
388
+ process.stderr.write(`\nDownloading local-mcp-helper v${version}...\n`)
377
389
 
378
390
  fs.mkdirSync(CACHE_DIR, { recursive: true })
379
- const tarPath = path.join(CACHE_DIR, `slack-proxy-${version}.tar.gz`)
391
+ const tarPath = path.join(CACHE_DIR, `local-mcp-helper-${version}.tar.gz`)
380
392
 
381
393
  try {
382
394
  await downloadFile(url, tarPath)
@@ -386,18 +398,21 @@ async function ensureSlackProxy() {
386
398
  execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
387
399
  fs.unlinkSync(tarPath)
388
400
 
389
- if (!fs.existsSync(binPath)) throw new Error('slack-proxy not found after extraction')
401
+ if (!fs.existsSync(binPath)) throw new Error('local-mcp-helper not found after extraction')
390
402
  fs.chmodSync(binPath, 0o755)
391
- try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.slack-proxy', binPath], { stdio: 'pipe' }) } catch { /* non-critical */ }
403
+ // Do NOT codesign: the pre-built binary's cdhash must remain exactly as
404
+ // distributed. Codesigning a fresh binary produces the same cdhash only
405
+ // when the content is identical, but we want the CI-signed cdhash to be
406
+ // the canonical identity used in TCC grants.
392
407
 
393
- fs.writeFileSync(verFile, version, 'utf8')
394
- process.stderr.write(` slack-proxy installed at ${binPath}\n`)
408
+ fs.writeFileSync(path.join(CACHE_DIR, '.helper-version'), version, 'utf8')
409
+ process.stderr.write(` local-mcp-helper installed at ${binPath}\n`)
395
410
  return binPath
396
411
  } catch (err) {
397
412
  try { if (fs.existsSync(tarPath)) fs.unlinkSync(tarPath) } catch {}
398
- process.stderr.write(` (slack-proxy not available: ${err.message})\n`)
413
+ process.stderr.write(` (local-mcp-helper not available: ${err.message})\n`)
399
414
  return null
400
415
  }
401
416
  }
402
417
 
403
- module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureM365Proxy, ensureSlackProxy, CACHE_DIR, TRAY_DIR }
418
+ module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, ensureHelper, CACHE_DIR, TRAY_DIR }
package/index.js CHANGED
@@ -136,10 +136,10 @@ async function main() {
136
136
  : ''
137
137
  if (cachedVersion && semverGte(cachedVersion, pkg.version)) {
138
138
  // Cached binary is same or newer — use fast path, no download needed
139
- const { ensureTray, ensureTeamsProxy, ensureM365Proxy } = require('./download')
139
+ const { ensureTray, ensureTeamsProxy, ensureHelper } = require('./download')
140
140
  ensureTray().catch(() => {})
141
141
  ensureTeamsProxy().catch(() => {})
142
- ensureM365Proxy().catch(() => {})
142
+ ensureHelper().catch(() => {})
143
143
  const child = spawn(stableBin, [], { stdio: 'inherit', env: process.env })
144
144
  child.on('error', () => process.exit(1))
145
145
  child.on('exit', (code) => process.exit(code ?? 0))
@@ -158,12 +158,12 @@ async function main() {
158
158
  }
159
159
 
160
160
  // Slow path: download/repair binary
161
- const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureM365Proxy } = require('./download')
161
+ const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper } = require('./download')
162
162
 
163
- // Actualizar tray + teams-proxy + m365-proxy en background
163
+ // Update tray + proxies in background. Helper is downloaded only on first install.
164
164
  ensureTray().catch(() => {})
165
165
  ensureTeamsProxy().catch(() => {})
166
- ensureM365Proxy().catch(() => {})
166
+ ensureHelper().catch(() => {})
167
167
 
168
168
  let runtime
169
169
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.92",
3
+ "version": "3.0.96",
4
4
  "description": "LMCP — connect Claude Desktop, Cursor, Windsurf to Mail, Calendar, Contacts, Teams, OneDrive on macOS. Privacy-first: all data stays on your Mac.",
5
5
  "main": "index.js",
6
6
  "bin": {