local-mcp 1.44.5 → 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 -59
  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,72 +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 que los archivos esenciales existan
106
- if (fs.existsSync(pythonBin) && fs.existsSync(serverPath)) {
107
- return { pythonBin, serverPath, runtimeDir: versionDir }
108
- }
109
- // Cache incompleto → limpiar y re-descargar
110
- if (fs.existsSync(versionDir)) {
111
- process.stderr.write(` Cache incompleto — re-descargando...\n`)
112
- fs.rmSync(versionDir, { recursive: true, force: true })
106
+ // Ya está en cache
107
+ if (fs.existsSync(binPath)) {
108
+ return { binPath, versionDir }
113
109
  }
114
110
 
115
- 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`)
116
112
  process.stderr.write(`Descargando desde ${url}\n`)
117
113
 
118
- fs.mkdirSync(CACHE_DIR, { recursive: true })
119
- 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`)
120
116
 
121
117
  await downloadFile(url, tarPath)
122
118
 
123
119
  process.stderr.write(` Extrayendo...\n`)
124
- fs.mkdirSync(versionDir, { recursive: true })
125
120
  execFileSync('tar', ['-xzf', tarPath, '-C', versionDir], { stdio: 'pipe' })
126
121
  fs.unlinkSync(tarPath)
127
122
 
128
- if (!fs.existsSync(pythonBin)) {
129
- 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}`)
130
125
  }
131
126
 
132
- // Re-firmar con ad-hoc — macOS invalida signatures al copiar fuera del .app
133
- 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)
134
131
  try {
135
- const glob = require('child_process').execSync
136
- // Firmar dylibs y el binario Python individualmente
137
- const targets = [
138
- path.join(versionDir, 'bin', 'python3'),
139
- path.join(versionDir, 'Frameworks'),
140
- ]
141
- for (const target of targets) {
142
- if (fs.existsSync(target)) {
143
- execFileSync('find', [target, '-name', '*.dylib', '-exec',
144
- 'codesign', '--force', '--sign', '-', '{}', ';'], { stdio: 'pipe' })
145
- if (fs.existsSync(path.join(versionDir, 'bin', 'python3'))) {
146
- execFileSync('codesign', ['--force', '--sign', '-',
147
- path.join(versionDir, 'bin', 'python3')], { stdio: 'pipe' })
148
- }
149
- break
150
- }
151
- }
152
- } catch (e) {
153
- process.stderr.write(` Aviso: codesign falló — continuando...\n`)
154
- }
132
+ execFileSync('codesign', ['--force', '--sign', '-', binPath], { stdio: 'pipe' })
133
+ } catch { /* no crítico */ }
155
134
 
156
- process.stderr.write(` Runtime listo en ${versionDir}\n`)
157
- 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 }
158
143
  }
159
144
 
160
- 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.5",
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": {