@xiehuan123/figma-mcp-server 1.0.1 → 1.0.2
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/README.md +7 -3
- package/package.json +1 -1
- package/src/index.js +60 -0
- package/src/svg-exporter.js +15 -5
- package/src/temp-manager.js +85 -0
package/README.md
CHANGED
|
@@ -126,14 +126,18 @@ Figma → Settings → Personal Access Tokens → 创建 token
|
|
|
126
126
|
|
|
127
127
|
```
|
|
128
128
|
.figma-temp/
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
raw/ # Figma API 原始响应数据
|
|
130
|
+
optimized/ # 优化后的 AI 友好数据
|
|
131
|
+
svg/ # 导出的 SVG 文件
|
|
132
|
+
icons/ # 图标索引 (index.json)
|
|
133
|
+
logs/ # API 调用日志
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
**生命周期规则:**
|
|
134
137
|
- MCP Server 启动时自动清空上一次的 `.figma-temp/` 目录
|
|
135
|
-
-
|
|
138
|
+
- 重新创建完整的子目录结构(raw/、optimized/、svg/、icons/、logs/)
|
|
136
139
|
- 会话期间的所有数据保留到下次启动
|
|
140
|
+
- 每次会话完全隔离,不会跨会话保留任何数据
|
|
137
141
|
|
|
138
142
|
## 核心特性
|
|
139
143
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -81,6 +81,9 @@ server.tool(
|
|
|
81
81
|
const nodeData = data.nodes[normalizedId];
|
|
82
82
|
if (!nodeData) return { content: [{ type: "text", text: `节点 ${normalizedId} 不存在` }] };
|
|
83
83
|
|
|
84
|
+
// 存储原始数据
|
|
85
|
+
tempManager.writeRaw(fileKey, normalizedId, nodeData);
|
|
86
|
+
|
|
84
87
|
const simplified = simplifyNode(nodeData.document, 0, depth);
|
|
85
88
|
const summary = generateSummary(simplified);
|
|
86
89
|
|
|
@@ -91,6 +94,18 @@ server.tool(
|
|
|
91
94
|
try {
|
|
92
95
|
const svgResults = await svgExporter.exportAndDownload(fileKey, exportableNodes);
|
|
93
96
|
svgSection = svgExporter.formatExportResults(svgResults);
|
|
97
|
+
// 记录图标到汇总索引
|
|
98
|
+
const iconEntries = [];
|
|
99
|
+
for (const [nodeIdKey, svgInfo] of svgResults.entries()) {
|
|
100
|
+
iconEntries.push({
|
|
101
|
+
fileKey,
|
|
102
|
+
nodeId: nodeIdKey,
|
|
103
|
+
name: svgInfo.filename || nodeIdKey,
|
|
104
|
+
svgPath: svgInfo.path || null,
|
|
105
|
+
source: "get_node",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (iconEntries.length > 0) tempManager.addIcons(iconEntries);
|
|
94
109
|
} catch (e) {
|
|
95
110
|
svgSection = `\n\n# SVG Export Error\n${e.message}`;
|
|
96
111
|
}
|
|
@@ -125,6 +140,9 @@ server.tool(
|
|
|
125
140
|
varSection = `\n\n# CSS 变量 (从节点绑定提取)\n:root {\n${varLines.join("\n")}\n}`;
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
// 存储优化后数据
|
|
144
|
+
tempManager.writeOptimized(fileKey, normalizedId, { summary, condensed, variables: nodeVarMap });
|
|
145
|
+
|
|
128
146
|
return {
|
|
129
147
|
content: [{
|
|
130
148
|
type: "text",
|
|
@@ -140,6 +158,9 @@ server.tool(
|
|
|
140
158
|
variables = nodeVarMap;
|
|
141
159
|
}
|
|
142
160
|
|
|
161
|
+
// 存储优化后数据
|
|
162
|
+
tempManager.writeOptimized(fileKey, normalizedId, { summary, tree: simplified, variables });
|
|
163
|
+
|
|
143
164
|
// 日志记录
|
|
144
165
|
logger.logOptimized("get_node", { fileKey, nodeId: normalizedId, format }, { summary, variables });
|
|
145
166
|
|
|
@@ -320,6 +341,19 @@ server.tool(
|
|
|
320
341
|
}
|
|
321
342
|
|
|
322
343
|
const output = svgExporter.formatExportResults(results);
|
|
344
|
+
// 记录图标到汇总索引
|
|
345
|
+
const iconEntries = [];
|
|
346
|
+
for (const [nodeIdKey, svgInfo] of results.entries()) {
|
|
347
|
+
iconEntries.push({
|
|
348
|
+
fileKey,
|
|
349
|
+
nodeId: nodeIdKey,
|
|
350
|
+
name: svgInfo.filename || nodeIdKey,
|
|
351
|
+
svgPath: svgInfo.path || null,
|
|
352
|
+
source: "export_svg",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (iconEntries.length > 0) tempManager.addIcons(iconEntries);
|
|
356
|
+
|
|
323
357
|
logger.logOptimized("export_svg", { fileKey, nodeIds: ids }, { exportedCount: results.size });
|
|
324
358
|
return { content: [{ type: "text", text: output }] };
|
|
325
359
|
} catch (e) {
|
|
@@ -424,6 +458,9 @@ server.tool(
|
|
|
424
458
|
const nodeData = nodeResult.nodes[normalizedId];
|
|
425
459
|
if (!nodeData) return { content: [{ type: "text", text: `节点 ${normalizedId} 不存在` }] };
|
|
426
460
|
|
|
461
|
+
// 存储原始数据
|
|
462
|
+
tempManager.writeRaw(fileKey, normalizedId, nodeData);
|
|
463
|
+
|
|
427
464
|
const node = nodeData.document;
|
|
428
465
|
|
|
429
466
|
// 构建 variable 映射
|
|
@@ -490,11 +527,34 @@ server.tool(
|
|
|
490
527
|
const svgResults = await svgExporter.exportAndDownload(fileKey, exportableNodes);
|
|
491
528
|
const svgSection = svgExporter.formatExportResults(svgResults);
|
|
492
529
|
if (svgSection) output.push(svgSection);
|
|
530
|
+
// 记录图标到汇总索引
|
|
531
|
+
const iconEntries = [];
|
|
532
|
+
for (const [nodeIdKey, svgInfo] of svgResults.entries()) {
|
|
533
|
+
iconEntries.push({
|
|
534
|
+
fileKey,
|
|
535
|
+
nodeId: nodeIdKey,
|
|
536
|
+
name: svgInfo.filename || nodeIdKey,
|
|
537
|
+
svgPath: svgInfo.path || null,
|
|
538
|
+
source: "get_page_for_codegen",
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (iconEntries.length > 0) tempManager.addIcons(iconEntries);
|
|
493
542
|
} catch (e) {
|
|
494
543
|
output.push(`\n## SVG Export Error\n${e.message}`);
|
|
495
544
|
}
|
|
496
545
|
}
|
|
497
546
|
|
|
547
|
+
// 存储优化后数据
|
|
548
|
+
tempManager.writeOptimized(fileKey, normalizedId, {
|
|
549
|
+
nodeName: node.name,
|
|
550
|
+
nodeType: node.type,
|
|
551
|
+
structure,
|
|
552
|
+
colors: [...colors],
|
|
553
|
+
fonts: [...fonts],
|
|
554
|
+
components,
|
|
555
|
+
tokens: tokensSummary || null,
|
|
556
|
+
});
|
|
557
|
+
|
|
498
558
|
// 日志记录
|
|
499
559
|
logger.logOptimized("get_page_for_codegen", { fileKey, nodeId: normalizedId }, { nodeType: node.type, nodeName: node.name });
|
|
500
560
|
|
package/src/svg-exporter.js
CHANGED
|
@@ -55,12 +55,12 @@ export class SvgExporter {
|
|
|
55
55
|
* @returns {string|null} 角色名或 null
|
|
56
56
|
*/
|
|
57
57
|
_shouldExport(node) {
|
|
58
|
-
//
|
|
59
|
-
if (
|
|
60
|
-
return
|
|
58
|
+
// 跳过组件实例内部的子节点(ID 含分号表示是实例内部节点)
|
|
59
|
+
if (node.id && node.id.includes(";")) {
|
|
60
|
+
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// 名称匹配图标模式(优先级最高)
|
|
64
64
|
if (ICON_PATTERN.test(node.name)) {
|
|
65
65
|
return "icon";
|
|
66
66
|
}
|
|
@@ -70,6 +70,11 @@ export class SvgExporter {
|
|
|
70
70
|
return "icon";
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// 顶层 VECTOR 类型节点(非嵌套在 frame 深处的装饰性向量)
|
|
74
|
+
if (VECTOR_TYPES.has(node.type)) {
|
|
75
|
+
return "vector";
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
// 包含 IMAGE fill
|
|
74
79
|
if (this._hasImageFill(node)) {
|
|
75
80
|
return "image";
|
|
@@ -120,6 +125,11 @@ export class SvgExporter {
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
const svgContent = await response.text();
|
|
128
|
+
if (!svgContent || svgContent.trim().length === 0) {
|
|
129
|
+
console.error(`[svg-exporter] Empty SVG content for ${nodeId}, skipping`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
123
133
|
const nodeInfo = nodeMap.get(nodeId);
|
|
124
134
|
const filename = this._buildFilename(nodeInfo);
|
|
125
135
|
|
|
@@ -151,7 +161,7 @@ export class SvgExporter {
|
|
|
151
161
|
.replace(/-+/g, "-")
|
|
152
162
|
.replace(/^-|-$/g, "")
|
|
153
163
|
.slice(0, 40);
|
|
154
|
-
const id = nodeInfo.id.replace(
|
|
164
|
+
const id = nodeInfo.id.replace(/[:.;]/g, "-");
|
|
155
165
|
return `${role}-${name}_${id}.svg`;
|
|
156
166
|
}
|
|
157
167
|
|
package/src/temp-manager.js
CHANGED
|
@@ -15,6 +15,10 @@ export class TempManager {
|
|
|
15
15
|
this.tempDir = path.join(projectRoot, ".figma-temp");
|
|
16
16
|
this.logsDir = path.join(this.tempDir, "logs");
|
|
17
17
|
this.svgDir = path.join(this.tempDir, "svg");
|
|
18
|
+
this.rawDir = path.join(this.tempDir, "raw");
|
|
19
|
+
this.optimizedDir = path.join(this.tempDir, "optimized");
|
|
20
|
+
this.iconsDir = path.join(this.tempDir, "icons");
|
|
21
|
+
this.iconsIndexPath = path.join(this.iconsDir, "index.json");
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** 初始化:清空旧数据,创建新目录 */
|
|
@@ -24,6 +28,11 @@ export class TempManager {
|
|
|
24
28
|
}
|
|
25
29
|
fs.mkdirSync(this.logsDir, { recursive: true });
|
|
26
30
|
fs.mkdirSync(this.svgDir, { recursive: true });
|
|
31
|
+
fs.mkdirSync(this.rawDir, { recursive: true });
|
|
32
|
+
fs.mkdirSync(this.optimizedDir, { recursive: true });
|
|
33
|
+
fs.mkdirSync(this.iconsDir, { recursive: true });
|
|
34
|
+
// 初始化图标索引
|
|
35
|
+
fs.writeFileSync(this.iconsIndexPath, JSON.stringify({ icons: [] }, null, 2), "utf-8");
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
getTempDir() {
|
|
@@ -38,6 +47,18 @@ export class TempManager {
|
|
|
38
47
|
return this.svgDir;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
getRawDir() {
|
|
51
|
+
return this.rawDir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getOptimizedDir() {
|
|
55
|
+
return this.optimizedDir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getIconsDir() {
|
|
59
|
+
return this.iconsDir;
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
/** 写入 SVG 文件,返回完整路径 */
|
|
42
63
|
writeSvg(filename, content) {
|
|
43
64
|
const filePath = path.join(this.svgDir, filename);
|
|
@@ -45,6 +66,70 @@ export class TempManager {
|
|
|
45
66
|
return filePath;
|
|
46
67
|
}
|
|
47
68
|
|
|
69
|
+
/** 写入原始 Figma API 数据 */
|
|
70
|
+
writeRaw(fileKey, nodeId, data) {
|
|
71
|
+
const safeNodeId = nodeId.replace(/:/g, "-");
|
|
72
|
+
const filename = `${fileKey}_${safeNodeId}.json`;
|
|
73
|
+
const filePath = path.join(this.rawDir, filename);
|
|
74
|
+
const content = JSON.stringify(data, null, 2);
|
|
75
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
76
|
+
return filePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 写入优化后的数据 */
|
|
80
|
+
writeOptimized(fileKey, nodeId, data) {
|
|
81
|
+
const safeNodeId = nodeId.replace(/:/g, "-");
|
|
82
|
+
const filename = `${fileKey}_${safeNodeId}.json`;
|
|
83
|
+
const filePath = path.join(this.optimizedDir, filename);
|
|
84
|
+
const content = JSON.stringify(data, null, 2);
|
|
85
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
86
|
+
return filePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 添加图标到汇总索引 */
|
|
90
|
+
addIcon(entry) {
|
|
91
|
+
const index = this._readIconsIndex();
|
|
92
|
+
const existing = index.icons.findIndex(
|
|
93
|
+
(i) => i.nodeId === entry.nodeId && i.fileKey === entry.fileKey
|
|
94
|
+
);
|
|
95
|
+
if (existing >= 0) {
|
|
96
|
+
index.icons[existing] = { ...index.icons[existing], ...entry, updatedAt: new Date().toISOString() };
|
|
97
|
+
} else {
|
|
98
|
+
index.icons.push({ ...entry, createdAt: new Date().toISOString() });
|
|
99
|
+
}
|
|
100
|
+
fs.writeFileSync(this.iconsIndexPath, JSON.stringify(index, null, 2), "utf-8");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 批量添加图标 */
|
|
104
|
+
addIcons(entries) {
|
|
105
|
+
const index = this._readIconsIndex();
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const existing = index.icons.findIndex(
|
|
108
|
+
(i) => i.nodeId === entry.nodeId && i.fileKey === entry.fileKey
|
|
109
|
+
);
|
|
110
|
+
if (existing >= 0) {
|
|
111
|
+
index.icons[existing] = { ...index.icons[existing], ...entry, updatedAt: new Date().toISOString() };
|
|
112
|
+
} else {
|
|
113
|
+
index.icons.push({ ...entry, createdAt: new Date().toISOString() });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
fs.writeFileSync(this.iconsIndexPath, JSON.stringify(index, null, 2), "utf-8");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 获取图标索引 */
|
|
120
|
+
getIconsIndex() {
|
|
121
|
+
return this._readIconsIndex();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_readIconsIndex() {
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(this.iconsIndexPath, "utf-8");
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
} catch {
|
|
129
|
+
return { icons: [] };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
48
133
|
/** 写入日志文件(非阻塞) */
|
|
49
134
|
writeLog(toolName, type, data) {
|
|
50
135
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|