beacon-mfg-mcp 1.4.1 → 1.4.2
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 +19 -3
- package/bin/beacon-mfg-mcp.js +20 -2
- package/bin/selfupdate.js +150 -0
- package/package.json +44 -44
- package/server.py +2 -2
package/RELEASE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Beacon-MFG 只读 MCP 服务 · v1.
|
|
1
|
+
# Beacon-MFG 只读 MCP 服务 · v1.4.2
|
|
2
2
|
|
|
3
3
|
让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
|
|
4
4
|
**检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
|
|
@@ -21,7 +21,23 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
|
|
|
21
21
|
后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
|
|
22
22
|
依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
|
|
23
23
|
|
|
24
|
-
## 本次更新(v1.
|
|
24
|
+
## 本次更新(v1.4.2)
|
|
25
|
+
- **自动升级(自更新)**:新增 `bin/selfupdate.js`,MCP 启动时以**独立 detached 子进程**拉起(绝不阻塞 MCP 启动),
|
|
26
|
+
节流查 npm registry `latest`(默认 24h 一次,可用 `BEACON_MCP_UPDATE_INTERVAL_H` 调),发现新版本即执行
|
|
27
|
+
`npm i -g beacon-mfg-mcp@<新版本>`(Windows 上正确走 `npm.cmd + shell`)。
|
|
28
|
+
- **跳过条件**:设 `BEACON_MCP_NO_UPDATE=1`、或已设 `BEACON_SOURCE` / `BEACON_REPO`(离线 / 本地仓库用户)时**不联网升级**,避免对本地仓库用户添乱。
|
|
29
|
+
- **并发保护**:原子目录锁 + 残留锁 10 分钟过期自愈(踩过死锁坑——残留锁会让之后所有启动永久跳过自更新)。
|
|
30
|
+
- **测试**:`test_update.js` 单元测试 23/23 通过,并以 fake-npm 拦截验证安装命令形态正确(`npm.cmd i -g beacon-mfg-mcp@1.4.2`)。
|
|
31
|
+
- **前向生效说明**:自更新是「向前生效」的——已装的 ≤1.4.1 旧版代码里没有 `selfupdate.js`,需**一次性手动** `npm i -g beacon-mfg-mcp@latest` 进入自动升级基线;之后永久自动。`npx beacon-mfg-mcp` 用户天然永远最新。
|
|
32
|
+
|
|
33
|
+
## 本次更新(v1.4.1,已发布)
|
|
34
|
+
- **直连优先 + 死代理容错**:修复「失效代理导致检索归零」——默认源不可达时直连兜底镜像,且报错信息直指真因(是代理挂了而非数据问题),`npm i -g` 装一次即享受(无需重启客户端)。
|
|
35
|
+
|
|
36
|
+
## 本次更新(v1.4.0,已发布)
|
|
37
|
+
- **标准分帧适配层 `mcp_adapter.py`**:补齐「严格分帧」客户端接入路径,与「换行 stdio」「Streamable HTTP」三条传输链路统一。
|
|
38
|
+
- **协议回归测试 `test_protocol.py`**:一条命令覆盖三条链路(握手 + 6 工具齐全 + 三城市真实检索 `via_index`),发版前必跑。
|
|
39
|
+
|
|
40
|
+
## 本次更新(v1.3.3,已发布)
|
|
25
41
|
- **本地检索免提交、免重启即时可见**:这是 v1.3.1「worktree 兜底」的收尾。v1.3.1 只让**单文件读**走了工作树,
|
|
26
42
|
但关键词/城市检索的**召回**走的是**批量读** `_read_many_text()`——它在 git 模式下恒走 `git archive HEAD`
|
|
27
43
|
(已提交快照),于是**改完数据、重建索引后,关键词检索仍读旧索引、必须 commit 才可见**。
|
|
@@ -54,7 +70,7 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
|
|
|
54
70
|
## 安装(三选一)
|
|
55
71
|
|
|
56
72
|
### 1. 从 GitHub Release 下载(推荐,零依赖)
|
|
57
|
-
下载本 Release 的 `beacon-mfg-mcp-mcp-v1.
|
|
73
|
+
下载本 Release 的 `beacon-mfg-mcp-mcp-v1.4.2.tar.gz`,解压后:
|
|
58
74
|
```jsonc
|
|
59
75
|
{
|
|
60
76
|
"mcpServers": {
|
package/bin/beacon-mfg-mcp.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// beacon-mfg-mcp 启动器:本 MCP 服务是纯 Python 标准库实现,这里只负责
|
|
4
|
-
// 在用户机器上找到 Python 解释器并拉起 server.py(stdio
|
|
5
|
-
|
|
4
|
+
// ① 在用户机器上找到 Python 解释器并拉起 server.py(stdio 协议)
|
|
5
|
+
// ② 顺手踢一个**独立进程**去做节流的自更新检查
|
|
6
|
+
const { spawnSync, spawn } = require('child_process');
|
|
6
7
|
const path = require('path');
|
|
7
8
|
|
|
8
9
|
const serverPy = path.join(__dirname, '..', 'server.py');
|
|
10
|
+
const selfUpdateJs = path.join(__dirname, 'selfupdate.js');
|
|
11
|
+
|
|
12
|
+
// 踢起自更新检查:detached + unref,主进程既不等它也不被它拖住。
|
|
13
|
+
// 为什么必须是独立进程而不是本进程里的 async 函数:
|
|
14
|
+
// 下面 spawnSync 是**同步阻塞**的,会冻结整个事件循环 —— async 的自更新
|
|
15
|
+
// 在 server 运行期间推不动,等它能跑时本进程已经 process.exit() 了,
|
|
16
|
+
// 更新逻辑永远跑不完(实测 stamp 根本不写)。独立进程不受此影响。
|
|
17
|
+
// 失败、离线、没权限一律静默:它只影响「下次启动是不是最新版」。
|
|
18
|
+
try {
|
|
19
|
+
const u = spawn(process.execPath, [selfUpdateJs], {
|
|
20
|
+
stdio: 'ignore',
|
|
21
|
+
detached: true,
|
|
22
|
+
windowsHide: true,
|
|
23
|
+
});
|
|
24
|
+
u.on('error', () => { /* 没有 node?不可能,自身就是 node 起的 */ });
|
|
25
|
+
u.unref();
|
|
26
|
+
} catch (_) { /* 自更新是加分项,绝不能影响 MCP 启动 */ }
|
|
9
27
|
|
|
10
28
|
// 探测可用的 Python 解释器(优先 python3,兼容 python / py)
|
|
11
29
|
const candidates = ['python3', 'python', 'py'];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// beacon-mfg-mcp 自更新检查(独立进程,由 launcher 以 detached 方式拉起)
|
|
4
|
+
//
|
|
5
|
+
// 【为什么必须独立成进程】
|
|
6
|
+
// launcher 用 spawnSync 同步拉起 server.py —— 那会**阻塞 Node 整个事件循环**。
|
|
7
|
+
// 如果把自更新写成 async 函数在主进程里跑,它在 spawnSync 期间推不动,
|
|
8
|
+
// 等 server 退出、事件循环恢复时,主进程紧跟着就 process.exit() 了,
|
|
9
|
+
// 于是更新逻辑永远跑不完(实测:stamp 根本没写)。
|
|
10
|
+
// 独立 detached 子进程不受主进程阻塞与退出影响,是唯一稳的做法。
|
|
11
|
+
//
|
|
12
|
+
// 【三条硬约束】
|
|
13
|
+
// 1. 绝不阻塞 MCP 启动:本进程是 detached + unref 的,主进程不等它。
|
|
14
|
+
// 2. 失败一律静默降级:离线 / 内网 / 没装 npm / 无写权限,都只当没更新过。
|
|
15
|
+
// 3. 节流 + 可关:默认 24h 查一次;BEACON_MCP_NO_UPDATE=1 完全关闭。
|
|
16
|
+
|
|
17
|
+
const { spawnSync, spawn } = require('child_process');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
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
|
+
}
|
|
98
|
+
|
|
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
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
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
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch(() => { unlock(); process.exit(0); });
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "beacon-mfg-mcp",
|
|
3
|
-
"version": "1.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
|
-
"keywords": [
|
|
6
|
-
"mcp",
|
|
7
|
-
"model-context-protocol",
|
|
8
|
-
"beacon-mfg",
|
|
9
|
-
"suppliers",
|
|
10
|
-
"manufacturing",
|
|
11
|
-
"china",
|
|
12
|
-
"sourcing"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/eiry16/beacon-mfg#readme",
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/eiry16/beacon-mfg.git"
|
|
18
|
-
},
|
|
19
|
-
"bugs": {
|
|
20
|
-
"url": "https://github.com/eiry16/beacon-mfg/issues"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"bin": {
|
|
24
|
-
"beacon-mfg-mcp": "bin/beacon-mfg-mcp.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"server.py",
|
|
28
|
-
"http_server.py",
|
|
29
|
-
"mcp_adapter.py",
|
|
30
|
-
"test_protocol.py",
|
|
31
|
-
"bin/",
|
|
32
|
-
"rfq-kernel/",
|
|
33
|
-
"copy-rfk.js",
|
|
34
|
-
"README.md",
|
|
35
|
-
"RELEASE.md"
|
|
36
|
-
],
|
|
37
|
-
"scripts": {
|
|
38
|
-
"prepack": "node copy-rfk.js"
|
|
39
|
-
},
|
|
40
|
-
"engines": {
|
|
41
|
-
"node": ">=16"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {}
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "beacon-mfg-mcp",
|
|
3
|
+
"version": "1.4.2",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"beacon-mfg",
|
|
9
|
+
"suppliers",
|
|
10
|
+
"manufacturing",
|
|
11
|
+
"china",
|
|
12
|
+
"sourcing"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/eiry16/beacon-mfg#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/eiry16/beacon-mfg.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/eiry16/beacon-mfg/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bin": {
|
|
24
|
+
"beacon-mfg-mcp": "bin/beacon-mfg-mcp.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"server.py",
|
|
28
|
+
"http_server.py",
|
|
29
|
+
"mcp_adapter.py",
|
|
30
|
+
"test_protocol.py",
|
|
31
|
+
"bin/",
|
|
32
|
+
"rfq-kernel/",
|
|
33
|
+
"copy-rfk.js",
|
|
34
|
+
"README.md",
|
|
35
|
+
"RELEASE.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"prepack": "node copy-rfk.js"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=16"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {}
|
|
44
|
+
}
|
package/server.py
CHANGED
|
@@ -271,9 +271,9 @@ def _pkg_version() -> str:
|
|
|
271
271
|
try:
|
|
272
272
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "package.json")
|
|
273
273
|
with open(p, "r", encoding="utf-8") as f:
|
|
274
|
-
return json.load(f).get("version", "1.
|
|
274
|
+
return json.load(f).get("version", "1.4.2")
|
|
275
275
|
except Exception:
|
|
276
|
-
return "1.
|
|
276
|
+
return "1.4.2"
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
# HTTP 基址列表(含兜底镜像)。默认部署 Cloudflare Pages 不可达时,自动回退到
|