local-mcp 3.0.420 → 3.0.421

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,9 +1,9 @@
1
1
  # Local MCP
2
2
 
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.
3
+ > **Use your Mac or Windows PC from ChatGPT & Claude.ai on the web** — plus 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, Notion, Microsoft 365, Notes, Reminders, OmniFocus, Word/Excel/PowerPoint and more. Runs on your computer, no API keys — 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
- [![macOS](https://img.shields.io/badge/macOS-13%2B-111111?style=flat-square)](https://local-mcp.com?ref=npm)
6
+ [![Platform](https://img.shields.io/badge/platform-macOS%2013%2B%20%7C%20Windows%2010%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
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/colibird-ai/local-mcp-releases/badge)](https://glama.ai/mcp/servers/colibird-ai/local-mcp-releases?ref=npm)
@@ -41,7 +41,7 @@ Auto-detects and configures Claude Desktop, Claude Code, Cursor, Windsurf, VS Co
41
41
 
42
42
  ## What it does
43
43
 
44
- LMCP runs a native MCP server that bridges your Mac apps to any AI client:
44
+ LMCP runs a native MCP server that bridges the apps on your Mac or Windows PC to any AI client. Availability varies by platform:
45
45
 
46
46
  | Service | Capabilities |
47
47
  |---------|-------------|
@@ -137,8 +137,6 @@ System (12): `lmcp_state` `get_config` `get_datetime` `daily_brief` `run_diagnos
137
137
 
138
138
  Shortcuts (3): `shortcuts_list` `shortcuts_run` `shortcuts_view`
139
139
 
140
- _Coming soon (not included in the 162 count above): **Evernote** (support is being rebuilt)._
141
-
142
140
   
143
141
 
144
142
  ## Safety
@@ -165,7 +163,7 @@ Full policy: **[local-mcp.com/en/privacy](https://local-mcp.com/en/privacy)**.
165
163
 
166
164
  ## Requirements
167
165
 
168
- - macOS 13 Ventura or later (Apple Silicon and Intel)
166
+ - macOS 13 Ventura or later (Apple Silicon and Intel), or Windows 10 or later (64-bit)
169
167
  - One of: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Zed
170
168
 
171
169
  ### Config file locations
package/download.js CHANGED
@@ -331,7 +331,7 @@ function downloadOnce(url, destPath) {
331
331
  }
332
332
  if (res.statusCode !== 200) {
333
333
  file.close()
334
- fs.unlinkSync(destPath)
334
+ fs.rmSync(destPath, { force: true })
335
335
  return reject(new Error(`HTTP ${res.statusCode} descargando binario`))
336
336
  }
337
337
 
@@ -352,7 +352,7 @@ function downloadOnce(url, destPath) {
352
352
 
353
353
  res.pipe(file)
354
354
  file.on('finish', () => { file.close(); process.stderr.write('\n'); resolve() })
355
- file.on('error', (err) => { fs.unlinkSync(destPath); reject(err) })
355
+ file.on('error', (err) => { fs.rmSync(destPath, { force: true }); reject(err) })
356
356
  }).on('error', reject)
357
357
  }
358
358
 
package/index.js CHANGED
@@ -268,11 +268,22 @@ function reportarLanzamiento(outcome, opciones) {
268
268
  if (ES_LANZAMIENTO) evento.reportar(outcome, evento.claseDeArgumento(cmd), opciones)
269
269
  }
270
270
  // Antes que nada: un `spawned` que en Windows esperaba su id sale ahora, por el proceso suelto (#21).
271
- if (ES_LANZAMIENTO) process.on('exit', () => { evento.soltarPendiente(); reportarLanzamiento('other') })
271
+ // La red dice además por qué (#82), como clase: una excepción que nadie atrapó, o no se sabe.
272
+ let razonDeSalida = 'unknown'
273
+ if (ES_LANZAMIENTO) {
274
+ process.on('uncaughtExceptionMonitor', () => { razonDeSalida = 'uncaught-exception' })
275
+ process.on('exit', () => { evento.soltarPendiente(); reportarLanzamiento('other', { reason: razonDeSalida }) })
276
+ }
277
+ // #82: un server que sale con error dentro de esta ventana no arrancó. El `spawned` espera a que
278
+ // pase; el server y el cliente no esperan nada: sólo se demora el evento.
279
+ const VENTANA_ARRANQUE_MS = 2000
272
280
 
273
281
  // Platform support: macOS (Swift tray + embedded server) and Windows (go-server standalone
274
282
  // binary downloaded from R2). Linux passes this guard so that `--help`, `status`, `update` and
275
283
  // `uninstall` answer, but it is NOT supported: the server path and `setup` say so (#9).
284
+ // Since local-mcp-releases#61 row 5 npm refuses the install on Linux (`os` in package.json), so
285
+ // this branch is no longer reached through `npm install`/`npx`. It stays for whoever gets past
286
+ // that: `node index.js` on a checkout, `npm install --force`, or a tool that ignores `os`.
276
287
  const SUPPORTED_PLATFORMS = ['darwin', 'win32', 'linux']
277
288
  if (!SUPPORTED_PLATFORMS.includes(process.platform)) {
278
289
  console.error(`LMCP supports macOS and Windows. Your platform (${process.platform}) is not supported yet — see https://local-mcp.com`)
@@ -344,7 +355,15 @@ async function main() {
344
355
  // sólo buscaba ayuda de CLI. Un comando de consulta no instala software.
345
356
  //
346
357
  // Sin argumentos sigue arrancando el server, que es como lo invocan los clientes de IA.
347
- if (cmd !== undefined && !COMANDOS.includes(cmd) && !SINONIMOS_DEL_SERVER.includes(cmd)) {
358
+ //
359
+ // #88: un cliente de IA (stdin sin TTY) que manda un argumento desconocido quiere el server, y
360
+ // con exit 2 reintentaba en bucle. Ahí se ignora el argumento (una línea por stderr, nunca
361
+ // stdout: es JSON-RPC) y se sigue por el camino del server, que reporta `spawned` con la clase.
362
+ // En una terminal (TTY) y ante un pedido de ayuda sigue la ayuda: una consulta no descarga ni instala.
363
+ const desconocido = cmd !== undefined && !COMANDOS.includes(cmd) && !SINONIMOS_DEL_SERVER.includes(cmd)
364
+ if (desconocido && !PEDIDOS_DE_AYUDA.includes(cmd) && !(process.stdin && process.stdin.isTTY)) {
365
+ process.stderr.write(`[LMCP] ignoring unknown argument ${JSON.stringify(String(cmd).slice(0, 40))}; starting the MCP server\n`)
366
+ } else if (desconocido) {
348
367
  const pedidoDeAyuda = PEDIDOS_DE_AYUDA.includes(cmd)
349
368
  mostrarAyuda()
350
369
  if (!pedidoDeAyuda) {
@@ -603,14 +622,28 @@ async function main() {
603
622
  try { _pkgVer = require('./package.json').version || '' } catch {}
604
623
  const child = spawn(binaryPath, spawnArgs, { stdio: 'inherit', env: { ...process.env, LMCP_NPM_VERSION: _pkgVer } })
605
624
  noteSpawnedServer(child.pid)
606
- // El server ya arrancó y este proceso queda vivo toda la sesión: el evento sale desde acá.
607
- child.on('spawn', () => reportarLanzamiento('spawned', { enProceso: true }))
625
+ // El server arrancó y sigue vivo pasada la ventana: este proceso queda toda la sesión y el
626
+ // evento sale desde acá.
627
+ let arranque = null
628
+ child.on('spawn', () => {
629
+ arranque = setTimeout(() => { arranque = null; reportarLanzamiento('spawned', { enProceso: true }) }, VENTANA_ARRANQUE_MS)
630
+ arranque.unref() // el evento nunca retiene al proceso ni demora una salida
631
+ })
608
632
  child.on('error', (err) => {
609
- reportarLanzamiento('spawn-failed')
633
+ reportarLanzamiento('spawn-failed', { reason: evento.razonDeError(err) })
610
634
  process.stderr.write(`Error ejecutando LMCP: ${err.message}\n`)
611
635
  process.exit(1)
612
636
  })
613
- child.on('exit', (code) => process.exit(code ?? 0))
637
+ child.on('exit', (code, signal) => {
638
+ // Dentro de la ventana (#82): salió con error = no arrancó (`spawn-failed`), nunca `spawned`.
639
+ // El código de salida pasa al cliente igual que siempre.
640
+ if (arranque) {
641
+ clearTimeout(arranque)
642
+ if (code === 0) reportarLanzamiento('spawned')
643
+ else reportarLanzamiento('spawn-failed', { reason: signal ? 'signal' : 'early-exit' })
644
+ }
645
+ process.exit(code ?? 0)
646
+ })
614
647
  }
615
648
 
616
649
  // On Windows: apply pending update before launching (same as Mac's UpdateManager)
@@ -747,7 +780,7 @@ async function main() {
747
780
  try {
748
781
  runtime = await ensureRuntime()
749
782
  } catch (err) {
750
- reportarLanzamiento('download-failed')
783
+ reportarLanzamiento('download-failed', { reason: evento.razonDeError(err) })
751
784
  process.stderr.write(`\nError al preparar el runtime de LMCP:\n${err.message}\n`)
752
785
  process.stderr.write(`Intentá reinstalar: npx local-mcp update\n\n`)
753
786
  process.exit(1)
@@ -783,6 +816,7 @@ async function main() {
783
816
  // round-trip that no static check can see.
784
817
  if (require.main === module) {
785
818
  main().catch(err => {
819
+ razonDeSalida = 'uncaught-exception'
786
820
  process.stderr.write(`Error fatal: ${err.message}\n`)
787
821
  process.exit(1)
788
822
  })
package/launch-event.js CHANGED
@@ -12,23 +12,49 @@
12
12
  // · escribir en stdout — es el protocolo;
13
13
  // · demorar el arranque del server o una salida — sin reintentos, timeout corto, todo error
14
14
  // tragado;
15
- // · mandar algo que no sea uno de los cinco campos: ni el argumento, ni rutas, ni entorno.
15
+ // · mandar algo que no sea uno de los campos del contrato, con valores de conjunto cerrado: ni
16
+ // el argumento, ni rutas, ni entorno, ni el mensaje de un error (#82: de un error sale su clase).
16
17
  const { https } = require('./user-agent')
17
18
  const cp = require('child_process')
18
19
  const DESTINO = { hostname: 'office-mcp-production.up.railway.app', path: '/launch-event' }
19
20
  const PLATAFORMAS = ['darwin', 'win32', 'linux', 'other']
20
- const CLASES = ['none', 'stdio', 'unknown']
21
+ const CLASES = ['none', 'stdio', 'unknown', 'start', 'serve', 'server', 'run', 'mcp', '--stdio', 'other-word', 'other-flag']
21
22
  const OUTCOMES = ['spawned', 'rejected-argument', 'unsupported-node', 'unsupported-platform',
22
23
  'download-failed', 'spawn-failed', 'uninstalled', 'other']
24
+ // #82: por qué falló, como clase. Sólo acompaña a un outcome de falla, y sólo donde hay algo que
25
+ // clasificar (`download-failed`, `spawn-failed`, `other`); `checksum-mismatch` e `init-failed`
26
+ // son del contrato y este wrapper no los produce.
27
+ const RAZONES = ['network', 'timeout', 'http-status', 'checksum-mismatch', 'disk-write',
28
+ 'permission-denied', 'binary-missing', 'early-exit', 'signal', 'uncaught-exception', 'init-failed', 'unknown']
29
+ const CAMINO = 'npm-wrapper' // launch_path: quién arrancó el server
23
30
  const RE_VERSION = /^\d{1,3}\.\d{1,3}\.\d{1,5}$/
24
31
  const RE_MAQUINA = /^[A-Za-z0-9._:{}-]{1,64}$/
25
32
  const [TIMEOUT_MS, TOPE_MS] = [3000, 4000] // el pedido · la vida del proceso suelto
26
33
  const TOPE_ID_MS = 2000 // Windows: cuánto puede esperar el `spawned` al id de hardware (#21)
27
34
 
28
- // Del argumento sólo viaja su clase. `stdio` es el único que se nombra: es el del incidente.
35
+ // Del argumento sólo viaja su clase (#88): `stdio` y las formas que los clientes inventan se
36
+ // nombran (conjunto fijo); cualquier otra cosa es `other-flag` si empieza con `-` y `other-word`
37
+ // si no. El texto crudo no sale nunca. `unknown` queda en el conjunto por compatibilidad: ya no se emite.
38
+ const NOMBRADOS = ['stdio', 'start', 'serve', 'server', 'run', 'mcp', '--stdio']
29
39
  function claseDeArgumento (cmd) {
30
40
  if (cmd === undefined) return 'none'
31
- return cmd === 'stdio' ? 'stdio' : 'unknown'
41
+ if (NOMBRADOS.includes(cmd)) return cmd
42
+ return typeof cmd === 'string' && cmd.startsWith('-') ? 'other-flag' : 'other-word'
43
+ }
44
+
45
+ // De un error sólo viaja su clase (#82). El mensaje se LEE acá —trae rutas y URLs— y no sale: lo
46
+ // que devuelve es siempre un valor de RAZONES. El orden importa: «EACCES … open <ruta>» es de
47
+ // permisos aunque la ruta diga otra cosa.
48
+ function razonDeError (err) {
49
+ let t = ''
50
+ try { t = `${(err && err.code) || ''} ${(err && err.message) || ''}` } catch {}
51
+ if (/\b(EACCES|EPERM)\b/.test(t)) return 'permission-denied'
52
+ if (/timeout|ETIMEDOUT/i.test(t)) return 'timeout'
53
+ if (/HTTP \d{3}|not yet available/.test(t)) return 'http-status'
54
+ if (/\b(ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|EPIPE)\b|socket hang up|CERT|ERR_TLS/.test(t)) return 'network'
55
+ if (/\b(ENOSPC|EROFS|EIO|EBUSY|EDQUOT)\b/.test(t)) return 'disk-write'
56
+ if (/\bENOENT\b|not found after extraction|no encontrado después de extraer|tar\.exe not found/.test(t)) return 'binary-missing'
57
+ return 'unknown'
32
58
  }
33
59
 
34
60
  // El heartbeat no tiene compuerta de consentimiento (ver #11). Este evento respeta además el
@@ -55,8 +81,11 @@ function cuerpo (outcome, clase, deps = {}) {
55
81
  package_version: version,
56
82
  platform: PLATAFORMAS.includes(plataforma) ? plataforma : 'other',
57
83
  arg_class: clase,
58
- outcome
84
+ outcome,
85
+ launch_path: CAMINO
59
86
  }
87
+ // Una razón fuera del conjunto no viaja, y un `spawned` no lleva razón; el evento sale igual.
88
+ if (outcome !== 'spawned' && RAZONES.includes(deps.reason)) c.reason = deps.reason
60
89
  const id = 'machineId' in deps ? deps.machineId : idDeMaquina()
61
90
  if (typeof id === 'string' && RE_MAQUINA.test(id)) c.machine_id = id
62
91
  return c
@@ -140,7 +169,7 @@ function idDeHardwareWindows (listo) {
140
169
  // Una persona en una terminal (stdin TTY) no es un cliente de IA lanzando el server: no reporta.
141
170
  let reportado = false
142
171
  let pendiente = null
143
- function reportar (outcome, clase, { enProceso = false } = {}) {
172
+ function reportar (outcome, clase, { enProceso = false, reason } = {}) {
144
173
  if (reportado) return
145
174
  reportado = true
146
175
  try {
@@ -162,7 +191,7 @@ function reportar (outcome, clase, { enProceso = false } = {}) {
162
191
  if (c) enviar(c, { soltar: true })
163
192
  return
164
193
  }
165
- lanzarSuelto(outcome, clase)
194
+ lanzarSuelto(outcome, clase, reason)
166
195
  } catch {}
167
196
  }
168
197
 
@@ -170,8 +199,9 @@ function reportar (outcome, clase, { enProceso = false } = {}) {
170
199
  // como reportado para que la red del `exit` tampoco lo mande como `other`.
171
200
  function silenciar () { reportado = true }
172
201
 
173
- function lanzarSuelto (outcome, clase) {
174
- const hijo = cp.spawn(process.execPath, [__filename, outcome, clase],
202
+ function lanzarSuelto (outcome, clase, reason) {
203
+ // La razón viaja como argumento sólo si es del conjunto: al proceso suelto no le llega texto libre.
204
+ const hijo = cp.spawn(process.execPath, [__filename, outcome, clase, ...(RAZONES.includes(reason) ? [reason] : [])],
175
205
  { detached: true, stdio: 'ignore', windowsHide: true })
176
206
  hijo.on('error', () => {})
177
207
  hijo.unref()
@@ -187,12 +217,12 @@ function soltarPendiente () {
187
217
  try { lanzarSuelto(outcome, clase) } catch {}
188
218
  }
189
219
 
190
- // Como programa es el proceso suelto: `node launch-event.js <outcome> <clase>`.
220
+ // Como programa es el proceso suelto: `node launch-event.js <outcome> <clase> [reason]`.
191
221
  if (require.main === module) {
192
222
  setTimeout(() => process.exit(0), TOPE_MS).unref()
193
- const c = apagado() ? null : cuerpo(process.argv[2], process.argv[3])
223
+ const c = apagado() ? null : cuerpo(process.argv[2], process.argv[3], { reason: process.argv[4] })
194
224
  if (c) enviar(c)
195
225
  }
196
226
 
197
- module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RE_VERSION, RE_MAQUINA, TOPE_ID_MS, claseDeArgumento, apagado, cuerpo, reportar, silenciar, soltarPendiente,
227
+ module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RAZONES, CAMINO, razonDeError, RE_VERSION, RE_MAQUINA, TOPE_ID_MS, claseDeArgumento, apagado, cuerpo, reportar, silenciar, soltarPendiente,
198
228
  _idDeHardwareWindows: idDeHardwareWindows } // costura de test
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.420",
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.",
3
+ "version": "3.0.421",
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, Notion & Office. Runs locally, no API keys.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "local-mcp": "index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "postinstall": "node postinstall.js",
11
- "test": "node --test test/*.test.js"
11
+ "test": "node --require ./test/aislado.preload.js --test test/*.test.js"
12
12
  },
13
13
  "files": [
14
14
  "index.js",
@@ -25,7 +25,6 @@
25
25
  },
26
26
  "os": [
27
27
  "darwin",
28
- "linux",
29
28
  "win32"
30
29
  ],
31
30
  "keywords": [
package/setup.js CHANGED
@@ -797,10 +797,12 @@ function _getRef() {
797
797
  // _getMachineId runs on every ensureBinary()/ensureTray()/... call (i.e. on every
798
798
  // npx spawn), and a Keychain write there would be needless overhead on a hot path
799
799
  // that setup.js — which only runs once at install time — doesn't share.
800
- function _getMachineId() {
801
- const id = download._getMachineId()
802
- if (_IS_MAC && id) {
803
- try { execSync(`security add-generic-password -s com.local-mcp.machine-id -a machine-id -w "${id}" -U 2>/dev/null`, { stdio: 'pipe' }) } catch {}
800
+ // `deps` es la costura de test (#84): sin ella un test pedía al Keychain REAL de quien lo corre
801
+ // que creara o pisara la identidad de la máquina. Producto: nadie la pasa, nada cambia.
802
+ function _getMachineId(deps = {}) {
803
+ const id = download._getMachineId(deps)
804
+ if ((deps.platform ? deps.platform === 'darwin' : _IS_MAC) && id) {
805
+ try { (deps.execImpl || execSync)(`security add-generic-password -s com.local-mcp.machine-id -a machine-id -w "${id}" -U 2>/dev/null`, { stdio: 'pipe' }) } catch {}
804
806
  }
805
807
  return id
806
808
  }