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.
Files changed (4) hide show
  1. package/download.js +97 -31
  2. package/index.js +23 -17
  3. package/package.json +4 -2
  4. 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
- 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
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
- const TRAY_APP = path.join(os.homedir(), 'Applications', 'LocalMCPTray.app')
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
- try {
26
- const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
27
- const id = r.toString().trim()
28
- if (id) return id
29
- } catch {}
30
- try {
31
- // Full hardware UUID (same format as Swift server)
32
- const ioreg = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { stdio: 'pipe' }).toString()
33
- const match = ioreg.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)
34
- return match ? match[1] : os.hostname()
35
- } catch {}
36
- return ''
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 'darwin-arm64'
42
- if (arch === 'x64') return 'darwin-x64'
43
- throw new Error(`Arquitectura no soportada: ${arch}. LMCP requiere macOS arm64 o x64.`)
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
- // URL del binario PyInstaller (nueva nomenclatura)
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
- // Clear quarantine separately — Node.js https downloads don't set it, but tar extraction
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 { /* no valid signature — apply ad-hoc fallback */ }
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 { /* non-critical */ }
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
- // Solo macOS
22
- if (process.platform !== 'darwin') {
23
- console.error('LMCP solo está disponible para macOS.')
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
- 'If you use nvm, run: nvm alias default 22\n' +
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 stableBin = path.join(CACHE_DIR, 'local-mcp-server')
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
- const { ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
121
- ensureTray().catch(() => {})
122
- ensureTeamsProxy().catch(() => {})
123
- ensureHelper().catch(() => {})
124
- ensureJXARunner().catch(() => {})
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. Helper + jxa-runner downloaded only on first install.
183
- ensureTray().catch(() => {})
184
- ensureTeamsProxy().catch(() => {})
185
- ensureHelper().catch(() => {})
186
- ensureJXARunner().catch(() => {})
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.138",
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
- // ── Rutas de config de cada cliente ──────────────────────────────────────────
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: path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
72
- detect: () => fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Claude')),
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: path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json'),
97
- detect: () => fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline')),
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
- return fs.existsSync(`/Applications/${name}.app`) ||
118
- fs.existsSync(path.join(HOME, `Applications/${name}.app`))
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
- try { execSync(`which ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
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
- const write = _atomicWriteConfig(client.cfgPath, cfg)
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(' Fix: nvm alias default 22 (then relaunch Claude Desktop)\n')
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('│ 1. Quit Cursor completely (Cmd+Q) ')
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
- console.log(`│ 1. Quit ${primaryClient} completely (Cmd+Q)${''.padEnd(Math.max(0, 30 - primaryClient.length))}│`)
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
  }