beacon-mfg-mcp 1.3.1 → 1.3.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/README.md CHANGED
@@ -46,8 +46,18 @@ npm i -g beacon-mfg-mcp # 或不安装直接用 npx beacon-mfg-mcp
46
46
  ```
47
47
 
48
48
  ```jsonc
49
- { "mcpServers": { "beacon-mfg": { "command": "beacon-mfg-mcp", "args": [] } } }
49
+ {
50
+ "mcpServers": {
51
+ "beacon-mfg": {
52
+ "command": "beacon-mfg-mcp",
53
+ "args": [],
54
+ "env": { "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8" }
55
+ }
56
+ }
57
+ }
50
58
  ```
59
+ > 启动器(`bin/beacon-mfg-mcp.js`)已默认注入上述两个变量;此处写出仅为显式/可覆盖。
60
+ > 二者保证 python 子进程以 UTF-8 输出,否则中文 Windows 上工具描述/结果会乱码。
51
61
 
52
62
  不填 `BEACON_REPO` 时默认走 `https://beacon-mfg.pages.dev`,即开即用。
53
63
 
@@ -92,6 +102,13 @@ npm i -g beacon-mfg-mcp # 或不安装直接用 npx beacon-mfg-mcp
92
102
  HTTP 模式带正确 `User-Agent` 与 `ETag` 304 缓存(与 App 同款策略),并落盘 `~/.cache/beacon-mcp-cache`,
93
103
  重复查询零传输。CF 部署版手机号可能是全号——隐私敏感场景请用方式 A。
94
104
 
105
+ > **CDN 兜底镜像(v1.3.2 新增)**:默认源 `beacon-mfg.pages.dev` 不可达(抖动/被墙)时,
106
+ > MCP 会**自动回退**到 GitHub 兜底镜像拉 `manifest.json` 与分片——`cdn.jsdelivr.net/gh/eiry16/beacon-mfg@main`
107
+ > 与 `raw.githubusercontent.com/eiry16/beacon-mfg/main`(与 Android App 同策略)。因此「免 clone 检索」
108
+ > 在单一 CDN 失效时仍可工作。如需自定义/追加镜像,设 `BEACON_MIRRORS`(逗号分隔的基址列表)。
109
+ > 注意:能力卡(`get_capability_card`)不在 git 内、仅存于 CF/R2,镜像兜底覆盖的是 manifest + 分片 + 档案这类主数据;
110
+ > 在中国大陆等直连 GitHub 困难的网络,给 MCP 进程设 `HTTPS_PROXY=http://127.0.0.1:7890`(或你的代理)可让镜像链路走代理。
111
+
95
112
  ## 远程 HTTP 端点(Streamable HTTP)
96
113
 
97
114
  `server.py` 本体是 **stdio**;`http_server.py` 是它的 **HTTP 传输适配层**(复用同一份 `_dispatch`/`TOOLS`,
@@ -169,7 +186,7 @@ MCP 服务有两种发布渠道(详见 `mcp/RELEASE.md`):
169
186
 
170
187
  - **GitHub Release(已配 CI)**:推送 `mcp-v*` 标签即由 `.github/workflows/mcp-release.yml`
171
188
  自动打包 `mcp/` 目录为 `beacon-mfg-mcp-mcp-vX.Y.Z.tar.gz` 并创建 Release。
172
- - **npm 包(已发布 ✅)**:`beacon-mfg-mcp` 已发布到 npm 公共仓库,当前版本 **`1.3.1`**
189
+ - **npm 包(已发布 ✅)**:`beacon-mfg-mcp` 已发布到 npm 公共仓库,当前版本 **`1.3.2`**
173
190
  (<https://www.npmjs.com/package/beacon-mfg-mcp>)。第三方可直接 `npm i -g beacon-mfg-mcp`
174
191
  或 `npx beacon-mfg-mcp`,客户端配置 `"command": "beacon-mfg-mcp"` 即可,无需 clone、无需填路径。
175
192
  **1.3.0 起 npm 包自带 rfq-kernel**,安装 → MCP 握手 → `tools/list` 暴露 6 个 tool 且全部可用
package/RELEASE.md CHANGED
@@ -1,4 +1,4 @@
1
- # Beacon-MFG 只读 MCP 服务 · v1.3.1
1
+ # Beacon-MFG 只读 MCP 服务 · v1.3.2
2
2
 
3
3
  让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
4
4
  **检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
@@ -21,7 +21,15 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
21
21
  后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
22
22
  依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
23
23
 
24
- ## 本次更新(v1.3.1)
24
+ ## 本次更新(v1.3.2)
25
+ - **npm 启动器锁定 UTF-8**:`bin/beacon-mfg-mcp.js` 拉起 python 时注入 `PYTHONUTF8=1` / `PYTHONIOENCODING=utf-8`,
26
+ 修复中文 Windows 上 python 按 GBK 写 stdout、被客户端按 UTF-8 解码导致的工具描述/结果乱码。
27
+ - **HTTP 兜底镜像**:默认源 `beacon-mfg.pages.dev` 不可达时,自动回退到 GitHub 镜像
28
+ `cdn.jsdelivr.net/gh/eiry16/beacon-mfg@main` 与 `raw.githubusercontent.com/eiry16/beacon-mfg/main`
29
+ (与 Android App 同策略),保证「免 clone 检索」在单一 CDN 失效时仍可工作;可用 `BEACON_MIRRORS` 追加。
30
+ - **serverInfo 版本对齐**:服务端自报版本改为读 `package.json`,不再硬编码,消除与 npm 包版本漂移。
31
+
32
+ ## 本次更新(v1.3.1,已发布)
25
33
  - **npm 包自带 rfq-kernel**:`prepack` 会把 `skills/rfq-kernel` 打进包内,因此 `npm i -g beacon-mfg-mcp`
26
34
  安装后 6 个 tool 全部可用(早于此版本的 npm 包不含该桥接,3 个寻源 tool 会降级)。
27
35
  GitHub Release tarball 同步包含 `rfq-kernel/`。
@@ -32,7 +40,7 @@ MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流
32
40
  ## 安装(三选一)
33
41
 
34
42
  ### 1. 从 GitHub Release 下载(推荐,零依赖)
35
- 下载本 Release 的 `beacon-mfg-mcp-mcp-v1.3.1.tar.gz`,解压后:
43
+ 下载本 Release 的 `beacon-mfg-mcp-mcp-v1.3.2.tar.gz`,解压后:
36
44
  ```jsonc
37
45
  {
38
46
  "mcpServers": {
@@ -55,7 +63,7 @@ npx beacon-mfg-mcp
55
63
  ```jsonc
56
64
  {
57
65
  "mcpServers": {
58
- "beacon-mfg": { "command": "beacon-mfg-mcp", "args": [] }
66
+ "beacon-mfg": { "command": "beacon-mfg-mcp", "args": [], "env": { "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8" } }
59
67
  }
60
68
  }
61
69
  ```
@@ -70,6 +78,8 @@ git clone https://github.com/eiry16/beacon-mfg.git
70
78
  - `BEACON_REPO`:本地 git 仓库路径;设了就用 `git show HEAD:` + 工作树 读(推荐:离线 + 掩码态隐私安全)。
71
79
  - `BEACON_SOURCE`:HTTP 基址,默认 `https://beacon-mfg.pages.dev`(可改自定义域名)。
72
80
  - `BEACON_WORKTREE`:本地模式是否优先读工作树(默认 `1`;设 `0` 恢复「只读已提交快照」)。
81
+ - `BEACON_MIRRORS`:HTTP 模式追加的兜底镜像基址(逗号分隔);默认源不可达时除内置的 jsDelivr/raw 外,可在此追加自有镜像。
82
+ - `HTTPS_PROXY` / `HTTP_PROXY`:python 子进程的代理;在中国大陆等直连 GitHub 困难的网络,设 `HTTPS_PROXY=http://127.0.0.1:7890` 可让镜像链路走代理。
73
83
 
74
84
  ## 许可
75
85
  MIT
@@ -26,5 +26,11 @@ if (!chosen) {
26
26
 
27
27
  const child = spawnSync(chosen, [serverPy, ...process.argv.slice(2)], {
28
28
  stdio: 'inherit',
29
+ // 显式锁定 UTF-8:中文 Windows 上 python 默认按 GBK 写 stdout,
30
+ // MCP 客户端按 UTF-8 解码会乱码。从启动器层面保证子进程用 UTF-8 输出。
31
+ env: Object.assign({}, process.env, {
32
+ PYTHONUTF8: '1',
33
+ PYTHONIOENCODING: 'utf-8',
34
+ }),
29
35
  });
30
36
  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.3.1",
3
+ "version": "1.3.2",
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",
package/server.py CHANGED
@@ -208,6 +208,33 @@ _alias_cache: Optional[Dict[str, Any]] = None
208
208
  _gb_name_cache: Optional[Dict[str, str]] = None
209
209
 
210
210
 
211
+ # 版本号单一来源:直接读同目录 package.json,避免 MCP 内部版本与发布包版本漂移。
212
+ def _pkg_version() -> str:
213
+ try:
214
+ p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "package.json")
215
+ with open(p, "r", encoding="utf-8") as f:
216
+ return json.load(f).get("version", "1.3.2")
217
+ except Exception:
218
+ return "1.3.2"
219
+
220
+
221
+ # HTTP 基址列表(含兜底镜像)。默认部署 Cloudflare Pages 不可达时,自动回退到
222
+ # GitHub 兜底镜像(与 Android App 同策略:pages.dev 主源 + jsDelivr / raw 镜像),
223
+ # 保证「免 clone 检索」在单一 CDN 抖动/被墙时仍可工作。可用 BEACON_MIRRORS 追加自定义镜像。
224
+ def _http_bases() -> List[str]:
225
+ bases = [BEACON_SOURCE]
226
+ if BEACON_SOURCE == DEFAULT_SOURCE:
227
+ bases.append("https://cdn.jsdelivr.net/gh/eiry16/beacon-mfg@main")
228
+ bases.append("https://raw.githubusercontent.com/eiry16/beacon-mfg/main")
229
+ m = os.environ.get("BEACON_MIRRORS")
230
+ if m:
231
+ for x in m.split(","):
232
+ x = x.strip().rstrip("/")
233
+ if x:
234
+ bases.append(x)
235
+ return bases
236
+
237
+
211
238
  # --------------------------------------------------------------------------- #
212
239
  # 使用量埋点(设计见 docs/MCP_USAGE_AUDIT.md §3.4 / §4 / §14)
213
240
  # --------------------------------------------------------------------------- #
@@ -362,48 +389,59 @@ def _git_show(relpath: str) -> Optional[str]:
362
389
  return None
363
390
 
364
391
 
392
+ # 单基址 HTTP 超时(秒)。多镜像回退时每个基址各自计时,避免单一 CDN 卡死拖垮整体。
393
+ _HTTP_TIMEOUT = 15
394
+
395
+
365
396
  def _http_get(relpath: str) -> Tuple[Optional[str], Optional[str]]:
366
- url = BEACON_SOURCE + "/" + relpath
367
- cap = _cache_path(url)
368
- etag = None
369
- if os.path.exists(cap):
397
+ """统一的 HTTP 读取:依次尝试 _http_bases() 中的基址(主源 + 兜底镜像),
398
+ 任一成功即返回;全部失败则用本地缓存兜底(保证离线可用)。"""
399
+ cached_body = None
400
+ cached_etag = None
401
+ for base in _http_bases():
402
+ url = base + "/" + relpath
403
+ cap = _cache_path(url)
404
+ etag = None
405
+ if os.path.exists(cap):
406
+ try:
407
+ with open(cap, "r", encoding="utf-8") as f:
408
+ blob = json.load(f)
409
+ etag = blob.get("etag")
410
+ cached_body = blob.get("body")
411
+ cached_etag = etag
412
+ except Exception:
413
+ etag = None
414
+ headers = {"User-Agent": UA, "Accept": "*/*"}
415
+ # 埋点头:服务端据此填 tool / tokens / hits 列。收不到也没关系 ——
416
+ # worker 那边**照样计数**,只是这三列为空(文档 §3.4)。
370
417
  try:
371
- with open(cap, "r", encoding="utf-8") as f:
372
- blob = json.load(f)
373
- etag = blob.get("etag")
418
+ headers.update(_beacon_headers())
374
419
  except Exception:
375
- etag = None
376
- headers = {"User-Agent": UA, "Accept": "*/*"}
377
- # 埋点头:服务端据此填 tool / tokens / hits 列。收不到也没关系 ——
378
- # worker 那边**照样计数**,只是这三列为空(文档 §3.4)。
379
- try:
380
- headers.update(_beacon_headers())
381
- except Exception:
382
- pass
383
- req = urllib.request.Request(url, headers=headers)
384
- if etag:
385
- req.add_header("If-None-Match", etag)
386
- try:
387
- resp = urllib.request.urlopen(req, timeout=30)
388
- body = resp.read().decode("utf-8")
389
- new_etag = resp.headers.get("ETag")
420
+ pass
421
+ req = urllib.request.Request(url, headers=headers)
422
+ if etag:
423
+ req.add_header("If-None-Match", etag)
390
424
  try:
391
- with open(cap, "w", encoding="utf-8") as f:
392
- json.dump({"etag": new_etag, "body": body}, f)
425
+ resp = urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT)
426
+ body = resp.read().decode("utf-8")
427
+ new_etag = resp.headers.get("ETag")
428
+ try:
429
+ with open(cap, "w", encoding="utf-8") as f:
430
+ json.dump({"etag": new_etag, "body": body}, f)
431
+ except Exception:
432
+ pass
433
+ return body, new_etag
434
+ except urllib.error.HTTPError as e:
435
+ if e.code == 304 and cached_body is not None:
436
+ return cached_body, etag
437
+ continue
393
438
  except Exception:
394
- pass
395
- return body, new_etag
396
- except urllib.error.HTTPError as e:
397
- if e.code == 304 and os.path.exists(cap):
398
- with open(cap, "r", encoding="utf-8") as f:
399
- return json.load(f).get("body"), etag
400
- return None, None
401
- except Exception:
402
- # 网络不通时若本地有缓存也返回,保证离线可用
403
- if os.path.exists(cap):
404
- with open(cap, "r", encoding="utf-8") as f:
405
- return json.load(f).get("body"), etag
406
- return None, None
439
+ # 当前基址不可达:尝试下一个兜底镜像;全部失败后在末尾用本地缓存兜底
440
+ continue
441
+ # 全部基址失败:若有任何本地缓存也返回,保证离线可用
442
+ if cached_body is not None:
443
+ return cached_body, cached_etag
444
+ return None, None
407
445
 
408
446
 
409
447
  def _worktree_read(relpath: str) -> Optional[str]:
@@ -1935,7 +1973,7 @@ def main() -> None:
1935
1973
  "result": {
1936
1974
  "protocolVersion": "2024-11-05",
1937
1975
  "capabilities": {"tools": {}},
1938
- "serverInfo": {"name": "beacon-mfg-readonly", "version": "1.3.0"},
1976
+ "serverInfo": {"name": "beacon-mfg-readonly", "version": _pkg_version()},
1939
1977
  },
1940
1978
  })
1941
1979
  elif method == "notifications/initialized":