local-mcp 3.0.369 → 3.0.370
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 +54 -14
- package/package.json +1 -1
- package/setup.js +77 -14
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 271 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
|
-
## All
|
|
77
|
+
## All 271 tools (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
|
@@ -81,6 +81,19 @@ function getArch() {
|
|
|
81
81
|
throw new Error(`Unsupported architecture: ${plat}/${arch}. LMCP requires arm64 or x64.`)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Version of the artifact a given URL actually points at, parsed from the
|
|
85
|
+
// published name lmcp-server-<version>-<os>-<arch>.tar.gz. Returns null when the
|
|
86
|
+
// name doesn't match, so the caller falls back explicitly instead of inventing one.
|
|
87
|
+
//
|
|
88
|
+
// Why this exists: /runtime/latest returns a top-level `version` that is the MAC
|
|
89
|
+
// release, while windows_*/linux_* are built from a separate win-* row. They
|
|
90
|
+
// diverge (2026-08-08: version=3.0.369 next to a 3.0.258 windows artifact), so on
|
|
91
|
+
// non-darwin the Mac number describes neither what gets downloaded nor what runs.
|
|
92
|
+
function versionFromArtifactUrl(url, arch) {
|
|
93
|
+
const m = new RegExp(`/lmcp-server-(.+)-${arch}\\.tar\\.gz$`).exec(String(url || ''))
|
|
94
|
+
return m ? m[1] : null
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
/**
|
|
85
98
|
* Obtiene la versión más reciente del binario desde el backend.
|
|
86
99
|
* Si esta máquina está en beta_machines, el backend retorna la versión beta.
|
|
@@ -167,25 +180,52 @@ async function ensureBinary() {
|
|
|
167
180
|
const binPath = path.join(CACHE_DIR, binName)
|
|
168
181
|
const versionFile = path.join(CACHE_DIR, '.go-server-version')
|
|
169
182
|
|
|
170
|
-
//
|
|
183
|
+
// Resolve the download URL BEFORE the cache check: the version this platform
|
|
184
|
+
// actually installs is the one in that URL, not info.version (the Mac release).
|
|
185
|
+
//
|
|
186
|
+
// Each branch knows its own version by construction, so there is no guessing:
|
|
187
|
+
// · published URL → the version is whatever that name says. If the name
|
|
188
|
+
// doesn't parse we FAIL rather than fall back to info.version, which would
|
|
189
|
+
// record a Mac number against a non-Mac artifact — the very bug this path
|
|
190
|
+
// exists to fix, reappearing in the one corner nobody would look at.
|
|
191
|
+
// · composed URL → we built it from info.version, so that IS the version.
|
|
192
|
+
const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
|
|
193
|
+
const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
|
|
194
|
+
|
|
195
|
+
let url, platformVersion
|
|
196
|
+
if (publishedUrl) {
|
|
197
|
+
platformVersion = versionFromArtifactUrl(publishedUrl, arch)
|
|
198
|
+
if (!platformVersion) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`No pude determinar la versión del artefacto publicado para ${arch}.\n` +
|
|
201
|
+
` URL: ${publishedUrl}\n` +
|
|
202
|
+
` Se esperaba un nombre lmcp-server-<versión>-${arch}.tar.gz.\n` +
|
|
203
|
+
`Reportalo: la convención de nombres del backend cambió.`
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
url = publishedUrl
|
|
207
|
+
} else {
|
|
208
|
+
platformVersion = version
|
|
209
|
+
url = `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check if already cached at the right version. Comparing against
|
|
213
|
+
// info.version here re-downloaded the SAME artifact on every Mac release and
|
|
214
|
+
// then recorded the Mac number, so the machine reported a build it wasn't
|
|
215
|
+
// running. Comparing against platformVersion keeps both directions honest:
|
|
216
|
+
// an unchanged Windows artifact is a cache hit, a new one is a miss.
|
|
171
217
|
if (fs.existsSync(binPath)) {
|
|
172
218
|
try {
|
|
173
219
|
const cached = fs.readFileSync(versionFile, 'utf8').trim()
|
|
174
|
-
if (cached ===
|
|
220
|
+
if (cached === platformVersion) return { binPath, versionDir: CACHE_DIR, version: platformVersion }
|
|
175
221
|
} catch {}
|
|
176
222
|
}
|
|
177
223
|
|
|
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`)
|
|
224
|
+
process.stderr.write(`\nLMCP v${platformVersion} (${arch}) not found in cache.\n`)
|
|
185
225
|
process.stderr.write(`Downloading from ${url}\n`)
|
|
186
226
|
|
|
187
227
|
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
188
|
-
const tarPath = path.join(CACHE_DIR, `go-server-${
|
|
228
|
+
const tarPath = path.join(CACHE_DIR, `go-server-${platformVersion}.tar.gz`)
|
|
189
229
|
|
|
190
230
|
try {
|
|
191
231
|
await downloadFile(url, tarPath)
|
|
@@ -196,7 +236,7 @@ async function ensureBinary() {
|
|
|
196
236
|
// Go binary not yet published for this version — fall back to message
|
|
197
237
|
try { fs.unlinkSync(tarPath) } catch {}
|
|
198
238
|
throw new Error(
|
|
199
|
-
`LMCP ${
|
|
239
|
+
`LMCP ${platformVersion} is not yet available for ${arch}.\n` +
|
|
200
240
|
`The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
|
|
201
241
|
)
|
|
202
242
|
}
|
|
@@ -205,10 +245,10 @@ async function ensureBinary() {
|
|
|
205
245
|
throw new Error(`Binary not found after extraction: ${binPath}`)
|
|
206
246
|
}
|
|
207
247
|
if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
|
|
208
|
-
fs.writeFileSync(versionFile,
|
|
248
|
+
fs.writeFileSync(versionFile, platformVersion)
|
|
209
249
|
|
|
210
250
|
process.stderr.write(` Ready at ${CACHE_DIR}\n`)
|
|
211
|
-
return { binPath, versionDir: CACHE_DIR, version }
|
|
251
|
+
return { binPath, versionDir: CACHE_DIR, version: platformVersion }
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
// macOS: Swift binary in a nested tarball
|
|
@@ -499,4 +539,4 @@ async function ensureSlackProxy() {
|
|
|
499
539
|
}
|
|
500
540
|
}
|
|
501
541
|
|
|
502
|
-
module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, CACHE_DIR, TRAY_DIR }
|
|
542
|
+
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.370",
|
|
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": {
|
package/setup.js
CHANGED
|
@@ -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
|