local-mcp 3.0.369 → 3.0.371
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 +4 -2
- package/download.js +96 -17
- package/package.json +4 -2
- package/setup.js +78 -15
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Local MCP
|
|
2
2
|
|
|
3
|
-
> **The only Mac MCP you can use from ChatGPT & Claude.ai on the web** — plus
|
|
3
|
+
> **The only Mac MCP you can use from ChatGPT & Claude.ai on the web** — plus 215+ local tools for Claude Desktop, Cursor, Windsurf, VS Code & Zed. Connect any AI to Mail, Calendar, Contacts, iMessage, Teams, Slack, WhatsApp, Signal, OneDrive, Google Drive, Microsoft 365, Notes, Reminders, OmniFocus, Safari, Chrome, Word/Excel/PowerPoint and more. 100% local, no API keys, free — your data never leaves your machine.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/local-mcp)
|
|
6
6
|
[](https://local-mcp.com?ref=npm)
|
|
@@ -74,7 +74,7 @@ LMCP runs a native MCP server that bridges your Mac apps to any AI client:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Tool catalog (macOS)
|
|
78
78
|
|
|
79
79
|
Email (12): `list_accounts` `list_email_accounts` `list_emails` `read_email` `send_email` `reply_email` `create_draft` `search_emails` `move_email` `save_attachment` `create_email_folder` `list_email_folders`
|
|
80
80
|
|
|
@@ -132,6 +132,8 @@ NordVPN (3): `nordvpn_status` `nordvpn_servers` `nordvpn_diagnose`
|
|
|
132
132
|
|
|
133
133
|
Referral (2): `create_referral_invites` `list_referral_candidates`
|
|
134
134
|
|
|
135
|
+
Agent mesh (9): `agent_mesh_status` `agent_mesh_create` `agent_mesh_invite` `agent_mesh_join` `agent_mesh_revoke` `agent_checkin` `agents_list` `agent_send` `agent_inbox`
|
|
136
|
+
|
|
135
137
|
System (12): `lmcp_state` `get_config` `get_datetime` `daily_brief` `run_diagnostics` `get_audit_log` `lmcp_install_upgrade` `lmcp_upgrade_diagnostics` `report_problem` `request_feature` `report_friction` `run_terminal_command`
|
|
136
138
|
|
|
137
139
|
_Coming soon (not included in the 162 count above): **Evernote** (support is being rebuilt)._
|
package/download.js
CHANGED
|
@@ -18,6 +18,15 @@ function extractTar(tarPath, destDir) {
|
|
|
18
18
|
const tarBin = process.platform === 'win32'
|
|
19
19
|
? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
20
20
|
: 'tar'
|
|
21
|
+
// Windows 10 1803+ ships bsdtar at System32\tar.exe. On an older/stripped Windows
|
|
22
|
+
// it's absent — fail with a clear cause instead of a bare execFileSync ENOENT that
|
|
23
|
+
// the caller would otherwise mask as "not yet available" (#1256).
|
|
24
|
+
if (process.platform === 'win32' && !fs.existsSync(tarBin)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`tar.exe not found at ${tarBin} — Windows 10 1803+ (with built-in bsdtar) is ` +
|
|
27
|
+
`required to unpack the LMCP server.`
|
|
28
|
+
)
|
|
29
|
+
}
|
|
21
30
|
execFileSync(tarBin, ['-xzf', tarPath, '-C', destDir], { stdio: 'pipe' })
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -81,6 +90,19 @@ function getArch() {
|
|
|
81
90
|
throw new Error(`Unsupported architecture: ${plat}/${arch}. LMCP requires arm64 or x64.`)
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
// Version of the artifact a given URL actually points at, parsed from the
|
|
94
|
+
// published name lmcp-server-<version>-<os>-<arch>.tar.gz. Returns null when the
|
|
95
|
+
// name doesn't match, so the caller falls back explicitly instead of inventing one.
|
|
96
|
+
//
|
|
97
|
+
// Why this exists: /runtime/latest returns a top-level `version` that is the MAC
|
|
98
|
+
// release, while windows_*/linux_* are built from a separate win-* row. They
|
|
99
|
+
// diverge (2026-08-08: version=3.0.369 next to a 3.0.258 windows artifact), so on
|
|
100
|
+
// non-darwin the Mac number describes neither what gets downloaded nor what runs.
|
|
101
|
+
function versionFromArtifactUrl(url, arch) {
|
|
102
|
+
const m = new RegExp(`/lmcp-server-(.+)-${arch}\\.tar\\.gz$`).exec(String(url || ''))
|
|
103
|
+
return m ? m[1] : null
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* Obtiene la versión más reciente del binario desde el backend.
|
|
86
108
|
* Si esta máquina está en beta_machines, el backend retorna la versión beta.
|
|
@@ -109,7 +131,7 @@ async function getLatestBinary() {
|
|
|
109
131
|
/**
|
|
110
132
|
* Descarga un archivo con barra de progreso.
|
|
111
133
|
*/
|
|
112
|
-
|
|
134
|
+
function downloadOnce(url, destPath) {
|
|
113
135
|
return new Promise((resolve, reject) => {
|
|
114
136
|
const file = fs.createWriteStream(destPath)
|
|
115
137
|
const proto = url.startsWith('https') ? https : http
|
|
@@ -151,6 +173,27 @@ async function downloadFile(url, destPath) {
|
|
|
151
173
|
})
|
|
152
174
|
}
|
|
153
175
|
|
|
176
|
+
// Retry transient download failures (timeouts, resets, 5xx). A 4xx is permanent —
|
|
177
|
+
// a missing/forbidden artifact won't appear on a retry, so fail fast and let the
|
|
178
|
+
// caller decide whether that means "not yet published". Backoff grows per attempt.
|
|
179
|
+
async function downloadFile(url, destPath) {
|
|
180
|
+
const maxAttempts = 3
|
|
181
|
+
let lastErr
|
|
182
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
183
|
+
try {
|
|
184
|
+
return await downloadOnce(url, destPath)
|
|
185
|
+
} catch (err) {
|
|
186
|
+
lastErr = err
|
|
187
|
+
if (/HTTP 4\d\d/.test(err.message)) throw err // permanent, don't retry
|
|
188
|
+
if (attempt < maxAttempts) {
|
|
189
|
+
process.stderr.write(`\n Download failed (${err.message}) — retrying (${attempt}/${maxAttempts - 1})...\n`)
|
|
190
|
+
await new Promise(r => setTimeout(r, 1000 * attempt))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw lastErr
|
|
195
|
+
}
|
|
196
|
+
|
|
154
197
|
/**
|
|
155
198
|
* Asegura que el binario esté descargado y listo.
|
|
156
199
|
* @returns {Promise<{binPath: string}>}
|
|
@@ -167,25 +210,52 @@ async function ensureBinary() {
|
|
|
167
210
|
const binPath = path.join(CACHE_DIR, binName)
|
|
168
211
|
const versionFile = path.join(CACHE_DIR, '.go-server-version')
|
|
169
212
|
|
|
170
|
-
//
|
|
213
|
+
// Resolve the download URL BEFORE the cache check: the version this platform
|
|
214
|
+
// actually installs is the one in that URL, not info.version (the Mac release).
|
|
215
|
+
//
|
|
216
|
+
// Each branch knows its own version by construction, so there is no guessing:
|
|
217
|
+
// · published URL → the version is whatever that name says. If the name
|
|
218
|
+
// doesn't parse we FAIL rather than fall back to info.version, which would
|
|
219
|
+
// record a Mac number against a non-Mac artifact — the very bug this path
|
|
220
|
+
// exists to fix, reappearing in the one corner nobody would look at.
|
|
221
|
+
// · composed URL → we built it from info.version, so that IS the version.
|
|
222
|
+
const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
|
|
223
|
+
const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
|
|
224
|
+
|
|
225
|
+
let url, platformVersion
|
|
226
|
+
if (publishedUrl) {
|
|
227
|
+
platformVersion = versionFromArtifactUrl(publishedUrl, arch)
|
|
228
|
+
if (!platformVersion) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`No pude determinar la versión del artefacto publicado para ${arch}.\n` +
|
|
231
|
+
` URL: ${publishedUrl}\n` +
|
|
232
|
+
` Se esperaba un nombre lmcp-server-<versión>-${arch}.tar.gz.\n` +
|
|
233
|
+
`Reportalo: la convención de nombres del backend cambió.`
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
url = publishedUrl
|
|
237
|
+
} else {
|
|
238
|
+
platformVersion = version
|
|
239
|
+
url = `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if already cached at the right version. Comparing against
|
|
243
|
+
// info.version here re-downloaded the SAME artifact on every Mac release and
|
|
244
|
+
// then recorded the Mac number, so the machine reported a build it wasn't
|
|
245
|
+
// running. Comparing against platformVersion keeps both directions honest:
|
|
246
|
+
// an unchanged Windows artifact is a cache hit, a new one is a miss.
|
|
171
247
|
if (fs.existsSync(binPath)) {
|
|
172
248
|
try {
|
|
173
249
|
const cached = fs.readFileSync(versionFile, 'utf8').trim()
|
|
174
|
-
if (cached ===
|
|
250
|
+
if (cached === platformVersion) return { binPath, versionDir: CACHE_DIR, version: platformVersion }
|
|
175
251
|
} catch {}
|
|
176
252
|
}
|
|
177
253
|
|
|
178
|
-
|
|
179
|
-
// Use the platform-specific URL from /runtime/latest if available (avoids version mismatch)
|
|
180
|
-
const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
|
|
181
|
-
const url = (info[archKey] && info[archKey].startsWith('http'))
|
|
182
|
-
? info[archKey]
|
|
183
|
-
: `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
|
|
184
|
-
process.stderr.write(`\nLMCP v${version} (${arch}) not found in cache.\n`)
|
|
254
|
+
process.stderr.write(`\nLMCP v${platformVersion} (${arch}) not found in cache.\n`)
|
|
185
255
|
process.stderr.write(`Downloading from ${url}\n`)
|
|
186
256
|
|
|
187
257
|
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
188
|
-
const tarPath = path.join(CACHE_DIR, `go-server-${
|
|
258
|
+
const tarPath = path.join(CACHE_DIR, `go-server-${platformVersion}.tar.gz`)
|
|
189
259
|
|
|
190
260
|
try {
|
|
191
261
|
await downloadFile(url, tarPath)
|
|
@@ -193,11 +263,20 @@ async function ensureBinary() {
|
|
|
193
263
|
extractTar(tarPath, CACHE_DIR)
|
|
194
264
|
fs.unlinkSync(tarPath)
|
|
195
265
|
} catch (err) {
|
|
196
|
-
// Go binary not yet published for this version — fall back to message
|
|
197
266
|
try { fs.unlinkSync(tarPath) } catch {}
|
|
267
|
+
// Only a 404 means the artifact for this version/arch genuinely isn't published.
|
|
268
|
+
// Every other failure (network, missing tar.exe, disk) is an environment problem —
|
|
269
|
+
// surface it verbatim so it's diagnosable. Masking ALL failures as "in preview" sent
|
|
270
|
+
// us chasing a backend version-alignment ghost that didn't exist (#1256): the artifact
|
|
271
|
+
// was published, valid and hash-correct; the client just failed to fetch/unpack it.
|
|
272
|
+
if (/HTTP 404/.test(err.message)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`LMCP ${platformVersion} is not yet available for ${arch}.\n` +
|
|
275
|
+
`The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
|
|
276
|
+
)
|
|
277
|
+
}
|
|
198
278
|
throw new Error(
|
|
199
|
-
`LMCP ${
|
|
200
|
-
`The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
|
|
279
|
+
`Failed to install LMCP ${platformVersion} (${arch}) from ${url}:\n ${err.message}`
|
|
201
280
|
)
|
|
202
281
|
}
|
|
203
282
|
|
|
@@ -205,10 +284,10 @@ async function ensureBinary() {
|
|
|
205
284
|
throw new Error(`Binary not found after extraction: ${binPath}`)
|
|
206
285
|
}
|
|
207
286
|
if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
|
|
208
|
-
fs.writeFileSync(versionFile,
|
|
287
|
+
fs.writeFileSync(versionFile, platformVersion)
|
|
209
288
|
|
|
210
289
|
process.stderr.write(` Ready at ${CACHE_DIR}\n`)
|
|
211
|
-
return { binPath, versionDir: CACHE_DIR, version }
|
|
290
|
+
return { binPath, versionDir: CACHE_DIR, version: platformVersion }
|
|
212
291
|
}
|
|
213
292
|
|
|
214
293
|
// macOS: Swift binary in a nested tarball
|
|
@@ -499,4 +578,4 @@ async function ensureSlackProxy() {
|
|
|
499
578
|
}
|
|
500
579
|
}
|
|
501
580
|
|
|
502
|
-
module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, CACHE_DIR, TRAY_DIR }
|
|
581
|
+
module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl, CACHE_DIR, TRAY_DIR }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.371",
|
|
4
4
|
"description": "Let ChatGPT, Claude, Cursor & any MCP client actually use your Mac — read & reply to email, manage your calendar, text over iMessage, find files, work with Teams, Slack & Office. On your Mac, no API keys, free.",
|
|
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
|
+
"linux",
|
|
25
|
+
"win32"
|
|
24
26
|
],
|
|
25
27
|
"keywords": [
|
|
26
28
|
"mcp",
|
package/setup.js
CHANGED
|
@@ -584,7 +584,7 @@ async function runSetup(opts = {}) {
|
|
|
584
584
|
if (configured.length > 0) {
|
|
585
585
|
if (healthOk) {
|
|
586
586
|
console.log(`✅ LMCP configured for: ${configured.join(', ')}\n`)
|
|
587
|
-
console.log(' ✓ Server binary verified —
|
|
587
|
+
console.log(' ✓ Server binary verified — 215+ tools ready\n')
|
|
588
588
|
} else {
|
|
589
589
|
console.log(`⚠ LMCP configured for: ${configured.join(', ')}`)
|
|
590
590
|
console.log(' Server binary could not be verified — it may still work after restart.\n')
|
|
@@ -662,29 +662,50 @@ async function runSetup(opts = {}) {
|
|
|
662
662
|
// can attach an email later via the tray or /settings to unlock cross-Mac
|
|
663
663
|
// sync + paid-license features. Non-fatal on failure — the install still
|
|
664
664
|
// succeeds and the server can retry at startup.
|
|
665
|
-
|
|
665
|
+
// Provisioning is ALWAYS anonymous, with or without an email — same change made in install.sh
|
|
666
|
+
// (#1083). Two separate ways this used to be skipped, and both left the machine with NO
|
|
667
|
+
// cloud_token, depending on /tunnel/token minting one for that address on first contact — the
|
|
668
|
+
// very TOFU path being removed (a stranger can pre-register someone else's address and end up
|
|
669
|
+
// sharing the token):
|
|
670
|
+
// 1. `if (!email)` — an install that supplied LMCP_EMAIL never registered at all.
|
|
671
|
+
// 2. `alreadyLinked` — a config that already carried license_email was skipped too, even
|
|
672
|
+
// with no token. That one is easier to miss: it fires on the SECOND run of a machine
|
|
673
|
+
// whose email was saved a few lines above.
|
|
674
|
+
// Both fail silently: setup finishes, nothing is printed, the relay simply never comes up.
|
|
675
|
+
{
|
|
666
676
|
try {
|
|
667
677
|
const cfg = (_safeReadConfig(cfgFile)).data || {}
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
678
|
+
const existing = (cfg.cloud_token && typeof cfg.cloud_token === 'string'
|
|
679
|
+
&& cfg.cloud_token.startsWith('lmcp-')) ? cfg.cloud_token : ''
|
|
680
|
+
let token = existing
|
|
681
|
+
if (!token) {
|
|
682
|
+
token = await _registerAnonToken()
|
|
683
|
+
if (token) {
|
|
673
684
|
const cfg2 = (_safeReadConfig(cfgFile)).data || {}
|
|
674
|
-
cfg2.cloud_token =
|
|
685
|
+
cfg2.cloud_token = token
|
|
675
686
|
_atomicWriteConfig(cfgFile, cfg2)
|
|
676
687
|
console.log(' ✓ Cloud relay activated (anonymous)')
|
|
677
688
|
}
|
|
678
689
|
}
|
|
690
|
+
// Associate the email with this machine's token — the one just minted OR the one it
|
|
691
|
+
// already had. Outside the "no token yet" branch on purpose: a machine re-running setup
|
|
692
|
+
// (reinstall, upgrade, second attempt) already has a token, and nesting this inside meant
|
|
693
|
+
// its email was dropped without a sound. The cloud_token travels as proof of possession:
|
|
694
|
+
// /install-event has no auth, so that UPDATE refuses to touch a row unless the caller
|
|
695
|
+
// holds that machine's token.
|
|
696
|
+
if (email && token) await _associateEmail(email, token)
|
|
679
697
|
} catch { /* non-fatal */ }
|
|
680
698
|
|
|
681
|
-
// Show settings URL with machine_id so user can register email (LMC-887)
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
699
|
+
// Show settings URL with machine_id so user can register email (LMC-887). Only when no
|
|
700
|
+
// email was supplied — someone who passed LMCP_EMAIL does not need to be asked for it.
|
|
701
|
+
if (!email) {
|
|
702
|
+
try {
|
|
703
|
+
const mid = _getMachineId()
|
|
704
|
+
if (mid) {
|
|
705
|
+
console.log(`\n \x1b[1m📧 Get update notifications:\x1b[0m https://local-mcp.com/settings?m=${mid}`)
|
|
706
|
+
}
|
|
707
|
+
} catch { /* non-fatal */ }
|
|
708
|
+
}
|
|
688
709
|
}
|
|
689
710
|
|
|
690
711
|
// LMCP_METHOD lets install.sh pass 'curl' so we can distinguish it from
|
|
@@ -771,6 +792,48 @@ function _getMachineId() {
|
|
|
771
792
|
return ''
|
|
772
793
|
}
|
|
773
794
|
|
|
795
|
+
/// Associate an email with this machine's tunnel token, presenting the token as proof of
|
|
796
|
+
/// possession. Setup used to get this for free by registering WITH the email; now that
|
|
797
|
+
/// provisioning is always anonymous, the association has to happen explicitly or these users
|
|
798
|
+
/// stay anonymous forever on the backend.
|
|
799
|
+
///
|
|
800
|
+
/// /install-event has no auth, so the UPDATE behind it refuses to touch a row unless the caller
|
|
801
|
+
/// holds that machine's token — that guard is what makes calling this from an unauthenticated
|
|
802
|
+
/// installer safe. Calling it when the email is already set is harmless: the backend also guards
|
|
803
|
+
/// on (email IS NULL OR email = ''), so a no-op is the normal case here, not an error.
|
|
804
|
+
///
|
|
805
|
+
/// Non-fatal on every failure path: setup must finish regardless.
|
|
806
|
+
async function _associateEmail(email, token) {
|
|
807
|
+
return new Promise((resolve) => {
|
|
808
|
+
try {
|
|
809
|
+
const https = require('https')
|
|
810
|
+
const payload = JSON.stringify({
|
|
811
|
+
stage: 'email_prompt',
|
|
812
|
+
email_prompt_result: 'submitted',
|
|
813
|
+
email,
|
|
814
|
+
machine_id: _getMachineId(),
|
|
815
|
+
cloud_token: token,
|
|
816
|
+
source: 'npx-setup',
|
|
817
|
+
})
|
|
818
|
+
const req = https.request({
|
|
819
|
+
host: BACKEND_HOST,
|
|
820
|
+
port: 443,
|
|
821
|
+
method: 'POST',
|
|
822
|
+
path: '/install-event',
|
|
823
|
+
headers: {
|
|
824
|
+
'Content-Type': 'application/json',
|
|
825
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
826
|
+
},
|
|
827
|
+
timeout: 5000,
|
|
828
|
+
}, (res) => { res.on('data', () => {}); res.on('end', () => resolve()) })
|
|
829
|
+
req.on('error', () => resolve())
|
|
830
|
+
req.on('timeout', () => { try { req.destroy() } catch {} resolve() })
|
|
831
|
+
req.write(payload)
|
|
832
|
+
req.end()
|
|
833
|
+
} catch { resolve() }
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
|
|
774
837
|
/// Claim an anonymous tunnel token via /tunnel/register-anon so cloud relay
|
|
775
838
|
/// activates without asking for an email (2026-04-19 anon-first rollout). The
|
|
776
839
|
/// token is keyed on the Mac's machine_id (IOPlatformUUID, shared via Keychain
|