@codify-ai/mcp-client 1.0.16 → 1.0.18
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 +328 -184
- package/mcp-client.min.js +1 -1
- package/package.json +3 -6
package/mcp-client.js
CHANGED
|
@@ -8,77 +8,6 @@ import { z } from 'zod'
|
|
|
8
8
|
import axios from 'axios'
|
|
9
9
|
import fs from 'fs'
|
|
10
10
|
import path from 'path'
|
|
11
|
-
import os from 'os'
|
|
12
|
-
import { fileURLToPath } from 'url'
|
|
13
|
-
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
15
|
-
const __dirname = path.dirname(__filename)
|
|
16
|
-
|
|
17
|
-
function installSkills() {
|
|
18
|
-
const skillName = 'codify-gen'
|
|
19
|
-
const sourceDir = path.join(__dirname, 'skills', skillName)
|
|
20
|
-
if (!fs.existsSync(sourceDir)) return
|
|
21
|
-
|
|
22
|
-
const home = os.homedir()
|
|
23
|
-
const cwd = process.cwd()
|
|
24
|
-
|
|
25
|
-
// 优先使用环境变量指定的路径
|
|
26
|
-
const envDir = process.env.CODIFY_SKILLS_DIR
|
|
27
|
-
? path.join(process.env.CODIFY_SKILLS_DIR, skillName)
|
|
28
|
-
: null
|
|
29
|
-
|
|
30
|
-
if (envDir) {
|
|
31
|
-
const targets = [envDir]
|
|
32
|
-
for (const targetDir of targets) {
|
|
33
|
-
try {
|
|
34
|
-
fs.mkdirSync(targetDir, { recursive: true })
|
|
35
|
-
for (const file of fs.readdirSync(sourceDir)) {
|
|
36
|
-
fs.copyFileSync(path.join(sourceDir, file), path.join(targetDir, file))
|
|
37
|
-
}
|
|
38
|
-
} catch { /* 静默失败 */ }
|
|
39
|
-
}
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 优先安装到当前项目(Cursor 启动 MCP 时 cwd 为项目根目录)
|
|
44
|
-
const projectSkillsDir = path.join(cwd, '.cursor', 'skills')
|
|
45
|
-
const projectTarget = path.join(projectSkillsDir, skillName)
|
|
46
|
-
const hasProjectCursor = fs.existsSync(path.join(cwd, '.cursor'))
|
|
47
|
-
|
|
48
|
-
// 自动探测用户级 IDE skills 目录
|
|
49
|
-
const candidates = [
|
|
50
|
-
path.join(home, '.cursor', 'skills', skillName), // Cursor
|
|
51
|
-
path.join(home, '.windsurf', 'skills', skillName), // Windsurf
|
|
52
|
-
path.join(home, '.codeium', 'windsurf', 'skills', skillName), // Codeium
|
|
53
|
-
path.join(home, '.trae', 'skills', skillName), // Trae
|
|
54
|
-
]
|
|
55
|
-
const userTargets = candidates.filter(p => fs.existsSync(path.dirname(path.dirname(p))))
|
|
56
|
-
|
|
57
|
-
// 构建最终安装列表:项目级优先,用户级兜底
|
|
58
|
-
const targets = []
|
|
59
|
-
if (hasProjectCursor) targets.push(projectTarget)
|
|
60
|
-
targets.push(...userTargets)
|
|
61
|
-
|
|
62
|
-
// 若都没探测到,回退到 Cursor 用户级
|
|
63
|
-
if (targets.length === 0) {
|
|
64
|
-
targets.push(path.join(home, '.cursor', 'skills', skillName))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
for (const targetDir of targets) {
|
|
68
|
-
try {
|
|
69
|
-
fs.mkdirSync(targetDir, { recursive: true })
|
|
70
|
-
for (const file of fs.readdirSync(sourceDir)) {
|
|
71
|
-
const src = path.join(sourceDir, file)
|
|
72
|
-
const dest = path.join(targetDir, file)
|
|
73
|
-
fs.copyFileSync(src, dest)
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
// 静默失败,不阻断 MCP 启动
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
installSkills()
|
|
82
11
|
|
|
83
12
|
function parseArgs() {
|
|
84
13
|
const args = process.argv.slice(2)
|
|
@@ -86,6 +15,38 @@ function parseArgs() {
|
|
|
86
15
|
for (let i = 0; i < args.length; i++) {
|
|
87
16
|
const arg = args[i]
|
|
88
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
|
+
`)
|
|
89
50
|
process.exit(0)
|
|
90
51
|
}
|
|
91
52
|
if (arg === '--version' || arg === '-v') {
|
|
@@ -105,6 +66,220 @@ function parseArgs() {
|
|
|
105
66
|
const { serverUrl: SERVER_URL } = parseArgs()
|
|
106
67
|
const ACCESS_KEY = process.env.CODIFY_ACCESS_KEY
|
|
107
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
|
+
|
|
108
283
|
// 辅助函数:检查值是否为空
|
|
109
284
|
function isEmpty(value) {
|
|
110
285
|
return (
|
|
@@ -119,7 +294,7 @@ function isEmpty(value) {
|
|
|
119
294
|
const mcpServer = new McpServer(
|
|
120
295
|
{
|
|
121
296
|
name: 'Codify-MCP-Client',
|
|
122
|
-
version: '1.0.
|
|
297
|
+
version: '1.0.18'
|
|
123
298
|
},
|
|
124
299
|
{
|
|
125
300
|
capabilities: {
|
|
@@ -130,6 +305,21 @@ const mcpServer = new McpServer(
|
|
|
130
305
|
}
|
|
131
306
|
)
|
|
132
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
|
+
)
|
|
133
323
|
// get_code
|
|
134
324
|
mcpServer.registerTool(
|
|
135
325
|
'get_code',
|
|
@@ -623,6 +813,72 @@ mcpServer.registerTool(
|
|
|
623
813
|
}
|
|
624
814
|
}
|
|
625
815
|
)
|
|
816
|
+
// design
|
|
817
|
+
mcpServer.registerTool(
|
|
818
|
+
'design',
|
|
819
|
+
|
|
820
|
+
{
|
|
821
|
+
description: '根据需求生成符合 Codify 规范的 HTML+CSS 代码',
|
|
822
|
+
inputSchema: {
|
|
823
|
+
requirement: z
|
|
824
|
+
.string()
|
|
825
|
+
.describe(
|
|
826
|
+
'界面需求描述,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等。工具会根据 Codify 生成规则自动生成代码。'
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
async (args) => {
|
|
831
|
+
try {
|
|
832
|
+
const { requirement } = args
|
|
833
|
+
|
|
834
|
+
if (!requirement) {
|
|
835
|
+
return {
|
|
836
|
+
content: [
|
|
837
|
+
{
|
|
838
|
+
type: 'text',
|
|
839
|
+
text: '参数错误: 未提供需求描述\n\n请提供界面需求,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等'
|
|
840
|
+
}
|
|
841
|
+
],
|
|
842
|
+
isError: true
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 完整的生成规则(作为备用)
|
|
847
|
+
|
|
848
|
+
// 返回提示,让 AI 先生成代码,然后再调用 send_to_codify 发送
|
|
849
|
+
// 注意:MCP 工具不能直接生成代码,所以返回一个提示消息
|
|
850
|
+
return {
|
|
851
|
+
content: [
|
|
852
|
+
{
|
|
853
|
+
type: 'text',
|
|
854
|
+
text: `📋 收到需求:${requirement}
|
|
855
|
+
|
|
856
|
+
${generationRules}
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## 发送代码:
|
|
861
|
+
代码生成并审计通过后,请立即调用 send_to_codify 工具发送代码
|
|
862
|
+
- send_to_codify({ code: "生成的完整代码" })
|
|
863
|
+
- 不要询问用户,直接执行发送操作
|
|
864
|
+
|
|
865
|
+
**请现在开始生成代码。**`
|
|
866
|
+
}
|
|
867
|
+
]
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
return {
|
|
871
|
+
content: [
|
|
872
|
+
{
|
|
873
|
+
type: 'text',
|
|
874
|
+
text: `❌ 处理失败: ${error.message || String(error)}`
|
|
875
|
+
}
|
|
876
|
+
],
|
|
877
|
+
isError: true
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
)
|
|
626
882
|
// send_to_codify
|
|
627
883
|
mcpServer.registerTool(
|
|
628
884
|
'send_to_codify',
|
|
@@ -748,118 +1004,6 @@ mcpServer.registerTool(
|
|
|
748
1004
|
}
|
|
749
1005
|
)
|
|
750
1006
|
|
|
751
|
-
// upload_image
|
|
752
|
-
mcpServer.registerTool(
|
|
753
|
-
'upload_image',
|
|
754
|
-
{
|
|
755
|
-
description: '将本地图片文件上传缓存到 MCP 服务器的 data/cache 目录,返回缓存路径',
|
|
756
|
-
inputSchema: {
|
|
757
|
-
filePath: z
|
|
758
|
-
.string()
|
|
759
|
-
.describe('本地图片文件的绝对路径,例如 /Users/xxx/Desktop/image.png')
|
|
760
|
-
}
|
|
761
|
-
},
|
|
762
|
-
async (args) => {
|
|
763
|
-
try {
|
|
764
|
-
const { filePath } = args
|
|
765
|
-
|
|
766
|
-
if (!filePath) {
|
|
767
|
-
return {
|
|
768
|
-
content: [{ type: 'text', text: '参数错误: 未提供 filePath' }],
|
|
769
|
-
isError: true
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// 读取本地文件
|
|
774
|
-
let fileBuffer
|
|
775
|
-
try {
|
|
776
|
-
fileBuffer = fs.readFileSync(filePath)
|
|
777
|
-
} catch (readError) {
|
|
778
|
-
return {
|
|
779
|
-
content: [
|
|
780
|
-
{
|
|
781
|
-
type: 'text',
|
|
782
|
-
text: `❌ 无法读取文件: ${filePath}\n错误: ${readError.message || String(readError)}`
|
|
783
|
-
}
|
|
784
|
-
],
|
|
785
|
-
isError: true
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const fileName = path.basename(filePath)
|
|
790
|
-
const ext = path.extname(fileName).toLowerCase()
|
|
791
|
-
const mimeMap = {
|
|
792
|
-
'.png': 'image/png',
|
|
793
|
-
'.jpg': 'image/jpeg',
|
|
794
|
-
'.jpeg': 'image/jpeg',
|
|
795
|
-
'.gif': 'image/gif',
|
|
796
|
-
'.webp': 'image/webp',
|
|
797
|
-
'.svg': 'image/svg+xml',
|
|
798
|
-
'.bmp': 'image/bmp',
|
|
799
|
-
'.ico': 'image/x-icon'
|
|
800
|
-
}
|
|
801
|
-
const mimeType = mimeMap[ext] || 'application/octet-stream'
|
|
802
|
-
const base64Data = fileBuffer.toString('base64')
|
|
803
|
-
|
|
804
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
805
|
-
if (ACCESS_KEY) {
|
|
806
|
-
headers['Authorization'] = `Bearer ${ACCESS_KEY}`
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
try {
|
|
810
|
-
const response = await axios.post(
|
|
811
|
-
`${SERVER_URL}/api/uploadImage`,
|
|
812
|
-
{ imageData: base64Data, fileName, mimeType },
|
|
813
|
-
{ headers }
|
|
814
|
-
)
|
|
815
|
-
const result = response.data
|
|
816
|
-
return {
|
|
817
|
-
content: [
|
|
818
|
-
{
|
|
819
|
-
type: 'text',
|
|
820
|
-
text: `✅ 图片已成功缓存到服务器\n- 原始文件: ${fileName}\n- 缓存路径: ${result.cachePath}\n- 文件大小: ${result.size} bytes\n- 类型: ${result.mimeType || mimeType}`
|
|
821
|
-
}
|
|
822
|
-
]
|
|
823
|
-
}
|
|
824
|
-
} catch (error) {
|
|
825
|
-
const status = error.response?.status
|
|
826
|
-
const errorData = error.response?.data || {}
|
|
827
|
-
|
|
828
|
-
if (status === 401 || status === 403) {
|
|
829
|
-
return {
|
|
830
|
-
content: [
|
|
831
|
-
{
|
|
832
|
-
type: 'text',
|
|
833
|
-
text: `❌ 认证失败 (${status})\n\n请检查 CODIFY_ACCESS_KEY 是否正确`
|
|
834
|
-
}
|
|
835
|
-
],
|
|
836
|
-
isError: true
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return {
|
|
840
|
-
content: [
|
|
841
|
-
{
|
|
842
|
-
type: 'text',
|
|
843
|
-
text: `❌ 上传失败: ${error.message || String(error)}${errorData.message ? `\n详情: ${errorData.message}` : ''}`
|
|
844
|
-
}
|
|
845
|
-
],
|
|
846
|
-
isError: true
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
} catch (error) {
|
|
850
|
-
return {
|
|
851
|
-
content: [
|
|
852
|
-
{
|
|
853
|
-
type: 'text',
|
|
854
|
-
text: `❌ 图片上传失败: ${error.message || String(error)}`
|
|
855
|
-
}
|
|
856
|
-
],
|
|
857
|
-
isError: true
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
)
|
|
862
|
-
|
|
863
1007
|
try {
|
|
864
1008
|
const transport = new StdioServerTransport()
|
|
865
1009
|
await mcpServer.connect(transport)
|
package/mcp-client.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z}from"zod";import axios from"axios";import t from"fs";import e from"path";import n from"os";import{fileURLToPath as r}from"url";const s=r(import.meta.url),o=e.dirname(s);!function(){const r="codify-gen",s=e.join(o,"skills",r);if(!t.existsSync(s))return;const i=n.homedir(),a=process.cwd(),c=process.env.CODIFY_SKILLS_DIR?e.join(process.env.CODIFY_SKILLS_DIR,r):null;if(c){const n=[c];for(const r of n)try{t.mkdirSync(r,{recursive:!0});for(const n of t.readdirSync(s))t.copyFileSync(e.join(s,n),e.join(r,n))}catch{}return}const p=e.join(a,".cursor","skills"),m=e.join(p,r),g=t.existsSync(e.join(a,".cursor")),d=[e.join(i,".cursor","skills",r),e.join(i,".windsurf","skills",r),e.join(i,".codeium","windsurf","skills",r),e.join(i,".trae","skills",r)].filter(n=>t.existsSync(e.dirname(e.dirname(n)))),l=[];g&&l.push(m),l.push(...d),0===l.length&&l.push(e.join(i,".cursor","skills",r));for(const n of l)try{t.mkdirSync(n,{recursive:!0});for(const r of t.readdirSync(s)){const o=e.join(s,r),i=e.join(n,r);t.copyFileSync(o,i)}}catch{}}();const{serverUrl:SERVER_URL}=function(){const t=process.argv.slice(2);let e=process.env.CODIFY_SERVER_URL||"http://localhost:8080";for(let n=0;n<t.length;n++){const r=t[n];"--help"!==r&&"-h"!==r||process.exit(0),"--version"!==r&&"-v"!==r||process.exit(0),r.startsWith("--url=")?e=r.substring(6):"--url"===r&&n+1<t.length?e=t[++n]:r.startsWith("-")||(e=r)}return{serverUrl:e}}(),ACCESS_KEY=process.env.CODIFY_ACCESS_KEY;function i(t){return null==t||""===t||"string"==typeof t&&""===t.trim()}const a=new McpServer({name:"Codify-MCP-Client",version:"1.0.16"},{capabilities:{resources:{},tools:{},prompts:{}}});a.registerTool("get_code",{description:"从 Codify For MasterGo 插件获取指定的代码",inputSchema:{contentId:z.string().describe("从Codify插件复制图层的指令"),outDir:z.string().describe("必填。保存生成代码和相关资源的本地目录。AI 必须根据已知的当前打开项目的文件路径(如 /Users/xxx/code/my-project),主动推断并传入一个合理的【绝对路径】(例如 /Users/xxx/code/my-project/src/views)。请注意,不要省略此参数,因为工具运行环境的默认路径可能是用户的 Home 目录而非项目根目录。")}},async n=>{try{const{contentId:r,outDir:s}=n;if(!r)return{content:[{type:"text",text:`参数错误: 未提供 contentId\n- args: ${JSON.stringify(n)}`}],isError:!0};const o={};ACCESS_KEY&&(o.Authorization=`Bearer ${ACCESS_KEY}`);const a=`${SERVER_URL}/api/getCode/${r}`;try{const n=(await axios.get(a,{headers:o})).data;if(!n.code)return{content:[{type:"text",text:`⚠️ 未找到代码内容\n- contentId: ${r}\n- 可能该内容尚未从 Codify 插件同步`}],isError:!1};const c=s?e.resolve(process.cwd(),s):e.resolve(process.cwd(),`codify-output/${r}`);t.existsSync(c)||t.mkdirSync(c,{recursive:!0});const p=`${r}.html`,m=e.join(c,p);t.writeFileSync(m,n.code,"utf8");const g=async(n,r,s)=>{if(i(n))return 0;let o;try{o="string"==typeof n?JSON.parse(n):n}catch(t){return 0}const a=Object.keys(o);if(0===a.length)return 0;const p=e.join(c,r);t.existsSync(p)||t.mkdirSync(p,{recursive:!0});const m=Object.entries(o).map(async([n,o])=>{const i=n.match(/(.+)\.([a-zA-Z0-9]+)$/);let a=n,c=s;i&&(a=i[1],c=i[2]),a=a.replace(/[^a-zA-Z0-9_-]/g,"_");const m=e.join(p,`${a}.${c}`);let g=o,d="utf8";if("string"!=typeof g)g=JSON.stringify(g,null,2),t.writeFileSync(m,g,d);else if(g.startsWith("http://")||g.startsWith("https://"))try{const e=await axios.get(g,{responseType:"arraybuffer"});t.writeFileSync(m,e.data)}catch(t){console.error(`[Codify MCP] Failed to download resource from ${g}:`,t.message)}else if(g.startsWith("data:image/")){const e=g.match(/^data:image\/([A-Za-z-+\/]+);base64,(.+)$/);e&&3===e.length&&(d="base64",g=e[2]),t.writeFileSync(m,g,d)}else"images"===r&&(d="base64"),t.writeFileSync(m,g,d)});return await Promise.all(m),a.length};let d={image:"asset/images",svg:"asset/icons",shape:"asset/shapes"};if(!i(n.resourcePath))try{const t="string"==typeof n.resourcePath?JSON.parse(n.resourcePath):n.resourcePath;t.image&&(d.image=t.image.replace(/^\.\//,"")),t.svg&&(d.svg=t.svg.replace(/^\.\//,"")),t.shape&&(d.shape=t.shape.replace(/^\.\//,""))}catch(t){console.error("[Codify MCP] Failed to parse resourcePath:",t)}const l=await g(n.shape,d.shape,"json"),y=await g(n.svg,d.svg,"svg"),u=await g(n.image,d.image,"png");let x="代码和资源已成功拉取并保存到本地。\n\n";return x+=`目标目录: ${c}\n`,x+=`代码文件: ${p} (长度: ${n.code.length} 字符)\n`,n.teamId&&(x+=`团队 ID: ${n.teamId}\n`),l>0&&(x+=`Shape: 成功保存 ${l} 个文件至 ${d.shape} 目录\n`),y>0&&(x+=`SVG: 成功保存 ${y} 个文件至 ${d.svg} 目录\n`),u>0&&(x+=`Image: 成功保存 ${u} 个文件至 ${d.image} 目录\n`),x+=`\n**执行结果**:以上文件已自动写入本地系统。请注意阅读 ${p} 并检查代码中使用的 relative 路径。若业务文件不叫 ${p},你可以依据此文件重构/合并到你的业务主入口中。`,{content:[{type:"text",text:x}]}}catch(t){const e=t.response?.status,n=t.response?.data||{};switch(e){case 404:return{content:[{type:"text",text:`❌ 未找到内容\n- contentId: ${r}\n- 请检查 contentId 是否正确,或该内容是否已从 Codify 插件同步`}],isError:!0};case 403:return void 0!==n.mcp_get_limit||n.message?.includes("limit")?{content:[{type:"text",text:`❌ 配额不足\n- ${n.message||"您已达到获取代码的次数限制"}\n- 剩余配额: ${void 0!==n.mcp_get_limit?n.mcp_get_limit:"未知"}\n- 请升级您的计划或联系支持团队`}],isError:!0}:{content:[{type:"text",text:`❌ 权限不足\n- contentId: ${r}\n- 您没有权限访问该内容\n- 请检查您的 access_key 是否包含正确的团队权限`}],isError:!0};case 401:return{content:[{type:"text",text:"❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确"}],isError:!0};default:return{content:[{type:"text",text:`❌ 获取代码失败: ${t.message||String(t)}${n.message?`\n详情: ${n.message}`:""}\n- contentId: ${r}\n- 服务器: ${SERVER_URL}`}],isError:!0}}}}catch(t){return{content:[{type:"text",text:`❌ 获取代码失败: ${t.message||String(t)}`}],isError:!0}}}),a.registerTool("get_code_list",{description:"获取所有可用的代码列表",inputSchema:z.object({}).describe("无需参数,直接调用即可获取所有代码列表")},async t=>{try{const t={};ACCESS_KEY&&(t.Authorization=`Bearer ${ACCESS_KEY}`);const e=`${SERVER_URL}/api/getCodeList`;try{const n=(await axios.get(e,{headers:t})).data;if(!Array.isArray(n))return{content:[{type:"text",text:"⚠️ 服务器返回的数据格式不正确\n- 期望: 数组\n- 实际: "+typeof n}],isError:!0};if(0===n.length)return{content:[{type:"text",text:"📋 代码列表为空\n\n当前没有可用的代码内容。\n请先从 Codify 插件同步代码。"}],isError:!1};let r=`✅ 成功获取代码列表 (共 ${n.length} 项)\n\n`;return r+="--- 代码列表 ---\n\n",n.forEach((t,e)=>{r+=`${e+1}. **${t.contentId}**\n`,r+=` - 代码长度: ${t.codeLength} 字符\n`,t.teamId&&(r+=` - 团队 ID: ${t.teamId}\n`),t.fileInfo&&(r+=` - 文件信息: ${JSON.stringify(t.fileInfo)}\n`),t.createdAt&&(r+=` - 创建时间: ${t.createdAt}\n`),t.updatedAt&&(r+=` - 更新时间: ${t.updatedAt}\n`),r+="\n"}),r+="\n--- 使用说明 ---\n",r+="使用 get_code 工具获取具体代码内容:\n",r+='- get_code({ contentId: "your-content-id" })',{content:[{type:"text",text:r}]}}catch(t){const e=t.response?.status,n=t.response?.data||{};switch(e){case 403:return{content:[{type:"text",text:"❌ 权限不足\n- 您没有权限访问代码列表\n- 请检查您的 access_key 是否包含正确的团队权限"}],isError:!0};case 401:return{content:[{type:"text",text:"❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确"}],isError:!0};default:return{content:[{type:"text",text:`❌ 获取代码列表失败: ${t.message||String(t)}${n.message?`\n详情: ${n.message}`:""}\n- 服务器: ${SERVER_URL}`}],isError:!0}}}}catch(t){return{content:[{type:"text",text:`❌ 获取代码列表失败: ${t.message||String(t)}`}],isError:!0}}}),a.registerTool("get_user_info",{description:"获取当前登录用户的信息,包括配额、团队等",inputSchema:z.object({}).describe("无需参数,直接调用即可获取当前用户信息")},async t=>{try{const t={};ACCESS_KEY&&(t.Authorization=`Bearer ${ACCESS_KEY}`);const e=`${SERVER_URL}/api/getUserInfo`;try{const n=(await axios.get(e,{headers:t})).data;let r="✅ 用户信息\n\n";return r+=`👤 用户ID: ${n.userId}\n`,n.userName&&(r+=`📧 用户名: ${n.userName}\n`),n.realname&&(r+=`📝 真实姓名: ${n.realname}\n`),r+="\n📊 配额信息:\n",n.quota&&(r+=` - 生成设计: ${n.quota.mcp_generate_count||0} / ${n.quota.mcp_generate_limit||"无限制"}\n`,r+=` - 获取代码: ${n.quota.mcp_get_count||0} / ${n.quota.mcp_get_limit||"无限制"}\n`),n.teams&&n.teams.length>0&&(r+=`\n👥 团队信息 (共 ${n.teams.length} 个团队):\n`,n.teams.forEach((t,e)=>{r+=` ${e+1}. ${t.name||t.teamId} (ID: ${t.teamId})\n`,t.auth&&(r+=` 权限: ${t.auth}\n`)})),n.currentTeamId&&(r+=`\n🔖 当前团队ID: ${n.currentTeamId}`),{content:[{type:"text",text:r}]}}catch(t){const e=t.response?.status,n=t.response?.data||{};switch(e){case 401:return{content:[{type:"text",text:"❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确"}],isError:!0};case 403:return{content:[{type:"text",text:`❌ 权限不足 (403)\n\n${n.message||"您没有权限访问该资源"}`}],isError:!0};default:return{content:[{type:"text",text:`❌ 获取用户信息失败: ${t.message||String(t)}${n.message?`\n详情: ${n.message}`:""}\n- 服务器: ${SERVER_URL}`}],isError:!0}}}}catch(t){return{content:[{type:"text",text:`❌ 获取用户信息失败: ${t.message||String(t)}`}],isError:!0}}}),a.registerTool("send_to_codify",{description:"将代码发送到 Codify 插件转换为设计稿",inputSchema:{code:z.string().describe("要发送的代码内容")}},async t=>{try{const{code:e}=t;if(!e)return{content:[{type:"text",text:"参数错误: 未提供代码内容"}],isError:!0};const n={"Content-Type":"application/json"};ACCESS_KEY&&(n.Authorization=`Bearer ${ACCESS_KEY}`);try{return(await axios.post(`${SERVER_URL}/api/sendToCanvas`,{code:e},{headers:n})).data.success,{content:[{type:"text",text:"✅ 代码已成功发送到 Codify 插件,等待转换为设计稿"}]}}catch(t){const e=t.response?.status,n=t.response?.data||{};return 404===e?{content:[{type:"text",text:"❌ 未找到活跃的 Codify 插件连接\n\n请确保:\n1. Codify 插件已打开并连接到服务器\n2. 使用了相同的 access_key"}],isError:!0}:400===e?{content:[{type:"text",text:`❌ 请求参数错误: ${n.message||"Bad request"}`}],isError:!0}:401===e||403===e?void 0!==n.mcp_generate_limit||n.message?.includes("limit")?{content:[{type:"text",text:`❌ 配额不足\n- ${n.message||"您已达到生成设计的次数限制"}\n- 剩余配额: ${void 0!==n.mcp_generate_limit?n.mcp_generate_limit:"未知"}\n- 请升级您的计划或联系支持团队`}],isError:!0}:{content:[{type:"text",text:`❌ 认证失败 (${e})\n\n请检查 CODIFY_ACCESS_KEY 是否正确`}],isError:!0}:{content:[{type:"text",text:`❌ 发送失败: ${t.message||String(t)}${n.message?`\n详情: ${n.message}`:""}`}],isError:!0}}}catch(t){return{content:[{type:"text",text:`❌ 代码发送失败: ${t.message||String(t)}`}],isError:!0}}}),a.registerTool("upload_image",{description:"将本地图片文件上传缓存到 MCP 服务器的 data/cache 目录,返回缓存路径",inputSchema:{filePath:z.string().describe("本地图片文件的绝对路径,例如 /Users/xxx/Desktop/image.png")}},async n=>{try{const{filePath:r}=n;if(!r)return{content:[{type:"text",text:"参数错误: 未提供 filePath"}],isError:!0};let s;try{s=t.readFileSync(r)}catch(t){return{content:[{type:"text",text:`❌ 无法读取文件: ${r}\n错误: ${t.message||String(t)}`}],isError:!0}}const o=e.basename(r),i={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".bmp":"image/bmp",".ico":"image/x-icon"}[e.extname(o).toLowerCase()]||"application/octet-stream",a=s.toString("base64"),c={"Content-Type":"application/json"};ACCESS_KEY&&(c.Authorization=`Bearer ${ACCESS_KEY}`);try{const t=(await axios.post(`${SERVER_URL}/api/uploadImage`,{imageData:a,fileName:o,mimeType:i},{headers:c})).data;return{content:[{type:"text",text:`✅ 图片已成功缓存到服务器\n- 原始文件: ${o}\n- 缓存路径: ${t.cachePath}\n- 文件大小: ${t.size} bytes\n- 类型: ${t.mimeType||i}`}]}}catch(t){const e=t.response?.status,n=t.response?.data||{};return 401===e||403===e?{content:[{type:"text",text:`❌ 认证失败 (${e})\n\n请检查 CODIFY_ACCESS_KEY 是否正确`}],isError:!0}:{content:[{type:"text",text:`❌ 上传失败: ${t.message||String(t)}${n.message?`\n详情: ${n.message}`:""}`}],isError:!0}}}catch(t){return{content:[{type:"text",text:`❌ 图片上传失败: ${t.message||String(t)}`}],isError:!0}}});try{const t=new StdioServerTransport;await a.connect(t)}catch(t){process.exit(1)}
|
|
2
|
+
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z}from"zod";import axios from"axios";import e from"fs";import t from"path";const{serverUrl:SERVER_URL}=function(){const e=process.argv.slice(2);let t=process.env.CODIFY_SERVER_URL||"http://localhost:8080";for(let n=0;n<e.length;n++){const s=e[n];"--help"!==s&&"-h"!==s||(console.log('\nCodify MCP Client - 连接到远程 Codify MCP 服务器\n\n用法:\n npx -y @codify-ai/mcp-client [选项]\n\n选项:\n --url=<URL> 服务器 URL (默认: https://mcp.codify-api.com)\n --help, -h 显示帮助信息\n --version, -v 显示版本号\n\n环境变量:\n CODIFY_SERVER_URL 服务器 URL\n CODIFY_ACCESS_KEY 访问密钥\n\n示例:\n npx -y @codify-ai/mcp-client --url=https://mcp.codify-api.com\n CODIFY_ACCESS_KEY=sk-xxx npx -y @codify-ai/mcp-client\n\nCursor 配置示例 (~/.cursor/mcp.json):\n{\n "mcpServers": {\n "codify": {\n "command": "npx",\n "args": ["-y", "@codify-ai/mcp-client", "--url=https://mcp.codify-api.com"],\n "env": {\n "CODIFY_ACCESS_KEY": "sk-your-access-key"\n }\n }\n }\n}\n'),process.exit(0)),"--version"!==s&&"-v"!==s||process.exit(0),s.startsWith("--url=")?t=s.substring(6):"--url"===s&&n+1<e.length?t=e[++n]:s.startsWith("-")||(t=s)}return{serverUrl:t}}(),ACCESS_KEY=process.env.CODIFY_ACCESS_KEY,n='\n# 📐 HTML + CSS 视觉创构与逆向转译协议 (Visual Generation & Reversal Protocol)\n\n## 🎯 角色与任务目标 (Dual-Core Persona)\n\n你是一个拥有双重核心的 **商业级 UI 创构与 Figma 逆向编译引擎**。你的大脑分为两个绝对隔离的模块,必须严格按顺序执行:\n\n**核心一:顶级 UI/UX 视觉架构师(负责“创造美”)**\n你需要根据用户的需求,动用 Dribbble/Behance 级别的顶尖商业审美。运用本提示词中的《核心设计哲学》,推导最匹配的视觉维度、极其优雅的排版、舒适的留白以及极具转化率的色彩系统。你的目标是:**设计出让人一眼惊艳的界面方案。**\n\n**核心二:严苛的 Figma 协议编译器(负责“格式化”)**\n一旦视觉方案定型,你必须将这个绝美的界面,**100% 严格地“降维、压缩、翻译”**成符合底层规范的代码。你不再是设计师,而是一个没有感情的机器,确保每一行代码都能被程序完美逆向解析为 Figma 图层。任何非标代码都会导致转换失败系统崩溃。\n\n## 📜 最终产出总纲 (Final Output Standards)\n\n你所有的工作产出,必须无条件满足以下 7 大黄金标准:\n\n1. **视觉标准**:Dribbble/Behance 级别的顶级商业 UI 审美。\n2. **代码标准**:仅返回包含在 `<main>` 根容器内的代码,全部使用 **Tailwind CSS Utility Classes**。\n3. **语法标准**:为了保证 1:1 还原 Figma 数值,**必须**使用 Tailwind 的 **Arbitrary Values (任意值)** 语法 (e.g., `w-[320px]`, `bg-[#F5F5F5]`),严禁使用依赖 Theme 的默认类名 (如 `w-1/2`, `bg-red-500`)。\n4. **结构标准**:完全符合下述的“图层原子化协议”。\n5. **命名规范**:每个标签必须包含 `data-name="..."`,使用语义化英文 (e.g., `card-container`, `user-avatar`)。\n6. **图片处理**:只需要返回 `<img src="{{keyword}}" />` 语义化内容即可,系统会根据`{{keyword}}`来搜索相关图片,如: `<img src="{{Cyberpunk City}}" />`。\n7. **图标系统**:使用 FontAwesome `fas`, `far` 系列图标 (`<i class="fas fa-...">`),必须在 class 中使用 `text-[size]` `text-[#color]` 定义。\n\n## 🔄 强制的执行生命周期 (Execution Lifecycle)\n\n为了保证美感与代码规范互不干扰,你必须严格按照以下两步输出:\n\n**第一步:输出视觉构思与自检 (Design Planning)**\n在写任何代码之前,必须先使用 `<design_plan>` 标签输出你的设计策略,打好高质量草稿:\n\n<design_plan>\n\n1. **风格定位**:选择的主辅视觉维度是什么?为什么?\n2. **色彩推导**:主色 (Hex)、强引导色、背景色的具体取值及语义理由。\n3. **结构拆解**:界面分为哪几个核心区块?主次容器的定宽与自适应比例是如何分配的?\n4. **编译红线自检**:弃用所有原生 input/button 标签,并绝对禁止使用 m- (Margin),所有间距将严格使用 Flex gap 或 p- 替代。\n\n</design_plan>\n\n**第二步:输出协议代码 (Protocol Output)**\n构思完成后,严格遵照红线标准输出代码,且**仅输出**包含在 `<main>` 根容器内的 HTML,包裹在 ```html 代码块中。\n\n## 🛑 红色警戒区 (Critical Constraints)\n\n**以下规则享有最高优先级,违反任何一条均视为 SYSTEM FAILURE:**\n\n### 1. 🚫 绝对禁用的属性 (Blocklist)\n\n- ❌ **严禁使用 Margin (Zero Tolerance)**:\n - 无论任何情况,**绝对禁止**出现 `m-[...]`, `my-[...]`, `mt-[...]` 等类名。\n - **替代方案 A** (均匀间距):在父容器 Flex 中使用 `gap-[数值]px`。\n - **替代方案 B** (内边距):如果是元素离边框的距离,使用父容器的 `p-[数值]px`。\n - **替代方案 C** (推挤布局):使用 `justify-between` 将首尾元素撑开。\n - **替代方案 D** (不均匀间距):必须通过“嵌套容器”解决(将相邻元素打组并设置独立的 gap)。\n- **禁止** 使用相对单位: `%`, `vw`, `vh`, `rem`, `em`, `calc()` (**必须**锁定使用 `px` 整数,如 `w-[320px]`)。\n- **禁止** 使用 Tailwind 具名颜色和透明度修饰符语法: `bg-red-500`, `text-[#fff]/50` (**必须**使用 Hex `#FFFFFF` 或 `rgba(0,0,0,0.5)` 任意值语法)。\n- **禁止** 使用 Grid 布局: 仅允许使用 `flex`。\n- **禁止** 省略 Flex 默认值。\n\n### 2. ⚠️ 强制显式声明 (Explicit Declaration)\n\n所有 Flex 容器**必须**写全以下 4 类属性,缺一不可:\n\n1. `flex`\n2. `flex-row` 或 `flex-col`\n3. `justify-start` / `justify-center` / `justify-between` ...\n4. `items-start` / `items-center` / `items-stretch` ...\n\n_(示例: `class="flex flex-col justify-start items-center ..." `)_\n\n## 🧠 核心设计哲学 (Design Philosophy)\n\n### 1. 视觉风格与材质 (Visual Style & Material)\n\n**原则**:形式追随功能。所有的视觉决策(颜色、布局、材质)必须服务于用户的心理模型和业务目标。\n\n- **[未来与深度]** (偏前沿探索):**磨砂玻璃特效** + **暗色模式**。利用光晕和透明度制造层级感。\n- **[效率与速度]** (偏专业工具):**洁净扁平风格** + **便当盒布局 (Bento UI)**。强调清晰边界、模块化,减少装饰。\n- **[信任与专业]** (偏金融/严谨):**瑞士极简风格**。大量留白 (Less Is More),依托排版和严格网格传达严谨性。\n- **[关怀与共鸣]** (偏人文/生活):**低饱和度自然色** + **极端圆角**。超柔和弥散阴影,传递呼吸感。\n- **[沉浸与表现]** (偏娱乐/叙事):**拟物化材质** + **强对比情绪色彩**。打破常规网格,低信息密度。\n\n### 2. 空间与排版组织原则 (Spatial & Typography)\n\n- **密度层级**:密度与重要性成反比。核心重点区域需要低密度/大边距。数据列表需要高密度/小边距。\n- **排版系统**:\n- 优先使用现代无衬线字体。\n- 标题和正文之间应建立显著的**字体粗细**和**字号**对比。\n- 正文行高保持为 `leading-[1.5]` 或 `leading-[1.6]`,以确保页面通透。\n\n### 3. 交互暗示与容错原则 (Affordance & Resilience)\n\n虽然输出的是静态 HTML,但在处理多项同类组件(如列表、导航、卡片组)时,**必须在同一个容器内,同时硬编码渲染出不同的交互状态**,以穷举展示组件的完整生命周期。\n\n- **⚠️ 警告**:不要仅仅依赖 Tailwind 的 `hover:` 伪类来实现交互。你必须通过直接改变特定 Item 的基础 class,让状态在静态截图中**同时可见**!\n\n### 4. 系统一致性约束 (System Integrity)\n\n**所有的设计决策必须映射到以下有限变量集(严禁出现奇数、小数或随机值):**\n\n- **色彩系统 (Color System)**:主色定义品牌;主色**互补色**用于强引导;主色**同类色**用于柔引导。严禁随意取色。\n- **空间间距 (8-Point Grid)**:必须遵循 8pt 网格系统,间距与内边距仅限:`8` / `12` / `16` / `20` / `24` / `32` / `40` (严格应用到 gap 和 padding)。\n- **圆角控制 (Border Radius)**:根据风格选择,默认 `rounded-[12px]` 起步。\n- **尺寸底线 (Typography/Size)**:最小点击热区 `44px`;最小阅读字号 `12px` (仅限注释),标准正文 `14px/16px`。\n- **阴影控制 (Shadows)**:必须使用弥散光影 如:`shadow-[0_10px_30px_rgba(0,0,0,0.08)]`,禁止生硬。\n\n## 🧬 图层原子化协议 (Atomic Structure)\n\n### 规则 A:容器与内容物理隔离\n\n- `<div>` 仅负责容器样式(背景/边框/阴影/布局)。\n- `<p>` / `<span>` / `<i>` 仅负责文本/图标样式。\n- 所有文本标签必须显式设置:`text-[<value>]`, `leading-[<value>]`, `font-[<value>]`, `text-[#Hex]`。\n- ❌ 错误:`<span class="bg-[#000] p-[10px]">Text</span>`\n- ✅ 正确:`<div class="bg-[#000] p-[10px]"><span class="text-[#FFF]">Text</span></div>`\n\n### 规则 B:标签语义与 Figma 文本折行法则 (Text Wrapping Physics)\n\n在 Figma 逆向解析中,文本标签决定了 Text Node 的行为。必须严格根据“折行预期”选择标签:\n\n- **`<p>` (Auto Height 换行文本)**:用于**包含完整句子、段落、描述**,或存在换行可能的文本。父容器或自身必须有宽度约束(如 `w-[300px]` 或 `flex-1`)。\n- **`<span>` (Auto Width 单行文本)**:仅限于**绝对不换行**的极短文本(如按钮文字、数字价格、标签 Badge、人名等短语)。\n- **`<img>` (按需使用)**:仅在**业务逻辑真正需要时**(如用户头像、商品封面)才使用,绝不要为了装饰而强行塞入。优先固定宽高,使用 `<img src="{{Keyword}}" />`。\n- **`<i>`**: FontAwesome 图标,必须通过 text 设定尺寸与颜色。\n\n### 规则 C:交互组件静态化 (Static Interaction)\n\n所有交互元素必须“冻结”为静态视觉层,**严禁使用原生控件**。\n\n- ❌ 禁用:`<input>`, `<select>`, `<textarea>`, `<button>`, `<form>`。\n- ✅ 必须:使用 `div` 模拟外观(例如输入框为一个带 border 的 div,内含 placeholder span)。\n\n## 🏗️ 布局物理法则 (Layout Physics)\n\n1. **拉伸逻辑**:主轴自适应填满使用 `flex-1`,交叉轴拉伸使用 `self-stretch`。\n2. **禁止比例 Flex**:严禁使用 `flex-[2.5]` 等浮点或比例。必须是定宽 vs 弹性。\n3. **多栏强制模式**:凡涉及左右/主次结构,**次要容器必须写死宽度 (如 `w-[360px]`),主要容器使用 `flex-1`**。\n\n## 🏆 黄金标准参考 (Reference Implementation)\n\n**必须严格模仿以下代码的【底层结构逻辑】。** 🚨 **绝对警告**:此 Demo 仅做“逆向解析代码规范”演示。在实际生成时,**绝不要生搬硬套**里面的图片或文本!你必须完全根据你自己的 `<design_plan>` 来推导业务逻辑。\n\n**💡 解析此 Demo 的满分细节 (自检标准):**\n\n1. **全显式 Flex & 零 Margin**:所有间距均由 `gap` 和 `p` 掌控。\n2. **8pt 网格系统**:所有的尺寸 (`24px`, `16px`, `12px`, `8px`, `56px`) 严格遵循 8 的倍数。\n3. **文本换行法则**:长段描述使用了 `<p>`,短语使用了 `<span>`。\n4. **状态穷举**:列表 (Menu List) 在同一个静态画面中,硬编码展示了“激活、悬停、默认”三种生命周期。\n5. **控件静态化**:Input 和 Button 全面被 `div` 替代。\n\n```html\n<main class="flex flex-col items-center justify-start w-[1440px] min-h-[1024px] bg-[#F3F4F6] p-[40px]">\n <div\n data-name="settings-card"\n 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)]"\n >\n <div data-name="card-header" class="flex flex-row items-start justify-between self-stretch">\n <img\n src="{{Professional UI Designer Avatar}}"\n alt="User"\n class="block w-[56px] h-[56px] rounded-[28px] object-cover"\n data-name="user-avatar"\n />\n <div data-name="status-badge" class="flex flex-row items-center justify-center bg-[#ECFDF5] px-[12px] py-[6px] rounded-[8px]">\n <span data-name="badge-text" class="text-[12px] leading-[1.2] font-[600] text-[#10B981]">PRO MEMBER</span>\n </div>\n </div>\n\n <div data-name="text-content" class="flex flex-col items-start justify-start gap-[8px]">\n <span data-name="user-name" class="text-[20px] leading-[1.2] font-[700] text-[#111827]">Alex Morgan</span>\n <p data-name="user-desc" class="text-[14px] leading-[1.5] font-[400] text-[#6B7280]">\n Manage your workspace settings, team members, and billing preferences here. Changes will be synced across all your active devices\n automatically.\n </p>\n </div>\n\n <div data-name="menu-list" class="flex flex-col items-stretch justify-start gap-[8px]">\n <div data-name="menu-item-active" class="flex flex-row items-center justify-start gap-[12px] bg-[#EEF2FF] p-[12px] rounded-[12px]">\n <i class="fas fa-layer-group text-[16px] text-[#4F46E5]" data-name="icon-active"></i>\n <span data-name="text-active" class="text-[14px] leading-[1.2] font-[600] text-[#4F46E5]">Workspace Setup</span>\n </div>\n\n <div data-name="menu-item-hover" class="flex flex-row items-center justify-start gap-[12px] bg-[#F9FAFB] p-[12px] rounded-[12px]">\n <i class="fas fa-users text-[16px] text-[#4B5563]" data-name="icon-hover"></i>\n <span data-name="text-hover" class="text-[14px] leading-[1.2] font-[500] text-[#4B5563]">Team Members</span>\n </div>\n\n <div data-name="menu-item-default" class="flex flex-row items-center justify-start gap-[12px] bg-transparent p-[12px] rounded-[12px]">\n <i class="fas fa-credit-card text-[16px] text-[#6B7280]" data-name="icon-default"></i>\n <span data-name="text-default" class="text-[14px] leading-[1.2] font-[500] text-[#6B7280]">Billing & Invoices</span>\n </div>\n </div>\n\n <div data-name="action-footer" class="flex flex-col items-stretch justify-start gap-[16px] border-t-[1px] border-[#F3F4F6] pt-[24px]">\n <div\n data-name="input-mock"\n class="flex flex-row items-center justify-start gap-[8px] bg-[#FFFFFF] border-[1px] border-[#E5E7EB] p-[12px] rounded-[12px]"\n >\n <i class="fas fa-envelope text-[14px] text-[#9CA3AF]" data-name="input-icon"></i>\n <span data-name="input-placeholder" class="text-[14px] leading-[1.2] font-[400] text-[#9CA3AF]">Invite via email...</span>\n </div>\n\n <div data-name="submit-btn" class="flex flex-row items-center justify-center bg-[#111827] p-[16px] rounded-[12px]">\n <span data-name="btn-text" class="text-[14px] leading-[1.2] font-[600] text-[#FFFFFF]">Send Invitation</span>\n </div>\n </div>\n </div>\n</main>\n```\n';function s(e){return null==e||""===e||"string"==typeof e&&""===e.trim()}const a=new McpServer({name:"Codify-MCP-Client",version:"1.0.18"},{capabilities:{resources:{},tools:{},prompts:{}}});a.registerResource("generation-rules","codify://generation-rules",{description:"Codify 代码生成规则"},async e=>({contents:[{uri:e.href,text:n,mimeType:"text/markdown"}]})),a.registerTool("get_code",{description:"从 Codify For MasterGo 插件获取指定的代码",inputSchema:{contentId:z.string().describe("从Codify插件复制图层的指令"),outDir:z.string().describe("必填。保存生成代码和相关资源的本地目录。AI 必须根据已知的当前打开项目的文件路径(如 /Users/xxx/code/my-project),主动推断并传入一个合理的【绝对路径】(例如 /Users/xxx/code/my-project/src/views)。请注意,不要省略此参数,因为工具运行环境的默认路径可能是用户的 Home 目录而非项目根目录。")}},async n=>{try{const{contentId:a,outDir:r}=n;if(!a)return{content:[{type:"text",text:`参数错误: 未提供 contentId\n- args: ${JSON.stringify(n)}`}],isError:!0};const i={};ACCESS_KEY&&(i.Authorization=`Bearer ${ACCESS_KEY}`);const o=`${SERVER_URL}/api/getCode/${a}`;try{const n=(await axios.get(o,{headers:i})).data;if(!n.code)return{content:[{type:"text",text:`⚠️ 未找到代码内容\n- contentId: ${a}\n- 可能该内容尚未从 Codify 插件同步`}],isError:!1};const c=r?t.resolve(process.cwd(),r):t.resolve(process.cwd(),`codify-output/${a}`);e.existsSync(c)||e.mkdirSync(c,{recursive:!0});const p=`${a}.html`,d=t.join(c,p);e.writeFileSync(d,n.code,"utf8");const l=async(n,a,r)=>{if(s(n))return 0;let i;try{i="string"==typeof n?JSON.parse(n):n}catch(e){return 0}const o=Object.keys(i);if(0===o.length)return 0;const p=t.join(c,a);e.existsSync(p)||e.mkdirSync(p,{recursive:!0});const d=Object.entries(i).map(async([n,s])=>{const i=n.match(/(.+)\.([a-zA-Z0-9]+)$/);let o=n,c=r;i&&(o=i[1],c=i[2]),o=o.replace(/[^a-zA-Z0-9_-]/g,"_");const d=t.join(p,`${o}.${c}`);let l=s,x="utf8";if("string"!=typeof l)l=JSON.stringify(l,null,2),e.writeFileSync(d,l,x);else if(l.startsWith("http://")||l.startsWith("https://"))try{const t=await axios.get(l,{responseType:"arraybuffer"});e.writeFileSync(d,t.data)}catch(e){console.error(`[Codify MCP] Failed to download resource from ${l}:`,e.message)}else if(l.startsWith("data:image/")){const t=l.match(/^data:image\/([A-Za-z-+\/]+);base64,(.+)$/);t&&3===t.length&&(x="base64",l=t[2]),e.writeFileSync(d,l,x)}else"images"===a&&(x="base64"),e.writeFileSync(d,l,x)});return await Promise.all(d),o.length};let x={image:"asset/images",svg:"asset/icons",shape:"asset/shapes"};if(!s(n.resourcePath))try{const e="string"==typeof n.resourcePath?JSON.parse(n.resourcePath):n.resourcePath;e.image&&(x.image=e.image.replace(/^\.\//,"")),e.svg&&(x.svg=e.svg.replace(/^\.\//,"")),e.shape&&(x.shape=e.shape.replace(/^\.\//,""))}catch(e){console.error("[Codify MCP] Failed to parse resourcePath:",e)}const m=await l(n.shape,x.shape,"json"),g=await l(n.svg,x.svg,"svg"),u=await l(n.image,x.image,"png");let f="代码和资源已成功拉取并保存到本地。\n\n";return f+=`目标目录: ${c}\n`,f+=`代码文件: ${p} (长度: ${n.code.length} 字符)\n`,n.teamId&&(f+=`团队 ID: ${n.teamId}\n`),m>0&&(f+=`Shape: 成功保存 ${m} 个文件至 ${x.shape} 目录\n`),g>0&&(f+=`SVG: 成功保存 ${g} 个文件至 ${x.svg} 目录\n`),u>0&&(f+=`Image: 成功保存 ${u} 个文件至 ${x.image} 目录\n`),f+=`\n**执行结果**:以上文件已自动写入本地系统。请注意阅读 ${p} 并检查代码中使用的 relative 路径。若业务文件不叫 ${p},你可以依据此文件重构/合并到你的业务主入口中。`,{content:[{type:"text",text:f}]}}catch(e){const t=e.response?.status,n=e.response?.data||{};switch(t){case 404:return{content:[{type:"text",text:`❌ 未找到内容\n- contentId: ${a}\n- 请检查 contentId 是否正确,或该内容是否已从 Codify 插件同步`}],isError:!0};case 403:return void 0!==n.mcp_get_limit||n.message?.includes("limit")?{content:[{type:"text",text:`❌ 配额不足\n- ${n.message||"您已达到获取代码的次数限制"}\n- 剩余配额: ${void 0!==n.mcp_get_limit?n.mcp_get_limit:"未知"}\n- 请升级您的计划或联系支持团队`}],isError:!0}:{content:[{type:"text",text:`❌ 权限不足\n- contentId: ${a}\n- 您没有权限访问该内容\n- 请检查您的 access_key 是否包含正确的团队权限`}],isError:!0};case 401:return{content:[{type:"text",text:"❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确"}],isError:!0};default:return{content:[{type:"text",text:`❌ 获取代码失败: ${e.message||String(e)}${n.message?`\n详情: ${n.message}`:""}\n- contentId: ${a}\n- 服务器: ${SERVER_URL}`}],isError:!0}}}}catch(e){return{content:[{type:"text",text:`❌ 获取代码失败: ${e.message||String(e)}`}],isError:!0}}}),a.registerTool("get_code_list",{description:"获取所有可用的代码列表",inputSchema:z.object({}).describe("无需参数,直接调用即可获取所有代码列表")},async e=>{try{const e={};ACCESS_KEY&&(e.Authorization=`Bearer ${ACCESS_KEY}`);const t=`${SERVER_URL}/api/getCodeList`;try{const n=(await axios.get(t,{headers:e})).data;if(!Array.isArray(n))return{content:[{type:"text",text:"⚠️ 服务器返回的数据格式不正确\n- 期望: 数组\n- 实际: "+typeof n}],isError:!0};if(0===n.length)return{content:[{type:"text",text:"📋 代码列表为空\n\n当前没有可用的代码内容。\n请先从 Codify 插件同步代码。"}],isError:!1};let s=`✅ 成功获取代码列表 (共 ${n.length} 项)\n\n`;return s+="--- 代码列表 ---\n\n",n.forEach((e,t)=>{s+=`${t+1}. **${e.contentId}**\n`,s+=` - 代码长度: ${e.codeLength} 字符\n`,e.teamId&&(s+=` - 团队 ID: ${e.teamId}\n`),e.fileInfo&&(s+=` - 文件信息: ${JSON.stringify(e.fileInfo)}\n`),e.createdAt&&(s+=` - 创建时间: ${e.createdAt}\n`),e.updatedAt&&(s+=` - 更新时间: ${e.updatedAt}\n`),s+="\n"}),s+="\n--- 使用说明 ---\n",s+="使用 get_code 工具获取具体代码内容:\n",s+='- get_code({ contentId: "your-content-id" })',{content:[{type:"text",text:s}]}}catch(e){const t=e.response?.status,n=e.response?.data||{};switch(t){case 403:return{content:[{type:"text",text:"❌ 权限不足\n- 您没有权限访问代码列表\n- 请检查您的 access_key 是否包含正确的团队权限"}],isError:!0};case 401:return{content:[{type:"text",text:"❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确"}],isError:!0};default:return{content:[{type:"text",text:`❌ 获取代码列表失败: ${e.message||String(e)}${n.message?`\n详情: ${n.message}`:""}\n- 服务器: ${SERVER_URL}`}],isError:!0}}}}catch(e){return{content:[{type:"text",text:`❌ 获取代码列表失败: ${e.message||String(e)}`}],isError:!0}}}),a.registerTool("get_user_info",{description:"获取当前登录用户的信息,包括配额、团队等",inputSchema:z.object({}).describe("无需参数,直接调用即可获取当前用户信息")},async e=>{try{const e={};ACCESS_KEY&&(e.Authorization=`Bearer ${ACCESS_KEY}`);const t=`${SERVER_URL}/api/getUserInfo`;try{const n=(await axios.get(t,{headers:e})).data;let s="✅ 用户信息\n\n";return s+=`👤 用户ID: ${n.userId}\n`,n.userName&&(s+=`📧 用户名: ${n.userName}\n`),n.realname&&(s+=`📝 真实姓名: ${n.realname}\n`),s+="\n📊 配额信息:\n",n.quota&&(s+=` - 生成设计: ${n.quota.mcp_generate_count||0} / ${n.quota.mcp_generate_limit||"无限制"}\n`,s+=` - 获取代码: ${n.quota.mcp_get_count||0} / ${n.quota.mcp_get_limit||"无限制"}\n`),n.teams&&n.teams.length>0&&(s+=`\n👥 团队信息 (共 ${n.teams.length} 个团队):\n`,n.teams.forEach((e,t)=>{s+=` ${t+1}. ${e.name||e.teamId} (ID: ${e.teamId})\n`,e.auth&&(s+=` 权限: ${e.auth}\n`)})),n.currentTeamId&&(s+=`\n🔖 当前团队ID: ${n.currentTeamId}`),{content:[{type:"text",text:s}]}}catch(e){const t=e.response?.status,n=e.response?.data||{};switch(t){case 401:return{content:[{type:"text",text:"❌ 认证失败 (401)\n\n请检查 CODIFY_ACCESS_KEY 是否正确"}],isError:!0};case 403:return{content:[{type:"text",text:`❌ 权限不足 (403)\n\n${n.message||"您没有权限访问该资源"}`}],isError:!0};default:return{content:[{type:"text",text:`❌ 获取用户信息失败: ${e.message||String(e)}${n.message?`\n详情: ${n.message}`:""}\n- 服务器: ${SERVER_URL}`}],isError:!0}}}}catch(e){return{content:[{type:"text",text:`❌ 获取用户信息失败: ${e.message||String(e)}`}],isError:!0}}}),a.registerTool("design",{description:"根据需求生成符合 Codify 规范的 HTML+CSS 代码",inputSchema:{requirement:z.string().describe('界面需求描述,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等。工具会根据 Codify 生成规则自动生成代码。')}},async e=>{try{const{requirement:t}=e;return t?{content:[{type:"text",text:`📋 收到需求:${t}\n\n${n}\n\n---\n\n## 发送代码:\n代码生成并审计通过后,请立即调用 send_to_codify 工具发送代码\n - send_to_codify({ code: "生成的完整代码" })\n - 不要询问用户,直接执行发送操作\n\n**请现在开始生成代码。**`}]}:{content:[{type:"text",text:'参数错误: 未提供需求描述\n\n请提供界面需求,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等'}],isError:!0}}catch(e){return{content:[{type:"text",text:`❌ 处理失败: ${e.message||String(e)}`}],isError:!0}}}),a.registerTool("send_to_codify",{description:"将代码发送到 Codify 插件转换为设计稿",inputSchema:{code:z.string().describe("要发送的代码内容")}},async e=>{try{const{code:t}=e;if(!t)return{content:[{type:"text",text:"参数错误: 未提供代码内容"}],isError:!0};const n={"Content-Type":"application/json"};ACCESS_KEY&&(n.Authorization=`Bearer ${ACCESS_KEY}`);try{return(await axios.post(`${SERVER_URL}/api/sendToCanvas`,{code:t},{headers:n})).data.success,{content:[{type:"text",text:"✅ 代码已成功发送到 Codify 插件,等待转换为设计稿"}]}}catch(e){const t=e.response?.status,n=e.response?.data||{};return 404===t?{content:[{type:"text",text:"❌ 未找到活跃的 Codify 插件连接\n\n请确保:\n1. Codify 插件已打开并连接到服务器\n2. 使用了相同的 access_key"}],isError:!0}:400===t?{content:[{type:"text",text:`❌ 请求参数错误: ${n.message||"Bad request"}`}],isError:!0}:401===t||403===t?void 0!==n.mcp_generate_limit||n.message?.includes("limit")?{content:[{type:"text",text:`❌ 配额不足\n- ${n.message||"您已达到生成设计的次数限制"}\n- 剩余配额: ${void 0!==n.mcp_generate_limit?n.mcp_generate_limit:"未知"}\n- 请升级您的计划或联系支持团队`}],isError:!0}:{content:[{type:"text",text:`❌ 认证失败 (${t})\n\n请检查 CODIFY_ACCESS_KEY 是否正确`}],isError:!0}:{content:[{type:"text",text:`❌ 发送失败: ${e.message||String(e)}${n.message?`\n详情: ${n.message}`:""}`}],isError:!0}}}catch(e){return{content:[{type:"text",text:`❌ 代码发送失败: ${e.message||String(e)}`}],isError:!0}}});try{const e=new StdioServerTransport;await a.connect(e)}catch(e){process.exit(1)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codify-ai/mcp-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "Codify MCP 客户端 - 连接到远程 Codify MCP 服务器,供 CLI 或 Cursor 等 IDE 使用",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,9 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"mcp-client.js",
|
|
18
18
|
"mcp-client.min.js",
|
|
19
|
-
"README.md"
|
|
20
|
-
"postinstall.cjs",
|
|
21
|
-
"skills/"
|
|
19
|
+
"README.md"
|
|
22
20
|
],
|
|
23
21
|
"engines": {
|
|
24
22
|
"node": ">=18.0.0",
|
|
@@ -31,8 +29,7 @@
|
|
|
31
29
|
"clean": "rm -rf dist",
|
|
32
30
|
"typecheck": "tsc --noEmit",
|
|
33
31
|
"prepare": "npm run build",
|
|
34
|
-
"build:deploy": "bash script/build-deploy.sh"
|
|
35
|
-
"postinstall": "node postinstall.cjs"
|
|
32
|
+
"build:deploy": "bash script/build-deploy.sh"
|
|
36
33
|
},
|
|
37
34
|
"devDependencies": {
|
|
38
35
|
"terser": "^5.26.0"
|