@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 CHANGED
@@ -126,14 +126,18 @@ Figma → Settings → Personal Access Tokens → 创建 token
126
126
 
127
127
  ```
128
128
  .figma-temp/
129
- logs/ # API 日志
130
- svg/ # 导出的 SVG 文件
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
- - 重新创建空的 `logs/` 和 `svg/` 子目录
138
+ - 重新创建完整的子目录结构(raw/、optimized/、svg/、icons/、logs/)
136
139
  - 会话期间的所有数据保留到下次启动
140
+ - 每次会话完全隔离,不会跨会话保留任何数据
137
141
 
138
142
  ## 核心特性
139
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiehuan123/figma-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Figma MCP Server - 将 Figma API 数据处理成 AI 友好格式",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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
 
@@ -55,12 +55,12 @@ export class SvgExporter {
55
55
  * @returns {string|null} 角色名或 null
56
56
  */
57
57
  _shouldExport(node) {
58
- // VECTOR 类型节点
59
- if (VECTOR_TYPES.has(node.type)) {
60
- return "vector";
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
 
@@ -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, "-");