@bilalba/fig-mcp 1.0.0

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.
Files changed (186) hide show
  1. package/README.md +112 -0
  2. package/dist/compare-get-vector.d.ts +2 -0
  3. package/dist/compare-get-vector.d.ts.map +1 -0
  4. package/dist/compare-get-vector.js +124 -0
  5. package/dist/compare-get-vector.js.map +1 -0
  6. package/dist/compare-mcp-vs-direct.d.ts +2 -0
  7. package/dist/compare-mcp-vs-direct.d.ts.map +1 -0
  8. package/dist/compare-mcp-vs-direct.js +173 -0
  9. package/dist/compare-mcp-vs-direct.js.map +1 -0
  10. package/dist/compare-renderers.d.ts +2 -0
  11. package/dist/compare-renderers.d.ts.map +1 -0
  12. package/dist/compare-renderers.js +110 -0
  13. package/dist/compare-renderers.js.map +1 -0
  14. package/dist/debug/debug-stroke-geom.d.ts +2 -0
  15. package/dist/debug/debug-stroke-geom.d.ts.map +1 -0
  16. package/dist/debug/debug-stroke-geom.js +67 -0
  17. package/dist/debug/debug-stroke-geom.js.map +1 -0
  18. package/dist/debug/debug-transforms.d.ts +2 -0
  19. package/dist/debug/debug-transforms.d.ts.map +1 -0
  20. package/dist/debug/debug-transforms.js +97 -0
  21. package/dist/debug/debug-transforms.js.map +1 -0
  22. package/dist/debug/debug-vertex.d.ts +2 -0
  23. package/dist/debug/debug-vertex.d.ts.map +1 -0
  24. package/dist/debug/debug-vertex.js +72 -0
  25. package/dist/debug/debug-vertex.js.map +1 -0
  26. package/dist/debug-group.d.ts +5 -0
  27. package/dist/debug-group.d.ts.map +1 -0
  28. package/dist/debug-group.js +44 -0
  29. package/dist/debug-group.js.map +1 -0
  30. package/dist/debug-stroke-geom.d.ts +2 -0
  31. package/dist/debug-stroke-geom.d.ts.map +1 -0
  32. package/dist/debug-stroke-geom.js +67 -0
  33. package/dist/debug-stroke-geom.js.map +1 -0
  34. package/dist/debug-transforms.d.ts +2 -0
  35. package/dist/debug-transforms.d.ts.map +1 -0
  36. package/dist/debug-transforms.js +97 -0
  37. package/dist/debug-transforms.js.map +1 -0
  38. package/dist/debug-vertex.d.ts +2 -0
  39. package/dist/debug-vertex.d.ts.map +1 -0
  40. package/dist/debug-vertex.js +72 -0
  41. package/dist/debug-vertex.js.map +1 -0
  42. package/dist/decode-vector-network.d.ts +5 -0
  43. package/dist/decode-vector-network.d.ts.map +1 -0
  44. package/dist/decode-vector-network.js +160 -0
  45. package/dist/decode-vector-network.js.map +1 -0
  46. package/dist/experimental/paint-utils.d.ts +35 -0
  47. package/dist/experimental/paint-utils.d.ts.map +1 -0
  48. package/dist/experimental/paint-utils.js +105 -0
  49. package/dist/experimental/paint-utils.js.map +1 -0
  50. package/dist/experimental/render-screen-v2.d.ts +32 -0
  51. package/dist/experimental/render-screen-v2.d.ts.map +1 -0
  52. package/dist/experimental/render-screen-v2.js +366 -0
  53. package/dist/experimental/render-screen-v2.js.map +1 -0
  54. package/dist/experimental/render-screen.d.ts +26 -0
  55. package/dist/experimental/render-screen.d.ts.map +1 -0
  56. package/dist/experimental/render-screen.js +547 -0
  57. package/dist/experimental/render-screen.js.map +1 -0
  58. package/dist/experimental/render-types.d.ts +43 -0
  59. package/dist/experimental/render-types.d.ts.map +1 -0
  60. package/dist/experimental/render-types.js +22 -0
  61. package/dist/experimental/render-types.js.map +1 -0
  62. package/dist/experimental/render-utils.d.ts +38 -0
  63. package/dist/experimental/render-utils.d.ts.map +1 -0
  64. package/dist/experimental/render-utils.js +126 -0
  65. package/dist/experimental/render-utils.js.map +1 -0
  66. package/dist/experimental/screenshot.d.ts +11 -0
  67. package/dist/experimental/screenshot.d.ts.map +1 -0
  68. package/dist/experimental/screenshot.js +26 -0
  69. package/dist/experimental/screenshot.js.map +1 -0
  70. package/dist/experimental/vector-renderer.d.ts +31 -0
  71. package/dist/experimental/vector-renderer.d.ts.map +1 -0
  72. package/dist/experimental/vector-renderer.js +427 -0
  73. package/dist/experimental/vector-renderer.js.map +1 -0
  74. package/dist/explore-images.d.ts +9 -0
  75. package/dist/explore-images.d.ts.map +1 -0
  76. package/dist/explore-images.js +307 -0
  77. package/dist/explore-images.js.map +1 -0
  78. package/dist/http-server.d.ts +8 -0
  79. package/dist/http-server.d.ts.map +1 -0
  80. package/dist/http-server.js +95 -0
  81. package/dist/http-server.js.map +1 -0
  82. package/dist/index.d.ts +9 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +34 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/inspect-fig.d.ts +16 -0
  87. package/dist/inspect-fig.d.ts.map +1 -0
  88. package/dist/inspect-fig.js +134 -0
  89. package/dist/inspect-fig.js.map +1 -0
  90. package/dist/inspect-frame.d.ts +2 -0
  91. package/dist/inspect-frame.d.ts.map +1 -0
  92. package/dist/inspect-frame.js +90 -0
  93. package/dist/inspect-frame.js.map +1 -0
  94. package/dist/inspect-nodes.d.ts +5 -0
  95. package/dist/inspect-nodes.d.ts.map +1 -0
  96. package/dist/inspect-nodes.js +193 -0
  97. package/dist/inspect-nodes.js.map +1 -0
  98. package/dist/mcp/server.d.ts +38 -0
  99. package/dist/mcp/server.d.ts.map +1 -0
  100. package/dist/mcp/server.js +1524 -0
  101. package/dist/mcp/server.js.map +1 -0
  102. package/dist/parser/fig-reader.d.ts +29 -0
  103. package/dist/parser/fig-reader.d.ts.map +1 -0
  104. package/dist/parser/fig-reader.js +182 -0
  105. package/dist/parser/fig-reader.js.map +1 -0
  106. package/dist/parser/index.d.ts +48 -0
  107. package/dist/parser/index.d.ts.map +1 -0
  108. package/dist/parser/index.js +106 -0
  109. package/dist/parser/index.js.map +1 -0
  110. package/dist/parser/kiwi-parser.d.ts +66 -0
  111. package/dist/parser/kiwi-parser.d.ts.map +1 -0
  112. package/dist/parser/kiwi-parser.js +491 -0
  113. package/dist/parser/kiwi-parser.js.map +1 -0
  114. package/dist/parser/layout-inference.d.ts +63 -0
  115. package/dist/parser/layout-inference.d.ts.map +1 -0
  116. package/dist/parser/layout-inference.js +446 -0
  117. package/dist/parser/layout-inference.js.map +1 -0
  118. package/dist/parser/types.d.ts +286 -0
  119. package/dist/parser/types.d.ts.map +1 -0
  120. package/dist/parser/types.js +6 -0
  121. package/dist/parser/types.js.map +1 -0
  122. package/dist/render-single.d.ts +2 -0
  123. package/dist/render-single.d.ts.map +1 -0
  124. package/dist/render-single.js +53 -0
  125. package/dist/render-single.js.map +1 -0
  126. package/dist/renderer/index.d.ts +16 -0
  127. package/dist/renderer/index.d.ts.map +1 -0
  128. package/dist/renderer/index.js +18 -0
  129. package/dist/renderer/index.js.map +1 -0
  130. package/dist/renderer/paint-utils.d.ts +35 -0
  131. package/dist/renderer/paint-utils.d.ts.map +1 -0
  132. package/dist/renderer/paint-utils.js +105 -0
  133. package/dist/renderer/paint-utils.js.map +1 -0
  134. package/dist/renderer/render-screen.d.ts +26 -0
  135. package/dist/renderer/render-screen.d.ts.map +1 -0
  136. package/dist/renderer/render-screen.js +547 -0
  137. package/dist/renderer/render-screen.js.map +1 -0
  138. package/dist/renderer/render-types.d.ts +43 -0
  139. package/dist/renderer/render-types.d.ts.map +1 -0
  140. package/dist/renderer/render-types.js +22 -0
  141. package/dist/renderer/render-types.js.map +1 -0
  142. package/dist/renderer/render-utils.d.ts +38 -0
  143. package/dist/renderer/render-utils.d.ts.map +1 -0
  144. package/dist/renderer/render-utils.js +126 -0
  145. package/dist/renderer/render-utils.js.map +1 -0
  146. package/dist/renderer/screenshot.d.ts +11 -0
  147. package/dist/renderer/screenshot.d.ts.map +1 -0
  148. package/dist/renderer/screenshot.js +26 -0
  149. package/dist/renderer/screenshot.js.map +1 -0
  150. package/dist/renderer/vector-renderer.d.ts +31 -0
  151. package/dist/renderer/vector-renderer.d.ts.map +1 -0
  152. package/dist/renderer/vector-renderer.js +427 -0
  153. package/dist/renderer/vector-renderer.js.map +1 -0
  154. package/dist/shared-config.d.ts +9 -0
  155. package/dist/shared-config.d.ts.map +1 -0
  156. package/dist/shared-config.js +9 -0
  157. package/dist/shared-config.js.map +1 -0
  158. package/dist/test-parser.d.ts +3 -0
  159. package/dist/test-parser.d.ts.map +1 -0
  160. package/dist/test-parser.js +74 -0
  161. package/dist/test-parser.js.map +1 -0
  162. package/dist/test-render-v2.d.ts +5 -0
  163. package/dist/test-render-v2.d.ts.map +1 -0
  164. package/dist/test-render-v2.js +76 -0
  165. package/dist/test-render-v2.js.map +1 -0
  166. package/dist/test-render.d.ts +5 -0
  167. package/dist/test-render.d.ts.map +1 -0
  168. package/dist/test-render.js +76 -0
  169. package/dist/test-render.js.map +1 -0
  170. package/dist/vector-export.d.ts +52 -0
  171. package/dist/vector-export.d.ts.map +1 -0
  172. package/dist/vector-export.js +628 -0
  173. package/dist/vector-export.js.map +1 -0
  174. package/dist/web-viewer/build-client.d.ts +6 -0
  175. package/dist/web-viewer/build-client.d.ts.map +1 -0
  176. package/dist/web-viewer/build-client.js +36 -0
  177. package/dist/web-viewer/build-client.js.map +1 -0
  178. package/dist/web-viewer/client/viewer.d.ts +7 -0
  179. package/dist/web-viewer/client/viewer.d.ts.map +1 -0
  180. package/dist/web-viewer/client/viewer.js +873 -0
  181. package/dist/web-viewer/client/viewer.js.map +1 -0
  182. package/dist/web-viewer/server.d.ts +16 -0
  183. package/dist/web-viewer/server.d.ts.map +1 -0
  184. package/dist/web-viewer/server.js +420 -0
  185. package/dist/web-viewer/server.js.map +1 -0
  186. package/package.json +66 -0
@@ -0,0 +1,1524 @@
1
+ /**
2
+ * MCP Server for .fig file parsing
3
+ *
4
+ * Provides tools for:
5
+ * - Parsing .fig files
6
+ * - Extracting document structure
7
+ * - Finding nodes by type/name
8
+ * - Getting layout information
9
+ * - Inspecting schema/raw data
10
+ */
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
14
+ import { parseFigFile, parseFigFileSimplified, getFigSchema, getFigRawMessage, listFigContents, simplifyNode, getDocumentSummary, findNodesByType, findNodesByName, hashBytesToHex, formatGUID, buildNodeIdIndex, buildNodePathIndex, } from "../parser/index.js";
15
+ import { renderScreen, generateScreenshot } from "../renderer/index.js";
16
+ import { isVectorNode, exportVector } from "../vector-export.js";
17
+ import { config } from "../shared-config.js";
18
+ // Cache for parsed fig files
19
+ const fileCache = new Map();
20
+ export function normalizeImageHash(value) {
21
+ if (!value)
22
+ return null;
23
+ if (typeof value === "string") {
24
+ return value.toLowerCase();
25
+ }
26
+ if (Array.isArray(value) && value.length === 20) {
27
+ const bytes = value.filter((b) => typeof b === "number");
28
+ if (bytes.length === 20) {
29
+ return bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
30
+ }
31
+ }
32
+ if (typeof value === "object") {
33
+ const obj = value;
34
+ if (obj["hash"] && typeof obj["hash"] === "object") {
35
+ return hashBytesToHex(obj["hash"]);
36
+ }
37
+ const hasByteKeys = Object.keys(obj).some((key) => /^\d+$/.test(key));
38
+ if (hasByteKeys) {
39
+ return hashBytesToHex(obj);
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ export function detectImageFormat(data) {
45
+ if (data[0] === 0xff && data[1] === 0xd8)
46
+ return "jpeg";
47
+ if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
48
+ return "png";
49
+ }
50
+ if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46)
51
+ return "gif";
52
+ if (data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46) {
53
+ return "webp";
54
+ }
55
+ return "unknown";
56
+ }
57
+ function normalizeNodeId(nodeId) {
58
+ // Trim whitespace
59
+ let normalized = nodeId.trim();
60
+ // Convert hyphen to colon if it looks like a node ID (digits-digits)
61
+ if (/^\d+-\d+$/.test(normalized)) {
62
+ normalized = normalized.replace('-', ':');
63
+ }
64
+ return normalized;
65
+ }
66
+ function extractImageReferences(node, nodePath) {
67
+ const refs = [];
68
+ const nodeRecord = node;
69
+ const fills = nodeRecord["fills"] ?? nodeRecord["fillPaints"];
70
+ if (!Array.isArray(fills))
71
+ return refs;
72
+ for (let i = 0; i < fills.length; i++) {
73
+ const paint = fills[i];
74
+ if (!paint || typeof paint !== "object")
75
+ continue;
76
+ const paintType = String(paint["type"] ?? "UNKNOWN");
77
+ const originalWidth = typeof paint["originalImageWidth"] === "number"
78
+ ? paint["originalImageWidth"]
79
+ : undefined;
80
+ const originalHeight = typeof paint["originalImageHeight"] === "number"
81
+ ? paint["originalImageHeight"]
82
+ : undefined;
83
+ const scaleMode = typeof paint["imageScaleMode"] === "string"
84
+ ? paint["imageScaleMode"]
85
+ : typeof paint["scaleMode"] === "string"
86
+ ? paint["scaleMode"]
87
+ : undefined;
88
+ const scale = typeof paint["scale"] === "number" ? paint["scale"] : undefined;
89
+ const rotation = typeof paint["rotation"] === "number" ? paint["rotation"] : undefined;
90
+ const imageHash = normalizeImageHash(paint["image"]) ??
91
+ normalizeImageHash(paint["imageHash"]);
92
+ if (imageHash) {
93
+ refs.push({
94
+ hash: imageHash,
95
+ kind: "image",
96
+ nodeId: formatGUID(node.guid),
97
+ nodeName: node.name,
98
+ nodeType: node.type,
99
+ nodePath,
100
+ paintIndex: i,
101
+ paintType,
102
+ originalWidth,
103
+ originalHeight,
104
+ scaleMode,
105
+ scale,
106
+ rotation,
107
+ });
108
+ }
109
+ const thumbHash = normalizeImageHash(paint["imageThumbnail"]);
110
+ if (thumbHash) {
111
+ refs.push({
112
+ hash: thumbHash,
113
+ kind: "thumbnail",
114
+ nodeId: formatGUID(node.guid),
115
+ nodeName: node.name,
116
+ nodeType: node.type,
117
+ nodePath,
118
+ paintIndex: i,
119
+ paintType,
120
+ originalWidth,
121
+ originalHeight,
122
+ scaleMode,
123
+ scale,
124
+ rotation,
125
+ });
126
+ }
127
+ }
128
+ return refs;
129
+ }
130
+ function summarizeFills(fills) {
131
+ const paintTypes = new Set();
132
+ for (const fill of fills) {
133
+ if (!fill || typeof fill !== "object")
134
+ continue;
135
+ const paint = fill;
136
+ paintTypes.add(String(paint["type"] ?? "UNKNOWN"));
137
+ }
138
+ return {
139
+ paintCount: fills.length,
140
+ paintTypes: Array.from(paintTypes),
141
+ };
142
+ }
143
+ function collectNodesWithFills(node, nodePathIndex, results, includeImageRefs, maxResults) {
144
+ if (maxResults !== undefined && results.length >= maxResults)
145
+ return true;
146
+ const nodeRecord = node;
147
+ const fills = nodeRecord["fills"] ?? nodeRecord["fillPaints"];
148
+ if (Array.isArray(fills) && fills.length > 0) {
149
+ const nodeId = formatGUID(node.guid);
150
+ const nodePath = nodePathIndex.get(nodeId) ?? node.name;
151
+ const entry = {
152
+ nodeId,
153
+ nodeName: node.name,
154
+ nodeType: node.type,
155
+ nodePath,
156
+ fillSummary: summarizeFills(fills),
157
+ };
158
+ if (includeImageRefs) {
159
+ entry.imageRefs = extractImageReferences(node, nodePath);
160
+ }
161
+ results.push(entry);
162
+ if (maxResults !== undefined && results.length >= maxResults)
163
+ return true;
164
+ }
165
+ if (node.children) {
166
+ for (const child of node.children) {
167
+ if (collectNodesWithFills(child, nodePathIndex, results, includeImageRefs, maxResults)) {
168
+ return true;
169
+ }
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ /**
175
+ * Get or parse a fig file with caching
176
+ */
177
+ export async function getOrParseFigFile(filePath) {
178
+ if (!fileCache.has(filePath)) {
179
+ const parsed = await parseFigFile(filePath);
180
+ const nodeIdIndex = buildNodeIdIndex(parsed.document);
181
+ const nodePathIndex = buildNodePathIndex(parsed.document);
182
+ fileCache.set(filePath, {
183
+ document: parsed.document,
184
+ meta: parsed.meta,
185
+ images: parsed.images,
186
+ thumbnail: parsed.thumbnail,
187
+ version: parsed.version,
188
+ nodeIdIndex,
189
+ nodePathIndex,
190
+ blobs: parsed.blobs,
191
+ });
192
+ }
193
+ return fileCache.get(filePath);
194
+ }
195
+ /**
196
+ * Create and configure the MCP server
197
+ */
198
+ export function createServer() {
199
+ const server = new Server({
200
+ name: "fig-mcp",
201
+ version: "1.0.0",
202
+ }, {
203
+ capabilities: {
204
+ tools: {},
205
+ resources: {},
206
+ },
207
+ });
208
+ // List available tools
209
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
210
+ return {
211
+ tools: [
212
+ {
213
+ name: "parse_fig_file",
214
+ description: "Parse a .fig file and return its document structure. Returns a simplified representation suitable for understanding the design hierarchy, layout, and styling.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ filePath: {
219
+ type: "string",
220
+ description: "Path to the .fig file",
221
+ },
222
+ maxDepth: {
223
+ type: "number",
224
+ description: "Maximum depth to traverse (default: 10)",
225
+ },
226
+ },
227
+ required: ["filePath"],
228
+ },
229
+ },
230
+ {
231
+ name: "get_document_summary",
232
+ description: "Get a text summary of the document structure showing node types, names, and dimensions in a tree format. Supports pagination to handle large documents.",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ filePath: {
237
+ type: "string",
238
+ description: "Path to the .fig file",
239
+ },
240
+ maxNodes: {
241
+ type: "number",
242
+ description: "Maximum number of nodes to return (default: 100)",
243
+ },
244
+ offset: {
245
+ type: "number",
246
+ description: "Number of nodes to skip for pagination (default: 0)",
247
+ },
248
+ },
249
+ required: ["filePath"],
250
+ },
251
+ },
252
+ {
253
+ name: "get_tree_summary",
254
+ description: "Get hierarchical summary with child counts for drill-down navigation. Returns high-level structure without full node details.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ filePath: {
259
+ type: "string",
260
+ description: "Path to the .fig file",
261
+ },
262
+ nodePath: {
263
+ type: "string",
264
+ description: "Optional path to start from (e.g., 'Page 1'). If not provided, starts from root.",
265
+ },
266
+ depth: {
267
+ type: "number",
268
+ description: "Levels to show (default: 2)",
269
+ },
270
+ },
271
+ required: ["filePath"],
272
+ },
273
+ },
274
+ {
275
+ name: "find_nodes",
276
+ description: "Find nodes in the document by type or name. Useful for locating specific components, frames, or text elements.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ filePath: {
281
+ type: "string",
282
+ description: "Path to the .fig file",
283
+ },
284
+ type: {
285
+ type: "string",
286
+ description: "Node type to find (e.g., FRAME, TEXT, COMPONENT, INSTANCE)",
287
+ },
288
+ name: {
289
+ type: "string",
290
+ description: "Node name to search for (partial match)",
291
+ },
292
+ },
293
+ required: ["filePath"],
294
+ },
295
+ },
296
+ {
297
+ name: "get_node_details",
298
+ description: "Get detailed information about a specific node by its path in the document tree. Use format like 'Page 1/Frame Name/Child Name'.",
299
+ inputSchema: {
300
+ type: "object",
301
+ properties: {
302
+ filePath: {
303
+ type: "string",
304
+ description: "Path to the .fig file",
305
+ },
306
+ nodePath: {
307
+ type: "string",
308
+ description: "Path to the node (e.g., 'Page 1/Header/Logo')",
309
+ },
310
+ maxDepth: {
311
+ type: "number",
312
+ description: "Maximum depth to traverse children (default: 1, max: 10)",
313
+ default: 1,
314
+ },
315
+ includeChildren: {
316
+ type: "boolean",
317
+ description: "Include child nodes in response (default: true)",
318
+ default: true,
319
+ },
320
+ includeStyles: {
321
+ type: "boolean",
322
+ description: "Include style properties (fills, strokes, effects) (default: true)",
323
+ default: true,
324
+ },
325
+ includeLayout: {
326
+ type: "boolean",
327
+ description: "Include layout inference (gap, padding, alignment) (default: true)",
328
+ default: true,
329
+ },
330
+ includeImageRefs: {
331
+ type: "boolean",
332
+ description: "Include image reference metadata (default: true)",
333
+ default: true,
334
+ },
335
+ includeEffects: {
336
+ type: "boolean",
337
+ description: "Include effects (shadows, blurs) (default: true)",
338
+ default: true,
339
+ },
340
+ compact: {
341
+ type: "boolean",
342
+ description: "Use compact JSON formatting (no indentation) (default: false)",
343
+ default: false,
344
+ },
345
+ },
346
+ required: ["filePath", "nodePath"],
347
+ },
348
+ },
349
+ {
350
+ name: "get_node_by_id",
351
+ description: "Get detailed information about a specific node by its GUID. Node ID format: 'sessionID:localID' (e.g., '457:1607'). Hyphen format also accepted.",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ filePath: {
356
+ type: "string",
357
+ description: "Path to the .fig file",
358
+ },
359
+ nodeId: {
360
+ type: "string",
361
+ description: "GUID string in format 'sessionID:localID' (e.g., '457:1607') or 'sessionID-localID'",
362
+ },
363
+ maxDepth: {
364
+ type: "number",
365
+ description: "Maximum depth to traverse children (default: 1, max: 10)",
366
+ default: 1,
367
+ },
368
+ includeChildren: {
369
+ type: "boolean",
370
+ description: "Include child nodes in response (default: true)",
371
+ default: true,
372
+ },
373
+ includeStyles: {
374
+ type: "boolean",
375
+ description: "Include style properties (fills, strokes, effects) (default: true)",
376
+ default: true,
377
+ },
378
+ includeLayout: {
379
+ type: "boolean",
380
+ description: "Include layout inference (gap, padding, alignment) (default: true)",
381
+ default: true,
382
+ },
383
+ includeImageRefs: {
384
+ type: "boolean",
385
+ description: "Include image reference metadata (default: true)",
386
+ default: true,
387
+ },
388
+ includeEffects: {
389
+ type: "boolean",
390
+ description: "Include effects (shadows, blurs) (default: true)",
391
+ default: true,
392
+ },
393
+ compact: {
394
+ type: "boolean",
395
+ description: "Use compact JSON formatting (no indentation) (default: false)",
396
+ default: false,
397
+ },
398
+ },
399
+ required: ["filePath", "nodeId"],
400
+ },
401
+ },
402
+ {
403
+ name: "get_layout_info",
404
+ description: "Get layout and spacing information for a node. Returns inferred flexbox-like properties including direction, gap, padding, and alignment.",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {
408
+ filePath: {
409
+ type: "string",
410
+ description: "Path to the .fig file",
411
+ },
412
+ nodePath: {
413
+ type: "string",
414
+ description: "Path to the node",
415
+ },
416
+ },
417
+ required: ["filePath", "nodePath"],
418
+ },
419
+ },
420
+ {
421
+ name: "list_pages",
422
+ description: "List all pages (canvases) in the .fig file.",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ filePath: {
427
+ type: "string",
428
+ description: "Path to the .fig file",
429
+ },
430
+ },
431
+ required: ["filePath"],
432
+ },
433
+ },
434
+ {
435
+ name: "get_page_contents",
436
+ description: "Get the contents of a specific page, including all top-level frames and their immediate children.",
437
+ inputSchema: {
438
+ type: "object",
439
+ properties: {
440
+ filePath: {
441
+ type: "string",
442
+ description: "Path to the .fig file",
443
+ },
444
+ pageName: {
445
+ type: "string",
446
+ description: "Name of the page to get contents for",
447
+ },
448
+ },
449
+ required: ["filePath", "pageName"],
450
+ },
451
+ },
452
+ {
453
+ name: "get_text_content",
454
+ description: "Extract all text content from the document or a specific node path.",
455
+ inputSchema: {
456
+ type: "object",
457
+ properties: {
458
+ filePath: {
459
+ type: "string",
460
+ description: "Path to the .fig file",
461
+ },
462
+ nodePath: {
463
+ type: "string",
464
+ description: "Optional path to limit text extraction scope",
465
+ },
466
+ },
467
+ required: ["filePath"],
468
+ },
469
+ },
470
+ {
471
+ name: "get_colors",
472
+ description: "Extract all unique colors used in the document, including fills and strokes.",
473
+ inputSchema: {
474
+ type: "object",
475
+ properties: {
476
+ filePath: {
477
+ type: "string",
478
+ description: "Path to the .fig file",
479
+ },
480
+ },
481
+ required: ["filePath"],
482
+ },
483
+ },
484
+ {
485
+ name: "list_nodes_with_fills",
486
+ description: "List nodes that have fill paints, with summary of paint types and optional image references.",
487
+ inputSchema: {
488
+ type: "object",
489
+ properties: {
490
+ filePath: {
491
+ type: "string",
492
+ description: "Path to the .fig file",
493
+ },
494
+ includeImageRefs: {
495
+ type: "boolean",
496
+ description: "Include image hash references for IMAGE fills",
497
+ },
498
+ maxResults: {
499
+ type: "number",
500
+ description: "Optional cap on number of nodes returned",
501
+ },
502
+ },
503
+ required: ["filePath"],
504
+ },
505
+ },
506
+ {
507
+ name: "get_schema_info",
508
+ description: "Get the kiwi schema information from the .fig file. Useful for debugging and understanding the file format.",
509
+ inputSchema: {
510
+ type: "object",
511
+ properties: {
512
+ filePath: {
513
+ type: "string",
514
+ description: "Path to the .fig file",
515
+ },
516
+ },
517
+ required: ["filePath"],
518
+ },
519
+ },
520
+ {
521
+ name: "get_raw_message",
522
+ description: "Get the raw decoded message from the .fig file. Warning: can be very large. Use for debugging only.",
523
+ inputSchema: {
524
+ type: "object",
525
+ properties: {
526
+ filePath: {
527
+ type: "string",
528
+ description: "Path to the .fig file",
529
+ },
530
+ maxSize: {
531
+ type: "number",
532
+ description: "Maximum size in characters to return (default: 50000)",
533
+ },
534
+ },
535
+ required: ["filePath"],
536
+ },
537
+ },
538
+ {
539
+ name: "list_archive_contents",
540
+ description: "List all files contained in the .fig archive.",
541
+ inputSchema: {
542
+ type: "object",
543
+ properties: {
544
+ filePath: {
545
+ type: "string",
546
+ description: "Path to the .fig file",
547
+ },
548
+ },
549
+ required: ["filePath"],
550
+ },
551
+ },
552
+ {
553
+ name: "clear_cache",
554
+ description: "Clear the file cache. Useful if the .fig file has been modified.",
555
+ inputSchema: {
556
+ type: "object",
557
+ properties: {
558
+ filePath: {
559
+ type: "string",
560
+ description: "Path to clear from cache (optional, clears all if not specified)",
561
+ },
562
+ },
563
+ },
564
+ },
565
+ {
566
+ name: "list_images",
567
+ description: "List all images in the .fig file with their metadata. Returns image hashes, dimensions, and which nodes reference them.",
568
+ inputSchema: {
569
+ type: "object",
570
+ properties: {
571
+ filePath: {
572
+ type: "string",
573
+ description: "Path to the .fig file",
574
+ },
575
+ },
576
+ required: ["filePath"],
577
+ },
578
+ },
579
+ {
580
+ name: "get_image",
581
+ description: "Get a specific image from the .fig file as a resource. Use the hash from list_images.",
582
+ inputSchema: {
583
+ type: "object",
584
+ properties: {
585
+ filePath: {
586
+ type: "string",
587
+ description: "Path to the .fig file",
588
+ },
589
+ imageHash: {
590
+ type: "string",
591
+ description: "The 40-character hex hash of the image",
592
+ },
593
+ },
594
+ required: ["filePath", "imageHash"],
595
+ },
596
+ },
597
+ {
598
+ name: "get_thumbnail",
599
+ description: "Get the document thumbnail image as a resource.",
600
+ inputSchema: {
601
+ type: "object",
602
+ properties: {
603
+ filePath: {
604
+ type: "string",
605
+ description: "Path to the .fig file",
606
+ },
607
+ },
608
+ required: ["filePath"],
609
+ },
610
+ },
611
+ {
612
+ name: "render_screen",
613
+ description: "Experimental: render a node subtree to a PNG screenshot using its bounds, fills, strokes, shadows, and text. Node ID format: 'sessionID:localID' (e.g., '457:1607'). Hyphen format also accepted.",
614
+ inputSchema: {
615
+ type: "object",
616
+ properties: {
617
+ filePath: {
618
+ type: "string",
619
+ description: "Path to the .fig file",
620
+ },
621
+ nodeId: {
622
+ type: "string",
623
+ description: "GUID string in format 'sessionID:localID' (e.g., '457:1607') or 'sessionID-localID'",
624
+ },
625
+ options: {
626
+ type: "object",
627
+ description: "Rendering options",
628
+ properties: {
629
+ maxDepth: { type: "number" },
630
+ includeText: { type: "boolean" },
631
+ includeFills: { type: "boolean" },
632
+ includeStrokes: { type: "boolean" },
633
+ includeImages: { type: "boolean" },
634
+ includeShadows: { type: "boolean", description: "Include drop shadows and inner shadows (default: true)" },
635
+ background: { type: "string" },
636
+ scale: { type: "number" },
637
+ maxWidth: { type: "number", description: "Maximum width in pixels (default: 800)" },
638
+ maxHeight: { type: "number", description: "Maximum height in pixels (default: 600)" },
639
+ },
640
+ },
641
+ },
642
+ required: ["filePath", "nodeId"],
643
+ },
644
+ },
645
+ {
646
+ name: "get_vector",
647
+ description: "Export a vector node as SVG, PDF, PNG, or WebP. SVG returns the vector path directly. PDF returns a vector-based PDF (ideal for iOS). PNG/WebP returns a rasterized image at specified dimensions.",
648
+ inputSchema: {
649
+ type: "object",
650
+ properties: {
651
+ filePath: {
652
+ type: "string",
653
+ description: "Path to the .fig file",
654
+ },
655
+ nodeId: {
656
+ type: "string",
657
+ description: "GUID string in format 'sessionID:localID' (e.g., '457:1682') or 'sessionID-localID'",
658
+ },
659
+ format: {
660
+ type: "string",
661
+ enum: ["svg", "pdf", "png", "webp"],
662
+ description: "Output format: svg (vector), pdf (vector for iOS), png/webp (raster)",
663
+ },
664
+ width: {
665
+ type: "number",
666
+ description: "Output width in pixels (required for png/webp, optional for pdf)",
667
+ },
668
+ height: {
669
+ type: "number",
670
+ description: "Output height in pixels (required for png/webp, optional for pdf)",
671
+ },
672
+ includeStyles: {
673
+ type: "boolean",
674
+ description: "Include fill/stroke styles from node (default: true)",
675
+ default: true,
676
+ },
677
+ },
678
+ required: ["filePath", "nodeId", "format"],
679
+ },
680
+ },
681
+ ],
682
+ };
683
+ });
684
+ // Handle tool calls
685
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
686
+ const { name, arguments: args } = request.params;
687
+ try {
688
+ switch (name) {
689
+ case "parse_fig_file": {
690
+ const { filePath, maxDepth = 10 } = args;
691
+ const result = await parseFigFileSimplified(filePath, maxDepth);
692
+ return {
693
+ content: [
694
+ {
695
+ type: "text",
696
+ text: JSON.stringify(result, null, 2),
697
+ },
698
+ ],
699
+ };
700
+ }
701
+ case "get_document_summary": {
702
+ const { filePath, maxNodes = 100, offset = 0 } = args;
703
+ const { document } = await getOrParseFigFile(filePath);
704
+ const fullSummary = getDocumentSummary(document);
705
+ const lines = fullSummary.split('\n');
706
+ const totalNodes = lines.length;
707
+ const paginatedLines = lines.slice(offset, offset + maxNodes);
708
+ let resultText = paginatedLines.join('\n');
709
+ if (totalNodes > maxNodes || offset > 0) {
710
+ const start = offset + 1;
711
+ const end = Math.min(offset + maxNodes, totalNodes);
712
+ resultText = `Showing nodes ${start}-${end} of ${totalNodes}\n\n${resultText}`;
713
+ if (end < totalNodes) {
714
+ resultText += `\n\n... (${totalNodes - end} more nodes)`;
715
+ }
716
+ }
717
+ return {
718
+ content: [
719
+ {
720
+ type: "text",
721
+ text: resultText,
722
+ },
723
+ ],
724
+ };
725
+ }
726
+ case "get_tree_summary": {
727
+ const { filePath, nodePath, depth = 2 } = args;
728
+ const { document, nodePathIndex } = await getOrParseFigFile(filePath);
729
+ // Find starting node
730
+ let startNode = document;
731
+ if (nodePath) {
732
+ // Try to find node by path
733
+ const pathParts = nodePath.split('/').filter(p => p);
734
+ let current = document;
735
+ for (const part of pathParts) {
736
+ const child = current.children?.find(c => c.name === part);
737
+ if (!child) {
738
+ return {
739
+ content: [{
740
+ type: "text",
741
+ text: `Node not found at path: ${nodePath}`,
742
+ }],
743
+ isError: true,
744
+ };
745
+ }
746
+ current = child;
747
+ }
748
+ startNode = current;
749
+ }
750
+ // Generate tree summary
751
+ const generateTreeSummary = (node, currentDepth = 0, maxDepth = 2, prefix = "") => {
752
+ if (currentDepth > maxDepth)
753
+ return "";
754
+ const childCount = node.children?.length ?? 0;
755
+ const childSuffix = childCount > 0 ? ` [${childCount} children]` : "";
756
+ let result = `${prefix}${node.name} (${node.type})${childSuffix}\n`;
757
+ if (currentDepth < maxDepth && node.children && node.children.length > 0) {
758
+ const isLast = (i) => i === node.children.length - 1;
759
+ node.children.forEach((child, i) => {
760
+ const childPrefix = prefix + (isLast(i) ? "└── " : "├── ");
761
+ const grandchildPrefix = prefix + (isLast(i) ? " " : "│ ");
762
+ result += generateTreeSummary(child, currentDepth + 1, maxDepth, childPrefix);
763
+ });
764
+ }
765
+ return result;
766
+ };
767
+ const summary = generateTreeSummary(startNode, 0, depth);
768
+ return {
769
+ content: [
770
+ {
771
+ type: "text",
772
+ text: summary,
773
+ },
774
+ ],
775
+ };
776
+ }
777
+ case "find_nodes": {
778
+ const { filePath, type, name: nodeName } = args;
779
+ const { document } = await getOrParseFigFile(filePath);
780
+ let results = [];
781
+ if (type) {
782
+ results = findNodesByType(document, type);
783
+ }
784
+ if (nodeName) {
785
+ const byName = findNodesByName(document, nodeName);
786
+ if (results.length > 0) {
787
+ // Intersection if both type and name specified
788
+ const nameSet = new Set(byName);
789
+ results = results.filter((n) => nameSet.has(n));
790
+ }
791
+ else {
792
+ results = byName;
793
+ }
794
+ }
795
+ // Simplify results
796
+ const simplified = results.map((n) => simplifyNode(n, 0, 2));
797
+ return {
798
+ content: [
799
+ {
800
+ type: "text",
801
+ text: JSON.stringify({
802
+ count: simplified.length,
803
+ nodes: simplified,
804
+ }, null, 2),
805
+ },
806
+ ],
807
+ };
808
+ }
809
+ case "get_node_details": {
810
+ const { filePath, nodePath, maxDepth = 1, includeChildren = true, includeStyles = true, includeLayout = true, includeImageRefs = true, includeEffects = true, compact = false, } = args;
811
+ // Validate maxDepth
812
+ const safeMaxDepth = Math.min(Math.max(maxDepth, 0), 10);
813
+ const { document } = await getOrParseFigFile(filePath);
814
+ const pathParts = nodePath.split("/").filter((p) => p.length > 0);
815
+ let current = document;
816
+ const resolvedParts = [];
817
+ for (const part of pathParts) {
818
+ if (!current?.children) {
819
+ current = undefined;
820
+ break;
821
+ }
822
+ current = current.children.find((c) => c.name.toLowerCase().includes(part.toLowerCase()));
823
+ if (current) {
824
+ resolvedParts.push(current.name);
825
+ }
826
+ }
827
+ if (!current) {
828
+ return {
829
+ content: [
830
+ {
831
+ type: "text",
832
+ text: `Node not found at path: ${nodePath}`,
833
+ },
834
+ ],
835
+ };
836
+ }
837
+ // Pass filtering options to simplifyNode
838
+ const simplified = simplifyNode(current, 0, includeChildren ? safeMaxDepth : 0, {
839
+ includeStyles,
840
+ includeLayout,
841
+ includeEffects,
842
+ });
843
+ const resolvedPath = resolvedParts.join("/");
844
+ // Conditionally include image refs
845
+ const imageRefs = includeImageRefs
846
+ ? extractImageReferences(current, resolvedPath.length > 0 ? resolvedPath : current.name)
847
+ : undefined;
848
+ const response = {
849
+ ...simplified,
850
+ ...(imageRefs && imageRefs.length > 0 && { imageRefs }),
851
+ };
852
+ return {
853
+ content: [
854
+ {
855
+ type: "text",
856
+ text: JSON.stringify(response, null, compact ? 0 : 2),
857
+ },
858
+ ],
859
+ };
860
+ }
861
+ case "get_node_by_id": {
862
+ const { filePath, nodeId, maxDepth = 1, includeChildren = true, includeStyles = true, includeLayout = true, includeImageRefs = true, includeEffects = true, compact = false, } = args;
863
+ // Validate maxDepth
864
+ const safeMaxDepth = Math.min(Math.max(maxDepth, 0), 10);
865
+ const { nodeIdIndex, nodePathIndex } = await getOrParseFigFile(filePath);
866
+ const normalizedId = normalizeNodeId(nodeId);
867
+ const node = nodeIdIndex.get(normalizedId);
868
+ if (!node) {
869
+ return {
870
+ content: [
871
+ {
872
+ type: "text",
873
+ text: `Node not found for id: ${nodeId}`,
874
+ },
875
+ ],
876
+ isError: true,
877
+ };
878
+ }
879
+ // Pass filtering options to simplifyNode
880
+ const simplified = simplifyNode(node, 0, includeChildren ? safeMaxDepth : 0, {
881
+ includeStyles,
882
+ includeLayout,
883
+ includeEffects,
884
+ });
885
+ const nodePath = nodePathIndex.get(normalizedId) ?? node.name;
886
+ // Conditionally include image refs
887
+ const imageRefs = includeImageRefs
888
+ ? extractImageReferences(node, nodePath)
889
+ : undefined;
890
+ const response = {
891
+ ...simplified,
892
+ nodePath,
893
+ ...(imageRefs && imageRefs.length > 0 && { imageRefs }),
894
+ };
895
+ return {
896
+ content: [
897
+ {
898
+ type: "text",
899
+ text: JSON.stringify(response, null, compact ? 0 : 2),
900
+ },
901
+ ],
902
+ };
903
+ }
904
+ case "get_layout_info": {
905
+ const { filePath, nodePath } = args;
906
+ const { document } = await getOrParseFigFile(filePath);
907
+ const pathParts = nodePath.split("/").filter((p) => p.length > 0);
908
+ let current = document;
909
+ for (const part of pathParts) {
910
+ if (!current?.children) {
911
+ current = undefined;
912
+ break;
913
+ }
914
+ current = current.children.find((c) => c.name.toLowerCase().includes(part.toLowerCase()));
915
+ }
916
+ if (!current) {
917
+ return {
918
+ content: [
919
+ {
920
+ type: "text",
921
+ text: `Node not found at path: ${nodePath}`,
922
+ },
923
+ ],
924
+ };
925
+ }
926
+ const simplified = simplifyNode(current, 0, 1);
927
+ return {
928
+ content: [
929
+ {
930
+ type: "text",
931
+ text: JSON.stringify({
932
+ name: simplified?.name,
933
+ type: simplified?.type,
934
+ bounds: simplified?.bounds,
935
+ layout: simplified?.layout,
936
+ style: simplified?.style,
937
+ }, null, 2),
938
+ },
939
+ ],
940
+ };
941
+ }
942
+ case "list_pages": {
943
+ const { filePath } = args;
944
+ const { document } = await getOrParseFigFile(filePath);
945
+ const pages = document.children?.map((c) => ({
946
+ name: c.name,
947
+ type: c.type,
948
+ childCount: c.children?.length ?? 0,
949
+ }));
950
+ return {
951
+ content: [
952
+ {
953
+ type: "text",
954
+ text: JSON.stringify({ pages }, null, 2),
955
+ },
956
+ ],
957
+ };
958
+ }
959
+ case "get_page_contents": {
960
+ const { filePath, pageName } = args;
961
+ const { document } = await getOrParseFigFile(filePath);
962
+ const page = document.children?.find((c) => c.name.toLowerCase().includes(pageName.toLowerCase()));
963
+ if (!page) {
964
+ return {
965
+ content: [
966
+ {
967
+ type: "text",
968
+ text: `Page not found: ${pageName}`,
969
+ },
970
+ ],
971
+ };
972
+ }
973
+ const simplified = simplifyNode(page, 0, 3);
974
+ return {
975
+ content: [
976
+ {
977
+ type: "text",
978
+ text: JSON.stringify(simplified, null, 2),
979
+ },
980
+ ],
981
+ };
982
+ }
983
+ case "get_text_content": {
984
+ const { filePath, nodePath } = args;
985
+ const { document } = await getOrParseFigFile(filePath);
986
+ let startNode = document;
987
+ if (nodePath) {
988
+ const pathParts = nodePath.split("/").filter((p) => p.length > 0);
989
+ for (const part of pathParts) {
990
+ if (!startNode?.children)
991
+ break;
992
+ const found = startNode.children.find((c) => c.name
993
+ .toLowerCase()
994
+ .includes(part.toLowerCase()));
995
+ if (found)
996
+ startNode = found;
997
+ }
998
+ }
999
+ const textNodes = findNodesByType(startNode, "TEXT");
1000
+ const texts = textNodes.map((n) => ({
1001
+ name: n.name,
1002
+ content: n.characters,
1003
+ }));
1004
+ return {
1005
+ content: [
1006
+ {
1007
+ type: "text",
1008
+ text: JSON.stringify({ texts }, null, 2),
1009
+ },
1010
+ ],
1011
+ };
1012
+ }
1013
+ case "get_colors": {
1014
+ const { filePath } = args;
1015
+ const { document } = await getOrParseFigFile(filePath);
1016
+ const colors = new Set();
1017
+ function extractColors(node) {
1018
+ const sceneNode = node;
1019
+ if (sceneNode.fills) {
1020
+ for (const fill of sceneNode.fills) {
1021
+ if (fill.color) {
1022
+ const { r, g, b, a } = fill.color;
1023
+ colors.add(`rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a.toFixed(2)})`);
1024
+ }
1025
+ }
1026
+ }
1027
+ if (sceneNode.strokes) {
1028
+ for (const stroke of sceneNode.strokes) {
1029
+ if (stroke.color) {
1030
+ const { r, g, b, a } = stroke.color;
1031
+ colors.add(`rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a.toFixed(2)})`);
1032
+ }
1033
+ }
1034
+ }
1035
+ if (node.children) {
1036
+ for (const child of node.children) {
1037
+ extractColors(child);
1038
+ }
1039
+ }
1040
+ }
1041
+ extractColors(document);
1042
+ return {
1043
+ content: [
1044
+ {
1045
+ type: "text",
1046
+ text: JSON.stringify({ colors: Array.from(colors).sort() }, null, 2),
1047
+ },
1048
+ ],
1049
+ };
1050
+ }
1051
+ case "get_schema_info": {
1052
+ const { filePath } = args;
1053
+ const result = await getFigSchema(filePath);
1054
+ return {
1055
+ content: [
1056
+ {
1057
+ type: "text",
1058
+ text: JSON.stringify(result, null, 2),
1059
+ },
1060
+ ],
1061
+ };
1062
+ }
1063
+ case "list_nodes_with_fills": {
1064
+ const { filePath, includeImageRefs, maxResults } = args;
1065
+ const { document, nodePathIndex } = await getOrParseFigFile(filePath);
1066
+ const nodes = [];
1067
+ collectNodesWithFills(document, nodePathIndex, nodes, includeImageRefs ?? false, maxResults);
1068
+ return {
1069
+ content: [
1070
+ {
1071
+ type: "text",
1072
+ text: JSON.stringify({
1073
+ count: nodes.length,
1074
+ nodes,
1075
+ }, null, 2),
1076
+ },
1077
+ ],
1078
+ };
1079
+ }
1080
+ case "get_raw_message": {
1081
+ const { filePath, maxSize = 50000 } = args;
1082
+ const result = await getFigRawMessage(filePath);
1083
+ let text = JSON.stringify(result, null, 2);
1084
+ if (text.length > maxSize) {
1085
+ text =
1086
+ text.substring(0, maxSize) +
1087
+ `\n\n... [truncated, total size: ${text.length} chars]`;
1088
+ }
1089
+ return {
1090
+ content: [
1091
+ {
1092
+ type: "text",
1093
+ text,
1094
+ },
1095
+ ],
1096
+ };
1097
+ }
1098
+ case "list_archive_contents": {
1099
+ const { filePath } = args;
1100
+ const contents = await listFigContents(filePath);
1101
+ return {
1102
+ content: [
1103
+ {
1104
+ type: "text",
1105
+ text: JSON.stringify({ files: contents }, null, 2),
1106
+ },
1107
+ ],
1108
+ };
1109
+ }
1110
+ case "list_images": {
1111
+ const { filePath } = args;
1112
+ const { document, images } = await getOrParseFigFile(filePath);
1113
+ const entries = new Map();
1114
+ for (const [hash, data] of images) {
1115
+ entries.set(hash, {
1116
+ hash,
1117
+ byteLength: data.length,
1118
+ format: detectImageFormat(data),
1119
+ references: [],
1120
+ dimensions: new Set(),
1121
+ });
1122
+ }
1123
+ const missingRefs = new Map();
1124
+ function walk(node, path) {
1125
+ const nodePath = path ? `${path}/${node.name}` : node.name;
1126
+ const refs = extractImageReferences(node, nodePath);
1127
+ for (const ref of refs) {
1128
+ const entry = entries.get(ref.hash);
1129
+ if (entry) {
1130
+ entry.references.push(ref);
1131
+ if (ref.originalWidth !== undefined && ref.originalHeight !== undefined) {
1132
+ entry.dimensions.add(`${ref.originalWidth}x${ref.originalHeight}`);
1133
+ }
1134
+ }
1135
+ else {
1136
+ if (!missingRefs.has(ref.hash)) {
1137
+ missingRefs.set(ref.hash, []);
1138
+ }
1139
+ missingRefs.get(ref.hash).push(ref);
1140
+ }
1141
+ }
1142
+ if (node.children) {
1143
+ for (const child of node.children) {
1144
+ walk(child, nodePath);
1145
+ }
1146
+ }
1147
+ }
1148
+ walk(document, "");
1149
+ const imagesList = Array.from(entries.values())
1150
+ .sort((a, b) => a.hash.localeCompare(b.hash))
1151
+ .map((entry) => ({
1152
+ hash: entry.hash,
1153
+ byteLength: entry.byteLength,
1154
+ format: entry.format,
1155
+ referenceCount: entry.references.length,
1156
+ dimensions: Array.from(entry.dimensions).map((dim) => {
1157
+ const [width, height] = dim.split("x").map((n) => Number(n));
1158
+ return { width, height };
1159
+ }),
1160
+ references: entry.references,
1161
+ }));
1162
+ const unresolvedReferences = Array.from(missingRefs.entries()).map(([hash, references]) => ({
1163
+ hash,
1164
+ referenceCount: references.length,
1165
+ references,
1166
+ }));
1167
+ return {
1168
+ content: [
1169
+ {
1170
+ type: "text",
1171
+ text: JSON.stringify({
1172
+ count: imagesList.length,
1173
+ images: imagesList,
1174
+ unresolvedReferences,
1175
+ }, null, 2),
1176
+ },
1177
+ ],
1178
+ };
1179
+ }
1180
+ case "get_image": {
1181
+ const { filePath, imageHash } = args;
1182
+ const { images } = await getOrParseFigFile(filePath);
1183
+ const normalized = imageHash.toLowerCase();
1184
+ const data = images.get(normalized) ?? images.get(imageHash);
1185
+ if (!data) {
1186
+ return {
1187
+ content: [
1188
+ {
1189
+ type: "text",
1190
+ text: `Image not found for hash: ${imageHash}`,
1191
+ },
1192
+ ],
1193
+ isError: true,
1194
+ };
1195
+ }
1196
+ const format = detectImageFormat(data);
1197
+ const mimeType = format === "jpeg"
1198
+ ? "image/jpeg"
1199
+ : format === "png"
1200
+ ? "image/png"
1201
+ : format === "gif"
1202
+ ? "image/gif"
1203
+ : format === "webp"
1204
+ ? "image/webp"
1205
+ : "application/octet-stream";
1206
+ // Return HTTP URL instead of blob
1207
+ const httpUrl = `http://localhost:${config.httpPort}/image/${encodeURIComponent(filePath)}/${normalized}`;
1208
+ return {
1209
+ content: [
1210
+ {
1211
+ type: "text",
1212
+ text: JSON.stringify({
1213
+ url: httpUrl,
1214
+ hash: normalized,
1215
+ format: mimeType,
1216
+ size: data.length,
1217
+ }),
1218
+ },
1219
+ ],
1220
+ };
1221
+ }
1222
+ case "get_thumbnail": {
1223
+ const { filePath } = args;
1224
+ const { thumbnail } = await getOrParseFigFile(filePath);
1225
+ if (!thumbnail) {
1226
+ return {
1227
+ content: [
1228
+ {
1229
+ type: "text",
1230
+ text: "No thumbnail.png found in the .fig file",
1231
+ },
1232
+ ],
1233
+ isError: true,
1234
+ };
1235
+ }
1236
+ // Return HTTP URL instead of blob
1237
+ const httpUrl = `http://localhost:${config.httpPort}/thumbnail/${encodeURIComponent(filePath)}`;
1238
+ return {
1239
+ content: [
1240
+ {
1241
+ type: "text",
1242
+ text: JSON.stringify({
1243
+ url: httpUrl,
1244
+ format: "image/png",
1245
+ size: thumbnail.length,
1246
+ }),
1247
+ },
1248
+ ],
1249
+ };
1250
+ }
1251
+ case "render_screen": {
1252
+ const { filePath, nodeId, options } = args;
1253
+ const { nodeIdIndex, images, blobs } = await getOrParseFigFile(filePath);
1254
+ const normalizedId = normalizeNodeId(nodeId);
1255
+ const node = nodeIdIndex.get(normalizedId);
1256
+ if (!node) {
1257
+ return {
1258
+ content: [
1259
+ {
1260
+ type: "text",
1261
+ text: `Node not found for id: ${nodeId}`,
1262
+ },
1263
+ ],
1264
+ isError: true,
1265
+ };
1266
+ }
1267
+ const result = renderScreen(node, images, blobs, {
1268
+ maxDepth: typeof options?.maxDepth === "number" ? options.maxDepth : undefined,
1269
+ includeText: typeof options?.includeText === "boolean" ? options.includeText : undefined,
1270
+ includeFills: typeof options?.includeFills === "boolean" ? options.includeFills : undefined,
1271
+ includeStrokes: typeof options?.includeStrokes === "boolean" ? options.includeStrokes : undefined,
1272
+ includeImages: typeof options?.includeImages === "boolean" ? options.includeImages : undefined,
1273
+ includeShadows: typeof options?.includeShadows === "boolean" ? options.includeShadows : undefined,
1274
+ background: typeof options?.background === "string" ? options.background : undefined,
1275
+ scale: typeof options?.scale === "number" ? options.scale : undefined,
1276
+ });
1277
+ // Convert SVG to PNG
1278
+ const screenshot = await generateScreenshot(result.svg, {
1279
+ maxWidth: typeof options?.maxWidth === "number" ? options.maxWidth : undefined,
1280
+ maxHeight: typeof options?.maxHeight === "number" ? options.maxHeight : undefined,
1281
+ });
1282
+ return {
1283
+ content: [
1284
+ {
1285
+ type: "image",
1286
+ data: screenshot.base64,
1287
+ mimeType: screenshot.mimeType,
1288
+ },
1289
+ ],
1290
+ };
1291
+ }
1292
+ case "get_vector": {
1293
+ const { filePath, nodeId, format, width, height, includeStyles = true, } = args;
1294
+ const { nodeIdIndex, blobs } = await getOrParseFigFile(filePath);
1295
+ const normalizedId = normalizeNodeId(nodeId);
1296
+ const node = nodeIdIndex.get(normalizedId);
1297
+ if (!node) {
1298
+ return {
1299
+ content: [
1300
+ {
1301
+ type: "text",
1302
+ text: `Node not found for id: ${nodeId}`,
1303
+ },
1304
+ ],
1305
+ isError: true,
1306
+ };
1307
+ }
1308
+ // Check if node is a vector type
1309
+ if (!isVectorNode(node)) {
1310
+ return {
1311
+ content: [
1312
+ {
1313
+ type: "text",
1314
+ text: `Node ${nodeId} (${node.type}) is not a vector node. Vector export requires VECTOR, LINE, STAR, ELLIPSE, REGULAR_POLYGON, or BOOLEAN_OPERATION nodes, or nodes with fillGeometry/strokeGeometry.`,
1315
+ },
1316
+ ],
1317
+ isError: true,
1318
+ };
1319
+ }
1320
+ // Validate dimensions for raster formats
1321
+ if ((format === "png" || format === "webp") && (!width || !height)) {
1322
+ return {
1323
+ content: [
1324
+ {
1325
+ type: "text",
1326
+ text: `width and height are required for ${format} format`,
1327
+ },
1328
+ ],
1329
+ isError: true,
1330
+ };
1331
+ }
1332
+ try {
1333
+ const result = await exportVector(node, blobs, format, {
1334
+ width,
1335
+ height,
1336
+ includeStyles,
1337
+ });
1338
+ if (format === "svg") {
1339
+ // Return SVG directly as text
1340
+ return {
1341
+ content: [
1342
+ {
1343
+ type: "text",
1344
+ text: JSON.stringify({
1345
+ format: "svg",
1346
+ width: result.width,
1347
+ height: result.height,
1348
+ mimeType: result.mimeType,
1349
+ svg: result.data,
1350
+ }, null, 2),
1351
+ },
1352
+ ],
1353
+ };
1354
+ }
1355
+ else {
1356
+ // For binary formats (PDF, PNG, WebP), return as base64
1357
+ const buffer = result.data;
1358
+ const base64 = buffer.toString("base64");
1359
+ return {
1360
+ content: [
1361
+ {
1362
+ type: "text",
1363
+ text: JSON.stringify({
1364
+ format: result.format,
1365
+ width: result.width,
1366
+ height: result.height,
1367
+ mimeType: result.mimeType,
1368
+ size: buffer.length,
1369
+ data: base64,
1370
+ }, null, 2),
1371
+ },
1372
+ ],
1373
+ };
1374
+ }
1375
+ }
1376
+ catch (exportError) {
1377
+ return {
1378
+ content: [
1379
+ {
1380
+ type: "text",
1381
+ text: `Vector export failed: ${exportError instanceof Error ? exportError.message : String(exportError)}`,
1382
+ },
1383
+ ],
1384
+ isError: true,
1385
+ };
1386
+ }
1387
+ }
1388
+ case "clear_cache": {
1389
+ const { filePath } = args;
1390
+ if (filePath) {
1391
+ fileCache.delete(filePath);
1392
+ return {
1393
+ content: [
1394
+ {
1395
+ type: "text",
1396
+ text: `Cleared cache for: ${filePath}`,
1397
+ },
1398
+ ],
1399
+ };
1400
+ }
1401
+ else {
1402
+ fileCache.clear();
1403
+ return {
1404
+ content: [
1405
+ {
1406
+ type: "text",
1407
+ text: "Cleared all cached files",
1408
+ },
1409
+ ],
1410
+ };
1411
+ }
1412
+ }
1413
+ default:
1414
+ throw new Error(`Unknown tool: ${name}`);
1415
+ }
1416
+ }
1417
+ catch (error) {
1418
+ const message = error instanceof Error ? error.message : String(error);
1419
+ return {
1420
+ content: [
1421
+ {
1422
+ type: "text",
1423
+ text: `Error: ${message}`,
1424
+ },
1425
+ ],
1426
+ isError: true,
1427
+ };
1428
+ }
1429
+ });
1430
+ // List resources (fig files in common locations could be listed here)
1431
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
1432
+ return { resources: [] };
1433
+ });
1434
+ // List resource templates
1435
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
1436
+ return {
1437
+ resourceTemplates: [
1438
+ {
1439
+ uriTemplate: "fig://{filePath}/images/{imageHash}",
1440
+ name: "Fig Image",
1441
+ description: "Image asset from a .fig file. filePath should be URL-encoded.",
1442
+ mimeType: "image/*",
1443
+ },
1444
+ {
1445
+ uriTemplate: "fig://{filePath}/thumbnail",
1446
+ name: "Fig Thumbnail",
1447
+ description: "Thumbnail preview of a .fig file. filePath should be URL-encoded.",
1448
+ mimeType: "image/png",
1449
+ },
1450
+ ],
1451
+ };
1452
+ });
1453
+ // Read resource
1454
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1455
+ const uri = request.params.uri;
1456
+ // Parse fig:// URIs
1457
+ // Format: fig://{encodedFilePath}/images/{imageHash}
1458
+ // Format: fig://{encodedFilePath}/thumbnail
1459
+ if (uri.startsWith("fig://")) {
1460
+ const path = uri.slice("fig://".length);
1461
+ // Check for thumbnail
1462
+ if (path.endsWith("/thumbnail")) {
1463
+ const encodedFilePath = path.slice(0, -"/thumbnail".length);
1464
+ const filePath = decodeURIComponent(encodedFilePath);
1465
+ const { thumbnail } = await getOrParseFigFile(filePath);
1466
+ if (!thumbnail) {
1467
+ throw new Error(`No thumbnail found in: ${filePath}`);
1468
+ }
1469
+ return {
1470
+ contents: [
1471
+ {
1472
+ uri,
1473
+ mimeType: "image/png",
1474
+ blob: Buffer.from(thumbnail).toString("base64"),
1475
+ },
1476
+ ],
1477
+ };
1478
+ }
1479
+ // Check for image
1480
+ const imageMatch = path.match(/^(.+)\/images\/([a-fA-F0-9]{40})$/);
1481
+ if (imageMatch) {
1482
+ const [, encodedFilePath, imageHash] = imageMatch;
1483
+ const filePath = decodeURIComponent(encodedFilePath);
1484
+ const { images } = await getOrParseFigFile(filePath);
1485
+ const normalized = imageHash.toLowerCase();
1486
+ const data = images.get(normalized) ?? images.get(imageHash);
1487
+ if (!data) {
1488
+ throw new Error(`Image not found: ${imageHash} in ${filePath}`);
1489
+ }
1490
+ const format = detectImageFormat(data);
1491
+ const mimeType = format === "jpeg"
1492
+ ? "image/jpeg"
1493
+ : format === "png"
1494
+ ? "image/png"
1495
+ : format === "gif"
1496
+ ? "image/gif"
1497
+ : format === "webp"
1498
+ ? "image/webp"
1499
+ : "application/octet-stream";
1500
+ return {
1501
+ contents: [
1502
+ {
1503
+ uri,
1504
+ mimeType,
1505
+ blob: Buffer.from(data).toString("base64"),
1506
+ },
1507
+ ],
1508
+ };
1509
+ }
1510
+ }
1511
+ throw new Error(`Resource not found: ${uri}`);
1512
+ });
1513
+ return server;
1514
+ }
1515
+ /**
1516
+ * Start the MCP server
1517
+ */
1518
+ export async function startServer() {
1519
+ const server = createServer();
1520
+ const transport = new StdioServerTransport();
1521
+ await server.connect(transport);
1522
+ console.error("Fig MCP server started");
1523
+ }
1524
+ //# sourceMappingURL=server.js.map