local-mcp 3.0.136 → 3.0.138
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/index.js +14 -0
- package/package.json +1 -1
- package/setup.js +273 -37
package/index.js
CHANGED
|
@@ -24,6 +24,20 @@ if (process.platform !== 'darwin') {
|
|
|
24
24
|
process.exit(1)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Node version guard — Claude Desktop can silently launch LMCP with an old
|
|
28
|
+
// nvm default (e.g. v11) that makes `npx -y` fail before this file loads.
|
|
29
|
+
// If this code IS running on an old Node, surface the error clearly so the
|
|
30
|
+
// user sees it in Claude Desktop's MCP logs instead of a cryptic spawn error.
|
|
31
|
+
if (parseInt(process.version.slice(1)) < 16) {
|
|
32
|
+
const msg = `LMCP requires Node 16+. Running ${process.version}.\n` +
|
|
33
|
+
'If you use nvm, run: nvm alias default 22\n' +
|
|
34
|
+
'Then restart Claude Desktop / Cursor.'
|
|
35
|
+
process.stderr.write(msg + '\n')
|
|
36
|
+
// Exit with a non-zero code so the MCP host marks the server as failed
|
|
37
|
+
// (vs hanging forever with no output).
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
const cmd = process.argv[2]
|
|
28
42
|
|
|
29
43
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.138",
|
|
4
4
|
"description": "LMCP — connect Claude Desktop, Cursor, Windsurf to Mail, Calendar, Contacts, Teams, OneDrive on macOS. Privacy-first: all data stays on your Mac.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/setup.js
CHANGED
|
@@ -16,6 +16,49 @@ const { execSync, execFileSync } = require('child_process')
|
|
|
16
16
|
const HOME = os.homedir()
|
|
17
17
|
const NPX_COMMAND = 'npx'
|
|
18
18
|
const NPX_ARGS = ['-y', 'local-mcp@latest']
|
|
19
|
+
|
|
20
|
+
// Resolve absolute path to npx from the current Node binary so Claude Desktop
|
|
21
|
+
// doesn't pick up an old npx from its own PATH (common with nvm users whose
|
|
22
|
+
// nvm default points to Node v11/v12 — those npx versions don't support -y).
|
|
23
|
+
// Falls back to plain 'npx' if resolution fails (non-nvm setups, symlinks, etc.)
|
|
24
|
+
function _resolveNpxPath() {
|
|
25
|
+
try {
|
|
26
|
+
const candidate = path.join(path.dirname(process.execPath), 'npx')
|
|
27
|
+
if (fs.existsSync(candidate)) return candidate
|
|
28
|
+
} catch {}
|
|
29
|
+
return NPX_COMMAND
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Write a shell launcher script that wraps npx with a Node version check and
|
|
33
|
+
// curl-based telemetry. This gives us visibility into failures that happen
|
|
34
|
+
// BEFORE Node loads our code (e.g. old nvm default in Claude Desktop's PATH).
|
|
35
|
+
// Returns the path to the launcher, or null if writing fails.
|
|
36
|
+
function _writeLaunchScript(npxAbsPath, cacheDir) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
39
|
+
const launcherPath = path.join(cacheDir, 'lmcp-launch.sh')
|
|
40
|
+
const script = [
|
|
41
|
+
'#!/bin/bash',
|
|
42
|
+
'# LMCP launcher — generated by npx local-mcp setup, do not edit.',
|
|
43
|
+
'# Checks Node version before launching npx; sends telemetry if too old.',
|
|
44
|
+
'NODE_VER=$(node --version 2>/dev/null || echo "none")',
|
|
45
|
+
'NODE_MAJOR=$(echo "$NODE_VER" | sed \'s/v//\' | cut -d. -f1)',
|
|
46
|
+
'if [ "${NODE_MAJOR:-0}" -lt 16 ] 2>/dev/null; then',
|
|
47
|
+
' # curl telemetry — no Node dependency, fire-and-forget',
|
|
48
|
+
` curl -sf --max-time 3 -X POST "https://${BACKEND_HOST}/install-event" \\`,
|
|
49
|
+
' -H "Content-Type: application/json" \\',
|
|
50
|
+
' -d "{\\\"stage\\\":\\\"node_too_old\\\",\\\"version\\\":\\\"$NODE_VER\\\"}" &>/dev/null &',
|
|
51
|
+
' printf \'LMCP requires Node 16+. Current: %s\\nFix: nvm alias default 22 — then restart Claude Desktop.\\n\' "$NODE_VER" >&2',
|
|
52
|
+
' exit 1',
|
|
53
|
+
'fi',
|
|
54
|
+
`exec "${npxAbsPath}" -y local-mcp@latest "$@"`,
|
|
55
|
+
].join('\n') + '\n'
|
|
56
|
+
fs.writeFileSync(launcherPath, script, { mode: 0o755 })
|
|
57
|
+
return launcherPath
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
19
62
|
const STABLE_LINK = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin', 'local-mcp-server')
|
|
20
63
|
const BACKEND_HOST = 'office-mcp-production.up.railway.app'
|
|
21
64
|
|
|
@@ -41,10 +84,17 @@ const CLIENTS = [
|
|
|
41
84
|
detect: () => fs.existsSync(path.join(HOME, '.codeium', 'windsurf')) || _appExists('Windsurf'),
|
|
42
85
|
},
|
|
43
86
|
{
|
|
44
|
-
id: 'vscode
|
|
45
|
-
name: 'VS Code
|
|
87
|
+
id: 'vscode',
|
|
88
|
+
name: 'VS Code',
|
|
46
89
|
cfgPath: path.join(HOME, '.vscode', 'mcp.json'),
|
|
47
|
-
detect: () => _cmdExists('code') || fs.existsSync(path.join(HOME, '.vscode')),
|
|
90
|
+
detect: () => _cmdExists('code') || _appExists('Visual Studio Code') || fs.existsSync(path.join(HOME, '.vscode')),
|
|
91
|
+
vscode: true, // native MCP uses "servers" key + type:"stdio"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'roo-cline',
|
|
95
|
+
name: 'Roo-Cline',
|
|
96
|
+
cfgPath: path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json'),
|
|
97
|
+
detect: () => fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline')),
|
|
48
98
|
},
|
|
49
99
|
{
|
|
50
100
|
id: 'zed',
|
|
@@ -72,42 +122,110 @@ function _cmdExists(cmd) {
|
|
|
72
122
|
try { execSync(`which ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
|
|
73
123
|
}
|
|
74
124
|
|
|
75
|
-
|
|
125
|
+
// ── Safe config read ─────────────────────────────────────────────────────────
|
|
126
|
+
// Returns { data, wasEmpty, hadParseError, error }.
|
|
127
|
+
// If the file exists but JSON.parse fails we BACK IT UP and return hadParseError=true
|
|
128
|
+
// so callers can skip the write instead of silently wiping the user's other MCPs.
|
|
129
|
+
|
|
130
|
+
function _safeReadConfig(filePath) {
|
|
131
|
+
if (!fs.existsSync(filePath)) {
|
|
132
|
+
return { data: {}, wasEmpty: true, hadParseError: false, error: '' }
|
|
133
|
+
}
|
|
134
|
+
let raw
|
|
135
|
+
try { raw = fs.readFileSync(filePath, 'utf8').trim() } catch (e) {
|
|
136
|
+
return { data: {}, wasEmpty: true, hadParseError: false, error: e.message }
|
|
137
|
+
}
|
|
138
|
+
if (!raw || raw === '{}') {
|
|
139
|
+
return { data: {}, wasEmpty: true, hadParseError: false, error: '' }
|
|
140
|
+
}
|
|
76
141
|
try {
|
|
77
|
-
return JSON.parse(
|
|
78
|
-
} catch {
|
|
79
|
-
|
|
142
|
+
return { data: JSON.parse(raw), wasEmpty: false, hadParseError: false, error: '' }
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// Existing config is not valid JSON — back it up, never overwrite
|
|
145
|
+
try { fs.copyFileSync(filePath, filePath + '.lmcp-backup') } catch {}
|
|
146
|
+
return { data: null, wasEmpty: false, hadParseError: true, error: e.message }
|
|
80
147
|
}
|
|
81
148
|
}
|
|
82
149
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
// ── Atomic write ──────────────────────────────────────────────────────────────
|
|
151
|
+
// Writes to a .tmp file then renames (atomic on POSIX). Verifies the written
|
|
152
|
+
// file is valid JSON and contains the local-mcp entry before returning ok=true.
|
|
153
|
+
|
|
154
|
+
function _atomicWriteConfig(filePath, data) {
|
|
155
|
+
const json = JSON.stringify(data, null, 2) + '\n'
|
|
156
|
+
try { JSON.parse(json) } catch (e) {
|
|
157
|
+
return { ok: false, error: 'merge produced invalid JSON: ' + e.message }
|
|
158
|
+
}
|
|
159
|
+
const tmpPath = filePath + '.lmcp-tmp'
|
|
160
|
+
try {
|
|
161
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
162
|
+
fs.writeFileSync(tmpPath, json, 'utf8')
|
|
163
|
+
fs.renameSync(tmpPath, filePath)
|
|
164
|
+
} catch (e) {
|
|
165
|
+
try { fs.unlinkSync(tmpPath) } catch {}
|
|
166
|
+
return { ok: false, error: e.message }
|
|
167
|
+
}
|
|
168
|
+
// Post-write verify
|
|
169
|
+
try {
|
|
170
|
+
const v = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
171
|
+
const hasEntry = !!(
|
|
172
|
+
(v.mcpServers && v.mcpServers['local-mcp']) ||
|
|
173
|
+
(v.servers && v.servers['local-mcp']) ||
|
|
174
|
+
(v.context_servers && v.context_servers['local-mcp'])
|
|
175
|
+
)
|
|
176
|
+
if (!hasEntry) return { ok: false, error: 'verify failed: local-mcp not found after write' }
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return { ok: false, error: 'verify read failed: ' + e.message }
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, error: '' }
|
|
86
181
|
}
|
|
87
182
|
|
|
88
|
-
// ── Inyectar config MCP en un cliente
|
|
183
|
+
// ── Inyectar config MCP en un cliente ────────────────────────────────────────
|
|
184
|
+
// Returns { ok, stage, error, existingMcpCount, preservedServers }.
|
|
185
|
+
// Never overwrites a file whose JSON cannot be parsed.
|
|
89
186
|
|
|
90
187
|
function injectMcpConfig(client, command = NPX_COMMAND, args = NPX_ARGS) {
|
|
91
|
-
|
|
188
|
+
// 1. Read existing config safely
|
|
189
|
+
const read = _safeReadConfig(client.cfgPath)
|
|
190
|
+
if (read.hadParseError) {
|
|
191
|
+
return { ok: false, stage: 'config_read', error: read.error, hadParseError: true,
|
|
192
|
+
existingMcpCount: 0, preservedServers: [] }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const cfg = read.data || {}
|
|
92
196
|
|
|
197
|
+
// Count other MCP servers before merge (for telemetry)
|
|
198
|
+
const existingServers = Object.keys(
|
|
199
|
+
cfg.mcpServers || cfg.servers || cfg.context_servers || {}
|
|
200
|
+
)
|
|
201
|
+
const otherServers = existingServers.filter(k => k !== 'local-mcp' && k !== 'office-mcp')
|
|
202
|
+
|
|
203
|
+
// 2. Merge — preserve everything, only add/replace local-mcp
|
|
93
204
|
if (client.zed) {
|
|
94
|
-
// Zed usa { "context_servers": { "local-mcp": { "command": {...} } } }
|
|
95
205
|
cfg.context_servers = cfg.context_servers || {}
|
|
96
|
-
cfg.context_servers['local-mcp'] = {
|
|
97
|
-
|
|
98
|
-
|
|
206
|
+
cfg.context_servers['local-mcp'] = { command: { path: command, args: args ?? [] } }
|
|
207
|
+
} else if (client.vscode) {
|
|
208
|
+
// VS Code native MCP (1.99+) uses "servers" key with type:"stdio"
|
|
209
|
+
cfg.servers = cfg.servers || {}
|
|
210
|
+
cfg.servers['local-mcp'] = { type: 'stdio', command, args: args ?? [] }
|
|
99
211
|
} else {
|
|
100
|
-
// Formato estándar MCP: { "mcpServers": { "local-mcp": { "command": "...", "args": [...] } } }
|
|
101
212
|
cfg.mcpServers = cfg.mcpServers || {}
|
|
102
|
-
// Limpiar entradas legacy
|
|
103
213
|
delete cfg.mcpServers['office-mcp']
|
|
104
214
|
const entry = { command }
|
|
105
215
|
if (args !== undefined) entry.args = args
|
|
106
216
|
cfg.mcpServers['local-mcp'] = entry
|
|
107
217
|
}
|
|
108
218
|
|
|
109
|
-
|
|
110
|
-
|
|
219
|
+
// 3. Atomic write + verify
|
|
220
|
+
const write = _atomicWriteConfig(client.cfgPath, cfg)
|
|
221
|
+
return {
|
|
222
|
+
ok: write.ok,
|
|
223
|
+
stage: write.ok ? 'config_write' : 'config_write_failed',
|
|
224
|
+
error: write.error,
|
|
225
|
+
hadParseError: false,
|
|
226
|
+
existingMcpCount: existingServers.length,
|
|
227
|
+
preservedServers: otherServers,
|
|
228
|
+
}
|
|
111
229
|
}
|
|
112
230
|
|
|
113
231
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
@@ -137,9 +255,21 @@ async function runSetup(opts = {}) {
|
|
|
137
255
|
console.log('║ LMCP — Setup Wizard ║')
|
|
138
256
|
console.log('╚══════════════════════════════════════╝\n')
|
|
139
257
|
|
|
258
|
+
// Warn if running on an old Node version. Claude Desktop resolves 'npx' from
|
|
259
|
+
// its own PATH (not the user's shell), so an old nvm default (v11/v12) causes
|
|
260
|
+
// a silent "You must supply a command" failure on first launch.
|
|
261
|
+
const nodeMajor = parseInt(process.version.slice(1))
|
|
262
|
+
if (nodeMajor < 16) {
|
|
263
|
+
console.error(`\n⚠️ Warning: you're running Node ${process.version}.`)
|
|
264
|
+
console.error(' Claude Desktop may fail to start LMCP because it finds an old npx on its PATH.')
|
|
265
|
+
console.error(' Fix: nvm alias default 22 (then relaunch Claude Desktop)\n')
|
|
266
|
+
}
|
|
267
|
+
|
|
140
268
|
// Pre-download binary so first launch is instant
|
|
141
|
-
// All clients use
|
|
142
|
-
|
|
269
|
+
// All clients use a shell launcher that wraps npx (self-healing if binary is
|
|
270
|
+
// missing/broken, and surfaces telemetry + clear errors if Node is too old).
|
|
271
|
+
let npxAbsPath = _resolveNpxPath() // baked into the launcher script
|
|
272
|
+
let stableCommand = NPX_COMMAND // fallback; replaced below once CACHE_DIR is known
|
|
143
273
|
let stableArgs = NPX_ARGS
|
|
144
274
|
let binaryVersion = ''
|
|
145
275
|
try {
|
|
@@ -177,14 +307,33 @@ async function runSetup(opts = {}) {
|
|
|
177
307
|
if (fs.existsSync(settingsSrc)) fs.copyFileSync(settingsSrc, settingsDst)
|
|
178
308
|
} catch {}
|
|
179
309
|
process.stderr.write('✓ Runtime ready\n\n')
|
|
310
|
+
// Write the launcher script now that we have CACHE_DIR.
|
|
311
|
+
// The launcher uses curl (no Node dependency) to send telemetry if Node is
|
|
312
|
+
// too old, then exec's npx. Claude Desktop runs this script instead of npx
|
|
313
|
+
// directly, so failures are visible in MCP logs AND in our backend events.
|
|
314
|
+
const launcherPath = _writeLaunchScript(npxAbsPath, CACHE_DIR)
|
|
315
|
+
if (launcherPath) stableCommand = launcherPath
|
|
180
316
|
} catch (err) {
|
|
181
317
|
process.stderr.write(` (Runtime download failed, will download on first run: ${err.message})\n\n`)
|
|
318
|
+
// Still try to write a launcher even if binary download failed — the version
|
|
319
|
+
// check + telemetry path is independent of the binary.
|
|
320
|
+
try {
|
|
321
|
+
const { CACHE_DIR } = require('./download')
|
|
322
|
+
const launcherPath = _writeLaunchScript(npxAbsPath, CACHE_DIR)
|
|
323
|
+
if (launcherPath) stableCommand = launcherPath
|
|
324
|
+
} catch {}
|
|
182
325
|
}
|
|
183
|
-
//
|
|
184
|
-
//
|
|
326
|
+
// stableCommand is now the shell launcher script path. It wraps npx with a
|
|
327
|
+
// Node version check + curl telemetry before exec'ing. If launcher creation
|
|
328
|
+
// failed it falls back to plain 'npx'.
|
|
185
329
|
|
|
186
330
|
// Detectar clientes
|
|
187
331
|
const detected = CLIENTS.filter(c => c.detect())
|
|
332
|
+
const notDetected = CLIENTS.filter(c => !c.detect())
|
|
333
|
+
_trackSetupStep('detect', detected.length > 0 ? 'ok' : 'no_clients', '', {
|
|
334
|
+
clientsFound: detected.map(c => c.id),
|
|
335
|
+
clientsNotFound: notDetected.map(c => c.id),
|
|
336
|
+
})
|
|
188
337
|
|
|
189
338
|
if (detected.length === 0) {
|
|
190
339
|
const entry = stableArgs !== undefined
|
|
@@ -222,14 +371,25 @@ async function runSetup(opts = {}) {
|
|
|
222
371
|
const failed = []
|
|
223
372
|
|
|
224
373
|
for (const client of detected) {
|
|
225
|
-
|
|
226
|
-
|
|
374
|
+
const result = injectMcpConfig(client, stableCommand, stableArgs)
|
|
375
|
+
if (result.ok) {
|
|
376
|
+
_trackSetupStep('config_write', 'ok', client.id, {
|
|
377
|
+
existingMcpCount: result.existingMcpCount,
|
|
378
|
+
preservedServers: result.preservedServers,
|
|
379
|
+
})
|
|
227
380
|
_trackConfigWritten(client.id || client.name, client.name)
|
|
228
381
|
configured.push(client.name)
|
|
229
382
|
console.log(`✓ ${client.name} configured`)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
383
|
+
} else {
|
|
384
|
+
const status = result.hadParseError ? 'parse_error' : 'write_error'
|
|
385
|
+
_trackSetupStep('config_write', status, client.id, { error: result.error })
|
|
386
|
+
failed.push({ name: client.name, reason: status, error: result.error })
|
|
387
|
+
if (result.hadParseError) {
|
|
388
|
+
console.error(`✗ ${client.name}: existing config has invalid JSON — backed up to ${client.cfgPath}.lmcp-backup`)
|
|
389
|
+
console.error(` Edit or remove the backup to fix: ${result.error}`)
|
|
390
|
+
} else {
|
|
391
|
+
console.error(`✗ ${client.name}: ${result.error}`)
|
|
392
|
+
}
|
|
233
393
|
}
|
|
234
394
|
}
|
|
235
395
|
|
|
@@ -240,10 +400,11 @@ async function runSetup(opts = {}) {
|
|
|
240
400
|
let email = process.env.LMCP_EMAIL || ''
|
|
241
401
|
if (email) {
|
|
242
402
|
try {
|
|
243
|
-
const
|
|
403
|
+
const r = _safeReadConfig(cfgFile)
|
|
404
|
+
const cfg = r.data || {}
|
|
244
405
|
if (!cfg.license_email) {
|
|
245
406
|
cfg.license_email = email
|
|
246
|
-
|
|
407
|
+
_atomicWriteConfig(cfgFile, cfg)
|
|
247
408
|
console.log(`✓ Email saved: ${email}`)
|
|
248
409
|
}
|
|
249
410
|
} catch { /* config not created yet — will be created on first run */ }
|
|
@@ -293,8 +454,9 @@ async function runSetup(opts = {}) {
|
|
|
293
454
|
email = ans
|
|
294
455
|
emailPromptResult = 'submitted'
|
|
295
456
|
try {
|
|
296
|
-
const
|
|
297
|
-
|
|
457
|
+
const r = _safeReadConfig(cfgFile)
|
|
458
|
+
const cfg = r.data || {}
|
|
459
|
+
if (!cfg.license_email) { cfg.license_email = email; _atomicWriteConfig(cfgFile, cfg) }
|
|
298
460
|
} catch {}
|
|
299
461
|
console.log(' ✓ Email saved')
|
|
300
462
|
} else {
|
|
@@ -308,6 +470,13 @@ async function runSetup(opts = {}) {
|
|
|
308
470
|
|
|
309
471
|
// Health check — verify binary works before showing success
|
|
310
472
|
const healthOk = _runHealthCheck()
|
|
473
|
+
_trackSetupStep('binary_health', healthOk ? 'ok' : 'failed', '', {})
|
|
474
|
+
|
|
475
|
+
// setup_complete summary
|
|
476
|
+
_trackSetupStep('setup_complete', configured.length > 0 ? 'ok' : 'failed', '', {
|
|
477
|
+
clientsFound: configured,
|
|
478
|
+
clientsNotFound: failed.map(f => f.name || f),
|
|
479
|
+
})
|
|
311
480
|
|
|
312
481
|
const hasCursor = configured.includes('Cursor')
|
|
313
482
|
|
|
@@ -340,17 +509,19 @@ async function runSetup(opts = {}) {
|
|
|
340
509
|
} else {
|
|
341
510
|
const primaryClient = configured[0]
|
|
342
511
|
console.log('┌─────────────────────────────────────────────────────┐')
|
|
343
|
-
console.log('│ NEXT STEP —
|
|
512
|
+
console.log('│ NEXT STEP — restart to activate: │')
|
|
344
513
|
console.log('│ │')
|
|
345
|
-
console.log(`│ Quit
|
|
346
|
-
console.log(
|
|
514
|
+
console.log(`│ 1. Quit ${primaryClient} completely (Cmd+Q)${''.padEnd(Math.max(0, 30 - primaryClient.length))}│`)
|
|
515
|
+
console.log(`│ 2. Reopen ${primaryClient.padEnd(41)}│`)
|
|
347
516
|
console.log('│ │')
|
|
348
517
|
console.log('│ Then try: "Summarize my unread emails" │')
|
|
349
518
|
console.log('└─────────────────────────────────────────────────────┘\n')
|
|
519
|
+
_trackRestartPrompted(primaryClient)
|
|
350
520
|
}
|
|
351
521
|
}
|
|
352
522
|
if (failed.length > 0) {
|
|
353
|
-
|
|
523
|
+
const failNames = failed.map(f => (typeof f === 'string' ? f : f.name)).join(', ')
|
|
524
|
+
console.log(`⚠ Could not configure: ${failNames} — check permissions and try again.\n`)
|
|
354
525
|
}
|
|
355
526
|
console.log(' Setup guide & troubleshooting: https://local-mcp.com/setup\n')
|
|
356
527
|
|
|
@@ -540,6 +711,46 @@ function _migrateCurlLaunchAgent() {
|
|
|
540
711
|
} catch { /* non-fatal — don't block install on unexpected plist content */ }
|
|
541
712
|
}
|
|
542
713
|
|
|
714
|
+
// ── Setup funnel telemetry ────────────────────────────────────────────────────
|
|
715
|
+
// Fire-and-forget. Sends one event per step to /setup-step so we can see exactly
|
|
716
|
+
// where each machine's install funnel breaks without waiting for a heartbeat.
|
|
717
|
+
|
|
718
|
+
function _trackSetupStep(step, status, client, detail) {
|
|
719
|
+
try {
|
|
720
|
+
const https = require('https')
|
|
721
|
+
const payload = {
|
|
722
|
+
machine_id: _getMachineId(),
|
|
723
|
+
install_id: process.env.INSTALL_ID || '',
|
|
724
|
+
method: process.env.LMCP_METHOD || 'npm',
|
|
725
|
+
step,
|
|
726
|
+
status,
|
|
727
|
+
client: client || '',
|
|
728
|
+
error: (detail && detail.error) ? String(detail.error).slice(0, 300) : '',
|
|
729
|
+
}
|
|
730
|
+
if (detail) {
|
|
731
|
+
if (detail.existingMcpCount >= 0) payload.existing_mcp_count = detail.existingMcpCount
|
|
732
|
+
if (detail.preservedServers && detail.preservedServers.length)
|
|
733
|
+
payload.preserved_servers = detail.preservedServers.join(',')
|
|
734
|
+
if (detail.clientsFound) payload.clients_found = detail.clientsFound.join(',')
|
|
735
|
+
if (detail.clientsNotFound) payload.clients_not_found = detail.clientsNotFound.join(',')
|
|
736
|
+
if (detail.toolCount >= 0) payload.tool_count = detail.toolCount
|
|
737
|
+
if (detail.binaryVersion) payload.binary_version = detail.binaryVersion
|
|
738
|
+
}
|
|
739
|
+
const data = JSON.stringify(payload)
|
|
740
|
+
const req = https.request({
|
|
741
|
+
hostname: BACKEND_HOST,
|
|
742
|
+
path: '/setup-step',
|
|
743
|
+
method: 'POST',
|
|
744
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
745
|
+
timeout: 5000,
|
|
746
|
+
})
|
|
747
|
+
req.on('response', (res) => res.resume())
|
|
748
|
+
req.on('error', () => {})
|
|
749
|
+
req.write(data)
|
|
750
|
+
req.end()
|
|
751
|
+
} catch { /* non-fatal */ }
|
|
752
|
+
}
|
|
753
|
+
|
|
543
754
|
function _trackConfigWritten(clientId, clientName) {
|
|
544
755
|
try {
|
|
545
756
|
const https = require('https')
|
|
@@ -594,6 +805,31 @@ function _trackEmailPrompt(result, email, errorMsg) {
|
|
|
594
805
|
} catch { /* non-fatal */ }
|
|
595
806
|
}
|
|
596
807
|
|
|
808
|
+
function _trackRestartPrompted(clientName) {
|
|
809
|
+
try {
|
|
810
|
+
const https = require('https')
|
|
811
|
+
const machineId = _getMachineId()
|
|
812
|
+
const payload = {
|
|
813
|
+
stage: 'restart_prompted',
|
|
814
|
+
client_name: clientName,
|
|
815
|
+
machine_id: machineId,
|
|
816
|
+
install_id: process.env.INSTALL_ID || '',
|
|
817
|
+
}
|
|
818
|
+
const data = JSON.stringify(payload)
|
|
819
|
+
const req = https.request({
|
|
820
|
+
hostname: BACKEND_HOST,
|
|
821
|
+
path: '/install-event',
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
|
|
824
|
+
timeout: 5000,
|
|
825
|
+
})
|
|
826
|
+
req.on('response', (res) => res.resume())
|
|
827
|
+
req.on('error', () => {})
|
|
828
|
+
req.write(data)
|
|
829
|
+
req.end()
|
|
830
|
+
} catch { /* non-fatal */ }
|
|
831
|
+
}
|
|
832
|
+
|
|
597
833
|
function _pingInstall(clients, method, email = '', binaryVersion = '') {
|
|
598
834
|
try {
|
|
599
835
|
const https = require('https')
|