local-mcp 3.0.418 → 3.0.420

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
@@ -8,7 +8,7 @@
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)
10
10
 
11
- **Runs on your computer. No tokens. No API keys. Free.**
11
+ **Runs on your computer. No tokens. No API keys.**
12
12
 
13
13
   
14
14
 
package/download.js CHANGED
@@ -5,8 +5,7 @@
5
5
  * Cache: ~/.local/share/local-mcp/bin/{version}/
6
6
  */
7
7
 
8
- const https = require('https')
9
- const http = require('http')
8
+ const { https, http } = require('./user-agent')
10
9
  const fs = require('fs')
11
10
  const path = require('path')
12
11
  const os = require('os')
@@ -543,20 +542,37 @@ async function ensureRuntime() {
543
542
  return { pythonBin: binPath, serverPath: binPath, runtimeDir: versionDir, binPath, version }
544
543
  }
545
544
 
545
+ /**
546
+ * The version a co-process is published at (lmcp-mac#382). `/runtime/latest` names it apart from
547
+ * the server's (`coprocess_version`, #3598); the Mac updater installs exactly that one and writes
548
+ * the same `.<name>-version` file. Asking for `version` instead made the two writers disagree
549
+ * whenever the two differ, and each re-downloaded what the other had just written.
550
+ */
551
+ function coprocessVersion(info) {
552
+ const cp = info && info.coprocess_version
553
+ return typeof cp === 'string' && /^[0-9][0-9A-Za-z.+-]*$/.test(cp) ? cp : info.version
554
+ }
555
+
546
556
  /**
547
557
  * Descarga el co-proceso teams-proxy si no está instalado o es una versión diferente.
548
558
  * El binary se instala en ~/.local/share/local-mcp/bin/teams-proxy
559
+ *
560
+ * lmcp-mac#382: not when the tray app is installed. The tray's updater owns teams-proxy (it
561
+ * verifies its digest and Developer ID; this does not), and trays 3.0.418-3.0.419 delete it every
562
+ * update cycle: npx put it back on every spawn — 140 addresses downloaded it 5-49 times a day.
549
563
  * @returns {Promise<string|null>} Ruta al binary, o null si falla
550
564
  */
551
- async function ensureTeamsProxy(platform = process.platform) {
565
+ async function ensureTeamsProxy(platform = process.platform, deps = {}) {
566
+ const { trayApps = [TRAY_APP, SYSTEM_TRAY_APP] } = deps
552
567
  // Only published for darwin (darwin-arm64/darwin-x64 artifacts) — mirrors the
553
568
  // guard ensureTray() already has. Without it, every Windows/Linux install fired
554
569
  // a doomed download attempt and printed a confusing "not available" error (#1390).
555
570
  // platform is a parameter (default process.platform) so the guard is testable
556
571
  // without a network mock — same pattern as _writeLaunchScript's isWin.
557
572
  if (platform !== 'darwin') return null
573
+ if (trayApps.some(app => app && _isUsableTrayBundle(app))) return null
558
574
  const info = await getLatestBinary()
559
- const version = info.version
575
+ const version = coprocessVersion(info)
560
576
  const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
561
577
  const binPath = path.join(CACHE_DIR, 'teams-proxy')
562
578
  const verFile = path.join(CACHE_DIR, '.teams-proxy-version')
@@ -874,7 +890,7 @@ async function ensureSlackProxy(platform = process.platform) {
874
890
  // without a network mock — same pattern as _writeLaunchScript's isWin.
875
891
  if (platform !== 'darwin') return null
876
892
  const info = await getLatestBinary()
877
- const version = info.version
893
+ const version = coprocessVersion(info) // lmcp-mac#382: the same version the tray installs
878
894
  const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
879
895
  const binPath = path.join(CACHE_DIR, 'slack-proxy')
880
896
  const verFile = path.join(CACHE_DIR, '.slack-proxy-version')
@@ -918,7 +934,7 @@ async function ensureSlackProxy(platform = process.platform) {
918
934
  }
919
935
 
920
936
  module.exports = {
921
- ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl,
937
+ ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl, coprocessVersion,
922
938
  latestVersionForPlatform, getArch,
923
939
  CACHE_DIR, TRAY_DIR, _getMachineId, LINUX_NO_SOPORTADO,
924
940
  _probeWinHardwareId, _resetWinHardwareIdCacheForTests, UUIDS_DE_HARDWARE_BASURA, _esIdDeHardwareValido,
package/index.js CHANGED
@@ -16,7 +16,7 @@ const { spawn, execFileSync, execSync } = require('child_process')
16
16
  const path = require('path')
17
17
  const os = require('os')
18
18
  const fs = require('fs')
19
- const https = require('https')
19
+ const { https } = require('./user-agent')
20
20
  const evento = require('./launch-event')
21
21
 
22
22
  // ── Version + liveness helpers (module scope so they are testable) ────────────
@@ -231,6 +231,25 @@ function writeUninstallSentinels() {
231
231
  }
232
232
  function clearUninstallSentinels() {
233
233
  for (const p of uninstallSentinelPaths()) { try { fs.unlinkSync(p) } catch {} }
234
+ try { fs.unlinkSync(avisoDesinstaladoPath()) } catch {}
235
+ }
236
+
237
+ // Marcador de espera (#48): tras una desinstalación el cliente de IA relanza el wrapper sin pausa
238
+ // (703 lanzamientos `uninstalled` en 24 h desde 14 máquinas). El primero dentro de la ventana avisa
239
+ // y reporta; los siguientes salen en silencio, sin evento ni mensaje. Un reinstalar limpia el marcador.
240
+ const VENTANA_AVISO_DESINSTALADO_MS = 30 * 60 * 1000
241
+ function avisoDesinstaladoPath() {
242
+ return path.join(path.dirname(require('./download').CACHE_DIR), '.uninstalled-aviso')
243
+ }
244
+ function avisoDesinstaladoVigente() {
245
+ try {
246
+ const t = fs.statSync(avisoDesinstaladoPath()).mtimeMs
247
+ return Date.now() - t >= 0 && Date.now() - t < VENTANA_AVISO_DESINSTALADO_MS
248
+ } catch { return false }
249
+ }
250
+ function marcarAvisoDesinstalado() {
251
+ try { fs.mkdirSync(path.dirname(avisoDesinstaladoPath()), { recursive: true }) } catch {}
252
+ try { fs.writeFileSync(avisoDesinstaladoPath(), String(Date.now())) } catch {}
234
253
  }
235
254
 
236
255
  const cmd = process.argv[2]
@@ -386,6 +405,25 @@ async function main() {
386
405
  // copy and the durable App Support copy (see uninstallSentinelPaths).
387
406
  writeUninstallSentinels()
388
407
 
408
+ // #48 (2): quitar la entrada de los clientes de IA que se alcanzan (mismo catálogo que el setup);
409
+ // lo que no se puede tocar se dice. Sin catálogo (sin red) se dice también, no se adivina.
410
+ try {
411
+ const { unconfigureClients } = require('./unconfigure')
412
+ const catalogFile = process.env.LMCP_CLIENTS_CATALOG_FILE
413
+ const catalog = catalogFile
414
+ ? JSON.parse(fs.readFileSync(catalogFile, 'utf8'))
415
+ : await require('./setup')._fetchClientsCatalog()
416
+ if (!catalog) {
417
+ console.log('No se pudo leer el catálogo de clientes: las entradas "local-mcp" de tus clientes de IA quedan; quitalas a mano.')
418
+ } else {
419
+ const r = unconfigureClients(catalog)
420
+ for (const x of r.removed) console.log(`Entrada local-mcp eliminada de ${x.client}: ${x.file}`)
421
+ for (const x of r.left) console.log(`Dejada en ${x.client}${x.file ? ' (' + x.file + ')' : ''}: ${x.reason}; quitá "local-mcp" a mano.`)
422
+ }
423
+ } catch (e) {
424
+ console.log('No se pudieron quitar las entradas de los clientes de IA: ' + e.message)
425
+ }
426
+
389
427
  if (fs.existsSync(CACHE_DIR)) {
390
428
  fs.rmSync(CACHE_DIR, { recursive: true, force: true })
391
429
  console.log(`Runtime eliminado de ${CACHE_DIR}`)
@@ -425,6 +463,11 @@ async function main() {
425
463
  // runtime-dir and durable App Support copies (see uninstallSentinelPaths).
426
464
  {
427
465
  if (anyUninstallSentinel()) {
466
+ if (avisoDesinstaladoVigente()) {
467
+ evento.silenciar()
468
+ process.exit(0)
469
+ }
470
+ marcarAvisoDesinstalado()
428
471
  reportarLanzamiento('uninstalled')
429
472
  process.stderr.write(
430
473
  '[LMCP] Local MCP was uninstalled — not starting.\n' +
package/launch-event.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // · demorar el arranque del server o una salida — sin reintentos, timeout corto, todo error
14
14
  // tragado;
15
15
  // · mandar algo que no sea uno de los cinco campos: ni el argumento, ni rutas, ni entorno.
16
- const https = require('https')
16
+ const { https } = require('./user-agent')
17
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']
@@ -166,6 +166,10 @@ function reportar (outcome, clase, { enProceso = false } = {}) {
166
166
  } catch {}
167
167
  }
168
168
 
169
+ // Un lanzamiento que el wrapper decide no reportar (#48: ya avisó dentro de la ventana) se marca
170
+ // como reportado para que la red del `exit` tampoco lo mande como `other`.
171
+ function silenciar () { reportado = true }
172
+
169
173
  function lanzarSuelto (outcome, clase) {
170
174
  const hijo = cp.spawn(process.execPath, [__filename, outcome, clase],
171
175
  { detached: true, stdio: 'ignore', windowsHide: true })
@@ -190,5 +194,5 @@ if (require.main === module) {
190
194
  if (c) enviar(c)
191
195
  }
192
196
 
193
- module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RE_VERSION, RE_MAQUINA, TOPE_ID_MS, claseDeArgumento, apagado, cuerpo, reportar, soltarPendiente,
197
+ module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RE_VERSION, RE_MAQUINA, TOPE_ID_MS, claseDeArgumento, apagado, cuerpo, reportar, silenciar, soltarPendiente,
194
198
  _idDeHardwareWindows: idDeHardwareWindows } // costura de test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.418",
3
+ "version": "3.0.420",
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": {
@@ -15,6 +15,8 @@
15
15
  "download.js",
16
16
  "setup.js",
17
17
  "launch-event.js",
18
+ "user-agent.js",
19
+ "unconfigure.js",
18
20
  "postinstall.js",
19
21
  "README.md"
20
22
  ],
package/setup.js CHANGED
@@ -67,7 +67,7 @@ function _writeLaunchScript(npxAbsPath, cacheDir, isWin = _IS_WIN) {
67
67
  'NODE_MAJOR=$(echo "$NODE_VER" | sed \'s/v//\' | cut -d. -f1)',
68
68
  'if [ "${NODE_MAJOR:-0}" -lt 16 ] 2>/dev/null; then',
69
69
  ' # curl telemetry — no Node dependency, fire-and-forget',
70
- ` curl -sf --max-time 3 -X POST "https://${BACKEND_HOST}/install-event" \\`,
70
+ ` curl -sf --max-time 3 -A "LMCP-npm (launcher)" -X POST "https://${BACKEND_HOST}/install-event" \\`,
71
71
  ' -H "Content-Type: application/json" \\',
72
72
  ' -d "{\\\"stage\\\":\\\"node_too_old\\\",\\\"version\\\":\\\"$NODE_VER\\\"}" &>/dev/null &',
73
73
  ' printf \'LMCP requires Node 16+. Current: %s\\nFix: nvm alias default 22 && nvm uninstall %s — then restart Claude Desktop.\\n\' "$NODE_VER" "$NODE_VER" >&2',
@@ -328,7 +328,7 @@ async function runSetup(opts = {}) {
328
328
  const _preToken = process.env.LMCP_PRE_TOKEN || ''
329
329
  if (_preToken.startsWith('lmcp-pre-')) {
330
330
  try {
331
- const https = require('https')
331
+ const { https } = require('./user-agent')
332
332
  const req = https.request({
333
333
  hostname: BACKEND_HOST,
334
334
  path: `/install/started/${_preToken}`,
@@ -819,7 +819,7 @@ function _getMachineId() {
819
819
  async function _associateEmail(email, token) {
820
820
  return new Promise((resolve) => {
821
821
  try {
822
- const https = require('https')
822
+ const { https } = require('./user-agent')
823
823
  const payload = JSON.stringify({
824
824
  stage: 'email_prompt',
825
825
  email_prompt_result: 'submitted',
@@ -892,7 +892,7 @@ function _writeCloudToken(cacheDir, token) {
892
892
  /// treat failure as non-fatal and fall through to the email-based path.
893
893
  async function _registerAnonToken() {
894
894
  return new Promise((resolve) => {
895
- const https = require('https')
895
+ const { https } = require('./user-agent')
896
896
  const machineId = _getMachineId()
897
897
  const payload = JSON.stringify({ machine_id: machineId })
898
898
  const req = https.request({
@@ -951,7 +951,7 @@ function _migrateCurlLaunchAgent() {
951
951
 
952
952
  function _trackSetupStep(step, status, client, detail) {
953
953
  try {
954
- const https = require('https')
954
+ const { https } = require('./user-agent')
955
955
  const payload = {
956
956
  machine_id: _getMachineId(),
957
957
  install_id: process.env.INSTALL_ID || '',
@@ -989,7 +989,7 @@ function _trackSetupStep(step, status, client, detail) {
989
989
 
990
990
  function _trackConfigWritten(clientId, clientName) {
991
991
  try {
992
- const https = require('https')
992
+ const { https } = require('./user-agent')
993
993
  const machineId = _getMachineId()
994
994
  const data = JSON.stringify({
995
995
  stage: 'config_written',
@@ -1015,7 +1015,7 @@ function _trackConfigWritten(clientId, clientName) {
1015
1015
 
1016
1016
  function _trackRestartPrompted(clientName) {
1017
1017
  try {
1018
- const https = require('https')
1018
+ const { https } = require('./user-agent')
1019
1019
  const machineId = _getMachineId()
1020
1020
  const payload = {
1021
1021
  stage: 'restart_prompted',
@@ -1040,7 +1040,7 @@ function _trackRestartPrompted(clientName) {
1040
1040
 
1041
1041
  function _pingInstall(clients, method, email = '', binaryVersion = '') {
1042
1042
  try {
1043
- const https = require('https')
1043
+ const { https } = require('./user-agent')
1044
1044
  const pkg = require('./package.json')
1045
1045
  const machineId = _getMachineId()
1046
1046
  // Persist the attribution ref so the server reports it on every heartbeat —
@@ -1133,7 +1133,7 @@ function _pingInstall(clients, method, email = '', binaryVersion = '') {
1133
1133
  // callers treat that as "don't launch anything," never as an error.
1134
1134
  function _fetchClientsCatalog(totalMs = 4000) {
1135
1135
  return new Promise((resolve) => {
1136
- const https = require('https')
1136
+ const { https } = require('./user-agent')
1137
1137
  const req = https.request({
1138
1138
  hostname: BACKEND_HOST, path: '/clients/catalog', method: 'GET', timeout: totalMs,
1139
1139
  }, (res) => {
package/unconfigure.js ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict'
2
+ // #48 (2): `npx local-mcp uninstall` quita la entrada `local-mcp` de las configs de los clientes de IA
3
+ // que puede alcanzar, con el MISMO catálogo que usa el setup (nada de lista de clientes aquí). Sin esto
4
+ // el cliente sigue lanzando el wrapper en cada arranque contra una máquina ya desinstalada.
5
+ //
6
+ // Sólo toca lo que puede deshacer sin riesgo: JSON estándar (cfgSchema mcpServers / servers /
7
+ // context_servers) y sólo la clave `local-mcp` dentro de ese objeto. Lo demás (TOML/YAML, esquemas
8
+ // propios, JSON con comentarios o inválido, comodines sin carpeta) no se reescribe: se devuelve en
9
+ // `left` con el motivo, para que el comando diga qué dejó.
10
+ const fs = require('fs')
11
+ const os = require('os')
12
+ const path = require('path')
13
+
14
+ const ENTRY = 'local-mcp'
15
+ const KEY_BY_SCHEMA = { mcpServers: 'mcpServers', servers: 'servers', context_servers: 'context_servers' }
16
+
17
+ function _expand(p, env) {
18
+ return p
19
+ .replace(/^~(?=[\\/]|$)/, env.home)
20
+ .replace(/%APPDATA%/g, env.appData)
21
+ .replace(/%LOCALAPPDATA%/g, env.localAppData)
22
+ }
23
+
24
+ // Un `*` sólo vale dentro de UN segmento; se resuelve contra el disco.
25
+ function _glob(p) {
26
+ if (!p.includes('*')) return [p]
27
+ const parts = p.split(/[\\/]/)
28
+ let bases = [parts[0] === '' ? path.sep : parts[0]]
29
+ for (const seg of parts.slice(1)) {
30
+ const next = []
31
+ for (const b of bases) {
32
+ if (!seg.includes('*')) { next.push(path.join(b, seg)); continue }
33
+ const re = new RegExp('^' + seg.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$')
34
+ let names = []
35
+ try { names = fs.readdirSync(b) } catch {}
36
+ for (const n of names) if (re.test(n)) next.push(path.join(b, n))
37
+ }
38
+ bases = next
39
+ }
40
+ return bases
41
+ }
42
+
43
+ function _paths(client, plat, env) {
44
+ const platform = (client.platforms || {})[plat]
45
+ if (!platform || !Array.isArray(platform.cfgPaths)) return []
46
+ const hosts = Array.isArray(platform.hosts) && platform.hosts.length ? platform.hosts : [null]
47
+ const out = []
48
+ for (const raw of platform.cfgPaths) {
49
+ for (const h of hosts) {
50
+ const withHost = h === null ? raw : raw.replace(/\{host\}/g, h)
51
+ if (withHost.includes('{host}')) continue
52
+ for (const f of _glob(_expand(withHost, env))) if (!out.includes(f)) out.push(f)
53
+ }
54
+ }
55
+ return out
56
+ }
57
+
58
+ function _writeAtomic(file, data) {
59
+ const tmp = file + '.lmcp-tmp'
60
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8')
61
+ fs.renameSync(tmp, file)
62
+ }
63
+
64
+ /**
65
+ * @param {object} catalog el catálogo servido (`clients[]`).
66
+ * @param {object} [opts] platform, home, appData, localAppData (para probar sin tocar el disco real).
67
+ * @returns {{removed: {client:string,file:string}[], left: {client:string,file?:string,reason:string}[]}}
68
+ */
69
+ function unconfigureClients(catalog, opts = {}) {
70
+ const home = opts.home || os.homedir()
71
+ const platform = opts.platform || process.platform
72
+ const env = {
73
+ home,
74
+ appData: opts.appData || process.env.APPDATA || path.join(home, 'AppData', 'Roaming'),
75
+ localAppData: opts.localAppData || process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'),
76
+ }
77
+ const plat = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : null
78
+ const removed = []
79
+ const left = []
80
+ if (!plat || !catalog || !Array.isArray(catalog.clients)) return { removed, left }
81
+ for (const c of catalog.clients) {
82
+ const files = _paths(c, plat, env).filter((f) => { try { return fs.statSync(f).isFile() } catch { return false } })
83
+ for (const file of files) {
84
+ const key = KEY_BY_SCHEMA[c.cfgSchema]
85
+ if (!key) { left.push({ client: c.id, file, reason: 'schema ' + c.cfgSchema }); continue }
86
+ let data
87
+ try { data = JSON.parse(fs.readFileSync(file, 'utf8')) } catch {
88
+ left.push({ client: c.id, file, reason: 'not plain JSON' }); continue
89
+ }
90
+ const block = data && typeof data === 'object' ? data[key] : null
91
+ if (!block || typeof block !== 'object' || !(ENTRY in block)) continue
92
+ delete block[ENTRY]
93
+ try { _writeAtomic(file, data); removed.push({ client: c.id, file }) } catch (e) {
94
+ left.push({ client: c.id, file, reason: e.message })
95
+ }
96
+ }
97
+ }
98
+ return { removed, left }
99
+ }
100
+
101
+ module.exports = { unconfigureClients }
package/user-agent.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+ // Every outbound HTTP request from npx carries `User-Agent: LMCP-npm/<version> (<platform>)`.
3
+ // Node sends none by default, and an empty-UA download of a binary from a fresh process looks
4
+ // like malware to a security gateway. Drop-in `https`/`http` wrappers: same signatures,
5
+ // the header is added unless the caller already set one. Resolve `request`/`get` at call time.
6
+ const https = require('https')
7
+ const http = require('http')
8
+
9
+ let VERSION = '0'
10
+ try { VERSION = require('./package.json').version || '0' } catch {}
11
+
12
+ function userAgent (platform = process.platform) {
13
+ return `LMCP-npm/${VERSION} (${platform})`
14
+ }
15
+
16
+ function conUA (opts) {
17
+ const o = { ...(opts || {}) }
18
+ const h = { ...(o.headers || {}) }
19
+ if (!Object.keys(h).some((k) => k.toLowerCase() === 'user-agent')) h['User-Agent'] = userAgent()
20
+ o.headers = h
21
+ return o
22
+ }
23
+
24
+ function envolver (mod) {
25
+ return {
26
+ request (opts, cb) {
27
+ if (typeof opts === 'string' || opts instanceof URL) return mod.request(opts, conUA({}), cb)
28
+ return mod.request(conUA(opts), cb)
29
+ },
30
+ get (url, opts, cb) {
31
+ if (typeof url === 'string' || url instanceof URL) {
32
+ if (typeof opts === 'function') return mod.get(url, conUA({}), opts)
33
+ return mod.get(url, conUA(opts), cb)
34
+ }
35
+ return mod.get(conUA(url), typeof opts === 'function' ? opts : cb)
36
+ }
37
+ }
38
+ }
39
+
40
+ module.exports = { userAgent, conUA, https: envolver(https), http: envolver(http) }