local-mcp 3.0.416 → 3.0.418

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
@@ -6,7 +6,7 @@
6
6
  [![macOS](https://img.shields.io/badge/macOS-13%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
- [![Glama](https://glama.ai/mcp/servers/lanchuske/local-mcp-releases/badge)](https://glama.ai/mcp/servers/lanchuske/local-mcp-releases?ref=npm)
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)
10
10
 
11
11
  **Runs on your computer. No tokens. No API keys. Free.**
12
12
 
@@ -203,4 +203,4 @@ Stops all background processes, removes the LaunchAgent, deletes the app and bin
203
203
 
204
204
  ## Links
205
205
 
206
- [Website](https://local-mcp.com?ref=npm) · [GitHub](https://github.com/lanchuske/local-mcp-releases) · [Guides](https://local-mcp.com/guides) · Support: ctpo@colibird.co
206
+ [Website](https://local-mcp.com?ref=npm) · [GitHub](https://github.com/colibird-ai/local-mcp-releases) · [Guides](https://local-mcp.com/guides) · Support: ctpo@colibird.co
package/download.js CHANGED
@@ -31,6 +31,7 @@ function extractTar(tarPath, destDir) {
31
31
  }
32
32
 
33
33
  const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
34
+ const LINUX_NO_SOPORTADO = 'Local MCP does not support Linux. It runs on macOS and Windows — https://local-mcp.com'
34
35
 
35
36
  // Platform-aware cache directory:
36
37
  // macOS: ~/.local/share/local-mcp/bin
@@ -108,6 +109,26 @@ function _writeWinMachineIdCache(cacheDir, id) {
108
109
  // memoized too, on purpose: a run where PowerShell is blocked shouldn't retry
109
110
  // it on every subsequent call in the same process.
110
111
  let _winHardwareIdCache
112
+ // Valores que Win32_ComputerSystemProduct.UUID devuelve y que NO identifican a una máquina:
113
+ // relleno, o —el caso peligroso— el MISMO valor en máquinas distintas (defaults de OEM/BIOS).
114
+ // Aceptar uno junta a N máquinas en una identidad, y la separa de su propio heartbeat, porque el
115
+ // cliente Go lo rechaza. Copia a mano de `knownGarbageHardwareIDs` de lmcp-win
116
+ // (internal/machineid/probe_windows.go, #1594): si esa lista cambia, ésta cambia con ella
117
+ // (colibird-ai/lmcp-npm#29). En mayúsculas; la comparación no distingue mayúsculas.
118
+ const UUIDS_DE_HARDWARE_BASURA = new Set([
119
+ 'NIL', // lo que imprime PowerShell cuando WMI no tiene nada que dar
120
+ '00000000-0000-0000-0000-000000000000',
121
+ 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF',
122
+ '03000200-0400-0500-0006-000700080009', // el default clásico de OEM/BIOS
123
+ ])
124
+ // Tope de cada llamada del probe (#42, O1). execSync bloquea el event loop: con WMI colgado, el
125
+ // proceso suelto del evento de lanzamiento no llegaba nunca a su propio tope de 4 s y quedaba vivo,
126
+ // oculto, para siempre. Con el tope, una llamada colgada se mata y el probe sigue (o termina vacío).
127
+ const TOPE_PROBE_MS = 2000
128
+ function _esIdDeHardwareValido(id) {
129
+ return !!id && !UUIDS_DE_HARDWARE_BASURA.has(String(id).toUpperCase())
130
+ }
131
+
111
132
  // execImpl is injectable (defaults to the real execSync) so tests can pin the
112
133
  // PowerShell-then-wmic ORDER without a Windows machine — see
113
134
  // npm/test/machine-id-windows-order.test.js.
@@ -119,21 +140,24 @@ function _probeWinHardwareId(execImpl = require('child_process').execSync) {
119
140
  // between npm and the go-server and reintroduced #1544 (0 of 9 Windows
120
141
  // installs joined their heartbeat because the two sides disagreed on id).
121
142
  try {
143
+ // windowsHide: lo corre un proceso sin consola propia (el emisor del evento de lanzamiento,
144
+ // el setup lanzado por un cliente de IA). Sin la opción, Windows puede mostrar una consola; el
145
+ // cliente Go pone HideWindow por lo mismo (#31). Medido sin ventana en 3.0.417; esto la descarta.
122
146
  const ps = execImpl(
123
147
  'powershell -NoProfile -Command "(Get-WmiObject Win32_ComputerSystemProduct).UUID"',
124
- { stdio: 'pipe' }
148
+ { stdio: 'pipe', windowsHide: true, timeout: TOPE_PROBE_MS }
125
149
  ).toString().trim()
126
150
  // PowerShell prints the literal string "nil", not an empty line, when WMI
127
151
  // has no UUID to give — an emptiness check alone would accept it as an id.
128
- if (ps && ps !== 'nil') {
152
+ if (_esIdDeHardwareValido(ps)) {
129
153
  _winHardwareIdCache = ps
130
154
  return _winHardwareIdCache
131
155
  }
132
156
  } catch {}
133
157
  try {
134
- const wmic = execImpl('wmic csproduct get UUID /value', { stdio: 'pipe' }).toString()
158
+ const wmic = execImpl('wmic csproduct get UUID /value', { stdio: 'pipe', windowsHide: true, timeout: TOPE_PROBE_MS }).toString()
135
159
  const match = wmic.match(/UUID=(.+)/)
136
- if (match) {
160
+ if (match && _esIdDeHardwareValido(match[1].trim())) {
137
161
  _winHardwareIdCache = match[1].trim()
138
162
  return _winHardwareIdCache
139
163
  }
@@ -154,6 +178,19 @@ function _resetWinHardwareIdCacheForTests() {
154
178
  // que invertir la prioridad hardware-antes-que-cache —la decision irreversible de
155
179
  // machineid.go— los pasaba a todos en verde. Sin argumentos se comporta igual que
156
180
  // antes; ningun caller de produccion pasa deps.
181
+ // The machine_id the macOS tray persisted in its config.json, or '' when there is none yet.
182
+ function _readTrayMachineId(deps = {}) {
183
+ const home = deps.homeDir || require('os').homedir()
184
+ const read = deps.readFileImpl || ((p) => require('fs').readFileSync(p, 'utf8'))
185
+ try {
186
+ const cfg = JSON.parse(read(require('path').join(home, 'Library', 'Application Support', 'Local MCP', 'config.json')))
187
+ const id = typeof cfg.machine_id === 'string' ? cfg.machine_id.trim() : ''
188
+ return id
189
+ } catch {
190
+ return ''
191
+ }
192
+ }
193
+
157
194
  function _getMachineId(deps = {}) {
158
195
  const platform = deps.platform || process.platform
159
196
  const cacheDir = deps.cacheDir || CACHE_DIR
@@ -165,6 +202,13 @@ function _getMachineId(deps = {}) {
165
202
 
166
203
  const execSync = deps.execImpl || require('child_process').execSync
167
204
  if (platform === 'darwin') {
205
+ // QA 3.0.418 (Dario, m4): the tray's own id lives in its config.json (it read the keychain
206
+ // when IT could). An npm run in a context without keychain access (an agent or SSH session)
207
+ // fell to the hardware UUID and minted the cloud token for a SECOND identity of the same Mac,
208
+ // so the server refused that Mac's device key, its machine approval and the agent mesh.
209
+ // The id the tray already uses comes first.
210
+ const trayId = _readTrayMachineId(deps)
211
+ if (trayId) return trayId
168
212
  try {
169
213
  const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
170
214
  const id = r.toString().trim()
@@ -178,7 +222,7 @@ function _getMachineId(deps = {}) {
178
222
  } else if (platform === 'win32') {
179
223
  const hw = _probeWinHardwareId(execSync)
180
224
  if (hw) {
181
- _writeWinMachineIdCache(cacheDir, hw)
225
+ if (!deps.soloLectura) _writeWinMachineIdCache(cacheDir, hw)
182
226
  return hw
183
227
  }
184
228
  // Both probes failed or are blocked — fall back to whatever the go-server
@@ -193,6 +237,10 @@ function _getMachineId(deps = {}) {
193
237
  if (mid) return mid
194
238
  } catch {}
195
239
  }
240
+ // El evento de lanzamiento (launch-event.js) pide soloLectura: no escribe el cache de arriba —
241
+ // en una máquina desinstalada recrearía el directorio que el usuario borró — y prefiere no
242
+ // tener id antes que uno derivado del hostname.
243
+ if (deps.soloLectura) return ''
196
244
  // Fallback: hash hostname instead of sending raw PII
197
245
  const crypto = require('crypto')
198
246
  return crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16)
@@ -247,12 +295,20 @@ async function getLatestBinary() {
247
295
  let data = ''
248
296
  res.on('data', chunk => data += chunk)
249
297
  res.on('end', () => {
298
+ let info
250
299
  try {
251
- const info = JSON.parse(data)
252
- resolve(info)
300
+ info = JSON.parse(data)
253
301
  } catch (e) {
254
- reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
302
+ return reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
303
+ }
304
+ // Un error del API también es JSON (`{"detail": …}` en un 5xx o un 429). Aceptarlo dejaba
305
+ // `version` en undefined y los co-procesos se pedían como `teams-proxy-undefined-…`: 25
306
+ // pedidos en un día de API caído (#38). Sin un 200 con versión no hay respuesta, y todo
307
+ // el que llama ya trata un rechazo como «no disponible».
308
+ if (res.statusCode !== 200 || !info || !/^[0-9][0-9A-Za-z.+-]*$/.test(String(info.version || ''))) {
309
+ return reject(new Error(`/runtime/latest no dio una versión (HTTP ${res.statusCode})`))
255
310
  }
311
+ resolve(info)
256
312
  })
257
313
  })
258
314
  req.on('error', reject)
@@ -331,6 +387,10 @@ async function downloadFile(url, destPath) {
331
387
  * @returns {Promise<{binPath: string}>}
332
388
  */
333
389
  async function ensureBinary(deps = {}) {
390
+ // Linux no está soportado (decisión de Dario, 2026-10-02; colibird-ai/lmcp-npm#9). Se dice
391
+ // ANTES de pedir nada: ni /runtime/latest ni un tarball. Anuncie lo que anuncie el API — llegó a
392
+ // anunciar URLs `linux_*` que daban 404, y la guarda vieja (sólo «si no hay URL») no saltaba.
393
+ if ((deps.platform || process.platform) === 'linux') throw new Error(LINUX_NO_SOPORTADO)
334
394
  // Seam for tests (same idiom as ensureTray's `deps`, #2654): lets a test pin the
335
395
  // platform and stub the network without touching process.platform or real HOME.
336
396
  const {
@@ -361,17 +421,6 @@ async function ensureBinary(deps = {}) {
361
421
  const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
362
422
  const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
363
423
 
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
-
375
424
  let url, platformVersion
376
425
  if (publishedUrl) {
377
426
  platformVersion = versionFromArtifactUrl(publishedUrl, arch)
@@ -871,8 +920,8 @@ async function ensureSlackProxy(platform = process.platform) {
871
920
  module.exports = {
872
921
  ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl,
873
922
  latestVersionForPlatform, getArch,
874
- CACHE_DIR, TRAY_DIR, _getMachineId,
875
- _probeWinHardwareId, _resetWinHardwareIdCacheForTests,
923
+ CACHE_DIR, TRAY_DIR, _getMachineId, LINUX_NO_SOPORTADO,
924
+ _probeWinHardwareId, _resetWinHardwareIdCacheForTests, UUIDS_DE_HARDWARE_BASURA, _esIdDeHardwareValido,
876
925
  _readWinMachineIdCache, _writeWinMachineIdCache, WIN_MACHINE_ID_CACHE_NAME,
877
926
  _retireSupersededTrays, _isUsableTrayBundle, SYSTEM_TRAY_APP,
878
927
  _bundleVersion, _isTrayRunningFrom, _versionIsNewer,
package/index.js CHANGED
@@ -17,6 +17,7 @@ const path = require('path')
17
17
  const os = require('os')
18
18
  const fs = require('fs')
19
19
  const https = require('https')
20
+ const evento = require('./launch-event')
20
21
 
21
22
  // ── Version + liveness helpers (module scope so they are testable) ────────────
22
23
 
@@ -232,12 +233,31 @@ function clearUninstallSentinels() {
232
233
  for (const p of uninstallSentinelPaths()) { try { fs.unlinkSync(p) } catch {} }
233
234
  }
234
235
 
235
- // Platform support: macOS (Swift tray + embedded server), Windows & Linux
236
- // (go-server standalone binary downloaded from R2). Re-enabled for the
237
- // Windows/Linux public re-launch — the waitlist gate is retired.
236
+ const cmd = process.argv[2]
237
+
238
+ // Comandos que este wrapper entiende. Sin argumentos arranca el server MCP por stdio, que es
239
+ // como lo lanzan los clientes de IA — ese camino NO cambia.
240
+ const COMANDOS = ['setup', 'update', 'uninstall', 'status']
241
+ const PEDIDOS_DE_AYUDA = ['--help', '-h', 'help']
242
+
243
+ // Evento de lanzamiento (launch-event.js, #11): cada lanzamiento del camino del server reporta
244
+ // UNA vez cómo terminó. Un lanzamiento es: sin argumento, `stdio`, o un argumento desconocido.
245
+ // Los comandos y la ayuda no lo son y no reportan. El `exit` de abajo es la red: una salida de
246
+ // ese camino que nadie nombró —una excepción incluida— sale como `other`, nunca en silencio.
247
+ const ES_LANZAMIENTO = require.main === module && !COMANDOS.includes(cmd) && !PEDIDOS_DE_AYUDA.includes(cmd)
248
+ function reportarLanzamiento(outcome, opciones) {
249
+ if (ES_LANZAMIENTO) evento.reportar(outcome, evento.claseDeArgumento(cmd), opciones)
250
+ }
251
+ // Antes que nada: un `spawned` que en Windows esperaba su id sale ahora, por el proceso suelto (#21).
252
+ if (ES_LANZAMIENTO) process.on('exit', () => { evento.soltarPendiente(); reportarLanzamiento('other') })
253
+
254
+ // Platform support: macOS (Swift tray + embedded server) and Windows (go-server standalone
255
+ // binary downloaded from R2). Linux passes this guard so that `--help`, `status`, `update` and
256
+ // `uninstall` answer, but it is NOT supported: the server path and `setup` say so (#9).
238
257
  const SUPPORTED_PLATFORMS = ['darwin', 'win32', 'linux']
239
258
  if (!SUPPORTED_PLATFORMS.includes(process.platform)) {
240
- console.error(`LMCP supports macOS, Windows and Linux. Your platform (${process.platform}) is not supported yet — see https://local-mcp.com`)
259
+ console.error(`LMCP supports macOS and Windows. Your platform (${process.platform}) is not supported yet — see https://local-mcp.com`)
260
+ reportarLanzamiento('unsupported-platform')
241
261
  process.exit(1)
242
262
  }
243
263
 
@@ -250,13 +270,12 @@ if (parseInt(process.version.slice(1)) < 16) {
250
270
  `If you use nvm, run: nvm alias default 22 && nvm uninstall ${process.version}\n` +
251
271
  'Then restart Claude Desktop / Cursor.'
252
272
  process.stderr.write(msg + '\n')
273
+ reportarLanzamiento('unsupported-node')
253
274
  // Exit with a non-zero code so the MCP host marks the server as failed
254
275
  // (vs hanging forever with no output).
255
276
  process.exit(1)
256
277
  }
257
278
 
258
- const cmd = process.argv[2]
259
-
260
279
  // Detect a MANUAL uninstall — the user dragged LocalMCPTray.app to the Trash (by far the
261
280
  // most common way to remove a Mac app) instead of running `npx local-mcp uninstall` or the
262
281
  // tray's Uninstall button, so no `.uninstalled` sentinel was written. Signature: the tray
@@ -279,15 +298,18 @@ function detectManualUninstall() {
279
298
  )
280
299
  }
281
300
 
282
- // Comandos que este wrapper entiende. Sin argumentos arranca el server MCP por stdio, que es
283
- // como lo lanzan los clientes de IA — ese camino NO cambia.
284
- const COMANDOS = ['setup', 'update', 'uninstall', 'status']
301
+ // `stdio` no es un comando: es el modo por defecto dicho en voz alta. Las guías públicas
302
+ // mandaron a escribir `"args": ["-y", "local-mcp@latest", "stdio"]` en la config manual de un
303
+ // cliente de IA, y esas configs siguen en máquinas de usuarios. Tratarlo como desconocido
304
+ // (3.0.413 a 3.0.416) las rompió: ayuda por stdout y exit 2 donde el cliente espera JSON-RPC.
305
+ const SINONIMOS_DEL_SERVER = ['stdio']
285
306
 
286
307
  function mostrarAyuda() {
287
308
  console.log(`local-mcp — Local MCP Server
288
309
 
289
310
  Uso:
290
311
  local-mcp arranca el server MCP por stdio (lo que hace un cliente de IA)
312
+ local-mcp stdio lo mismo, dicho explícitamente
291
313
  local-mcp setup instala y configura
292
314
  local-mcp status muestra el estado
293
315
  local-mcp update limpia el cache del runtime
@@ -303,11 +325,12 @@ async function main() {
303
325
  // sólo buscaba ayuda de CLI. Un comando de consulta no instala software.
304
326
  //
305
327
  // Sin argumentos sigue arrancando el server, que es como lo invocan los clientes de IA.
306
- if (cmd !== undefined && !COMANDOS.includes(cmd)) {
307
- const pedidoDeAyuda = ['--help', '-h', 'help'].includes(cmd)
328
+ if (cmd !== undefined && !COMANDOS.includes(cmd) && !SINONIMOS_DEL_SERVER.includes(cmd)) {
329
+ const pedidoDeAyuda = PEDIDOS_DE_AYUDA.includes(cmd)
308
330
  mostrarAyuda()
309
331
  if (!pedidoDeAyuda) {
310
332
  console.error(`\nNo conozco el comando "${cmd}".`)
333
+ reportarLanzamiento('rejected-argument')
311
334
  }
312
335
  process.exit(pedidoDeAyuda ? 0 : 2)
313
336
  }
@@ -317,7 +340,8 @@ async function main() {
317
340
  clearUninstallSentinels()
318
341
  const { runSetup } = require('./setup')
319
342
  await runSetup()
320
- process.exit(0)
343
+ // En Linux el setup no instaló nada (#9): quien lo encadena en un script tiene que verlo.
344
+ process.exit(process.platform === 'linux' ? 1 : 0)
321
345
  }
322
346
 
323
347
  if (cmd === 'update') {
@@ -381,7 +405,6 @@ async function main() {
381
405
  try {
382
406
  const s = JSON.parse(data)
383
407
  console.log(`LMCP v${s.version} — ${s.server}`)
384
- console.log(`Licencia: ${s.license?.status} (${s.license?.days_left}d)`)
385
408
  } catch { console.log(data) }
386
409
  })
387
410
  }).on('error', () => console.log('Servidor no disponible en localhost:8765'))
@@ -402,6 +425,7 @@ async function main() {
402
425
  // runtime-dir and durable App Support copies (see uninstallSentinelPaths).
403
426
  {
404
427
  if (anyUninstallSentinel()) {
428
+ reportarLanzamiento('uninstalled')
405
429
  process.stderr.write(
406
430
  '[LMCP] Local MCP was uninstalled — not starting.\n' +
407
431
  ' Reinstall: npx local-mcp setup\n' +
@@ -421,6 +445,7 @@ async function main() {
421
445
  // and we short-circuit here, which is harmless.
422
446
  {
423
447
  if (detectManualUninstall()) {
448
+ reportarLanzamiento('uninstalled')
424
449
  writeUninstallSentinels()
425
450
  const trayPlist = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.local-mcp.tray.plist')
426
451
  if (fs.existsSync(trayPlist)) {
@@ -435,6 +460,15 @@ async function main() {
435
460
  }
436
461
  }
437
462
 
463
+ // ── Linux no está soportado (#9) ────────────────────────────────────────────
464
+ // Se dice acá, antes de cualquier descarga: sin esto el wrapper pedía un binario que no se
465
+ // publica y terminaba en un 404. Va después de los chequeos de desinstalación, que no bajan nada.
466
+ if (process.platform === 'linux') {
467
+ process.stderr.write(require('./download').LINUX_NO_SOPORTADO + '\n')
468
+ reportarLanzamiento('unsupported-platform')
469
+ process.exit(1)
470
+ }
471
+
438
472
  // ── Modo default: stdio MCP server ──────────────────────────────────────────
439
473
  const { CACHE_DIR } = require('./download')
440
474
  // macOS ships the Swift `local-mcp-server` (standalone fallback to the embedded
@@ -526,7 +560,10 @@ async function main() {
526
560
  try { _pkgVer = require('./package.json').version || '' } catch {}
527
561
  const child = spawn(binaryPath, spawnArgs, { stdio: 'inherit', env: { ...process.env, LMCP_NPM_VERSION: _pkgVer } })
528
562
  noteSpawnedServer(child.pid)
563
+ // El server ya arrancó y este proceso queda vivo toda la sesión: el evento sale desde acá.
564
+ child.on('spawn', () => reportarLanzamiento('spawned', { enProceso: true }))
529
565
  child.on('error', (err) => {
566
+ reportarLanzamiento('spawn-failed')
530
567
  process.stderr.write(`Error ejecutando LMCP: ${err.message}\n`)
531
568
  process.exit(1)
532
569
  })
@@ -667,6 +704,7 @@ async function main() {
667
704
  try {
668
705
  runtime = await ensureRuntime()
669
706
  } catch (err) {
707
+ reportarLanzamiento('download-failed')
670
708
  process.stderr.write(`\nError al preparar el runtime de LMCP:\n${err.message}\n`)
671
709
  process.stderr.write(`Intentá reinstalar: npx local-mcp update\n\n`)
672
710
  process.exit(1)
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ // launch-event.js — el evento de lanzamiento del wrapper (colibird-ai/lmcp-npm#11, parte C).
4
+ //
5
+ // Por qué existe: de 3.0.413 a 3.0.416 el wrapper rechazó `stdio` y ninguna config manual
6
+ // conectaba. Nadie lo vio en diez días: el wrapper salía antes de que existiera telemetría.
7
+ //
8
+ // Contrato: contracts/launch-telemetry.v1.json de lmcp-api (copia fijada en npm/contracts/, que
9
+ // NO se publica: por eso los conjuntos están acá como constantes, y un test los compara).
10
+ //
11
+ // Corre en cada arranque de cada cliente de IA. Lo que no puede pasar nunca:
12
+ // · escribir en stdout — es el protocolo;
13
+ // · demorar el arranque del server o una salida — sin reintentos, timeout corto, todo error
14
+ // tragado;
15
+ // · mandar algo que no sea uno de los cinco campos: ni el argumento, ni rutas, ni entorno.
16
+ const https = require('https')
17
+ const cp = require('child_process')
18
+ const DESTINO = { hostname: 'office-mcp-production.up.railway.app', path: '/launch-event' }
19
+ const PLATAFORMAS = ['darwin', 'win32', 'linux', 'other']
20
+ const CLASES = ['none', 'stdio', 'unknown']
21
+ const OUTCOMES = ['spawned', 'rejected-argument', 'unsupported-node', 'unsupported-platform',
22
+ 'download-failed', 'spawn-failed', 'uninstalled', 'other']
23
+ const RE_VERSION = /^\d{1,3}\.\d{1,3}\.\d{1,5}$/
24
+ const RE_MAQUINA = /^[A-Za-z0-9._:{}-]{1,64}$/
25
+ const [TIMEOUT_MS, TOPE_MS] = [3000, 4000] // el pedido · la vida del proceso suelto
26
+ const TOPE_ID_MS = 2000 // Windows: cuánto puede esperar el `spawned` al id de hardware (#21)
27
+
28
+ // Del argumento sólo viaja su clase. `stdio` es el único que se nombra: es el del incidente.
29
+ function claseDeArgumento (cmd) {
30
+ if (cmd === undefined) return 'none'
31
+ return cmd === 'stdio' ? 'stdio' : 'unknown'
32
+ }
33
+
34
+ // El heartbeat no tiene compuerta de consentimiento (ver #11). Este evento respeta además el
35
+ // único interruptor que ya existe en el server de Mac.
36
+ function apagado (env = process.env) {
37
+ return ['1', 'true', 'yes'].includes(String(env.LMCP_DISABLE_TELEMETRY || '').toLowerCase())
38
+ }
39
+
40
+ // El id que usa el heartbeat, sin escribir nada y sin el fallback del hostname. Lo que no
41
+ // calza con el patrón del contrato (una ruta en LMCP_MACHINE_ID, por ejemplo) no viaja.
42
+ function idDeMaquina () {
43
+ try {
44
+ const id = require('./download')._getMachineId({ soloLectura: true })
45
+ return RE_MAQUINA.test(id) ? id : undefined
46
+ } catch { return undefined }
47
+ }
48
+
49
+ // null = no se manda. Sólo salen valores de los conjuntos del contrato.
50
+ function cuerpo (outcome, clase, deps = {}) {
51
+ const version = 'version' in deps ? deps.version : require('./package.json').version
52
+ const plataforma = deps.platform || process.platform
53
+ if (!OUTCOMES.includes(outcome) || !CLASES.includes(clase) || !RE_VERSION.test(version)) return null
54
+ const c = {
55
+ package_version: version,
56
+ platform: PLATAFORMAS.includes(plataforma) ? plataforma : 'other',
57
+ arg_class: clase,
58
+ outcome
59
+ }
60
+ const id = 'machineId' in deps ? deps.machineId : idDeMaquina()
61
+ if (typeof id === 'string' && RE_MAQUINA.test(id)) c.machine_id = id
62
+ return c
63
+ }
64
+
65
+ // Un POST, sin reintento, respuesta ignorada. `soltar` es para el pedido que sale desde el
66
+ // wrapper vivo: el socket no retiene al proceso.
67
+ function enviar (c, { soltar = false } = {}) {
68
+ try {
69
+ const datos = JSON.stringify(c)
70
+ const req = https.request({
71
+ ...DESTINO,
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(datos) },
74
+ timeout: TIMEOUT_MS
75
+ }, (res) => res.resume())
76
+ req.on('error', () => {})
77
+ req.on('timeout', () => { try { req.destroy() } catch {} })
78
+ if (soltar) req.on('socket', (s) => { try { s.unref() } catch {} })
79
+ req.end(datos)
80
+ } catch {}
81
+ }
82
+
83
+ // Windows, sin bloquear (#21): el mismo id que manda el heartbeat del cliente Go
84
+ // (lmcp-win internal/machineid/probe_windows.go: PowerShell, después wmic, TrimSpace, sin cambiar
85
+ // mayúsculas, sin los ids basura), por execFile y sin shell. Llama a `listo` UNA vez: con el id, o
86
+ // con undefined si los dos fallan, devuelven basura o pasa el tope. Del cache y del hash del
87
+ // hostname del cliente Go no se toma nada: un id rechazado no viaja.
88
+ // Lo que corre después de que reportar() volvió (#42, O2/O3/O5): pasado el tope no se lanza otro
89
+ // probe; el probe vivo se mata al terminar y cuando el wrapper sale (matarProbe); y ningún
90
+ // callback puede tirar, porque una excepción acá mataría al wrapper y con él la sesión MCP.
91
+ let probeVivo = null
92
+ let cancelarProbe = () => {}
93
+ function matarProbe () {
94
+ const p = probeVivo
95
+ probeVivo = null
96
+ try { if (p) p.kill() } catch {}
97
+ }
98
+ function idDeHardwareWindows (listo) {
99
+ let hecho = false
100
+ const fin = (id) => {
101
+ if (hecho) return
102
+ hecho = true
103
+ clearTimeout(tope)
104
+ matarProbe()
105
+ try { listo(id) } catch {}
106
+ }
107
+ const tope = setTimeout(() => fin(undefined), TOPE_ID_MS)
108
+ cancelarProbe = () => { hecho = true; clearTimeout(tope); matarProbe() }
109
+ const opciones = { windowsHide: true, timeout: TOPE_ID_MS }
110
+ const correr = (cmd, args, alResponder) => {
111
+ if (hecho) return
112
+ try {
113
+ probeVivo = cp.execFile(cmd, args, opciones, (err, out) => {
114
+ probeVivo = null
115
+ try { alResponder(err ? '' : String(out)) } catch { fin(undefined) }
116
+ })
117
+ } catch { fin(undefined) }
118
+ }
119
+ try {
120
+ const { _esIdDeHardwareValido: valido } = require('./download')
121
+ correr('powershell', ['-NoProfile', '-Command', '(Get-WmiObject Win32_ComputerSystemProduct).UUID'], (out) => {
122
+ const ps = out.trim()
123
+ if (valido(ps)) return fin(ps)
124
+ correr('wmic', ['csproduct', 'get', 'UUID', '/value'], (out2) => {
125
+ const m = out2.match(/UUID=(.+)/)
126
+ const id = m ? m[1].trim() : ''
127
+ fin(valido(id) ? id : undefined)
128
+ })
129
+ })
130
+ } catch { fin(undefined) }
131
+ }
132
+
133
+ // Un evento por lanzamiento: la primera llamada decide, las demás no hacen nada.
134
+ // enProceso el server ya arrancó y el wrapper queda vivo toda la sesión (`spawned`): el
135
+ // pedido sale desde acá, sin proceso extra.
136
+ // si no el wrapper está por salir: un proceso suelto manda el evento y el wrapper no
137
+ // lo espera. Sin stdout heredado y, en Windows, sin ventana de consola.
138
+ // En Windows sin LMCP_MACHINE_ID, el `spawned` espera al id sin bloquear (idDeHardwareWindows);
139
+ // si el wrapper sale antes, soltarPendiente() lo manda por el proceso suelto, que busca el id solo.
140
+ // Una persona en una terminal (stdin TTY) no es un cliente de IA lanzando el server: no reporta.
141
+ let reportado = false
142
+ let pendiente = null
143
+ function reportar (outcome, clase, { enProceso = false } = {}) {
144
+ if (reportado) return
145
+ reportado = true
146
+ try {
147
+ if (apagado() || (process.stdin && process.stdin.isTTY)) return
148
+ if (!OUTCOMES.includes(outcome) || !CLASES.includes(clase)) return
149
+ if (enProceso) {
150
+ const idEntorno = (process.env.LMCP_MACHINE_ID || '').trim()
151
+ if (process.platform === 'win32' && !idEntorno) {
152
+ pendiente = { outcome, clase }
153
+ idDeHardwareWindows((id) => {
154
+ if (!pendiente) return // ya salió por el proceso suelto
155
+ pendiente = null
156
+ const c = cuerpo(outcome, clase, { machineId: id })
157
+ if (c) enviar(c, { soltar: true })
158
+ })
159
+ return
160
+ }
161
+ const c = cuerpo(outcome, clase, process.platform === 'win32' ? { machineId: idEntorno } : {})
162
+ if (c) enviar(c, { soltar: true })
163
+ return
164
+ }
165
+ lanzarSuelto(outcome, clase)
166
+ } catch {}
167
+ }
168
+
169
+ function lanzarSuelto (outcome, clase) {
170
+ const hijo = cp.spawn(process.execPath, [__filename, outcome, clase],
171
+ { detached: true, stdio: 'ignore', windowsHide: true })
172
+ hijo.on('error', () => {})
173
+ hijo.unref()
174
+ }
175
+
176
+ // El wrapper está por salir con el `spawned` todavía esperando su id: sale por el proceso suelto,
177
+ // y el probe que seguía vivo se mata (el timeout de execFile vive en el wrapper y se va con él).
178
+ function soltarPendiente () {
179
+ cancelarProbe()
180
+ if (!pendiente) return
181
+ const { outcome, clase } = pendiente
182
+ pendiente = null
183
+ try { lanzarSuelto(outcome, clase) } catch {}
184
+ }
185
+
186
+ // Como programa es el proceso suelto: `node launch-event.js <outcome> <clase>`.
187
+ if (require.main === module) {
188
+ setTimeout(() => process.exit(0), TOPE_MS).unref()
189
+ const c = apagado() ? null : cuerpo(process.argv[2], process.argv[3])
190
+ if (c) enviar(c)
191
+ }
192
+
193
+ module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RE_VERSION, RE_MAQUINA, TOPE_ID_MS, claseDeArgumento, apagado, cuerpo, reportar, soltarPendiente,
194
+ _idDeHardwareWindows: idDeHardwareWindows } // costura de test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.416",
3
+ "version": "3.0.418",
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": {
@@ -14,6 +14,7 @@
14
14
  "index.js",
15
15
  "download.js",
16
16
  "setup.js",
17
+ "launch-event.js",
17
18
  "postinstall.js",
18
19
  "README.md"
19
20
  ],
@@ -68,10 +69,10 @@
68
69
  "homepage": "https://local-mcp.com",
69
70
  "repository": {
70
71
  "type": "git",
71
- "url": "https://github.com/lanchuske/local-mcp-releases.git"
72
+ "url": "https://github.com/colibird-ai/local-mcp-releases.git"
72
73
  },
73
74
  "bugs": {
74
- "url": "https://github.com/lanchuske/local-mcp-releases/issues"
75
+ "url": "https://github.com/colibird-ai/local-mcp-releases/issues"
75
76
  },
76
77
  "mcpName": "com.local-mcp/local-mcp"
77
78
  }
package/setup.js CHANGED
@@ -107,7 +107,7 @@ function _localMcpConfigDir(platform = process.platform, home = HOME, appData =
107
107
 
108
108
  function _cmdExists(cmd) {
109
109
  const which = _IS_WIN ? 'where' : 'which'
110
- try { execSync(`${which} ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
110
+ try { execSync(`${which} ${cmd}`, { stdio: 'pipe', windowsHide: true }); return true } catch { return false }
111
111
  }
112
112
 
113
113
  // ── Safe config read ─────────────────────────────────────────────────────────
@@ -237,11 +237,12 @@ function _launcherArgsForConfigureClients(platform, command, args) {
237
237
  return platform === 'darwin' ? { command, args } : { command: null, args: [] }
238
238
  }
239
239
 
240
- // §3.8 — Linux's fixed result shape: no capability, no report, exit 0 (not
241
- // having a writer yet is not a failure). Extracted so the contract itself is
242
- // directly testable instead of only reachable by reading runSetup's source.
240
+ // §3.8 — Linux's fixed result shape: no capability, no report, exit 1. It said exit 0 while
241
+ // Linux was "no writer yet"; since #9 Linux is not supported, and a setup that installed nothing
242
+ // is a failed setup: a script that chains on it has to see it. Extracted so the contract itself
243
+ // is directly testable instead of only reachable by reading runSetup's source.
243
244
  function _notSupportedOnPlatformResult() {
244
- return { runtime: 'not_supported_on_platform', exitCode: 0, report: null }
245
+ return { runtime: 'not_supported_on_platform', exitCode: 1, report: null }
245
246
  }
246
247
 
247
248
  // Corre `--configure-clients --json`. Nunca cuelga el instalador: un binario que no responde
@@ -253,7 +254,9 @@ function runConfigureClients(bin, command, args, opts = {}) {
253
254
  if (!bin) return { runtime: 'no_runtime', exitCode: 2, report: null }
254
255
  const argv = ['--configure-clients', '--json']
255
256
  if (command) argv.push('--command=' + command, ...(args || []).map(a => '--arg=' + a))
256
- const r = spawn(bin, argv, { input: '', timeout: timeoutMs, encoding: 'utf8' })
257
+ // windowsHide en los tres procesos que setup arranca en Windows (#31): todos capturan su
258
+ // salida, ninguno es para mirar, y setup puede correr sin consola (un agente, postinstall).
259
+ const r = spawn(bin, argv, { input: '', timeout: timeoutMs, encoding: 'utf8', windowsHide: true })
257
260
  if (r.error && r.error.code === 'ETIMEDOUT') return { runtime: 'timeout', exitCode: 2, report: null }
258
261
  if (r.error) return { runtime: 'failed', reason: r.error.message, exitCode: 2, report: null }
259
262
  let report = null
@@ -296,10 +299,27 @@ async function runSetup(opts = {}) {
296
299
  // #3212 — `--json`: el reporte estructurado va solo por stdout; todo lo demás, a stderr.
297
300
  const jsonMode = !!opts.json || process.argv.includes('--json')
298
301
  if (jsonMode) console.log = (...a) => { process.stderr.write(a.join(' ') + '\n') }
299
- // Cross-platform: macOS configures the Swift tray + embedded server; Windows/Linux
300
- // download the go-server standalone binary (download.js) and write the MCP client
301
- // config (the config writer already handles Windows paths). The Mac-only waitlist
302
- // gate was retired for the Windows/Linux public re-launch.
302
+ // Linux no está soportado (#9): se dice y no se toca nada — sin descarga, sin launcher, sin
303
+ // eventos de instalación ni heartbeat (antes terminaba en «LMCP is installed and running»
304
+ // después de un 404). El reporte --json conserva su forma (§3.8); el código de salida es 1.
305
+ if (process.platform === 'linux') {
306
+ const sinLinux = _notSupportedOnPlatformResult()
307
+ console.log('\n ' + require('./download').LINUX_NO_SOPORTADO + '\n Nothing was installed.\n')
308
+ if (jsonMode) {
309
+ process.stdout.write(JSON.stringify({
310
+ schema: CLIENTS_REPORT_SCHEMA,
311
+ installer_version: require('./package.json').version,
312
+ runtime: sinLinux.runtime,
313
+ catalog: null,
314
+ clients: [],
315
+ permissions: 'unknown_until_tray_runs',
316
+ }) + '\n')
317
+ }
318
+ process.exitCode = sinLinux.exitCode
319
+ return
320
+ }
321
+ // Cross-platform: macOS configures the Swift tray + embedded server; Windows downloads the
322
+ // go-server standalone binary (download.js) and writes the MCP client config.
303
323
  const forceAll = opts.all || process.argv.includes('--all')
304
324
  const refArg = process.argv.find(a => a.startsWith('--ref='))
305
325
  if (refArg) process.env.LMCP_REF = refArg.split('=')[1]
@@ -469,16 +489,6 @@ async function runSetup(opts = {}) {
469
489
  console.log(' npx local-mcp setup\n')
470
490
  console.log(' Full setup guide: https://local-mcp.com/setup')
471
491
  }
472
- } else {
473
- // Linux (§3.8): no runtime-native config writer exists yet. Say so plainly,
474
- // point at the manual guide, and touch nothing — no per-client guessing,
475
- // no files written, no client identity hardcoded here.
476
- clientsRun = _notSupportedOnPlatformResult()
477
- _trackSetupStep('detect', 'not_supported_on_platform', '', { npmVersion: _npmPkgVersion })
478
- console.log('\n LMCP is installed and running.')
479
- console.log(' Automatic AI-client configuration is not available on Linux yet.')
480
- console.log(' Add LMCP to your AI client\'s MCP config manually — see:')
481
- console.log(' https://local-mcp.com/setup\n')
482
492
  }
483
493
 
484
494
  const cfgDir = _localMcpConfigDir()
@@ -613,8 +623,8 @@ async function runSetup(opts = {}) {
613
623
  // Anon-first cloud relay activation (replaces the interactive email prompt
614
624
  // removed 2026-04-19). When no email is set, register an anonymous tunnel
615
625
  // token keyed on machine_id so cloud relay comes online immediately. Users
616
- // can attach an email later via the tray or /settings to unlock cross-Mac
617
- // sync + paid-license features. Non-fatal on failure — the install still
626
+ // can attach an email later via the tray or /settings for cross-Mac
627
+ // sync. Non-fatal on failure — the install still
618
628
  // succeeds and the server can retry at startup.
619
629
  // Provisioning is ALWAYS anonymous, with or without an email — same change made in install.sh
620
630
  // (#1083). Two separate ways this used to be skipped, and both left the machine with NO
@@ -678,7 +688,7 @@ async function runSetup(opts = {}) {
678
688
 
679
689
  // #3212 — el reporte para quien instaló sin mirar (un agente): qué quedó hecho y qué espera
680
690
  // a la persona. Los permisos, sólo si los midió el tray. clientsRun is always set by this
681
- // point — darwin/win32 from runConfigureClients, linux from its own not_supported sentinel.
691
+ // point — darwin/win32 from runConfigureClients (linux returned at the top, #9).
682
692
  if (jsonMode) {
683
693
  const rep = clientsRun.report || {}
684
694
  const out = {
@@ -747,6 +757,7 @@ function _runHealthCheck() {
747
757
  const result = execFileSync(binPath, args, {
748
758
  timeout: 10000,
749
759
  stdio: ['pipe', 'pipe', 'pipe'],
760
+ windowsHide: true,
750
761
  env: { ...process.env, HOME },
751
762
  })
752
763
  const output = result.toString().trim()
@@ -1183,6 +1194,7 @@ async function _autoLaunchClient(clientsRun, opts = {}) {
1183
1194
 
1184
1195
  module.exports = {
1185
1196
  runSetup, runConfigureClients, _findServerBinary, _launcherArgsForConfigureClients,
1197
+ _cmdExists, _runHealthCheck,
1186
1198
  CLIENTS_REPORT_SCHEMA,
1187
1199
  _notSupportedOnPlatformResult,
1188
1200
  _launcherSpawnSpec, _writeLaunchScript, _writeLaunchSpec,