local-mcp 3.0.385 → 3.0.409
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/package.json +1 -1
- package/setup.js +218 -297
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.409",
|
|
4
4
|
"description": "Let ChatGPT, Claude, Cursor & any MCP client actually use your Mac — read & reply to email, manage your calendar, text over iMessage, find files, work with Teams, Slack & Office. On your Mac, no API keys, free.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/setup.js
CHANGED
|
@@ -81,7 +81,6 @@ function _writeLaunchScript(npxAbsPath, cacheDir, isWin = _IS_WIN) {
|
|
|
81
81
|
return null
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
const STABLE_LINK = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin', 'local-mcp-server')
|
|
85
84
|
const BACKEND_HOST = 'office-mcp-production.up.railway.app'
|
|
86
85
|
|
|
87
86
|
// ── Platform-aware config paths for AI clients ─────────────────────────────
|
|
@@ -93,58 +92,6 @@ const _IS_WIN = process.platform === 'win32'
|
|
|
93
92
|
const _IS_MAC = process.platform === 'darwin'
|
|
94
93
|
const _APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming')
|
|
95
94
|
|
|
96
|
-
// Windows MSIX (Microsoft Store / WinGet) installs read config from a
|
|
97
|
-
// virtualized path inside %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude.
|
|
98
|
-
// The non-MSIX (.exe) install reads from %APPDATA%\Claude.
|
|
99
|
-
// We detect which one exists and write to both if needed.
|
|
100
|
-
function _findMsixClaudePath() {
|
|
101
|
-
if (!_IS_WIN) return null
|
|
102
|
-
const localAppData = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local')
|
|
103
|
-
const packagesDir = path.join(localAppData, 'Packages')
|
|
104
|
-
try {
|
|
105
|
-
const entries = fs.readdirSync(packagesDir)
|
|
106
|
-
const claudePkg = entries.find(e => e.startsWith('Claude_'))
|
|
107
|
-
if (claudePkg) {
|
|
108
|
-
return path.join(packagesDir, claudePkg, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json')
|
|
109
|
-
}
|
|
110
|
-
} catch {}
|
|
111
|
-
return null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function _claudeDesktopPath() {
|
|
115
|
-
if (_IS_WIN) {
|
|
116
|
-
// Prefer MSIX path if the package exists
|
|
117
|
-
const msix = _findMsixClaudePath()
|
|
118
|
-
if (msix) return msix
|
|
119
|
-
return path.join(_APPDATA, 'Claude', 'claude_desktop_config.json')
|
|
120
|
-
}
|
|
121
|
-
if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
122
|
-
return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json')
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// All paths where we should write the config (MSIX + non-MSIX on Windows)
|
|
126
|
-
function _claudeDesktopAllPaths() {
|
|
127
|
-
if (!_IS_WIN) return [_claudeDesktopPath()]
|
|
128
|
-
const paths = [path.join(_APPDATA, 'Claude', 'claude_desktop_config.json')]
|
|
129
|
-
const msix = _findMsixClaudePath()
|
|
130
|
-
if (msix && !paths.includes(msix)) paths.push(msix)
|
|
131
|
-
return paths
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function _claudeDesktopDetect() {
|
|
135
|
-
if (_IS_WIN) {
|
|
136
|
-
// Check both MSIX and non-MSIX paths
|
|
137
|
-
return _claudeDesktopAllPaths().some(p => fs.existsSync(path.dirname(p)))
|
|
138
|
-
}
|
|
139
|
-
return fs.existsSync(path.dirname(_claudeDesktopPath()))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function _rooClinePath() {
|
|
143
|
-
if (_IS_WIN) return path.join(_APPDATA, 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
144
|
-
if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
145
|
-
return path.join(HOME, '.config', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
146
|
-
}
|
|
147
|
-
|
|
148
95
|
// Where LMCP's own config.json (license_email, cloud_token, activation_checklist_pending)
|
|
149
96
|
// lives. Used to hardcode the macOS "Library/Application Support" path unconditionally, so
|
|
150
97
|
// on Windows it resolved to a nonsense C:\Users\X\Library\...\Local MCP directory that
|
|
@@ -156,75 +103,8 @@ function _localMcpConfigDir(platform = process.platform, home = HOME, appData =
|
|
|
156
103
|
return path.join(home, '.config', 'local-mcp')
|
|
157
104
|
}
|
|
158
105
|
|
|
159
|
-
const CLIENTS = [
|
|
160
|
-
{
|
|
161
|
-
id: 'claude-desktop',
|
|
162
|
-
name: 'Claude Desktop',
|
|
163
|
-
cfgPath: _claudeDesktopPath(),
|
|
164
|
-
detect: _claudeDesktopDetect,
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
id: 'cursor',
|
|
168
|
-
name: 'Cursor',
|
|
169
|
-
cfgPath: path.join(HOME, '.cursor', 'mcp.json'),
|
|
170
|
-
detect: () => fs.existsSync(path.join(HOME, '.cursor')) || _appExists('Cursor'),
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
id: 'windsurf',
|
|
174
|
-
name: 'Windsurf',
|
|
175
|
-
cfgPath: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
176
|
-
detect: () => fs.existsSync(path.join(HOME, '.codeium', 'windsurf')) || _appExists('Windsurf'),
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
id: 'vscode',
|
|
180
|
-
name: 'VS Code',
|
|
181
|
-
cfgPath: path.join(HOME, '.vscode', 'mcp.json'),
|
|
182
|
-
detect: () => _cmdExists('code') || _appExists('Visual Studio Code') || fs.existsSync(path.join(HOME, '.vscode')),
|
|
183
|
-
vscode: true, // native MCP uses "servers" key + type:"stdio"
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
id: 'roo-cline',
|
|
187
|
-
name: 'Roo-Cline',
|
|
188
|
-
cfgPath: _rooClinePath(),
|
|
189
|
-
detect: () => fs.existsSync(path.dirname(_rooClinePath())),
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
id: 'zed',
|
|
193
|
-
name: 'Zed',
|
|
194
|
-
cfgPath: path.join(HOME, '.config', 'zed', 'settings.json'),
|
|
195
|
-
detect: () => fs.existsSync(path.join(HOME, '.config', 'zed')) || _appExists('Zed'),
|
|
196
|
-
zed: true, // Zed usa formato diferente dentro de settings.json
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
id: 'claude-code',
|
|
200
|
-
name: 'Claude Code',
|
|
201
|
-
// Claude Code reads MCP servers from ~/.claude.json (user-scope top-level
|
|
202
|
-
// mcpServers), not ~/.claude/settings.json. Writing settings.json never
|
|
203
|
-
// actually configured Claude Code.
|
|
204
|
-
cfgPath: path.join(HOME, '.claude.json'),
|
|
205
|
-
detect: () => fs.existsSync(path.join(HOME, '.claude')) || fs.existsSync(path.join(HOME, '.claude.json')) || _cmdExists('claude'),
|
|
206
|
-
},
|
|
207
|
-
]
|
|
208
|
-
|
|
209
106
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
210
107
|
|
|
211
|
-
function _appExists(name) {
|
|
212
|
-
if (_IS_MAC) {
|
|
213
|
-
return fs.existsSync(`/Applications/${name}.app`) ||
|
|
214
|
-
fs.existsSync(path.join(HOME, `Applications/${name}.app`))
|
|
215
|
-
}
|
|
216
|
-
if (_IS_WIN) {
|
|
217
|
-
// Check common Windows install locations
|
|
218
|
-
const pf = process.env.ProgramFiles || 'C:\\Program Files'
|
|
219
|
-
const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'
|
|
220
|
-
const localApp = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local')
|
|
221
|
-
return fs.existsSync(path.join(pf, name)) ||
|
|
222
|
-
fs.existsSync(path.join(pf86, name)) ||
|
|
223
|
-
fs.existsSync(path.join(localApp, name))
|
|
224
|
-
}
|
|
225
|
-
return false // Linux: rely on _cmdExists or config path detection
|
|
226
|
-
}
|
|
227
|
-
|
|
228
108
|
function _cmdExists(cmd) {
|
|
229
109
|
const which = _IS_WIN ? 'where' : 'which'
|
|
230
110
|
try { execSync(`${which} ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
|
|
@@ -297,9 +177,8 @@ function _atomicWriteConfig(filePath, data) {
|
|
|
297
177
|
// Node-based MCP host — Cursor, VS Code, Windsurf, Cline, Roo — got `spawn EINVAL`
|
|
298
178
|
// and showed local-mcp as failed, on the very platform this release re-launches.
|
|
299
179
|
//
|
|
300
|
-
// The fix is to spawn the interpreter and pass the script as an ARGUMENT
|
|
301
|
-
//
|
|
302
|
-
// is never the executable; cmd.exe is.
|
|
180
|
+
// The fix is to spawn the interpreter and pass the script as an ARGUMENT. A batch
|
|
181
|
+
// file is never the executable; cmd.exe is.
|
|
303
182
|
//
|
|
304
183
|
// The invariant a test can check without knowing this function's shape: `command`
|
|
305
184
|
// must never be a batch file. That is the precise condition Node refuses, not a
|
|
@@ -324,100 +203,99 @@ function _writeLaunchSpec(npxAbsPath, cacheDir, isWin = _IS_WIN) {
|
|
|
324
203
|
return _launcherSpawnSpec(_writeLaunchScript(npxAbsPath, cacheDir, isWin), isWin)
|
|
325
204
|
}
|
|
326
205
|
|
|
327
|
-
//
|
|
328
|
-
// Split out of injectMcpConfig so the same merge logic runs independently per path —
|
|
329
|
-
// see the comment on injectMcpConfig for why that independence matters.
|
|
330
|
-
function _mergeLocalMcpEntry(cfg, client, command, args) {
|
|
331
|
-
const existingServers = Object.keys(
|
|
332
|
-
cfg.mcpServers || cfg.servers || cfg.context_servers || {}
|
|
333
|
-
)
|
|
334
|
-
const otherServers = existingServers.filter(k => k !== 'local-mcp' && k !== 'office-mcp')
|
|
335
|
-
|
|
336
|
-
if (client.zed) {
|
|
337
|
-
cfg.context_servers = cfg.context_servers || {}
|
|
338
|
-
cfg.context_servers['local-mcp'] = { command: { path: command, args: args ?? [] } }
|
|
339
|
-
} else if (client.vscode) {
|
|
340
|
-
// VS Code native MCP (1.99+) uses "servers" key with type:"stdio"
|
|
341
|
-
cfg.servers = cfg.servers || {}
|
|
342
|
-
cfg.servers['local-mcp'] = { type: 'stdio', command, args: args ?? [] }
|
|
343
|
-
} else {
|
|
344
|
-
cfg.mcpServers = cfg.mcpServers || {}
|
|
345
|
-
delete cfg.mcpServers['office-mcp']
|
|
346
|
-
const entry = { command }
|
|
347
|
-
if (args !== undefined) entry.args = args
|
|
348
|
-
cfg.mcpServers['local-mcp'] = entry
|
|
349
|
-
}
|
|
350
|
-
return { existingCount: existingServers.length, otherServers }
|
|
351
|
-
}
|
|
206
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
352
207
|
|
|
353
|
-
// ──
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
208
|
+
// ── #3212 · Mac: las apps de IA las configura el binario, con el modelo del producto ──────────
|
|
209
|
+
// El binario decide y escribe con el mismo escritor que el tray (JSONC que no toca, YAML, TOML,
|
|
210
|
+
// el lock compartido) y devuelve un reporte versionado. Acá sólo se lee. En Windows y Linux sigue
|
|
211
|
+
// el camino JS de abajo hasta que go-server tenga el mismo flag: #3212 no se cierra antes.
|
|
212
|
+
const CLIENTS_REPORT_SCHEMA = 1
|
|
213
|
+
|
|
214
|
+
// platform is a PARAMETER, not a read of _IS_WIN, for the same reason
|
|
215
|
+
// _writeLaunchScript's isWin parameter exists (#1394 §C): a mutation to the
|
|
216
|
+
// win32 branch has to be exercisable on any machine running this suite, not
|
|
217
|
+
// only on a real Windows box.
|
|
218
|
+
// `home` is a parameter for the same reason `platform` is: the third candidate is the stable
|
|
219
|
+
// link under the user's home, so without it the lookup READS THE MACHINE and a test asserting
|
|
220
|
+
// "finds nothing" passes only where LMCP is not installed. Same seam as _localMcpConfigDir.
|
|
221
|
+
// Every home-derived candidate must come from THIS argument; one that reaches for os.homedir()
|
|
222
|
+
// again puts the machine back in the answer.
|
|
223
|
+
function _findServerBinary(embedded, cacheDir, platform = process.platform, home = HOME) {
|
|
224
|
+
const binName = platform === 'win32' ? 'lmcp-server.exe' : 'lmcp-server'
|
|
225
|
+
const stableLink = path.join(home, '.local', 'share', 'local-mcp', 'bin', 'local-mcp-server')
|
|
226
|
+
const cands = [embedded, cacheDir && path.join(cacheDir, binName), stableLink].filter(Boolean)
|
|
227
|
+
return cands.find(p => { try { return fs.statSync(p).isFile() } catch { return false } }) || null
|
|
228
|
+
}
|
|
375
229
|
|
|
376
|
-
|
|
377
|
-
|
|
230
|
+
// --command=/--arg= point the binary at the npx-launcher indirection darwin
|
|
231
|
+
// uses to reach the local-mcp-server binary. go-server on Windows has no
|
|
232
|
+
// such indirection and REJECTS these flags outright with exit 2 (contract
|
|
233
|
+
// §3.4) — passing them there would fail every Windows install. Extracted so
|
|
234
|
+
// this platform rule is unit-testable without invoking runSetup's full
|
|
235
|
+
// side-effect chain.
|
|
236
|
+
function _launcherArgsForConfigureClients(platform, command, args) {
|
|
237
|
+
return platform === 'darwin' ? { command, args } : { command: null, args: [] }
|
|
238
|
+
}
|
|
378
239
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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.
|
|
243
|
+
function _notSupportedOnPlatformResult() {
|
|
244
|
+
return { runtime: 'not_supported_on_platform', exitCode: 0, report: null }
|
|
245
|
+
}
|
|
382
246
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
247
|
+
// Corre `--configure-clients --json`. Nunca cuelga el instalador: un binario que no responde
|
|
248
|
+
// termina en failed(timeout); uno viejo, que no conoce el flag, no imprime el reporte y queda en
|
|
249
|
+
// runtime_too_old. exitCode: 0 bien · 1 algún cliente falló · 2 no se pudo configurar.
|
|
250
|
+
function runConfigureClients(bin, command, args, opts = {}) {
|
|
251
|
+
const spawn = opts.spawnSync || require('child_process').spawnSync
|
|
252
|
+
const timeoutMs = opts.timeoutMs || 90000
|
|
253
|
+
if (!bin) return { runtime: 'no_runtime', exitCode: 2, report: null }
|
|
254
|
+
const argv = ['--configure-clients', '--json']
|
|
255
|
+
if (command) argv.push('--command=' + command, ...(args || []).map(a => '--arg=' + a))
|
|
256
|
+
const r = spawn(bin, argv, { input: '', timeout: timeoutMs, encoding: 'utf8' })
|
|
257
|
+
if (r.error && r.error.code === 'ETIMEDOUT') return { runtime: 'timeout', exitCode: 2, report: null }
|
|
258
|
+
if (r.error) return { runtime: 'failed', reason: r.error.message, exitCode: 2, report: null }
|
|
259
|
+
let report = null
|
|
260
|
+
try { report = JSON.parse(String(r.stdout || '').trim().split('\n').pop()) } catch { /* sin reporte */ }
|
|
261
|
+
if (!report || typeof report !== 'object' || report.schema === undefined) {
|
|
262
|
+
return { runtime: 'runtime_too_old', exitCode: 2, report: null }
|
|
397
263
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const failed = perPath.filter((r) => !r.ok)
|
|
401
|
-
if (failed.length > 0) {
|
|
402
|
-
for (const f of failed) {
|
|
403
|
-
process.stderr.write(` ⚠ ${client.name}: could not update ${f.path} (${f.error}) — other MCP servers configured only there may be affected\n`)
|
|
404
|
-
}
|
|
264
|
+
if (report.schema !== CLIENTS_REPORT_SCHEMA) {
|
|
265
|
+
return { runtime: 'unsupported_report_schema', reason: String(report.schema), exitCode: 2, report: null }
|
|
405
266
|
}
|
|
267
|
+
const failedAny = (report.clients || []).some(c => c.status === 'failed')
|
|
268
|
+
return { runtime: 'ok', exitCode: failedAny ? 1 : 0, report }
|
|
269
|
+
}
|
|
406
270
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
271
|
+
// Los permisos pendientes los mide el tray (GET /permissions): medidos desde este proceso
|
|
272
|
+
// describirían a la terminal. Se reintenta un rato porque el tray acaba de arrancar.
|
|
273
|
+
async function _fetchTrayPermissions(totalMs = 10000) {
|
|
274
|
+
const http = require('http')
|
|
275
|
+
const once = () => new Promise(resolve => {
|
|
276
|
+
const req = http.get({ host: '127.0.0.1', port: 8765, path: '/permissions', timeout: 1500 }, res => {
|
|
277
|
+
let body = ''
|
|
278
|
+
res.on('data', d => { body += d })
|
|
279
|
+
res.on('end', () => {
|
|
280
|
+
try { const o = JSON.parse(body); resolve(o && o.source === 'tray' ? o : null) } catch { resolve(null) }
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
req.on('timeout', () => { req.destroy(); resolve(null) })
|
|
284
|
+
req.on('error', () => resolve(null))
|
|
285
|
+
})
|
|
286
|
+
const end = Date.now() + totalMs
|
|
287
|
+
while (Date.now() < end) {
|
|
288
|
+
const o = await once()
|
|
289
|
+
if (o) return o
|
|
290
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
415
291
|
}
|
|
292
|
+
return null
|
|
416
293
|
}
|
|
417
294
|
|
|
418
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
419
|
-
|
|
420
295
|
async function runSetup(opts = {}) {
|
|
296
|
+
// #3212 — `--json`: el reporte estructurado va solo por stdout; todo lo demás, a stderr.
|
|
297
|
+
const jsonMode = !!opts.json || process.argv.includes('--json')
|
|
298
|
+
if (jsonMode) console.log = (...a) => { process.stderr.write(a.join(' ') + '\n') }
|
|
421
299
|
// Cross-platform: macOS configures the Swift tray + embedded server; Windows/Linux
|
|
422
300
|
// download the go-server standalone binary (download.js) and write the MCP client
|
|
423
301
|
// config (the config writer already handles Windows paths). The Mac-only waitlist
|
|
@@ -551,71 +429,56 @@ async function runSetup(opts = {}) {
|
|
|
551
429
|
// failed it falls back to plain 'npx'.
|
|
552
430
|
|
|
553
431
|
// Detectar clientes
|
|
554
|
-
const detected = CLIENTS.filter(c => c.detect())
|
|
555
|
-
const notDetected = CLIENTS.filter(c => !c.detect())
|
|
556
432
|
const _npmPkgVersion = (() => { try { return require('./package.json').version } catch { return '' } })()
|
|
557
|
-
_trackSetupStep('detect', detected.length > 0 ? 'ok' : 'no_clients', '', {
|
|
558
|
-
clientsFound: detected.map(c => c.id),
|
|
559
|
-
clientsNotFound: notDetected.map(c => c.id),
|
|
560
|
-
npmVersion: _npmPkgVersion,
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
if (detected.length === 0) {
|
|
564
|
-
const entry = stableArgs !== undefined
|
|
565
|
-
? { command: stableCommand, args: stableArgs }
|
|
566
|
-
: { command: stableCommand }
|
|
567
|
-
const snippet = JSON.stringify({ mcpServers: { 'local-mcp': entry } }, null, 2)
|
|
568
|
-
.split('\n').map(l => ' ' + l).join('\n')
|
|
569
|
-
|
|
570
|
-
console.log('╔══════════════════════════════════════════════════════════╗')
|
|
571
|
-
console.log('║ LMCP installed — no AI client detected yet ║')
|
|
572
|
-
console.log('╚══════════════════════════════════════════════════════════╝\n')
|
|
573
|
-
console.log(' To get started, add LMCP to your AI client config:\n')
|
|
574
|
-
console.log(' ── Claude Desktop ────────────────────────────────────────')
|
|
575
|
-
console.log(` File: ${_claudeDesktopPath()}\n`)
|
|
576
|
-
console.log(snippet)
|
|
577
|
-
console.log('\n ── Cursor ────────────────────────────────────────────────')
|
|
578
|
-
console.log(' File: ~/.cursor/mcp.json\n')
|
|
579
|
-
console.log(snippet)
|
|
580
|
-
console.log('\n ── VS Code (Cline / GitHub Copilot) ──────────────────────')
|
|
581
|
-
console.log(' File: ~/.vscode/mcp.json\n')
|
|
582
|
-
console.log(snippet)
|
|
583
|
-
console.log('\n Then restart your AI client and try:')
|
|
584
|
-
console.log(' "Summarize my unread emails"\n')
|
|
585
|
-
console.log(' ── Already installed an AI client? ───────────────────────')
|
|
586
|
-
console.log(' Run this to auto-configure it:')
|
|
587
|
-
console.log(' npx local-mcp setup\n')
|
|
588
|
-
console.log(' Full setup guide: https://local-mcp.com/setup')
|
|
589
|
-
console.log('─────────────────────────────────────────────────────────────\n')
|
|
590
|
-
return
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
console.log(`Detected: ${detected.map(c => c.name).join(', ')}\n`)
|
|
594
|
-
|
|
595
433
|
const configured = []
|
|
596
434
|
const failed = []
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
console.
|
|
435
|
+
let clientsRun = null // #3212: en la Mac, el reporte del binario
|
|
436
|
+
// #3212 — Windows now shares the exact darwin path: the go-server binary
|
|
437
|
+
// decides and writes (lmcp-server --configure-clients --json), never a
|
|
438
|
+
// second JS-side client list. Linux has no runtime-native writer yet
|
|
439
|
+
// (§3.8): a generic message, no per-client guesswork, no files written.
|
|
440
|
+
if (process.platform === 'darwin' || _IS_WIN) {
|
|
441
|
+
const { CACHE_DIR } = require('./download')
|
|
442
|
+
const launcher = _launcherArgsForConfigureClients(process.platform, stableCommand, stableArgs)
|
|
443
|
+
clientsRun = runConfigureClients(_findServerBinary(_embeddedServer, CACHE_DIR), launcher.command, launcher.args)
|
|
444
|
+
const clientes = (clientsRun.report && clientsRun.report.clients) || []
|
|
445
|
+
_trackSetupStep('detect', clientsRun.runtime === 'ok' ? 'ok' : clientsRun.runtime, '', {
|
|
446
|
+
clientsFound: clientes.filter(c => c.status !== 'not_installed').map(c => c.id),
|
|
447
|
+
clientsNotFound: clientes.filter(c => c.status === 'not_installed').map(c => c.id),
|
|
448
|
+
npmVersion: _npmPkgVersion,
|
|
449
|
+
})
|
|
450
|
+
for (const c of clientes) {
|
|
451
|
+
if (c.status === 'configured') {
|
|
452
|
+
configured.push(c.name)
|
|
453
|
+
_trackConfigWritten(c.id, c.name)
|
|
454
|
+
console.log(`✓ ${c.name} configured`)
|
|
455
|
+
} else if (c.status === 'already') {
|
|
456
|
+
console.log(`✓ ${c.name} already configured`)
|
|
457
|
+
} else if (c.status === 'failed') {
|
|
458
|
+
failed.push({ name: c.name, reason: 'failed', error: c.reason })
|
|
459
|
+
console.error(`✗ ${c.name}: ${c.reason || 'failed'}`)
|
|
460
|
+
} else if (c.status !== 'not_installed') {
|
|
461
|
+
console.log(`· ${c.name}: ${c.status.replace(/_/g, ' ')}${c.reason ? ' — ' + c.reason : ''}`)
|
|
617
462
|
}
|
|
618
463
|
}
|
|
464
|
+
if (clientsRun.runtime !== 'ok') {
|
|
465
|
+
console.error(`⚠ AI apps not configured yet (${clientsRun.runtime}) — open LMCP from the system tray and use Connect.`)
|
|
466
|
+
} else if (clientes.length > 0 && clientes.every(c => c.status === 'not_installed')) {
|
|
467
|
+
console.log('\n No AI client detected yet — LMCP is installed and ready.')
|
|
468
|
+
console.log(' Install one (Claude Desktop, Cursor, VS Code...), then run:')
|
|
469
|
+
console.log(' npx local-mcp setup\n')
|
|
470
|
+
console.log(' Full setup guide: https://local-mcp.com/setup')
|
|
471
|
+
}
|
|
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')
|
|
619
482
|
}
|
|
620
483
|
|
|
621
484
|
const cfgDir = _localMcpConfigDir()
|
|
@@ -713,7 +576,9 @@ async function runSetup(opts = {}) {
|
|
|
713
576
|
// JSON files we write above — so we don't auto-configure it (a JSON write would be
|
|
714
577
|
// useless and a blind YAML merge could corrupt the user's config). If Hermes is
|
|
715
578
|
// present, print the exact CLI one-liner instead.
|
|
716
|
-
|
|
579
|
+
const _hermes = clientsRun && clientsRun.report && (clientsRun.report.clients || []).find(c => c.id === 'hermes')
|
|
580
|
+
const _hermesListo = !!_hermes && ['configured', 'already'].includes(_hermes.status)
|
|
581
|
+
if (!_hermesListo && (fs.existsSync(path.join(HOME, '.hermes')) || _cmdExists('hermes'))) {
|
|
717
582
|
console.log('┌─────────────────────────────────────────────────────┐')
|
|
718
583
|
console.log('│ HERMES AGENT detected — add LMCP with its CLI: │')
|
|
719
584
|
console.log('│ │')
|
|
@@ -733,11 +598,17 @@ async function runSetup(opts = {}) {
|
|
|
733
598
|
|
|
734
599
|
// Instalar y lanzar el tray (menu bar app) — arm64 only, non-fatal
|
|
735
600
|
await _installTray()
|
|
601
|
+
// Tray permissions are a macOS-only concept (contract §3.1: Windows/Linux
|
|
602
|
+
// report "not_applicable", not "unknown_until_tray_runs") — polling
|
|
603
|
+
// 127.0.0.1:8765/permissions off darwin would just cost up to 10s waiting
|
|
604
|
+
// on an endpoint that was never going to answer.
|
|
605
|
+
const trayPermissions = _IS_MAC ? (clientsRun ? await _fetchTrayPermissions() : null) : 'not_applicable'
|
|
736
606
|
await _installTeamsProxy()
|
|
737
607
|
await _installSlackProxy()
|
|
738
608
|
|
|
739
|
-
// Auto-launch primary configured AI client
|
|
740
|
-
|
|
609
|
+
// Auto-launch the primary configured AI client (macOS only — win32/linux
|
|
610
|
+
// just name the next_step) + show "Try this now" banner.
|
|
611
|
+
await _autoLaunchClient(clientsRun)
|
|
741
612
|
|
|
742
613
|
// Anon-first cloud relay activation (replaces the interactive email prompt
|
|
743
614
|
// removed 2026-04-19). When no email is set, register an anonymous tunnel
|
|
@@ -804,6 +675,23 @@ async function runSetup(opts = {}) {
|
|
|
804
675
|
// LMCP_METHOD lets install.sh pass 'curl' so we can distinguish it from
|
|
805
676
|
// a direct 'npx local-mcp setup' invocation in the analytics.
|
|
806
677
|
_pingInstall(configured, process.env.LMCP_METHOD || opts.method || 'npx-setup', email, binaryVersion)
|
|
678
|
+
|
|
679
|
+
// #3212 — el reporte para quien instaló sin mirar (un agente): qué quedó hecho y qué espera
|
|
680
|
+
// 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.
|
|
682
|
+
if (jsonMode) {
|
|
683
|
+
const rep = clientsRun.report || {}
|
|
684
|
+
const out = {
|
|
685
|
+
schema: CLIENTS_REPORT_SCHEMA,
|
|
686
|
+
installer_version: _npmPkgVersion,
|
|
687
|
+
runtime: clientsRun.runtime,
|
|
688
|
+
catalog: rep.catalog || null,
|
|
689
|
+
clients: rep.clients || [],
|
|
690
|
+
permissions: trayPermissions || 'unknown_until_tray_runs',
|
|
691
|
+
}
|
|
692
|
+
process.stdout.write(JSON.stringify(out) + '\n')
|
|
693
|
+
}
|
|
694
|
+
process.exitCode = clientsRun.exitCode
|
|
807
695
|
}
|
|
808
696
|
|
|
809
697
|
async function _installTeamsProxy() {
|
|
@@ -1212,46 +1100,79 @@ function _pingInstall(clients, method, email = '', binaryVersion = '') {
|
|
|
1212
1100
|
} catch { /* no bloquear */ }
|
|
1213
1101
|
}
|
|
1214
1102
|
|
|
1103
|
+
// _fetchClientsCatalog fetches the served catalog (the SAME endpoint the
|
|
1104
|
+
// go-server's RefreshFromRemote uses) so _autoLaunchClient can find a
|
|
1105
|
+
// client's macOS .app path without ever typing a client's identity a
|
|
1106
|
+
// second time in this file. 4s budget (#3212 §3.9): a slow/unreachable
|
|
1107
|
+
// backend must never block the rest of setup. Returns null on any failure —
|
|
1108
|
+
// callers treat that as "don't launch anything," never as an error.
|
|
1109
|
+
function _fetchClientsCatalog(totalMs = 4000) {
|
|
1110
|
+
return new Promise((resolve) => {
|
|
1111
|
+
const https = require('https')
|
|
1112
|
+
const req = https.request({
|
|
1113
|
+
hostname: BACKEND_HOST, path: '/clients/catalog', method: 'GET', timeout: totalMs,
|
|
1114
|
+
}, (res) => {
|
|
1115
|
+
let body = ''
|
|
1116
|
+
res.on('data', (d) => { body += d })
|
|
1117
|
+
res.on('end', () => { try { resolve(JSON.parse(body)) } catch { resolve(null) } })
|
|
1118
|
+
})
|
|
1119
|
+
req.on('error', () => resolve(null))
|
|
1120
|
+
req.on('timeout', () => { try { req.destroy() } catch {} resolve(null) })
|
|
1121
|
+
req.end()
|
|
1122
|
+
})
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1215
1125
|
/**
|
|
1216
|
-
* Auto-launch the primary configured AI client and print a "Try this now" banner
|
|
1217
|
-
*
|
|
1126
|
+
* Auto-launch the primary configured AI client and print a "Try this now" banner —
|
|
1127
|
+
* derived entirely from ConfigureAll's report and the served catalog (#3212 item 23):
|
|
1128
|
+
* no client name is ever typed here. macOS actually opens the app (via the
|
|
1129
|
+
* catalog's own .app detectPath); win32/linux only name the next_step, since
|
|
1130
|
+
* neither platform has an "open this app" primitive LMCP can safely use.
|
|
1131
|
+
* Non-fatal at every step — install succeeds even if the catalog fetch or
|
|
1132
|
+
* open() fails or times out.
|
|
1218
1133
|
*/
|
|
1219
|
-
function _autoLaunchClient(
|
|
1220
|
-
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
//
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1134
|
+
async function _autoLaunchClient(clientsRun, opts = {}) {
|
|
1135
|
+
const platform = opts.platform || process.platform
|
|
1136
|
+
const exec = opts.execFileSync || execFileSync
|
|
1137
|
+
const fetchCatalog = opts.fetchCatalog || _fetchClientsCatalog
|
|
1138
|
+
|
|
1139
|
+
const clientes = (clientsRun && clientsRun.report && clientsRun.report.clients) || []
|
|
1140
|
+
// "restart"/"restart_agent_mode" are the only next_steps that name reopening
|
|
1141
|
+
// an app; "new_session" (e.g. Claude Code, a CLI) and "open_mcp_page" have no
|
|
1142
|
+
// app to relaunch, and the catalog lookup below naturally no-ops for them too
|
|
1143
|
+
// (a CLI-only entry has no macOS .app detectPath to find).
|
|
1144
|
+
const entry = clientes.find(c => c.status === 'configured' && c.next_step && c.next_step.startsWith('restart'))
|
|
1145
|
+
if (!entry) return
|
|
1146
|
+
|
|
1147
|
+
if (platform !== 'darwin') {
|
|
1148
|
+
const verb = entry.next_step === 'restart_agent_mode'
|
|
1149
|
+
? `Toggle agent mode off and back on in ${entry.name}`
|
|
1150
|
+
: `Restart ${entry.name}`
|
|
1151
|
+
console.log(`\n✅ LMCP is ready! ${verb} to activate it.\n`)
|
|
1152
|
+
return
|
|
1238
1153
|
}
|
|
1239
|
-
|
|
1154
|
+
|
|
1155
|
+
const catalog = await fetchCatalog()
|
|
1156
|
+
const found = catalog && (catalog.clients || []).find(c => c.id === entry.id)
|
|
1157
|
+
const macos = found && found.platforms && found.platforms.macos
|
|
1158
|
+
const appPath = ((macos && macos.detectPaths) || []).find(p => p.endsWith('.app'))
|
|
1159
|
+
if (!appPath) return
|
|
1240
1160
|
|
|
1241
1161
|
try {
|
|
1242
|
-
|
|
1243
|
-
console.log(`\n✅ LMCP is ready! ${
|
|
1244
|
-
console.log('Try asking
|
|
1162
|
+
exec('open', [appPath.replace(/^~/, HOME)], { stdio: 'pipe', timeout: 5000 })
|
|
1163
|
+
console.log(`\n✅ LMCP is ready! ${entry.name} is opening now.\n`)
|
|
1164
|
+
console.log('Try asking: "Read my last 3 emails and summarize them"\n')
|
|
1245
1165
|
} catch {
|
|
1246
1166
|
// open failed (app not installed or already open) — non-fatal
|
|
1247
|
-
return
|
|
1248
1167
|
}
|
|
1249
|
-
|
|
1250
1168
|
}
|
|
1251
1169
|
|
|
1252
1170
|
module.exports = {
|
|
1253
|
-
runSetup,
|
|
1171
|
+
runSetup, runConfigureClients, _findServerBinary, _launcherArgsForConfigureClients,
|
|
1172
|
+
CLIENTS_REPORT_SCHEMA,
|
|
1173
|
+
_notSupportedOnPlatformResult,
|
|
1254
1174
|
_launcherSpawnSpec, _writeLaunchScript, _writeLaunchSpec,
|
|
1255
1175
|
_getMachineId, _healthCheckSpec, _localMcpConfigDir,
|
|
1256
1176
|
_cloudTokenPath, _readCachedCloudToken, _writeCloudToken, _registerAnonToken,
|
|
1177
|
+
_autoLaunchClient, _fetchClientsCatalog,
|
|
1257
1178
|
}
|