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 +126 -0
- package/download.js +314 -0
- package/index.js +139 -0
- package/package.json +74 -0
- package/postinstall.js +23 -0
- package/setup.js +252 -0
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
|
+
[](https://www.npmjs.com/package/local-mcp)
|
|
6
|
+
[](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 }
|