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.
Files changed (3) hide show
  1. package/download.js +44 -60
  2. package/index.js +4 -26
  3. 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 runtime Python de Local MCP desde R2.
4
- * Cache: ~/.local/share/local-mcp/runtime/{version}/
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', 'runtime')
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 runtime desde el backend.
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 getLatestRuntime() {
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
- resolve(JSON.parse(data))
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 del runtime')) })
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 seguimiento de progreso.
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: 120000 }, (res) => {
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 runtime`))
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 runtime... ${pct}%`)
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 runtime Python esté descargado y listo.
93
- * @returns {Promise<{pythonBin: string, serverPath: string, runtimeDir: string}>}
93
+ * Asegura que el binario esté descargado y listo.
94
+ * @returns {Promise<{binPath: string}>}
94
95
  */
95
- async function ensureRuntime() {
96
- const arch = getArch()
97
- const info = await getLatestRuntime()
96
+ async function ensureBinary() {
97
+ const arch = getArch()
98
+ const info = await getLatestBinary()
98
99
  const version = info.version
99
- const url = info.url || `https://download.local-mcp.com/local-mcp-runtime-${version}-${arch}.tar.gz`
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 pythonBin = path.join(versionDir, 'bin', 'python3')
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 — verificar integridad básica (python313.zip requerido)
106
- const zipPath = path.join(versionDir, 'lib', 'python313.zip')
107
- if (fs.existsSync(pythonBin) && fs.existsSync(serverPath) && fs.existsSync(zipPath)) {
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 runtime v${version} no encontrado en cache.\n`)
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(CACHE_DIR, { recursive: true })
120
- const tarPath = path.join(CACHE_DIR, `runtime-${version}.tar.gz`)
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(pythonBin)) {
130
- throw new Error(`Runtime extraído pero no se encontró ${pythonBin}`)
123
+ if (!fs.existsSync(binPath)) {
124
+ throw new Error(`Binario no encontrado después de extraer: ${binPath}`)
131
125
  }
132
126
 
133
- // Re-firmar con ad-hoc — macOS invalida signatures al copiar fuera del .app
134
- process.stderr.write(` Firmando binarios...\n`)
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
- const glob = require('child_process').execSync
137
- // Firmar dylibs y el binario Python individualmente
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(` Runtime listo en ${versionDir}\n`)
158
- return { pythonBin, serverPath, runtimeDir: versionDir }
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 = { ensureRuntime, getLatestRuntime, CACHE_DIR }
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
- const { pythonBin, serverPath, runtimeDir } = runtime
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(pythonBin, [serverPath, 'stdio', ...extraArgs], {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "1.44.4",
3
+ "version": "1.44.6",
4
4
  "description": "Connect Claude, Cursor, Windsurf and other AI agents to Mail, Calendar, Contacts, Teams and OneDrive on Mac — no cloud, no tokens",
5
5
  "main": "index.js",
6
6
  "bin": {