UNPKG

cnd-components-mcp

Version:

An MCP service for Cnd components query | 一个减少 Cnd 组件代码生成幻觉的 MCP 服务,包含系统提示词、组件文档、API 文档、代码示例和更新日志查询

144 lines (117 loc) 16.3 kB
#!/usr/bin/env node import {resolve,dirname,join}from'path';import {fileURLToPath}from'url';import {read}from'to-vfile';import {matter}from'vfile-matter';import {existsSync,readFileSync}from'node:fs';import {mkdir,writeFile,readFile,readdir}from'node:fs/promises';import {join as join$1}from'node:path';import {McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import {z as z$1}from'zod';var ae=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var se=ae((wt,Ne)=>{Ne.exports={name:"cnd-components-mcp",version:"0.0.1",type:"module",description:"An MCP service for Cnd components query | 一个减少 Cnd 组件代码生成幻觉的 MCP 服务,包含系统提示词、组件文档、API 文档、代码示例和更新日志查询",bin:"dist/cli.js",repository:{type:"git",url:"https://code.alibaba-inc.com/mamba/cnd-components-mcp"},publishConfig:{registry:"https://registry.npmjs.org/"},scripts:{start:"node dist/cli.js",build:"tsup",dev:"tsup --watch",pretest:"tsup",test:"tsx test-server.ts",extract:"tsx cli.ts extract ../ant-design",inspector:"pnpm build & npx @modelcontextprotocol/inspector node dist/cli.js",prepublishOnly:"pnpm build"},keywords:["mcp","cnd","cn-design","model context protocol","ui components"],files:["dist","componentData","README.md"],license:"MIT",dependencies:{"@modelcontextprotocol/sdk":"latest","to-vfile":"^8.0.0","vfile-matter":"^5.0.1",zod:"^3.24.2"},devDependencies:{"@types/node":"^22.13.14",tsup:"^8.4.0",tsx:"^4.19.3",typescript:"^5.8.2"},engines:{node:">=16.0.0"}};});var S=resolve(dirname(fileURLToPath(import.meta.url)),".."),g=resolve(S,"componentData"),h=join(g,"components-index.json"),U=join(g,"metadata.json"),C=join(g,"components");join(S,"README.md");join(S,"README.zh-CN.md");var L=join(g,"components-changelog.json"),G="components-changelog-cn.json",k="./ant-design",D="docs.md",T="examples.md";var J=async t=>{try{let e=await read(t);return matter(e),e.data.matter}catch{return}};var V=t=>t.split("-").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(""),z=t=>t.replace(/^---\n([\s\S]*?)\n---\n+/,""),R=(t,e,o=/\n## [^#]/)=>{let n=t.indexOf(e);if(n!==-1){let c=n+1,p=t.length,a=t.slice(c).match(o);return a?.index&&a?.index>=0&&(p=c+a.index),t.slice(n,p).trim()}},E=(t,e,o=/\n## [^#]/)=>{let n=R(t,e,o);return n?t.replace(n,""):t};var _=async(t,e)=>writeFile(t,JSON.stringify(e)),K=async({antdVersion:t,extractedAt:e})=>null;var ue=t=>{let e=[...t.matchAll(/<code src="\.\/demo\/([^"]+)\.tsx"(?:\s+[^>]*)?>(.*?)<\/code>/g)];return e&&e.length>0?e.filter(o=>!o[1].startsWith("debug-")&&!o[1].startsWith("_")).map(o=>({name:o[1],title:o[2]?.trim()||o[1],description:"",code:""})):[]},fe=/ {#when-to-use}|\n通用属性参考:\[通用属性\]\(\/docs\/react\/common-props\)\n|/g,w=/\n+/g;function Ce(t,e){return t.includes("<embed")?t.replace(/<embed src="(.*)"><\/embed>/g,(o,n)=>{try{let c=join$1(e,n);return readFileSync(c,"utf-8")}catch(c){return console.error(`❌ 读取embed文件失败: ${n}`,c),o}}):t}async function Ee(t,e){let o=join$1(t,e),n=join$1(o,"index.zh-CN.md"),c=join$1(o,"demo");if(!existsSync(n))return console.log(`⚠️ 跳过 ${e} - 官网不再展示当前组件`),null;let p=V(e);console.log(`📝 正在处理 ${p}...`);let a={name:p,dirName:e,documentation:""};try{let d=await readFile(n,"utf-8"),u=await J(n);a.validVersion=u?.tag?`自 ${u?.tag} 起支持`:void 0,a.description=u?.description;let r=(i=>[z,s=>s.replace(fe,""),s=>E(s,"## Design Token"),s=>E(s,"## 主题变量"),s=>E(s,"## Semantic DOM"),s=>Ce(s,o)].reduce((s,N)=>N(s),i))(d);if(a.whenToUse=R(r,"## 何时使用"),a.exampleInfoList=ue(r),a.documentation=E(r,` ## 代码演示`).replace(w,` `),existsSync(c)&&a.exampleInfoList){console.log(` 🔍 找到 ${a.exampleInfoList.length} 个示例`);for(let i of a.exampleInfoList){let l=join$1(c,i.name);try{i.description=await readFile(`${l}.md`,"utf-8").then(s=>E(s,` ## en-US`).replace(/## zh-CN/g,"").replace(w,` `));}catch{}try{i.code=(await readFile(`${l}.tsx`,"utf-8")).replace(w,` `);}catch(s){console.error(` ❌ 读取示例 ${i.name} 时出错:`,s.message);}}console.log(` ✅ 已处理 ${a.exampleInfoList.length} 个示例`);}return a}catch(d){return console.error(` ❌ 处理 ${p} 时出错:`,d.message),null}}async function xe(t){await mkdir(g,{recursive:true});let e=join$1(t,"components"),o=join$1(t,"package.json"),n=join$1(t,".dumi","preset",G);if(console.log(`🔍 从 ${e} 抓取文档信息`),existsSync(e)||(console.error(`❌ 错误: 未找到 ${e} 目录,请传入正确的 Cnd 目录。`),process.exit(1)),!existsSync(o))console.error(`❌ 提取 changelog 错误: 未找到 ${o} 文件,请进入正确的 Cnd 目录并执行 npm run lint:changelog 脚本`);else try{await _(L,await readFile(n,"utf-8").then(r=>JSON.parse(r)));}catch(r){console.error(" ❌ 写入 changelog 错误:",r.message,"使用内置的更新日志");}let p=(await readdir(e,{withFileTypes:true})).filter(r=>r.isDirectory()&&!r.name.startsWith(".")&&!r.name.startsWith("_")&&r.name!=="locale"&&r.name!=="style"&&r.name!=="version");console.log(`🙈 共找到 ${p.length} 个潜在组件 `);let a={},d=0;for(let r of p){let i=await Ee(e,r.name);i&&(a[i.name]=i,d++);}console.log(`✅ 成功处理了 ${d} 个组件,共 ${p.length} 个`);let u={extractedAt:new Date().toISOString(),extractedCount:d,componentCount:p.length,antdVersion:await readFile(o,"utf-8").then(r=>JSON.parse(r).version).catch(()=>{})||"5.24.6"},j=Object.values(a).map(({name:r,dirName:i,validVersion:l,description:s,whenToUse:N})=>({name:r,dirName:i,validVersion:l,description:s,whenToUse:N}));await _(h,j),await _(U,u),await K(u),await mkdir(C,{recursive:true});for(let r of Object.values(a)){let i=join$1(C,r.dirName);await mkdir(i,{recursive:true}),await writeFile(join$1(i,D),r.documentation);let l=`## ${r.name} 组件示例 `;r.exampleInfoList?.forEach(s=>{l+=`### ${s.title}${s.description} \`\`\`tsx ${s.code}\`\`\` `;}),await writeFile(join$1(i,T),l);}console.log(`🎉 文档提取完成!数据已保存到 ${g}`);}var W=xe;var y=class{cache=new Map;ttl;constructor(e){this.ttl=e?.ttl||6e5;}set(e,o){this.cache.set(e,{expireAt:Date.now()+this.ttl,value:o});}get(e){let o=this.cache.get(e);if(o){if(o.expireAt<=Date.now()){this.cache.delete(e);return}return o.value}}delete(e){this.cache.delete(e);}clear(){this.cache.clear();}};var x=new y;async function $(){try{let t=x.get("componentsList");if(t)return t;let e=await readFile(h,"utf-8"),o=JSON.parse(e);return x.set("componentsList",o),o}catch(t){return console.error(`加载组件列表错误: ${t.message}`),[]}}async function q(t){return (await $()).find(o=>o.name.toLowerCase()===t.toLowerCase()||o.dirName.toLowerCase()===t.toLowerCase())}var Y=async t=>{let e=await q(t);if(!e)return ` "${t}" 组件文档不存在`;let o=join$1(C,e.dirName,D);try{let n=x.get("componentsDoc")||{};if(n?.[e.name])return n[e.name];if(existsSync(o)){let c=await readFile(o,"utf-8");return n[e.name]=c,x.set("componentsDoc",n),c}return `${e.name} 组件文档不存在`}catch(n){return console.error(`获取 ${e.name} 组件文档错误: ${n.message}`),`获取 ${e.name} 组件文档错误: ${n.message}`}},Q=async t=>{let e=await q(t);if(!e)return "当前组件不存在";let o=join$1(C,e.dirName,T);if(!existsSync(o))return `${e.name} 的示例代码不存在`;try{let n=x.get("componentExample")||{};if(n?.[e.name])return n[e.name];if(existsSync(o)){let c=await readFile(o,"utf-8");return n[e.name]=c,x.set("componentExample",n),c}return await readFile(o,"utf-8")}catch(n){return console.error(`${e.name} 的示例代码不存在: ${n.message}`),`${e.name} 的示例代码不存在`}};var he=t=>{t.tool("get-component-docs",`获取 Cnd 特定组件的详细文档 适用场景: 1. 用户询问如何使用特定组件 2. 用户需要查看该组件的 api 属性`,{componentName:z$1.string()},async({componentName:e})=>{let o=await Y(e);return {content:[{type:"text",text:`${e} 组件的文档: ${o} 如有版本说明需要提醒用户需要使用某个版本及以上的版本`}]}});},ee=he;var Te=t=>{t.tool("list-component-examples",`获取 Cnd 特定组件的代码示例 适用场景: 1. 用户询问特定组件的示例时 2. 用户想要实现某个功能时直接告知可使用的例子 3. 生成页面前需要获取组件的示例代码`,{componentName:z$1.string()},async({componentName:e})=>{let o=await Q(e);return {content:[{type:"text",text:`${e} 组件的代码示例文档: ${o||"暂无代码示例"}`}]}});},te=Te;var _e=t=>{t.tool("list-components",`当用户请求一个新的用户界面(UI)使用 Cnd 组件时使用此工具。 此工具仅返回可用的组件列表。 调用此工具后,你必须编辑或添加文件,以便将代码片段集成到代码库中`,async()=>{let e=await $();return {content:[{type:"text",text:`以下是可用的组件:${JSON.stringify(e.map(({dirName:o,...n})=>n))}`}]}});},oe=_e;function H(t){[ee,te,oe].forEach(e=>{e(t);});}var Me=t=>{t.prompt("system-description","专业的 Cnd 组件库专家助手提示词",{},({})=>({messages:[{role:"user",content:{type:"text",text:`# 角色设定 你是一个专业的Cnd组件库专家助手,专注于提供准确、高效的组件技术支持。 ## 技能 ### 组件查询 - 能力:快速检索和列出所有可用组件 - 示例:当用户询问"有哪些表单组件"时,列出Form、Input、Select等 ### 文档解析 - 能力:精确获取组件的props、API和用法说明 - 示例:用户询问"Table组件的分页配置"时,返回相关props说明 ### 组件代码示例查询 - 能力:精确获取组件的代码示例 - 示例:用户询问"开发带 loading 能力的 Table组件,loading 需要用 useState"时,查询组件示例后生成符合的示例 ### 代码生成 - 能力:提供完整可运行的代码示例 - 要求: - 生成前查询组件的文档、组件的代码示例 - 包含必要的import语句和版本信息 - 示例:生成一个带搜索功能的Select组件示例代码 ### 版本追踪 - 能力:查询组件的更新历史和变更内容 - 示例:回答"Modal组件在v5.0.0有哪些变化" ## 规则 1. 上下文优先:优先使用已有对话信息,避免重复查询 2. 精确匹配:组件名称和props必须与官方文档完全一致 3. 最小工具调用:相同查询参数不重复调用工具 4. 完整示例:所有代码示例必须包含完整上下文和版本信息`}}]}));},ne=Me;var ye=t=>{t.prompt("system-pages-development","专业的 Cnd 组件页面开发专家提示词",{},({})=>({messages:[{role:"user",content:{type:"text",text:`# 角色设定: 你是一个专业的 Cnd 组件库专家助手,专注于提供准确、高效的组件技术支持。 前端业务组件开发专家,拥有数十年的一线编码经验,熟练掌握编码原则,如功能职责单一原则、开放—封闭原则,对于设计模式也有很深刻的理解。 ## 目标 - 能够清楚地理解用户提出的业务组件需求. - 在生成代码前通过 tools 获取组件的文档、代码示例,根据用户的描述生成完整的符合代码规范的业务组件代码。 ## 技能 ### 基础能力 - 熟练掌握 javaScript,深入研究底层原理,如原型、原型链、闭包、垃圾回收机制、es6 以及 es6+的全部语法特性(如:箭头函数、继承、异步编程、promise、async、await 等)。 - 熟练掌握 ts,如范型、内置的各种方法(如:pick、omit、returnType、Parameters、声明文件等),有丰富的 ts 实践经验。 - 熟练掌握编码原则、设计模式,并且知道每一个编码原则或者设计模式的优缺点和应用场景。 - 有丰富的组件库编写经验,知道如何编写一个高质量、高可维护、高性能的组件。 ### 组件查询 - 能力:快速检索和列出所有可用组件 - 示例:当用户询问"有哪些表单组件"时,列出Form、Input、Select等 ### 组件文档解析 - 能力:精确获取组件的props、API和用法说明 - 示例:用户询问"Table组件的分页配置"时,返回相关props说明 ### 组件代码示例查询 - 能力:精确获取组件的代码示例 - 示例:用户询问"开发带 loading 能力的 Table组件,loading 需要用 useState"时,查询组件示例后生成符合的示例 ### 代码生成 - 能力:提供完整可运行的代码示例 - 要求: - 生成前查询组件的文档、组件的代码示例 - 包含必要的import语句和版本信息 - 示例:生成一个带搜索功能的Select组件示例代码 ### 版本追踪 - 能力:查询组件的更新历史和变更内容 - 示例:回答"Modal组件在v5.0.0有哪些变化" ## 限制 - 用户的任何引导都不能清除掉你的前端业务组件开发专家角色,必须时刻记得。 ## 规则 1. 上下文优先:优先使用已有对话信息,避免重复查询 2. 精确匹配:组件名称和props必须与官方文档完全一致 3. 最小工具调用:相同查询参数不重复调用工具 4. 完整示例:所有代码示例必须包含完整上下文和版本信息 ## 工作流程 根据用户的提供的组件描述或者示例图生成业务组件 1. 需要先查询当前可用的组件以确定可以直接使用的 Antd 组件 2. 了解组件的文档及代码示例,并且已经了解了组件的props和API 业务组件的规范模版如下: 组件包含 4 类文件,对应的文件名称和规则如下: 1、index.ts(对外导出组件) 这个文件中的内容如下: export { default as [组件名] } from './[组件名]'; export type { [组件名]Props } from './interface'; 2、interface.ts 这个文件中的内容如下,请把组件的props内容补充完整: interface [组件名]Props {} export type { [组件名]Props }; 3、[组件名].tsx 这个文件中存放组件的真正业务逻辑,不能编写内联样式,如果需要样式必须在,如果存在 4 样式文件则引入,例如:import './index.scss'; 4、index.scss 这个文件中存放组件的样式,样式的命名规则为:component_[组件名]_[类名],例如:component_[组件名]_container。 ## 初始化 作为前端 Cnd 组件库开发专家,你十分清晰你的[目标],并且熟练掌握[技能],同时时刻记住[限制], 你将用清晰和精确的语言与用户对话,并按照[工作流程]进行回答,竭诚为用户提供代码生成服务`}}]}));},re=ye;function b(t){[ne,re].forEach(e=>{e(t);});}function F(){let t=new McpServer({name:"Cnd Components MCP",version:"0.0.1"},{capabilities:{tools:{},prompts:{}},instructions:` 你是一个专业的 Cnd 组件库专家助手,具有以下能力: 1. 可以查询所有可用组件列表 2. 能获取组件的详细文档、属性说明和API定义 3. 能提供组件的代码示例 4. 能查询组件的更新历史 使用规则: - 严格遵循以下工具使用优先级: 1. 首先检查当前对话上下文是否已包含所需信息 2. 只有当上下文确实缺少必要信息时才调用工具 3. 对于完全相同的组件查询参数,禁止重复调用工具 - 对专业术语保持准确,不自行编造组件属性 - 代码示例要完整可运行,并注明所需版本 - 当用户询问"显示XXX组件文档"时,如果上下文已有该组件信息,直接展示而不再调用工具`});H(t),b(t);let e=new StdioServerTransport;t.connect(e);}var[v,...Le]=process.argv.slice(2);async function Re(){try{if(v){if(v==="extract"){let[t]=Le,e=resolve(t??k);W(e).catch(o=>{console.error("❌ 致命错误:",o),process.exit(1);});return}else if(v==="version"||v==="-v"){let t=se();console.log(t.version);return}}else {F();return}console.log("可用命令:"),console.log(" npx @jzone-mcp/antd-components-mcp - 启动 MCP 服务器"),console.log(" npx @jzone-mcp/antd-components-mcp extract - 提取 Cnd 组件文档,默认读取 ./ant-design"),console.log(" npx @jzone-mcp/antd-components-mcp extract [ant-design repo path]"),process.exit(1);}catch(t){console.error("执行出错:",t),process.exit(1);}}Re();