beacon-mfg-mcp 0.1.4 → 1.3.0

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 (36) hide show
  1. package/README.md +55 -5
  2. package/RELEASE.md +19 -4
  3. package/copy-rfk.js +44 -0
  4. package/http_server.py +224 -0
  5. package/package.json +42 -35
  6. package/rfq-kernel/README.md +98 -0
  7. package/rfq-kernel/SKILL.md +80 -0
  8. package/rfq-kernel/audience/domestic_downstream.json +48 -0
  9. package/rfq-kernel/audience/intl_buyer.json +49 -0
  10. package/rfq-kernel/demo.py +212 -0
  11. package/rfq-kernel/demo_out.txt +166 -0
  12. package/rfq-kernel/industry/die_casting.json +51 -0
  13. package/rfq-kernel/industry/drinkware.json +110 -0
  14. package/rfq-kernel/industry/electronics.json +67 -0
  15. package/rfq-kernel/industry/fasteners.json +63 -0
  16. package/rfq-kernel/industry/injection.json +64 -0
  17. package/rfq-kernel/industry/machining.json +81 -0
  18. package/rfq-kernel/industry/material_handling.json +99 -0
  19. package/rfq-kernel/industry/mattress.json +104 -0
  20. package/rfq-kernel/industry/raw_material.json +58 -0
  21. package/rfq-kernel/industry/sheet_metal.json +69 -0
  22. package/rfq-kernel/industry/surface_treatment.json +64 -0
  23. package/rfq-kernel/schema/envelope.schema.json +67 -0
  24. package/rfq-kernel/schema/rfq.schema.json +127 -0
  25. package/rfq-kernel/schema/supplier_card.schema.json +79 -0
  26. package/rfq-kernel/src/beacon_adapter.py +247 -0
  27. package/rfq-kernel/src/intake.py +367 -0
  28. package/rfq-kernel/src/kernel.py +146 -0
  29. package/rfq-kernel/src/mcp_bridge.py +590 -0
  30. package/rfq-kernel/suppliers/SUP-DW-001.json +34 -0
  31. package/rfq-kernel/suppliers/SUP-DW-002.json +32 -0
  32. package/rfq-kernel/suppliers/SUP-DW-003.json +35 -0
  33. package/rfq-kernel/suppliers/SUP-DW-004.json +34 -0
  34. package/rfq-kernel/suppliers/SUP-DW-005.json +32 -0
  35. package/rfq-kernel/suppliers/SUP-MT-001.json +34 -0
  36. package/server.py +1965 -1851
package/README.md CHANGED
@@ -51,6 +51,12 @@ npm i -g beacon-mfg-mcp # 或不安装直接用 npx beacon-mfg-mcp
51
51
 
52
52
  不填 `BEACON_REPO` 时默认走 `https://beacon-mfg.pages.dev`,即开即用。
53
53
 
54
+ > **v1.3.0 起 npm 包自带 `rfq-kernel`**:`start_sourcing` / `answer_sourcing` / `refine_sourcing`
55
+ > 三个寻源 tool 依赖该桥接模块。`prepack` 会把仓库平级的 `skills/rfq-kernel` 打进包内,
56
+ > 所以 `npm i -g beacon-mfg-mcp`(≥1.3.0)后 6 个 tool 全部可用。
57
+ > ⚠️ **早于此版本的 npm 包不含 rfq-kernel**,那 3 个寻源 tool 会返回「桥接未就绪」——
58
+ > 要么升级到 1.3.0+,要么改用「方式 A / 方式 B」源码接入。
59
+
54
60
  **方式 A — 本地 git 仓库(离线 + 隐私安全 + 零 CF 流量)**
55
61
 
56
62
  ```jsonc
@@ -66,9 +72,15 @@ npm i -g beacon-mfg-mcp # 或不安装直接用 npx beacon-mfg-mcp
66
72
  }
67
73
  ```
68
74
 
69
- 本地模式只通过 `git show HEAD:<path>` 读取**已提交**内容——这是掩码态手机号(138\*\*\*\*0000),
75
+ 本地模式通过 `git show HEAD:<path>` 读取已提交内容——这是掩码态手机号(138\*\*\*\*0000),
70
76
  且不会触发 `maskphone` 的 smudge、不会碰 git index 锁。
71
77
 
78
+ > **闭合检索缺口(v1.3.0 新增)**:默认 `BEACON_WORKTREE=1`,本地模式会**优先读工作树文件**
79
+ > (含尚未提交 / 刚修改的数据),只在文件缺失或单对象 JSON 解析失败(cron 写入中途的半成品)时
80
+ > 退回 `git show HEAD` 已提交快照,再否则走 HTTP。这样 cron 刚写入、尚未 `git commit` 的记录也能被
81
+ > `get_vendor` / `search_vendors` 检索到。设 `BEACON_WORKTREE=0` 可恢复「只读已提交快照」的严格行为
82
+ > (仍不触发 smudge / 不碰 index 锁)。
83
+
72
84
  **方式 B — Cloudflare Pages 公开端点(最省事,需联网)**
73
85
 
74
86
  不设置 `BEACON_REPO` 即可,默认基址 `https://beacon-mfg.pages.dev`。可改:
@@ -80,6 +92,41 @@ npm i -g beacon-mfg-mcp # 或不安装直接用 npx beacon-mfg-mcp
80
92
  HTTP 模式带正确 `User-Agent` 与 `ETag` 304 缓存(与 App 同款策略),并落盘 `~/.cache/beacon-mcp-cache`,
81
93
  重复查询零传输。CF 部署版手机号可能是全号——隐私敏感场景请用方式 A。
82
94
 
95
+ ## 远程 HTTP 端点(Streamable HTTP)
96
+
97
+ `server.py` 本体是 **stdio**;`http_server.py` 是它的 **HTTP 传输适配层**(复用同一份 `_dispatch`/`TOOLS`,
98
+ 业务逻辑一行没动,所以 stdio 与 HTTP 两条链路不会分叉)。
99
+
100
+ ```bash
101
+ MCP_PORT=8787 python http_server.py # 默认 0.0.0.0:8787,端点 /mcp,健康检查 /health
102
+ ```
103
+
104
+ 客户端配置(`type: http` 即 Streamable HTTP):
105
+
106
+ ```jsonc
107
+ { "mcpServers": { "beacon-mfg": { "type": "http", "url": "https://<你的地址>/mcp" } } }
108
+ ```
109
+
110
+ **先用隧道验证**(零成本、今天即可验证协议,但 PC 关机即失效):
111
+
112
+ ```bash
113
+ cloudflared tunnel --url http://localhost:8787 # 拿到 https://xxx.trycloudflare.com
114
+ # 客户端填 https://xxx.trycloudflare.com/mcp
115
+ ```
116
+
117
+ > ⚠️ **现状说明**:`*.trycloudflare.com` 是本机隧道,**不是 7×24**。
118
+ > 若要做成真正常驻的远程端点,需要一台常驻 Python 宿主(云主机 / 容器 PaaS)。
119
+ > 注意 `server.py` 用了 `ThreadPoolExecutor` 并行拉分片,因此**不适合** Cloudflare Python Workers(beta,线程受限)。
120
+
121
+ ### 已实测(本地 HTTP 全流程)
122
+
123
+ `GET /health` 200 → `POST /mcp initialize` 200 并下发 `Mcp-Session-Id` →
124
+ `notifications/initialized` **202 空体** → `tools/list` 6 个 tool →
125
+ `tools/call search_vendors{city:苏州, query:"精密 加工"}` 返回 **3742 条命中**(`via_index: true`)。
126
+
127
+ 实现要点:`initialize` 响应 `protocolVersion: 2024-11-05`;本服务**不提供** server→client 的 SSE
128
+ 主动推送(只读检索不需要),故 `GET /mcp` 返回 405 —— 这是 MCP 规范允许的(MAY NOT)。
129
+
83
130
  ## 影响评估(对现有系统为零影响)
84
131
 
85
132
  ### 1) 对 Android App(Beacon-MFG)— 无影响
@@ -94,8 +141,10 @@ cron / GUI 是**写方**:`fetch_batch` / `postfetch` 改写 `data/gb`、`data/
94
141
  (经 maskphone clean 过滤脱敏),再发布到 R2 / Pages / GitHub(`fetch_batch` 还持有仓库级 PID 锁
95
142
  `.fetch_batch.lock`)。MCP 是**只读**且不调用任何后端脚本,因此:
96
143
 
97
- - **硬规则 1 — 只读已发布快照,不碰实时工作树**:MCP 只从「CF 公开端点」或「`git show HEAD:` 已提交内容」
98
- 读取,**绝不读 cron 正在写的实时工作树**,避免读到半成品 JSON。本服务严格按此实现。
144
+ - **硬规则 1 — 默认只读已提交快照,工作树读取受控**:MCP 只从「CF 公开端点」或「`git show HEAD:` 已提交内容」
145
+ 读取;`BEACON_WORKTREE=1`(默认)时额外优先读工作树文件以闭合检索缺口,但**仅直接 `open()` 工作树、
146
+ 不触发 `git checkout`/smudge、不碰 index 锁**,因此不会重掩码手机号,也只在单对象 JSON 解析失败
147
+ (半成品)时退回 HEAD,避免读到损坏内容。`BEACON_WORKTREE=0` 则恢复纯「只读已提交快照」。
99
148
  - **硬规则 2 — 绝不 `git checkout` / `git restore` 本仓库**:这是历史「42578 个手机号被无声掩码」事故的
100
149
  根因——smudge 过滤器会把 index 里的脱敏内容写回工作树,且 `git status` 仍显干净。本服务**只**用
101
150
  `git show HEAD:`(不触发 smudge、不碰 index 锁),从根上避开。
@@ -120,10 +169,11 @@ MCP 服务有两种发布渠道(详见 `mcp/RELEASE.md`):
120
169
 
121
170
  - **GitHub Release(已配 CI)**:推送 `mcp-v*` 标签即由 `.github/workflows/mcp-release.yml`
122
171
  自动打包 `mcp/` 目录为 `beacon-mfg-mcp-mcp-vX.Y.Z.tar.gz` 并创建 Release。
123
- - **npm 包(已发布 ✅)**:`beacon-mfg-mcp` 已于 **2026-09-24** 发布到 npm 公共仓库,当前版本 **`0.1.3`**
172
+ - **npm 包(已发布 ✅)**:`beacon-mfg-mcp` 已发布到 npm 公共仓库,当前版本 **`1.3.0`**
124
173
  (<https://www.npmjs.com/package/beacon-mfg-mcp>)。第三方可直接 `npm i -g beacon-mfg-mcp`
125
174
  或 `npx beacon-mfg-mcp`,客户端配置 `"command": "beacon-mfg-mcp"` 即可,无需 clone、无需填路径。
126
- 已实测:安装 → MCP 握手 → `tools/list` 全通(暴露 6 个 tool)。
175
+ **1.3.0 起 npm 包自带 rfq-kernel**,安装 → MCP 握手 → `tools/list` 暴露 6 个 tool 且全部可用
176
+ (含 `start/answer/refine_sourcing` 三个寻源 tool;更早版本不含该桥接,那 3 个 tool 会降级)。
127
177
 
128
178
  ### 发布到 npm 的步骤(需要 Access Token,不是 2FA 种子)
129
179
 
package/RELEASE.md CHANGED
@@ -1,4 +1,4 @@
1
- # Beacon-MFG 只读 MCP 服务 · v0.1.0
1
+ # Beacon-MFG 只读 MCP 服务 · v1.3.0
2
2
 
3
3
  让任意支持 MCP 的主流 agent(Claude Desktop / Cline / Continue / WorkBuddy 等)能够
4
4
  **检索与调用**已发布到 Cloudflare Pages(或 GitHub)的灯塔工厂供应商数据,
@@ -8,17 +8,31 @@
8
8
  MCP 只做**只读检索**。后端数据采集、英文翻译、库维护等流水线**不进 MCP、不暴露、不持密钥**,
9
9
  也不会落在任何写路径上。对 Android App 与 cron/GUI 流水线**零影响**(详见 `mcp/README.md`)。
10
10
 
11
- ## 三个只读 tool
11
+ ## 六个只读 tool
12
12
  | tool | 用途 | 关键参数 |
13
13
  |------|------|----------|
14
14
  | `search_vendors` | 按 关键词 / 城市 / 国标码 检索,返回精简档案 | `query` `city` `gb` `limit` `offset` |
15
15
  | `get_vendor` | 按 id(+国标码)取完整中文档案 | `id`(必填) `gb`(可选) |
16
16
  | `get_capability_card` | 按 id 取能力卡(工艺/设备/产能/认证) | `id`(必填) |
17
+ | `start_sourcing` | 【客户 agent 自动触发】识别找厂/代工/采购意图 → 品类识别→宽召回→澄清,返回 `session_id` | `demand_text`(必填) `audience_id` |
18
+ | `answer_sourcing` | 续接澄清轮次:写回客户答案,返回下一轮澄清或初选列表 | `session_id`(必填) `answers` |
19
+ | `refine_sourcing` | 推荐轮次交互:`details`/`more`/`best` | `session_id`(必填) `action` `value` |
20
+
21
+ 后三个构成「采购寻源」多轮会话链路 `start_sourcing → answer_sourcing → refine_sourcing`,
22
+ 依赖仓库平级的 `skills/rfq-kernel` 桥接模块。
23
+
24
+ ## 本次更新(v1.3.0)
25
+ - **npm 包自带 rfq-kernel**:`prepack` 会把 `skills/rfq-kernel` 打进包内,因此 `npm i -g beacon-mfg-mcp`
26
+ 安装后 6 个 tool 全部可用(早于此版本的 npm 包不含该桥接,3 个寻源 tool 会降级)。
27
+ GitHub Release tarball 同步包含 `rfq-kernel/`。
28
+ - **闭合检索缺口**:本地 git 模式默认 `BEACON_WORKTREE=1`,优先读工作树文件(含未提交/刚修改的数据),
29
+ 仅在文件缺失或单对象 JSON 解析失败(半成品)时退回 `git show HEAD`,再否则走 HTTP。
30
+ 设 `BEACON_WORKTREE=0` 可恢复「只读已提交快照」的严格行为。
17
31
 
18
32
  ## 安装(三选一)
19
33
 
20
34
  ### 1. 从 GitHub Release 下载(推荐,零依赖)
21
- 下载本 Release 的 `beacon-mfg-mcp-mcp-v0.1.0.tar.gz`,解压后:
35
+ 下载本 Release 的 `beacon-mfg-mcp-mcp-v1.3.0.tar.gz`,解压后:
22
36
  ```jsonc
23
37
  {
24
38
  "mcpServers": {
@@ -53,8 +67,9 @@ git clone https://github.com/eiry16/beacon-mfg.git
53
67
  ```
54
68
 
55
69
  ## 环境变量
56
- - `BEACON_REPO`:本地 git 仓库路径;设了就用 `git show HEAD:` 读(推荐:离线 + 掩码态隐私安全)。
70
+ - `BEACON_REPO`:本地 git 仓库路径;设了就用 `git show HEAD:` + 工作树 读(推荐:离线 + 掩码态隐私安全)。
57
71
  - `BEACON_SOURCE`:HTTP 基址,默认 `https://beacon-mfg.pages.dev`(可改自定义域名)。
72
+ - `BEACON_WORKTREE`:本地模式是否优先读工作树(默认 `1`;设 `0` 恢复「只读已提交快照」)。
58
73
 
59
74
  ## 许可
60
75
  MIT
package/copy-rfk.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // prepack 步骤:把仓库平级的 skills/rfq-kernel 拷进 mcp/rfq-kernel,
4
+ // 使 npm 包与 GitHub Release tarball 自带 rfq-kernel —— 否则 start_sourcing /
5
+ // answer_sourcing / refine_sourcing 三个寻源 tool 会因桥接缺失而降级失效。
6
+ // 幂等:每次先清掉旧副本再拷;跳过 __pycache__ 与 *.pyc。
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const HERE = __dirname; // mcp
11
+ const SRC = path.resolve(HERE, '..', 'skills', 'rfq-kernel');
12
+ const DEST = path.join(HERE, 'rfq-kernel');
13
+
14
+ function rmSync(p) {
15
+ if (!fs.existsSync(p)) return;
16
+ const st = fs.statSync(p);
17
+ if (st.isDirectory()) {
18
+ for (const e of fs.readdirSync(p)) rmSync(path.join(p, e));
19
+ try { fs.rmdirSync(p); } catch (e) {}
20
+ } else {
21
+ try { fs.unlinkSync(p); } catch (e) {}
22
+ }
23
+ }
24
+
25
+ function copySync(src, dest) {
26
+ const st = fs.statSync(src);
27
+ if (st.isDirectory()) {
28
+ if (path.basename(src) === '__pycache__') return; // 不携带编译缓存
29
+ fs.mkdirSync(dest, { recursive: true });
30
+ for (const e of fs.readdirSync(src)) copySync(path.join(src, e), path.join(dest, e));
31
+ } else {
32
+ if (src.endsWith('.pyc')) return;
33
+ fs.copyFileSync(src, dest);
34
+ }
35
+ }
36
+
37
+ if (!fs.existsSync(SRC) || !fs.statSync(SRC).isDirectory()) {
38
+ process.stderr.write('[copy-rfk] 源目录不存在,跳过:' + SRC + '\n');
39
+ process.exit(0);
40
+ }
41
+ rmSync(DEST);
42
+ copySync(SRC, DEST);
43
+ rmSync(path.join(DEST, '__pycache__'));
44
+ process.stdout.write('[copy-rfk] 已同步 rfq-kernel -> ' + DEST + '\n');
package/http_server.py ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """beacon-mfg MCP —— Streamable HTTP 传输适配层(远程端点的第一步)。
4
+
5
+ 设计要点
6
+ --------
7
+ * **不改动 server.py 本体**:直接复用它的 `_dispatch` / `TOOLS`,业务逻辑一行不动,
8
+ 因此「本地 stdio」与「远程 HTTP」两条链路共用同一份检索实现,不会分叉。
9
+ * 协议:MCP **Streamable HTTP**(2025-03-26)。请求一律 `POST /mcp`,
10
+ 响应 `application/json`;`initialize` 时下发 `Mcp-Session-Id`。
11
+ * 本服务器**不提供** server→client 的 SSE 主动推送(只读检索不需要),
12
+ 故 `GET /mcp` 返回 405 —— 这是规范允许的(MAY NOT)。
13
+ * 仅用 Python 标准库,与 server.py 的零依赖原则一致。
14
+
15
+ 用法
16
+ ----
17
+ MCP_PORT=8787 python http_server.py
18
+ # 健康检查:GET /health
19
+ # 客户端配置:{ "type": "http", "url": "https://<你的地址>/mcp" }
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import sys
25
+ import uuid
26
+ import threading
27
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
28
+
29
+ # 保证可直接 `python http_server.py` 运行(同级 import server)
30
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
+
32
+ import server as S # noqa: E402
33
+
34
+ PROTOCOL_VERSION = "2024-11-05"
35
+ SERVER_INFO = {"name": "beacon-mfg-readonly", "version": "1.3.0"}
36
+ MCP_PATH = os.environ.get("MCP_PATH", "/mcp")
37
+
38
+ _sessions = set()
39
+ _lock = threading.Lock()
40
+
41
+
42
+ def _log(msg: str) -> None:
43
+ sys.stderr.write("[beacon-mcp-http] " + msg + "\n")
44
+ sys.stderr.flush()
45
+
46
+
47
+ def _result(mid, result):
48
+ return {"jsonrpc": "2.0", "id": mid, "result": result}
49
+
50
+
51
+ def _error(mid, code, message):
52
+ return {"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}}
53
+
54
+
55
+ def _handle_msg(msg):
56
+ """处理单条 JSON-RPC 消息。
57
+
58
+ 返回 (响应对象 | None, 是否需要新建会话)。
59
+ 响应为 None 表示这是通知(notification),按规范回 202、无 body。
60
+ """
61
+ if not isinstance(msg, dict):
62
+ return _error(None, -32600, "Invalid Request"), False
63
+
64
+ method = msg.get("method") or ""
65
+ mid = msg.get("id")
66
+
67
+ if method == "initialize":
68
+ return _result(mid, {
69
+ "protocolVersion": PROTOCOL_VERSION,
70
+ "capabilities": {"tools": {}},
71
+ "serverInfo": SERVER_INFO,
72
+ }), True
73
+
74
+ if method.startswith("notifications/"):
75
+ return None, False
76
+
77
+ if method == "ping":
78
+ return _result(mid, {}), False
79
+
80
+ if method == "tools/list":
81
+ return _result(mid, {"tools": S.TOOLS}), False
82
+
83
+ if method == "tools/call":
84
+ params = msg.get("params") or {}
85
+ name = params.get("name", "")
86
+ res = S._dispatch(name, params.get("arguments") or {})
87
+ return _result(mid, {
88
+ "content": [{"type": "text", "text": json.dumps(res, ensure_ascii=False)}],
89
+ "isError": isinstance(res, dict) and "error" in res,
90
+ }), False
91
+
92
+ if mid is not None:
93
+ return _result(mid, {}), False
94
+ return None, False
95
+
96
+
97
+ class MCPHandler(BaseHTTPRequestHandler):
98
+ server_version = "beacon-mcp-http/1.0"
99
+ protocol_version = "HTTP/1.1"
100
+
101
+ # ── 工具方法 ──────────────────────────────────────────────
102
+ def _cors(self):
103
+ self.send_header("Access-Control-Allow-Origin", "*")
104
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
105
+ self.send_header(
106
+ "Access-Control-Allow-Headers",
107
+ "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Authorization",
108
+ )
109
+ self.send_header("Access-Control-Expose-Headers", "Mcp-Session-Id")
110
+
111
+ def _send_json(self, code, obj, sid=None, body_empty=False):
112
+ payload = b"" if body_empty else json.dumps(obj, ensure_ascii=False).encode("utf-8")
113
+ self.send_response(code)
114
+ if not body_empty:
115
+ self.send_header("Content-Type", "application/json; charset=utf-8")
116
+ self.send_header("Content-Length", str(len(payload)))
117
+ else:
118
+ self.send_header("Content-Length", "0")
119
+ if sid:
120
+ self.send_header("Mcp-Session-Id", sid)
121
+ self._cors()
122
+ self.end_headers()
123
+ if payload:
124
+ self.wfile.write(payload)
125
+
126
+ def log_message(self, fmt, *args): # 静音默认访问日志,统一走 stderr
127
+ _log(fmt % args)
128
+
129
+ # ── 路由 ─────────────────────────────────────────────────
130
+ def do_OPTIONS(self):
131
+ self.send_response(204)
132
+ self._cors()
133
+ self.end_headers()
134
+
135
+ def do_GET(self):
136
+ path = self.path.split("?")[0].rstrip("/")
137
+ if path.endswith("/health"):
138
+ self._send_json(200, {
139
+ "ok": True,
140
+ "server": SERVER_INFO,
141
+ "protocolVersion": PROTOCOL_VERSION,
142
+ "mcpPath": MCP_PATH,
143
+ "source": getattr(S, "BEACON_SOURCE", ""),
144
+ "tools": [t["name"] for t in S.TOOLS],
145
+ })
146
+ return
147
+ if path == MCP_PATH.rstrip("/"):
148
+ # 本服务不提供 server→client 的 SSE 主动推送(只读检索不需要)
149
+ self._send_json(405, _error(None, -32000, "SSE stream not offered; use POST"))
150
+ return
151
+ self._send_json(404, _error(None, -32000, "not found"))
152
+
153
+ def do_DELETE(self):
154
+ path = self.path.split("?")[0].rstrip("/")
155
+ if path == MCP_PATH.rstrip("/"):
156
+ sid = self.headers.get("Mcp-Session-Id")
157
+ if sid:
158
+ with _lock:
159
+ _sessions.discard(sid)
160
+ self.send_response(204)
161
+ self._cors()
162
+ self.end_headers()
163
+ return
164
+ self._send_json(404, _error(None, -32000, "not found"))
165
+
166
+ def do_POST(self):
167
+ path = self.path.split("?")[0].rstrip("/")
168
+ if path != MCP_PATH.rstrip("/"):
169
+ self._send_json(404, _error(None, -32000, "not found"))
170
+ return
171
+
172
+ length = int(self.headers.get("Content-Length") or 0)
173
+ raw = self.rfile.read(length).decode("utf-8") if length else ""
174
+ try:
175
+ msg = json.loads(raw)
176
+ except Exception:
177
+ self._send_json(400, _error(None, -32700, "Parse error"))
178
+ return
179
+
180
+ sid = self.headers.get("Mcp-Session-Id")
181
+
182
+ # 批量请求:逐条处理,整体回数组
183
+ if isinstance(msg, list):
184
+ out = []
185
+ for m in msg:
186
+ resp, _ = _handle_msg(m)
187
+ if resp is not None:
188
+ out.append(resp)
189
+ self._send_json(200, out, sid=sid)
190
+ return
191
+
192
+ try:
193
+ resp, need_new_session = _handle_msg(msg)
194
+ except BaseException as e: # noqa: BLE001
195
+ self._send_json(500, _error(msg.get("id"), -32603, f"Internal error: {e}"))
196
+ return
197
+
198
+ if need_new_session:
199
+ sid = uuid.uuid4().hex
200
+ with _lock:
201
+ _sessions.add(sid)
202
+
203
+ if resp is None:
204
+ self._send_json(202, None, sid=sid, body_empty=True)
205
+ return
206
+ self._send_json(200, resp, sid=sid)
207
+
208
+
209
+ def main() -> None:
210
+ host = os.environ.get("MCP_HOST", "0.0.0.0")
211
+ port = int(os.environ.get("MCP_PORT", "8787"))
212
+ httpd = ThreadingHTTPServer((host, port), MCPHandler)
213
+ _log(f"listening http://{host}:{port}{MCP_PATH} source={getattr(S, 'BEACON_SOURCE', '')}")
214
+ _log(f"tools: {[t['name'] for t in S.TOOLS]}")
215
+ try:
216
+ httpd.serve_forever()
217
+ except KeyboardInterrupt:
218
+ pass
219
+ finally:
220
+ httpd.server_close()
221
+
222
+
223
+ if __name__ == "__main__":
224
+ main()
package/package.json CHANGED
@@ -1,35 +1,42 @@
1
- {
2
- "name": "beacon-mfg-mcp",
3
- "version": "0.1.4",
4
- "description": "Read-only MCP server exposing the BeaconMFG灯塔工厂 supplier directory (search vendors, get vendor detail, get capability card) to any MCP-compatible agent. No API key, no repo clone required.",
5
- "keywords": [
6
- "mcp",
7
- "model-context-protocol",
8
- "beacon-mfg",
9
- "suppliers",
10
- "manufacturing",
11
- "china"
12
- ],
13
- "homepage": "https://github.com/eiry16/beacon-mfg#readme",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/eiry16/beacon-mfg.git"
17
- },
18
- "bugs": {
19
- "url": "https://github.com/eiry16/beacon-mfg/issues"
20
- },
21
- "license": "MIT",
22
- "bin": {
23
- "beacon-mfg-mcp": "bin/beacon-mfg-mcp.js"
24
- },
25
- "files": [
26
- "server.py",
27
- "bin/",
28
- "README.md",
29
- "RELEASE.md"
30
- ],
31
- "engines": {
32
- "node": ">=16"
33
- },
34
- "dependencies": {}
35
- }
1
+ {
2
+ "name": "beacon-mfg-mcp",
3
+ "version": "1.3.0",
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
+ "bin/",
30
+ "rfq-kernel/",
31
+ "copy-rfk.js",
32
+ "README.md",
33
+ "RELEASE.md"
34
+ ],
35
+ "scripts": {
36
+ "prepack": "node copy-rfk.js"
37
+ },
38
+ "engines": {
39
+ "node": ">=16"
40
+ },
41
+ "dependencies": {}
42
+ }
@@ -0,0 +1,98 @@
1
+ # rfq-kernel(已移植进 beacon-mfg)
2
+
3
+ 原 `rfq-kernel/` 原型(需求侧接客引擎)整体移植到 beacon-mfg,作为
4
+ `skills/rfq-kernel/`。后续修改都在 beacon-mfg 项目内进行。
5
+
6
+ ## 目录
7
+
8
+ ```
9
+ skills/rfq-kernel/
10
+ SKILL.md # agent 侧 skill 说明,含与能力卡的关系
11
+ demo.py # 端到端演示(示例场景 + 认证强校验 + 真实卡接入)
12
+ schema/
13
+ rfq.schema.json # 标准询价单 RFQ
14
+ envelope.schema.json # 工厂响应统一信封
15
+ supplier_card.schema.json # 标准化供应商卡(含认证对象强校验)
16
+ industry/ # 行业 pack(11 个):drinkware / mattress / sheet_metal /
17
+ # machining / injection / die_casting / electronics /
18
+ # surface_treatment / fasteners / raw_material /
19
+ # material_handling(输送/物流搬运,2026-09-22 新增)
20
+ audience/ # 客户视图:intl_buyer / domestic_downstream
21
+ suppliers/ # 示例供应商卡(认证已改为对象格式)
22
+ src/
23
+ kernel.py # 协议内核 + cert_satisfied
24
+ intake.py # 需求侧流程(解析/召回/澄清/精筛/投影)
25
+ beacon_adapter.py # beacon-mfg L1 能力卡 → 标准供应商卡(单向)
26
+ ```
27
+
28
+ ## 三个关键结论(对应主人的三问)
29
+
30
+ 1. **固化接入**:rfq-kernel 现在住在 beacon-mfg 内,通过 `beacon_adapter.py`
31
+ 单向消费 beacon-mfg 的真实能力卡,不重写、不替代它。
32
+
33
+ 2. **认证必须三方可验证**:`certifications` 改为对象 `{code, cert_no, issuer,
34
+ valid_until, verified}`。`cert_satisfied()` 规定——**只有 `verified=true`
35
+ 且 `cert_no` 非空才「作数」**。ISO9001 这类认证需要上传证书号并被核验才算数,
36
+ 裸声称等同没有(见 demo 的「认证强校验单元」)。
37
+
38
+ 3. **它和能力卡不是一回事,不能直接替代**:
39
+ - beacon-mfg 能力卡 = 供给数据(工厂能做什么),由采集/认领流水线生成。
40
+ - rfq-kernel = 需求解析 + 匹配引擎,能力卡是它的输入。
41
+ - **能跑通**:`beacon_adapter` 把真实卡转成标准供应商卡后,内核零改动即可匹配
42
+ (新增 `sheet_metal` pack 验证)。但今天 beacon-mfg 自动卡 `limits` 全 null、
43
+ `certifications` 为 null,**匹配会优雅降级**——交期/认证标「需向工厂确认」,
44
+ 而非编造数字。这正是「静态优先、动态降级」的设计。
45
+
46
+ ## 验证
47
+
48
+ ```bash
49
+ cd skills/rfq-kernel && PYTHONIOENCODING=utf-8 python demo.py
50
+ ```
51
+
52
+ 预期:示例场景 3 家可承接 / 2 家结构化拒单;认证强校验三档(满足/未核验/缺失);
53
+ 真实卡 `CN-MFG-0029509` 经适配器接入后工艺匹配但硬指标全空 → 降级不中断;
54
+ 跨行业 mattress 复用同一内核。
55
+
56
+ ## 生产接线(怎么真正跑起来)
57
+
58
+ 两层协作,互不替代:
59
+
60
+ | 层 | 位置 | 职责 |
61
+ |---|---|---|
62
+ | 需求侧匹配 | `skills/rfq-kernel/`(本技能) | 模糊需求 → 宽召回 → 澄清 → 精筛 → 结构化 RFQ 信封 + 候选 |
63
+ | 供给侧投递 | `server/routers/rfq.py`(`POST /v1/rfq`) | 读候选卡的 `rfq` 块拿入口 → 投递 → 收讫确认 → 审计存证 |
64
+
65
+ - **适配器读权威卡路径**:`beacon_adapter.load_real_card()` 优先读
66
+ `skills/registry/capability/{id}.json`(与 `rfq.py` 投递读取同源),`skills/vendors/{id}/`
67
+ 兜底。生产环境不要再 grep `data/gb` 分片。
68
+ - **适配器携带三个关键信息**:能力卡上的 `rfq` 块(`endpoint`/`protocol`,决定能否投递)、
69
+ `claim.status`(信任等级锚点)、`quality.certifications`(按 `evidence` + `number`
70
+ 判定 `verified`)。
71
+ - **客户 Agent 流程**:rfq-kernel 匹配出候选 → 对带 `rfq` 块的候选调用 `POST /v1/rfq`
72
+ 投递(平台中转:强制 Bearer 凭证、审计存证、非本机地址默认不真实投递)。
73
+
74
+ ## 企业怎么"按新方案建立 skill"(空 skill 无意义 / 认领入口已存在)
75
+
76
+ - **不用建空 skill**:框架就是能力卡本身。自动采集的卡 `claim.status=unclaimed`、
77
+ limits 全 null,正是 14000+ 家爬来企业的现状——再写空 skill 只是重复现状。
78
+ - **正确做法 = 认领后自填**:走 `server/routers/claim.py` 微信验证码认领流
79
+ (App 端入口,对接 `docs/vendor-onboarding-design.html`),`claim.status` 由
80
+ `unclaimed→claimed→verified`,随后在卡上补全 `limits` / `materials` /
81
+ `quality.certifications` / `rfq` 块即可。
82
+ - **RFQ 可达性 = 能力卡上的 `rfq` 块**(`{schema:"rfq/v1", protocol, endpoint}`)。
83
+ 全库已有 7 家声明了 `rfq` 块,含两家非采集真实企业:苏州赤兔 `CN-I-0000001`、
84
+ 耐特斯 `CN-MFG-0020317`。
85
+ - `schema/supplier.schema.json` 的 `agent.capabilities`(含 `rfq`)是 L0 记录上的
86
+ Phase-1 预留字段,当前未启用;**生效的 RFQ 能力位是能力卡上的 `rfq` 块**。
87
+
88
+ ## 双源重叠已修复(mattress)
89
+
90
+ 床垫 pack 曾把 `BS5852/CA TB117/GB8624` 同时放进 `fire_retardant`(行业属性)和
91
+ `certifications`(内核认证),造成两套真相。已砍掉:
92
+
93
+ - `fire_retardant` 改为**物理阻燃特性**(`有阻燃处理` / `无阻燃处理` / `未指定`,`late` 档),
94
+ 不再含标准码。
95
+ - 标准合规(`BS5852/CA TB117/GB8624/CertiPUR/OEKO-TEX`)只留在 `certifications`,
96
+ 作为三方可验证的合规真相源。
97
+ - 示例卡 `suppliers/SUP-MT-001.json` 的 `industry_ext.fire_retardant` 同步改为 `["有阻燃处理"]`。
98
+ - demo 验证:`certifications_required` 出现 `BS5852`,`fire_retardant` 不再出现标准码。