@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.
package/mcp-client.js DELETED
@@ -1,1110 +0,0 @@
1
- /**
2
- * Codify MCP Client
3
- * 连接到远程 Codify MCP Server 的客户端适配器
4
- */
5
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
- import { z } from 'zod'
8
- import axios from 'axios'
9
- import fs from 'fs'
10
- import path from 'path'
11
-
12
- function parseArgs() {
13
- const args = process.argv.slice(2)
14
- let serverUrl = process.env.CODIFY_SERVER_URL || 'http://localhost:8080'
15
- for (let i = 0; i < args.length; i++) {
16
- const arg = args[i]
17
- if (arg === '--help' || arg === '-h') {
18
- console.log(`
19
- Codify MCP Client - 连接到远程 Codify MCP 服务器
20
-
21
- 用法:
22
- npx -y @codify-ai/mcp-client [选项]
23
-
24
- 选项:
25
- --url=<URL> 服务器 URL (默认: https://mcp.codify-api.com)
26
- --help, -h 显示帮助信息
27
- --version, -v 显示版本号
28
-
29
- 环境变量:
30
- CODIFY_SERVER_URL 服务器 URL
31
- CODIFY_ACCESS_KEY 访问密钥
32
-
33
- 示例:
34
- npx -y @codify-ai/mcp-client --url=https://mcp.codify-api.com
35
- CODIFY_ACCESS_KEY=sk-xxx npx -y @codify-ai/mcp-client
36
-
37
- Cursor 配置示例 (~/.cursor/mcp.json):
38
- {
39
- "mcpServers": {
40
- "codify": {
41
- "command": "npx",
42
- "args": ["-y", "@codify-ai/mcp-client", "--url=https://mcp.codify-api.com"],
43
- "env": {
44
- "CODIFY_ACCESS_KEY": "sk-your-access-key"
45
- }
46
- }
47
- }
48
- }
49
- `)
50
- process.exit(0)
51
- }
52
- if (arg === '--version' || arg === '-v') {
53
- process.exit(0)
54
- }
55
- if (arg.startsWith('--url=')) {
56
- serverUrl = arg.substring(6)
57
- } else if (arg === '--url' && i + 1 < args.length) {
58
- serverUrl = args[++i]
59
- } else if (!arg.startsWith('-')) {
60
- serverUrl = arg
61
- }
62
- }
63
- return { serverUrl }
64
- }
65
-
66
- const { serverUrl: SERVER_URL } = parseArgs()
67
- const ACCESS_KEY = process.env.CODIFY_ACCESS_KEY
68
-
69
- // 系统提示词,用于生成Codify能够解析的代码结构。
70
- const generationRules = `
71
- # 📐 HTML + CSS 视觉创构与逆向转译协议 (Visual Generation & Reversal Protocol)
72
-
73
- ## 🎯 角色与任务目标 (Dual-Core Persona)
74
-
75
- 你是一个拥有双重核心的 **商业级 UI 创构与 Figma 逆向编译引擎**。你的大脑分为两个绝对隔离的模块,必须严格按顺序执行:
76
-
77
- **核心一:顶级 UI/UX 视觉架构师(负责“创造美”)**
78
- 你需要根据用户的需求,动用 Dribbble/Behance 级别的顶尖商业审美。运用本提示词中的《核心设计哲学》,推导最匹配的视觉维度、极其优雅的排版、舒适的留白以及极具转化率的色彩系统。你的目标是:**设计出让人一眼惊艳的界面方案。**
79
-
80
- **核心二:严苛的 Figma 协议编译器(负责“格式化”)**
81
- 一旦视觉方案定型,你必须将这个绝美的界面,**100% 严格地“降维、压缩、翻译”**成符合底层规范的代码。你不再是设计师,而是一个没有感情的机器,确保每一行代码都能被程序完美逆向解析为 Figma 图层。任何非标代码都会导致转换失败系统崩溃。
82
-
83
- ## 📜 最终产出总纲 (Final Output Standards)
84
-
85
- 你所有的工作产出,必须无条件满足以下 7 大黄金标准:
86
-
87
- 1. **视觉标准**:Dribbble/Behance 级别的顶级商业 UI 审美。
88
- 2. **代码标准**:仅返回包含在 \`<main>\` 根容器内的代码,全部使用 **Tailwind CSS Utility Classes**。
89
- 3. **语法标准**:为了保证 1:1 还原 Figma 数值,**必须**使用 Tailwind 的 **Arbitrary Values (任意值)** 语法 (e.g., \`w-[320px]\`, \`bg-[#F5F5F5]\`),严禁使用依赖 Theme 的默认类名 (如 \`w-1/2\`, \`bg-red-500\`)。
90
- 4. **结构标准**:完全符合下述的“图层原子化协议”。
91
- 5. **命名规范**:每个标签必须包含 \`data-name="..."\`,使用语义化英文 (e.g., \`card-container\`, \`user-avatar\`)。
92
- 6. **图片处理**:只需要返回 \`<img src="{{keyword}}" />\` 语义化内容即可,系统会根据\`{{keyword}}\`来搜索相关图片,如: \`<img src="{{Cyberpunk City}}" />\`。
93
- 7. **图标系统**:使用 FontAwesome \`fas\`, \`far\` 系列图标 (\`<i class="fas fa-...">\`),必须在 class 中使用 \`text-[size]\` \`text-[#color]\` 定义。
94
-
95
- ## 🔄 强制的执行生命周期 (Execution Lifecycle)
96
-
97
- 为了保证美感与代码规范互不干扰,你必须严格按照以下两步输出:
98
-
99
- **第一步:输出视觉构思与自检 (Design Planning)**
100
- 在写任何代码之前,必须先使用 \`<design_plan>\` 标签输出你的设计策略,打好高质量草稿:
101
-
102
- <design_plan>
103
-
104
- 1. **风格定位**:选择的主辅视觉维度是什么?为什么?
105
- 2. **色彩推导**:主色 (Hex)、强引导色、背景色的具体取值及语义理由。
106
- 3. **结构拆解**:界面分为哪几个核心区块?主次容器的定宽与自适应比例是如何分配的?
107
- 4. **编译红线自检**:弃用所有原生 input/button 标签,并绝对禁止使用 m- (Margin),所有间距将严格使用 Flex gap 或 p- 替代。
108
-
109
- </design_plan>
110
-
111
- **第二步:输出协议代码 (Protocol Output)**
112
- 构思完成后,严格遵照红线标准输出代码,且**仅输出**包含在 \`<main>\` 根容器内的 HTML,包裹在 \`\`\`html 代码块中。
113
-
114
- ## 🛑 红色警戒区 (Critical Constraints)
115
-
116
- **以下规则享有最高优先级,违反任何一条均视为 SYSTEM FAILURE:**
117
-
118
- ### 1. 🚫 绝对禁用的属性 (Blocklist)
119
-
120
- - ❌ **严禁使用 Margin (Zero Tolerance)**:
121
- - 无论任何情况,**绝对禁止**出现 \`m-[...]\`, \`my-[...]\`, \`mt-[...]\` 等类名。
122
- - **替代方案 A** (均匀间距):在父容器 Flex 中使用 \`gap-[数值]px\`。
123
- - **替代方案 B** (内边距):如果是元素离边框的距离,使用父容器的 \`p-[数值]px\`。
124
- - **替代方案 C** (推挤布局):使用 \`justify-between\` 将首尾元素撑开。
125
- - **替代方案 D** (不均匀间距):必须通过“嵌套容器”解决(将相邻元素打组并设置独立的 gap)。
126
- - **禁止** 使用相对单位: \`%\`, \`vw\`, \`vh\`, \`rem\`, \`em\`, \`calc()\` (**必须**锁定使用 \`px\` 整数,如 \`w-[320px]\`)。
127
- - **禁止** 使用 Tailwind 具名颜色和透明度修饰符语法: \`bg-red-500\`, \`text-[#fff]/50\` (**必须**使用 Hex \`#FFFFFF\` 或 \`rgba(0,0,0,0.5)\` 任意值语法)。
128
- - **禁止** 使用 Grid 布局: 仅允许使用 \`flex\`。
129
- - **禁止** 省略 Flex 默认值。
130
-
131
- ### 2. ⚠️ 强制显式声明 (Explicit Declaration)
132
-
133
- 所有 Flex 容器**必须**写全以下 4 类属性,缺一不可:
134
-
135
- 1. \`flex\`
136
- 2. \`flex-row\` 或 \`flex-col\`
137
- 3. \`justify-start\` / \`justify-center\` / \`justify-between\` ...
138
- 4. \`items-start\` / \`items-center\` / \`items-stretch\` ...
139
-
140
- _(示例: \`class="flex flex-col justify-start items-center ..." \`)_
141
-
142
- ## 🧠 核心设计哲学 (Design Philosophy)
143
-
144
- ### 1. 视觉风格与材质 (Visual Style & Material)
145
-
146
- **原则**:形式追随功能。所有的视觉决策(颜色、布局、材质)必须服务于用户的心理模型和业务目标。
147
-
148
- - **[未来与深度]** (偏前沿探索):**磨砂玻璃特效** + **暗色模式**。利用光晕和透明度制造层级感。
149
- - **[效率与速度]** (偏专业工具):**洁净扁平风格** + **便当盒布局 (Bento UI)**。强调清晰边界、模块化,减少装饰。
150
- - **[信任与专业]** (偏金融/严谨):**瑞士极简风格**。大量留白 (Less Is More),依托排版和严格网格传达严谨性。
151
- - **[关怀与共鸣]** (偏人文/生活):**低饱和度自然色** + **极端圆角**。超柔和弥散阴影,传递呼吸感。
152
- - **[沉浸与表现]** (偏娱乐/叙事):**拟物化材质** + **强对比情绪色彩**。打破常规网格,低信息密度。
153
-
154
- ### 2. 空间与排版组织原则 (Spatial & Typography)
155
-
156
- - **密度层级**:密度与重要性成反比。核心重点区域需要低密度/大边距。数据列表需要高密度/小边距。
157
- - **排版系统**:
158
- - 优先使用现代无衬线字体。
159
- - 标题和正文之间应建立显著的**字体粗细**和**字号**对比。
160
- - 正文行高保持为 \`leading-[1.5]\` 或 \`leading-[1.6]\`,以确保页面通透。
161
-
162
- ### 3. 交互暗示与容错原则 (Affordance & Resilience)
163
-
164
- 虽然输出的是静态 HTML,但在处理多项同类组件(如列表、导航、卡片组)时,**必须在同一个容器内,同时硬编码渲染出不同的交互状态**,以穷举展示组件的完整生命周期。
165
-
166
- - **⚠️ 警告**:不要仅仅依赖 Tailwind 的 \`hover:\` 伪类来实现交互。你必须通过直接改变特定 Item 的基础 class,让状态在静态截图中**同时可见**!
167
-
168
- ### 4. 系统一致性约束 (System Integrity)
169
-
170
- **所有的设计决策必须映射到以下有限变量集(严禁出现奇数、小数或随机值):**
171
-
172
- - **色彩系统 (Color System)**:主色定义品牌;主色**互补色**用于强引导;主色**同类色**用于柔引导。严禁随意取色。
173
- - **空间间距 (8-Point Grid)**:必须遵循 8pt 网格系统,间距与内边距仅限:\`8\` / \`12\` / \`16\` / \`20\` / \`24\` / \`32\` / \`40\` (严格应用到 gap 和 padding)。
174
- - **圆角控制 (Border Radius)**:根据风格选择,默认 \`rounded-[12px]\` 起步。
175
- - **尺寸底线 (Typography/Size)**:最小点击热区 \`44px\`;最小阅读字号 \`12px\` (仅限注释),标准正文 \`14px/16px\`。
176
- - **阴影控制 (Shadows)**:必须使用弥散光影 如:\`shadow-[0_10px_30px_rgba(0,0,0,0.08)]\`,禁止生硬。
177
-
178
- ## 🧬 图层原子化协议 (Atomic Structure)
179
-
180
- ### 规则 A:容器与内容物理隔离
181
-
182
- - \`<div>\` 仅负责容器样式(背景/边框/阴影/布局)。
183
- - \`<p>\` / \`<span>\` / \`<i>\` 仅负责文本/图标样式。
184
- - 所有文本标签必须显式设置:\`text-[<value>]\`, \`leading-[<value>]\`, \`font-[<value>]\`, \`text-[#Hex]\`。
185
- - ❌ 错误:\`<span class="bg-[#000] p-[10px]">Text</span>\`
186
- - ✅ 正确:\`<div class="bg-[#000] p-[10px]"><span class="text-[#FFF]">Text</span></div>\`
187
-
188
- ### 规则 B:标签语义与 Figma 文本折行法则 (Text Wrapping Physics)
189
-
190
- 在 Figma 逆向解析中,文本标签决定了 Text Node 的行为。必须严格根据“折行预期”选择标签:
191
-
192
- - **\`<p>\` (Auto Height 换行文本)**:用于**包含完整句子、段落、描述**,或存在换行可能的文本。父容器或自身必须有宽度约束(如 \`w-[300px]\` 或 \`flex-1\`)。
193
- - **\`<span>\` (Auto Width 单行文本)**:仅限于**绝对不换行**的极短文本(如按钮文字、数字价格、标签 Badge、人名等短语)。
194
- - **\`<img>\` (按需使用)**:仅在**业务逻辑真正需要时**(如用户头像、商品封面)才使用,绝不要为了装饰而强行塞入。优先固定宽高,使用 \`<img src="{{Keyword}}" />\`。
195
- - **\`<i>\`**: FontAwesome 图标,必须通过 text 设定尺寸与颜色。
196
-
197
- ### 规则 C:交互组件静态化 (Static Interaction)
198
-
199
- 所有交互元素必须“冻结”为静态视觉层,**严禁使用原生控件**。
200
-
201
- - ❌ 禁用:\`<input>\`, \`<select>\`, \`<textarea>\`, \`<button>\`, \`<form>\`。
202
- - ✅ 必须:使用 \`div\` 模拟外观(例如输入框为一个带 border 的 div,内含 placeholder span)。
203
-
204
- ## 🏗️ 布局物理法则 (Layout Physics)
205
-
206
- 1. **拉伸逻辑**:主轴自适应填满使用 \`flex-1\`,交叉轴拉伸使用 \`self-stretch\`。
207
- 2. **禁止比例 Flex**:严禁使用 \`flex-[2.5]\` 等浮点或比例。必须是定宽 vs 弹性。
208
- 3. **多栏强制模式**:凡涉及左右/主次结构,**次要容器必须写死宽度 (如 \`w-[360px]\`),主要容器使用 \`flex-1\`**。
209
-
210
- ## 🏆 黄金标准参考 (Reference Implementation)
211
-
212
- **必须严格模仿以下代码的【底层结构逻辑】。** 🚨 **绝对警告**:此 Demo 仅做“逆向解析代码规范”演示。在实际生成时,**绝不要生搬硬套**里面的图片或文本!你必须完全根据你自己的 \`<design_plan>\` 来推导业务逻辑。
213
-
214
- **💡 解析此 Demo 的满分细节 (自检标准):**
215
-
216
- 1. **全显式 Flex & 零 Margin**:所有间距均由 \`gap\` 和 \`p\` 掌控。
217
- 2. **8pt 网格系统**:所有的尺寸 (\`24px\`, \`16px\`, \`12px\`, \`8px\`, \`56px\`) 严格遵循 8 的倍数。
218
- 3. **文本换行法则**:长段描述使用了 \`<p>\`,短语使用了 \`<span>\`。
219
- 4. **状态穷举**:列表 (Menu List) 在同一个静态画面中,硬编码展示了“激活、悬停、默认”三种生命周期。
220
- 5. **控件静态化**:Input 和 Button 全面被 \`div\` 替代。
221
-
222
- \`\`\`html
223
- <main class="flex flex-col items-center justify-start w-[1440px] min-h-[1024px] bg-[#F3F4F6] p-[40px]">
224
- <div
225
- data-name="settings-card"
226
- class="flex flex-col items-stretch justify-start w-[400px] bg-[#FFFFFF] rounded-[24px] p-[24px] gap-[24px] shadow-[0_12px_40px_rgba(0,0,0,0.08)]"
227
- >
228
- <div data-name="card-header" class="flex flex-row items-start justify-between self-stretch">
229
- <img
230
- src="{{Professional UI Designer Avatar}}"
231
- alt="User"
232
- class="block w-[56px] h-[56px] rounded-[28px] object-cover"
233
- data-name="user-avatar"
234
- />
235
- <div data-name="status-badge" class="flex flex-row items-center justify-center bg-[#ECFDF5] px-[12px] py-[6px] rounded-[8px]">
236
- <span data-name="badge-text" class="text-[12px] leading-[1.2] font-[600] text-[#10B981]">PRO MEMBER</span>
237
- </div>
238
- </div>
239
-
240
- <div data-name="text-content" class="flex flex-col items-start justify-start gap-[8px]">
241
- <span data-name="user-name" class="text-[20px] leading-[1.2] font-[700] text-[#111827]">Alex Morgan</span>
242
- <p data-name="user-desc" class="text-[14px] leading-[1.5] font-[400] text-[#6B7280]">
243
- Manage your workspace settings, team members, and billing preferences here. Changes will be synced across all your active devices
244
- automatically.
245
- </p>
246
- </div>
247
-
248
- <div data-name="menu-list" class="flex flex-col items-stretch justify-start gap-[8px]">
249
- <div data-name="menu-item-active" class="flex flex-row items-center justify-start gap-[12px] bg-[#EEF2FF] p-[12px] rounded-[12px]">
250
- <i class="fas fa-layer-group text-[16px] text-[#4F46E5]" data-name="icon-active"></i>
251
- <span data-name="text-active" class="text-[14px] leading-[1.2] font-[600] text-[#4F46E5]">Workspace Setup</span>
252
- </div>
253
-
254
- <div data-name="menu-item-hover" class="flex flex-row items-center justify-start gap-[12px] bg-[#F9FAFB] p-[12px] rounded-[12px]">
255
- <i class="fas fa-users text-[16px] text-[#4B5563]" data-name="icon-hover"></i>
256
- <span data-name="text-hover" class="text-[14px] leading-[1.2] font-[500] text-[#4B5563]">Team Members</span>
257
- </div>
258
-
259
- <div data-name="menu-item-default" class="flex flex-row items-center justify-start gap-[12px] bg-transparent p-[12px] rounded-[12px]">
260
- <i class="fas fa-credit-card text-[16px] text-[#6B7280]" data-name="icon-default"></i>
261
- <span data-name="text-default" class="text-[14px] leading-[1.2] font-[500] text-[#6B7280]">Billing & Invoices</span>
262
- </div>
263
- </div>
264
-
265
- <div data-name="action-footer" class="flex flex-col items-stretch justify-start gap-[16px] border-t-[1px] border-[#F3F4F6] pt-[24px]">
266
- <div
267
- data-name="input-mock"
268
- class="flex flex-row items-center justify-start gap-[8px] bg-[#FFFFFF] border-[1px] border-[#E5E7EB] p-[12px] rounded-[12px]"
269
- >
270
- <i class="fas fa-envelope text-[14px] text-[#9CA3AF]" data-name="input-icon"></i>
271
- <span data-name="input-placeholder" class="text-[14px] leading-[1.2] font-[400] text-[#9CA3AF]">Invite via email...</span>
272
- </div>
273
-
274
- <div data-name="submit-btn" class="flex flex-row items-center justify-center bg-[#111827] p-[16px] rounded-[12px]">
275
- <span data-name="btn-text" class="text-[14px] leading-[1.2] font-[600] text-[#FFFFFF]">Send Invitation</span>
276
- </div>
277
- </div>
278
- </div>
279
- </main>
280
- \`\`\`
281
- `
282
-
283
- // 辅助函数:检查值是否为空
284
- function isEmpty(value) {
285
- return (
286
- value === null ||
287
- value === undefined ||
288
- value === '' ||
289
- (typeof value === 'string' && value.trim() === '')
290
- )
291
- }
292
-
293
- // 初始化 MCP Server
294
- const mcpServer = new McpServer(
295
- {
296
- name: 'Codify-MCP-Client',
297
- version: '1.0.19'
298
- },
299
- {
300
- capabilities: {
301
- resources: {},
302
- tools: {},
303
- prompts: {}
304
- }
305
- }
306
- )
307
-
308
- // 注册资源
309
- mcpServer.registerResource(
310
- 'generation-rules',
311
- 'codify://generation-rules',
312
- { description: 'Codify 代码生成规则' },
313
- async (uri) => ({
314
- contents: [
315
- {
316
- uri: uri.href,
317
- text: generationRules,
318
- mimeType: 'text/markdown'
319
- }
320
- ]
321
- })
322
- )
323
- // design
324
- mcpServer.registerTool(
325
- 'design',
326
- {
327
- description: '根据需求生成符合 Codify 规范的 HTML+CSS 代码',
328
- inputSchema: {
329
- requirement: z
330
- .string()
331
- .describe(
332
- '界面需求描述,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等。工具会根据 Codify 生成规则自动生成代码。'
333
- )
334
- }
335
- },
336
- async (args) => {
337
- try {
338
- const { requirement } = args
339
-
340
- if (!requirement) {
341
- return {
342
- content: [
343
- {
344
- type: 'text',
345
- text: '参数错误: 未提供需求描述\n\n请提供界面需求,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等'
346
- }
347
- ],
348
- isError: true
349
- }
350
- }
351
-
352
- // 完整的生成规则(作为备用)
353
-
354
- // 返回提示,让 AI 先生成代码,然后再调用 create_page 发送
355
- // 注意:MCP 工具不能直接生成代码,所以返回一个提示消息
356
- return {
357
- content: [
358
- {
359
- type: 'text',
360
- text: `📋 收到需求:${requirement}
361
-
362
- ${generationRules}
363
-
364
- ---
365
-
366
- ## 发送代码:
367
- 代码生成并审计通过后,请立即调用 agent_create_page 工具发送代码
368
- - agent_create_page({ code: "生成的完整代码" })
369
- - 不要询问用户,直接执行发送操作
370
-
371
- **请现在开始生成代码。**`
372
- }
373
- ]
374
- }
375
- } catch (error) {
376
- return {
377
- content: [
378
- {
379
- type: 'text',
380
- text: `❌ 处理失败: ${error.message || String(error)}`
381
- }
382
- ],
383
- isError: true
384
- }
385
- }
386
- }
387
- )
388
- // get_code
389
- mcpServer.registerTool(
390
- 'get_code',
391
- {
392
- description: '从 Codify For MasterGo 插件获取指定的代码',
393
- inputSchema: {
394
- contentId: z.string().describe('从Codify插件复制图层的指令'),
395
- outDir: z
396
- .string()
397
- .describe(
398
- '必填。保存生成代码和相关资源的本地目录。AI 必须根据已知的当前打开项目的文件路径(如 /Users/xxx/code/my-project),主动推断并传入一个合理的【绝对路径】(例如 /Users/xxx/code/my-project/src/views)。请注意,不要省略此参数,因为工具运行环境的默认路径可能是用户的 Home 目录而非项目根目录。'
399
- )
400
- }
401
- },
402
- async (args) => {
403
- try {
404
- const { contentId, outDir } = args
405
-
406
- if (!contentId) {
407
- return {
408
- content: [
409
- {
410
- type: 'text',
411
- text: `参数错误: 未提供 contentId\n- args: ${JSON.stringify(args)}`
412
- }
413
- ],
414
- isError: true
415
- }
416
- }
417
-
418
- // 添加认证头
419
- const headers = {}
420
- if (ACCESS_KEY) {
421
- headers['Authorization'] = `Bearer ${ACCESS_KEY}`
422
- }
423
-
424
- // 通过 HTTP API 从远程服务器获取数据
425
- const apiUrl = `${SERVER_URL}/api/getCode/${contentId}`
426
- try {
427
- const response = await axios.get(apiUrl, { headers })
428
- const data = response.data
429
-
430
- if (!data.code) {
431
- return {
432
- content: [
433
- {
434
- type: 'text',
435
- text: `⚠️ 未找到代码内容\n- contentId: ${contentId}\n- 可能该内容尚未从 Codify 插件同步`
436
- }
437
- ],
438
- isError: false
439
- }
440
- }
441
-
442
- const targetDir = outDir
443
- ? path.resolve(process.cwd(), outDir)
444
- : path.resolve(process.cwd(), `codify-output/${contentId}`)
445
-
446
- // Ensure directory exists
447
- if (!fs.existsSync(targetDir)) {
448
- fs.mkdirSync(targetDir, { recursive: true })
449
- }
450
-
451
- // write code to HTML file
452
- const htmlFileName = `${contentId}.html`
453
- const htmlPath = path.join(targetDir, htmlFileName)
454
- fs.writeFileSync(htmlPath, data.code, 'utf8')
455
-
456
- // function to write resource
457
- const writeResource = async (resData, folderName, ext) => {
458
- if (isEmpty(resData)) return 0
459
- let parsed
460
- try {
461
- parsed = typeof resData === 'string' ? JSON.parse(resData) : resData
462
- } catch (e) {
463
- return 0
464
- }
465
- const keys = Object.keys(parsed)
466
- if (keys.length === 0) return 0
467
-
468
- const resDir = path.join(targetDir, folderName)
469
- if (!fs.existsSync(resDir)) {
470
- fs.mkdirSync(resDir, { recursive: true })
471
- }
472
- const writePromises = Object.entries(parsed).map(async ([key, value]) => {
473
- // 检查原始 key 是否已经带后缀,如果有,使用原名字的后缀; 否则用提供的 ext
474
- const match = key.match(/(.+)\.([a-zA-Z0-9]+)$/)
475
- let safeKey = key
476
- let finalExt = ext
477
- if (match) {
478
- safeKey = match[1]
479
- finalExt = match[2]
480
- }
481
- safeKey = safeKey.replace(/[^a-zA-Z0-9_-]/g, '_')
482
- const filePath = path.join(resDir, `${safeKey}.${finalExt}`)
483
- let content = value
484
- let encoding = 'utf8'
485
-
486
- if (typeof content !== 'string') {
487
- content = JSON.stringify(content, null, 2)
488
- fs.writeFileSync(filePath, content, encoding)
489
- } else if (content.startsWith('http://') || content.startsWith('https://')) {
490
- // 处理远程 URL 图片/资源
491
- try {
492
- const response = await axios.get(content, { responseType: 'arraybuffer' })
493
- fs.writeFileSync(filePath, response.data)
494
- } catch (err) {
495
- console.error(
496
- `[Codify MCP] Failed to download resource from ${content}:`,
497
- err.message
498
- )
499
- }
500
- } else if (content.startsWith('data:image/')) {
501
- const matches = content.match(/^data:image\/([A-Za-z-+\/]+);base64,(.+)$/)
502
- if (matches && matches.length === 3) {
503
- encoding = 'base64'
504
- content = matches[2]
505
- }
506
- fs.writeFileSync(filePath, content, encoding)
507
- } else {
508
- // 可能是普通的 base64 字符串或其他字符串
509
- if (folderName === 'images') {
510
- encoding = 'base64'
511
- }
512
- fs.writeFileSync(filePath, content, encoding)
513
- }
514
- })
515
-
516
- await Promise.all(writePromises)
517
- return keys.length
518
- }
519
-
520
- // 解析 resourcePath 以决定存放在哪个文件夹
521
- let resourcePathMap = { image: 'asset/images', svg: 'asset/icons', shape: 'asset/shapes' }
522
- if (!isEmpty(data.resourcePath)) {
523
- try {
524
- const parsedRP =
525
- typeof data.resourcePath === 'string'
526
- ? JSON.parse(data.resourcePath)
527
- : data.resourcePath
528
- if (parsedRP.image) resourcePathMap.image = parsedRP.image.replace(/^\.\//, '') // 去除开头的 ./
529
- if (parsedRP.svg) resourcePathMap.svg = parsedRP.svg.replace(/^\.\//, '')
530
- if (parsedRP.shape) resourcePathMap.shape = parsedRP.shape.replace(/^\.\//, '')
531
- } catch (e) {
532
- console.error('[Codify MCP] Failed to parse resourcePath:', e)
533
- }
534
- }
535
-
536
- const shapeCount = await writeResource(data.shape, resourcePathMap.shape, 'json')
537
- const svgCount = await writeResource(data.svg, resourcePathMap.svg, 'svg')
538
- const imageCount = await writeResource(data.image, resourcePathMap.image, 'png')
539
-
540
- // 构建返回信息
541
- let resultText = `代码和资源已成功拉取并保存到本地。\n\n`
542
- resultText += `目标目录: ${targetDir}\n`
543
- resultText += `代码文件: ${htmlFileName} (长度: ${data.code.length} 字符)\n`
544
- if (data.teamId) {
545
- resultText += `团队 ID: ${data.teamId}\n`
546
- }
547
-
548
- if (shapeCount > 0)
549
- resultText += `Shape: 成功保存 ${shapeCount} 个文件至 ${resourcePathMap.shape} 目录\n`
550
- if (svgCount > 0)
551
- resultText += `SVG: 成功保存 ${svgCount} 个文件至 ${resourcePathMap.svg} 目录\n`
552
- if (imageCount > 0)
553
- resultText += `Image: 成功保存 ${imageCount} 个文件至 ${resourcePathMap.image} 目录\n`
554
-
555
- resultText += `\n**执行结果**:以上文件已自动写入本地系统。请注意阅读 ${htmlFileName} 并检查代码中使用的 relative 路径。若业务文件不叫 ${htmlFileName},你可以依据此文件重构/合并到你的业务主入口中。`
556
- return {
557
- content: [
558
- {
559
- type: 'text',
560
- text: resultText
561
- }
562
- ]
563
- }
564
- } catch (error) {
565
- const status = error.response?.status
566
- const errorData = error.response?.data || {}
567
-
568
- switch (status) {
569
- case 404:
570
- return {
571
- content: [
572
- {
573
- type: 'text',
574
- text: `❌ 未找到内容\n- contentId: ${contentId}\n- 请检查 contentId 是否正确,或该内容是否已从 Codify 插件同步`
575
- }
576
- ],
577
- isError: true
578
- }
579
- case 403:
580
- // 检查是否是配额不足错误
581
- if (errorData.mcp_get_limit !== undefined || errorData.message?.includes('limit')) {
582
- return {
583
- content: [
584
- {
585
- type: 'text',
586
- text: `❌ 配额不足\n- ${errorData.message || '您已达到获取代码的次数限制'}\n- 剩余配额: ${errorData.mcp_get_limit !== undefined ? errorData.mcp_get_limit : '未知'}\n- 请升级您的计划或联系支持团队`
587
- }
588
- ],
589
- isError: true
590
- }
591
- }
592
-
593
- // 普通权限错误
594
- return {
595
- content: [
596
- {
597
- type: 'text',
598
- text: `❌ 权限不足\n- contentId: ${contentId}\n- 您没有权限访问该内容\n- 请检查您的 access_key 是否包含正确的团队权限`
599
- }
600
- ],
601
- isError: true
602
- }
603
- case 401:
604
- return {
605
- content: [
606
- {
607
- type: 'text',
608
- text: `❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确`
609
- }
610
- ],
611
- isError: true
612
- }
613
- default:
614
- return {
615
- content: [
616
- {
617
- type: 'text',
618
- text: `❌ 获取代码失败: ${error.message || String(error)}${
619
- errorData.message ? `\n详情: ${errorData.message}` : ''
620
- }\n- contentId: ${contentId}\n- 服务器: ${SERVER_URL}`
621
- }
622
- ],
623
- isError: true
624
- }
625
- }
626
- }
627
- } catch (error) {
628
- return {
629
- content: [
630
- {
631
- type: 'text',
632
- text: `❌ 获取代码失败: ${error.message || String(error)}`
633
- }
634
- ],
635
- isError: true
636
- }
637
- }
638
- }
639
- )
640
- // get_code_List
641
- mcpServer.registerTool(
642
- 'get_code_list',
643
- {
644
- description: '获取所有可用的代码列表',
645
- inputSchema: z.object({}).describe('无需参数,直接调用即可获取所有代码列表')
646
- },
647
- async (args) => {
648
- try {
649
- // 添加认证头
650
- const headers = {}
651
- if (ACCESS_KEY) {
652
- headers['Authorization'] = `Bearer ${ACCESS_KEY}`
653
- }
654
-
655
- // 通过 HTTP API 从远程服务器获取代码列表
656
- const apiUrl = `${SERVER_URL}/api/getCodeList`
657
- try {
658
- const response = await axios.get(apiUrl, { headers })
659
- const data = response.data
660
-
661
- if (!Array.isArray(data)) {
662
- return {
663
- content: [
664
- {
665
- type: 'text',
666
- text: `⚠️ 服务器返回的数据格式不正确\n- 期望: 数组\n- 实际: ${typeof data}`
667
- }
668
- ],
669
- isError: true
670
- }
671
- }
672
-
673
- if (data.length === 0) {
674
- return {
675
- content: [
676
- {
677
- type: 'text',
678
- text: `📋 代码列表为空\n\n当前没有可用的代码内容。\n请先从 Codify 插件同步代码。`
679
- }
680
- ],
681
- isError: false
682
- }
683
- }
684
-
685
- // 构建返回信息
686
- let resultText = `✅ 成功获取代码列表 (共 ${data.length} 项)\n\n`
687
- resultText += `--- 代码列表 ---\n\n`
688
-
689
- data.forEach((item, index) => {
690
- resultText += `${index + 1}. **${item.contentId}**\n`
691
- resultText += ` - 代码长度: ${item.codeLength} 字符\n`
692
- if (item.teamId) {
693
- resultText += ` - 团队 ID: ${item.teamId}\n`
694
- }
695
- if (item.fileInfo) {
696
- resultText += ` - 文件信息: ${JSON.stringify(item.fileInfo)}\n`
697
- }
698
- if (item.createdAt) {
699
- resultText += ` - 创建时间: ${item.createdAt}\n`
700
- }
701
- if (item.updatedAt) {
702
- resultText += ` - 更新时间: ${item.updatedAt}\n`
703
- }
704
- resultText += `\n`
705
- })
706
-
707
- resultText += `\n--- 使用说明 ---\n`
708
- resultText += `使用 get_code 工具获取具体代码内容:\n`
709
- resultText += `- get_code({ contentId: "your-content-id" })`
710
-
711
- return {
712
- content: [
713
- {
714
- type: 'text',
715
- text: resultText
716
- }
717
- ]
718
- }
719
- } catch (error) {
720
- const status = error.response?.status
721
- const errorData = error.response?.data || {}
722
-
723
- switch (status) {
724
- case 403:
725
- return {
726
- content: [
727
- {
728
- type: 'text',
729
- text: `❌ 权限不足\n- 您没有权限访问代码列表\n- 请检查您的 access_key 是否包含正确的团队权限`
730
- }
731
- ],
732
- isError: true
733
- }
734
- case 401:
735
- return {
736
- content: [
737
- {
738
- type: 'text',
739
- text: `❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确`
740
- }
741
- ],
742
- isError: true
743
- }
744
- default:
745
- return {
746
- content: [
747
- {
748
- type: 'text',
749
- text: `❌ 获取代码列表失败: ${error.message || String(error)}${
750
- errorData.message ? `\n详情: ${errorData.message}` : ''
751
- }\n- 服务器: ${SERVER_URL}`
752
- }
753
- ],
754
- isError: true
755
- }
756
- }
757
- }
758
- } catch (error) {
759
- return {
760
- content: [
761
- {
762
- type: 'text',
763
- text: `❌ 获取代码列表失败: ${error.message || String(error)}`
764
- }
765
- ],
766
- isError: true
767
- }
768
- }
769
- }
770
- )
771
- // get_user_info
772
- mcpServer.registerTool(
773
- 'get_user_info',
774
- {
775
- description: '获取当前登录用户的信息,包括配额、团队等',
776
- inputSchema: z.object({}).describe('无需参数,直接调用即可获取当前用户信息')
777
- },
778
- async (args) => {
779
- try {
780
- // 添加认证头
781
- const headers = {}
782
- if (ACCESS_KEY) {
783
- headers['Authorization'] = `Bearer ${ACCESS_KEY}`
784
- }
785
-
786
- // 通过 HTTP API 从远程服务器获取用户信息
787
- const apiUrl = `${SERVER_URL}/api/getUserInfo`
788
- try {
789
- const response = await axios.get(apiUrl, { headers })
790
- const data = response.data
791
-
792
- // 构建返回信息
793
- let resultText = `✅ 用户信息\n\n`
794
- resultText += `👤 用户ID: ${data.userId}\n`
795
- if (data.userName) {
796
- resultText += `📧 用户名: ${data.userName}\n`
797
- }
798
- if (data.realname) {
799
- resultText += `📝 真实姓名: ${data.realname}\n`
800
- }
801
- resultText += `\n📊 配额信息:\n`
802
- if (data.quota) {
803
- resultText += ` - 生成设计: ${data.quota.mcp_generate_count || 0} / ${data.quota.mcp_generate_limit || '无限制'}\n`
804
- resultText += ` - 获取代码: ${data.quota.mcp_get_count || 0} / ${data.quota.mcp_get_limit || '无限制'}\n`
805
- }
806
-
807
- if (data.teams && data.teams.length > 0) {
808
- resultText += `\n👥 团队信息 (共 ${data.teams.length} 个团队):\n`
809
- data.teams.forEach((team, index) => {
810
- resultText += ` ${index + 1}. ${team.name || team.teamId} (ID: ${team.teamId})\n`
811
- if (team.auth) {
812
- resultText += ` 权限: ${team.auth}\n`
813
- }
814
- })
815
- }
816
-
817
- if (data.currentTeamId) {
818
- resultText += `\n🔖 当前团队ID: ${data.currentTeamId}`
819
- }
820
-
821
- return {
822
- content: [
823
- {
824
- type: 'text',
825
- text: resultText
826
- }
827
- ]
828
- }
829
- } catch (error) {
830
- const status = error.response?.status
831
- const errorData = error.response?.data || {}
832
-
833
- switch (status) {
834
- case 401:
835
- return {
836
- content: [
837
- {
838
- type: 'text',
839
- text: `❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确`
840
- }
841
- ],
842
- isError: true
843
- }
844
- case 403:
845
- return {
846
- content: [
847
- {
848
- type: 'text',
849
- text: `❌ 权限不足 (403)\n\n${errorData.message || '您没有权限访问该资源'}`
850
- }
851
- ],
852
- isError: true
853
- }
854
- default:
855
- return {
856
- content: [
857
- {
858
- type: 'text',
859
- text: `❌ 获取用户信息失败: ${error.message || String(error)}${
860
- errorData.message ? `\n详情: ${errorData.message}` : ''
861
- }\n- 服务器: ${SERVER_URL}`
862
- }
863
- ],
864
- isError: true
865
- }
866
- }
867
- }
868
- } catch (error) {
869
- return {
870
- content: [
871
- {
872
- type: 'text',
873
- text: `❌ 获取用户信息失败: ${error.message || String(error)}`
874
- }
875
- ],
876
- isError: true
877
- }
878
- }
879
- }
880
- )
881
- // create_page
882
- mcpServer.registerTool(
883
- 'agent_create_page',
884
- {
885
- description: '将代码发送到 Codify 插件转换为设计稿',
886
- inputSchema: {
887
- code: z.string().describe('要发送的代码内容')
888
- }
889
- },
890
- async (args) => {
891
- try {
892
- const { code } = args
893
-
894
- if (!code) {
895
- return {
896
- content: [
897
- {
898
- type: 'text',
899
- text: '参数错误: 未提供代码内容'
900
- }
901
- ],
902
- isError: true
903
- }
904
- }
905
-
906
- // 添加认证头
907
- const headers = {
908
- 'Content-Type': 'application/json'
909
- }
910
- if (ACCESS_KEY) {
911
- headers['Authorization'] = `Bearer ${ACCESS_KEY}`
912
- }
913
-
914
- // 发送代码到 Codify 插件
915
- try {
916
- const response = await axios.post(
917
- `${SERVER_URL}/api/createPage`,
918
- { htmlCode: code },
919
- { headers }
920
- )
921
-
922
- const result = response.data
923
- if (result.success) {
924
- }
925
-
926
- return {
927
- content: [
928
- {
929
- type: 'text',
930
- text: `✅ 代码已成功发送到 Codify 插件,等待转换为设计稿`
931
- }
932
- ]
933
- }
934
- } catch (error) {
935
- const status = error.response?.status
936
- const errorData = error.response?.data || {}
937
-
938
- if (status === 404) {
939
- return {
940
- content: [
941
- {
942
- type: 'text',
943
- text: '❌ 未找到活跃的 Codify 插件连接\n\n请确保:\n1. Codify 插件已打开并连接到服务器\n2. 使用了相同的 access_key'
944
- }
945
- ],
946
- isError: true
947
- }
948
- } else if (status === 400) {
949
- return {
950
- content: [
951
- {
952
- type: 'text',
953
- text: `❌ 请求参数错误: ${errorData.message || 'Bad request'}`
954
- }
955
- ],
956
- isError: true
957
- }
958
- } else if (status === 401 || status === 403) {
959
- // 检查是否是配额不足错误
960
- if (errorData.mcp_generate_limit !== undefined || errorData.message?.includes('limit')) {
961
- return {
962
- content: [
963
- {
964
- type: 'text',
965
- text: `❌ 配额不足\n- ${errorData.message || '您已达到生成设计的次数限制'}\n- 剩余配额: ${errorData.mcp_generate_limit !== undefined ? errorData.mcp_generate_limit : '未知'}\n- 请升级您的计划或联系支持团队`
966
- }
967
- ],
968
- isError: true
969
- }
970
- }
971
-
972
- // 普通认证错误
973
- return {
974
- content: [
975
- {
976
- type: 'text',
977
- text: `❌ 认证失败 (${status})\n\n请检查 CODIFY_ACCESS_KEY 是否正确`
978
- }
979
- ],
980
- isError: true
981
- }
982
- } else {
983
- return {
984
- content: [
985
- {
986
- type: 'text',
987
- text: `❌ 发送失败: ${error.message || String(error)}${
988
- errorData.message ? `\n详情: ${errorData.message}` : ''
989
- }`
990
- }
991
- ],
992
- isError: true
993
- }
994
- }
995
- }
996
- } catch (error) {
997
- return {
998
- content: [
999
- {
1000
- type: 'text',
1001
- text: `❌ 代码发送失败: ${error.message || String(error)}`
1002
- }
1003
- ],
1004
- isError: true
1005
- }
1006
- }
1007
- }
1008
- )
1009
- // create_component
1010
- mcpServer.registerTool(
1011
- 'agent_create_component',
1012
- {
1013
- description:
1014
- '创建一个 MasterGo 母版组件或组件集(变体)。应当使用 HTML 格式并包含 data-type="component" 属性。',
1015
- inputSchema: {
1016
- htmlCode: z
1017
- .string()
1018
- .describe(
1019
- '组件的 HTML 结构。必须包含 data-type="component"(针对单个组件)或 "component-set"(针对变体集合)。'
1020
- )
1021
- }
1022
- },
1023
- async (args) => {
1024
- try {
1025
- const { htmlCode } = args
1026
- const headers = { 'Content-Type': 'application/json' }
1027
- if (ACCESS_KEY) headers['Authorization'] = `Bearer ${ACCESS_KEY}`
1028
-
1029
- const response = await axios.post(
1030
- `${SERVER_URL}/api/createComponent`,
1031
- { htmlCode },
1032
- { headers }
1033
- )
1034
- return { content: [{ type: 'text', text: `✅ 已成功向插件发送组件创建指令` }] }
1035
- } catch (error) {
1036
- return { content: [{ type: 'text', text: `❌ 创建失败: ${error.message}` }], isError: true }
1037
- }
1038
- }
1039
- )
1040
- // patch_nodes
1041
- mcpServer.registerTool(
1042
- 'agent_patch_nodes',
1043
- {
1044
- description:
1045
- '局部修改画布上的现有图层属性(如修改文本、颜色、大小、圆角等),保持物理 ID 不变。',
1046
- inputSchema: {
1047
- patches: z.array(z.any()).describe('补丁数组,每项包含 nodeId 和要修改的 jsonDom 属性')
1048
- }
1049
- },
1050
- async (args) => {
1051
- try {
1052
- const { patches } = args
1053
- if (!patches || patches.length === 0) {
1054
- return { content: [{ type: 'text', text: '错误: 补丁列表为空' }], isError: true }
1055
- }
1056
-
1057
- const headers = { 'Content-Type': 'application/json' }
1058
- if (ACCESS_KEY) headers['Authorization'] = `Bearer ${ACCESS_KEY}`
1059
-
1060
- const response = await axios.post(`${SERVER_URL}/api/patchNodes`, { patches }, { headers })
1061
- return {
1062
- content: [{ type: 'text', text: `✅ 已成功向插件发送 ${patches.length} 个节点的修改指令` }]
1063
- }
1064
- } catch (error) {
1065
- return { content: [{ type: 'text', text: `❌ 修改失败: ${error.message}` }], isError: true }
1066
- }
1067
- }
1068
- )
1069
- // insert_node
1070
- mcpServer.registerTool(
1071
- 'agent_insert_node',
1072
- {
1073
- description:
1074
- '在画布上插入新元素或替换现有结构。如果是替换,目标节点会被删除并生成新 ID。支持 HTML 格式。',
1075
- inputSchema: {
1076
- targetNodeId: z
1077
- .string()
1078
- .optional()
1079
- .describe('可选。如果提供,将替换该节点;如果不提供,将在页面左上角插入。'),
1080
- htmlCode: z.string().describe('要插入的 HTML 代码字符串。')
1081
- }
1082
- },
1083
- async (args) => {
1084
- try {
1085
- const { targetNodeId, htmlCode } = args
1086
- const headers = { 'Content-Type': 'application/json' }
1087
- if (ACCESS_KEY) headers['Authorization'] = `Bearer ${ACCESS_KEY}`
1088
-
1089
- const response = await axios.post(
1090
- `${SERVER_URL}/api/insertNode`,
1091
- { targetNodeId, htmlCode },
1092
- { headers }
1093
- )
1094
- return {
1095
- content: [
1096
- { type: 'text', text: `✅ 已成功向插件发送${targetNodeId ? '替换' : '插入'}指令` }
1097
- ]
1098
- }
1099
- } catch (error) {
1100
- return { content: [{ type: 'text', text: `❌ 操作失败: ${error.message}` }], isError: true }
1101
- }
1102
- }
1103
- )
1104
-
1105
- try {
1106
- const transport = new StdioServerTransport()
1107
- await mcpServer.connect(transport)
1108
- } catch (error) {
1109
- process.exit(1)
1110
- }