local-mcp 1.44.4 → 1.44.6
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 +44 -60
- package/index.js +4 -26
- package/package.json +1 -1
package/download.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
/**
|
|
3
|
-
* download.js — descarga y cachea el
|
|
4
|
-
*
|
|
3
|
+
* download.js — descarga y cachea el binario standalone de Local MCP desde R2.
|
|
4
|
+
* El binario fue compilado con PyInstaller — no requiere Python instalado.
|
|
5
|
+
* Cache: ~/.local/share/local-mcp/bin/{version}/
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const https = require('https')
|
|
@@ -12,7 +13,7 @@ const os = require('os')
|
|
|
12
13
|
const { execFileSync } = require('child_process')
|
|
13
14
|
|
|
14
15
|
const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
|
|
15
|
-
const CACHE_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', '
|
|
16
|
+
const CACHE_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin')
|
|
16
17
|
|
|
17
18
|
function getArch() {
|
|
18
19
|
const arch = process.arch
|
|
@@ -22,29 +23,29 @@ function getArch() {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Obtiene la versión más reciente del
|
|
26
|
-
* @returns {Promise<{version: string, url: string, checksum: string}>}
|
|
26
|
+
* Obtiene la versión más reciente del binario desde el backend.
|
|
27
27
|
*/
|
|
28
|
-
async function
|
|
28
|
+
async function getLatestBinary() {
|
|
29
29
|
return new Promise((resolve, reject) => {
|
|
30
30
|
const req = https.get(`${BACKEND_URL}/runtime/latest`, { timeout: 10000 }, (res) => {
|
|
31
31
|
let data = ''
|
|
32
32
|
res.on('data', chunk => data += chunk)
|
|
33
33
|
res.on('end', () => {
|
|
34
34
|
try {
|
|
35
|
-
|
|
35
|
+
const info = JSON.parse(data)
|
|
36
|
+
resolve(info)
|
|
36
37
|
} catch (e) {
|
|
37
38
|
reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
|
|
38
39
|
}
|
|
39
40
|
})
|
|
40
41
|
})
|
|
41
42
|
req.on('error', reject)
|
|
42
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout al consultar versión
|
|
43
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout al consultar versión')) })
|
|
43
44
|
})
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
* Descarga un archivo con
|
|
48
|
+
* Descarga un archivo con barra de progreso.
|
|
48
49
|
*/
|
|
49
50
|
async function downloadFile(url, destPath) {
|
|
50
51
|
return new Promise((resolve, reject) => {
|
|
@@ -52,7 +53,7 @@ async function downloadFile(url, destPath) {
|
|
|
52
53
|
const proto = url.startsWith('https') ? https : http
|
|
53
54
|
|
|
54
55
|
const request = (reqUrl) => {
|
|
55
|
-
proto.get(reqUrl, { timeout:
|
|
56
|
+
proto.get(reqUrl, { timeout: 300000 }, (res) => {
|
|
56
57
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
57
58
|
file.close()
|
|
58
59
|
return request(res.headers.location)
|
|
@@ -60,7 +61,7 @@ async function downloadFile(url, destPath) {
|
|
|
60
61
|
if (res.statusCode !== 200) {
|
|
61
62
|
file.close()
|
|
62
63
|
fs.unlinkSync(destPath)
|
|
63
|
-
return reject(new Error(`HTTP ${res.statusCode} descargando
|
|
64
|
+
return reject(new Error(`HTTP ${res.statusCode} descargando binario`))
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const total = parseInt(res.headers['content-length'] || '0', 10)
|
|
@@ -72,7 +73,7 @@ async function downloadFile(url, destPath) {
|
|
|
72
73
|
if (total > 0) {
|
|
73
74
|
const pct = Math.floor(downloaded / total * 100)
|
|
74
75
|
if (pct !== lastPct && pct % 10 === 0) {
|
|
75
|
-
process.stderr.write(`\r Descargando
|
|
76
|
+
process.stderr.write(`\r Descargando... ${pct}%`)
|
|
76
77
|
lastPct = pct
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -89,73 +90,56 @@ async function downloadFile(url, destPath) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
|
-
* Asegura que el
|
|
93
|
-
* @returns {Promise<{
|
|
93
|
+
* Asegura que el binario esté descargado y listo.
|
|
94
|
+
* @returns {Promise<{binPath: string}>}
|
|
94
95
|
*/
|
|
95
|
-
async function
|
|
96
|
-
const arch
|
|
97
|
-
const info
|
|
96
|
+
async function ensureBinary() {
|
|
97
|
+
const arch = getArch()
|
|
98
|
+
const info = await getLatestBinary()
|
|
98
99
|
const version = info.version
|
|
99
|
-
|
|
100
|
+
// URL del binario PyInstaller (nueva nomenclatura)
|
|
101
|
+
const url = `https://download.local-mcp.com/local-mcp-server-${version}-${arch}.tar.gz`
|
|
100
102
|
|
|
101
103
|
const versionDir = path.join(CACHE_DIR, version)
|
|
102
|
-
const
|
|
103
|
-
const serverPath = path.join(versionDir, 'server.py')
|
|
104
|
+
const binPath = path.join(versionDir, 'local-mcp-server', 'local-mcp-server')
|
|
104
105
|
|
|
105
|
-
// Ya está en cache
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return { pythonBin, serverPath, runtimeDir: versionDir }
|
|
109
|
-
}
|
|
110
|
-
// Cache incompleto (falta zip u otros archivos) → limpiar y re-descargar
|
|
111
|
-
if (fs.existsSync(versionDir)) {
|
|
112
|
-
process.stderr.write(` Cache incompleto — re-descargando...\n`)
|
|
113
|
-
fs.rmSync(versionDir, { recursive: true, force: true })
|
|
106
|
+
// Ya está en cache
|
|
107
|
+
if (fs.existsSync(binPath)) {
|
|
108
|
+
return { binPath, versionDir }
|
|
114
109
|
}
|
|
115
110
|
|
|
116
|
-
process.stderr.write(`\nLocal MCP
|
|
111
|
+
process.stderr.write(`\nLocal MCP v${version} no encontrado en cache.\n`)
|
|
117
112
|
process.stderr.write(`Descargando desde ${url}\n`)
|
|
118
113
|
|
|
119
|
-
fs.mkdirSync(
|
|
120
|
-
const tarPath = path.join(
|
|
114
|
+
fs.mkdirSync(versionDir, { recursive: true })
|
|
115
|
+
const tarPath = path.join(versionDir, `server-${version}.tar.gz`)
|
|
121
116
|
|
|
122
117
|
await downloadFile(url, tarPath)
|
|
123
118
|
|
|
124
119
|
process.stderr.write(` Extrayendo...\n`)
|
|
125
|
-
fs.mkdirSync(versionDir, { recursive: true })
|
|
126
120
|
execFileSync('tar', ['-xzf', tarPath, '-C', versionDir], { stdio: 'pipe' })
|
|
127
121
|
fs.unlinkSync(tarPath)
|
|
128
122
|
|
|
129
|
-
if (!fs.existsSync(
|
|
130
|
-
throw new Error(`
|
|
123
|
+
if (!fs.existsSync(binPath)) {
|
|
124
|
+
throw new Error(`Binario no encontrado después de extraer: ${binPath}`)
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
//
|
|
134
|
-
|
|
127
|
+
// Asegurar que sea ejecutable
|
|
128
|
+
fs.chmodSync(binPath, 0o755)
|
|
129
|
+
|
|
130
|
+
// Re-firmar ad-hoc (macOS puede invalidar al copiar/extraer)
|
|
135
131
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const targets = [
|
|
139
|
-
path.join(versionDir, 'bin', 'python3'),
|
|
140
|
-
path.join(versionDir, 'Frameworks'),
|
|
141
|
-
]
|
|
142
|
-
for (const target of targets) {
|
|
143
|
-
if (fs.existsSync(target)) {
|
|
144
|
-
execFileSync('find', [target, '-name', '*.dylib', '-exec',
|
|
145
|
-
'codesign', '--force', '--sign', '-', '{}', ';'], { stdio: 'pipe' })
|
|
146
|
-
if (fs.existsSync(path.join(versionDir, 'bin', 'python3'))) {
|
|
147
|
-
execFileSync('codesign', ['--force', '--sign', '-',
|
|
148
|
-
path.join(versionDir, 'bin', 'python3')], { stdio: 'pipe' })
|
|
149
|
-
}
|
|
150
|
-
break
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
} catch (e) {
|
|
154
|
-
process.stderr.write(` Aviso: codesign falló — continuando...\n`)
|
|
155
|
-
}
|
|
132
|
+
execFileSync('codesign', ['--force', '--sign', '-', binPath], { stdio: 'pipe' })
|
|
133
|
+
} catch { /* no crítico */ }
|
|
156
134
|
|
|
157
|
-
process.stderr.write(`
|
|
158
|
-
return {
|
|
135
|
+
process.stderr.write(` Listo en ${versionDir}\n`)
|
|
136
|
+
return { binPath, versionDir }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Mantener compatibilidad con código que usa ensureRuntime
|
|
140
|
+
async function ensureRuntime() {
|
|
141
|
+
const { binPath, versionDir } = await ensureBinary()
|
|
142
|
+
return { pythonBin: binPath, serverPath: binPath, runtimeDir: versionDir, binPath }
|
|
159
143
|
}
|
|
160
144
|
|
|
161
|
-
module.exports = {
|
|
145
|
+
module.exports = { ensureBinary, ensureRuntime, CACHE_DIR }
|
package/index.js
CHANGED
|
@@ -82,34 +82,12 @@ async function main() {
|
|
|
82
82
|
process.exit(1)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Variables de entorno para el servidor Python
|
|
88
|
-
const frameworksPath = path.join(runtimeDir, 'Frameworks')
|
|
89
|
-
const libPath = path.join(runtimeDir, 'lib', 'python3.13')
|
|
90
|
-
const env = { ...process.env }
|
|
91
|
-
|
|
92
|
-
// Frameworks bundleados (libssl, libcrypto, liblzma, etc.)
|
|
93
|
-
if (require('fs').existsSync(frameworksPath)) {
|
|
94
|
-
env.DYLD_FRAMEWORK_PATH = frameworksPath + (env.DYLD_FRAMEWORK_PATH ? ':' + env.DYLD_FRAMEWORK_PATH : '')
|
|
95
|
-
env.DYLD_LIBRARY_PATH = frameworksPath + (env.DYLD_LIBRARY_PATH ? ':' + env.DYLD_LIBRARY_PATH : '')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// PYTHONPATH: módulos del runtime (sin zip — magic number varía entre builds)
|
|
99
|
-
// PYTHONNOUSERSITE: evita mezclar con site-packages del sistema
|
|
100
|
-
env.PYTHONNOUSERSITE = '1'
|
|
101
|
-
const sitePkgs = path.join(libPath, 'site-packages')
|
|
102
|
-
const libDynload = path.join(libPath, 'lib-dynload')
|
|
103
|
-
const pyPaths = [libPath, sitePkgs, libDynload]
|
|
104
|
-
.filter(p => require('fs').existsSync(p))
|
|
105
|
-
if (pyPaths.length > 0) {
|
|
106
|
-
env.PYTHONPATH = pyPaths.join(':') + (env.PYTHONPATH ? ':' + env.PYTHONPATH : '')
|
|
107
|
-
}
|
|
108
|
-
|
|
85
|
+
// Binario PyInstaller standalone — sin Python, sin dependencias externas
|
|
86
|
+
const { binPath } = runtime
|
|
109
87
|
const extraArgs = process.argv.slice(cmd ? 2 : 3)
|
|
110
|
-
const result = spawnSync(
|
|
88
|
+
const result = spawnSync(binPath, ['stdio', ...extraArgs], {
|
|
111
89
|
stdio: 'inherit',
|
|
112
|
-
env,
|
|
90
|
+
env: process.env,
|
|
113
91
|
})
|
|
114
92
|
|
|
115
93
|
if (result.error) {
|
package/package.json
CHANGED