beacon-mfg-mcp 1.5.7 → 1.5.8

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/RELEASE.md CHANGED
@@ -39,3 +39,7 @@ python http_server.py # Streamable HTTP,端点 /mcp
39
39
  ```
40
40
 
41
41
  数据默认取自公开数据源,无需密钥、无需克隆仓库。
42
+
43
+ ## 本次更新
44
+
45
+ - 稳定性与接入体验改进。
package/bin/selfupdate.js CHANGED
@@ -1,150 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // beacon-mfg-mcp 自更新检查(独立进程,由 launcher 以 detached 方式拉起)
3
+ // beacon-mfg-mcp 自更新:**薄壳**,只负责找到 Python 并把 selfupdate.py 拉起来。
4
4
  //
5
- // 【为什么必须独立成进程】
6
- // launcher 用 spawnSync 同步拉起 server.py —— 那会**阻塞 Node 整个事件循环**。
7
- // 如果把自更新写成 async 函数在主进程里跑,它在 spawnSync 期间推不动,
8
- // 等 server 退出、事件循环恢复时,主进程紧跟着就 process.exit() 了,
9
- // 于是更新逻辑永远跑不完。
10
- // 独立 detached 子进程不受主进程阻塞与退出影响,是唯一稳的做法。
5
+ // 为什么改成薄壳:自更新逻辑的**唯一事实来源**是 mcp/selfupdate.py。
6
+ // 原先这里有一份完整的 JS 实现,而 server.py 直连的用户根本走不到它 —— 于是
7
+ // 「只有 npm 启动器会检查更新」成了盲区,且两份实现容易漂移。
8
+ // 现在:server.py 启动时会自行拉起 selfupdate.py(任何接入方式都覆盖),
9
+ // 本文件仅为兼容旧调用路径(launcher / 手动执行)保留。
11
10
  //
12
- // 【三条硬约束】
13
- // 1. 绝不阻塞 MCP 启动:本进程是 detached + unref 的,主进程不等它。
14
- // 2. 失败一律静默降级:离线 / 内网 / 没装 npm / 无写权限,都只当没更新过。
15
- // 3. 节流 + 可关:默认 24h 查一次;BEACON_MCP_NO_UPDATE=1 完全关闭。
11
+ // 用法:node bin/selfupdate.js [--check|--print-mode|--force]
16
12
 
17
- const { spawnSync, spawn } = require('child_process');
18
- const fs = require('fs');
19
- const os = require('os');
13
+ const { spawnSync } = require('child_process');
20
14
  const path = require('path');
21
- const https = require('https');
22
-
23
- const PKG = require('../package.json');
24
- const CURRENT = PKG.version || '0.0.0';
25
-
26
- const STATE_DIR = path.join(os.homedir() || '.', '.beacon-mfg');
27
- const STAMP = path.join(STATE_DIR, 'update-check.json');
28
- const LOCK = path.join(STATE_DIR, 'update.lock');
29
- const REGISTRY = 'https://registry.npmjs.org/beacon-mfg-mcp/latest';
30
- const INTERVAL_H = Number(process.env.BEACON_MCP_UPDATE_INTERVAL_H || 24);
31
-
32
- function note(msg) {
33
- // 只能写 stderr:stdout 可能是 MCP 协议通道(在独立进程里虽然安全,
34
- // 但保持一致,将来若被复用也不会误伤)。
35
- try { process.stderr.write('[beacon-mfg-mcp] ' + msg + '\n'); } catch (_) {}
36
- }
37
-
38
- function semverGt(a, b) {
39
- const pa = String(a).split('.').map((x) => parseInt(x, 10) || 0);
40
- const pb = String(b).split('.').map((x) => parseInt(x, 10) || 0);
41
- for (let i = 0; i < 3; i++) {
42
- if ((pa[i] || 0) > (pb[i] || 0)) return true;
43
- if ((pa[i] || 0) < (pb[i] || 0)) return false;
44
- }
45
- return false;
46
- }
47
-
48
- function shouldSkip() {
49
- if (process.env.BEACON_MCP_NO_UPDATE === '1') return true;
50
- // 离线仓库用户(数据来自本地 git/目录),联网升级只会添乱
51
- if (process.env.BEACON_SOURCE || process.env.BEACON_REPO) return true;
52
- return false;
53
- }
54
-
55
- function readStamp() {
56
- try { return JSON.parse(fs.readFileSync(STAMP, 'utf8')).ts || 0; } catch (_) { return 0; }
57
- }
58
- function writeStamp(extra) {
59
- try {
60
- fs.mkdirSync(STATE_DIR, { recursive: true });
61
- fs.writeFileSync(STAMP, JSON.stringify(
62
- Object.assign({ ts: Date.now(), version: CURRENT }, extra || {})));
63
- } catch (_) {}
64
- }
65
- // 锁的过期时间:超过就视为持有者已死,直接接管。
66
- // 为什么必须有:detached 子进程有可能被中途杀掉(launcher 退出、Windows job object),
67
- // 停在「已加锁、未解锁」之间 —— 残留锁会让**此后所有启动**
68
- // 永久跳过自更新,而且现场毫无线索。所以锁必须能自愈。
69
- const LOCK_STALE_MS = Number(process.env.BEACON_MCP_UPDATE_LOCK_STALE_MIN || 10) * 60000;
70
-
71
- function lockHeld() {
72
- // flag 'wx' = 原子创建,文件已存在会抛错。用 writeFileSync 覆盖写是**不互斥**的
73
- // (两个进程都会「拿到锁」然后互相 unlink),并发保护就成了摆设。
74
- const grab = () => {
75
- const fd = fs.openSync(LOCK, 'wx');
76
- fs.writeSync(fd, String(process.pid) + ' ' + Date.now());
77
- fs.closeSync(fd);
78
- };
79
- try {
80
- fs.mkdirSync(STATE_DIR, { recursive: true });
81
- grab();
82
- return true;
83
- } catch (_) {
84
- try {
85
- const st = fs.statSync(LOCK);
86
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
87
- fs.unlinkSync(LOCK); // 持有者已死,回收
88
- grab();
89
- return true;
90
- }
91
- } catch (_) { /* 回收也失败:只当没拿到锁 */ }
92
- return false;
93
- }
94
- }
95
- function unlock() {
96
- try { fs.unlinkSync(LOCK); } catch (_) {}
97
- }
15
+ const fs = require('fs');
98
16
 
99
- function fetchLatest(timeoutMs) {
100
- return new Promise((resolve) => {
101
- let done = false;
102
- const finish = (v) => { if (!done) { done = true; resolve(v); } };
103
- let req;
104
- try {
105
- req = https.get(REGISTRY, { headers: { Accept: 'application/json' } },
106
- (r) => {
107
- if (r.statusCode !== 200) { r.resume(); return finish(null); }
108
- let b = '';
109
- r.setEncoding('utf8');
110
- r.on('data', (c) => { b += c; if (b.length > 20000) r.destroy(); });
111
- r.on('end', () => {
112
- try { finish(JSON.parse(b).version || null); } catch (_) { finish(null); }
113
- });
114
- });
115
- } catch (_) { return finish(null); }
116
- req.setTimeout(timeoutMs, () => { try { req.destroy(); } catch (_) {} finish(null); });
117
- req.on('error', () => finish(null));
118
- });
17
+ const script = path.join(__dirname, '..', 'selfupdate.py');
18
+ if (!fs.existsSync(script)) {
19
+ // 没有 Python 侧实现就静默退出:自更新失败绝不影响 MCP 服务
20
+ process.exit(0);
119
21
  }
120
22
 
121
- const IS_WIN = process.platform === 'win32';
122
- // Windows 上 npm 是 npm.cmd 批处理,必须 shell:true,否则 FileNotFoundError。
123
- const NPM = IS_WIN ? 'npm.cmd' : 'npm';
124
- const NPM_OPTS = { stdio: 'ignore', shell: IS_WIN, env: process.env };
125
-
126
- async function main() {
127
- if (shouldSkip()) return;
128
- if (Date.now() - readStamp() < INTERVAL_H * 3600 * 1000) return; // 节流
129
- if (!lockHeld()) return; // 并发保护
130
-
131
- const latest = await fetchLatest(5000);
132
- if (!latest) { writeStamp({ result: 'registry_unreachable' }); unlock(); return; }
133
- writeStamp({ result: 'checked', latest });
134
- if (!semverGt(latest, CURRENT)) { unlock(); return; } // 已最新
135
-
136
- note('发现新版 ' + latest + '(当前 ' + CURRENT + '),正在更新…');
137
- // 同步等安装结果:detached 进程本身就是后台的,这里等一会儿无副作用,
138
- // 换来的是能把「成功/失败」明确告诉用户。
139
- const r = spawnSync(NPM, ['i', '-g', 'beacon-mfg-mcp@' + latest],
140
- Object.assign({ timeout: 180000 }, NPM_OPTS));
141
- unlock();
142
- if (r.status === 0) {
143
- note('已更新 ' + CURRENT + ' → ' + latest + ',下次启动生效。');
144
- } else {
145
- note('自动更新未成功(可能缺权限或网络受限)。'
146
- + '可手动执行:npm i -g beacon-mfg-mcp@latest');
147
- }
23
+ const candidates = ['python3', 'python', 'py'];
24
+ let chosen = null;
25
+ for (const c of candidates) {
26
+ const r = spawnSync(c, ['--version'], { stdio: 'ignore' });
27
+ if (r.status === 0) { chosen = c; break; }
148
28
  }
29
+ if (!chosen) process.exit(0);
149
30
 
150
- main().catch(() => { unlock(); process.exit(0); });
31
+ const child = spawnSync(chosen, [script, ...process.argv.slice(2)], { stdio: 'inherit' });
32
+ process.exit(child.status === null ? 0 : child.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beacon-mfg-mcp",
3
- "version": "1.5.7",
3
+ "version": "1.5.8",
4
4
  "description": "Read-only MCP server exposing the BeaconMFG灯塔工厂 supplier directory (search vendors, get vendor detail, get capability card, multi-turn sourcing) to any MCP-compatible agent. No API key required.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -41,3 +41,4 @@
41
41
  },
42
42
  "dependencies": {}
43
43
  }
44
+
package/server.py CHANGED
@@ -3213,8 +3213,41 @@ def _log(msg: str) -> None:
3213
3213
  sys.stderr.flush()
3214
3214
 
3215
3215
 
3216
+ def _maybe_spawn_selfupdate() -> None:
3217
+ """启动时踢一次自更新检查(detached、不阻塞、失败一律静默)。
3218
+
3219
+ 修掉的盲区:自更新原先只挂在 npm 启动器(bin/beacon-mfg-mcp.js)上,
3220
+ 凡是以 `python server.py` 直接接入的用户(包括本地仓库用户)永远不会
3221
+ 收到“有新版 / 仓库落后”的提示。这里让**任何接入方式**都会检查一次,
3222
+ 并把结果写进 ~/.beacon-mfg/update-check.json、把提示写到 stderr
3223
+ (MCP 客户端会把它记进日志,因此用户可见)。
3224
+ """
3225
+ try:
3226
+ if os.environ.get("BEACON_MCP_NO_UPDATE") == "1":
3227
+ return
3228
+ script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "selfupdate.py")
3229
+ if not os.path.isfile(script):
3230
+ return
3231
+ kwargs: Dict[str, Any] = {}
3232
+ if os.name == "nt":
3233
+ # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP:不随本进程退出而结束
3234
+ kwargs["creationflags"] = 0x00000008 | 0x00000200
3235
+ else:
3236
+ kwargs["start_new_session"] = True
3237
+ subprocess.Popen(
3238
+ [sys.executable, script],
3239
+ stdin=subprocess.DEVNULL,
3240
+ stdout=subprocess.DEVNULL,
3241
+ stderr=None, # 继承:提示进入客户端日志
3242
+ **kwargs,
3243
+ )
3244
+ except Exception:
3245
+ pass
3246
+
3247
+
3216
3248
  def main() -> None:
3217
3249
  _log(f"start; source={BEACON_SOURCE} repo={REPO or '(http)'}")
3250
+ _maybe_spawn_selfupdate()
3218
3251
  for raw in sys.stdin:
3219
3252
  raw = raw.strip()
3220
3253
  if not raw: