local-mcp 3.0.138 → 3.0.140
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 +97 -31
- package/index.js +23 -17
- package/package.json +4 -2
- package/setup.js +113 -16
package/download.js
CHANGED
|
@@ -13,34 +13,61 @@ const os = require('os')
|
|
|
13
13
|
const { execFileSync } = require('child_process')
|
|
14
14
|
|
|
15
15
|
const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
// Platform-aware cache directory:
|
|
18
|
+
// macOS: ~/.local/share/local-mcp/bin
|
|
19
|
+
// Windows: %LOCALAPPDATA%/local-mcp/bin
|
|
20
|
+
// Linux: ~/.local/share/local-mcp/bin
|
|
21
|
+
const CACHE_DIR = process.platform === 'win32'
|
|
22
|
+
? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'local-mcp', 'bin')
|
|
23
|
+
: path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin')
|
|
24
|
+
|
|
25
|
+
const TRAY_DIR = process.platform === 'win32'
|
|
26
|
+
? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'local-mcp', 'tray')
|
|
27
|
+
: path.join(os.homedir(), '.local', 'share', 'local-mcp', 'tray')
|
|
28
|
+
|
|
18
29
|
// Install to ~/Applications (user-owned) to avoid macOS App Management TCC prompt.
|
|
19
30
|
// /Applications requires kTCCServiceAppManagement permission which shows "node would like
|
|
20
31
|
// to access data from other apps" on every tray update — confusing and unnecessary.
|
|
21
|
-
|
|
32
|
+
// On Windows/Linux there is no tray app yet.
|
|
33
|
+
const TRAY_APP = process.platform === 'darwin'
|
|
34
|
+
? path.join(os.homedir(), 'Applications', 'LocalMCPTray.app')
|
|
35
|
+
: null
|
|
22
36
|
|
|
23
37
|
function _getMachineId() {
|
|
24
38
|
const { execSync } = require('child_process')
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
if (process.platform === 'darwin') {
|
|
40
|
+
try {
|
|
41
|
+
const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
|
|
42
|
+
const id = r.toString().trim()
|
|
43
|
+
if (id) return id
|
|
44
|
+
} catch {}
|
|
45
|
+
try {
|
|
46
|
+
const ioreg = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { stdio: 'pipe' }).toString()
|
|
47
|
+
const match = ioreg.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)
|
|
48
|
+
return match ? match[1] : os.hostname()
|
|
49
|
+
} catch {}
|
|
50
|
+
} else if (process.platform === 'win32') {
|
|
51
|
+
try {
|
|
52
|
+
const wmic = execSync('wmic csproduct get UUID /value', { stdio: 'pipe' }).toString()
|
|
53
|
+
const match = wmic.match(/UUID=(.+)/)
|
|
54
|
+
if (match) return match[1].trim()
|
|
55
|
+
} catch {}
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
const mid = fs.readFileSync('/etc/machine-id', 'utf8').trim()
|
|
59
|
+
if (mid) return mid
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
return os.hostname()
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
function getArch() {
|
|
66
|
+
const plat = process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : 'darwin'
|
|
40
67
|
const arch = process.arch
|
|
41
|
-
if (arch === 'arm64') return
|
|
42
|
-
if (arch === 'x64') return
|
|
43
|
-
throw new Error(`
|
|
68
|
+
if (arch === 'arm64') return `${plat}-arm64`
|
|
69
|
+
if (arch === 'x64') return `${plat}-amd64`
|
|
70
|
+
throw new Error(`Unsupported architecture: ${plat}/${arch}. LMCP requires arm64 or x64.`)
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
/**
|
|
@@ -121,7 +148,55 @@ async function ensureBinary() {
|
|
|
121
148
|
const arch = getArch()
|
|
122
149
|
const info = await getLatestBinary()
|
|
123
150
|
const version = info.version
|
|
124
|
-
|
|
151
|
+
|
|
152
|
+
// On non-darwin, use the Go server binary (cross-platform).
|
|
153
|
+
// The Go binary is a single file, not a nested tarball.
|
|
154
|
+
if (process.platform !== 'darwin') {
|
|
155
|
+
const binName = process.platform === 'win32' ? 'lmcp-server.exe' : 'lmcp-server'
|
|
156
|
+
const binPath = path.join(CACHE_DIR, binName)
|
|
157
|
+
const versionFile = path.join(CACHE_DIR, '.go-server-version')
|
|
158
|
+
|
|
159
|
+
// Check if already cached at the right version
|
|
160
|
+
if (fs.existsSync(binPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const cached = fs.readFileSync(versionFile, 'utf8').trim()
|
|
163
|
+
if (cached === version) return { binPath, versionDir: CACHE_DIR, version }
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Download the Go binary tarball from R2
|
|
168
|
+
const url = `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
|
|
169
|
+
process.stderr.write(`\nLMCP v${version} (${arch}) not found in cache.\n`)
|
|
170
|
+
process.stderr.write(`Downloading from ${url}\n`)
|
|
171
|
+
|
|
172
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
173
|
+
const tarPath = path.join(CACHE_DIR, `go-server-${version}.tar.gz`)
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await downloadFile(url, tarPath)
|
|
177
|
+
process.stderr.write(` Extracting...\n`)
|
|
178
|
+
execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
|
|
179
|
+
fs.unlinkSync(tarPath)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Go binary not yet published for this version — fall back to message
|
|
182
|
+
try { fs.unlinkSync(tarPath) } catch {}
|
|
183
|
+
throw new Error(
|
|
184
|
+
`LMCP ${version} is not yet available for ${arch}.\n` +
|
|
185
|
+
`The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(binPath)) {
|
|
190
|
+
throw new Error(`Binary not found after extraction: ${binPath}`)
|
|
191
|
+
}
|
|
192
|
+
if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
|
|
193
|
+
fs.writeFileSync(versionFile, version)
|
|
194
|
+
|
|
195
|
+
process.stderr.write(` Ready at ${CACHE_DIR}\n`)
|
|
196
|
+
return { binPath, versionDir: CACHE_DIR, version }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// macOS: Swift binary in a nested tarball
|
|
125
200
|
const url = `https://download.local-mcp.com/local-mcp-server-${version}-${arch}.tar.gz`
|
|
126
201
|
|
|
127
202
|
const versionDir = path.join(CACHE_DIR, version)
|
|
@@ -148,28 +223,19 @@ async function ensureBinary() {
|
|
|
148
223
|
throw new Error(`Binario no encontrado después de extraer: ${binPath}`)
|
|
149
224
|
}
|
|
150
225
|
|
|
151
|
-
// Asegurar que sea ejecutable
|
|
152
226
|
fs.chmodSync(binPath, 0o755)
|
|
153
227
|
|
|
154
|
-
|
|
155
|
-
// on some macOS versions can inherit it from the archive. Non-critical.
|
|
156
|
-
try { execFileSync('xattr', ['-d', 'com.apple.quarantine', binPath], { stdio: 'pipe' }) } catch { /* no quarantine, fine */ }
|
|
228
|
+
try { execFileSync('xattr', ['-d', 'com.apple.quarantine', binPath], { stdio: 'pipe' }) } catch {}
|
|
157
229
|
|
|
158
|
-
// Preserve the Developer ID signature from the build pipeline.
|
|
159
|
-
// On macOS 26+ ad-hoc signatures (--sign -) don't create a stable TCC identity:
|
|
160
|
-
// each new CDHash is treated as a new app, triggering "node would like to access
|
|
161
|
-
// data from other apps" on every JXA call. Only fall back to ad-hoc if the binary
|
|
162
|
-
// has no valid signature at all (e.g. a dev build that was never properly signed).
|
|
163
|
-
// See: FB-74E468, FB-017B8E — permission prompt loop on macOS 26 beta.
|
|
164
230
|
let hasSig = false
|
|
165
231
|
try {
|
|
166
232
|
execFileSync('codesign', ['--verify', binPath], { stdio: 'pipe' })
|
|
167
233
|
hasSig = true
|
|
168
|
-
} catch {
|
|
234
|
+
} catch {}
|
|
169
235
|
if (!hasSig) {
|
|
170
236
|
try {
|
|
171
237
|
execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', binPath], { stdio: 'pipe' })
|
|
172
|
-
} catch {
|
|
238
|
+
} catch {}
|
|
173
239
|
}
|
|
174
240
|
|
|
175
241
|
process.stderr.write(` Listo en ${versionDir}\n`)
|
package/index.js
CHANGED
|
@@ -18,9 +18,10 @@ const fs = require('fs')
|
|
|
18
18
|
const https = require('https')
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// Platform support: macOS (full), Windows (Go server), Linux (Go server)
|
|
22
|
+
const SUPPORTED_PLATFORMS = ['darwin', 'win32', 'linux']
|
|
23
|
+
if (!SUPPORTED_PLATFORMS.includes(process.platform)) {
|
|
24
|
+
console.error(`LMCP is not available for ${process.platform}. Supported: macOS, Windows, Linux.`)
|
|
24
25
|
process.exit(1)
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -30,7 +31,7 @@ if (process.platform !== 'darwin') {
|
|
|
30
31
|
// user sees it in Claude Desktop's MCP logs instead of a cryptic spawn error.
|
|
31
32
|
if (parseInt(process.version.slice(1)) < 16) {
|
|
32
33
|
const msg = `LMCP requires Node 16+. Running ${process.version}.\n` +
|
|
33
|
-
|
|
34
|
+
`If you use nvm, run: nvm alias default 22 && nvm uninstall ${process.version}\n` +
|
|
34
35
|
'Then restart Claude Desktop / Cursor.'
|
|
35
36
|
process.stderr.write(msg + '\n')
|
|
36
37
|
// Exit with a non-zero code so the MCP host marks the server as failed
|
|
@@ -89,7 +90,8 @@ async function main() {
|
|
|
89
90
|
|
|
90
91
|
// ── Modo default: stdio MCP server ──────────────────────────────────────────
|
|
91
92
|
const { CACHE_DIR } = require('./download')
|
|
92
|
-
const
|
|
93
|
+
const binName = process.platform === 'win32' ? 'local-mcp-server.exe' : 'local-mcp-server'
|
|
94
|
+
const stableBin = path.join(CACHE_DIR, binName)
|
|
93
95
|
const versionFile = path.join(CACHE_DIR, '.server-version')
|
|
94
96
|
const pkg = require('./package.json')
|
|
95
97
|
|
|
@@ -117,11 +119,13 @@ async function main() {
|
|
|
117
119
|
: ''
|
|
118
120
|
if (cachedVersion && semverGte(cachedVersion, pkg.version)) {
|
|
119
121
|
// Cached binary is same or newer — use fast path, no download needed
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
if (process.platform === 'darwin') {
|
|
123
|
+
const { ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
|
|
124
|
+
ensureTray().catch(() => {})
|
|
125
|
+
ensureTeamsProxy().catch(() => {})
|
|
126
|
+
ensureHelper().catch(() => {})
|
|
127
|
+
ensureJXARunner().catch(() => {})
|
|
128
|
+
}
|
|
125
129
|
const child = spawn(stableBin, [], { stdio: 'inherit', env: process.env })
|
|
126
130
|
child.on('error', () => process.exit(1))
|
|
127
131
|
child.on('exit', (code) => process.exit(code ?? 0))
|
|
@@ -150,7 +154,7 @@ async function main() {
|
|
|
150
154
|
const tmpPath = stableBin + '.tmp'
|
|
151
155
|
fs.copyFileSync(binPath, tmpPath)
|
|
152
156
|
fs.chmodSync(tmpPath, 0o755)
|
|
153
|
-
try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {}
|
|
157
|
+
if (process.platform === 'darwin') { try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {} }
|
|
154
158
|
fs.renameSync(tmpPath, stableBin)
|
|
155
159
|
fs.writeFileSync(versionFile, downloadedVersion || latestVersion)
|
|
156
160
|
} catch {}
|
|
@@ -179,11 +183,13 @@ async function main() {
|
|
|
179
183
|
// Slow path: download/repair binary
|
|
180
184
|
const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
|
|
181
185
|
|
|
182
|
-
// Update tray + proxies in background
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
// Update tray + proxies in background (macOS only).
|
|
187
|
+
if (process.platform === 'darwin') {
|
|
188
|
+
ensureTray().catch(() => {})
|
|
189
|
+
ensureTeamsProxy().catch(() => {})
|
|
190
|
+
ensureHelper().catch(() => {})
|
|
191
|
+
ensureJXARunner().catch(() => {})
|
|
192
|
+
}
|
|
187
193
|
|
|
188
194
|
let runtime
|
|
189
195
|
try {
|
|
@@ -201,7 +207,7 @@ async function main() {
|
|
|
201
207
|
const tmpPath = stableBin + '.tmp'
|
|
202
208
|
fs.copyFileSync(binPath, tmpPath)
|
|
203
209
|
fs.chmodSync(tmpPath, 0o755)
|
|
204
|
-
try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {}
|
|
210
|
+
if (process.platform === 'darwin') { try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {} }
|
|
205
211
|
fs.renameSync(tmpPath, stableBin)
|
|
206
212
|
// Write the ACTUAL downloaded binary version (not pkg.version) so the fast path
|
|
207
213
|
// knows what version is on disk. Using pkg.version here caused LMC-375: machines
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.140",
|
|
4
4
|
"description": "LMCP — connect Claude Desktop, Cursor, Windsurf to Mail, Calendar, Contacts, Teams, OneDrive on macOS. Privacy-first: all data stays on your Mac.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"node": ">=18"
|
|
21
21
|
},
|
|
22
22
|
"os": [
|
|
23
|
-
"darwin"
|
|
23
|
+
"darwin",
|
|
24
|
+
"win32",
|
|
25
|
+
"linux"
|
|
24
26
|
],
|
|
25
27
|
"keywords": [
|
|
26
28
|
"mcp",
|
package/setup.js
CHANGED
|
@@ -34,6 +34,19 @@ function _resolveNpxPath() {
|
|
|
34
34
|
// BEFORE Node loads our code (e.g. old nvm default in Claude Desktop's PATH).
|
|
35
35
|
// Returns the path to the launcher, or null if writing fails.
|
|
36
36
|
function _writeLaunchScript(npxAbsPath, cacheDir) {
|
|
37
|
+
// On Windows, don't write a bash launch script — use npx directly
|
|
38
|
+
if (_IS_WIN) {
|
|
39
|
+
try {
|
|
40
|
+
// Write a .cmd wrapper that Claude Desktop can execute
|
|
41
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
42
|
+
const cmdPath = path.join(cacheDir, 'lmcp-launch.cmd')
|
|
43
|
+
const script = `@echo off\r\n"${npxAbsPath}" -y local-mcp@latest %*\r\n`
|
|
44
|
+
fs.writeFileSync(cmdPath, script)
|
|
45
|
+
return cmdPath
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
37
50
|
try {
|
|
38
51
|
fs.mkdirSync(cacheDir, { recursive: true })
|
|
39
52
|
const launcherPath = path.join(cacheDir, 'lmcp-launch.sh')
|
|
@@ -48,7 +61,7 @@ function _writeLaunchScript(npxAbsPath, cacheDir) {
|
|
|
48
61
|
` curl -sf --max-time 3 -X POST "https://${BACKEND_HOST}/install-event" \\`,
|
|
49
62
|
' -H "Content-Type: application/json" \\',
|
|
50
63
|
' -d "{\\\"stage\\\":\\\"node_too_old\\\",\\\"version\\\":\\\"$NODE_VER\\\"}" &>/dev/null &',
|
|
51
|
-
' printf \'LMCP requires Node 16+. Current: %s\\nFix: nvm alias default 22 — then restart Claude Desktop.\\n\' "$NODE_VER" >&2',
|
|
64
|
+
' printf \'LMCP requires Node 16+. Current: %s\\nFix: nvm alias default 22 && nvm uninstall %s — then restart Claude Desktop.\\n\' "$NODE_VER" "$NODE_VER" >&2',
|
|
52
65
|
' exit 1',
|
|
53
66
|
'fi',
|
|
54
67
|
`exec "${npxAbsPath}" -y local-mcp@latest "$@"`,
|
|
@@ -62,14 +75,73 @@ function _writeLaunchScript(npxAbsPath, cacheDir) {
|
|
|
62
75
|
const STABLE_LINK = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin', 'local-mcp-server')
|
|
63
76
|
const BACKEND_HOST = 'office-mcp-production.up.railway.app'
|
|
64
77
|
|
|
65
|
-
// ──
|
|
78
|
+
// ── Platform-aware config paths for AI clients ─────────────────────────────
|
|
79
|
+
// macOS: ~/Library/Application Support/...
|
|
80
|
+
// Windows: %APPDATA%/... (most Electron apps use APPDATA)
|
|
81
|
+
// Linux: ~/.config/... (XDG standard)
|
|
82
|
+
|
|
83
|
+
const _IS_WIN = process.platform === 'win32'
|
|
84
|
+
const _IS_MAC = process.platform === 'darwin'
|
|
85
|
+
const _APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming')
|
|
86
|
+
|
|
87
|
+
// Windows MSIX (Microsoft Store / WinGet) installs read config from a
|
|
88
|
+
// virtualized path inside %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude.
|
|
89
|
+
// The non-MSIX (.exe) install reads from %APPDATA%\Claude.
|
|
90
|
+
// We detect which one exists and write to both if needed.
|
|
91
|
+
function _findMsixClaudePath() {
|
|
92
|
+
if (!_IS_WIN) return null
|
|
93
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local')
|
|
94
|
+
const packagesDir = path.join(localAppData, 'Packages')
|
|
95
|
+
try {
|
|
96
|
+
const entries = fs.readdirSync(packagesDir)
|
|
97
|
+
const claudePkg = entries.find(e => e.startsWith('Claude_'))
|
|
98
|
+
if (claudePkg) {
|
|
99
|
+
return path.join(packagesDir, claudePkg, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json')
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _claudeDesktopPath() {
|
|
106
|
+
if (_IS_WIN) {
|
|
107
|
+
// Prefer MSIX path if the package exists
|
|
108
|
+
const msix = _findMsixClaudePath()
|
|
109
|
+
if (msix) return msix
|
|
110
|
+
return path.join(_APPDATA, 'Claude', 'claude_desktop_config.json')
|
|
111
|
+
}
|
|
112
|
+
if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
113
|
+
return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// All paths where we should write the config (MSIX + non-MSIX on Windows)
|
|
117
|
+
function _claudeDesktopAllPaths() {
|
|
118
|
+
if (!_IS_WIN) return [_claudeDesktopPath()]
|
|
119
|
+
const paths = [path.join(_APPDATA, 'Claude', 'claude_desktop_config.json')]
|
|
120
|
+
const msix = _findMsixClaudePath()
|
|
121
|
+
if (msix && !paths.includes(msix)) paths.push(msix)
|
|
122
|
+
return paths
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _claudeDesktopDetect() {
|
|
126
|
+
if (_IS_WIN) {
|
|
127
|
+
// Check both MSIX and non-MSIX paths
|
|
128
|
+
return _claudeDesktopAllPaths().some(p => fs.existsSync(path.dirname(p)))
|
|
129
|
+
}
|
|
130
|
+
return fs.existsSync(path.dirname(_claudeDesktopPath()))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _rooClinePath() {
|
|
134
|
+
if (_IS_WIN) return path.join(_APPDATA, 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
135
|
+
if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
136
|
+
return path.join(HOME, '.config', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
137
|
+
}
|
|
66
138
|
|
|
67
139
|
const CLIENTS = [
|
|
68
140
|
{
|
|
69
141
|
id: 'claude-desktop',
|
|
70
142
|
name: 'Claude Desktop',
|
|
71
|
-
cfgPath:
|
|
72
|
-
detect:
|
|
143
|
+
cfgPath: _claudeDesktopPath(),
|
|
144
|
+
detect: _claudeDesktopDetect,
|
|
73
145
|
},
|
|
74
146
|
{
|
|
75
147
|
id: 'cursor',
|
|
@@ -93,8 +165,8 @@ const CLIENTS = [
|
|
|
93
165
|
{
|
|
94
166
|
id: 'roo-cline',
|
|
95
167
|
name: 'Roo-Cline',
|
|
96
|
-
cfgPath:
|
|
97
|
-
detect: () => fs.existsSync(path.
|
|
168
|
+
cfgPath: _rooClinePath(),
|
|
169
|
+
detect: () => fs.existsSync(path.dirname(_rooClinePath())),
|
|
98
170
|
},
|
|
99
171
|
{
|
|
100
172
|
id: 'zed',
|
|
@@ -114,12 +186,25 @@ const CLIENTS = [
|
|
|
114
186
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
115
187
|
|
|
116
188
|
function _appExists(name) {
|
|
117
|
-
|
|
118
|
-
|
|
189
|
+
if (_IS_MAC) {
|
|
190
|
+
return fs.existsSync(`/Applications/${name}.app`) ||
|
|
191
|
+
fs.existsSync(path.join(HOME, `Applications/${name}.app`))
|
|
192
|
+
}
|
|
193
|
+
if (_IS_WIN) {
|
|
194
|
+
// Check common Windows install locations
|
|
195
|
+
const pf = process.env.ProgramFiles || 'C:\\Program Files'
|
|
196
|
+
const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'
|
|
197
|
+
const localApp = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local')
|
|
198
|
+
return fs.existsSync(path.join(pf, name)) ||
|
|
199
|
+
fs.existsSync(path.join(pf86, name)) ||
|
|
200
|
+
fs.existsSync(path.join(localApp, name))
|
|
201
|
+
}
|
|
202
|
+
return false // Linux: rely on _cmdExists or config path detection
|
|
119
203
|
}
|
|
120
204
|
|
|
121
205
|
function _cmdExists(cmd) {
|
|
122
|
-
|
|
206
|
+
const which = _IS_WIN ? 'where' : 'which'
|
|
207
|
+
try { execSync(`${which} ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
|
|
123
208
|
}
|
|
124
209
|
|
|
125
210
|
// ── Safe config read ─────────────────────────────────────────────────────────
|
|
@@ -185,6 +270,11 @@ function _atomicWriteConfig(filePath, data) {
|
|
|
185
270
|
// Never overwrites a file whose JSON cannot be parsed.
|
|
186
271
|
|
|
187
272
|
function injectMcpConfig(client, command = NPX_COMMAND, args = NPX_ARGS) {
|
|
273
|
+
// On Windows, Claude Desktop needs cmd /c wrapper to find npx
|
|
274
|
+
if (_IS_WIN && client.id === 'claude-desktop') {
|
|
275
|
+
command = 'cmd'
|
|
276
|
+
args = ['/c', 'npx', '-y', 'local-mcp@latest']
|
|
277
|
+
}
|
|
188
278
|
// 1. Read existing config safely
|
|
189
279
|
const read = _safeReadConfig(client.cfgPath)
|
|
190
280
|
if (read.hadParseError) {
|
|
@@ -217,7 +307,14 @@ function injectMcpConfig(client, command = NPX_COMMAND, args = NPX_ARGS) {
|
|
|
217
307
|
}
|
|
218
308
|
|
|
219
309
|
// 3. Atomic write + verify
|
|
220
|
-
|
|
310
|
+
// On Windows, Claude Desktop MSIX reads from a different path than the .exe install.
|
|
311
|
+
// Write to all known paths so both install types work.
|
|
312
|
+
const allPaths = (client.id === 'claude-desktop' && _IS_WIN) ? _claudeDesktopAllPaths() : [client.cfgPath]
|
|
313
|
+
let write = { ok: false, error: 'no paths' }
|
|
314
|
+
for (const p of allPaths) {
|
|
315
|
+
fs.mkdirSync(path.dirname(p), { recursive: true })
|
|
316
|
+
write = _atomicWriteConfig(p, cfg)
|
|
317
|
+
}
|
|
221
318
|
return {
|
|
222
319
|
ok: write.ok,
|
|
223
320
|
stage: write.ok ? 'config_write' : 'config_write_failed',
|
|
@@ -262,7 +359,7 @@ async function runSetup(opts = {}) {
|
|
|
262
359
|
if (nodeMajor < 16) {
|
|
263
360
|
console.error(`\n⚠️ Warning: you're running Node ${process.version}.`)
|
|
264
361
|
console.error(' Claude Desktop may fail to start LMCP because it finds an old npx on its PATH.')
|
|
265
|
-
console.error(
|
|
362
|
+
console.error(` Fix: nvm alias default 22 && nvm uninstall ${process.version} (then relaunch Claude Desktop)\n`)
|
|
266
363
|
}
|
|
267
364
|
|
|
268
365
|
// Pre-download binary so first launch is instant
|
|
@@ -494,7 +591,7 @@ async function runSetup(opts = {}) {
|
|
|
494
591
|
console.log('┌─────────────────────────────────────────────────────┐')
|
|
495
592
|
console.log('│ CURSOR — activate in 3 steps: │')
|
|
496
593
|
console.log('│ │')
|
|
497
|
-
console.log(
|
|
594
|
+
console.log(`│ 1. Quit Cursor completely (${_IS_WIN ? 'Alt+F4' : 'Cmd+Q'})${_IS_WIN ? ' ' : ' '}│`)
|
|
498
595
|
console.log('│ 2. Reopen Cursor │')
|
|
499
596
|
console.log('│ 3. Cursor Settings (⚙) → MCP → find "local-mcp" │')
|
|
500
597
|
console.log('│ → toggle it ON │')
|
|
@@ -511,7 +608,8 @@ async function runSetup(opts = {}) {
|
|
|
511
608
|
console.log('┌─────────────────────────────────────────────────────┐')
|
|
512
609
|
console.log('│ NEXT STEP — restart to activate: │')
|
|
513
610
|
console.log('│ │')
|
|
514
|
-
|
|
611
|
+
const quitKey = _IS_WIN ? 'Alt+F4' : 'Cmd+Q'
|
|
612
|
+
console.log(`│ 1. Quit ${primaryClient} completely (${quitKey})${''.padEnd(Math.max(0, 28 - primaryClient.length - quitKey.length))}│`)
|
|
515
613
|
console.log(`│ 2. Reopen ${primaryClient.padEnd(41)}│`)
|
|
516
614
|
console.log('│ │')
|
|
517
615
|
console.log('│ Then try: "Summarize my unread emails" │')
|
|
@@ -592,15 +690,14 @@ async function _installSlackProxy() {
|
|
|
592
690
|
}
|
|
593
691
|
|
|
594
692
|
async function _installTray() {
|
|
693
|
+
// Tray is macOS-only — skip on Windows/Linux
|
|
694
|
+
if (_IS_WIN || process.platform === 'linux') return
|
|
595
695
|
try {
|
|
596
696
|
const { ensureTray } = require('./download')
|
|
597
697
|
const trayApp = await ensureTray()
|
|
598
698
|
if (!trayApp) return // x64 — skip silencioso
|
|
599
699
|
console.log('\n✓ Tray installed — look for the LMCP icon in your menu bar\n')
|
|
600
|
-
// ensureTray() ya escribió el LaunchAgent y lo cargó con RunAtLoad=true
|
|
601
|
-
// No hace falta llamar open() por separado
|
|
602
700
|
} catch (err) {
|
|
603
|
-
// No fatal — el servidor MCP funciona igual sin el tray
|
|
604
701
|
process.stderr.write(` (Tray not available: ${err.message})\n\n`)
|
|
605
702
|
}
|
|
606
703
|
}
|