纠错: Tool-Only 架构是唯一解
这里我们要纠正一个前文提到的概念性错误,其实也不算错误吧,属于现实太残酷:
MCP 协议里提供的四种能力:“握手”、“工具”、“资源”、“提示词”,目前所有的主流AI编程IDE都只支持一下两个:
- 握手
- 工具 至于 “资源”、“提示词” 这两个,不用想了,都不会支持的。
因此,我们在这里必须明确:
Tools 是我们 MCP 实现里唯一能提供能力的出口,其他协议一律视为过时的文档,完全不用参考。
像不像面对restful 规范,很多企业100%使用post?
一、为解决幻觉,我进行了架构设计
上一节课里,我们通过 mcp 的 tools 能力,可以让AI在碰到特定代码块时,抽取特定code查询低代码的schema。
但是一个严重的问题就出现了。
AI对咱们公司里的代码库并不熟悉,对于低代码库里的某个组件支持哪些属性并不确定。
只能靠连蒙带猜,于时幻觉出现了。
为了解决这个问题,我们需要让AI可以在短上下文里,有针对性地查看到一些确定的信息,从而确定组件支持哪些属性。
这里,我提供一下我实际遇到的问题解法:
- AI 不知道低代码平台里,组件别名与实际组件的映射。
- AI 不知道实际组件具备哪些属性和方法。
- AI 不知道组件的具体实现
问题来了,怎么解决?
我梳理了这样一个架构图。
- 首先,我会基于现有的组件库代码,生成两份中间数据:
- json画的组件库文档
- 组件库和低代码组件的映射关系
- 给AI三层降级方案:
- 优先找mcp获取准确的组件库文档
- 如果解决不了问题,则找mcp获取组件源码
- 如果还解决不了,就去项目的node_modules里找类型文件。
哈哈,这个架构和之前两课小打小闹的实现不同,伴随着很多的工程实践,应该更具参考价值。
二、引入组件库,脚本生成中间数据
在上一节我们搭建了 MCP Server 的骨架,但现在的 Server 还是个空壳。为了让 AI 懂我们的业务组件(假设名字叫:Chun Components),我们需要建立一条从源码到AI 可读数据的自动化流水线。
2.1 Submodule 引入组件库
我们的 MCP Server 通常是一个独立的项目,但它需要时刻“盯着”主业务组件库的变动。为了避免手动复制粘贴代码(容易导致版本滞后),最佳实践是使用 Git Submodule。
这样做的好处是:解耦与实时性。MCP Server 只是组件库的“观察者”,而不是持有者。
操作步骤:
在 MCP 项目根目录下,将业务组件库挂载到 vendor 目录:
mkdir vendor
git submodule add git@gitlab.example.com:frontend/chun-components.git vendor/chun-components
挂载完成后,你的目录结构应该如下所示:
mcp-server/
├── vendor/
│ └── chun-components/ # 👈 真实源码在这里
│ ├── packages/
│ │ ├── chun-ui/ # 底层核心库 (Source of Truth)
│ │ └── app-ui/ # 业务封装层 (别名来源)
├── scripts/ # 数据生成脚本
└── index.js # MCP Server 入口
2.2 生成 JSON 化的组件文档
AI 并不擅长直接阅读分散在数百个文件中的 .tsx 源码,而且原始源码中包含大量复杂的逻辑实现(Render Logic),这些对 AI 编写配置毫无意义,甚至会消耗大量 Token。
我们需要的是一份经过清洗的 API 字典。
我们编写 scripts/gen-mcp-docs.js,利用 react-docgen-typescript 工具,将 TypeScript 定义提取为结构化的 JSON。
核心代码实现:
// scripts/gen-mcp-docs.js
import { parse } from 'react-docgen-typescript';
// 1. 配置过滤器:只保留业务属性,过滤原生 DOM 属性 (如 className, onClick)
// 这一步对于节省 Token 和防止 AI 幻觉至关重要!
const options = {
savePropValueAsString: true,
propFilter: (prop) => {
if (prop.parent && /node_modules/.test(prop.parent.fileName)) {
// 比如:我们不希望 AI 看到 button 的 onDragStart 属性
return false;
}
return true;
}
};
// 2. 扫描并解析
const docsMap = {};
const filePaths = globSync('vendor/chun-components/packages/chun-ui/**/*.tsx');
filePaths.forEach(filePath => {
const docs = parse(filePath, options);
docs.forEach(doc => {
// 生成 key-value 结构
docsMap[doc.displayName] = {
description: doc.description,
props: doc.props, // 包含 type, required, description, defaultValue
relativePath: path.relative(ROOT, filePath) // 👈 留存路径,为后续源码降级做准备
};
});
});
// 3. 落盘
fs.writeFileSync('data/chun-docs.json', JSON.stringify(docsMap));
运行该脚本后,我们将得到一份 data/chun-docs.json,这就是 AI 的“组件字典”。
通过这个字典,我们可以通过名称找到组件的结构化属性和源码地址。
2.3 形成组件和低代码的映射表
这是低代码场景中独有的核心痛点。
-
现象:低代码平台的 Schema 里写的是 widget: "TextArea"(业务别名)。
-
本质:底层代码里其实是 export class ChunTextArea(真实组件名)。
-
问题:如果 AI 拿着 TextArea 去查上面的字典,会查无此人。
我们需要一张“藏宝图”,告诉 AI:“当你看到 TextArea 时,请去查 ChunTextArea 的文档。”
我们编写 scripts/scan-imports.js,通过静态分析 (Static Analysis) 业务库的入口文件,自动建立这份映射。
核心逻辑:
-
扫描导出:读取 app-ui/src/index.ts,找到 export { default as TextArea } ...。
-
溯源导入:打开对应的组件文件,找到 import { ChunTextArea } from '@chun-cscec/chun-ui'。
建立连接:生成 TextArea -> ChunTextArea 的键值对。
核心代码示例:
// scripts/scan-imports.js
// 正则:穿透封装层,直接找到 @chun/XXX 底层组件
const IMPORT_CORE_REGEX = /import\s+\{\s*(\w+)\s*\}\s+from\s+['"]@chun\/[^'"]+['"]/;
// ... (扫描逻辑) ...
if (coreMatch) {
const publicName = "TextArea"; // 来自 index.ts
const coreName = coreMatch[1]; // 提取出的 "ChunTextArea"
mapping[publicName] = coreName;
}
运行脚本后,生成 data/component-alias.json:
{
"TextArea": "ChunTextArea",
"Select": "ChunSelect",
"UserPicker": "ChunUserSelect"
}
有了这份 Docs (字典) 和 Alias (索引),我们的 MCP Server 就具备了“懂行”的基础。在下一章,我们将把这些数据装载进 Server,构建强大的查询工具。
三、依靠tools提供各种能力
三、依靠 Tools 提供各种能力 在目前的 AI IDE 生态(如 Trae, OpenCode)中,AI Agent 对 Tools(工具) 的支持远好于 Resources(资源)。AI 更倾向于在遇到问题时主动发起函数调用,而不是被动阅读挂载的静态文件。
因此,我们将摒弃标准的 resources/read 接口,将所有能力封装为三个核心 Tool,构建一套“主动检索”体系。
3.1 Server 初始化:构建“内存数据库”
Server 启动的第一步,不是监听端口,而是将第二节生成的 chun-docs.json(文档)和 component-alias.json(别名表)全部加载到内存中。
这实际上是构建了一个只读的内存数据库,它保证了 AI 的查询是毫秒级响应的,不消耗任何 IO 等待时间。
关键代码逻辑 (index.js):
// 定义内存缓存
let docsMap = {}; // 核心文档库
let lookupMap = {}; // 影子索引 (支持忽略大小写查找)
let aliasMap = {}; // 别名映射表 (TextArea -> ChunTextArea)
// ... (文件读取逻辑) ...
// 预处理:构建全方位的索引
Object.keys(aliasMap).forEach(alias => {
// 1. 存原始映射: "TextArea" -> "ChunTextArea"
// 2. 存小写映射: "textarea" -> "ChunTextArea" (由 lookupMap 再次解析)
aliasMap[alias.toLowerCase()] = aliasMap[alias];
});
3.2 核心工具:智能查询 (get_component_props)
这是本 Server 的皇冠明珠。它不再是一个简单的 Key-Value 查表器,而是一个带有四级容错策略的检索引擎。
当 AI 在低代码 Schema 中看到 widget: "TextArea" 并询问属性时,这个工具会执行以下逻辑:
-
Level 1 (直连):输入是 ChunTextArea 吗?是 -> 直接返回文档。
-
Level 2 (别名):输入是 TextArea 吗?查 aliasMap -> 发现映射到 ChunTextArea -> 返回文档。
-
Level 3 (模糊):输入是 textarea 吗?转小写查表 -> 命中。
-
Level 4 (推断):输入是 Button?尝试补全前缀 ChunButton -> 命中。
返回数据的玄机:
我们在返回文档时,不仅仅返回 props 定义,还注入了 Meta 信息。这告诉 AI:“虽然你查的是 A,但我给你的是 B(它是 A 的底层实现)”。这能有效防止 AI 产生幻觉。
// Tool 实现片段
const responseText = {
_meta: {
query: inputName, // AI 查的词: "TextArea"
resolvedTo: targetCoreName, // 实际解析为: "ChunTextArea"
source: "alias_mapping"
},
...doc // 展开 props 定义
};
3.3 兜底工具:源码降级 (read_component_source)
如果组件文档未生成(比如刚写的新组件),或者是第三方库(如 Antd),AI 该怎么办?
我们提供了一个“文件系统穿透”工具。它不仅能读取 vendor 下的自研代码,还能通过白名单机制读取 node_modules。
策略设计:
-
输入:filePath (相对路径)
-
自动寻址:
-
如果是 chun-ui/src/... -> 去 vendor 目录找。
-
如果是 antd/lib/... -> 去 node_modules 目录找。
-
这给了 AI 极其强大的自救能力。当文档查询失败时,Prompt 会引导 AI:“请尝试直接读取 node_modules 下的 .d.ts 类型定义文件。”
通过这三个工具,我们形成了一个完美的闭环:
获取上下文 (get_page_detail) -> 拿到组件名 TextArea。
智能查询 (get_component_props) -> 映射为 ChunTextArea 并获取文档。
极端兜底 (read_component_source) -> 文档缺失时直接读源码。
这套 Tool-Only 架构,使得我们的 MCP Server 能够适配目前市面上几乎所有的 AI IDE,稳健且高效。
四、优化 AGENTS.md,让 AI 遵守规则
如果说 index.js 是 MCP Server 的“手脚”,那么 AGENTS.md 就是它的“大脑”。
在低代码开发中,AI 最容易犯两个错误:
-
瞎猜属性:因为不知道组件的真实定义,凭空捏造 style 或 className(而低代码引擎可能只支持 customProps)。
-
逻辑混乱:分不清“数据库里的配置”和“代码里的覆写”谁优先级更高。
我们需要编写一份工业级的提示词协议,强制 AI 遵守我们的游戏规则。
4.1 确立“工具优先”的原则
在 Prompt 的开头,必须明确禁止 AI 进行“无根据的推断”。我们采用 Tool-Only Mode,意味着一切信息获取必须经过工具。
Prompt 策略:
❌ 禁止:禁止 AI 凭借训练数据中的通用 React 知识来猜测组件属性。
✅ 强制:遇到组件,必须调用 get_component_props。
4.2 定义别名解析策略 (Alias Strategy)
这是配合我们第三节“四级跳查询”的关键。我们需要告诉 AI:“你看到的 TextArea 只是个代号,不要慌,直接查,我会告诉你它的真名。”
Prompt 片段:
“遇到的 TextArea 等简写时,无需猜测。直接调用 get_component_props(name="TextArea")。工具会自动将其映射为 ChunTextArea 并返回标准文档。请根据返回结果中的 _meta.resolvedTo 字段来理解组件的真实身份。”
4.3 规范页面分析逻辑 (ChunParser)
这是低代码场景中最复杂的逻辑:上下文合并 (Context Merge)。 页面配置一部分存在数据库里(Schema),另一部分写在前端代码里(Overrides)。AI 必须懂得如何将这两者合并。
SOP 设计:
-
触发:看到 ChunPageParser pageCode="..."。
-
动作:立即调用 get_page_detail_by_code 抓取 Schema。
-
合并:代码中的 customProps 优先级高于 Schema。如果代码里写了 readonly: true,哪怕数据库里是 false,也要以代码为准。
4.4 最终产物:AGENTS.md 模板
将上述逻辑整合,我们得到了这份可以直接投喂给 Trae/OpenCode 的最终版协议:
# 🤖 Chun 低代码开发助手指南
你是一个专业的低代码开发专家,连接了一个标准的 MCP Server。为了防止幻觉,请严格遵守以下协议。
---
## 📚 1. 组件查阅协议 (Component Lookup)
### 核心原则
在编写或分析组件配置时,**严禁猜测**属性名。你必须通过工具获取准确定义。
### 标准作业流程 (SOP)
#### 🟢 第一步:查阅组件
**场景**: 需要了解组件属性(如 `ChunButton` 或业务别名 `TextArea`)时。
**动作**:
1. **调用工具**: `get_component_props(name="TextArea")`。
* *注:支持业务别名,工具会自动映射到标准组件名 (如 ChunTextArea)。*
2. **应用属性**: 观察返回 JSON 中的 `_meta.resolvedTo` 字段,基于该文档定义编写代码。
#### 🟡 第二步:源码降级
**场景**: 仅当文档查询失败,且系统提示可以降级时。
**动作**:
1. 根据建议调用 `read_component_source`。
* **内部组件**: 如 `chun-ui/src/button/index.tsx`
* **第三方组件**: 如 `antd/lib/button/index.d.ts`
---
## 📄 2. 页面分析规范 (<ChunPageParser>)
当你阅读的代码中出现 `<ChunPageParser>` 组件时,**必须**严格遵守以下步骤:
### 🛠️ 分析步骤 (Analysis Workflow)
1. **提取 pageCode**: 从属性中识别 `pageCode` (例如 "P2025...")。
2. **🚀 强制调用工具**: **立即**使用 `get_page_detail_by_code(code="...")` 获取 Schema。
* **注意**: 不要询问用户,直接查询。
3. **合并上下文 (Context Merge)**:
* **基准**: 工具返回的 `pageData` (数据库默认配置)。
* **覆盖**: 代码中的 `props` 属性。
* **优先级**: `props` > `pageData`。
### ⚠️ 特殊规则:Table 列属性覆写
略
---
## 🧩 3. 导入规范
* **Chun 组件库引用**: 必须从 `@chun-cscec/xxxx` 导入。
* ❌ `import { Button } from 'antd';`
* ✅ `import { Button } from '@chun-cscec/xxxx';`