beacon-mfg-mcp 1.4.2 → 1.5.1

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
@@ -1,4 +1,4 @@
1
- # Beacon-MFG 只读 MCP 服务 · v1.4.2
1
+ # Beacon-MFG 只读 MCP 服务 · v1.5.1
2
2
 
3
3
  让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
4
4
  **检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
@@ -21,7 +21,54 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
21
21
  后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
22
22
  依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
23
23
 
24
- ## 本次更新(v1.4.2)
24
+ ## 本次更新(v1.5.1)
25
+ **修「刚导入的企业在 MCP 上查不到」——idmap 新号守卫。**
26
+
27
+ v1.5.0 的 idmap 有个致命假设:片存在但片里没这条 id ⇒ 判「不存在」(这样才能省掉
28
+ 18.4 MB 的 name-index 兜底)。但**抓取流水线与政府信源导入每天都在发新号**,
29
+ 而 idmap 只在 manifest 步重算、只在 pages 步上云 —— 于是「刚导入、还没发布」的新 id
30
+ 天然不在已发布快照里,会被这条判定**静默误杀**(政府名录一次能进上千家,症状是查不到、不报错)。
31
+
32
+ - `idmap/meta.json` 与 `manifest.metadata.idmap` 新增 **`max_id`**(已发布的最大 id 数字)。
33
+ 客户端据此区分:`id <= max_id` 且片里没有 ⇒ 真不存在(照样省掉兜底);
34
+ `id > max_id` ⇒ 尚未发布的新号,老老实实走兜底。**正确性优先于那点延迟。**
35
+ - 两个守卫的**顺序不能反**:`max_id` 比 `buckets` 精确(后者只到千位)。
36
+ 实测踩过:buckets=183 / max_id=182302 时,id=0187302 的 bucket=187 已越界,
37
+ 但它只是「比最大号大 5000」的新号 —— 先走 buckets 守卫就被误杀了。
38
+ - 生成器侧加了**条数对账**(`n_rec + other != len(rows)` 直接抛错)——
39
+ 少一条就是一个 id 永远查不到,这种丢失是静默的,必须在生成时拦住。
40
+ - 顺带加了 idmap 残留片清理(dedup 真删实体 id 时,旧的高位片会被 manifest 哈希并上传,
41
+ 白占流量与仓库体积)。
42
+
43
+ ## 本次更新(v1.5.0,已发布)
44
+ **`get_vendor` 从 4.5 分钟降到 6 秒(45×)。** 起因是外部实测反馈「HTTP 模式比本地 git 模式慢约 100 倍,
45
+ 按 id 取一家企业要 270 秒」。定位结论:慢不在计算,而在**为了找一个 id 而下载整张 18.4 MB 的
46
+ `name-index.jsonl`**——id 只是其中一行。
47
+
48
+ - **idmap 定址分片**:新增 `skills/registry/index/idmap/`,`id → 国标码` 按 id 千位切片(183 片 × 1000 id,
49
+ 命中那片约 20 KB)。片名由 id 直接算出(`CN-MFG-0093680` → `93680 // 1000` → `093.jsonl`),
50
+ **不需要目录表、不需要额外请求**。
51
+ - **零 404 的开关设计**:idmap 的元信息放在 `manifest.metadata.idmap` 里——清单本来就要拉,所以是零额外请求。
52
+ 清单里**没有**这个块 = 该快照尚未发布 idmap → 整体走老路径。这样「MCP 先升级、数据后发布」的窗口期
53
+ 不会变成满屏 404(实测过:一次 404 的冷启动兜底要 44 秒)。
54
+ - **按内容哈希缓存分片**:清单里每片都登记了内容 sha1,用它当缓存键——内容不变则**完全不走网络**,
55
+ 取到后顺手校验,不符就不写缓存。
56
+ - **zh 详情分片改并发取**;**HTTP 超时 15→30 s**;**显式声明 gzip 并手动解压**
57
+ (urllib 不会自动解压,JSON 压缩 6~20×,此前一直是裸传)。
58
+ - **实测**(`scripts/bench_get_vendor.py`,缓存目录强制隔离):
59
+
60
+ | 场景 | 冷启动 | 复用缓存 |
61
+ |------|--------|----------|
62
+ | 线上 CDN(本次前) | 270.6 s | — |
63
+ | 线上 CDN(本次后) | **5.95 s** | **1.22 s** |
64
+ | 本地快照 | 1.77 s | 0.80 s |
65
+
66
+ - **恢复 `mcp/test_protocol.py`**:上一个提交误删了发布前必跑的协议回归测试,本次还原(313 行)。
67
+ 发布前实测三条链路 **16/16 通过**(HTTP / stdio / 标准分帧适配层 + 死代理容错)。
68
+ - 数据侧(`skills/registry/index/idmap/**` 与含 `metadata.idmap` 的 `data/manifest.json`)已同步发布到 Pages,
69
+ 旧版 MCP 不受影响(读不到 idmap 块就走老路径)。
70
+
71
+ ## 本次更新(v1.4.2,已发布)
25
72
  - **自动升级(自更新)**:新增 `bin/selfupdate.js`,MCP 启动时以**独立 detached 子进程**拉起(绝不阻塞 MCP 启动),
26
73
  节流查 npm registry `latest`(默认 24h 一次,可用 `BEACON_MCP_UPDATE_INTERVAL_H` 调),发现新版本即执行
27
74
  `npm i -g beacon-mfg-mcp@<新版本>`(Windows 上正确走 `npm.cmd + shell`)。
@@ -70,7 +117,7 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
70
117
  ## 安装(三选一)
71
118
 
72
119
  ### 1. 从 GitHub Release 下载(推荐,零依赖)
73
- 下载本 Release 的 `beacon-mfg-mcp-mcp-v1.4.2.tar.gz`,解压后:
120
+ 下载本 Release 的 `beacon-mfg-mcp-mcp-v1.5.1.tar.gz`,解压后:
74
121
  ```jsonc
75
122
  {
76
123
  "mcpServers": {
@@ -1,54 +1,54 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // beacon-mfg-mcp 启动器:本 MCP 服务是纯 Python 标准库实现,这里只负责
4
- // ① 在用户机器上找到 Python 解释器并拉起 server.py(stdio 协议)
5
- // ② 顺手踢一个**独立进程**去做节流的自更新检查
6
- const { spawnSync, spawn } = require('child_process');
7
- const path = require('path');
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 启动 */ }
27
-
28
- // 探测可用的 Python 解释器(优先 python3,兼容 python / py)
29
- const candidates = ['python3', 'python', 'py'];
30
- let chosen = null;
31
- for (const c of candidates) {
32
- const r = spawnSync(c, ['--version'], { stdio: 'ignore' });
33
- if (r.status === 0) {
34
- chosen = c;
35
- break;
36
- }
37
- }
38
- if (!chosen) {
39
- process.stderr.write(
40
- '[beacon-mfg-mcp] 未找到 Python 3。请先安装 Python 3.8+ 并加入 PATH,再重试。\n'
41
- );
42
- process.exit(1);
43
- }
44
-
45
- const child = spawnSync(chosen, [serverPy, ...process.argv.slice(2)], {
46
- stdio: 'inherit',
47
- // 显式锁定 UTF-8:中文 Windows 上 python 默认按 GBK 写 stdout,
48
- // MCP 客户端按 UTF-8 解码会乱码。从启动器层面保证子进程用 UTF-8 输出。
49
- env: Object.assign({}, process.env, {
50
- PYTHONUTF8: '1',
51
- PYTHONIOENCODING: 'utf-8',
52
- }),
53
- });
54
- process.exit(child.status === null ? 1 : child.status);
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // beacon-mfg-mcp 启动器:本 MCP 服务是纯 Python 标准库实现,这里只负责
4
+ // ① 在用户机器上找到 Python 解释器并拉起 server.py(stdio 协议)
5
+ // ② 顺手踢一个**独立进程**去做节流的自更新检查
6
+ const { spawnSync, spawn } = require('child_process');
7
+ const path = require('path');
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 启动 */ }
27
+
28
+ // 探测可用的 Python 解释器(优先 python3,兼容 python / py)
29
+ const candidates = ['python3', 'python', 'py'];
30
+ let chosen = null;
31
+ for (const c of candidates) {
32
+ const r = spawnSync(c, ['--version'], { stdio: 'ignore' });
33
+ if (r.status === 0) {
34
+ chosen = c;
35
+ break;
36
+ }
37
+ }
38
+ if (!chosen) {
39
+ process.stderr.write(
40
+ '[beacon-mfg-mcp] 未找到 Python 3。请先安装 Python 3.8+ 并加入 PATH,再重试。\n'
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ const child = spawnSync(chosen, [serverPy, ...process.argv.slice(2)], {
46
+ stdio: 'inherit',
47
+ // 显式锁定 UTF-8:中文 Windows 上 python 默认按 GBK 写 stdout,
48
+ // MCP 客户端按 UTF-8 解码会乱码。从启动器层面保证子进程用 UTF-8 输出。
49
+ env: Object.assign({}, process.env, {
50
+ PYTHONUTF8: '1',
51
+ PYTHONIOENCODING: 'utf-8',
52
+ }),
53
+ });
54
+ process.exit(child.status === null ? 1 : child.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beacon-mfg-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.5.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
5
  "keywords": [
6
6
  "mcp",
@@ -27,7 +27,6 @@
27
27
  "server.py",
28
28
  "http_server.py",
29
29
  "mcp_adapter.py",
30
- "test_protocol.py",
31
30
  "bin/",
32
31
  "rfq-kernel/",
33
32
  "copy-rfk.js",
@@ -9,6 +9,15 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  import sys
12
+ # [utf8-guard] Windows 下 stdout/stderr 非 TTY 时按 locale 编码(中文 Windows = cp936/GBK),
13
+ # 而本脚本输出的 ✗ ✓ ⚠ 等符号 GBK 码表里根本没有 → print 当场抛
14
+ # UnicodeEncodeError: 'gbk' codec can't encode character '\u2717'
15
+ # 整条流水线会断在打印那一行。统一 UTF-8 + 兜底替换:宁可显示成 '?',也绝不因打印而崩。
16
+ for _s in (sys.stdout, sys.stderr):
17
+ try:
18
+ _s.reconfigure(encoding="utf-8", errors="replace")
19
+ except Exception:
20
+ pass
12
21
  from datetime import datetime, timezone
13
22
  from pathlib import Path
14
23