beacon-mfg-mcp 1.11.1 → 1.11.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.py +43 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beacon-mfg-mcp",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "Read-only MCP server exposing the BeaconMFG灯塔 directory of business entities across ALL GB/T 4754 industry divisions (manufacturing, services, retail, logistics, catering, IT, and more) to any MCP-compatible agent. No signup and no API key: an anonymous identity is generated on first run, phone binding is optional (only needed for L2 capability cards).",
5
5
  "keywords": [
6
6
  "mcp",
package/server.py CHANGED
@@ -220,7 +220,7 @@ def _recall_for_sourcing(text: str, top_k: int = 200, pack_id: str | None = None
220
220
  # 都对齐到这个值,`scripts/check_version.py` 在发布前校验它们没有漂移。
221
221
  # 读不到 package.json 时的兜底值 —— 改版本时**必须**和 package.json 一起改,
222
222
  # 否则 check_version.py 会报错(这就是它的用途)。
223
- VERSION_FALLBACK = "1.11.1"
223
+ VERSION_FALLBACK = "1.11.2"
224
224
 
225
225
 
226
226
  def _pkg_version() -> str:
@@ -414,6 +414,30 @@ def _header_value(v: Any) -> str:
414
414
  return urllib.parse.quote(s, safe=",._-")
415
415
 
416
416
 
417
+ def _quote_path(relpath: str) -> str:
418
+ """把可能含非 ASCII 的相对路径转成**可放进 HTTP 请求行**的 ASCII 串。
419
+
420
+ ⚠ 这是与 `_header_value()` 并列的**第二处编码边界**,漏了同样致命:
421
+ `http.client` 组装请求行时按 `'ascii'` 编码,路径里出现原始 UTF-8 字节
422
+ 就在**建连之前**抛 `UnicodeEncodeError: 'ascii' codec can't encode ...`。
423
+
424
+ 2026-10-24 实测路径:`_city_postings()` 拼 `skills/registry/index/city/贵阳.json`,
425
+ 城市名是发布侧的**原始中文文件名**(391 个城市全是中文名,没有哈希化)——
426
+ 中文城市 + 冷缓存首个查询必定在这一跳失败。
427
+
428
+ 为什么不在 `_city_postings()` 里就地 quote:`fetch_text()` 会**先用 relpath
429
+ 读本地工作树/dirgit HEAD**(本地仓库模式),把 relpath 提前编码会让本地读
430
+ 全部落空。所以编码只发生在「真正要发 HTTP」这一层,relpath 本身保持原样。
431
+
432
+ 只对含非 ASCII 的路径处理:纯 ASCII 路径(绝大多数分片、索引桶)走原样,
433
+ 零行为变化,也不会把已带 `%` 的路径二次编码。
434
+ """
435
+ if relpath.isascii():
436
+ return relpath
437
+ return urllib.parse.quote(relpath, safe="/")
438
+
439
+
440
+
417
441
  def _beacon_headers() -> Dict[str, str]:
418
442
  """随请求带出的头。
419
443
 
@@ -568,8 +592,16 @@ def _http_diagnose(relpath: str, last_err: str = "") -> str:
568
592
  网络不可达」,而 1.4.x~1.11.0 的真实故障是请求头里的中文 token 触发
569
593
  `UnicodeEncodeError` —— 让人白去查代理/VPN(2026-10-24 用户实测反馈)。
570
594
  """
571
- # 请求头编码问题:与网络、代理、CDN 全都无关,必须单独说清楚。
595
+ # 编码问题:与网络、代理、CDN 全都无关,必须单独说清楚。
596
+ # 且必须区分错在**请求行(URL)还是请求头**——两者排查方向完全不同:
597
+ # 'ascii' codec → 路径里混进了非 ASCII(如 city/贵阳.json)
598
+ # 'latin-1' codec → 请求头里混进了非 ASCII(如 X-Beacon-Tokens: 海伦)
599
+ # 早期把两者都写成「请求头」,用户按那个方向去查会一无所获(2026-10-24 反馈)。
572
600
  if "UnicodeEncodeError" in last_err or "latin-1" in last_err:
601
+ if "'ascii'" in last_err:
602
+ return ("请求路径 `%s` 含非 ASCII 字符,无法编码成 HTTP 请求行"
603
+ "(**本地编码问题,不是网络问题**);最后错误: %s"
604
+ % (relpath, last_err))
573
605
  return ("请求头包含非 ASCII 字符,无法编码成 HTTP 头(**本地编码问题,"
574
606
  "不是网络问题**);最后错误: " + last_err)
575
607
  hints = []
@@ -626,7 +658,7 @@ def _http_get(relpath: str) -> Tuple[Optional[str], Optional[str]]:
626
658
  last_err = ""
627
659
  _fatal = False
628
660
  for base in _http_bases():
629
- url = base + "/" + relpath
661
+ url = base + "/" + _quote_path(relpath)
630
662
  cap = _cache_path(url)
631
663
  etag = None
632
664
  if os.path.exists(cap):
@@ -2946,6 +2978,7 @@ def _city_postings(city: str) -> Optional[Dict[str, int]]:
2946
2978
  if not city:
2947
2979
  return {}
2948
2980
  safe = city.replace("/", "").replace("\\", "")
2981
+ err_before = _HTTP_LAST_ERROR[0] # 记住进入前的错误,供兜底成功后撤销
2949
2982
  txt = _index_text(INDEX_DIR + "/city/" + safe + ".json")
2950
2983
  if txt:
2951
2984
  try:
@@ -2957,6 +2990,13 @@ def _city_postings(city: str) -> Optional[Dict[str, int]]:
2957
2990
  txt = _index_text(INDEX_DIR + "/city.json")
2958
2991
  if not txt:
2959
2992
  return None
2993
+ # 走到这里说明**全表兜底取到了**。单城文件只是「省流量」的可选快路径,
2994
+ # 它失败(4xx / 编码错误 / 该城市无文件)不影响结果完整性 —— 所以不能让它
2995
+ # 冒到 `fetch_error` 里。实测代价:0 条结果时返回体夹一条「请求头编码错误」,
2996
+ # 调用方会判定「工具坏了」,而真实结论只是「库里确实没有这家企业」。
2997
+ # 只撤销**本函数内新产生**的错误,外层(manifest / 分片哈希不符)照旧保留。
2998
+ if _HTTP_LAST_ERROR[0] != err_before:
2999
+ _HTTP_LAST_ERROR[0] = err_before
2960
3000
  try:
2961
3001
  full = json.loads(txt)
2962
3002
  except Exception: