diff --git a/.ua/backlog.json b/.ua/backlog.json index cf62271..ca1fd77 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1564,6 +1564,14 @@ "acceptance": "ua info <组件id> 末尾列出包含该组件的配方 id 与标题(无命中不追加空行);--json 输出增加 recipes 字段;MCP get_component 组件级返回同步带 recipes。", "notes": "", "status": "done" + }, + { + "id": "g151", + "title": "verify 校验 MCP 工具清单三处一致", + "motivation": "MCP 工具从 4 个扩到 6 个时,README/AGENTS/CLI 清单靠手改;漏一处就会让 Agent 读到过期的工具名,而 verify 当时不管这块。", + "acceptance": "MCP_TOOL_NAMES 为单一来源,cmdMcp 启动时自证注册表与之一致;verify 检查 README/AGENTS/CLI 清单逐个写出工具名,缺失即报错并指出漏了哪个(已负向实测)。", + "notes": "", + "status": "open" } ] } diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index 60f3377..98e91ac 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -26,6 +26,7 @@ const INDEX_FILE = path.join(INDEX_DIR, 'index.json'); const LLMS_FILE = path.join(ROOT, 'llms.txt'); const LLMS_FULL = path.join(ROOT, 'llms-full.txt'); const RECIPES_FILE = path.join(ROOT, 'packages', 'tools', 'cli', 'recipes.md'); // 组合配方单一来源(人类版与 MCP 共用) +const MCP_TOOL_NAMES = ['list_components', 'get_component', 'search_components', 'get_tokens', 'list_recipes', 'get_recipe']; // MCP 工具单一来源:注册表与三处文档都对它校验 /*** 汇总各包 ua.manifest.json 与 package.json 版本(扫描 packages/ 两层目录) ***/ function collectManifests() { @@ -580,6 +581,9 @@ function cmdMcp() { const recipes = parseRecipes(); // 配方清单:MCP 侧只读一次 const compIds = collectComponents(packages).map((x) => x.comp.id); // 组件全集:把配方里的组件名回指到注册表 + if (tools.map((t) => t.name).join() !== MCP_TOOL_NAMES.join()) { + throw new Error(`MCP 工具注册表与 MCP_TOOL_NAMES 不一致:${tools.map((t) => t.name).join(',')}`); // 常量是文档校验的基准,先自证 + } /*** 从配方正文里认出用到的组件 id(按词边界匹配,避免 toast 命中 toastError) ***/ const recipeComponents = (r) => @@ -987,6 +991,26 @@ function checkIterations() { return issues; } +/*** MCP 工具清单校验:README / AGENTS / CLI 清单必须逐个写出实际注册的工具名(新增工具漏改文档会命中) ***/ +function checkMcpDocs() { + const issues = []; + const files = [ + ['README.md', path.join(ROOT, 'README.md')], + ['AGENTS.md', path.join(ROOT, 'AGENTS.md')], + ['packages/tools/cli/ua.manifest.json', path.join(ROOT, 'packages', 'tools', 'cli', 'ua.manifest.json')], + ]; + for (const [label, file] of files) { + if (!fs.existsSync(file)) { + issues.push(`${label} 不存在:MCP 工具清单无处登记`); + continue; + } + const text = fs.readFileSync(file, 'utf8'); + const miss = MCP_TOOL_NAMES.filter((n) => !text.includes(n)); + if (miss.length) issues.push(`${label} 的 MCP 工具清单缺少:${miss.join(' / ')}(改了 ua mcp 的工具就要同步这三处)`); + } + return issues; +} + /*** 配方结构校验:编号连续不重复、每条都有最小代码块与要点、通用约定段存在(MCP 与 llms-full 都按这个结构取用) ***/ function checkRecipes() { const issues = []; @@ -1093,6 +1117,7 @@ function cmdVerify() { if (demoSource) issues.push(...checkDemoBundle(anchors)); // 产物与源码同步校验 if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验'); issues.push(...checkRecipes()); // 配方结构:编号连续 + 骨架 + 要点,保证 MCP 取得到 + issues.push(...checkMcpDocs()); // MCP 工具清单:改注册表必须同步 README / AGENTS / CLI 清单 for (const pkg of pkgs) { const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平 @@ -1141,7 +1166,7 @@ function cmdVerify() { console.error(`${issues.length} violation(s)`); process.exit(1); } - console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌,README 组件清单与分组同步,backlog ↔ 留痕一致,组合配方结构完整)'); + console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌,README 组件清单与分组同步,backlog ↔ 留痕一致,组合配方结构完整,MCP 工具清单三处一致)'); } /*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/