@codify-ai/mcp-client 1.0.19 → 1.0.24

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.
@@ -0,0 +1,35 @@
1
+ # 🧠 核心设计哲学 (Design Philosophy)
2
+
3
+ ### 1. 视觉风格与材质 (Visual Style & Material)
4
+
5
+ **原则**:形式追随功能。所有的视觉决策(颜色、布局、材质)必须服务于用户的心理模型和业务目标。
6
+
7
+ - **[未来与深度]** (偏前沿探索):**磨砂玻璃特效** + **暗色模式**。利用光晕和透明度制造层级感。
8
+ - **[效率与速度]** (偏专业工具):**洁净扁平风格** + **便当盒布局 (Bento UI)**。强调清晰边界、模块化,减少装饰。
9
+ - **[信任与专业]** (偏金融/严谨):**瑞士极简风格**。大量留白 (Less Is More),依托排版和严格网格传达严谨性。
10
+ - **[关怀与共鸣]** (偏人文/生活):**低饱和度自然色** + **极端圆角**。超柔和弥散阴影,传递呼吸感。
11
+ - **[沉浸与表现]** (偏娱乐/叙事):**拟物化材质** + **强对比情绪色彩**。打破常规网格,低信息密度。
12
+
13
+ ### 2. 空间与排版组织原则 (Spatial & Typography)
14
+
15
+ - **密度层级**:密度与重要性成反比。核心重点区域需要低密度/大边距。数据列表需要高密度/小边距。
16
+ - **排版系统**:
17
+ - 优先使用 modern sans-serif fonts.
18
+ - 标题和正文之间应建立显著的**字体粗细**和**字号**对比。
19
+ - 正文行高保持为 `leading-[1.5]` 或 `leading-[1.6]`,以确保页面通透。
20
+
21
+ ### 3. 交互暗示与容错原则 (Affordance & Resilience)
22
+
23
+ 虽然输出的是静态 HTML,但在处理多项同类组件(如列表、导航、卡片组)时,**必须在同一个容器内,同时硬编码渲染出不同的交互状态**,以穷举展示组件的完整生命周期。
24
+
25
+ - **⚠️ 警告**:不要仅仅依赖 Tailwind 的 `hover:` 伪类来实现交互。你必须通过直接改变特定 Item 的基础 class,让状态在静态截图中**同时可见**!
26
+
27
+ ### 4. 系统一致性约束 (System Integrity)
28
+
29
+ **所有的设计决策必须映射到以下有限变量集(严禁出现奇数、小数 or 随机值):**
30
+
31
+ - **色彩系统 (Color System)**:主色定义品牌;主色**互补色**用于强引导;主色**同类色**用于柔引导。严禁随意取色。
32
+ - **空间间距 (8-Point Grid)**:必须遵循 8pt 网格系统,间距与内边距仅限:`8` / `12` / `16` / `20` / `24` / `32` / `40` (严格应用到 gap 和 padding)。
33
+ - **圆角控制 (Border Radius)**:根据风格选择,默认 `rounded-[12px]` 起步。
34
+ - **尺寸底线 (Typography/Size)**:最小点击热区 `44px`;最小阅读字号 `12px` (仅限注释),标准正文 `14px/16px`。
35
+ - **阴影控制 (Shadows)**:必须使用弥散光影 如:`shadow-[0_10px_30px_rgba(0,0,0,0.08)]`,禁止生硬。
package/dist/index.js ADDED
@@ -0,0 +1,614 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { readFileSync, existsSync, mkdirSync } from "fs";
5
+ import path, { dirname, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { z } from "zod";
8
+ import axios from "axios";
9
+ import fs from "fs/promises";
10
+ import { parse } from "node-html-parser";
11
+ const __dirname$1 = dirname(fileURLToPath(import.meta.url));
12
+ const generationRules = readFileSync(
13
+ resolve(__dirname$1, "rules.md"),
14
+ "utf-8"
15
+ );
16
+ const designPhilosophy = readFileSync(
17
+ resolve(__dirname$1, "design-philosophy.md"),
18
+ "utf-8"
19
+ );
20
+ const SERVER_URL = process.env.CODIFY_SERVER_URL || "http://localhost:8080";
21
+ const ACCESS_KEY = process.env.CODIFY_ACCESS_KEY;
22
+ const CODIFY_DOC_DIR = ".codify";
23
+ const CODIFY_OUTPUT_DIR = ".codify-output";
24
+ const isEmpty = (value) => value === null || value === void 0 || value === "" || typeof value === "string" && value.trim() === "";
25
+ function buildSuccessText(result, actionName = "操作") {
26
+ let responseText = `✅ ${actionName}已成功完成`;
27
+ if (result.rootNodeId) responseText += `
28
+ - 根节点 ID: ${result.rootNodeId}`;
29
+ if (result.documentId) responseText += `
30
+ - 文档: ${result.documentName || ""} (${result.documentId})`;
31
+ if (result.documentPageId) responseText += `
32
+ - 页面: ${result.documentPageName || ""} (${result.documentPageId})`;
33
+ return responseText;
34
+ }
35
+ async function callApi(method, endpoint, data = null) {
36
+ const headers = {};
37
+ if (ACCESS_KEY) headers["Authorization"] = `Bearer ${ACCESS_KEY}`;
38
+ const config = {
39
+ method,
40
+ url: `${SERVER_URL}${endpoint}`,
41
+ headers,
42
+ ...data && { data }
43
+ };
44
+ try {
45
+ const response = await axios(config);
46
+ return { data: response.data, error: null };
47
+ } catch (error) {
48
+ const status = error.response?.status;
49
+ const errorData = error.response?.data || {};
50
+ let message = errorData.message || error.message;
51
+ if (status === 401) message = "认证失败,请检查 CODIFY_ACCESS_KEY";
52
+ if (status === 403) {
53
+ if (errorData.mcp_get_limit !== void 0 || errorData.mcp_generate_limit !== void 0 || message.includes("limit")) {
54
+ message = `配额不足: ${message}`;
55
+ } else {
56
+ message = `权限不足: ${message}`;
57
+ }
58
+ }
59
+ if (status === 404) message = "未找到 API 终结点或活跃连接,请确保插件已打开";
60
+ return { data: null, error: { status, message, data: errorData } };
61
+ }
62
+ }
63
+ function parseResourcePath(resourcePath) {
64
+ const map = { image: "asset/images", svg: "asset/icons", shape: "asset/shapes" };
65
+ if (!isEmpty(resourcePath)) {
66
+ try {
67
+ const parsed = typeof resourcePath === "string" ? JSON.parse(resourcePath) : resourcePath;
68
+ if (parsed.image) map.image = parsed.image.replace(/^\.\//, "");
69
+ if (parsed.svg) map.svg = parsed.svg.replace(/^\.\//, "");
70
+ if (parsed.shape) map.shape = parsed.shape.replace(/^\.\//, "");
71
+ } catch (e) {
72
+ }
73
+ }
74
+ return map;
75
+ }
76
+ async function writeResource(resData, targetDir, folderName, ext) {
77
+ if (isEmpty(resData)) return 0;
78
+ const parsed = typeof resData === "string" ? JSON.parse(resData) : resData;
79
+ const keys = Object.keys(parsed);
80
+ if (keys.length === 0) return 0;
81
+ const resDir = path.join(targetDir, folderName);
82
+ if (!existsSync(resDir)) mkdirSync(resDir, { recursive: true });
83
+ const writePromises = Object.entries(parsed).map(async ([key, value]) => {
84
+ const match = key.match(/(.+)\.([a-zA-Z0-9]+)$/);
85
+ const safeKey = (match ? match[1] : key).replace(/[^a-zA-Z0-9_-]/g, "_");
86
+ const finalExt = match ? match[2] : ext;
87
+ const filePath = path.join(resDir, `${safeKey}.${finalExt}`);
88
+ let content = value;
89
+ if (typeof content === "string" && content.startsWith("http")) {
90
+ try {
91
+ const response = await axios.get(content, { responseType: "arraybuffer" });
92
+ await fs.writeFile(filePath, response.data);
93
+ } catch (err) {
94
+ console.error(`[Codify MCP] 资源下载失败 ${content}:`, err.message);
95
+ }
96
+ } else if (typeof content === "string" && content.startsWith("data:image/")) {
97
+ const parts = content.split(";base64,");
98
+ if (parts.length === 2) await fs.writeFile(filePath, parts[1], "base64");
99
+ } else {
100
+ const dataToWrite = typeof content === "object" ? JSON.stringify(content, null, 2) : content;
101
+ const encoding = finalExt === "png" || finalExt === "jpg" || finalExt === "jpeg" ? "base64" : "utf8";
102
+ await fs.writeFile(filePath, dataToWrite, encoding);
103
+ }
104
+ });
105
+ await Promise.all(writePromises);
106
+ return keys.length;
107
+ }
108
+ async function saveCodeAndResources({
109
+ baseDir,
110
+ outDir,
111
+ documentId,
112
+ documentPageId,
113
+ contentId,
114
+ nodeId,
115
+ nodeName,
116
+ code,
117
+ resourcePath,
118
+ shape,
119
+ svg,
120
+ image,
121
+ isSelection = false
122
+ }) {
123
+ let targetDir = outDir ? path.resolve(baseDir, outDir) : documentId && documentPageId ? path.join(baseDir, CODIFY_DOC_DIR, String(documentId).replace(/:/g, "-"), String(documentPageId).replace(/:/g, "-"), contentId && !isSelection && !nodeId ? "code" : "") : isSelection ? path.join(baseDir, CODIFY_OUTPUT_DIR, "selection") : path.join(baseDir, `${CODIFY_OUTPUT_DIR}${contentId ? "/" + contentId : ""}`);
124
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
125
+ const safeId = String(nodeId || Date.now()).replace(/:/g, "-");
126
+ const safeName = String(nodeName || (isSelection ? "selection" : "node")).replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, "-");
127
+ const htmlFileName = isSelection || nodeId || nodeName ? `${safeName}-${safeId}.html` : `${contentId || "index"}.html`;
128
+ const htmlPath = path.join(targetDir, htmlFileName);
129
+ if (code) {
130
+ await fs.writeFile(htmlPath, code, "utf8");
131
+ }
132
+ const resPathMap = parseResourcePath(resourcePath);
133
+ const stats = await Promise.all([
134
+ writeResource(shape, targetDir, resPathMap.shape, "json"),
135
+ writeResource(svg, targetDir, resPathMap.svg, "svg"),
136
+ writeResource(image, targetDir, resPathMap.image, "png")
137
+ ]);
138
+ return { targetDir, htmlFileName, htmlPath, shapeCount: stats[0], svgCount: stats[1], imageCount: stats[2], resourcePathMap: resPathMap };
139
+ }
140
+ const createComponentTool = {
141
+ name: "agent_create_component",
142
+ description: '创建一个 MasterGo 母版组件或组件集(变体)。应当使用 HTML 格式并包含 data-type="component" 属性。',
143
+ inputSchema: {
144
+ code: z.string().describe('组件的 HTML 结构。必须包含 data-type="component" 或 "component-set"。')
145
+ },
146
+ handler: async (args) => {
147
+ const { code } = args;
148
+ const { error } = await callApi("POST", "/api/createComponent", { code });
149
+ if (error) return { content: [{ type: "text", text: `❌ 创建失败: ${error.message}` }], isError: true };
150
+ return { content: [{ type: "text", text: "✅ 已成功向插件发送组件创建指令" }] };
151
+ }
152
+ };
153
+ const createPageTool = {
154
+ name: "agent_create_page",
155
+ description: "将代码发送到 Codify 插件转换为设计稿。若 HTML 已保存为本地文件,请通过 filePath 传入,避免大段 code 占用 Token。",
156
+ inputSchema: {
157
+ code: z.string().optional().describe("【可选】要发送的 HTML 代码内容。小片段可直接传此参数。"),
158
+ filePath: z.string().optional().describe("【可选】本地 HTML 文件的绝对路径。创建设计稿且文件已存在时,优先传此参数,工具会读取后发送并执行落盘。"),
159
+ projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
160
+ saveCodeToLocal: z.boolean().default(true).describe("是否将插件返回的渲染结果保存到本地 .codify 目录(落盘机制)")
161
+ },
162
+ handler: async (args) => {
163
+ const { projectDir, saveCodeToLocal = true } = args;
164
+ let code = args.code || "";
165
+ if (args.filePath) {
166
+ try {
167
+ const absolutePath = path.resolve(args.filePath);
168
+ code = await fs.readFile(absolutePath, "utf8");
169
+ } catch (e) {
170
+ return { content: [{ type: "text", text: `❌ 读取文件失败: ${e.message}` }], isError: true };
171
+ }
172
+ }
173
+ if (!code) return { content: [{ type: "text", text: "参数错误: 请提供 code 或 filePath" }], isError: true };
174
+ const cleanCode = code.replace(/<design_plan>[\s\S]*?<\/design_plan>/gi, "").trim();
175
+ const { data, error } = await callApi("POST", "/api/createPage", { code: cleanCode });
176
+ if (error) return { content: [{ type: "text", text: `❌ 发送失败: ${error.message}` }], isError: true };
177
+ let responseText = buildSuccessText(data, "设计稿生成");
178
+ if (saveCodeToLocal) {
179
+ try {
180
+ const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
181
+ const saveResult = await saveCodeAndResources({
182
+ baseDir,
183
+ code: data.htmlCode || code,
184
+ documentId: data.documentId,
185
+ documentPageId: data.documentPageId,
186
+ nodeId: data.rootNodeId,
187
+ nodeName: data.nodeName,
188
+ resourcePath: data.resourcePath,
189
+ shape: data.shape,
190
+ svg: data.svg,
191
+ image: data.image
192
+ });
193
+ responseText += `
194
+ - 代码已本地持久化至: ${saveResult.htmlPath}`;
195
+ } catch (e) {
196
+ responseText += `
197
+ - ⚠️ 本地保存失败: ${e.message}`;
198
+ }
199
+ }
200
+ return { content: [{ type: "text", text: responseText }] };
201
+ }
202
+ };
203
+ const designTool = {
204
+ name: "design",
205
+ description: "根据需求生成符合 Codify 规范的 HTML+CSS 代码",
206
+ inputSchema: {
207
+ requirement: z.string().describe('界面需求描述,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等。')
208
+ },
209
+ handler: async (args) => {
210
+ const { requirement } = args;
211
+ if (!requirement) {
212
+ return { content: [{ type: "text", text: "参数错误: 未提供需求描述" }], isError: true };
213
+ }
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: `📋 收到需求:${requirement}
219
+
220
+ ${generationRules}
221
+
222
+ ---
223
+
224
+ ## 发送代码:
225
+ 代码生成并审计通过后,请立即调用 agent_create_page 工具发送代码
226
+ - agent_create_page({ code: "生成的完整代码", projectDir: "当前工作目录" })
227
+ - 不要询问用户,直接执行发送操作
228
+
229
+ **请现在开始生成代码。**`
230
+ }
231
+ ]
232
+ };
233
+ }
234
+ };
235
+ const getCodeTool = {
236
+ name: "get_code",
237
+ description: "从 Codify For MasterGo 插件获取指定的代码",
238
+ inputSchema: {
239
+ contentId: z.string().describe("从Codify插件复制图层的指令"),
240
+ documentId: z.string().optional().describe("当前 MasterGo 文档 ID。"),
241
+ documentPageId: z.string().optional().describe("当前 MasterGo 页面 ID。"),
242
+ projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
243
+ outDir: z.string().describe("【必填】保存代码和资源的绝对路径")
244
+ },
245
+ handler: async (args) => {
246
+ const { contentId, outDir, projectDir, documentId, documentPageId } = args;
247
+ if (!contentId) return { content: [{ type: "text", text: "参数错误: 未提供 contentId" }], isError: true };
248
+ const { data, error } = await callApi("GET", `/api/getCode/${contentId}`);
249
+ if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
250
+ if (!data.code) return { content: [{ type: "text", text: "⚠️ 未找到代码内容" }] };
251
+ const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
252
+ const saveResult = await saveCodeAndResources({
253
+ baseDir,
254
+ outDir,
255
+ documentId,
256
+ documentPageId,
257
+ contentId,
258
+ code: data.code,
259
+ resourcePath: data.resourcePath,
260
+ shape: data.shape,
261
+ svg: data.svg,
262
+ image: data.image
263
+ });
264
+ let resultText = `✅ 代码拉取完成
265
+ - 目录: ${saveResult.targetDir}
266
+ - 文件: ${saveResult.htmlFileName}
267
+ `;
268
+ if (saveResult.shapeCount > 0) resultText += `- Shape: ${saveResult.shapeCount} 个
269
+ `;
270
+ if (saveResult.svgCount > 0) resultText += `- SVG: ${saveResult.svgCount} 个
271
+ `;
272
+ if (saveResult.imageCount > 0) resultText += `- Image: ${saveResult.imageCount} 个
273
+ `;
274
+ return { content: [{ type: "text", text: resultText }] };
275
+ }
276
+ };
277
+ const getCodeListTool = {
278
+ name: "get_code_list",
279
+ description: "获取所有可用的代码列表",
280
+ inputSchema: z.object({}).describe("无需参数获取代码列表"),
281
+ handler: async () => {
282
+ const { data, error } = await callApi("GET", "/api/getCodeList");
283
+ if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
284
+ if (!Array.isArray(data) || data.length === 0) {
285
+ return { content: [{ type: "text", text: "📋 代码列表为空" }] };
286
+ }
287
+ let resultText = `✅ 成功获取代码列表 (共 ${data.length} 项)
288
+
289
+ `;
290
+ data.forEach((item, index) => {
291
+ resultText += `${index + 1}. **${item.contentId}**
292
+ `;
293
+ resultText += ` - 长度: ${item.codeLength} 字符 | 创建于: ${item.createdAt || "未知"}
294
+
295
+ `;
296
+ });
297
+ return { content: [{ type: "text", text: resultText }] };
298
+ }
299
+ };
300
+ const getSelectionCodeTool = {
301
+ name: "get_selection_code",
302
+ description: "获取 MasterGo 中当前选中图层(或指定图层)的代码,并智能落盘到本地 .codify 目录。\n\n【强制触发场景】凡用户说「将选中的 xxx 改成 yyy」「将选中的按钮改成红色」「修改当前选中的图层」等含「选中」的修改指令,你必须首先调用此工具拉取当前画布选中节点的最新代码,再根据返回的 HTML 进行修改,禁止凭空生成或直接修改本地文件。",
303
+ inputSchema: {
304
+ projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
305
+ id: z.string().optional().describe("【选填】MasterGo图层ID (例如 123:456)。如果提供,将直接拉取该ID的代码;如果不提供,将拉取当前选中图层的代码。")
306
+ },
307
+ handler: async (args) => {
308
+ const { projectDir, id } = args;
309
+ const endpoint = id ? `/api/getSelectionCode?id=${encodeURIComponent(id)}` : "/api/getSelectionCode";
310
+ const { data, error } = await callApi("GET", endpoint);
311
+ if (error) {
312
+ if (error.status === 400 && error.data?.error === "NoSelection") {
313
+ return { content: [{ type: "text", text: "❌ 获取失败: 没有选中任何图层。请先在 MasterGo 中选中一个图层。" }], isError: true };
314
+ }
315
+ return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
316
+ }
317
+ const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
318
+ const nodeInfo = data.nodeInfo || {};
319
+ const { rootId, documentId, documentPageId, nodeId, nodeName, parentId } = nodeInfo;
320
+ if (!parentId || parentId === "null") {
321
+ const saveResult = await saveCodeAndResources({
322
+ baseDir,
323
+ documentId,
324
+ documentPageId,
325
+ nodeId: rootId || nodeId,
326
+ // 使用 rootId 作为文件名后缀
327
+ nodeName,
328
+ code: data.code,
329
+ resourcePath: data.resourcePath,
330
+ shape: data.shape,
331
+ svg: data.svg,
332
+ image: data.image
333
+ });
334
+ console.log("saveResult", saveResult);
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: `✅ 成功获取并保存根节点代码
340
+ - 节点: ${nodeName} (${nodeId})
341
+ - 文件: ${saveResult.htmlPath}
342
+
343
+ 请查看该文件了解当前结构。`
344
+ }
345
+ ]
346
+ };
347
+ } else {
348
+ if (!documentId || !documentPageId) {
349
+ return { content: [{ type: "text", text: `❌ 更新失败: 缺少 documentId 或 documentPageId 信息。` }], isError: true };
350
+ }
351
+ const targetDir = path.join(baseDir, CODIFY_DOC_DIR, String(documentId).replace(/:/g, "-"), String(documentPageId).replace(/:/g, "-"));
352
+ if (!existsSync(targetDir)) {
353
+ return { content: [{ type: "text", text: `❌ 更新失败: 找不到根节点所在目录 ${targetDir}。请先选中并获取根节点的代码。` }], isError: true };
354
+ }
355
+ const files = await fs.readdir(targetDir);
356
+ const safeRootId = String(rootId).replace(/:/g, "-");
357
+ const rootFile = files.find((f) => f.endsWith(`-${safeRootId}.html`));
358
+ if (!rootFile) {
359
+ return { content: [{ type: "text", text: `❌ 更新失败: 在目录中找不到包含根节点 ID (${rootId}) 的文件。请先选中并获取根节点的代码。` }], isError: true };
360
+ }
361
+ const rootFilePath = path.join(targetDir, rootFile);
362
+ const htmlContent = await fs.readFile(rootFilePath, "utf8");
363
+ const rootNode = parse(htmlContent);
364
+ const targetElement = rootNode.querySelector(`[data-node-id="${nodeId}"]`);
365
+ if (targetElement) {
366
+ targetElement.replaceWith(data.code);
367
+ } else if (nodeInfo.parentId) {
368
+ const parentElement = rootNode.querySelector(`[data-node-id="${nodeInfo.parentId}"]`);
369
+ if (parentElement) {
370
+ parentElement.insertAdjacentHTML("beforeend", data.code);
371
+ } else {
372
+ return { content: [{ type: "text", text: `❌ 更新失败: 在文件 ${rootFile} 中找不到当前节点(${nodeId}),也找不到父节点(${nodeInfo.parentId})。` }], isError: true };
373
+ }
374
+ } else {
375
+ return { content: [{ type: "text", text: `❌ 更新失败: 在文件 ${rootFile} 中找不到 data-node-id="${nodeId}" 的元素,且未提供 parentId 作为后备。` }], isError: true };
376
+ }
377
+ await fs.writeFile(rootFilePath, rootNode.toString(), "utf8");
378
+ await saveCodeAndResources({
379
+ baseDir,
380
+ documentId,
381
+ documentPageId,
382
+ nodeId,
383
+ nodeName,
384
+ code: "",
385
+ // 我们已经手动更新了 HTML,这里传空字符串,主要是为了利用它去保存资源
386
+ resourcePath: data.resourcePath,
387
+ shape: data.shape,
388
+ svg: data.svg,
389
+ image: data.image
390
+ }).catch((e) => console.error("保存子节点资源失败:", e));
391
+ return {
392
+ content: [
393
+ {
394
+ type: "text",
395
+ text: `✅ 成功更新子节点代码
396
+ - 节点: ${nodeName} (${nodeId})
397
+ - 已更新文件: ${rootFilePath}`
398
+ }
399
+ ]
400
+ };
401
+ }
402
+ }
403
+ };
404
+ const getUserInfoTool = {
405
+ name: "get_user_info",
406
+ description: "获取当前登录用户的信息,包括配额、团队等",
407
+ inputSchema: z.object({}).describe("获取当前用户信息"),
408
+ handler: async () => {
409
+ const { data, error } = await callApi("GET", "/api/getUserInfo");
410
+ if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
411
+ let resultText = `✅ 用户信息
412
+
413
+ 👤 用户: ${data.realname || data.userName || data.userId}
414
+ `;
415
+ if (data.quota) {
416
+ resultText += `
417
+ 📊 配额:
418
+ `;
419
+ resultText += ` - 生成设计: ${data.quota.mcp_generate_count || 0} / ${data.quota.mcp_generate_limit || "无"}
420
+ `;
421
+ resultText += ` - 获取代码: ${data.quota.mcp_get_count || 0} / ${data.quota.mcp_get_limit || "无"}
422
+ `;
423
+ }
424
+ if (data.teams?.length) {
425
+ resultText += `
426
+ 👥 团队:
427
+ `;
428
+ data.teams.forEach((t, i) => resultText += ` ${i + 1}. ${t.name} (ID: ${t.teamId})
429
+ `);
430
+ }
431
+ return { content: [{ type: "text", text: resultText }] };
432
+ }
433
+ };
434
+ const updateNodeTool = {
435
+ name: "agent_update_node",
436
+ description: '将修改后的 HTML 代码发回 MasterGo 画布进行更新。\n\n【调用前置条件】若用户说的是「将选中的 xxx 改成 yyy」等含「选中」的修改指令,你必须先调用 get_selection_code 拉取当前选中节点代码,修改完成后再调用本工具,不得跳过前置步骤。\n\n【局部修改(改文字/颜色/某处)】只传你改动的那一个节点(含 data-node-id 及子节点)的 HTML 片段到 code 参数,严禁传整个文件。\n\n【全量同步(用户明确说"同步到设计稿")】才用 filePath 传本地文件绝对路径,禁止用 Read 工具读大文件再塞进 code。\n\n若是从 Vue/React 业务代码同步,请剔除框架指令(v-for、@click 等),只传纯 HTML。',
437
+ inputSchema: {
438
+ documentId: z.string().optional().describe("当前 MasterGo 文档 ID。"),
439
+ documentPageId: z.string().optional().describe("当前 MasterGo 页面 ID。"),
440
+ targetNodeId: z.string().optional().describe("【选填】目标图层 ID (例如 123:456)。如果不传,则默认更新 MasterGo 中当前选中的图层。"),
441
+ code: z.string().optional().describe("【可选】修改后的 HTML 代码片段。如果只是局部小修改,可以直接传代码。"),
442
+ filePath: z.string().optional().describe("【可选】如果需要发送整个文件或大量代码,请提供该文件的绝对路径,工具会自动读取并发送,绝对不要把整个文件的代码塞进 code 参数里!")
443
+ },
444
+ handler: async (args) => {
445
+ const { targetNodeId, documentId, documentPageId, filePath } = args;
446
+ let finalCode = args.code || "";
447
+ if (filePath) {
448
+ try {
449
+ const absolutePath = path.resolve(filePath);
450
+ finalCode = await fs.readFile(absolutePath, "utf8");
451
+ } catch (e) {
452
+ return { content: [{ type: "text", text: `❌ 读取文件失败: ${e.message}` }], isError: true };
453
+ }
454
+ }
455
+ if (!finalCode) {
456
+ return { content: [{ type: "text", text: `❌ 必须提供 code 或 filePath` }], isError: true };
457
+ }
458
+ const { data, error } = await callApi("POST", "/api/updateNode", { code: finalCode, targetNodeId, documentId, documentPageId });
459
+ if (error) return { content: [{ type: "text", text: `❌ 更新失败: ${error.message}` }], isError: true };
460
+ return {
461
+ content: [{ type: "text", text: `✅ 已成功发送更新指令${data.nodeId ? `
462
+ - 结果节点 ID: ${data.nodeId}` : ""}
463
+
464
+ ⚠️ 强烈建议:为了保证本地 .codify 目录中的基准 HTML 与设计稿保持一致,请立即调用 get_selection_code 工具(传入对应的根节点 ID),拉取最新的纯净 HTML 并覆盖本地基准文件。` }]
465
+ };
466
+ }
467
+ };
468
+ const getDesignDiffTool = {
469
+ name: "get_design_diff",
470
+ description: "获取最新设计稿代码与本地 .codify 目录中旧代码的差异 (Diff)。当用户要求“同步设计稿到业务代码”或“对比最新设计稿”时使用。注意:拿到差异后,请根据 data-node-id 精准修改用户的业务代码,绝对不要破坏原有的框架逻辑(如 Vue/React 指令)。",
471
+ inputSchema: {
472
+ projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
473
+ id: z.string().optional().describe("【选填】MasterGo图层ID (例如 123:456)。如果不传,则默认获取当前选中图层的代码进行对比。")
474
+ },
475
+ handler: async (args) => {
476
+ const { projectDir, id } = args;
477
+ const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
478
+ let oldHtml = "";
479
+ let localFilePath = "";
480
+ if (id) {
481
+ const safeId = String(id).replace(/:/g, "-");
482
+ const findFile = async (dir) => {
483
+ const entries = await fs.readdir(dir, { withFileTypes: true });
484
+ for (const entry of entries) {
485
+ const fullPath = path.join(dir, entry.name);
486
+ if (entry.isDirectory()) {
487
+ const found = await findFile(fullPath);
488
+ if (found) return found;
489
+ } else if (entry.name.endsWith(`-${safeId}.html`)) {
490
+ return fullPath;
491
+ }
492
+ }
493
+ return null;
494
+ };
495
+ const codifyDir = path.join(baseDir, CODIFY_DOC_DIR);
496
+ if (existsSync(codifyDir)) {
497
+ localFilePath = await findFile(codifyDir) || "";
498
+ if (localFilePath) {
499
+ oldHtml = await fs.readFile(localFilePath, "utf8");
500
+ }
501
+ }
502
+ }
503
+ if (!oldHtml) {
504
+ return {
505
+ content: [{
506
+ type: "text",
507
+ text: id ? `❌ 在本地 .codify 目录中找不到 ID 为 ${id} 的代码文件。请确保已执行过拉取操作。` : `❌ 请提供图层 ID 以便从本地 .codify 目录加载基准代码进行 Diff。`
508
+ }],
509
+ isError: true
510
+ };
511
+ }
512
+ const { data, error } = await callApi("POST", "/api/getDesignDiff", {
513
+ code: oldHtml,
514
+ id
515
+ });
516
+ if (error) {
517
+ return { content: [{ type: "text", text: `❌ 获取设计稿差异失败: ${error.message}` }], isError: true };
518
+ }
519
+ const diffs = data.diffs || [];
520
+ if (diffs.length === 0) {
521
+ return {
522
+ content: [
523
+ {
524
+ type: "text",
525
+ text: `✅ 设计稿与本地代码完全一致,没有发现任何变更。`
526
+ }
527
+ ]
528
+ };
529
+ }
530
+ return {
531
+ content: [
532
+ {
533
+ type: "text",
534
+ text: `✅ 成功获取设计稿差异 (共 ${diffs.length} 处变更):
535
+
536
+ \`\`\`json
537
+ ${JSON.stringify(diffs, null, 2)}
538
+ \`\`\`
539
+
540
+ 请根据以上 Diff 数据,结合 data-node-id,将变更合并到用户的业务代码(无论是 Vue、React、原生代码)和 .codify 的基准文件。`
541
+ }
542
+ ]
543
+ };
544
+ }
545
+ };
546
+ const allTools = [
547
+ designTool,
548
+ getCodeTool,
549
+ getCodeListTool,
550
+ getUserInfoTool,
551
+ createPageTool,
552
+ createComponentTool,
553
+ getSelectionCodeTool,
554
+ updateNodeTool,
555
+ getDesignDiffTool
556
+ ];
557
+ function registerAllTools(mcpServer2) {
558
+ allTools.forEach((tool) => {
559
+ mcpServer2.registerTool(tool.name, {
560
+ description: tool.description,
561
+ inputSchema: tool.inputSchema
562
+ }, tool.handler);
563
+ });
564
+ }
565
+ const mcpServer = new McpServer(
566
+ {
567
+ name: "Codify-MCP-Client",
568
+ version: "1.0.22"
569
+ },
570
+ {
571
+ capabilities: {
572
+ resources: {},
573
+ tools: {},
574
+ prompts: {}
575
+ }
576
+ }
577
+ );
578
+ mcpServer.registerResource(
579
+ "generation-rules",
580
+ "codify://generation-rules",
581
+ { description: "Codify 逆向转译协议(必须严格遵守的 HTML/Tailwind 语法规范)" },
582
+ async (uri) => ({
583
+ contents: [
584
+ {
585
+ uri: uri.toString(),
586
+ text: generationRules,
587
+ mimeType: "text/markdown"
588
+ }
589
+ ]
590
+ })
591
+ );
592
+ mcpServer.registerResource(
593
+ "design-philosophy",
594
+ "codify://design-philosophy",
595
+ { description: "Codify 核心设计哲学(仅在从零设计新页面或重构 UI 风格时加载,用于指导商业级审美)" },
596
+ async (uri) => ({
597
+ contents: [
598
+ {
599
+ uri: uri.toString(),
600
+ text: designPhilosophy,
601
+ mimeType: "text/markdown"
602
+ }
603
+ ]
604
+ })
605
+ );
606
+ registerAllTools(mcpServer);
607
+ try {
608
+ const transport = new StdioServerTransport();
609
+ await mcpServer.connect(transport);
610
+ console.error(`[Codify MCP] Client connected to ${SERVER_URL}`);
611
+ } catch (error) {
612
+ console.error(`[Codify MCP] Connection failed:`, error.message);
613
+ process.exit(1);
614
+ }