@pandanpc/mcp-server 0.1.30 → 0.1.33
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 +13 -0
- package/index.js +45 -22
- package/install.js +4 -42
- package/network.js +45 -0
- package/package.json +6 -3
- package/updater.js +145 -0
package/README.md
CHANGED
|
@@ -68,6 +68,19 @@ command = "pandanpc-mcp"
|
|
|
68
68
|
|
|
69
69
|
Account operations require `note_login` when not already authenticated. Reading guides does not require a note login.
|
|
70
70
|
|
|
71
|
+
## Automatic runtime updates
|
|
72
|
+
|
|
73
|
+
The npm launcher starts the last verified runtime immediately and checks for a newer stable release in a separate background process (at most once every six hours; failed checks back off for 15 minutes). It downloads the platform binary into the user's cache, verifies SHA-256, and runs a bounded startup check before atomically selecting it for the next launch. Current MCP sessions are never restarted or replaced. Offline or failed updates keep the existing runtime usable.
|
|
74
|
+
|
|
75
|
+
The cache defaults to `~/.pandanpc-mcp/runtime/<platform>-<arch>` and does not require write access to a global npm installation. Each version uses its own file, including on Windows. Older cached executables are retained because another session may still use them; remove the cache only after closing those sessions if you want to reclaim disk space.
|
|
76
|
+
|
|
77
|
+
- `pandanpc-mcp --version` shows the selected runtime and npm launcher versions.
|
|
78
|
+
- `pandanpc-mcp --update` checks/downloads now; reconnect afterwards to use the result. Status messages go to stderr, never the MCP protocol stream.
|
|
79
|
+
- Set `PANDANPC_MCP_AUTO_UPDATE=0` in the MCP client's environment to disable checks and pin the runtime bundled with the installed npm version.
|
|
80
|
+
- `PANDANPC_MCP_CACHE_DIR` optionally selects a different user-writable cache directory.
|
|
81
|
+
|
|
82
|
+
Existing users must first upgrade to a launcher containing this feature with `npm install -g @pandanpc/mcp-server@latest`, then reconnect. Automatic updates cover the executable and its embedded guides, not the npm launcher code or separately copied Skill files. A future release requiring a different updater protocol must be installed through npm; `--update` reports that condition. Directly launching a downloaded Rust binary bypasses the npm updater.
|
|
83
|
+
|
|
71
84
|
## Optional Agent Skill
|
|
72
85
|
|
|
73
86
|
The package includes `skills/pandanpc/SKILL.md` and its references. These are the same guide files embedded in the MCP binary, so examples stay aligned with the installed version. MCP-only clients can call `get_guide`; they do not need filesystem or Skill support.
|
package/index.js
CHANGED
|
@@ -8,30 +8,53 @@ const path = require('path')
|
|
|
8
8
|
const { spawn } = require('child_process')
|
|
9
9
|
const fs = require('fs')
|
|
10
10
|
const { binaryName } = require('./binary-name')
|
|
11
|
+
const pkg = require('./package.json')
|
|
12
|
+
const { selectRuntime, startBackgroundUpdate, checkForUpdates } = require('./updater')
|
|
11
13
|
|
|
12
14
|
const binName = binaryName()
|
|
13
|
-
const
|
|
15
|
+
const bundledPath = path.join(__dirname, 'bin', binName)
|
|
16
|
+
const runtime = selectRuntime()
|
|
17
|
+
let child
|
|
14
18
|
|
|
15
|
-
if (
|
|
16
|
-
console.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
if (process.argv[2] === '--version') {
|
|
20
|
+
console.log(`pandanpc-mcp ${runtime.version} (launcher ${pkg.version})`)
|
|
21
|
+
process.exit(0)
|
|
22
|
+
}
|
|
23
|
+
if (process.argv[2] === '--update') {
|
|
24
|
+
checkForUpdates({ force: true }).then(result => {
|
|
25
|
+
console.error(`[pandanpc-mcp] update: ${result.status}${result.version ? ` ${result.version}` : ''}${result.error ? ` (${result.error})` : ''}`)
|
|
26
|
+
if (result.status === 'manual-update-required') console.error('Update the launcher: npm install -g @pandanpc/mcp-server@latest')
|
|
27
|
+
process.exit(result.status === 'failed' ? 1 : 0)
|
|
28
|
+
}).catch(error => { console.error(error.message); process.exit(1) })
|
|
29
|
+
} else {
|
|
30
|
+
launch(runtime)
|
|
31
|
+
startBackgroundUpdate()
|
|
32
|
+
// Keep long-lived sessions aware of releases without holding the process open.
|
|
33
|
+
setInterval(startBackgroundUpdate, 15 * 60 * 1000).unref()
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
child.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
function launch(selected) {
|
|
37
|
+
if (!fs.existsSync(selected.file)) {
|
|
38
|
+
console.error(`[pandanpc-mcp] binary not found at ${selected.file}; reinstall @pandanpc/mcp-server`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
child = spawn(selected.file, process.argv.slice(2), {
|
|
42
|
+
stdio: 'inherit', windowsHide: true,
|
|
43
|
+
env: { ...process.env, PANDANPC_MCP_RUNTIME_VERSION: selected.version },
|
|
44
|
+
})
|
|
45
|
+
child.on('exit', (code, signal) => {
|
|
46
|
+
if (signal) process.kill(process.pid, signal)
|
|
47
|
+
else process.exit(code ?? 0)
|
|
48
|
+
})
|
|
49
|
+
child.on('error', err => {
|
|
50
|
+
if (selected.file !== bundledPath) {
|
|
51
|
+
console.error('[pandanpc-mcp] cached runtime could not start; using bundled version')
|
|
52
|
+
launch({ file: bundledPath, version: pkg.version })
|
|
53
|
+
} else {
|
|
54
|
+
console.error(`[pandanpc-mcp] failed to spawn: ${err.message}`)
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
process.on('SIGINT', () => { if (child) child.kill('SIGINT') })
|
|
60
|
+
process.on('SIGTERM', () => { if (child) child.kill('SIGTERM') })
|
package/install.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* postinstall: download the pandanpc-mcp binary matching the current platform
|
|
4
4
|
*
|
|
5
|
-
* Source: https://
|
|
5
|
+
* Source: https://download.pandanpc.com/mcp-server/v<version>/pandanpc-mcp-<platform>
|
|
6
6
|
* Integrity: verified against SHA256SUMS
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const fs = require('fs')
|
|
10
10
|
const path = require('path')
|
|
11
|
-
const
|
|
11
|
+
const { download, fetchText } = require('./network')
|
|
12
12
|
const crypto = require('crypto')
|
|
13
13
|
const { binaryName } = require('./binary-name')
|
|
14
14
|
|
|
15
15
|
const pkg = require('./package.json')
|
|
16
16
|
const VERSION = pkg.version
|
|
17
|
-
const BASE_URL = `https://
|
|
17
|
+
const BASE_URL = `https://download.pandanpc.com/mcp-server/v${VERSION}`
|
|
18
18
|
|
|
19
19
|
const PLATFORM_MAP = {
|
|
20
20
|
'linux-x64': 'pandanpc-mcp-linux-x64',
|
|
@@ -32,31 +32,6 @@ function detectPlatform() {
|
|
|
32
32
|
return { key, name }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function download(url, dest) {
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
const file = fs.createWriteStream(dest)
|
|
38
|
-
const req = https.get(url, (res) => {
|
|
39
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
40
|
-
file.close()
|
|
41
|
-
fs.unlinkSync(dest)
|
|
42
|
-
return download(res.headers.location, dest).then(resolve, reject)
|
|
43
|
-
}
|
|
44
|
-
if (res.statusCode !== 200) {
|
|
45
|
-
file.close()
|
|
46
|
-
fs.unlinkSync(dest)
|
|
47
|
-
return reject(new Error(`HTTP ${res.statusCode} for ${url}`))
|
|
48
|
-
}
|
|
49
|
-
res.pipe(file)
|
|
50
|
-
file.on('finish', () => file.close(resolve))
|
|
51
|
-
})
|
|
52
|
-
req.on('error', (err) => {
|
|
53
|
-
file.close()
|
|
54
|
-
try { fs.unlinkSync(dest) } catch {}
|
|
55
|
-
reject(err)
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
35
|
function sha256(file) {
|
|
61
36
|
return new Promise((resolve, reject) => {
|
|
62
37
|
const hash = crypto.createHash('sha256')
|
|
@@ -79,20 +54,6 @@ function checksumFor(sumsText, artifactName) {
|
|
|
79
54
|
return checksum
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
async function fetchText(url) {
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
https.get(url, (res) => {
|
|
85
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
86
|
-
return fetchText(res.headers.location).then(resolve, reject)
|
|
87
|
-
}
|
|
88
|
-
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`))
|
|
89
|
-
let body = ''
|
|
90
|
-
res.on('data', (c) => (body += c))
|
|
91
|
-
res.on('end', () => resolve(body))
|
|
92
|
-
}).on('error', reject)
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
57
|
function removeTemporaryFile(file) {
|
|
97
58
|
try { fs.unlinkSync(file) } catch (error) {
|
|
98
59
|
if (error?.code !== 'ENOENT') throw error
|
|
@@ -162,6 +123,7 @@ if (require.main === module) {
|
|
|
162
123
|
}
|
|
163
124
|
|
|
164
125
|
module.exports = {
|
|
126
|
+
detectPlatform,
|
|
165
127
|
checksumFor,
|
|
166
128
|
ensureBinary,
|
|
167
129
|
}
|
package/network.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const https = require('https')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { pipeline } = require('stream')
|
|
4
|
+
|
|
5
|
+
// Bound both idle time and total transfer time; never follow a downgrade to HTTP.
|
|
6
|
+
function transfer(url, destination, limit, timeout, redirects = 0) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const parsed = new URL(url)
|
|
9
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return reject(new Error('HTTPS URL required'))
|
|
10
|
+
let timer, piping = false
|
|
11
|
+
const finish = (error, value) => { clearTimeout(timer); error ? reject(error) : resolve(value) }
|
|
12
|
+
const req = https.get(parsed, (res) => {
|
|
13
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
14
|
+
res.destroy()
|
|
15
|
+
clearTimeout(timer)
|
|
16
|
+
if (redirects >= 3) return reject(new Error('Too many redirects'))
|
|
17
|
+
return transfer(new URL(res.headers.location, parsed).href, destination, limit, timeout, redirects + 1).then(resolve, reject)
|
|
18
|
+
}
|
|
19
|
+
if (res.statusCode !== 200) { res.destroy(); return finish(new Error(`HTTP ${res.statusCode}`)) }
|
|
20
|
+
let bytes = 0
|
|
21
|
+
res.on('data', chunk => {
|
|
22
|
+
bytes += chunk.length
|
|
23
|
+
if (bytes > limit) req.destroy(new Error('Response too large'))
|
|
24
|
+
})
|
|
25
|
+
if (destination) {
|
|
26
|
+
piping = true
|
|
27
|
+
pipeline(res, fs.createWriteStream(destination, { flags: 'wx', mode: 0o600 }), error => finish(error || (bytes > limit || !res.complete ? new Error('Incomplete or oversized response') : null)))
|
|
28
|
+
} else {
|
|
29
|
+
const chunks = []
|
|
30
|
+
res.on('data', chunk => chunks.push(chunk))
|
|
31
|
+
res.on('error', error => finish(error))
|
|
32
|
+
res.on('aborted', () => finish(new Error('Response interrupted')))
|
|
33
|
+
res.on('end', () => finish(bytes > limit || !res.complete ? new Error('Incomplete or oversized response') : null, Buffer.concat(chunks).toString('utf8')))
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
req.on('error', error => { if (!piping) finish(error) })
|
|
37
|
+
req.setTimeout(15000, () => req.destroy(new Error('Network idle timeout')))
|
|
38
|
+
timer = setTimeout(() => req.destroy(new Error('Network deadline exceeded')), timeout)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
fetchText: (url, { limit = 1024 * 1024, timeout = 20000 } = {}) => transfer(url, null, limit, timeout),
|
|
44
|
+
download: (url, destination, { limit = 64 * 1024 * 1024, timeout = 90000 } = {}) => transfer(url, destination, limit, timeout),
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pandanpc/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "PandaNpc MCP Server — PandaNote and Agent connection management for Codex, Claude Code, and other MCP clients",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"index.js",
|
|
25
25
|
"install.js",
|
|
26
26
|
"README.md",
|
|
27
|
-
"skills"
|
|
27
|
+
"skills",
|
|
28
|
+
"network.js",
|
|
29
|
+
"updater.js"
|
|
28
30
|
],
|
|
29
31
|
"scripts": {
|
|
30
32
|
"postinstall": "node install.js"
|
|
@@ -47,5 +49,6 @@
|
|
|
47
49
|
"bugs": {
|
|
48
50
|
"url": "https://github.com/pandanpc-inc/mcp-server/issues"
|
|
49
51
|
},
|
|
50
|
-
"mcpName": "com.pandanpc/mcp-server"
|
|
52
|
+
"mcpName": "com.pandanpc/mcp-server",
|
|
53
|
+
"pandanpcUpdaterProtocol": 1
|
|
51
54
|
}
|
package/updater.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const crypto = require('crypto')
|
|
5
|
+
const { spawn, spawnSync } = require('child_process')
|
|
6
|
+
const { binaryName } = require('./binary-name')
|
|
7
|
+
const { ensureBinary, detectPlatform } = require('./install')
|
|
8
|
+
const { fetchText } = require('./network')
|
|
9
|
+
const pkg = require('./package.json')
|
|
10
|
+
|
|
11
|
+
const PROTOCOL = 1
|
|
12
|
+
const INTERVAL = 6 * 60 * 60 * 1000
|
|
13
|
+
const RETRY_INTERVAL = 15 * 60 * 1000
|
|
14
|
+
const REGISTRY = 'https://registry.npmjs.org/@pandanpc%2fmcp-server/latest'
|
|
15
|
+
const quiet = { log() {}, warn() {} }
|
|
16
|
+
|
|
17
|
+
function versionParts(v) {
|
|
18
|
+
if (typeof v !== 'string' || !/^\d+\.\d+\.\d+$/.test(v)) return null
|
|
19
|
+
const parts = v.split('.').map(Number)
|
|
20
|
+
return parts.every(Number.isSafeInteger) ? parts : null
|
|
21
|
+
}
|
|
22
|
+
function compareVersions(a, b) {
|
|
23
|
+
const x = versionParts(a), y = versionParts(b)
|
|
24
|
+
if (!x || !y) throw new Error('Invalid stable version')
|
|
25
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i] ? 1 : -1
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
function cacheRoot() {
|
|
29
|
+
return path.join(process.env.PANDANPC_MCP_CACHE_DIR || path.join(os.homedir(), '.pandanpc-mcp', 'runtime'), `${process.platform}-${process.arch}`)
|
|
30
|
+
}
|
|
31
|
+
function readJson(file) { try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return null } }
|
|
32
|
+
function writeJson(file, value) {
|
|
33
|
+
const temp = `${file}.${crypto.randomUUID()}.tmp`
|
|
34
|
+
try { fs.writeFileSync(temp, JSON.stringify(value), { mode: 0o600 }); fs.renameSync(temp, file) }
|
|
35
|
+
finally { try { fs.unlinkSync(temp) } catch {} }
|
|
36
|
+
}
|
|
37
|
+
function activeRuntime(root, bundledVersion = pkg.version, platform = process.platform) {
|
|
38
|
+
try {
|
|
39
|
+
const active = readJson(path.join(root, 'active.json'))
|
|
40
|
+
if (!active || !versionParts(active.version) || compareVersions(active.version, bundledVersion) <= 0) return null
|
|
41
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(active.directory) || !active.directory.startsWith(active.version + '-')) return null
|
|
42
|
+
if (!/^[a-f0-9]{64}$/.test(active.checksum)) return null
|
|
43
|
+
const file = path.join(root, active.directory, binaryName(platform, active.version))
|
|
44
|
+
const stat = fs.lstatSync(file)
|
|
45
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > 64 * 1024 * 1024) return null
|
|
46
|
+
if (crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') !== active.checksum) return null
|
|
47
|
+
if (platform !== 'win32') fs.accessSync(file, fs.constants.X_OK)
|
|
48
|
+
return { file, version: active.version }
|
|
49
|
+
} catch { return null }
|
|
50
|
+
}
|
|
51
|
+
function selectRuntime({ root = cacheRoot(), bundledVersion = pkg.version, platform = process.platform, disabled = process.env.PANDANPC_MCP_AUTO_UPDATE === '0' } = {}) {
|
|
52
|
+
const bundled = { file: path.join(__dirname, 'bin', binaryName(platform, bundledVersion)), version: bundledVersion }
|
|
53
|
+
return (!disabled && activeRuntime(root, bundledVersion, platform)) || bundled
|
|
54
|
+
}
|
|
55
|
+
function due(root, now = Date.now()) {
|
|
56
|
+
const state = readJson(path.join(root, 'check.json'))
|
|
57
|
+
return !state || !Number.isFinite(state.nextCheck) || state.nextCheck <= now || state.nextCheck > now + INTERVAL
|
|
58
|
+
}
|
|
59
|
+
function acquireLock(root, now) {
|
|
60
|
+
const file = path.join(root, 'update.lock')
|
|
61
|
+
const nonce = crypto.randomUUID()
|
|
62
|
+
try {
|
|
63
|
+
const fd = fs.openSync(file, 'wx', 0o600)
|
|
64
|
+
try { fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, nonce })) } finally { fs.closeSync(fd) }
|
|
65
|
+
return () => { if (readJson(file)?.nonce === nonce) { try { fs.unlinkSync(file) } catch {} } }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code !== 'EEXIST') throw error
|
|
68
|
+
// Reap only a dead owner, never an updater that is still running.
|
|
69
|
+
const first = fs.statSync(file)
|
|
70
|
+
const owner = readJson(file)
|
|
71
|
+
if (now - first.mtimeMs < 5 * 60 * 1000) return null
|
|
72
|
+
if (owner && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
73
|
+
try { process.kill(owner.pid, 0); return null } catch (e) { if (e.code !== 'ESRCH') return null }
|
|
74
|
+
}
|
|
75
|
+
// A concurrent reaper must not unlink a replacement lock.
|
|
76
|
+
const again = fs.statSync(file)
|
|
77
|
+
if (first.ino !== again.ino || first.mtimeMs !== again.mtimeMs) return null
|
|
78
|
+
fs.unlinkSync(file)
|
|
79
|
+
return null // next launch retries; don't race the replacement owner
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function probeBinary(file) {
|
|
83
|
+
const r = spawnSync(file, ['--help'], { timeout: 5000, killSignal: 'SIGKILL', windowsHide: true, encoding: 'utf8', maxBuffer: 1024 * 1024 })
|
|
84
|
+
if (r.error || r.status !== 0) throw new Error('Downloaded runtime failed its startup check')
|
|
85
|
+
}
|
|
86
|
+
async function checkForUpdates(options = {}) {
|
|
87
|
+
const root = options.root || cacheRoot()
|
|
88
|
+
const bundledVersion = options.bundledVersion || pkg.version
|
|
89
|
+
const platform = options.platform || process.platform
|
|
90
|
+
const platformInfo = options.platformInfo || detectPlatform()
|
|
91
|
+
const fetchTextFn = options.fetchTextFn || fetchText
|
|
92
|
+
const now = (options.now || Date.now)()
|
|
93
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 })
|
|
94
|
+
if (!options.force && !due(root, now)) return { status: 'throttled' }
|
|
95
|
+
const unlock = acquireLock(root, now)
|
|
96
|
+
if (!unlock) return { status: 'busy' }
|
|
97
|
+
let directory
|
|
98
|
+
try {
|
|
99
|
+
if (!options.force && !due(root, now)) return { status: 'throttled' }
|
|
100
|
+
writeJson(path.join(root, 'check.json'), { nextCheck: now + RETRY_INTERVAL })
|
|
101
|
+
const metadata = JSON.parse(await fetchTextFn(REGISTRY))
|
|
102
|
+
if (!versionParts(metadata.version)) throw new Error('Invalid release version')
|
|
103
|
+
const current = activeRuntime(root, bundledVersion, platform)
|
|
104
|
+
const currentVersion = current?.version || bundledVersion
|
|
105
|
+
if (compareVersions(metadata.version, currentVersion) <= 0) {
|
|
106
|
+
writeJson(path.join(root, 'check.json'), { nextCheck: now + INTERVAL })
|
|
107
|
+
return { status: 'current', version: currentVersion }
|
|
108
|
+
}
|
|
109
|
+
if (metadata.pandanpcUpdaterProtocol !== PROTOCOL) {
|
|
110
|
+
writeJson(path.join(root, 'check.json'), { nextCheck: now + INTERVAL, manualUpdateRequired: metadata.version })
|
|
111
|
+
return { status: 'manual-update-required', version: metadata.version }
|
|
112
|
+
}
|
|
113
|
+
const version = metadata.version
|
|
114
|
+
const name = `${version}-${crypto.randomUUID()}`
|
|
115
|
+
directory = path.join(root, name)
|
|
116
|
+
fs.mkdirSync(directory, { mode: 0o700 })
|
|
117
|
+
const file = path.join(directory, binaryName(platform, version))
|
|
118
|
+
const result = await ensureBinary({ key: platformInfo.key, artifactName: platformInfo.name, localBin: file, platform,
|
|
119
|
+
baseUrl: `https://cos.pandanpc.com/mcp-server/v${version}`, fetchTextFn,
|
|
120
|
+
...(options.downloadFn ? { downloadFn: options.downloadFn } : {}), logger: quiet })
|
|
121
|
+
await (options.probeFn || probeBinary)(file)
|
|
122
|
+
const newer = activeRuntime(root, bundledVersion, platform)
|
|
123
|
+
if (newer && compareVersions(newer.version, version) >= 0) return { status: 'current', version: newer.version }
|
|
124
|
+
writeJson(path.join(root, 'active.json'), { version, directory: name, checksum: result.checksum })
|
|
125
|
+
directory = null // published versions may be in use; never delete them automatically
|
|
126
|
+
writeJson(path.join(root, 'check.json'), { nextCheck: now + INTERVAL })
|
|
127
|
+
return { status: 'ready', version }
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return { status: 'failed', error: error.message }
|
|
130
|
+
} finally {
|
|
131
|
+
if (directory) fs.rmSync(directory, { recursive: true, force: true })
|
|
132
|
+
unlock()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function startBackgroundUpdate() {
|
|
136
|
+
if (process.env.PANDANPC_MCP_AUTO_UPDATE === '0') return
|
|
137
|
+
try {
|
|
138
|
+
if (!due(cacheRoot())) return
|
|
139
|
+
const child = spawn(process.execPath, [__filename], { detached: true, windowsHide: true, stdio: 'ignore' })
|
|
140
|
+
child.on('error', () => {})
|
|
141
|
+
child.unref()
|
|
142
|
+
} catch {} // updates must never prevent an MCP session from starting
|
|
143
|
+
}
|
|
144
|
+
if (require.main === module) checkForUpdates().catch(() => {})
|
|
145
|
+
module.exports = { compareVersions, selectRuntime, checkForUpdates, startBackgroundUpdate }
|