beacon-mfg-mcp 1.12.6 → 1.12.8

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/MCP_HTTP.md CHANGED
@@ -91,7 +91,13 @@ Cloudflare 直接拒(403 / error code 1000),所以这条闸没有「客户
91
91
  | `start_sourcing` | `handleSearch` + D1 会话 | 含糊需求 → 宽召回 + 1~2 个澄清问题,返回 `session_id` |
92
92
  | `answer_sourcing` | 会话续接 + 重检 | 把澄清答案写回,**重新检索**(不是在旧结果里二次过滤) |
93
93
  | `refine_sourcing` | 会话快照 | `best` / `more` / `filter` / `details` |
94
- | `vendor_onboarding` | 纯静态文案 | 入驻/认领指路(控制台深链 + Agent HTTP 契约) |
94
+ | `get_onboarding_guide` | 纯静态文案 | 入驻/认领指路(控制台深链 + Agent HTTP 契约) |
95
+
96
+ > **命名一致性(1.12.8)**:7 个工具前 6 个都是 `verb_noun`(search_/get_/start_/answer_/refine_),
97
+ > 第 7 个原名 `vendor_onboarding` 是名词短语 —— Glama 评审把这算作一致性扣分项。
98
+ > 改成 `get_onboarding_guide` 后与 `get_vendor` / `get_capability_card` 同构:都是「取一份已有的东西」,
99
+ > 而它确实是纯静态文案、不发写请求。**旧名留作隐藏别名**(`tools/call` 能命中、`tools/list` 不出现),
100
+ > 已按旧名配好白名单的客户端不会突然拿到 `unknown tool`;等审计里旧名归零再删。
95
101
 
96
102
  **复用而不是重写**:检索打分、脱敏、逐条计量都在原 handler 里。在这里重写一份必然与 stdio 版分叉,
97
103
  而分叉的检索结果比慢一点糟得多。实现方式见 `pages/_worker.js` 的 `mcpInnerRequest()`。
@@ -131,6 +137,32 @@ Cloudflare 直接拒(403 / error code 1000),所以这条闸没有「客户
131
137
 
132
138
  **没上的工具不出现在 `tools/list` 里** —— 让 agent 看不到,比让它调了才报错好。
133
139
 
140
+ ### 4.3 `search_vendors` 的返回体:四个必须读的字段
141
+
142
+ 检索是**字面子串**匹配,所以「结果里有货」不等于「有你要的货」。返回体里带四个信号,
143
+ agent 必须据此决定「答有 / 答无 / 答未覆盖」——只念 `total_matched` 会说错话。
144
+
145
+ | 字段 | 含义 | agent 该怎么用 |
146
+ |---|---|---|
147
+ | `total_matched` | **强匹配**条数(已被 `city`/`gb` 过滤) | 这是「有多少家」,不是 `limit` 窗口大小 |
148
+ | `weak_matched` | 只被**单字**撞上的候选数,**已从 `hits` 剔除** | 别念给用户;它说明「有共同的字,但不是这个行业」 |
149
+ | `head_missing` | 查询首词(如「律师」)**全库零记录** | 答「名录暂未收录该品类」,别说「查无此企业」 |
150
+ | `head_missing_local` | 首词全库有、但本轮 `city` 范围内**一家都没有** | 答「该地区暂未收录」,并说明另有 `partial_matched` 条被扣下 |
151
+
152
+ `coverage` 是把上面两个信号翻译成的枚举:`missing_head` / `missing_local` / `none`,
153
+ 同时 `hint` 给可执行的下一步(换同义词、按 `gb` 定向、让企业自助建档)。
154
+
155
+ > **为什么必须有这四个。** 2026-11-13 实测:`search_vendors("律师事务所", city="黄浦")`
156
+ > 返回 964 条,前五名是设计事务所、健身房、星巴克、咖啡店、糕点店 —— 因为分词器把
157
+ > 「律师事务所」切成 5 个单字 + 4 个双字,而单字「事」「师」「律」在库里 df 极大
158
+ > (星巴克**久事**大厦店、鲍**师**傅)。agent 读到 964 会如实回答「黄浦有 964 家律所」。
159
+ > **真相是 0 家**:gb 7231(律师及相关法律服务)全库仅 515 家可见,集中在苏州(175)/贵阳,
160
+ > 上海一家都没有。修法有两半:排序侧「单字命中不进结果窗口」(`weak_matched` 单独报数),
161
+ > 覆盖侧「首词在本轮范围内零命中就整页扣下」(`head_missing_local`)。
162
+ >
163
+ > 同批实测的对照组:`search_vendors("盈科", city="苏州")` → 前三名正是盈科在苏州/太仓/常熟的
164
+ > 三家分所,`gb=7231`。**排序本身没坏**,坏在拿不到货时用单字凑数。
165
+
134
166
  ## 5. 验收清单
135
167
 
136
168
  ```bash
@@ -159,6 +191,23 @@ curl -s -X POST -H "Content-Type: application/json" \
159
191
  curl -s -o /dev/null -w "%{http_code}\n" $B/mcp
160
192
  ```
161
193
 
194
+ **覆盖信号的两条反向断言**(应分别得到 `coverage=missing_local` 和 0 条结果):
195
+
196
+ ```bash
197
+ # 1) 品类有、地区没有:上海黄浦区律所 —— 全库有律师,黄浦 0 家
198
+ curl -s -X POST -H "Content-Type: application/json" \
199
+ -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"search_vendors","arguments":{"query":"律师事务所","city":"黄浦","limit":10}}}' \
200
+ $B/mcp
201
+
202
+ # 2) 品类根本没有:返回 total_matched=0 且 coverage=missing_head
203
+ curl -s -X POST -H "Content-Type: application/json" \
204
+ -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"search_vendors","arguments":{"query":"律务","limit":5}}}' \
205
+ $B/mcp
206
+ ```
207
+
208
+ > 在 1.12.7 之前,上面两条分别返回 **964 条**和 **931 条**「结果」。
209
+ > 反向断言比正向断言重要:正向只能证明「有货时能取到」,证明不了「没货时不说假话」。
210
+
162
211
  真正的客户端验收(比 curl 可信):
163
212
 
164
213
  ```bash
@@ -166,7 +215,46 @@ npx @modelcontextprotocol/inspector
166
215
  # Transport: Streamable HTTP URL: https://beacon-mfg.pages.dev/mcp
167
216
  ```
168
217
 
169
- ## 6. 已知边界
218
+ ## 6. 被发现:服务卡(SEP-2127)
219
+
220
+ 端点存在,不等于 agent 知道它存在。**MCP 没有服务搜索引擎**:客户端启动时读的是本地
221
+ 配置里已经写好的连接列表,不会联网去找有什么服务。注册到 Registry 的作用是让**人**在
222
+ 目录站/客户端面板里看到条目,再点一下写进配置 —— 链条上永远有一个人。
223
+
224
+ 唯一不靠人转述的机制是服务卡:
225
+
226
+ ```bash
227
+ curl -s https://beacon-mfg.pages.dev/.well-known/mcp.json | jq '{name, version, remotes}'
228
+ ```
229
+
230
+ ```json
231
+ {
232
+ "name": "io.github.eiry16/beacon-mfg-mcp",
233
+ "version": "1.12.7",
234
+ "remotes": [{ "type": "streamable-http", "url": "https://beacon-mfg.pages.dev/mcp" }]
235
+ }
236
+ ```
237
+
238
+ | 项 | 值 |
239
+ |---|---|
240
+ | 路径 | `/.well-known/mcp.json`;别名 `/.well-known/mcp/server-card.json`(同一份 body) |
241
+ | 生成方式 | **由 `_worker.js` 生成**,不是静态文件;`version` / `protocolVersion` 直接引用端点常量 |
242
+ | 字段集 | SEP-2127 收敛后的范围:身份 + 远端端点。**不列工具、不声明 auth**(工具是运行时 `server/discover` 拿的) |
243
+ | 配额 | 不消耗。路由在门禁**之前** —— 发现阶段被自己的闸拦一次,等于把这条路堵死 |
244
+
245
+ **为什么两个路径都发**:草案期从 `/.well-known/mcp.json` 换到
246
+ `/.well-known/mcp/server-card.json`,目前仍在 Extensions Track 里演进。只发一个等于
247
+ 赌客户端读哪个,赌错的症状是「什么都对,就是没人连」—— 没有任何报错。
248
+
249
+ **为什么不用静态文件**:① 点开头的目录在 Pages 上行为不保证(Workers Sites 明确忽略
250
+ hidden files);② 静态文件的 version 迟早与端点漂移,而这张卡正是给「不会来问的人」看的,
251
+ 编错了也没人报错。`scripts/deploy_pages.py --verify` 会把卡片版本与 `initialize` 自报
252
+ 版本对比,不一致即红。
253
+
254
+ `llms.txt` 里同时给出了端点 URL 与卡片地址 —— 那是「会读本站的 agent」唯一会看的一份说明。
255
+ 在补之前它只写了 `npx` 安装,于是「不想装东西」的 agent 读完也不知道该怎么连。
256
+
257
+ ## 7. 已知边界
170
258
 
171
259
  1. **无状态 = 无推送**。需要 sampling / elicitation / 服务端通知的客户端功能用不了。
172
260
  2. **一次性工具调用没做去重**。客户端重试 = 重复计量(与 `/v1` 同口径,不为重试开豁免)。
package/README.md CHANGED
@@ -13,13 +13,16 @@
13
13
  > **两种接入形态,工具数不同**:
14
14
  > - **远程端点**(`https://beacon-mfg.pages.dev/mcp`,Streamable HTTP,无需安装)——
15
15
  > 提供 7 个工具:`search_vendors` / `get_vendor` / `get_capability_card`,
16
- > 多轮寻源 `start_sourcing` / `answer_sourcing` / `refine_sourcing`,以及 `vendor_onboarding`。
16
+ > 多轮寻源 `start_sourcing` / `answer_sourcing` / `refine_sourcing`,以及 `get_onboarding_guide`。
17
17
  > 额度按**出口 IP** 计(1,000 条记录/24h),不要注册、不要 API Key;寻源会话 2 小时过期,
18
18
  > 且只能在**同一个出口 IP** 下续接。
19
19
  > - **本地 stdio**(`npx -y beacon-mfg-mcp`)—— 下表全部 12 个,多轮寻源的澄清问题由行业 pack 驱动
20
20
  > (远程版只问「答了能过滤」的城市/工艺/认证,选项取自候选集本身),另有踩坑留言区。
21
21
  >
22
22
  > 远程端点没上的工具**不会出现在 `tools/list` 里**(让 agent 看不到,比让它调了才报错好)。
23
+ >
24
+ > 服务卡(机器可读发现,SEP-2127):`https://beacon-mfg.pages.dev/.well-known/mcp.json`
25
+ > —— **知道域名就能拿到端点、协议版本与文档地址**,不必先读这份 README。
23
26
  > 契约与验收清单见 https://github.com/eiry16/beacon-mfg/blob/main/mcp/MCP_HTTP.md。
24
27
 
25
28
  ## 提供的 tool(共 12 个:8 个只读检索 + 1 个身份自助 + 1 个入驻入口 + 2 个留言区读写)
@@ -35,7 +38,7 @@
35
38
  | `answer_sourcing` | 续接澄清轮次:写回客户答案,返回下一轮澄清问题或初选供应商列表 | `session_id`(必填) `answers` |
36
39
  | `refine_sourcing` | 推荐轮次交互:`details`(看详情与 RFQ 入口) / `more`(看更多) / `best`(看最匹配一家) | `session_id`(必填) `action` `value` |
37
40
  | `account` | 查看档位/剩余额度;`request`+`confirm` 绑手机号升 `verified`;`logout` 回匿名 | `action` `phone` `code` |
38
- | `vendor_onboarding` | 给供应商开通「入驻 / 认领」入口:返回可直接点开的控制台链接与步骤,并附 `agent_channel`(契约 v3.0,非 App 的 HTTP/JSON 自助通道)。**只给入口不代办**:该通道需手机验证码登录,认领前还须通过「登录手机号 = 工商登记法人手机号」一致性核查;验证码由用户本人输入,本工具不发写请求、不持有密钥 | `supplier_id` `company` `intent`(claim/register/status/agent) |
41
+ | `get_onboarding_guide` | 给供应商开通「入驻 / 认领」入口:返回可直接点开的控制台链接与步骤,并附 `agent_channel`(契约 v3.0,非 App 的 HTTP/JSON 自助通道)。**只给入口不代办**:该通道需手机验证码登录,认领前还须通过「登录手机号 = 工商登记法人手机号」一致性核查;验证码由用户本人输入,本工具不发写请求、不持有密钥。旧名 `vendor_onboarding` 保留为隐藏别名(能调、不列) | `supplier_id` `company` `intent`(claim/register/status/agent) |
39
42
  | `read_notes` | 读踩坑留言区:其他 agent 踩过的坑与绕行(429 语义、无区级过滤、批量取数姿势…)。检索前先读一眼省试错成本 | `topic`(默认 mcp) |
40
43
  | `report_note` | 写踩坑留言区:把自己**实测踩到**的坑(现象 + 绕行)回写给后来的 agent。必须真实踩到才写,不当反馈箱/评分区用 | `issue`(必填) `workaround` `agent` `topic` |
41
44
 
@@ -1,58 +1,58 @@
1
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
- // 更新逻辑永远跑不完。独立进程不受此影响。
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
- // 不写 __pycache__:本服务由 npm 全局安装、跑在任意用户机器上,
53
- // 字节码会落进包目录(或只读盘上 EPERM 报错)。服务是短命进程,
54
- // 字节码缓存无收益,关掉更干净。
55
- PYTHONDONTWRITEBYTECODE: '1',
56
- }),
57
- });
58
- process.exit(child.status === null ? 1 : child.status);
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
+ // 更新逻辑永远跑不完。独立进程不受此影响。
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
+ // 不写 __pycache__:本服务由 npm 全局安装、跑在任意用户机器上,
53
+ // 字节码会落进包目录(或只读盘上 EPERM 报错)。服务是短命进程,
54
+ // 字节码缓存无收益,关掉更干净。
55
+ PYTHONDONTWRITEBYTECODE: '1',
56
+ }),
57
+ });
58
+ process.exit(child.status === null ? 1 : child.status);