beacon-mfg-mcp 1.12.7 → 1.12.9

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()`。
@@ -157,6 +163,28 @@ agent 必须据此决定「答有 / 答无 / 答未覆盖」——只念 `total_
157
163
  > 同批实测的对照组:`search_vendors("盈科", city="苏州")` → 前三名正是盈科在苏州/太仓/常熟的
158
164
  > 三家分所,`gb=7231`。**排序本身没坏**,坏在拿不到货时用单字凑数。
159
165
 
166
+ ### 4.4 入参别名与未知参数(1.12.9)
167
+
168
+ 工具的参数名是**固定**的(`search_vendors` 用 `query`,不是 `keyword`)。为了让「差一点」
169
+ 的调用不至于变成一次静默的错误结果,入口做了两件事:
170
+
171
+ | 情形 | 行为 | 回执里能看到 |
172
+ |---|---|---|
173
+ | 近义词(`keyword`/`q`/`term`/`name` → `query`;`token`/`vendor_id` → `id`;`query` → `demand_text`…) | 自动归一,调用正常执行 | `arg_aliases: {"keyword":"query"}` |
174
+ | 归一不了的陌生参数名,**且该工具的主参数为空** | 拒绝执行:`unknown_argument` + 点名正确参数名 | 不返回任何记录,**不消耗额度** |
175
+ | 陌生参数名,但主参数已给 | 放行,结果有效 | `unknown_args: [...]` + `hint` 说明「已忽略」 |
176
+ | 正式名与别名同时给 | 正式名优先,别名丢弃 | `arg_aliases` 为空 |
177
+
178
+ > **为什么必须拦。** 2026-11-14 实测:按 `keyword` 调 `search_vendors`(schema 里叫 `query`),
179
+ > 服务端把未知参数静默丢掉,只剩 `city` 成立 → 返回 3000 条与关键词毫无关系的记录
180
+ > (洗衣店 / 装饰加工厂 / 私厨),`terms_hit: 0`,**既无 error 也无 warning**。
181
+ > 这种「看起来成功」的错比 400 更坏:agent 会把「小白熊洗衣」当成素粉店念给用户。
182
+ >
183
+ > 同批还修了一个更隐蔽的:查不到的 token 走的是上游纯文本 `Not found`(404),
184
+ > `resp.json()` 一抛就被当成「工具崩了」报 `-32603 tool_failed`(线上原话是
185
+ > `Unexpected token 'N', "Not found" is not valid JSON`)。现在统一翻成
186
+ > `{"error":"not_found", "hint":"...用 search_vendors 重新取 token..."}`。
187
+
160
188
  ## 5. 验收清单
161
189
 
162
190
  ```bash
@@ -209,7 +237,46 @@ npx @modelcontextprotocol/inspector
209
237
  # Transport: Streamable HTTP URL: https://beacon-mfg.pages.dev/mcp
210
238
  ```
211
239
 
212
- ## 6. 已知边界
240
+ ## 6. 被发现:服务卡(SEP-2127)
241
+
242
+ 端点存在,不等于 agent 知道它存在。**MCP 没有服务搜索引擎**:客户端启动时读的是本地
243
+ 配置里已经写好的连接列表,不会联网去找有什么服务。注册到 Registry 的作用是让**人**在
244
+ 目录站/客户端面板里看到条目,再点一下写进配置 —— 链条上永远有一个人。
245
+
246
+ 唯一不靠人转述的机制是服务卡:
247
+
248
+ ```bash
249
+ curl -s https://beacon-mfg.pages.dev/.well-known/mcp.json | jq '{name, version, remotes}'
250
+ ```
251
+
252
+ ```json
253
+ {
254
+ "name": "io.github.eiry16/beacon-mfg-mcp",
255
+ "version": "1.12.7",
256
+ "remotes": [{ "type": "streamable-http", "url": "https://beacon-mfg.pages.dev/mcp" }]
257
+ }
258
+ ```
259
+
260
+ | 项 | 值 |
261
+ |---|---|
262
+ | 路径 | `/.well-known/mcp.json`;别名 `/.well-known/mcp/server-card.json`(同一份 body) |
263
+ | 生成方式 | **由 `_worker.js` 生成**,不是静态文件;`version` / `protocolVersion` 直接引用端点常量 |
264
+ | 字段集 | SEP-2127 收敛后的范围:身份 + 远端端点。**不列工具、不声明 auth**(工具是运行时 `server/discover` 拿的) |
265
+ | 配额 | 不消耗。路由在门禁**之前** —— 发现阶段被自己的闸拦一次,等于把这条路堵死 |
266
+
267
+ **为什么两个路径都发**:草案期从 `/.well-known/mcp.json` 换到
268
+ `/.well-known/mcp/server-card.json`,目前仍在 Extensions Track 里演进。只发一个等于
269
+ 赌客户端读哪个,赌错的症状是「什么都对,就是没人连」—— 没有任何报错。
270
+
271
+ **为什么不用静态文件**:① 点开头的目录在 Pages 上行为不保证(Workers Sites 明确忽略
272
+ hidden files);② 静态文件的 version 迟早与端点漂移,而这张卡正是给「不会来问的人」看的,
273
+ 编错了也没人报错。`scripts/deploy_pages.py --verify` 会把卡片版本与 `initialize` 自报
274
+ 版本对比,不一致即红。
275
+
276
+ `llms.txt` 里同时给出了端点 URL 与卡片地址 —— 那是「会读本站的 agent」唯一会看的一份说明。
277
+ 在补之前它只写了 `npx` 安装,于是「不想装东西」的 agent 读完也不知道该怎么连。
278
+
279
+ ## 7. 已知边界
213
280
 
214
281
  1. **无状态 = 无推送**。需要 sampling / elicitation / 服务端通知的客户端功能用不了。
215
282
  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
 
@@ -43,6 +46,11 @@
43
46
 
44
47
  所有返回都是 JSON 文本。找不到时返回 `{ "error": ... }` 或 `has_card:false`,不会抛协议错。
45
48
 
49
+ **参数名写了近义词也能用**(`keyword`/`q`/`term`/`name` → `query`,`token`/`vendor_id` → `id`,
50
+ `query` → `demand_text`…),回执里用 `arg_aliases` 说明被改写成了什么。远程端点还多一层:
51
+ 归一不了的陌生参数名,如果该工具的主参数为空,直接回 `unknown_argument` 并点名正确参数 ——
52
+ **不会**退化成「只按剩下的条件检索」然后给你一堆无关记录。见 `MCP_HTTP.md` §4.4。
53
+
46
54
  ### `linked_entity`:招牌名 ≠ 工商登记名
47
55
 
48
56
  名录里的 `company` 是**门店招牌/经营名**(来自地图、黄页、展会),而工商登记名往往更长、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beacon-mfg-mcp",
3
- "version": "1.12.7",
3
+ "version": "1.12.9",
4
4
  "mcpName": "io.github.eiry16/beacon-mfg-mcp",
5
5
  "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).",
6
6
  "keywords": [
package/server.py CHANGED
@@ -29,7 +29,7 @@ Continue / 常见 MCP 客户端 等主流 MCP 客户端。
29
29
  answer_sourcing 续接澄清轮次 / 返回初选供应商
30
30
  refine_sourcing 推荐轮交互:details / more / best
31
31
  account 调用身份与配额(匿名可用)
32
- vendor_onboarding 供应商入驻 / 认领入口
32
+ get_onboarding_guide 供应商入驻 / 认领入口(旧名 vendor_onboarding 仍可用)
33
33
  read_notes 读踩坑留言区(别的 agent 踩过的坑与绕行)
34
34
  report_note 写踩坑留言区(必须实测踩到才写)"""
35
35
 
@@ -5719,7 +5719,7 @@ TOOLS = [
5719
5719
  },
5720
5720
  },
5721
5721
  {
5722
- "name": "vendor_onboarding",
5722
+ "name": "get_onboarding_guide",
5723
5723
  "description": "给供应商开通「入驻 / 认领」入口:返回一个**可直接点开**的控制台链接与步骤。"
5724
5724
  "当用户说「我是某某公司的」「我想入驻 / 上架产品 / 让客户搜到我」"
5725
5725
  "「我要认领我的企业」「怎么注册成供应商」「我的审核到哪一步了」时调用。"
@@ -6038,6 +6038,70 @@ def _product_tokens(name: str, args: Dict[str, Any]) -> List[str]:
6038
6038
  return out[:6]
6039
6039
 
6040
6040
 
6041
+ # ── 入参别名(与远程 worker 的 MCP_ARG_ALIASES 同一口径,2026-11-14 起)──────
6042
+ # 起因:模型常把 search_vendors 的关键词参数写成 `keyword`,而 schema 里叫 `query`。
6043
+ # 服务端一旦静默丢弃未知参数,就只剩 city 成立 —— 返回一堆与关键词毫无关系的记录,
6044
+ # 而且看起来完全像成功(有 total、有条目、无报错)。远程端已加「未知参数拦截」,
6045
+ # 本地这侧只做归一:stdio 的 12 个工具签名比远程宽,且终端里看得见输出、能自己修正。
6046
+ _ARG_ALIASES: Dict[str, Dict[str, str]] = {
6047
+ "search_vendors": {
6048
+ "keyword": "query", "keywords": "query", "q": "query", "kw": "query",
6049
+ "term": "query", "terms": "query", "text": "query", "search_text": "query",
6050
+ "name": "query", "company": "query", "company_name": "query",
6051
+ "product": "query", "products": "query", "word": "query",
6052
+ "city_name": "city", "region": "city", "province": "city",
6053
+ "industry_code": "gb", "gb_code": "gb", "gbt": "gb", "industry": "gb",
6054
+ "industry_codes": "gbs", "gb_codes": "gbs",
6055
+ "size": "limit", "count": "limit", "top_k": "limit", "page_size": "limit",
6056
+ },
6057
+ "get_vendor": {
6058
+ "token": "id", "vendor_id": "id", "company_id": "id", "subject_id": "id",
6059
+ "entity_id": "id", "vid": "id", "supplier_id": "id",
6060
+ },
6061
+ "get_capability_card": {
6062
+ "token": "id", "vendor_id": "id", "company_id": "id", "subject_id": "id",
6063
+ "entity_id": "id", "vid": "id", "supplier_id": "id",
6064
+ },
6065
+ "start_sourcing": {
6066
+ "query": "demand_text", "keyword": "demand_text", "demand": "demand_text",
6067
+ "need": "demand_text", "requirement": "demand_text", "text": "demand_text",
6068
+ "brief": "demand_text", "city_name": "city", "region": "city",
6069
+ },
6070
+ "answer_sourcing": {
6071
+ "session": "session_id", "sid": "session_id",
6072
+ "answer": "answers", "reply": "answers", "response": "answers",
6073
+ },
6074
+ "refine_sourcing": {
6075
+ "session": "session_id", "sid": "session_id",
6076
+ "mode": "action", "op": "action", "type": "action",
6077
+ "keyword": "value", "query": "value", "q": "value", "token": "value",
6078
+ },
6079
+ "get_onboarding_guide": {
6080
+ "id": "supplier_id", "token": "supplier_id", "vendor_id": "supplier_id",
6081
+ "name": "company", "company_name": "company",
6082
+ "action": "intent", "mode": "intent",
6083
+ },
6084
+ }
6085
+
6086
+
6087
+ def _norm_args(name: str, args: Dict[str, Any]) -> Dict[str, Any]:
6088
+ """未知的近义词参数 → 正式名。正式名已给时以正式名为准(别名不覆盖)。"""
6089
+ alias = _ARG_ALIASES.get(name)
6090
+ if not isinstance(args, dict) or not args or not alias:
6091
+ return args
6092
+ out = dict(args)
6093
+ taken = set()
6094
+ for k in list(args.keys()):
6095
+ target = alias.get(str(k).lower())
6096
+ if not target:
6097
+ continue
6098
+ if target in args or target in taken: # 显式给了正式名 / 已有别名占用
6099
+ continue
6100
+ out[target] = out.pop(k)
6101
+ taken.add(target)
6102
+ return out
6103
+
6104
+
6041
6105
  def _dispatch(name: str, args: Dict[str, Any]) -> Dict[str, Any]:
6042
6106
  """tool 调用的唯一入口 —— 埋点就挂在这里(文档 §4)。
6043
6107
 
@@ -6045,7 +6109,8 @@ def _dispatch(name: str, args: Dict[str, Any]) -> Dict[str, Any]:
6045
6109
  被调用函数内部,tokens 必须在请求发出前就位;而 hits 只能等结果出来后
6046
6110
  回填,供**后续**请求带上(一次检索的头几个请求因此没有 hits 列,属正常)。
6047
6111
  """
6048
- args = _vid_in(args or {})
6112
+ args = _norm_args(name, args or {})
6113
+ args = _vid_in(args)
6049
6114
  # 每次调用重置「取数失败原因」,避免上一次的超时/401 文案串到这一次的正常结果上
6050
6115
  _HTTP_LAST_ERROR[0] = ""
6051
6116
  _FETCH_ABORTED[0] = ""
@@ -6126,7 +6191,10 @@ def _dispatch_inner(name: str, args: Dict[str, Any]) -> Dict[str, Any]:
6126
6191
  return refine_sourcing(args.get("session_id", ""), args.get("action", ""), args.get("value"))
6127
6192
  if name == "account":
6128
6193
  return account(args.get("action", "status"), args.get("phone", ""), args.get("code", ""))
6129
- if name == "vendor_onboarding":
6194
+ if name in ("get_onboarding_guide", "vendor_onboarding"):
6195
+ # 旧名 vendor_onboarding 是 1.12.8 之前的写法,作隐藏别名保留
6196
+ # (不出现在 tools/list 里,但调用能命中),避免已配旧名的客户端报
6197
+ # unknown tool。等 REAL 调用日志里旧名归零再删。
6130
6198
  return vendor_onboarding(args.get("supplier_id", ""), args.get("company", ""),
6131
6199
  args.get("intent", "claim"))
6132
6200
  if name == "read_notes":