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