local-mcp 1.2.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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # local-mcp
2
+
3
+ > MCP server for macOS — connect Claude Desktop, Cursor, Windsurf, VS Code and Zed to Mail, Calendar, Contacts, Teams, OneDrive, OmniFocus, Reminders, Notes, Messages, Stocks and more.
4
+
5
+ [![npm](https://img.shields.io/npm/v/local-mcp)](https://www.npmjs.com/package/local-mcp)
6
+ [![platform](https://img.shields.io/badge/platform-macOS-blue)](https://local-mcp.com?utm_source=npm)
7
+
8
+ **All data stays on your Mac.** No cloud. No tokens. No API keys.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npx -y local-mcp@latest setup
14
+ ```
15
+
16
+ Auto-detects and configures Claude Desktop, Cursor, Windsurf, VS Code, and Zed. Restart your AI client once.
17
+
18
+ ## Or add directly to your MCP config
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "local-mcp": {
24
+ "command": "npx",
25
+ "args": ["-y", "local-mcp@latest"]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## What it does
32
+
33
+ Local MCP runs a native MCP server that bridges your Mac apps to any AI client:
34
+
35
+ | Service | Capabilities |
36
+ |---------|-------------|
37
+ | **Mail** | Read, search, send, reply — Gmail, Outlook, iCloud, Exchange |
38
+ | **Calendar** | List, create, delete events across all accounts |
39
+ | **Contacts** | Search and list from Contacts.app |
40
+ | **Teams** | Read chats, channels, messages (no tokens needed) |
41
+ | **OneDrive** | List, read, write, search, move, delete files |
42
+ | **Documents** | Word, Excel, PowerPoint, PDF |
43
+ | **Outlook** | Read, search, send emails, list/create calendar events (direct Outlook.app access) |
44
+ | **Reminders & To Do** | List, create, complete — includes Microsoft To Do via sync |
45
+ | **OmniFocus** | List tasks, projects, tags — create and complete tasks |
46
+ | **Notes** | List, read, search, create notes |
47
+ | **Messages** | List chats, read and search iMessages |
48
+ | **Stocks** | Real-time quotes, historical charts, symbol search |
49
+ | **Finder** | Search and list files |
50
+ | **Safari** | List bookmarks |
51
+
52
+ ## 82 MCP Tools
53
+
54
+ Email: `list_accounts` `list_emails` `read_email` `send_email` `reply_email` `search_emails` `move_email` `save_attachment` `create_email_folder`
55
+
56
+ Calendar: `list_calendar_names` `list_calendar_events` `create_calendar_event` `delete_calendar_event`
57
+
58
+ Contacts: `list_contacts`
59
+
60
+ Teams: `teams_list_teams` `teams_list_channels` `teams_list_chats` `teams_read_channel_messages` `teams_read_chat_messages`
61
+
62
+ OneDrive: `onedrive_root` `onedrive_list_files` `onedrive_read_file` `onedrive_write_file` `onedrive_move_file` `onedrive_delete_file` `onedrive_search_files`
63
+
64
+ Documents: `word_create` `word_read` `word_append` `excel_create` `excel_read` `excel_write_cell` `ppt_create` `ppt_read` `pdf_read`
65
+
66
+ Outlook: `outlook_list_emails` `outlook_read_email` `outlook_search_emails` `outlook_send_email` `outlook_list_events` `outlook_create_event`
67
+
68
+ Reminders & To Do: `todo_list_lists` `todo_list_tasks` `todo_create_task` `todo_complete_task`
69
+
70
+ OmniFocus: `list_omnifocus_tasks` `list_omnifocus_projects` `list_omnifocus_tags` `search_omnifocus_tasks` `create_omnifocus_task` `complete_omnifocus_task`
71
+
72
+ Notes: `list_notes` `read_note` `search_notes` `create_note`
73
+
74
+ Messages: `list_message_chats` `read_messages` `search_messages`
75
+
76
+ Stocks: `stocks_get_quote` `stocks_get_chart` `stocks_search_symbol`
77
+
78
+ Finder: `finder_search` `finder_list`
79
+
80
+ Safari: `list_safari_bookmarks`
81
+
82
+ Utilities: `diagnose` `version_info` `report_bug` `request_feature` `send_logs` `lmcp_help` `list_my_feedback` `close_feedback`
83
+
84
+ ## Safety
85
+
86
+ - Email send/reply always shows a preview before sending — you confirm first
87
+ - Calendar events require explicit calendar name — never creates in the wrong one
88
+ - All destructive operations require confirmation
89
+
90
+ ## Requirements
91
+
92
+ - macOS 12 Monterey or later (Apple Silicon and Intel)
93
+ - Node.js 18+
94
+ - One of: Claude Desktop, Cursor, Windsurf, VS Code, Zed
95
+
96
+ ## Config file locations
97
+
98
+ | Client | Path |
99
+ |--------|------|
100
+ | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` |
101
+ | Cursor | `~/.cursor/mcp.json` |
102
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
103
+ | VS Code | `~/.vscode/mcp.json` |
104
+ | Zed | `~/.config/zed/settings.json` |
105
+
106
+ ## Commands
107
+
108
+ ```bash
109
+ npx -y local-mcp@latest setup # configure all detected AI clients
110
+ npx -y local-mcp@latest update # force re-download of the runtime
111
+ npx -y local-mcp@latest status # show server status
112
+ ```
113
+
114
+ ## Full app (optional)
115
+
116
+ For the menu bar status icon, auto-start on login, and Cloud Relay:
117
+
118
+ ```bash
119
+ curl -fsSL https://local-mcp.com/install | bash
120
+ ```
121
+
122
+ ## Links
123
+
124
+ - [Website](https://local-mcp.com?utm_source=npm)
125
+ - [Pricing](https://local-mcp.com/#pricing?utm_source=npm)
126
+ - Support: support@local-mcp.com
package/download.js ADDED
@@ -0,0 +1,314 @@
1
+ 'use strict'
2
+ /**
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}/
6
+ */
7
+
8
+ const https = require('https')
9
+ const http = require('http')
10
+ const fs = require('fs')
11
+ const path = require('path')
12
+ const os = require('os')
13
+ const { execFileSync } = require('child_process')
14
+
15
+ const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
16
+ const CACHE_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin')
17
+ const TRAY_DIR = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'tray') // metadata dir
18
+ const TRAY_APP = '/Applications/LocalMCPTray.app'
19
+
20
+ function _getMachineId() {
21
+ const { execSync } = require('child_process')
22
+ const crypto = require('crypto')
23
+ try {
24
+ const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
25
+ const id = r.toString().trim()
26
+ if (id) return id
27
+ } catch {}
28
+ try {
29
+ const ioreg = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { stdio: 'pipe' }).toString()
30
+ const match = ioreg.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)
31
+ const hwUuid = match ? match[1] : os.hostname()
32
+ return crypto.createHash('sha256').update(hwUuid).digest('hex').slice(0, 24)
33
+ } catch {}
34
+ return ''
35
+ }
36
+
37
+ function getArch() {
38
+ const arch = process.arch
39
+ if (arch === 'arm64') return 'darwin-arm64'
40
+ if (arch === 'x64') return 'darwin-x64'
41
+ throw new Error(`Arquitectura no soportada: ${arch}. Local MCP requiere macOS arm64 o x64.`)
42
+ }
43
+
44
+ /**
45
+ * Obtiene la versión más reciente del binario desde el backend.
46
+ * Si esta máquina está en beta_machines, el backend retorna la versión beta.
47
+ */
48
+ async function getLatestBinary() {
49
+ const machineId = _getMachineId()
50
+ const qs = machineId ? `?machine_id=${encodeURIComponent(machineId)}` : ''
51
+ return new Promise((resolve, reject) => {
52
+ const req = https.get(`${BACKEND_URL}/runtime/latest${qs}`, { timeout: 10000 }, (res) => {
53
+ let data = ''
54
+ res.on('data', chunk => data += chunk)
55
+ res.on('end', () => {
56
+ try {
57
+ const info = JSON.parse(data)
58
+ resolve(info)
59
+ } catch (e) {
60
+ reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
61
+ }
62
+ })
63
+ })
64
+ req.on('error', reject)
65
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout al consultar versión')) })
66
+ })
67
+ }
68
+
69
+ /**
70
+ * Descarga un archivo con barra de progreso.
71
+ */
72
+ async function downloadFile(url, destPath) {
73
+ return new Promise((resolve, reject) => {
74
+ const file = fs.createWriteStream(destPath)
75
+ const proto = url.startsWith('https') ? https : http
76
+
77
+ const request = (reqUrl) => {
78
+ proto.get(reqUrl, { timeout: 300000 }, (res) => {
79
+ if (res.statusCode === 301 || res.statusCode === 302) {
80
+ file.close()
81
+ return request(res.headers.location)
82
+ }
83
+ if (res.statusCode !== 200) {
84
+ file.close()
85
+ fs.unlinkSync(destPath)
86
+ return reject(new Error(`HTTP ${res.statusCode} descargando binario`))
87
+ }
88
+
89
+ const total = parseInt(res.headers['content-length'] || '0', 10)
90
+ let downloaded = 0
91
+ let lastPct = -1
92
+
93
+ res.on('data', chunk => {
94
+ downloaded += chunk.length
95
+ if (total > 0) {
96
+ const pct = Math.floor(downloaded / total * 100)
97
+ if (pct !== lastPct && pct % 10 === 0) {
98
+ process.stderr.write(`\r Descargando... ${pct}%`)
99
+ lastPct = pct
100
+ }
101
+ }
102
+ })
103
+
104
+ res.pipe(file)
105
+ file.on('finish', () => { file.close(); process.stderr.write('\n'); resolve() })
106
+ file.on('error', (err) => { fs.unlinkSync(destPath); reject(err) })
107
+ }).on('error', reject)
108
+ }
109
+
110
+ request(url)
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Asegura que el binario esté descargado y listo.
116
+ * @returns {Promise<{binPath: string}>}
117
+ */
118
+ async function ensureBinary() {
119
+ const arch = getArch()
120
+ const info = await getLatestBinary()
121
+ const version = info.version
122
+ // URL del binario PyInstaller (nueva nomenclatura)
123
+ const url = `https://download.local-mcp.com/local-mcp-server-${version}-${arch}.tar.gz`
124
+
125
+ const versionDir = path.join(CACHE_DIR, version)
126
+ const binPath = path.join(versionDir, 'local-mcp-server', 'local-mcp-server')
127
+
128
+ // Ya está en cache
129
+ if (fs.existsSync(binPath)) {
130
+ return { binPath, versionDir }
131
+ }
132
+
133
+ process.stderr.write(`\nLocal MCP v${version} no encontrado en cache.\n`)
134
+ process.stderr.write(`Descargando desde ${url}\n`)
135
+
136
+ fs.mkdirSync(versionDir, { recursive: true })
137
+ const tarPath = path.join(versionDir, `server-${version}.tar.gz`)
138
+
139
+ await downloadFile(url, tarPath)
140
+
141
+ process.stderr.write(` Extrayendo...\n`)
142
+ execFileSync('tar', ['-xzf', tarPath, '-C', versionDir], { stdio: 'pipe' })
143
+ fs.unlinkSync(tarPath)
144
+
145
+ if (!fs.existsSync(binPath)) {
146
+ throw new Error(`Binario no encontrado después de extraer: ${binPath}`)
147
+ }
148
+
149
+ // Asegurar que sea ejecutable
150
+ fs.chmodSync(binPath, 0o755)
151
+
152
+ // Re-firmar ad-hoc con bundle identifier estable.
153
+ // --identifier da al binario una identidad TCC persistente aunque cambie la ruta de versión.
154
+ // Sin esto, macOS atribuye los permisos TCC al proceso padre (node) → diálogo cada reinicio.
155
+ try {
156
+ execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', binPath], { stdio: 'pipe' })
157
+ } catch { /* no crítico */ }
158
+
159
+ process.stderr.write(` Listo en ${versionDir}\n`)
160
+ return { binPath, versionDir }
161
+ }
162
+
163
+ // Mantener compatibilidad con código que usa ensureRuntime
164
+ async function ensureRuntime() {
165
+ const { binPath, versionDir } = await ensureBinary()
166
+ return { pythonBin: binPath, serverPath: binPath, runtimeDir: versionDir, binPath }
167
+ }
168
+
169
+ /**
170
+ * Descarga el co-proceso teams-proxy si no está instalado o es una versión diferente.
171
+ * El binary se instala en ~/.local/share/local-mcp/bin/teams-proxy
172
+ * @returns {Promise<string|null>} Ruta al binary, o null si falla
173
+ */
174
+ async function ensureTeamsProxy() {
175
+ const info = await getLatestBinary()
176
+ const version = info.version
177
+ const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
178
+ const binPath = path.join(CACHE_DIR, 'teams-proxy')
179
+ const verFile = path.join(CACHE_DIR, '.teams-proxy-version')
180
+
181
+ // Ya instalado y actualizado
182
+ if (fs.existsSync(binPath) && fs.existsSync(verFile)) {
183
+ if (fs.readFileSync(verFile, 'utf8').trim() === version) return binPath
184
+ }
185
+
186
+ const url = `https://download.local-mcp.com/teams-proxy-${version}-${arch}.tar.gz`
187
+ process.stderr.write(`\nDescargando teams-proxy v${version}...\n`)
188
+
189
+ fs.mkdirSync(CACHE_DIR, { recursive: true })
190
+ const tarPath = path.join(CACHE_DIR, `teams-proxy-${version}.tar.gz`)
191
+
192
+ try {
193
+ await downloadFile(url, tarPath)
194
+ const stat = fs.statSync(tarPath)
195
+ if (stat.size < 1000) { fs.unlinkSync(tarPath); throw new Error(`Descarga incompleta (${stat.size} bytes)`) }
196
+
197
+ execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
198
+ fs.unlinkSync(tarPath)
199
+
200
+ if (!fs.existsSync(binPath)) throw new Error('teams-proxy no encontrado tras extraer')
201
+ fs.chmodSync(binPath, 0o755)
202
+ try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.teams-proxy', binPath], { stdio: 'pipe' }) } catch { /* no crítico */ }
203
+
204
+ fs.writeFileSync(verFile, version, 'utf8')
205
+ process.stderr.write(` teams-proxy instalado en ${binPath}\n`)
206
+ return binPath
207
+ } catch (err) {
208
+ try { if (fs.existsSync(tarPath)) fs.unlinkSync(tarPath) } catch {}
209
+ process.stderr.write(` (teams-proxy no disponible: ${err.message})\n`)
210
+ return null
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Descarga e instala el tray SwiftUI (menu bar app) — universal binary (arm64 + Intel).
216
+ * @returns {Promise<string|null>} Ruta al .app instalado, o null si falla
217
+ */
218
+ async function ensureTray() {
219
+
220
+ const info = await getLatestBinary()
221
+ const version = info.version
222
+ const trayApp = TRAY_APP
223
+ const verFile = path.join(TRAY_DIR, '.version')
224
+
225
+ // Migrar instalación vieja de ~/.local/share si existe
226
+ const oldTrayApp = path.join(TRAY_DIR, 'LocalMCPTray.app')
227
+ if (fs.existsSync(oldTrayApp)) {
228
+ try { execFileSync('rm', ['-rf', oldTrayApp], { stdio: 'pipe' }) } catch { /* ignorar */ }
229
+ }
230
+
231
+ // Ya instalado y actualizado
232
+ if (fs.existsSync(trayApp) && fs.existsSync(verFile)) {
233
+ if (fs.readFileSync(verFile, 'utf8').trim() === version) return trayApp
234
+ }
235
+
236
+ const url = `https://download.local-mcp.com/local-mcp-tray-${version}-darwin-universal.tar.gz`
237
+ process.stderr.write(`\nDescargando tray v${version}...\n`)
238
+
239
+ fs.mkdirSync(TRAY_DIR, { recursive: true })
240
+ const tarPath = path.join(TRAY_DIR, `tray-${version}.tar.gz`)
241
+
242
+ // Descargar ANTES de borrar el viejo — si la descarga falla, el tray actual sigue intacto
243
+ await downloadFile(url, tarPath)
244
+
245
+ // Verificar que el archivo descargado no está vacío
246
+ const tarStat = fs.statSync(tarPath)
247
+ if (tarStat.size < 10000) {
248
+ fs.unlinkSync(tarPath)
249
+ throw new Error(`Descarga del tray incompleta (${tarStat.size} bytes)`)
250
+ }
251
+
252
+ // Recién ahora borrar el viejo e instalar el nuevo
253
+ if (fs.existsSync(trayApp)) {
254
+ try { execFileSync('rm', ['-rf', trayApp], { stdio: 'pipe' }) } catch { /* ignorar */ }
255
+ }
256
+
257
+ process.stderr.write(` Instalando tray en /Applications...\n`)
258
+ execFileSync('tar', ['-xzf', tarPath, '-C', '/Applications'], { stdio: 'pipe' })
259
+ fs.unlinkSync(tarPath)
260
+
261
+ if (!fs.existsSync(trayApp)) {
262
+ throw new Error(`Tray no encontrado tras extraer: ${trayApp}`)
263
+ }
264
+
265
+ // Quitar quarantine + firmar ad-hoc
266
+ try { execFileSync('xattr', ['-rd', 'com.apple.quarantine', trayApp], { stdio: 'pipe' }) } catch { /* no crítico */ }
267
+ const trayBin = path.join(trayApp, 'Contents', 'MacOS', 'LocalMCPTray')
268
+ try { execFileSync('codesign', ['--force', '--sign', '-', trayBin], { stdio: 'pipe' }) } catch { /* no crítico */ }
269
+
270
+ fs.writeFileSync(verFile, version, 'utf8')
271
+
272
+ // LaunchAgent — auto-start en cada reboot de sesión
273
+ _writeTrayLaunchAgent(trayApp)
274
+
275
+ process.stderr.write(` Tray instalado en ${trayApp}\n`)
276
+ return trayApp
277
+ }
278
+
279
+ function _writeTrayLaunchAgent(trayApp) {
280
+ const trayBin = path.join(trayApp, 'Contents', 'MacOS', 'LocalMCPTray')
281
+ const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents')
282
+ const plistPath = path.join(plistDir, 'com.local-mcp.tray.plist')
283
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
284
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
285
+ <plist version="1.0">
286
+ <dict>
287
+ <key>Label</key>
288
+ <string>com.local-mcp.tray</string>
289
+ <key>ProgramArguments</key>
290
+ <array>
291
+ <string>${trayBin}</string>
292
+ </array>
293
+ <key>RunAtLoad</key>
294
+ <true/>
295
+ <key>KeepAlive</key>
296
+ <false/>
297
+ <key>ThrottleInterval</key>
298
+ <integer>10</integer>
299
+ </dict>
300
+ </plist>
301
+ `
302
+ try {
303
+ fs.mkdirSync(plistDir, { recursive: true })
304
+ fs.writeFileSync(plistPath, plist, 'utf8')
305
+ // Matar proceso viejo (si estaba corriendo por cualquier medio)
306
+ try { execFileSync('pkill', ['-x', 'LocalMCPTray'], { stdio: 'pipe' }) } catch { /* no estaba corriendo */ }
307
+ // Recargar LaunchAgent
308
+ try { execFileSync('launchctl', ['unload', plistPath], { stdio: 'pipe' }) } catch {}
309
+ execFileSync('launchctl', ['load', plistPath], { stdio: 'pipe' })
310
+ // launchctl load con RunAtLoad=true arranca el proceso — no necesitamos open()
311
+ } catch { /* no crítico */ }
312
+ }
313
+
314
+ module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, CACHE_DIR, TRAY_DIR }
package/index.js ADDED
@@ -0,0 +1,139 @@
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 { spawn } = require('child_process')
15
+ const path = require('path')
16
+ const os = require('os')
17
+ const fs = require('fs')
18
+
19
+ // Solo macOS
20
+ if (process.platform !== 'darwin') {
21
+ console.error('Local MCP solo está disponible para macOS.')
22
+ process.exit(1)
23
+ }
24
+
25
+ const cmd = process.argv[2]
26
+
27
+ async function main() {
28
+ if (cmd === 'setup') {
29
+ const { runSetup } = require('./setup')
30
+ await runSetup()
31
+ return
32
+ }
33
+
34
+ if (cmd === 'update') {
35
+ const { CACHE_DIR } = require('./download')
36
+ const fs = require('fs')
37
+ if (fs.existsSync(CACHE_DIR)) {
38
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true })
39
+ console.log('Cache del runtime eliminado.')
40
+ }
41
+ // Re-descargar en el próximo arranque
42
+ console.log('Ejecutá "npx local-mcp" nuevamente para descargar la última versión.')
43
+ return
44
+ }
45
+
46
+ if (cmd === 'uninstall') {
47
+ const { CACHE_DIR } = require('./download')
48
+ const fs = require('fs')
49
+ if (fs.existsSync(CACHE_DIR)) {
50
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true })
51
+ console.log(`Runtime eliminado de ${CACHE_DIR}`)
52
+ } else {
53
+ console.log('No hay runtime en cache.')
54
+ }
55
+ return
56
+ }
57
+
58
+ if (cmd === 'status') {
59
+ const http = require('http')
60
+ http.get('http://localhost:8765/status', (res) => {
61
+ let data = ''
62
+ res.on('data', c => data += c)
63
+ res.on('end', () => {
64
+ try {
65
+ const s = JSON.parse(data)
66
+ console.log(`Local MCP v${s.version} — ${s.server}`)
67
+ console.log(`Licencia: ${s.license?.status} (${s.license?.days_left}d)`)
68
+ } catch { console.log(data) }
69
+ })
70
+ }).on('error', () => console.log('Servidor no disponible en localhost:8765'))
71
+ return
72
+ }
73
+
74
+ // Modo default: stdio MCP server
75
+ const { ensureRuntime, ensureTray } = require('./download')
76
+
77
+ // Actualizar tray en background en cada startup (sin bloquear el arranque del servidor)
78
+ ensureTray().catch(() => { /* no crítico */ })
79
+
80
+ let runtime
81
+ try {
82
+ runtime = await ensureRuntime()
83
+ } catch (err) {
84
+ process.stderr.write(`\nError al preparar el runtime de Local MCP:\n${err.message}\n`)
85
+ process.stderr.write(`Intentá reinstalar: npx local-mcp update\n\n`)
86
+ process.exit(1)
87
+ }
88
+
89
+ const { binPath } = runtime
90
+
91
+ // ── Symlink estable + migración de config de Claude Desktop ──────────────────
92
+ // El binario corre como hijo de node → TCC muestra "node" en los diálogos de
93
+ // permisos (Messages, Mail, etc.) en lugar de "local-mcp-server".
94
+ // Solución: crear un symlink estable y apuntar Claude Desktop directamente
95
+ // al binario. En la próxima sesión, Claude Desktop llama al binario sin node
96
+ // como padre → TCC ve "com.local-mcp.server" directamente.
97
+ const { CACHE_DIR } = require('./download')
98
+ const stableLink = path.join(CACHE_DIR, 'local-mcp-server')
99
+ try {
100
+ if (fs.existsSync(stableLink)) fs.unlinkSync(stableLink)
101
+ fs.symlinkSync(binPath, stableLink)
102
+
103
+ // Migrar claude_desktop_config.json si todavía usa npx
104
+ const claudeConfig = path.join(
105
+ os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'
106
+ )
107
+ if (fs.existsSync(claudeConfig)) {
108
+ const cfg = JSON.parse(fs.readFileSync(claudeConfig, 'utf8'))
109
+ const srv = cfg.mcpServers?.['local-mcp']
110
+ if (srv && srv.command === 'npx') {
111
+ cfg.mcpServers['local-mcp'] = { command: stableLink }
112
+ fs.writeFileSync(claudeConfig, JSON.stringify(cfg, null, 2))
113
+ process.stderr.write(`\n✓ Config de Claude Desktop actualizado.\n`)
114
+ process.stderr.write(` Reiniciá Claude Desktop para que Local MCP corra sin node como padre.\n`)
115
+ process.stderr.write(` (Esto elimina los diálogos de permisos repetidos.)\n\n`)
116
+ }
117
+ }
118
+ } catch { /* no crítico — seguimos con spawn normal */ }
119
+
120
+ // Spawn para esta sesión (siguiente sesión ya será binario directo)
121
+ const child = spawn(binPath, [], {
122
+ stdio: 'inherit',
123
+ env: process.env,
124
+ })
125
+
126
+ child.on('error', (err) => {
127
+ process.stderr.write(`Error ejecutando Local MCP: ${err.message}\n`)
128
+ process.exit(1)
129
+ })
130
+
131
+ child.on('exit', (code) => {
132
+ process.exit(code ?? 0)
133
+ })
134
+ }
135
+
136
+ main().catch(err => {
137
+ process.stderr.write(`Error fatal: ${err.message}\n`)
138
+ process.exit(1)
139
+ })
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "local-mcp",
3
+ "version": "1.2.0",
4
+ "description": "MCP server for macOS — connect Claude Desktop, Cursor, Windsurf to Mail, Calendar, Contacts, Teams, OneDrive. Privacy-first: all data stays on your Mac.",
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
+ "mcp-server",
29
+ "claude",
30
+ "claude-desktop",
31
+ "cursor",
32
+ "windsurf",
33
+ "vscode",
34
+ "zed",
35
+ "ai",
36
+ "ai-agent",
37
+ "ai-tools",
38
+ "macos",
39
+ "mac",
40
+ "apple",
41
+ "mail",
42
+ "email",
43
+ "gmail",
44
+ "outlook",
45
+ "exchange",
46
+ "icloud",
47
+ "calendar",
48
+ "contacts",
49
+ "teams",
50
+ "microsoft-teams",
51
+ "onedrive",
52
+ "word",
53
+ "excel",
54
+ "powerpoint",
55
+ "pdf",
56
+ "local",
57
+ "privacy",
58
+ "offline",
59
+ "no-cloud",
60
+ "productivity",
61
+ "automation"
62
+ ],
63
+ "author": "Local MCP <hello@local-mcp.com>",
64
+ "license": "SEE LICENSE IN LICENSE",
65
+ "homepage": "https://local-mcp.com",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/lanchuske/local-mcp.git"
69
+ },
70
+ "bugs": {
71
+ "url": "https://github.com/lanchuske/local-mcp/issues"
72
+ },
73
+ "mcpName": "io.github.lanchuske/local-mcp"
74
+ }
package/postinstall.js ADDED
@@ -0,0 +1,23 @@
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
+ process.stderr.write(`\nSetup failed: ${err.message}\n`)
21
+ process.stderr.write('Run manually: npx -y local-mcp@latest setup\n\n')
22
+ process.exit(0)
23
+ })
package/setup.js ADDED
@@ -0,0 +1,252 @@
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 AI clients detected.')
120
+ console.log('Install Claude Desktop, Cursor, Windsurf, or VS Code and run this command again.\n')
121
+ console.log('Or add this manually to your AI client config:')
122
+ console.log(JSON.stringify({ mcpServers: { 'local-mcp': { command: NPX_COMMAND, args: NPX_ARGS } } }, null, 2))
123
+ return
124
+ }
125
+
126
+ console.log(`Detected: ${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} configured`)
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 saved: ${email}`)
153
+ }
154
+ } catch { /* config not created yet — will be created on first run */ }
155
+ }
156
+
157
+ console.log('\n──────────────────────────────────────')
158
+ if (configured.length > 0) {
159
+ console.log(`✅ Local MCP configured for: ${configured.join(', ')}\n`)
160
+ console.log('┌─────────────────────────────────────────────────────┐')
161
+ console.log('│ NEXT STEP — This is important: │')
162
+ console.log('│ │')
163
+ console.log(`│ Quit and reopen ${configured[0].padEnd(35)}│`)
164
+ console.log('│ Local MCP will appear automatically. │')
165
+ console.log('│ │')
166
+ console.log('│ Then try: "Summarize my unread emails" │')
167
+ console.log('└─────────────────────────────────────────────────────┘\n')
168
+ }
169
+ if (failed.length > 0) {
170
+ console.log(`⚠ Could not configure: ${failed.join(', ')} — check permissions and try again.\n`)
171
+ }
172
+
173
+ // Instalar y lanzar el tray (menu bar app) — arm64 only, non-fatal
174
+ await _installTray()
175
+ await _installTeamsProxy()
176
+
177
+ // Ping de tracking (fire-and-forget, no bloquea)
178
+ _pingInstall(configured, opts.method || 'setup')
179
+ }
180
+
181
+ async function _installTeamsProxy() {
182
+ try {
183
+ const { ensureTeamsProxy } = require('./download')
184
+ await ensureTeamsProxy()
185
+ } catch (err) {
186
+ process.stderr.write(` (teams-proxy not available: ${err.message})\n`)
187
+ }
188
+ }
189
+
190
+ async function _installTray() {
191
+ try {
192
+ const { ensureTray } = require('./download')
193
+ const trayApp = await ensureTray()
194
+ if (!trayApp) return // x64 — skip silencioso
195
+ console.log('\n✓ Tray installed — look for the Local MCP icon in your menu bar\n')
196
+ // ensureTray() ya escribió el LaunchAgent y lo cargó con RunAtLoad=true
197
+ // No hace falta llamar open() por separado
198
+ } catch (err) {
199
+ // No fatal — el servidor MCP funciona igual sin el tray
200
+ process.stderr.write(` (Tray not available: ${err.message})\n\n`)
201
+ }
202
+ }
203
+
204
+ function _getMachineId() {
205
+ try {
206
+ // Try reading from macOS Keychain (same as server.py)
207
+ const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
208
+ const id = r.toString().trim()
209
+ if (id) return id
210
+ } catch {}
211
+ try {
212
+ // Generate from hardware UUID
213
+ const crypto = require('crypto')
214
+ const ioreg = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { stdio: 'pipe' }).toString()
215
+ const match = ioreg.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)
216
+ const hwUuid = match ? match[1] : require('os').hostname()
217
+ const id = crypto.createHash('sha256').update(hwUuid).digest('hex').slice(0, 24)
218
+ // Save to Keychain
219
+ try { execSync(`security add-generic-password -s com.local-mcp.machine-id -a machine-id -w ${id} -U 2>/dev/null`, { stdio: 'pipe' }) } catch {}
220
+ return id
221
+ } catch {}
222
+ return ''
223
+ }
224
+
225
+ function _pingInstall(clients, method) {
226
+ try {
227
+ const https = require('https')
228
+ const pkg = require('./package.json')
229
+ const data = JSON.stringify({
230
+ version: pkg.version,
231
+ os_version: require('os').release(),
232
+ arch: process.arch,
233
+ node_version: process.version,
234
+ method: method,
235
+ clients_configured: clients,
236
+ machine_id: _getMachineId(),
237
+ source: process.env.LMCP_SOURCE || '',
238
+ })
239
+ const req = https.request({
240
+ hostname: 'office-mcp-production.up.railway.app',
241
+ path: '/install/npm',
242
+ method: 'POST',
243
+ headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
244
+ timeout: 5000,
245
+ })
246
+ req.on('error', () => {}) // silencioso
247
+ req.write(data)
248
+ req.end()
249
+ } catch { /* no bloquear */ }
250
+ }
251
+
252
+ module.exports = { runSetup, injectMcpConfig, CLIENTS }