local-mcp 3.0.419 → 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) ────────────
@@ -405,6 +405,25 @@ async function main() {
405
405
  // copy and the durable App Support copy (see uninstallSentinelPaths).
406
406
  writeUninstallSentinels()
407
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
+
408
427
  if (fs.existsSync(CACHE_DIR)) {
409
428
  fs.rmSync(CACHE_DIR, { recursive: true, force: true })
410
429
  console.log(`Runtime eliminado de ${CACHE_DIR}`)
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']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.419",
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) }