#!/usr/bin/env node
/**
* m8-generator-mcp - MCP Server 入口
*
* 提供 M8 框架开发辅助工具(对外暴露):
* - create_page: 一站式页面生成工具
* - create_ioc_page: IOC 低码组件生成工具
* - get_prompt: 获取 AI 配置的最佳 System Prompt
* - validate_code_standards: 验证代码是否符合 M8 规范
*
* 内部辅助函数(不对外暴露):
* - getComponentDoc: 获取组件文档
* - getStandardDocs: 获取规范文档
* - getUtilDocs: 获取工具函数文档
* - getStandardRules: 获取规范规则
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import {
loadKnowledgeBase,
getComponentDoc,
getStandardDocs,
getUtilDocs,
getStandardRules,
getStandardCategories,
} from "./knowledge/index.js";
import { generateModuleFiles } from "./tools/generate_module_structure.js";
import {
generateIocComponentFiles,
validateComponentName,
} from "./tools/generate_ioc_component.js";
import {
CORE_STANDARDS,
generateChecklist,
} from "./knowledge/standards-summary.js";
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs/promises";
import * as path from "path";
const execAsync = promisify(exec);
// 创建 MCP Server
const server = new McpServer({
name: "m8-generator-mcp",
version: "1.0.0",
});
// 加载知识库
const knowledgeBase = loadKnowledgeBase();
// ============================================================
// 内部辅助函数(不对外暴露为 MCP 工具)
// ============================================================
/**
* 内部函数:获取组件用法
* 获取指定 UI 组件的详细用法、Props 和示例
*/
function internalGetComponentUsage(componentName: string): string {
const doc = getComponentDoc(knowledgeBase, componentName);
if (!doc) {
return `未找到组件 "${componentName}" 的文档。\n\n可用组件列表:\n${knowledgeBase.components.map((c) => `- ${c.name}`).join("\n")}`;
}
const standards = getStandardDocs(knowledgeBase);
return `# ${doc.name} 组件用法\n\n${doc.description}\n\n## Props\n\n${doc.props}\n\n## Events\n\n${doc.events}\n\n## 示例代码\n\n${doc.examples.join("\n\n---\n\n")}\n\n---\n\n## M8 开发规范提醒\n\n${standards}`;
}
/**
* 内部函数:搜索 Util 工具函数
*/
function internalSearchUtils(keyword: string): string {
const utilDocs = getUtilDocs(knowledgeBase, keyword);
if (!utilDocs) {
return `未找到与 "${keyword}" 相关的 Util 工具函数。\n\n可用的工具函数文档:\n- Ajax 与文件上传 (ajax, upload)\n- 工具函数库 (string, os 等)`;
}
return utilDocs;
}
/**
* 内部函数:获取 M8 开发规范
*/
function internalGetStandardRules(category: string): string {
const rules = getStandardRules(knowledgeBase, category);
if (!rules) {
const categories = getStandardCategories();
return `未找到 "${category}" 相关规范。\n\n可用的规范类别:\n${categories.map((c) => `- ${c}`).join("\n")}`;
}
return rules;
}
/**
* 内部函数:获取完整的 M8 开发规范
*/
function internalGetAllStandards(
vueVersion?: "2" | "3",
includeChecklist: boolean = true,
): string {
const fullStandards = getStandardDocs(knowledgeBase);
let summaryOutput = `
################################################################################
## 🔴 M8 开发规范摘要(必须严格遵循)
################################################################################
`;
for (const [, section] of Object.entries(CORE_STANDARDS)) {
summaryOutput += `### ${section.title}\n`;
for (const rule of section.rules) {
summaryOutput += `- ${rule}\n`;
}
summaryOutput += "\n";
}
if (vueVersion) {
summaryOutput += `
################################################################################
## ⚠️ 当前目标版本:Vue ${vueVersion}
################################################################################
`;
if (vueVersion === "2") {
summaryOutput += `
### Vue2 特定要求:
- 使用 Options API(data、methods、computed、watch)
- data 必须是返回对象的函数
- 样式穿透使用 ::v-deep .class
- 状态管理使用 Vuex (store.js)
- 获取路由参数:Util.getExtraDataByKey(key)
- Props 使用对象形式定义,包含 type 和 default
- 数组和对象的 default 必须是工厂函数:() => []
`;
} else {
summaryOutput += `
### Vue3 特定要求:
- 必须使用 <script setup> 语法
- 使用 TypeScript 时添加 lang="ts"
- 使用 defineProps 和 withDefaults 定义 Props
- 使用 defineEmits 定义事件
- 使用 ref() 和 reactive() 定义响应式数据
- 样式穿透使用 :deep(.class) 语法
- 状态管理使用 Pinia (store.ts)
- 获取路由参数:onLoad((options) => {})
`;
}
}
let checklist = "";
if (includeChecklist) {
checklist = generateChecklist(vueVersion);
}
return (
summaryOutput +
"\n\n" +
fullStandards +
(checklist ? "\n\n" + checklist : "")
);
}
// ============================================================
// 对外暴露的 MCP 工具(仅 3 个)
// ============================================================
/**
* 工具 1: validate_code_standards(对外暴露)
* 验证代码是否符合 M8 规范
*/
server.tool(
"validate_code_standards",
"验证给定的代码片段是否符合 M8 开发规范,返回不符合规范的问题列表和修改建议。",
{
code: z.string().describe("需要验证的代码内容"),
file_type: z
.enum(["vue", "scss", "js", "ts", "mock", "router"])
.describe("文件类型"),
vue_version: z.enum(["2", "3"]).optional().describe("Vue 版本,默认 3"),
},
async ({
code,
file_type,
vue_version = "3",
}: {
code: string;
file_type: string;
vue_version?: "2" | "3";
}) => {
const issues: string[] = [];
const suggestions: string[] = [];
// 通用检查
if (code.includes("\t")) {
issues.push("❌ 使用了 tab 缩进");
suggestions.push("✅ 改用 4 个空格缩进");
}
if (code.includes("var ")) {
issues.push("❌ 使用了 var 声明变量");
suggestions.push("✅ 使用 const 或 let 替代");
}
if (code.match(/console\.log\(\s*\)/)) {
issues.push("❌ 存在空的 console.log()");
suggestions.push("✅ console.log 必须包含描述信息");
}
// Vue 文件检查
if (file_type === "vue") {
// 头部注释检查
if (
!code.includes("@作者") ||
!code.includes("@创建时间") ||
!code.includes("@版权")
) {
issues.push("❌ 缺少完整的头部注释");
suggestions.push(
"✅ 添加包含 @作者、@创建时间、@修改时间、@版本、@版权、@描述 的头部注释",
);
}
// 全局变量 import 检查
if (
code.includes("import { Config }") ||
code.includes("import { Util }") ||
code.includes("import { ejs }")
) {
issues.push("❌ 错误地 import 了全局变量(Config/Util/ejs)");
suggestions.push(
"✅ Config、Util、ejs 是全局变量,直接使用即可,禁止 import",
);
}
// 样式分离检查
if (code.match(/<style[^>]*>[\s\S]*?[^@import][\s\S]*?<\/style>/)) {
const styleMatch = code.match(/<style[^>]*>([\s\S]*?)<\/style>/);
if (
styleMatch &&
!styleMatch[1].trim().startsWith("@import") &&
styleMatch[1].trim().length > 50
) {
issues.push("❌ Vue 文件中直接编写了 CSS 代码");
suggestions.push(
"✅ 样式应放在 css/ 目录下的 .scss 文件中,使用 @import 引入",
);
}
}
// scoped 检查
if (code.includes("<style") && !code.includes("scoped")) {
issues.push("❌ style 标签缺少 scoped 属性");
suggestions.push("✅ 添加 scoped 属性限制样式作用域");
}
// Vue3 特定检查
if (vue_version === "3") {
if (
code.includes("<script>") &&
!code.includes("<script setup") &&
!code.includes('<script lang="ts" setup')
) {
issues.push("❌ Vue3 未使用 <script setup> 语法");
suggestions.push(
'✅ Vue3 必须使用 <script setup> 或 <script lang="ts" setup>',
);
}
if (code.includes("::v-deep")) {
issues.push("❌ Vue3 使用了错误的样式穿透语法 ::v-deep");
suggestions.push("✅ Vue3 样式穿透应使用 :deep(.class)");
}
}
// Vue2 特定检查
if (vue_version === "2") {
if (code.includes(":deep(")) {
issues.push("❌ Vue2 使用了错误的样式穿透语法 :deep()");
suggestions.push("✅ Vue2 样式穿透应使用 ::v-deep .class");
}
if (code.includes("<script setup")) {
issues.push("❌ Vue2 不支持 <script setup> 语法");
suggestions.push("✅ Vue2 应使用 Options API");
}
}
}
// SCSS 文件检查
if (file_type === "scss") {
// BEM 命名检查
if (code.match(/\.[A-Z]/)) {
issues.push("❌ CSS 类名使用了大写字母");
suggestions.push("✅ class 应使用小写字母 + 中划线,遵循 BEM 规范");
}
// !important 检查
if (code.includes("!important")) {
issues.push("⚠️ 使用了 !important");
suggestions.push("✅ 避免使用 !important,除非覆盖第三方库样式");
}
// 深层嵌套检查
const nestingLevel = (code.match(/\{/g) || []).length;
if (nestingLevel > 10) {
issues.push("⚠️ 可能存在过深的嵌套");
suggestions.push("✅ CSS 嵌套不应超过 3 层");
}
}
// Mock 文件检查
if (file_type === "mock") {
if (!code.includes("import Mock from '@mock'")) {
issues.push("❌ Mock 文件未正确导入 Mock");
suggestions.push("✅ 使用 import Mock from '@mock' 导入");
}
if (!code.includes("resultData") || !code.includes("export default")) {
issues.push("❌ Mock 文件结构不正确");
suggestions.push("✅ 必须导出 resultData 数组");
}
}
// Router 文件检查
if (file_type === "router") {
if (code.includes("import") && code.includes(".vue")) {
issues.push("❌ router.js 中 import 了 Vue 组件");
suggestions.push("✅ router.js 中无需 import 组件");
}
if (!code.includes("routes") || !code.includes("export default")) {
issues.push("❌ 路由文件结构不正确");
suggestions.push("✅ 必须导出包含 routes 数组的对象");
}
}
// TypeScript 检查
if (file_type === "ts") {
if (code.includes(": any")) {
issues.push("❌ 使用了 any 类型");
suggestions.push("✅ 禁止使用 any,使用具体类型或 unknown");
}
if (code.includes("enum ")) {
issues.push("⚠️ 使用了 enum 枚举");
suggestions.push("✅ 建议使用 const 对象 + as const 替代枚举");
}
}
// 构建输出
let output = "";
if (issues.length === 0) {
output = "✅ 代码符合 M8 开发规范,未发现问题。";
} else {
output = `## 🔍 规范检查结果\n\n发现 ${issues.length} 个问题:\n\n`;
output += "### 问题列表\n";
issues.forEach((issue, i) => {
output += `${i + 1}. ${issue}\n`;
});
output += "\n### 修改建议\n";
suggestions.forEach((suggestion, i) => {
output += `${i + 1}. ${suggestion}\n`;
});
}
return {
content: [
{
type: "text" as const,
text: output,
},
],
};
},
);
/**
* 工具 2: create_page(对外暴露)
* 一站式页面生成工具,内置前置流程,用户只需描述需求即可
*/
server.tool(
"create_page",
"【推荐使用】一站式 M8 页面生成工具。自动完成:1.加载完整规范 2.生成代码 3.验证规范。用户只需描述页面需求即可,无需手动调用其他工具。",
{
module_name: z
.string()
.describe("模块名称(小写下划线格式,如 user_list、order_detail)"),
page_name: z.string().optional().describe("页面名称,默认 index"),
page_type: z
.enum(["form", "list", "detail", "custom"])
.optional()
.describe(
"页面类型:form(表单)、list(列表)、detail(详情)、custom(自定义),默认 custom",
),
vue_version: z
.enum(["2", "3"])
.describe(
"Vue 版本,必填。请根据项目 package.json 中的 vue 依赖版本填写:2.x.x 填 '2',3.x.x 填 '3'",
),
description: z
.string()
.describe("页面功能描述,如:用户登录页面、商品列表、订单详情等"),
requirements: z
.string()
.optional()
.describe(
"具体需求描述,如:包含用户名密码输入框、支持下拉刷新分页、显示订单状态等",
),
author: z.string().optional().describe("作者名称,默认 developer"),
components: z
.array(z.string())
.optional()
.describe("需要使用的组件列表,如 ['em-form', 'em-field', 'em-button']"),
skip_mock: z
.boolean()
.optional()
.describe("是否跳过生成 mock.js,默认 false"),
},
async ({
module_name,
page_name = "index",
page_type = "custom",
vue_version,
description,
requirements,
author = "developer",
components,
skip_mock = false,
}: {
module_name: string;
page_name?: string;
page_type?: "form" | "list" | "detail" | "custom";
vue_version: "2" | "3";
description: string;
requirements?: string;
author?: string;
components?: string[];
skip_mock?: boolean;
}) => {
let output = "";
// ============================================================
// 步骤 1:加载完整规范
// ============================================================
output += `# 🚀 M8 页面生成报告\n\n`;
output += `## 📋 步骤 1:加载开发规范\n\n`;
output += `已加载 M8 完整开发规范(01-project 到 07-router,共 15 个规范文件)\n`;
output += `目标 Vue 版本:**Vue ${vue_version}**\n\n`;
// 获取版本特定规范摘要
const vueSpecificRules =
vue_version === "2"
? `
### Vue2 规范要点:
- 使用 Options API(data、methods、computed、watch)
- data 必须是返回对象的函数
- 样式穿透:\`::v-deep .class\`
- 状态管理:Vuex (store.js)
- 获取路由参数:\`Util.getExtraDataByKey(key)\`
`
: `
### Vue3 规范要点:
- 必须使用 \`<script setup>\` 语法
- 使用 TypeScript:\`<script lang="ts" setup>\`
- 样式穿透:\`:deep(.class)\`
- 状态管理:Pinia (store.ts)
- 获取路由参数:\`onLoad((options) => {})\`
`;
output += vueSpecificRules + "\n";
// ============================================================
// 步骤 2:生成代码
// ============================================================
output += `## 📦 步骤 2:生成模块代码\n\n`;
output += `- 模块名:\`${module_name}\`\n`;
output += `- 页面名:\`${page_name}\`\n`;
output += `- 页面类型:${page_type}\n`;
output += `- 功能描述:${description}\n`;
if (requirements) {
output += `- 具体需求:${requirements}\n`;
}
output += `\n`;
// 生成文件
const files = generateModuleFiles({
moduleName: module_name,
pageName: page_name,
vueVersion: vue_version,
author: author,
description: description,
skipMock: skip_mock,
});
// 输出目录结构
output += `### 目录结构\n\n\`\`\`\nsrc/pages/${module_name}/\n`;
for (const file of files) {
output += `├── ${file.path}\n`;
}
output += `\`\`\`\n\n`;
// 输出文件内容
output += `### 文件内容\n\n`;
for (const file of files) {
const ext = file.path.split(".").pop() || "";
output += `#### ${file.path}\n\n\`\`\`${ext}\n${file.content}\n\`\`\`\n\n`;
}
// ============================================================
// 步骤 3:组件用法提示(如果指定了组件)
// ============================================================
if (components && components.length > 0) {
output += `## 🧩 步骤 3:相关组件用法\n\n`;
for (const compName of components) {
const doc = getComponentDoc(knowledgeBase, compName);
if (doc) {
output += `### ${doc.name}\n\n`;
output += `${doc.description}\n\n`;
output += `**Props 摘要**:\n${doc.props.substring(0, 500)}...\n\n`;
}
}
}
// ============================================================
// 步骤 4:规范验证提示
// ============================================================
output += `## ✅ 步骤 ${components && components.length > 0 ? "4" : "3"}:规范检查清单\n\n`;
output += generateChecklist(vue_version);
output += `\n`;
// ============================================================
// 附加:完整规范参考
// ============================================================
output += `---\n\n`;
output += `## 📚 附录:M8 开发规范摘要\n\n`;
// 添加核心规范摘要
for (const [, section] of Object.entries(CORE_STANDARDS)) {
output += `### ${section.title}\n`;
// 只显示前 5 条规则,避免输出过长
const rulesToShow = section.rules.slice(0, 5);
for (const rule of rulesToShow) {
output += `- ${rule}\n`;
}
if (section.rules.length > 5) {
output += `- ...(共 ${section.rules.length} 条规则)\n`;
}
output += "\n";
}
// 页面类型特定提示
if (page_type !== "custom") {
output += `---\n\n## 💡 ${page_type} 页面开发提示\n\n`;
if (page_type === "form") {
output += `### 表单页面要点:\n`;
output += `- 使用 \`em-form\` 包裹表单,配合 \`@submit\` 和 \`@failed\` 事件\n`;
output += `- 输入框使用 \`em-field\`,必须设置 \`placeholder\`\n`;
output += `- readonly 状态必须同时添加 \`is-link\` 和 \`clickable\`\n`;
output += `- 日期选择优先使用 \`ejs.ui.pickDate()\` 而非组件\n`;
output += `- **提交按钮**:\`em-button\` 必须使用 \`native-type="submit"\`(非 form-type)\n`;
output += `- **textarea**:必须添加 \`:autosize="{ minHeight: 24 }"\` 属性\n\n`;
output += `### ⚠️ em-field 中使用特殊组件必须用 #input 插槽\n\n`;
output += `以下组件在 \`em-field\` 中使用时,**必须**用 \`<template #input>\` 包裹:\n`;
output += `- em-radio-group / em-radio(单选框)\n`;
output += `- em-checkbox-group / em-checkbox(复选框)\n`;
output += `- em-switch(开关)\n`;
output += `- em-uploader(文件上传)\n`;
output += `- em-stepper(步进器)\n`;
output += `- em-rate(评分)\n`;
output += `- em-slider(滑块)\n\n`;
output += `### em-radio 单选框正确用法:\n`;
output += `\`\`\`vue\n`;
output += `<!-- ✅ 正确:使用 #input 插槽 -->\n`;
output += `<em-field name="leaveType" label="请假类型">\n`;
output += ` <template #input>\n`;
output += ` <em-radio-group v-model="formData.leaveType" direction="horizontal">\n`;
output += ` <em-radio name="personal">事假</em-radio>\n`;
output += ` <em-radio name="sick">病假</em-radio>\n`;
output += ` <em-radio name="annual">年假</em-radio>\n`;
output += ` </em-radio-group>\n`;
output += ` </template>\n`;
output += `</em-field>\n`;
output += `\n`;
output += `<!-- ❌ 错误:直接作为子元素 -->\n`;
output += `<em-field name="leaveType" label="请假类型">\n`;
output += ` <em-radio-group v-model="formData.leaveType">\n`;
output += ` <em-radio name="personal">事假</em-radio>\n`;
output += ` </em-radio-group>\n`;
output += `</em-field>\n`;
output += `\`\`\`\n\n`;
output += `### API 请求规范:\n`;
output += `- 使用 \`Util.ajax\` 发起请求,配合 \`dataPath: 'custom'\`\n`;
output += `- **成功状态码判断**:\`res?.status?.code === 1\` 代表成功(非 0)\n`;
output += `- 请求参数不要有拖尾逗号\n\n`;
output += `### 表单校验规范:\n\n`;
output += `#### ⚠️ 重要:validator 函数必须定义在 methods/script 中\n`;
output += `- **禁止**在模板的 \`:rules\` 中直接使用 \`Util.string.isMobile(val)\`\n`;
output += `- **必须**先在 methods(Vue2)或 script(Vue3)中定义校验函数\n`;
output += `- 然后在 \`:rules\` 中引用该函数\n\n`;
output += `#### 1. 必填校验\n`;
output += `- 使用 \`em-field\` 的 \`required\` 属性标记必填字段\n`;
output += `- 配合 \`rules\` 属性设置校验规则:\`[{ required: true, message: '请输入xxx' }]\`\n`;
output += `- 提交时通过 \`@failed\` 事件捕获校验失败信息\n\n`;
output += `#### 2. 常规字段校验(使用 Util.string)\n`;
output += `- 手机号验证:\`Util.string.isMobile(value)\` - 校验 11 位手机号格式\n`;
output += `- 身份证验证:\`Util.string.isIdCard(value)\` - 校验 15/18 位身份证号\n`;
output += `- 邮箱验证:\`Util.string.isEmail(value)\` - 校验邮箱格式\n`;
output += `- 银行卡验证:\`Util.string.isBankCard(value)\` - 校验银行卡号格式\n`;
output += `- 中文姓名验证:\`Util.string.isChineseName(value)\` - 校验 2-10 位中文字符\n`;
output += `- 车牌号验证:\`Util.string.isCarNumber(value)\` - 校验车牌号格式\n\n`;
output += `#### 3. 其他字段校验(使用正则表达式)\n`;
output += `- 纯数字:\`/^\\d+$/\`\n`;
output += `- 金额(两位小数):\`/^\\d+(\\.\\d{1,2})?$/\`\n`;
output += `- 正整数:\`/^[1-9]\\d*$/\`\n`;
output += `- 字母数字组合:\`/^[a-zA-Z0-9]+$/\`\n`;
output += `- URL 地址:\`/^https?:\\/\\/[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$/\`\n`;
output += `- 邮政编码:\`/^\\d{6}$/\`\n`;
output += `- 固定电话:\`/^0\\d{2,3}-?\\d{7,8}$/\`\n\n`;
output += `#### 4. 时间范围校验\n`;
output += `- 开始时间不能晚于结束时间,需在提交前进行校验\n`;
output += `- 示例代码:\n`;
output += `\`\`\`javascript\n`;
output += `// 时间范围校验函数\n`;
output += `function validateTimeRange(startTime, endTime) {\n`;
output += ` if (!startTime || !endTime) return true; // 非必填时允许为空\n`;
output += ` const start = new Date(startTime).getTime();\n`;
output += ` const end = new Date(endTime).getTime();\n`;
output += ` if (start > end) {\n`;
output += ` ejs.ui.toast('开始时间不能晚于结束时间');\n`;
output += ` return false;\n`;
output += ` }\n`;
output += ` return true;\n`;
output += `}\n`;
output += `\n`;
output += `// 在表单提交时调用\n`;
output += `const onSubmit = () => {\n`;
output += ` if (!validateTimeRange(formData.startTime, formData.endTime)) {\n`;
output += ` return;\n`;
output += ` }\n`;
output += ` // 继续提交逻辑...\n`;
output += `};\n`;
output += `\`\`\`\n\n`;
output += `#### 5. ✅ 正确的校验规则配置示例(Vue3)\n`;
output += `\`\`\`vue\n`;
output += `<template>\n`;
output += ` <em-form @submit="onSubmit" @failed="onFailed">\n`;
output += ` <em-field\n`;
output += ` v-model="formData.mobile"\n`;
output += ` label="手机号"\n`;
output += ` placeholder="请输入手机号"\n`;
output += ` required\n`;
output += ` :rules="mobileRules"\n`;
output += ` />\n`;
output += ` <em-field\n`;
output += ` v-model="formData.idCard"\n`;
output += ` label="身份证号"\n`;
output += ` placeholder="请输入身份证号"\n`;
output += ` required\n`;
output += ` :rules="idCardRules"\n`;
output += ` />\n`;
output += ` </em-form>\n`;
output += `</template>\n`;
output += `\n`;
output += `<script setup lang="ts">\n`;
output += `import { reactive } from 'vue';\n`;
output += `\n`;
output += `const formData = reactive({\n`;
output += ` mobile: '',\n`;
output += ` idCard: ''\n`;
output += `});\n`;
output += `\n`;
output += `// ✅ 正确:在 script 中定义校验函数\n`;
output += `const validateMobile = (val: string) => Util.string.isMobile(val);\n`;
output += `const validateIdCard = (val: string) => Util.string.isIdCard(val);\n`;
output += `\n`;
output += `// ✅ 正确:定义校验规则数组\n`;
output += `const mobileRules = [\n`;
output += ` { required: true, message: '请输入手机号' },\n`;
output += ` { validator: validateMobile, message: '手机号格式不正确' }\n`;
output += `];\n`;
output += `\n`;
output += `const idCardRules = [\n`;
output += ` { required: true, message: '请输入身份证号' },\n`;
output += ` { validator: validateIdCard, message: '身份证号格式不正确' }\n`;
output += `];\n`;
output += `</script>\n`;
output += `\`\`\`\n\n`;
output += `#### 6. ❌ 错误示例(不要这样写)\n`;
output += `\`\`\`vue\n`;
output += `<!-- ❌ 错误:在模板中直接使用 Util -->\n`;
output += `<em-field\n`;
output += ` v-model="formData.mobile"\n`;
output += ` :rules="[\n`;
output += ` { required: true, message: '请输入手机号' },\n`;
output += ` { validator: (val) => Util.string.isMobile(val), message: '格式错误' }\n`;
output += ` ]"\n`;
output += `/>\n`;
output += `<!-- 上面的写法会报错:Util is not defined on the instance -->\n`;
output += `\`\`\`\n`;
} else if (page_type === "list") {
output += `### 列表页面要点:\n`;
output += `- 使用 \`em-minirefresh\` 实现下拉刷新和分页加载\n`;
output += `- 配置 \`url\`、\`request-data\`、\`change-data\` 属性\n`;
output += `- 列表项使用 \`em-cell\` 组件\n`;
output += `- 点击跳转使用 \`ejs.page.open('./detail', { id: item.id })\`\n`;
output += `- 请求前 \`ejs.ui.showWaiting()\`,完成后 \`ejs.ui.closeWaiting()\`\n`;
} else if (page_type === "detail") {
output += `### 详情页面要点:\n`;
output += `- Vue2 获取参数:\`Util.getExtraDataByKey('id')\`\n`;
output += `- Vue3 获取参数:\`onLoad((options) => { const id = options.id })\`\n`;
output += `- 使用 \`Util.ajax()\` 加载详情数据\n`;
output += `- 加载时显示 \`ejs.ui.showWaiting()\`\n`;
output += `- 错误处理使用 \`ejs.ui.toast()\`\n`;
}
}
return {
content: [
{
type: "text" as const,
text: output,
},
],
};
},
);
/**
* 工具 3: create_ioc_page(对外暴露)
* 生成符合低码规范的 IOC 组件结构
*
* 此工具直接返回所有文件的完整代码,由 AI 助手写入到目标项目
*/
server.tool(
"create_ioc_page",
`【低码组件生成】生成符合 M8 低码平台规范的组件代码。
🔴 **重要:此工具会自动在项目根目录执行 npm run ioc create 命令创建组件!**
⚠️ **【最重要】优先使用 em-组件库!**:
- **轮播/走马灯**:必须使用 \`em-swipe\` + \`em-swipe-item\`,禁止自行实现轮播
- **图片展示**:必须使用 \`em-image\`(支持懒加载、失败占位)
- **列表/单元格**:使用 \`em-cell\` / \`em-cell-group\`
- **按钮**:使用 \`em-button\`
- **弹窗**:使用 \`em-popup\` / \`em-dialog\`
- **标签**:使用 \`em-tag\`
- **下拉刷新**:使用 \`em-minirefresh\`
- 只有在组件库没有匹配组件时,才允许自定义实现!
📋 **使用前必读 - 开发流程**:
1. **指定项目路径**:必须提供 project_path 参数,指向包含 package.json 的项目根目录
2. **自动创建组件**:工具会在项目目录下执行 \`npm run ioc create ComponentName\` 创建组件目录结构
3. **返回生成代码**:返回完善后的代码内容,由 AI 助手覆盖写入对应文件
4. **⚠️ 必须检查 base.js**:确保 module_name 与组件文件夹名一致,name 为组件中文名
📁 生成的目录结构(自动创建):
- js/base.js(🔴 必须检查:name 中文名、module_name 必须=目录名、version)
- js/config.js(属性/事件配置 - 含详细可配置项)
- mock/data.js(模拟数据 - 需与 index.vue 的数据结构匹配)
- index.vue(组件主文件 - 🔴 必须使用 em- 组件库)
- css/index.scss(样式)
- plugin/eventgenerate/index.js(事件生成插件)
- plugin/boxoptions/config.js(容器配置插件)
- index.js(组件注册入口)
📌 支持的配置项类型:
- text: 文本输入
- boolean: 开关
- color: 颜色选择器
- number: 数字输入
- image: 图片URL
- uploadimage: 图片上传(推荐)
- select: 下拉选择
- array: 数组配置(配合 dynamic:true 和 template)
💡 **配置设计建议**:
- 布局类:showTitle, showIcon, showBorder, layout, columns
- 样式类:backgroundColor, titleColor, titleSize, borderRadius
- 交互类:clickable, disabled, loading
- 数据类:使用 array 类型支持动态列表`,
{
project_path: z
.string()
.describe(
"【必填】目标项目的根目录绝对路径,必须是包含 package.json 的目录。npm run ioc create 命令将在此目录下执行。例如:D:/projects/my-app 或 /home/user/my-app",
),
component_name: z
.string()
.describe(
"组件名称,必须使用大驼峰命名法(PascalCase),如 ProductList、UserInfo、OrderDetail。此名称将用于:1. 组件目录名 2. js/base.js 中的 module_name",
),
display_name: z
.string()
.describe(
"组件中文名称,将显示在低码平台中,如:商品列表、用户信息、订单详情",
),
description: z
.string()
.describe("组件功能描述,详细说明组件的功能、UI 布局、交互逻辑"),
author: z.string().optional().describe("作者名称,默认 developer"),
config_options: z
.array(
z.object({
name: z.string().describe("配置项名称(英文)"),
displayName: z.string().describe("配置项显示名称(中文)"),
type: z
.enum([
"text",
"boolean",
"color",
"select",
"number",
"image",
"uploadimage",
"array",
])
.describe("配置项类型,uploadimage 为图片上传控件"),
defaultValue: z.any().optional().describe("默认值"),
}),
)
.optional()
.describe("自定义属性配置项列表"),
custom_events: z
.array(
z.object({
name: z.string().describe("事件名称(英文,如 onItemClick)"),
displayName: z
.string()
.describe("事件显示名称(中文,如 列表项点击)"),
}),
)
.optional()
.describe("自定义事件列表(除标准的 onMounted、onClick、onChange 外)"),
mock_data_structure: z
.string()
.optional()
.describe(
"Mock 数据结构描述,如:包含 id、name、status、description 字段的列表",
),
},
async ({
project_path,
component_name,
display_name,
description,
author = "developer",
config_options,
custom_events,
mock_data_structure,
}: {
project_path: string;
component_name: string;
display_name: string;
description: string;
author?: string;
config_options?: Array<{
name: string;
displayName: string;
type: string;
defaultValue?: any;
}>;
custom_events?: Array<{ name: string; displayName: string }>;
mock_data_structure?: string;
}) => {
let output = "";
// ============================================================
// 步骤 1:验证组件名称
// ============================================================
const validation = validateComponentName(component_name);
if (!validation.valid) {
return {
content: [
{
type: "text" as const,
text: `❌ 组件名称验证失败:${validation.message}\n\n组件名称必须使用大驼峰命名法(PascalCase),如:ProductList、UserInfo、OrderDetail`,
},
],
};
}
// ============================================================
// 步骤 2:验证项目路径并执行 npm run ioc create
// ============================================================
output += `# 🚀 IOC 低码组件生成报告\n\n`;
// 验证项目路径
const normalizedPath = project_path.replace(/\\/g, "/");
let projectPathValid = false;
let packageJsonExists = false;
try {
const packageJsonPath = path.join(project_path, "package.json");
await fs.access(packageJsonPath);
packageJsonExists = true;
projectPathValid = true;
} catch (e) {
// package.json 不存在
}
if (!projectPathValid || !packageJsonExists) {
return {
content: [
{
type: "text" as const,
text: `❌ 项目路径验证失败!\n\n**提供的路径**:\`${project_path}\`\n\n**错误原因**:该目录下未找到 \`package.json\` 文件。\n\n**请确保**:\n1. 路径指向项目根目录(包含 package.json)\n2. 路径使用绝对路径格式\n3. Windows 示例:\`D:/projects/my-app\` 或 \`D:\\\\projects\\\\my-app\`\n4. Linux/Mac 示例:\`/home/user/my-app\``,
},
],
};
}
output += `## 📋 组件信息\n\n`;
output += `- **组件名称(目录名)**:\`${component_name}\`\n`;
output += `- **中文名称**:${display_name}\n`;
output += `- **功能描述**:${description}\n`;
output += `- **作者**:${author}\n`;
output += `- **目标项目路径**:\`${normalizedPath}\`\n\n`;
// ============================================================
// 步骤 3:在项目目录下执行 npm run ioc create
// ============================================================
output += `## 🔧 执行 npm run ioc create\n\n`;
let cmdSuccess = false;
let cmdOutput = "";
let cmdError = "";
try {
// 确保使用绝对路径
// 对于 Windows 路径(如 D:\xxx 或 D:/xxx),直接使用
// 对于相对路径,基于 project_path 解析
let resolvedProjectPath = project_path;
// 标准化路径:将反斜杠转换为斜杠
resolvedProjectPath = resolvedProjectPath.replace(/\\/g, "/");
// 验证是否为绝对路径
const isAbsolute =
path.isAbsolute(resolvedProjectPath) ||
/^[a-zA-Z]:[\\/]/.test(project_path); // Windows 驱动器路径
if (!isAbsolute) {
// 如果不是绝对路径,返回错误
throw new Error(
`project_path 必须是绝对路径。收到的路径: ${project_path}`,
);
}
// 对于 Windows,确保使用正确的路径格式
if (/^[a-zA-Z]:/.test(project_path)) {
// Windows 路径,使用 path.normalize 确保格式正确
resolvedProjectPath = path.normalize(project_path);
} else {
resolvedProjectPath = path.resolve(project_path);
}
output += `### 路径信息\n\n`;
output += `- **传入的 project_path**:\`${project_path}\`\n`;
output += `- **解析后的路径**:\`${resolvedProjectPath}\`\n`;
output += `- **当前工作目录**:\`${process.cwd()}\`\n\n`;
output += `正在目录 \`${resolvedProjectPath}\` 下执行命令:\n\n`;
output += `\`\`\`bash\nnpm run ioc create ${component_name}\n\`\`\`\n\n`;
const result = await execAsync(`npm run ioc create ${component_name}`, {
cwd: resolvedProjectPath,
timeout: 30000, // 30秒超时
env: { ...process.env }, // 继承环境变量
});
cmdSuccess = true;
cmdOutput = result.stdout || "";
cmdError = result.stderr || "";
output += `✅ **命令执行成功!**\n\n`;
if (cmdOutput) {
output += `**输出**:\n\`\`\`\n${cmdOutput}\n\`\`\`\n\n`;
}
} catch (error: any) {
cmdSuccess = false;
cmdError = error.message || String(error);
output += `⚠️ **命令执行失败**\n\n`;
output += `**错误信息**:\n\`\`\`\n${cmdError}\n\`\`\`\n\n`;
output += `**可能的原因**:\n`;
output += `1. 项目中未配置 \`ioc\` 脚本,请在 package.json 的 scripts 中添加:\n`;
output += ` \`"ioc": "tsx src/cli/ioc.ts"\`\n`;
output += `2. 依赖未安装,请先执行 \`npm install\`\n`;
output += `3. tsx 未安装,请执行 \`npm install -D tsx\`\n\n`;
output += `**继续生成代码**:即使命令执行失败,下面仍会输出完整的组件代码供手动创建。\n\n`;
}
// ============================================================
// 步骤 4:生成组件代码
// ============================================================
// 生成文件内容
const files = generateIocComponentFiles({
componentName: component_name,
displayName: display_name,
description: description,
author: author,
});
// 输出目录结构
output += `## 📁 目录结构\n\n`;
if (cmdSuccess) {
output += `以下目录结构已由 \`npm run ioc create\` 命令自动创建:\n\n`;
} else {
output += `请在目标项目中创建以下目录结构:\n\n`;
}
output += `\`\`\`\n${component_name}/\n`;
output += `├── css/\n`;
output += `│ └── index.scss\n`;
output += `├── js/\n`;
output += `│ ├── base.js\n`;
output += `│ └── config.js\n`;
output += `├── mock/\n`;
output += `│ └── data.js\n`;
output += `├── plugin/\n`;
output += `│ ├── eventgenerate/\n`;
output += `│ │ └── index.js\n`;
output += `│ └── boxoptions/\n`;
output += `│ └── config.js\n`;
output += `├── index.js\n`;
output += `└── index.vue\n`;
output += `\`\`\`\n\n`;
// ============================================================
// 步骤 5:输出所有文件的完整代码
// ============================================================
output += `## 📄 生成的代码文件\n\n`;
output += `**请将以下代码文件写入到目标项目的 \`${component_name}/\` 目录中:**\n\n`;
for (const file of files) {
const ext = file.path.split(".").pop() || "";
const langMap: Record<string, string> = {
js: "javascript",
vue: "vue",
scss: "scss",
};
const lang = langMap[ext] || ext;
output += `### 📝 ${component_name}/${file.path}\n\n`;
output += `\`\`\`${lang}\n${file.content}\n\`\`\`\n\n`;
}
// 附加说明
if (config_options && config_options.length > 0) {
output += `## 🔧 自定义配置提示\n\n`;
output += `您指定了 ${config_options.length} 个自定义配置项,请在 \`js/config.js\` 中根据需求添加。\n\n`;
}
if (custom_events && custom_events.length > 0) {
output += `## ⚡ 自定义事件提示\n\n`;
output += `您指定了 ${custom_events.length} 个自定义事件,请在 \`js/config.js\` 的 interaction.event 中添加。\n\n`;
}
output += `---\n\n`;
output += `## 📚 开发建议\n\n`;
output += `### 🔍 第一步:查找相关组件文档\n`;
output += `请使用 **getComponentDoc** 内置工具查找以下常用组件的详细用法:\n\n`;
output += `| 组件名 | 用途 | 查询命令 |\n`;
output += `|--------|------|----------|\n`;
output += `| em-button | 按钮 | getComponentDoc('em-button') |\n`;
output += `| em-cell | 单元格/列表项 | getComponentDoc('em-cell') |\n`;
output += `| em-field | 表单输入 | getComponentDoc('em-field') |\n`;
output += `| em-image | 图片展示 | getComponentDoc('em-image') |\n`;
output += `| em-icon | 图标 | getComponentDoc('em-icon') |\n`;
output += `| em-tag | 标签 | getComponentDoc('em-tag') |\n`;
output += `| em-popup | 弹出层 | getComponentDoc('em-popup') |\n`;
output += `| em-swipe | 轮播 | getComponentDoc('em-swipe') |\n\n`;
output += `### 🔧 第二步:设计详细的可配置项\n`;
output += `请在 \`js/config.js\` 中为组件设计丰富的配置项,让低码用户能够灵活定制:\n\n`;
output += `**推荐配置项类别:**\n\n`;
output += `| 类别 | 配置项示例 | 类型 |\n`;
output += `|------|------------|------|\n`;
output += `| 显示控制 | showTitle, showIcon, showArrow, showBorder | boolean |\n`;
output += `| 文字样式 | titleText, titleColor, titleSize, titleWeight | text/color |\n`;
output += `| 布局设置 | layout(horizontal/vertical), columns, gap, align | select/number |\n`;
output += `| 背景样式 | backgroundColor, backgroundImage, borderRadius | color/uploadimage |\n`;
output += `| 间距设置 | padding, margin, itemSpacing | text (带px单位) |\n`;
output += `| 尺寸设置 | width, height, iconSize, imageHeight | text |\n`;
output += `| 交互状态 | clickable, disabled, loading | boolean |\n`;
output += `| 列表配置 | listItems (使用 array 类型) | array |\n\n`;
output += `**array 类型配置示例(用于列表/轮播等):**\n\n`;
output += `\`\`\`javascript\n`;
output += `{\n`;
output += ` displayName: '列表项配置',\n`;
output += ` name: 'listItems',\n`;
output += ` type: 'array',\n`;
output += ` dynamic: true, // 允许动态增删\n`;
output += ` value: [],\n`;
output += ` template: {\n`;
output += ` displayName: '列表项',\n`;
output += ` value: [\n`;
output += ` { name: 'title', displayName: '标题', type: 'text', value: '' },\n`;
output += ` { name: 'icon', displayName: '图标', type: 'uploadimage', value: '' },\n`;
output += ` { name: 'link', displayName: '跳转链接', type: 'text', value: '' }\n`;
output += ` ]\n`;
output += ` }\n`;
output += `}\n`;
output += `\`\`\`\n\n`;
output += `### 💡 第三步:优先使用 em 组件\n`;
output += `在完善 \`index.vue\` 的 UI 结构时,**强烈建议优先使用 em-组件库**(如 \`em-button\`, \`em-field\`, \`em-cell\` 等),以保证与 M8 平台的视觉统一性。\n\n`;
output += `### 🚀 下一步行动\n`;
output += `1. 使用 \`getComponentDoc\` 查询需要的 em-* 组件用法\n`;
output += `2. 进入目录:\`cd components/card_components/${component_name}\`\n`;
output += `3. 完善 \`js/config.js\` 中的配置项(尽可能详细)\n`;
output += `4. 完善 \`index.vue\` 中的 UI 结构和业务逻辑\n`;
output += `5. 使用 \`npm run serve\` 启动项目进行预览\n`;
return {
content: [
{
type: "text" as const,
text: output,
},
],
};
},
);
/**
* 工具 4: get_prompt(对外暴露)
* 获取 M8 代码生成的最佳 System Prompt
*/
server.tool(
"get_prompt",
"获取配置给 AI (Cursor/Windsurf) 的最佳 System Prompt,包含核心规范和工具调用指令。建议将其复制到 .cursorrules 或 .windsurfrules 文件中。",
{},
async () => {
const prompt = `
# Role
M8 规范代码生成专家
# Tool Capabilities
你已挂载 \`m8-generator-mcp\` 工具。当需要生成页面时,请**必须**使用 \`create_page\` 工具,而不要尝试自己手写代码。
# Critical Rules (严格执行)
1. **调用工具**:当用户描述 "生成页面"、"新建模块" 需求时,**立即**调用 \`create_page\`。
- \`vue_version\`: 务必根据 package.json 确认是 "2" 还是 "3"。
- \`module_name\`: 参数必须转为小写下划线 (snake_case)。
2. **版本规范 (生死线)**:
- **Vue 2**: 必须使用 \`Options API\` + \`Vuex\` + \`::v-deep\`。
- **Vue 3**: 必须使用 \`<script setup lang="ts">\` + \`Pinia\` + \`:deep()\` + \`TypeScript Interface\`。
3. **全局变量禁令**:
- ❌ **严禁 Import**:\`import { Util }\`, \`import { Config }\`, \`import { ejs }\`。
- ✅ **直接使用**:直接在代码里写 \`Util.ajax(...)\`, \`Config.serverUrl\`, \`ejs.ui.toast(...)\`。
4. **样式规范**:
- 不要将 CSS 写在 Vue 文件的 \`<style>\` 块中。
- 必须生成 \`css/[module].scss\` 并使用 \`@import\` 引入。
- 类名必须遵循 BEM (如 \`login__btn--active\`)。
# Check Before Output
在输出代码给用户前,请自行核对工具返回的 [Checklist],如果发现任何不符合 M8 规范的地方(特别是 Import 了全局变量),请自动修正后再展示。
`;
return {
content: [
{
type: "text" as const,
text: prompt.trim(),
},
],
};
},
);
// 启动 Server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("m8-generator-mcp server started");
}
main().catch(console.error);