local-mcp 3.0.136 → 3.0.137
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 +200 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.137",
|
|
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
|
@@ -41,10 +41,17 @@ const CLIENTS = [
|
|
|
41
41
|
detect: () => fs.existsSync(path.join(HOME, '.codeium', 'windsurf')) || _appExists('Windsurf'),
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
|
-
id: 'vscode
|
|
45
|
-
name: 'VS Code
|
|
44
|
+
id: 'vscode',
|
|
45
|
+
name: 'VS Code',
|
|
46
46
|
cfgPath: path.join(HOME, '.vscode', 'mcp.json'),
|
|
47
|
-
detect: () => _cmdExists('code') || fs.existsSync(path.join(HOME, '.vscode')),
|
|
47
|
+
detect: () => _cmdExists('code') || _appExists('Visual Studio Code') || fs.existsSync(path.join(HOME, '.vscode')),
|
|
48
|
+
vscode: true, // native MCP uses "servers" key + type:"stdio"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'roo-cline',
|
|
52
|
+
name: 'Roo-Cline',
|
|
53
|
+
cfgPath: path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json'),
|
|
54
|
+
detect: () => fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline')),
|
|
48
55
|
},
|
|
49
56
|
{
|
|
50
57
|
id: 'zed',
|
|
@@ -72,42 +79,110 @@ function _cmdExists(cmd) {
|
|
|
72
79
|
try { execSync(`which ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
// ── Safe config read ─────────────────────────────────────────────────────────
|
|
83
|
+
// Returns { data, wasEmpty, hadParseError, error }.
|
|
84
|
+
// If the file exists but JSON.parse fails we BACK IT UP and return hadParseError=true
|
|
85
|
+
// so callers can skip the write instead of silently wiping the user's other MCPs.
|
|
86
|
+
|
|
87
|
+
function _safeReadConfig(filePath) {
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
return { data: {}, wasEmpty: true, hadParseError: false, error: '' }
|
|
90
|
+
}
|
|
91
|
+
let raw
|
|
92
|
+
try { raw = fs.readFileSync(filePath, 'utf8').trim() } catch (e) {
|
|
93
|
+
return { data: {}, wasEmpty: true, hadParseError: false, error: e.message }
|
|
94
|
+
}
|
|
95
|
+
if (!raw || raw === '{}') {
|
|
96
|
+
return { data: {}, wasEmpty: true, hadParseError: false, error: '' }
|
|
97
|
+
}
|
|
76
98
|
try {
|
|
77
|
-
return JSON.parse(
|
|
78
|
-
} catch {
|
|
79
|
-
|
|
99
|
+
return { data: JSON.parse(raw), wasEmpty: false, hadParseError: false, error: '' }
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// Existing config is not valid JSON — back it up, never overwrite
|
|
102
|
+
try { fs.copyFileSync(filePath, filePath + '.lmcp-backup') } catch {}
|
|
103
|
+
return { data: null, wasEmpty: false, hadParseError: true, error: e.message }
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
// ── Atomic write ──────────────────────────────────────────────────────────────
|
|
108
|
+
// Writes to a .tmp file then renames (atomic on POSIX). Verifies the written
|
|
109
|
+
// file is valid JSON and contains the local-mcp entry before returning ok=true.
|
|
110
|
+
|
|
111
|
+
function _atomicWriteConfig(filePath, data) {
|
|
112
|
+
const json = JSON.stringify(data, null, 2) + '\n'
|
|
113
|
+
try { JSON.parse(json) } catch (e) {
|
|
114
|
+
return { ok: false, error: 'merge produced invalid JSON: ' + e.message }
|
|
115
|
+
}
|
|
116
|
+
const tmpPath = filePath + '.lmcp-tmp'
|
|
117
|
+
try {
|
|
118
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
119
|
+
fs.writeFileSync(tmpPath, json, 'utf8')
|
|
120
|
+
fs.renameSync(tmpPath, filePath)
|
|
121
|
+
} catch (e) {
|
|
122
|
+
try { fs.unlinkSync(tmpPath) } catch {}
|
|
123
|
+
return { ok: false, error: e.message }
|
|
124
|
+
}
|
|
125
|
+
// Post-write verify
|
|
126
|
+
try {
|
|
127
|
+
const v = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
128
|
+
const hasEntry = !!(
|
|
129
|
+
(v.mcpServers && v.mcpServers['local-mcp']) ||
|
|
130
|
+
(v.servers && v.servers['local-mcp']) ||
|
|
131
|
+
(v.context_servers && v.context_servers['local-mcp'])
|
|
132
|
+
)
|
|
133
|
+
if (!hasEntry) return { ok: false, error: 'verify failed: local-mcp not found after write' }
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return { ok: false, error: 'verify read failed: ' + e.message }
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, error: '' }
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
// ── Inyectar config MCP en un cliente
|
|
140
|
+
// ── Inyectar config MCP en un cliente ────────────────────────────────────────
|
|
141
|
+
// Returns { ok, stage, error, existingMcpCount, preservedServers }.
|
|
142
|
+
// Never overwrites a file whose JSON cannot be parsed.
|
|
89
143
|
|
|
90
144
|
function injectMcpConfig(client, command = NPX_COMMAND, args = NPX_ARGS) {
|
|
91
|
-
|
|
145
|
+
// 1. Read existing config safely
|
|
146
|
+
const read = _safeReadConfig(client.cfgPath)
|
|
147
|
+
if (read.hadParseError) {
|
|
148
|
+
return { ok: false, stage: 'config_read', error: read.error, hadParseError: true,
|
|
149
|
+
existingMcpCount: 0, preservedServers: [] }
|
|
150
|
+
}
|
|
92
151
|
|
|
152
|
+
const cfg = read.data || {}
|
|
153
|
+
|
|
154
|
+
// Count other MCP servers before merge (for telemetry)
|
|
155
|
+
const existingServers = Object.keys(
|
|
156
|
+
cfg.mcpServers || cfg.servers || cfg.context_servers || {}
|
|
157
|
+
)
|
|
158
|
+
const otherServers = existingServers.filter(k => k !== 'local-mcp' && k !== 'office-mcp')
|
|
159
|
+
|
|
160
|
+
// 2. Merge — preserve everything, only add/replace local-mcp
|
|
93
161
|
if (client.zed) {
|
|
94
|
-
// Zed usa { "context_servers": { "local-mcp": { "command": {...} } } }
|
|
95
162
|
cfg.context_servers = cfg.context_servers || {}
|
|
96
|
-
cfg.context_servers['local-mcp'] = {
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
cfg.context_servers['local-mcp'] = { command: { path: command, args: args ?? [] } }
|
|
164
|
+
} else if (client.vscode) {
|
|
165
|
+
// VS Code native MCP (1.99+) uses "servers" key with type:"stdio"
|
|
166
|
+
cfg.servers = cfg.servers || {}
|
|
167
|
+
cfg.servers['local-mcp'] = { type: 'stdio', command, args: args ?? [] }
|
|
99
168
|
} else {
|
|
100
|
-
// Formato estándar MCP: { "mcpServers": { "local-mcp": { "command": "...", "args": [...] } } }
|
|
101
169
|
cfg.mcpServers = cfg.mcpServers || {}
|
|
102
|
-
// Limpiar entradas legacy
|
|
103
170
|
delete cfg.mcpServers['office-mcp']
|
|
104
171
|
const entry = { command }
|
|
105
172
|
if (args !== undefined) entry.args = args
|
|
106
173
|
cfg.mcpServers['local-mcp'] = entry
|
|
107
174
|
}
|
|
108
175
|
|
|
109
|
-
|
|
110
|
-
|
|
176
|
+
// 3. Atomic write + verify
|
|
177
|
+
const write = _atomicWriteConfig(client.cfgPath, cfg)
|
|
178
|
+
return {
|
|
179
|
+
ok: write.ok,
|
|
180
|
+
stage: write.ok ? 'config_write' : 'config_write_failed',
|
|
181
|
+
error: write.error,
|
|
182
|
+
hadParseError: false,
|
|
183
|
+
existingMcpCount: existingServers.length,
|
|
184
|
+
preservedServers: otherServers,
|
|
185
|
+
}
|
|
111
186
|
}
|
|
112
187
|
|
|
113
188
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
@@ -185,6 +260,11 @@ async function runSetup(opts = {}) {
|
|
|
185
260
|
|
|
186
261
|
// Detectar clientes
|
|
187
262
|
const detected = CLIENTS.filter(c => c.detect())
|
|
263
|
+
const notDetected = CLIENTS.filter(c => !c.detect())
|
|
264
|
+
_trackSetupStep('detect', detected.length > 0 ? 'ok' : 'no_clients', '', {
|
|
265
|
+
clientsFound: detected.map(c => c.id),
|
|
266
|
+
clientsNotFound: notDetected.map(c => c.id),
|
|
267
|
+
})
|
|
188
268
|
|
|
189
269
|
if (detected.length === 0) {
|
|
190
270
|
const entry = stableArgs !== undefined
|
|
@@ -222,14 +302,25 @@ async function runSetup(opts = {}) {
|
|
|
222
302
|
const failed = []
|
|
223
303
|
|
|
224
304
|
for (const client of detected) {
|
|
225
|
-
|
|
226
|
-
|
|
305
|
+
const result = injectMcpConfig(client, stableCommand, stableArgs)
|
|
306
|
+
if (result.ok) {
|
|
307
|
+
_trackSetupStep('config_write', 'ok', client.id, {
|
|
308
|
+
existingMcpCount: result.existingMcpCount,
|
|
309
|
+
preservedServers: result.preservedServers,
|
|
310
|
+
})
|
|
227
311
|
_trackConfigWritten(client.id || client.name, client.name)
|
|
228
312
|
configured.push(client.name)
|
|
229
313
|
console.log(`✓ ${client.name} configured`)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
314
|
+
} else {
|
|
315
|
+
const status = result.hadParseError ? 'parse_error' : 'write_error'
|
|
316
|
+
_trackSetupStep('config_write', status, client.id, { error: result.error })
|
|
317
|
+
failed.push({ name: client.name, reason: status, error: result.error })
|
|
318
|
+
if (result.hadParseError) {
|
|
319
|
+
console.error(`✗ ${client.name}: existing config has invalid JSON — backed up to ${client.cfgPath}.lmcp-backup`)
|
|
320
|
+
console.error(` Edit or remove the backup to fix: ${result.error}`)
|
|
321
|
+
} else {
|
|
322
|
+
console.error(`✗ ${client.name}: ${result.error}`)
|
|
323
|
+
}
|
|
233
324
|
}
|
|
234
325
|
}
|
|
235
326
|
|
|
@@ -240,10 +331,11 @@ async function runSetup(opts = {}) {
|
|
|
240
331
|
let email = process.env.LMCP_EMAIL || ''
|
|
241
332
|
if (email) {
|
|
242
333
|
try {
|
|
243
|
-
const
|
|
334
|
+
const r = _safeReadConfig(cfgFile)
|
|
335
|
+
const cfg = r.data || {}
|
|
244
336
|
if (!cfg.license_email) {
|
|
245
337
|
cfg.license_email = email
|
|
246
|
-
|
|
338
|
+
_atomicWriteConfig(cfgFile, cfg)
|
|
247
339
|
console.log(`✓ Email saved: ${email}`)
|
|
248
340
|
}
|
|
249
341
|
} catch { /* config not created yet — will be created on first run */ }
|
|
@@ -293,8 +385,9 @@ async function runSetup(opts = {}) {
|
|
|
293
385
|
email = ans
|
|
294
386
|
emailPromptResult = 'submitted'
|
|
295
387
|
try {
|
|
296
|
-
const
|
|
297
|
-
|
|
388
|
+
const r = _safeReadConfig(cfgFile)
|
|
389
|
+
const cfg = r.data || {}
|
|
390
|
+
if (!cfg.license_email) { cfg.license_email = email; _atomicWriteConfig(cfgFile, cfg) }
|
|
298
391
|
} catch {}
|
|
299
392
|
console.log(' ✓ Email saved')
|
|
300
393
|
} else {
|
|
@@ -308,6 +401,13 @@ async function runSetup(opts = {}) {
|
|
|
308
401
|
|
|
309
402
|
// Health check — verify binary works before showing success
|
|
310
403
|
const healthOk = _runHealthCheck()
|
|
404
|
+
_trackSetupStep('binary_health', healthOk ? 'ok' : 'failed', '', {})
|
|
405
|
+
|
|
406
|
+
// setup_complete summary
|
|
407
|
+
_trackSetupStep('setup_complete', configured.length > 0 ? 'ok' : 'failed', '', {
|
|
408
|
+
clientsFound: configured,
|
|
409
|
+
clientsNotFound: failed.map(f => f.name || f),
|
|
410
|
+
})
|
|
311
411
|
|
|
312
412
|
const hasCursor = configured.includes('Cursor')
|
|
313
413
|
|
|
@@ -340,17 +440,19 @@ async function runSetup(opts = {}) {
|
|
|
340
440
|
} else {
|
|
341
441
|
const primaryClient = configured[0]
|
|
342
442
|
console.log('┌─────────────────────────────────────────────────────┐')
|
|
343
|
-
console.log('│ NEXT STEP —
|
|
443
|
+
console.log('│ NEXT STEP — restart to activate: │')
|
|
344
444
|
console.log('│ │')
|
|
345
|
-
console.log(`│ Quit
|
|
346
|
-
console.log(
|
|
445
|
+
console.log(`│ 1. Quit ${primaryClient} completely (Cmd+Q)${''.padEnd(Math.max(0, 30 - primaryClient.length))}│`)
|
|
446
|
+
console.log(`│ 2. Reopen ${primaryClient.padEnd(41)}│`)
|
|
347
447
|
console.log('│ │')
|
|
348
448
|
console.log('│ Then try: "Summarize my unread emails" │')
|
|
349
449
|
console.log('└─────────────────────────────────────────────────────┘\n')
|
|
450
|
+
_trackRestartPrompted(primaryClient)
|
|
350
451
|
}
|
|
351
452
|
}
|
|
352
453
|
if (failed.length > 0) {
|
|
353
|
-
|
|
454
|
+
const failNames = failed.map(f => (typeof f === 'string' ? f : f.name)).join(', ')
|
|
455
|
+
console.log(`⚠ Could not configure: ${failNames} — check permissions and try again.\n`)
|
|
354
456
|
}
|
|
355
457
|
console.log(' Setup guide & troubleshooting: https://local-mcp.com/setup\n')
|
|
356
458
|
|
|
@@ -540,6 +642,46 @@ function _migrateCurlLaunchAgent() {
|
|
|
540
642
|
} catch { /* non-fatal — don't block install on unexpected plist content */ }
|
|
541
643
|
}
|
|
542
644
|
|
|
645
|
+
// ── Setup funnel telemetry ────────────────────────────────────────────────────
|
|
646
|
+
// Fire-and-forget. Sends one event per step to /setup-step so we can see exactly
|
|
647
|
+
// where each machine's install funnel breaks without waiting for a heartbeat.
|
|
648
|
+
|
|
649
|
+
function _trackSetupStep(step, status, client, detail) {
|
|
650
|
+
try {
|
|
651
|
+
const https = require('https')
|
|
652
|
+
const payload = {
|
|
653
|
+
machine_id: _getMachineId(),
|
|
654
|
+
install_id: process.env.INSTALL_ID || '',
|
|
655
|
+
method: process.env.LMCP_METHOD || 'npm',
|
|
656
|
+
step,
|
|
657
|
+
status,
|
|
658
|
+
client: client || '',
|
|
659
|
+
error: (detail && detail.error) ? String(detail.error).slice(0, 300) : '',
|
|
660
|
+
}
|
|
661
|
+
if (detail) {
|
|
662
|
+
if (detail.existingMcpCount >= 0) payload.existing_mcp_count = detail.existingMcpCount
|
|
663
|
+
if (detail.preservedServers && detail.preservedServers.length)
|
|
664
|
+
payload.preserved_servers = detail.preservedServers.join(',')
|
|
665
|
+
if (detail.clientsFound) payload.clients_found = detail.clientsFound.join(',')
|
|
666
|
+
if (detail.clientsNotFound) payload.clients_not_found = detail.clientsNotFound.join(',')
|
|
667
|
+
if (detail.toolCount >= 0) payload.tool_count = detail.toolCount
|
|
668
|
+
if (detail.binaryVersion) payload.binary_version = detail.binaryVersion
|
|
669
|
+
}
|
|
670
|
+
const data = JSON.stringify(payload)
|
|
671
|
+
const req = https.request({
|
|
672
|
+
hostname: BACKEND_HOST,
|
|
673
|
+
path: '/setup-step',
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
676
|
+
timeout: 5000,
|
|
677
|
+
})
|
|
678
|
+
req.on('response', (res) => res.resume())
|
|
679
|
+
req.on('error', () => {})
|
|
680
|
+
req.write(data)
|
|
681
|
+
req.end()
|
|
682
|
+
} catch { /* non-fatal */ }
|
|
683
|
+
}
|
|
684
|
+
|
|
543
685
|
function _trackConfigWritten(clientId, clientName) {
|
|
544
686
|
try {
|
|
545
687
|
const https = require('https')
|
|
@@ -594,6 +736,31 @@ function _trackEmailPrompt(result, email, errorMsg) {
|
|
|
594
736
|
} catch { /* non-fatal */ }
|
|
595
737
|
}
|
|
596
738
|
|
|
739
|
+
function _trackRestartPrompted(clientName) {
|
|
740
|
+
try {
|
|
741
|
+
const https = require('https')
|
|
742
|
+
const machineId = _getMachineId()
|
|
743
|
+
const payload = {
|
|
744
|
+
stage: 'restart_prompted',
|
|
745
|
+
client_name: clientName,
|
|
746
|
+
machine_id: machineId,
|
|
747
|
+
install_id: process.env.INSTALL_ID || '',
|
|
748
|
+
}
|
|
749
|
+
const data = JSON.stringify(payload)
|
|
750
|
+
const req = https.request({
|
|
751
|
+
hostname: BACKEND_HOST,
|
|
752
|
+
path: '/install-event',
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
|
|
755
|
+
timeout: 5000,
|
|
756
|
+
})
|
|
757
|
+
req.on('response', (res) => res.resume())
|
|
758
|
+
req.on('error', () => {})
|
|
759
|
+
req.write(data)
|
|
760
|
+
req.end()
|
|
761
|
+
} catch { /* non-fatal */ }
|
|
762
|
+
}
|
|
763
|
+
|
|
597
764
|
function _pingInstall(clients, method, email = '', binaryVersion = '') {
|
|
598
765
|
try {
|
|
599
766
|
const https = require('https')
|