local-mcp 3.0.417 → 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/download.js CHANGED
@@ -109,6 +109,26 @@ function _writeWinMachineIdCache(cacheDir, id) {
109
109
  // memoized too, on purpose: a run where PowerShell is blocked shouldn't retry
110
110
  // it on every subsequent call in the same process.
111
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
+
112
132
  // execImpl is injectable (defaults to the real execSync) so tests can pin the
113
133
  // PowerShell-then-wmic ORDER without a Windows machine — see
114
134
  // npm/test/machine-id-windows-order.test.js.
@@ -120,21 +140,24 @@ function _probeWinHardwareId(execImpl = require('child_process').execSync) {
120
140
  // between npm and the go-server and reintroduced #1544 (0 of 9 Windows
121
141
  // installs joined their heartbeat because the two sides disagreed on id).
122
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.
123
146
  const ps = execImpl(
124
147
  'powershell -NoProfile -Command "(Get-WmiObject Win32_ComputerSystemProduct).UUID"',
125
- { stdio: 'pipe' }
148
+ { stdio: 'pipe', windowsHide: true, timeout: TOPE_PROBE_MS }
126
149
  ).toString().trim()
127
150
  // PowerShell prints the literal string "nil", not an empty line, when WMI
128
151
  // has no UUID to give — an emptiness check alone would accept it as an id.
129
- if (ps && ps !== 'nil') {
152
+ if (_esIdDeHardwareValido(ps)) {
130
153
  _winHardwareIdCache = ps
131
154
  return _winHardwareIdCache
132
155
  }
133
156
  } catch {}
134
157
  try {
135
- 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()
136
159
  const match = wmic.match(/UUID=(.+)/)
137
- if (match) {
160
+ if (match && _esIdDeHardwareValido(match[1].trim())) {
138
161
  _winHardwareIdCache = match[1].trim()
139
162
  return _winHardwareIdCache
140
163
  }
@@ -155,6 +178,19 @@ function _resetWinHardwareIdCacheForTests() {
155
178
  // que invertir la prioridad hardware-antes-que-cache —la decision irreversible de
156
179
  // machineid.go— los pasaba a todos en verde. Sin argumentos se comporta igual que
157
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
+
158
194
  function _getMachineId(deps = {}) {
159
195
  const platform = deps.platform || process.platform
160
196
  const cacheDir = deps.cacheDir || CACHE_DIR
@@ -166,6 +202,13 @@ function _getMachineId(deps = {}) {
166
202
 
167
203
  const execSync = deps.execImpl || require('child_process').execSync
168
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
169
212
  try {
170
213
  const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
171
214
  const id = r.toString().trim()
@@ -252,12 +295,20 @@ async function getLatestBinary() {
252
295
  let data = ''
253
296
  res.on('data', chunk => data += chunk)
254
297
  res.on('end', () => {
298
+ let info
255
299
  try {
256
- const info = JSON.parse(data)
257
- resolve(info)
300
+ info = JSON.parse(data)
258
301
  } catch (e) {
259
- 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})`))
260
310
  }
311
+ resolve(info)
261
312
  })
262
313
  })
263
314
  req.on('error', reject)
@@ -870,7 +921,7 @@ module.exports = {
870
921
  ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl,
871
922
  latestVersionForPlatform, getArch,
872
923
  CACHE_DIR, TRAY_DIR, _getMachineId, LINUX_NO_SOPORTADO,
873
- _probeWinHardwareId, _resetWinHardwareIdCacheForTests,
924
+ _probeWinHardwareId, _resetWinHardwareIdCacheForTests, UUIDS_DE_HARDWARE_BASURA, _esIdDeHardwareValido,
874
925
  _readWinMachineIdCache, _writeWinMachineIdCache, WIN_MACHINE_ID_CACHE_NAME,
875
926
  _retireSupersededTrays, _isUsableTrayBundle, SYSTEM_TRAY_APP,
876
927
  _bundleVersion, _isTrayRunningFrom, _versionIsNewer,
package/index.js CHANGED
@@ -248,7 +248,8 @@ const ES_LANZAMIENTO = require.main === module && !COMANDOS.includes(cmd) && !PE
248
248
  function reportarLanzamiento(outcome, opciones) {
249
249
  if (ES_LANZAMIENTO) evento.reportar(outcome, evento.claseDeArgumento(cmd), opciones)
250
250
  }
251
- if (ES_LANZAMIENTO) process.on('exit', () => reportarLanzamiento('other'))
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') })
252
253
 
253
254
  // Platform support: macOS (Swift tray + embedded server) and Windows (go-server standalone
254
255
  // binary downloaded from R2). Linux passes this guard so that `--help`, `status`, `update` and
@@ -404,7 +405,6 @@ async function main() {
404
405
  try {
405
406
  const s = JSON.parse(data)
406
407
  console.log(`LMCP v${s.version} — ${s.server}`)
407
- console.log(`Licencia: ${s.license?.status} (${s.license?.days_left}d)`)
408
408
  } catch { console.log(data) }
409
409
  })
410
410
  }).on('error', () => console.log('Servidor no disponible en localhost:8765'))
package/launch-event.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // tragado;
15
15
  // · mandar algo que no sea uno de los cinco campos: ni el argumento, ni rutas, ni entorno.
16
16
  const https = require('https')
17
- const { spawn } = require('child_process')
17
+ const cp = require('child_process')
18
18
  const DESTINO = { hostname: 'office-mcp-production.up.railway.app', path: '/launch-event' }
19
19
  const PLATAFORMAS = ['darwin', 'win32', 'linux', 'other']
20
20
  const CLASES = ['none', 'stdio', 'unknown']
@@ -23,6 +23,7 @@ const OUTCOMES = ['spawned', 'rejected-argument', 'unsupported-node', 'unsupport
23
23
  const RE_VERSION = /^\d{1,3}\.\d{1,3}\.\d{1,5}$/
24
24
  const RE_MAQUINA = /^[A-Za-z0-9._:{}-]{1,64}$/
25
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)
26
27
 
27
28
  // Del argumento sólo viaja su clase. `stdio` es el único que se nombra: es el del incidente.
28
29
  function claseDeArgumento (cmd) {
@@ -79,13 +80,66 @@ function enviar (c, { soltar = false } = {}) {
79
80
  } catch {}
80
81
  }
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
+
82
133
  // Un evento por lanzamiento: la primera llamada decide, las demás no hacen nada.
83
134
  // enProceso el server ya arrancó y el wrapper queda vivo toda la sesión (`spawned`): el
84
135
  // pedido sale desde acá, sin proceso extra.
85
136
  // si no el wrapper está por salir: un proceso suelto manda el evento y el wrapper no
86
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.
87
140
  // Una persona en una terminal (stdin TTY) no es un cliente de IA lanzando el server: no reporta.
88
141
  let reportado = false
142
+ let pendiente = null
89
143
  function reportar (outcome, clase, { enProceso = false } = {}) {
90
144
  if (reportado) return
91
145
  reportado = true
@@ -93,18 +147,42 @@ function reportar (outcome, clase, { enProceso = false } = {}) {
93
147
  if (apagado() || (process.stdin && process.stdin.isTTY)) return
94
148
  if (!OUTCOMES.includes(outcome) || !CLASES.includes(clase)) return
95
149
  if (enProceso) {
96
- // Windows: el id sale de un PowerShell, que acá no se lanza. Sin id, ese `spawned` no se une a un heartbeat.
97
- const c = cuerpo(outcome, clase, process.platform === 'win32' ? { machineId: process.env.LMCP_MACHINE_ID } : {})
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 } : {})
98
162
  if (c) enviar(c, { soltar: true })
99
163
  return
100
164
  }
101
- const hijo = spawn(process.execPath, [__filename, outcome, clase],
102
- { detached: true, stdio: 'ignore', windowsHide: true })
103
- hijo.on('error', () => {})
104
- hijo.unref()
165
+ lanzarSuelto(outcome, clase)
105
166
  } catch {}
106
167
  }
107
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
+
108
186
  // Como programa es el proceso suelto: `node launch-event.js <outcome> <clase>`.
109
187
  if (require.main === module) {
110
188
  setTimeout(() => process.exit(0), TOPE_MS).unref()
@@ -112,4 +190,5 @@ if (require.main === module) {
112
190
  if (c) enviar(c)
113
191
  }
114
192
 
115
- module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RE_VERSION, RE_MAQUINA, claseDeArgumento, apagado, cuerpo, reportar }
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.417",
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": {
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 ─────────────────────────────────────────────────────────
@@ -254,7 +254,9 @@ function runConfigureClients(bin, command, args, opts = {}) {
254
254
  if (!bin) return { runtime: 'no_runtime', exitCode: 2, report: null }
255
255
  const argv = ['--configure-clients', '--json']
256
256
  if (command) argv.push('--command=' + command, ...(args || []).map(a => '--arg=' + a))
257
- 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 })
258
260
  if (r.error && r.error.code === 'ETIMEDOUT') return { runtime: 'timeout', exitCode: 2, report: null }
259
261
  if (r.error) return { runtime: 'failed', reason: r.error.message, exitCode: 2, report: null }
260
262
  let report = null
@@ -621,8 +623,8 @@ async function runSetup(opts = {}) {
621
623
  // Anon-first cloud relay activation (replaces the interactive email prompt
622
624
  // removed 2026-04-19). When no email is set, register an anonymous tunnel
623
625
  // token keyed on machine_id so cloud relay comes online immediately. Users
624
- // can attach an email later via the tray or /settings to unlock cross-Mac
625
- // 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
626
628
  // succeeds and the server can retry at startup.
627
629
  // Provisioning is ALWAYS anonymous, with or without an email — same change made in install.sh
628
630
  // (#1083). Two separate ways this used to be skipped, and both left the machine with NO
@@ -755,6 +757,7 @@ function _runHealthCheck() {
755
757
  const result = execFileSync(binPath, args, {
756
758
  timeout: 10000,
757
759
  stdio: ['pipe', 'pipe', 'pipe'],
760
+ windowsHide: true,
758
761
  env: { ...process.env, HOME },
759
762
  })
760
763
  const output = result.toString().trim()
@@ -1191,6 +1194,7 @@ async function _autoLaunchClient(clientsRun, opts = {}) {
1191
1194
 
1192
1195
  module.exports = {
1193
1196
  runSetup, runConfigureClients, _findServerBinary, _launcherArgsForConfigureClients,
1197
+ _cmdExists, _runHealthCheck,
1194
1198
  CLIENTS_REPORT_SCHEMA,
1195
1199
  _notSupportedOnPlatformResult,
1196
1200
  _launcherSpawnSpec, _writeLaunchScript, _writeLaunchSpec,