local-mcp 3.0.139 → 3.0.141

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 (2) hide show
  1. package/package.json +1 -1
  2. package/setup.js +99 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.139",
3
+ "version": "3.0.141",
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": {
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')
@@ -71,13 +84,49 @@ const _IS_WIN = process.platform === 'win32'
71
84
  const _IS_MAC = process.platform === 'darwin'
72
85
  const _APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming')
73
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
+
74
105
  function _claudeDesktopPath() {
75
- if (_IS_WIN) return path.join(_APPDATA, 'Claude', 'claude_desktop_config.json')
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
+ }
76
112
  if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
77
113
  return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json')
78
114
  }
79
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
+
80
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
+ }
81
130
  return fs.existsSync(path.dirname(_claudeDesktopPath()))
82
131
  }
83
132
 
@@ -221,6 +270,11 @@ function _atomicWriteConfig(filePath, data) {
221
270
  // Never overwrites a file whose JSON cannot be parsed.
222
271
 
223
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
+ }
224
278
  // 1. Read existing config safely
225
279
  const read = _safeReadConfig(client.cfgPath)
226
280
  if (read.hadParseError) {
@@ -253,7 +307,14 @@ function injectMcpConfig(client, command = NPX_COMMAND, args = NPX_ARGS) {
253
307
  }
254
308
 
255
309
  // 3. Atomic write + verify
256
- 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
+ }
257
318
  return {
258
319
  ok: write.ok,
259
320
  stage: write.ok ? 'config_write' : 'config_write_failed',
@@ -453,35 +514,44 @@ async function runSetup(opts = {}) {
453
514
  let emailPromptResult = 'not_shown_no_tty'
454
515
  let emailPromptError = ''
455
516
  try {
456
- const ttyFd = fs.openSync('/dev/tty', 'r+')
457
- const ttyIn = require('stream').Readable.from(
458
- (function* () {
459
- const buf = Buffer.alloc(256)
460
- let line = ''
461
- while (true) {
462
- const n = fs.readSync(ttyFd, buf, 0, 1, null)
463
- if (n === 0) break
464
- const ch = buf.slice(0, n).toString()
465
- if (ch === '\n' || ch === '\r') break
466
- line += ch
467
- }
468
- yield line
469
- })()
470
- )
471
- const ttyOut = new (require('stream').Writable)({
472
- write(chunk, _enc, cb) { fs.writeSync(ttyFd, chunk); cb() },
473
- })
474
- const rl = require('readline').createInterface({ input: ttyIn, output: ttyOut, terminal: true })
517
+ let rl
518
+ let ttyFd = null
519
+
520
+ if (_IS_WIN) {
521
+ // Windows: use process.stdin directly (works in PowerShell and cmd)
522
+ rl = require('readline').createInterface({ input: process.stdin, output: process.stderr })
523
+ } else {
524
+ // macOS/Linux: open /dev/tty to read from terminal even when stdin is piped
525
+ ttyFd = fs.openSync('/dev/tty', 'r+')
526
+ const ttyIn = require('stream').Readable.from(
527
+ (function* () {
528
+ const buf = Buffer.alloc(256)
529
+ let line = ''
530
+ while (true) {
531
+ const n = fs.readSync(ttyFd, buf, 0, 1, null)
532
+ if (n === 0) break
533
+ const ch = buf.slice(0, n).toString()
534
+ if (ch === '\n' || ch === '\r') break
535
+ line += ch
536
+ }
537
+ yield line
538
+ })()
539
+ )
540
+ const ttyOut = new (require('stream').Writable)({
541
+ write(chunk, _enc, cb) { fs.writeSync(ttyFd, chunk); cb() },
542
+ })
543
+ rl = require('readline').createInterface({ input: ttyIn, output: ttyOut, terminal: true })
544
+ }
475
545
 
476
- process.stdout.write('\n 📧 Email for update notifications (optional, press Enter to skip): ')
546
+ process.stderr.write('\n Email for update notifications (optional, press Enter to skip): ')
477
547
  emailPromptResult = 'shown'
478
548
  const ans = await Promise.race([
479
549
  new Promise(res => {
480
- rl.once('line', a => { rl.close(); try { fs.closeSync(ttyFd) } catch {} ; res((a || '').trim()) })
550
+ rl.once('line', a => { rl.close(); if (ttyFd !== null) try { fs.closeSync(ttyFd) } catch {} ; res((a || '').trim()) })
481
551
  }),
482
552
  new Promise(res => setTimeout(() => {
483
553
  try { rl.close() } catch {}
484
- try { fs.closeSync(ttyFd) } catch {}
554
+ if (ttyFd !== null) try { fs.closeSync(ttyFd) } catch {}
485
555
  res('')
486
556
  }, 30000)),
487
557
  ])
@@ -530,7 +600,7 @@ async function runSetup(opts = {}) {
530
600
  console.log('┌─────────────────────────────────────────────────────┐')
531
601
  console.log('│ CURSOR — activate in 3 steps: │')
532
602
  console.log('│ │')
533
- console.log('│ 1. Quit Cursor completely (Cmd+Q) ')
603
+ console.log(`│ 1. Quit Cursor completely (${_IS_WIN ? 'Alt+F4' : 'Cmd+Q'})${_IS_WIN ? ' ' : ' '}│`)
534
604
  console.log('│ 2. Reopen Cursor │')
535
605
  console.log('│ 3. Cursor Settings (⚙) → MCP → find "local-mcp" │')
536
606
  console.log('│ → toggle it ON │')
@@ -547,7 +617,8 @@ async function runSetup(opts = {}) {
547
617
  console.log('┌─────────────────────────────────────────────────────┐')
548
618
  console.log('│ NEXT STEP — restart to activate: │')
549
619
  console.log('│ │')
550
- console.log(`│ 1. Quit ${primaryClient} completely (Cmd+Q)${''.padEnd(Math.max(0, 30 - primaryClient.length))}│`)
620
+ const quitKey = _IS_WIN ? 'Alt+F4' : 'Cmd+Q'
621
+ console.log(`│ 1. Quit ${primaryClient} completely (${quitKey})${''.padEnd(Math.max(0, 28 - primaryClient.length - quitKey.length))}│`)
551
622
  console.log(`│ 2. Reopen ${primaryClient.padEnd(41)}│`)
552
623
  console.log('│ │')
553
624
  console.log('│ Then try: "Summarize my unread emails" │')
@@ -628,15 +699,14 @@ async function _installSlackProxy() {
628
699
  }
629
700
 
630
701
  async function _installTray() {
702
+ // Tray is macOS-only — skip on Windows/Linux
703
+ if (_IS_WIN || process.platform === 'linux') return
631
704
  try {
632
705
  const { ensureTray } = require('./download')
633
706
  const trayApp = await ensureTray()
634
707
  if (!trayApp) return // x64 — skip silencioso
635
708
  console.log('\n✓ Tray installed — look for the LMCP icon in your menu bar\n')
636
- // ensureTray() ya escribió el LaunchAgent y lo cargó con RunAtLoad=true
637
- // No hace falta llamar open() por separado
638
709
  } catch (err) {
639
- // No fatal — el servidor MCP funciona igual sin el tray
640
710
  process.stderr.write(` (Tray not available: ${err.message})\n\n`)
641
711
  }
642
712
  }