local-mcp 1.44.0

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 ADDED
@@ -0,0 +1,155 @@
1
+ 'use strict'
2
+ /**
3
+ * download.js — descarga y cachea el runtime Python de Local MCP desde R2.
4
+ * Cache: ~/.local/share/local-mcp/runtime/{version}/
5
+ */
6
+
7
+ const https = require('https')
8
+ const http = require('http')
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+ const os = require('os')
12
+ const { execFileSync } = require('child_process')
13
+
14
+ const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
15
+ const CACHE_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'runtime')
16
+
17
+ function getArch() {
18
+ const arch = process.arch
19
+ if (arch === 'arm64') return 'darwin-arm64'
20
+ if (arch === 'x64') return 'darwin-x64'
21
+ throw new Error(`Arquitectura no soportada: ${arch}. Local MCP requiere macOS arm64 o x64.`)
22
+ }
23
+
24
+ /**
25
+ * Obtiene la versión más reciente del runtime desde el backend.
26
+ * @returns {Promise<{version: string, url: string, checksum: string}>}
27
+ */
28
+ async function getLatestRuntime() {
29
+ return new Promise((resolve, reject) => {
30
+ const req = https.get(`${BACKEND_URL}/runtime/latest`, { timeout: 10000 }, (res) => {
31
+ let data = ''
32
+ res.on('data', chunk => data += chunk)
33
+ res.on('end', () => {
34
+ try {
35
+ resolve(JSON.parse(data))
36
+ } catch (e) {
37
+ reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
38
+ }
39
+ })
40
+ })
41
+ req.on('error', reject)
42
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout al consultar versión del runtime')) })
43
+ })
44
+ }
45
+
46
+ /**
47
+ * Descarga un archivo con seguimiento de progreso.
48
+ */
49
+ async function downloadFile(url, destPath) {
50
+ return new Promise((resolve, reject) => {
51
+ const file = fs.createWriteStream(destPath)
52
+ const proto = url.startsWith('https') ? https : http
53
+
54
+ const request = (reqUrl) => {
55
+ proto.get(reqUrl, { timeout: 120000 }, (res) => {
56
+ if (res.statusCode === 301 || res.statusCode === 302) {
57
+ file.close()
58
+ return request(res.headers.location)
59
+ }
60
+ if (res.statusCode !== 200) {
61
+ file.close()
62
+ fs.unlinkSync(destPath)
63
+ return reject(new Error(`HTTP ${res.statusCode} descargando runtime`))
64
+ }
65
+
66
+ const total = parseInt(res.headers['content-length'] || '0', 10)
67
+ let downloaded = 0
68
+ let lastPct = -1
69
+
70
+ res.on('data', chunk => {
71
+ downloaded += chunk.length
72
+ if (total > 0) {
73
+ const pct = Math.floor(downloaded / total * 100)
74
+ if (pct !== lastPct && pct % 10 === 0) {
75
+ process.stderr.write(`\r Descargando runtime... ${pct}%`)
76
+ lastPct = pct
77
+ }
78
+ }
79
+ })
80
+
81
+ res.pipe(file)
82
+ file.on('finish', () => { file.close(); process.stderr.write('\n'); resolve() })
83
+ file.on('error', (err) => { fs.unlinkSync(destPath); reject(err) })
84
+ }).on('error', reject)
85
+ }
86
+
87
+ request(url)
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Asegura que el runtime Python esté descargado y listo.
93
+ * @returns {Promise<{pythonBin: string, serverPath: string, runtimeDir: string}>}
94
+ */
95
+ async function ensureRuntime() {
96
+ const arch = getArch()
97
+ const info = await getLatestRuntime()
98
+ const version = info.version
99
+ const url = info.url || `https://download.local-mcp.com/local-mcp-runtime-${version}-${arch}.tar.gz`
100
+
101
+ const versionDir = path.join(CACHE_DIR, version)
102
+ const pythonBin = path.join(versionDir, 'bin', 'python3')
103
+ const serverPath = path.join(versionDir, 'server.py')
104
+
105
+ // Ya está en cache
106
+ if (fs.existsSync(pythonBin) && fs.existsSync(serverPath)) {
107
+ return { pythonBin, serverPath, runtimeDir: versionDir }
108
+ }
109
+
110
+ process.stderr.write(`\nLocal MCP runtime v${version} no encontrado en cache.\n`)
111
+ process.stderr.write(`Descargando desde ${url}\n`)
112
+
113
+ fs.mkdirSync(CACHE_DIR, { recursive: true })
114
+ const tarPath = path.join(CACHE_DIR, `runtime-${version}.tar.gz`)
115
+
116
+ await downloadFile(url, tarPath)
117
+
118
+ process.stderr.write(` Extrayendo...\n`)
119
+ fs.mkdirSync(versionDir, { recursive: true })
120
+ execFileSync('tar', ['-xzf', tarPath, '-C', versionDir], { stdio: 'pipe' })
121
+ fs.unlinkSync(tarPath)
122
+
123
+ if (!fs.existsSync(pythonBin)) {
124
+ throw new Error(`Runtime extraído pero no se encontró ${pythonBin}`)
125
+ }
126
+
127
+ // Re-firmar con ad-hoc — macOS invalida signatures al copiar fuera del .app
128
+ process.stderr.write(` Firmando binarios...\n`)
129
+ try {
130
+ const glob = require('child_process').execSync
131
+ // Firmar dylibs y el binario Python individualmente
132
+ const targets = [
133
+ path.join(versionDir, 'bin', 'python3'),
134
+ path.join(versionDir, 'Frameworks'),
135
+ ]
136
+ for (const target of targets) {
137
+ if (fs.existsSync(target)) {
138
+ execFileSync('find', [target, '-name', '*.dylib', '-exec',
139
+ 'codesign', '--force', '--sign', '-', '{}', ';'], { stdio: 'pipe' })
140
+ if (fs.existsSync(path.join(versionDir, 'bin', 'python3'))) {
141
+ execFileSync('codesign', ['--force', '--sign', '-',
142
+ path.join(versionDir, 'bin', 'python3')], { stdio: 'pipe' })
143
+ }
144
+ break
145
+ }
146
+ }
147
+ } catch (e) {
148
+ process.stderr.write(` Aviso: codesign falló — continuando...\n`)
149
+ }
150
+
151
+ process.stderr.write(` Runtime listo en ${versionDir}\n`)
152
+ return { pythonBin, serverPath, runtimeDir: versionDir }
153
+ }
154
+
155
+ module.exports = { ensureRuntime, getLatestRuntime, CACHE_DIR }
package/index.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ /**
4
+ * index.js — entry point del paquete npm local-mcp.
5
+ *
6
+ * Modos:
7
+ * npx local-mcp → lanza MCP server en stdio (para Claude Desktop, Cursor, etc.)
8
+ * npx local-mcp setup → wizard de configuración de clientes
9
+ * npx local-mcp update → fuerza re-descarga del runtime
10
+ * npx local-mcp uninstall → elimina cache del runtime
11
+ * npx local-mcp status → muestra estado del servidor local
12
+ */
13
+
14
+ const { spawnSync } = require('child_process')
15
+ const path = require('path')
16
+ const os = require('os')
17
+
18
+ // Solo macOS
19
+ if (process.platform !== 'darwin') {
20
+ console.error('Local MCP solo está disponible para macOS.')
21
+ process.exit(1)
22
+ }
23
+
24
+ const cmd = process.argv[2]
25
+
26
+ async function main() {
27
+ if (cmd === 'setup') {
28
+ const { runSetup } = require('./setup')
29
+ await runSetup()
30
+ return
31
+ }
32
+
33
+ if (cmd === 'update') {
34
+ const { CACHE_DIR } = require('./download')
35
+ const fs = require('fs')
36
+ if (fs.existsSync(CACHE_DIR)) {
37
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true })
38
+ console.log('Cache del runtime eliminado.')
39
+ }
40
+ // Re-descargar en el próximo arranque
41
+ console.log('Ejecutá "npx local-mcp" nuevamente para descargar la última versión.')
42
+ return
43
+ }
44
+
45
+ if (cmd === 'uninstall') {
46
+ const { CACHE_DIR } = require('./download')
47
+ const fs = require('fs')
48
+ if (fs.existsSync(CACHE_DIR)) {
49
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true })
50
+ console.log(`Runtime eliminado de ${CACHE_DIR}`)
51
+ } else {
52
+ console.log('No hay runtime en cache.')
53
+ }
54
+ return
55
+ }
56
+
57
+ if (cmd === 'status') {
58
+ const http = require('http')
59
+ http.get('http://localhost:8765/status', (res) => {
60
+ let data = ''
61
+ res.on('data', c => data += c)
62
+ res.on('end', () => {
63
+ try {
64
+ const s = JSON.parse(data)
65
+ console.log(`Local MCP v${s.version} — ${s.server}`)
66
+ console.log(`Licencia: ${s.license?.status} (${s.license?.days_left}d)`)
67
+ } catch { console.log(data) }
68
+ })
69
+ }).on('error', () => console.log('Servidor no disponible en localhost:8765'))
70
+ return
71
+ }
72
+
73
+ // Modo default: stdio MCP server
74
+ const { ensureRuntime } = require('./download')
75
+
76
+ let runtime
77
+ try {
78
+ runtime = await ensureRuntime()
79
+ } catch (err) {
80
+ process.stderr.write(`\nError al preparar el runtime de Local MCP:\n${err.message}\n`)
81
+ process.stderr.write(`Intentá reinstalar: npx local-mcp update\n\n`)
82
+ process.exit(1)
83
+ }
84
+
85
+ const { pythonBin, serverPath, runtimeDir } = runtime
86
+
87
+ // Frameworks bundleados (libssl, libcrypto, etc.)
88
+ const frameworksPath = path.join(runtimeDir, 'Frameworks')
89
+ const env = { ...process.env }
90
+ if (require('fs').existsSync(frameworksPath)) {
91
+ env.DYLD_FRAMEWORK_PATH = frameworksPath + (env.DYLD_FRAMEWORK_PATH ? ':' + env.DYLD_FRAMEWORK_PATH : '')
92
+ env.DYLD_LIBRARY_PATH = frameworksPath + (env.DYLD_LIBRARY_PATH ? ':' + env.DYLD_LIBRARY_PATH : '')
93
+ }
94
+
95
+ const extraArgs = process.argv.slice(cmd ? 2 : 3)
96
+ const result = spawnSync(pythonBin, [serverPath, 'stdio', ...extraArgs], {
97
+ stdio: 'inherit',
98
+ env,
99
+ })
100
+
101
+ if (result.error) {
102
+ process.stderr.write(`Error ejecutando Local MCP: ${result.error.message}\n`)
103
+ process.exit(1)
104
+ }
105
+
106
+ process.exit(result.status ?? 0)
107
+ }
108
+
109
+ main().catch(err => {
110
+ process.stderr.write(`Error fatal: ${err.message}\n`)
111
+ process.exit(1)
112
+ })
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "local-mcp",
3
+ "version": "1.44.0",
4
+ "description": "Connect Claude, Cursor, Windsurf and other AI agents to Mail, Calendar, Contacts, Teams and OneDrive on Mac — no cloud, no tokens",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "local-mcp": "index.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node postinstall.js"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "download.js",
15
+ "setup.js",
16
+ "postinstall.js",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "os": [
23
+ "darwin"
24
+ ],
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "claude",
29
+ "cursor",
30
+ "windsurf",
31
+ "ai",
32
+ "macos",
33
+ "mail",
34
+ "calendar",
35
+ "contacts",
36
+ "teams",
37
+ "onedrive",
38
+ "local"
39
+ ],
40
+ "author": "Local MCP <hello@local-mcp.com>",
41
+ "license": "SEE LICENSE IN LICENSE",
42
+ "homepage": "https://local-mcp.com",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/lanchuske/local-mcp.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/lanchuske/local-mcp/issues"
49
+ }
50
+ }
package/postinstall.js ADDED
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+ /**
3
+ * postinstall.js — se ejecuta automáticamente después de `npm install -g local-mcp`.
4
+ * No corre cuando se usa npx (npm_config_global no está seteado).
5
+ */
6
+
7
+ // Skip si: npx (no global), CI, o el usuario lo desactivó
8
+ if (!process.env.npm_config_global || process.env.CI || process.env.SKIP_SETUP) {
9
+ process.exit(0)
10
+ }
11
+
12
+ // Solo macOS
13
+ if (process.platform !== 'darwin') {
14
+ process.exit(0)
15
+ }
16
+
17
+ const { runSetup } = require('./setup')
18
+
19
+ runSetup({ all: true }).catch(err => {
20
+ // No fallar la instalación si el setup falla
21
+ process.stderr.write(`\nAviso: setup automático falló: ${err.message}\n`)
22
+ process.stderr.write('Ejecutá "local-mcp setup" manualmente para configurar tus clientes.\n\n')
23
+ process.exit(0)
24
+ })
package/setup.js ADDED
@@ -0,0 +1,172 @@
1
+ 'use strict'
2
+ /**
3
+ * setup.js — wizard de configuración de clientes MCP.
4
+ * Detecta clientes instalados y escribe la config npx automáticamente.
5
+ *
6
+ * Uso:
7
+ * local-mcp setup → configura todos los clientes detectados
8
+ * local-mcp setup --all → configura todos sin preguntar
9
+ */
10
+
11
+ const fs = require('fs')
12
+ const path = require('path')
13
+ const os = require('os')
14
+ const { execSync, execFileSync } = require('child_process')
15
+
16
+ const HOME = os.homedir()
17
+ const NPX_COMMAND = 'npx'
18
+ const NPX_ARGS = ['-y', 'local-mcp@latest']
19
+
20
+ // ── Rutas de config de cada cliente ──────────────────────────────────────────
21
+
22
+ const CLIENTS = [
23
+ {
24
+ id: 'claude-desktop',
25
+ name: 'Claude Desktop',
26
+ cfgPath: path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
27
+ detect: () => fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Claude')),
28
+ },
29
+ {
30
+ id: 'cursor',
31
+ name: 'Cursor',
32
+ cfgPath: path.join(HOME, '.cursor', 'mcp.json'),
33
+ detect: () => fs.existsSync(path.join(HOME, '.cursor')) || _appExists('Cursor'),
34
+ },
35
+ {
36
+ id: 'windsurf',
37
+ name: 'Windsurf',
38
+ cfgPath: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
39
+ detect: () => fs.existsSync(path.join(HOME, '.codeium', 'windsurf')) || _appExists('Windsurf'),
40
+ },
41
+ {
42
+ id: 'vscode-cline',
43
+ name: 'VS Code (Cline)',
44
+ cfgPath: path.join(HOME, '.vscode', 'mcp.json'),
45
+ detect: () => _cmdExists('code') || fs.existsSync(path.join(HOME, '.vscode')),
46
+ },
47
+ {
48
+ id: 'zed',
49
+ name: 'Zed',
50
+ cfgPath: path.join(HOME, '.config', 'zed', 'settings.json'),
51
+ detect: () => fs.existsSync(path.join(HOME, '.config', 'zed')) || _appExists('Zed'),
52
+ zed: true, // Zed usa formato diferente dentro de settings.json
53
+ },
54
+ ]
55
+
56
+ // ── Helpers ───────────────────────────────────────────────────────────────────
57
+
58
+ function _appExists(name) {
59
+ return fs.existsSync(`/Applications/${name}.app`) ||
60
+ fs.existsSync(path.join(HOME, `Applications/${name}.app`))
61
+ }
62
+
63
+ function _cmdExists(cmd) {
64
+ try { execSync(`which ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
65
+ }
66
+
67
+ function _readJson(filePath) {
68
+ try {
69
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
70
+ } catch {
71
+ return {}
72
+ }
73
+ }
74
+
75
+ function _writeJson(filePath, data) {
76
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
77
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8')
78
+ }
79
+
80
+ // ── Inyectar config MCP en un cliente estándar ────────────────────────────────
81
+
82
+ function injectMcpConfig(client) {
83
+ const cfg = _readJson(client.cfgPath)
84
+
85
+ if (client.zed) {
86
+ // Zed usa { "context_servers": { "local-mcp": { "command": {...} } } }
87
+ cfg.context_servers = cfg.context_servers || {}
88
+ cfg.context_servers['local-mcp'] = {
89
+ command: { path: NPX_COMMAND, args: NPX_ARGS },
90
+ }
91
+ } else {
92
+ // Formato estándar MCP: { "mcpServers": { "local-mcp": { "command": "npx", "args": [...] } } }
93
+ cfg.mcpServers = cfg.mcpServers || {}
94
+ // Limpiar entradas legacy
95
+ delete cfg.mcpServers['office-mcp']
96
+ cfg.mcpServers['local-mcp'] = {
97
+ command: NPX_COMMAND,
98
+ args: NPX_ARGS,
99
+ }
100
+ }
101
+
102
+ _writeJson(client.cfgPath, cfg)
103
+ return true
104
+ }
105
+
106
+ // ── Main ──────────────────────────────────────────────────────────────────────
107
+
108
+ async function runSetup(opts = {}) {
109
+ const forceAll = opts.all || process.argv.includes('--all')
110
+
111
+ console.log('\n╔══════════════════════════════════════╗')
112
+ console.log('║ Local MCP — Setup Wizard ║')
113
+ console.log('╚══════════════════════════════════════╝\n')
114
+
115
+ // Detectar clientes
116
+ const detected = CLIENTS.filter(c => c.detect())
117
+
118
+ if (detected.length === 0) {
119
+ console.log('No se detectaron clientes MCP instalados.')
120
+ console.log('Instalá Claude Desktop, Cursor, Windsurf o VS Code y ejecutá este comando nuevamente.\n')
121
+ console.log('Config manual (copiá en el archivo de config de tu cliente):')
122
+ console.log(JSON.stringify({ mcpServers: { 'local-mcp': { command: NPX_COMMAND, args: NPX_ARGS } } }, null, 2))
123
+ return
124
+ }
125
+
126
+ console.log(`Clientes detectados: ${detected.map(c => c.name).join(', ')}\n`)
127
+
128
+ const configured = []
129
+ const failed = []
130
+
131
+ for (const client of detected) {
132
+ try {
133
+ injectMcpConfig(client)
134
+ configured.push(client.name)
135
+ console.log(`✓ ${client.name} configurado`)
136
+ } catch (err) {
137
+ failed.push(client.name)
138
+ console.error(`✗ ${client.name}: ${err.message}`)
139
+ }
140
+ }
141
+
142
+ // Escribir email al config si viene por env
143
+ const email = process.env.LMCP_EMAIL || ''
144
+ if (email) {
145
+ const cfgDir = path.join(HOME, 'Library', 'Application Support', 'Local MCP')
146
+ const cfgFile = path.join(cfgDir, 'config.json')
147
+ try {
148
+ const cfg = _readJson(cfgFile)
149
+ if (!cfg.license_email) {
150
+ cfg.license_email = email
151
+ _writeJson(cfgFile, cfg)
152
+ console.log(`✓ Email guardado en config: ${email}`)
153
+ }
154
+ } catch { /* config no existe aún, se creará al arrancar */ }
155
+ }
156
+
157
+ console.log('\n──────────────────────────────────────')
158
+ if (configured.length > 0) {
159
+ console.log(`✅ Configurado: ${configured.join(', ')}`)
160
+ console.log(' Reiniciá tu cliente AI para cargar Local MCP.\n')
161
+ }
162
+ if (failed.length > 0) {
163
+ console.log(`⚠ Falló: ${failed.join(', ')} — revisá los permisos e intentá de nuevo.\n`)
164
+ }
165
+
166
+ console.log('Config MCP aplicada:')
167
+ console.log(JSON.stringify({ mcpServers: { 'local-mcp': { command: NPX_COMMAND, args: NPX_ARGS } } }, null, 2))
168
+ console.log('\nPara instalar la app completa con tray (opcional):')
169
+ console.log(' curl -fsSL https://local-mcp.com/install | bash\n')
170
+ }
171
+
172
+ module.exports = { runSetup, injectMcpConfig, CLIENTS }