local-mcp 3.0.113 → 3.0.115
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/download.js +47 -1
- package/index.js +44 -38
- package/package.json +1 -1
- package/setup.js +3 -16
package/download.js
CHANGED
|
@@ -439,4 +439,50 @@ async function ensureHelper() {
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Downloads local-mcp-jxa-runner on first install only.
|
|
444
|
+
* CRITICAL: never replaces an existing jxa-runner binary — the runner owns
|
|
445
|
+
* Automation (JXA) TCC grants for Mail, Messages, Safari, etc. via its cdhash.
|
|
446
|
+
* Replacing the binary silently resets those grants and users would have to
|
|
447
|
+
* re-authorize in System Settings → Privacy & Security → Automation.
|
|
448
|
+
* @returns {Promise<string|null>} Path to the binary, or null if it fails
|
|
449
|
+
*/
|
|
450
|
+
async function ensureJXARunner() {
|
|
451
|
+
const binPath = path.join(CACHE_DIR, 'local-mcp-jxa-runner')
|
|
452
|
+
|
|
453
|
+
// Already installed — never replace. Stable cdhash = stable TCC grants.
|
|
454
|
+
if (fs.existsSync(binPath)) return binPath
|
|
455
|
+
|
|
456
|
+
const info = await getLatestBinary()
|
|
457
|
+
const version = info.version
|
|
458
|
+
const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
|
|
459
|
+
const url = `https://download.local-mcp.com/local-mcp-jxa-runner-${version}-${arch}.tar.gz`
|
|
460
|
+
process.stderr.write(`\nDownloading local-mcp-jxa-runner v${version}...\n`)
|
|
461
|
+
|
|
462
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
463
|
+
const tarPath = path.join(CACHE_DIR, `local-mcp-jxa-runner-${version}.tar.gz`)
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
await downloadFile(url, tarPath)
|
|
467
|
+
const stat = fs.statSync(tarPath)
|
|
468
|
+
if (stat.size < 1000) { fs.unlinkSync(tarPath); throw new Error(`Incomplete download (${stat.size} bytes)`) }
|
|
469
|
+
|
|
470
|
+
execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
|
|
471
|
+
fs.unlinkSync(tarPath)
|
|
472
|
+
|
|
473
|
+
if (!fs.existsSync(binPath)) throw new Error('local-mcp-jxa-runner not found after extraction')
|
|
474
|
+
fs.chmodSync(binPath, 0o755)
|
|
475
|
+
// Do NOT codesign: the pre-built binary's cdhash must remain exactly as
|
|
476
|
+
// distributed so the Automation TCC entry stays valid permanently.
|
|
477
|
+
|
|
478
|
+
fs.writeFileSync(path.join(CACHE_DIR, '.jxa-runner-version'), version, 'utf8')
|
|
479
|
+
process.stderr.write(` local-mcp-jxa-runner installed at ${binPath}\n`)
|
|
480
|
+
return binPath
|
|
481
|
+
} catch (err) {
|
|
482
|
+
try { if (fs.existsSync(tarPath)) fs.unlinkSync(tarPath) } catch {}
|
|
483
|
+
process.stderr.write(` (local-mcp-jxa-runner not available: ${err.message})\n`)
|
|
484
|
+
return null
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, ensureHelper, ensureJXARunner, CACHE_DIR, TRAY_DIR }
|
package/index.js
CHANGED
|
@@ -17,39 +17,6 @@ const os = require('os')
|
|
|
17
17
|
const fs = require('fs')
|
|
18
18
|
const https = require('https')
|
|
19
19
|
|
|
20
|
-
// Track first run / version change so analytics catch users who launch
|
|
21
|
-
// via `npx local-mcp` without going through `setup` (manual MCP config).
|
|
22
|
-
function _pingFirstRun(versionChanged) {
|
|
23
|
-
try {
|
|
24
|
-
const pkg = require('./package.json')
|
|
25
|
-
const machineId = (() => {
|
|
26
|
-
try {
|
|
27
|
-
const out = execSync("system_profiler SPHardwareDataType 2>/dev/null | awk '/Hardware UUID/ { print $3 }'", { encoding: 'utf8' }).trim()
|
|
28
|
-
return out || ''
|
|
29
|
-
} catch { return '' }
|
|
30
|
-
})()
|
|
31
|
-
const data = JSON.stringify({
|
|
32
|
-
version: pkg.version,
|
|
33
|
-
os_version: os.release(),
|
|
34
|
-
arch: process.arch,
|
|
35
|
-
node_version: process.version,
|
|
36
|
-
method: 'npx-default',
|
|
37
|
-
machine_id: machineId,
|
|
38
|
-
source: process.env.LMCP_SOURCE || '',
|
|
39
|
-
ref: process.env.LMCP_REF || '',
|
|
40
|
-
})
|
|
41
|
-
const req = https.request({
|
|
42
|
-
hostname: 'office-mcp-production.up.railway.app',
|
|
43
|
-
path: '/install/npm',
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
|
|
46
|
-
timeout: 5000,
|
|
47
|
-
})
|
|
48
|
-
req.on('error', () => {})
|
|
49
|
-
req.write(data)
|
|
50
|
-
req.end()
|
|
51
|
-
} catch { /* fire and forget */ }
|
|
52
|
-
}
|
|
53
20
|
|
|
54
21
|
// Solo macOS
|
|
55
22
|
if (process.platform !== 'darwin') {
|
|
@@ -136,21 +103,59 @@ async function main() {
|
|
|
136
103
|
: ''
|
|
137
104
|
if (cachedVersion && semverGte(cachedVersion, pkg.version)) {
|
|
138
105
|
// Cached binary is same or newer — use fast path, no download needed
|
|
139
|
-
const { ensureTray, ensureTeamsProxy, ensureHelper } = require('./download')
|
|
106
|
+
const { ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
|
|
140
107
|
ensureTray().catch(() => {})
|
|
141
108
|
ensureTeamsProxy().catch(() => {})
|
|
142
109
|
ensureHelper().catch(() => {})
|
|
110
|
+
ensureJXARunner().catch(() => {})
|
|
143
111
|
const child = spawn(stableBin, [], { stdio: 'inherit', env: process.env })
|
|
144
112
|
child.on('error', () => process.exit(1))
|
|
145
113
|
child.on('exit', (code) => process.exit(code ?? 0))
|
|
114
|
+
|
|
115
|
+
// Background update check — fires 10s after startup, at most once per 24h.
|
|
116
|
+
// Covers machines where the tray is dead and Sparkle can't update the binary.
|
|
117
|
+
const lastCheckFile = path.join(CACHE_DIR, '.last-update-check')
|
|
118
|
+
function _needsUpdateCheck() {
|
|
119
|
+
try { return Date.now() - parseInt(fs.readFileSync(lastCheckFile, 'utf8').trim(), 10) > 86400000 } catch { return true }
|
|
120
|
+
}
|
|
121
|
+
if (_needsUpdateCheck()) {
|
|
122
|
+
try { fs.writeFileSync(lastCheckFile, String(Date.now())) } catch {}
|
|
123
|
+
const _cachedVersion = cachedVersion // capture for closure
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
const req = https.get('https://office-mcp-production.up.railway.app/runtime/latest', { timeout: 8000 }, (res) => {
|
|
126
|
+
let buf = ''
|
|
127
|
+
res.on('data', c => buf += c)
|
|
128
|
+
res.on('end', () => {
|
|
129
|
+
try {
|
|
130
|
+
const { version: latestVersion } = JSON.parse(buf)
|
|
131
|
+
if (latestVersion && !semverGte(_cachedVersion, latestVersion)) {
|
|
132
|
+
// New version available — download silently for next restart
|
|
133
|
+
const { ensureRuntime } = require('./download')
|
|
134
|
+
ensureRuntime().then(({ binPath, version: downloadedVersion }) => {
|
|
135
|
+
try {
|
|
136
|
+
const tmpPath = stableBin + '.tmp'
|
|
137
|
+
fs.copyFileSync(binPath, tmpPath)
|
|
138
|
+
fs.chmodSync(tmpPath, 0o755)
|
|
139
|
+
try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {}
|
|
140
|
+
fs.renameSync(tmpPath, stableBin)
|
|
141
|
+
fs.writeFileSync(versionFile, downloadedVersion || latestVersion)
|
|
142
|
+
} catch {}
|
|
143
|
+
}).catch(() => {})
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
req.on('error', () => {})
|
|
149
|
+
req.on('timeout', () => { try { req.destroy() } catch {} })
|
|
150
|
+
}, 10000)
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
return
|
|
147
154
|
}
|
|
148
155
|
// Cached binary is older than our npm package — download/update
|
|
149
156
|
process.stderr.write(`Updating LMCP server (${cachedVersion || '?'} → ${pkg.version})...\n`)
|
|
150
|
-
_pingFirstRun(true)
|
|
151
157
|
} else {
|
|
152
158
|
// No cached binary version → first run via default mode
|
|
153
|
-
_pingFirstRun(false)
|
|
154
159
|
}
|
|
155
160
|
if (stat.isSymbolicLink()) fs.unlinkSync(stableBin)
|
|
156
161
|
} catch {
|
|
@@ -158,12 +163,13 @@ async function main() {
|
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
// Slow path: download/repair binary
|
|
161
|
-
const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper } = require('./download')
|
|
166
|
+
const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
|
|
162
167
|
|
|
163
|
-
// Update tray + proxies in background. Helper
|
|
168
|
+
// Update tray + proxies in background. Helper + jxa-runner downloaded only on first install.
|
|
164
169
|
ensureTray().catch(() => {})
|
|
165
170
|
ensureTeamsProxy().catch(() => {})
|
|
166
171
|
ensureHelper().catch(() => {})
|
|
172
|
+
ensureJXARunner().catch(() => {})
|
|
167
173
|
|
|
168
174
|
let runtime
|
|
169
175
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.115",
|
|
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
|
@@ -327,7 +327,9 @@ async function runSetup(opts = {}) {
|
|
|
327
327
|
} catch { /* non-fatal */ }
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
// LMCP_METHOD lets install.sh pass 'curl' so we can distinguish it from
|
|
331
|
+
// a direct 'npx local-mcp setup' invocation in the analytics.
|
|
332
|
+
_pingInstall(configured, process.env.LMCP_METHOD || opts.method || 'npx-setup', email)
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
async function _installTeamsProxy() {
|
|
@@ -576,21 +578,6 @@ function _autoLaunchClient(configured) {
|
|
|
576
578
|
return
|
|
577
579
|
}
|
|
578
580
|
|
|
579
|
-
// Log auto-launch event (fire-and-forget)
|
|
580
|
-
try {
|
|
581
|
-
const https = require('https')
|
|
582
|
-
const data = JSON.stringify({ event: 'install_auto_launch', client: clientName })
|
|
583
|
-
const req = https.request({
|
|
584
|
-
hostname: BACKEND_HOST,
|
|
585
|
-
path: '/install-event',
|
|
586
|
-
method: 'POST',
|
|
587
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
|
|
588
|
-
timeout: 5000,
|
|
589
|
-
})
|
|
590
|
-
req.on('error', () => {})
|
|
591
|
-
req.write(data)
|
|
592
|
-
req.end()
|
|
593
|
-
} catch {}
|
|
594
581
|
}
|
|
595
582
|
|
|
596
583
|
module.exports = { runSetup, injectMcpConfig, CLIENTS }
|