@seed-design/mcp 1.2.1 → 1.3.1

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/dist/index.js ADDED
@@ -0,0 +1,537 @@
1
+ import { createRestNormalizer, figma, react, getFigmaColorVariableNames } from '@seed-design/figma';
2
+ import { z } from 'zod';
3
+ import { Api } from 'figma-api';
4
+
5
+ function createFigmaRestClient(personalAccessToken) {
6
+ const api = new Api({
7
+ personalAccessToken
8
+ });
9
+ return {
10
+ async getFileNodes (fileKey, nodeIds) {
11
+ const response = await api.getFileNodes({
12
+ file_key: fileKey
13
+ }, {
14
+ ids: nodeIds.join(",")
15
+ });
16
+ return response;
17
+ }
18
+ };
19
+ }
20
+ /**
21
+ * https://www.figma.com/:file_type/:file_key/:file_name?node-id=:id
22
+ *
23
+ * file_type:
24
+ * - design
25
+ * - file (legacy)
26
+ *
27
+ * Note: While node-id is separated by hyphens ('-') in the URL,
28
+ * it must be converted to colons (':') when making API calls.
29
+ * e.g. URL "node-id=794-1987" → API "794:1987"
30
+ */ function parseFigmaUrl(url) {
31
+ const __url = (()=>{
32
+ try {
33
+ return new URL(url);
34
+ } catch {
35
+ throw new Error(`Invalid URL format: ${url}`);
36
+ }
37
+ })();
38
+ const pathMatch = __url.pathname.match(/^\/(design|file)\/([A-Za-z0-9]+)/);
39
+ const rawNodeId = __url.searchParams.get("node-id");
40
+ if (!pathMatch) throw new Error("Invalid Figma URL: Expected format https://www.figma.com/design/{fileKey}/... or /file/{fileKey}/...");
41
+ if (!rawNodeId) throw new Error("Invalid Figma URL: Missing node-id query parameter");
42
+ return {
43
+ fileKey: pathMatch[2],
44
+ nodeId: rawNodeId.replace(/-/g, ":")
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Format an error for logging
50
+ */ function formatError(error) {
51
+ if (error instanceof Error) {
52
+ return error.message;
53
+ }
54
+ return String(error);
55
+ }
56
+
57
+ /**
58
+ * Format an object response
59
+ */ function formatObjectResponse(result) {
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text: JSON.stringify(result)
65
+ }
66
+ ]
67
+ };
68
+ }
69
+ /**
70
+ * Format a text response
71
+ */ function formatTextResponse(text) {
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text
77
+ }
78
+ ]
79
+ };
80
+ }
81
+ /**
82
+ * Format an image response
83
+ */ function formatImageResponse(imageData, mimeType = "image/png") {
84
+ return {
85
+ content: [
86
+ {
87
+ type: "image",
88
+ data: imageData,
89
+ mimeType
90
+ }
91
+ ]
92
+ };
93
+ }
94
+ /**
95
+ * Format an error response
96
+ */ function formatErrorResponse(toolName, error) {
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: `Error in ${toolName}: ${formatError(error)}`
102
+ }
103
+ ]
104
+ };
105
+ }
106
+
107
+ function createToolContext(figmaClient, restClient, config, mode) {
108
+ return {
109
+ sendCommandToFigma: figmaClient?.sendCommandToFigma ?? null,
110
+ restClient,
111
+ mode,
112
+ extend: config?.extend
113
+ };
114
+ }
115
+ function resolveRestClient(personalAccessToken, context) {
116
+ if (context.mode === "websocket") {
117
+ return null;
118
+ }
119
+ if (personalAccessToken) {
120
+ return createFigmaRestClient(personalAccessToken);
121
+ }
122
+ return context.restClient;
123
+ }
124
+ async function fetchNodeData(params, context) {
125
+ const { fileKey, nodeId, personalAccessToken } = params;
126
+ const restClient = resolveRestClient(personalAccessToken, context);
127
+ const { sendCommandToFigma } = context;
128
+ if (restClient && fileKey) {
129
+ const response = await restClient.getFileNodes(fileKey, [
130
+ nodeId
131
+ ]);
132
+ const nodeData = response.nodes[nodeId];
133
+ if (!nodeData) throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
134
+ return nodeData;
135
+ }
136
+ if (sendCommandToFigma) {
137
+ return await sendCommandToFigma("get_node_info", {
138
+ nodeId
139
+ });
140
+ }
141
+ throw new Error("No connection available. Provide figmaUrl/fileKey with personalAccessToken or FIGMA_PERSONAL_ACCESS_TOKEN, or use WebSocket mode with Figma Plugin.");
142
+ }
143
+ async function fetchMultipleNodesData(params, context) {
144
+ const { fileKey, nodeIds, personalAccessToken } = params;
145
+ const restClient = resolveRestClient(personalAccessToken, context);
146
+ const { sendCommandToFigma } = context;
147
+ if (restClient && fileKey) {
148
+ const response = await restClient.getFileNodes(fileKey, nodeIds);
149
+ return response.nodes;
150
+ }
151
+ if (sendCommandToFigma) {
152
+ const results = {};
153
+ await Promise.all(nodeIds.map(async (nodeId)=>{
154
+ const data = await sendCommandToFigma("get_node_info", {
155
+ nodeId
156
+ });
157
+ results[nodeId] = data;
158
+ }));
159
+ return results;
160
+ }
161
+ throw new Error("No connection available. Provide figmaUrl/fileKey with personalAccessToken or FIGMA_PERSONAL_ACCESS_TOKEN, or use WebSocket mode with Figma Plugin.");
162
+ }
163
+ function requireWebSocket(context) {
164
+ if (!context.sendCommandToFigma) throw new Error("WebSocket not available. This tool requires Figma Plugin connection.");
165
+ }
166
+
167
+ const singleNodeBaseSchema = z.object({
168
+ figmaUrl: z.url().optional().describe("Figma node URL. Extracts fileKey and nodeId automatically when provided."),
169
+ fileKey: z.string().optional().describe("Figma file key. Use with nodeId when not using figmaUrl."),
170
+ nodeId: z.string().optional().describe("Node ID (e.g., '0:1')."),
171
+ personalAccessToken: z.string().optional().describe("Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided.")
172
+ });
173
+ const multiNodeBaseSchema = z.object({
174
+ fileKey: z.string().optional().describe("Figma file key. Required when WebSocket connection is not available."),
175
+ nodeIds: z.array(z.string()).describe("Array of node IDs (e.g., ['0:1', '0:2'])"),
176
+ personalAccessToken: z.string().optional().describe("Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided to be used when WebSocket connection is not available.")
177
+ });
178
+ function getSingleNodeParamsSchema(mode) {
179
+ switch(mode){
180
+ case "websocket":
181
+ return singleNodeBaseSchema.pick({
182
+ nodeId: true
183
+ }).required();
184
+ default:
185
+ return singleNodeBaseSchema;
186
+ }
187
+ }
188
+ function getMultiNodeParamsSchema(mode) {
189
+ switch(mode){
190
+ case "websocket":
191
+ return multiNodeBaseSchema.pick({
192
+ nodeIds: true
193
+ });
194
+ case "rest":
195
+ return multiNodeBaseSchema.required({
196
+ fileKey: true
197
+ });
198
+ default:
199
+ return multiNodeBaseSchema;
200
+ }
201
+ }
202
+ function resolveSingleNodeParams(params) {
203
+ if (params.figmaUrl) {
204
+ const parsed = parseFigmaUrl(params.figmaUrl);
205
+ return {
206
+ fileKey: parsed.fileKey,
207
+ nodeId: parsed.nodeId,
208
+ personalAccessToken: params.personalAccessToken
209
+ };
210
+ }
211
+ if (!params.nodeId) {
212
+ throw new Error("Either figmaUrl or nodeId must be provided. Use figmaUrl for automatic parsing, or provide fileKey + nodeId directly.");
213
+ }
214
+ return {
215
+ fileKey: params.fileKey,
216
+ nodeId: params.nodeId,
217
+ personalAccessToken: params.personalAccessToken
218
+ };
219
+ }
220
+ function getSingleNodeDescription(baseDescription, mode) {
221
+ switch(mode){
222
+ case "rest":
223
+ return `${baseDescription} Provide either: (1) figmaUrl (e.g., https://www.figma.com/design/ABC/Name?node-id=0-1), or (2) fileKey + nodeId.`;
224
+ case "websocket":
225
+ return `${baseDescription} Provide nodeId. Requires WebSocket connection with Figma Plugin.`;
226
+ case "all":
227
+ return `${baseDescription} Provide either: (1) figmaUrl (e.g., https://www.figma.com/design/ABC/Name?node-id=0-1), (2) fileKey + nodeId, or (3) nodeId only for WebSocket mode.`;
228
+ }
229
+ }
230
+ function getMultiNodeDescription(baseDescription, mode) {
231
+ switch(mode){
232
+ case "rest":
233
+ return `${baseDescription} Provide fileKey + nodeIds.`;
234
+ case "websocket":
235
+ return `${baseDescription} Provide nodeIds. Requires WebSocket connection with Figma Plugin.`;
236
+ case "all":
237
+ return `${baseDescription} Provide either: (1) fileKey + nodeIds for REST API, or (2) nodeIds only for WebSocket mode. If you have multiple URLs, call get_node_info for each URL instead.`;
238
+ }
239
+ }
240
+ function registerTools(server, figmaClient, restClient, config, mode) {
241
+ const context = createToolContext(figmaClient, restClient, config, mode);
242
+ const singleNodeParamsSchema = getSingleNodeParamsSchema(mode);
243
+ const multiNodeParamsSchema = getMultiNodeParamsSchema(mode);
244
+ const shouldRegisterWebSocketOnlyTools = mode === "websocket" || mode === "all";
245
+ // REST API + WebSocket Tools (hybrid)
246
+ // These tools support both REST API and WebSocket modes
247
+ // Component Info Tool (REST API + WebSocket)
248
+ server.registerTool("get_component_info", {
249
+ description: getSingleNodeDescription("Get detailed information about a specific component node in Figma.", mode),
250
+ inputSchema: singleNodeParamsSchema
251
+ }, async (params)=>{
252
+ try {
253
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
254
+ const result = await fetchNodeData({
255
+ fileKey,
256
+ nodeId,
257
+ personalAccessToken
258
+ }, context);
259
+ const node = result.document;
260
+ if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") {
261
+ return formatErrorResponse("get_component_info", new Error(`Node with ID ${nodeId} is not a component node`));
262
+ }
263
+ const key = result.componentSets[nodeId]?.key ?? result.components[nodeId]?.key;
264
+ if (!key) {
265
+ return formatErrorResponse("get_component_info", new Error(`${nodeId} is not present in exported component data`));
266
+ }
267
+ return formatObjectResponse({
268
+ name: node.name,
269
+ key,
270
+ componentPropertyDefinitions: node.componentPropertyDefinitions
271
+ });
272
+ } catch (error) {
273
+ return formatErrorResponse("get_component_info", error);
274
+ }
275
+ });
276
+ // Node Info Tool (REST API + WebSocket)
277
+ server.registerTool("get_node_info", {
278
+ description: getSingleNodeDescription("Get detailed information about a specific node in Figma.", mode),
279
+ inputSchema: singleNodeParamsSchema
280
+ }, async (params)=>{
281
+ try {
282
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
283
+ const result = await fetchNodeData({
284
+ fileKey,
285
+ nodeId,
286
+ personalAccessToken
287
+ }, context);
288
+ const normalizer = createRestNormalizer(result);
289
+ const node = normalizer(result.document);
290
+ const noInferPipeline = figma.createPipeline({
291
+ shouldInferAutoLayout: false,
292
+ shouldInferVariableName: false
293
+ });
294
+ const inferPipeline = figma.createPipeline({
295
+ shouldInferAutoLayout: true,
296
+ shouldInferVariableName: true
297
+ });
298
+ const original = noInferPipeline.generateCode(node, {
299
+ shouldPrintSource: true
300
+ })?.jsx ?? "Failed to generate summarized node info";
301
+ const inferred = inferPipeline.generateCode(node, {
302
+ shouldPrintSource: true
303
+ })?.jsx ?? "Failed to generate summarized node info";
304
+ return formatObjectResponse({
305
+ original: {
306
+ data: original,
307
+ description: "Original Figma node info"
308
+ },
309
+ inferred: {
310
+ data: inferred,
311
+ description: "AutoLayout Inferred Figma node info"
312
+ }
313
+ });
314
+ } catch (error) {
315
+ return formatTextResponse(`Error in get_node_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
316
+ }
317
+ });
318
+ // Nodes Info Tool (REST API + WebSocket)
319
+ server.registerTool("get_nodes_info", {
320
+ description: getMultiNodeDescription("Get detailed information about multiple nodes in Figma.", mode),
321
+ inputSchema: multiNodeParamsSchema
322
+ }, async ({ fileKey, nodeIds, personalAccessToken })=>{
323
+ try {
324
+ if (nodeIds.length === 0) {
325
+ return formatErrorResponse("get_nodes_info", new Error("No node IDs provided"));
326
+ }
327
+ const nodesData = await fetchMultipleNodesData({
328
+ fileKey,
329
+ nodeIds,
330
+ personalAccessToken
331
+ }, context);
332
+ const results = nodeIds.map((nodeId)=>{
333
+ const nodeData = nodesData[nodeId];
334
+ if (!nodeData) {
335
+ return {
336
+ nodeId,
337
+ error: `Node ${nodeId} not found`
338
+ };
339
+ }
340
+ const normalizer = createRestNormalizer(nodeData);
341
+ const node = normalizer(nodeData.document);
342
+ const noInferPipeline = figma.createPipeline({
343
+ shouldInferAutoLayout: false,
344
+ shouldInferVariableName: false
345
+ });
346
+ const inferPipeline = figma.createPipeline({
347
+ shouldInferAutoLayout: true,
348
+ shouldInferVariableName: true
349
+ });
350
+ const original = noInferPipeline.generateCode(node, {
351
+ shouldPrintSource: true
352
+ })?.jsx ?? "Failed to generate summarized node info";
353
+ const inferred = inferPipeline.generateCode(node, {
354
+ shouldPrintSource: true
355
+ })?.jsx ?? "Failed to generate summarized node info";
356
+ return {
357
+ nodeId,
358
+ original: {
359
+ data: original,
360
+ description: "Original Figma node info"
361
+ },
362
+ inferred: {
363
+ data: inferred,
364
+ description: "AutoLayout Inferred Figma node info"
365
+ }
366
+ };
367
+ });
368
+ return formatObjectResponse(results);
369
+ } catch (error) {
370
+ return formatTextResponse(`Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
371
+ }
372
+ });
373
+ // Get Node React Code Tool (REST API + WebSocket)
374
+ server.registerTool("get_node_react_code", {
375
+ description: getSingleNodeDescription("Get the React code for a specific node in Figma.", mode),
376
+ inputSchema: singleNodeParamsSchema
377
+ }, async (params)=>{
378
+ try {
379
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
380
+ const result = await fetchNodeData({
381
+ fileKey,
382
+ nodeId,
383
+ personalAccessToken
384
+ }, context);
385
+ const normalizer = createRestNormalizer(result);
386
+ const pipeline = react.createPipeline({
387
+ shouldInferAutoLayout: true,
388
+ shouldInferVariableName: true,
389
+ extend: context.extend
390
+ });
391
+ const generated = pipeline.generateCode(normalizer(result.document), {
392
+ shouldPrintSource: false
393
+ });
394
+ if (!generated) {
395
+ return formatTextResponse("Failed to generate code\n\n⚠️ Please make sure you have the latest version of the Figma library.");
396
+ }
397
+ return formatTextResponse(`${generated.imports}\n\n${generated.jsx}`);
398
+ } catch (error) {
399
+ return formatTextResponse(`Error in get_node_react_code: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
400
+ }
401
+ });
402
+ // Utility Tools (No Figma connection required)
403
+ // Retrieve Color Variable Names Tool
404
+ server.registerTool("retrieve_color_variable_names", {
405
+ description: "Retrieve available SEED Design color variable names by scope. No Figma connection required.",
406
+ inputSchema: z.object({
407
+ scope: z.enum([
408
+ "fg",
409
+ "bg",
410
+ "stroke",
411
+ "palette"
412
+ ]).array().describe("The scope of the color variable names to retrieve")
413
+ })
414
+ }, async ({ scope })=>{
415
+ try {
416
+ const result = getFigmaColorVariableNames(scope);
417
+ return formatObjectResponse(result);
418
+ } catch (error) {
419
+ return formatErrorResponse("retrieve_color_variable_names", error);
420
+ }
421
+ });
422
+ if (shouldRegisterWebSocketOnlyTools) {
423
+ // WebSocket Only Tools
424
+ server.registerTool("join_channel", {
425
+ description: "Join a specific channel to communicate with Figma (WebSocket mode only)",
426
+ inputSchema: z.object({
427
+ channel: z.string().describe("The name of the channel to join").default("")
428
+ })
429
+ }, async ({ channel })=>{
430
+ try {
431
+ if (!figmaClient) return formatErrorResponse("join_channel", new Error("WebSocket not available. This tool requires Figma Plugin connection."));
432
+ if (!channel) // If no channel provided, ask the user for input
433
+ return {
434
+ ...formatTextResponse("Please provide a channel name to join:"),
435
+ followUp: {
436
+ tool: "join_channel",
437
+ description: "Join the specified channel"
438
+ }
439
+ };
440
+ await figmaClient.joinChannel(channel);
441
+ return formatTextResponse(`Successfully joined channel: ${channel}`);
442
+ } catch (error) {
443
+ return formatErrorResponse("join_channel", error);
444
+ }
445
+ });
446
+ // Document Info Tool
447
+ server.registerTool("get_document_info", {
448
+ description: "Get detailed information about the current Figma document (WebSocket mode only)"
449
+ }, async ()=>{
450
+ try {
451
+ requireWebSocket(context);
452
+ const result = await context.sendCommandToFigma("get_document_info");
453
+ return formatObjectResponse(result);
454
+ } catch (error) {
455
+ return formatErrorResponse("get_document_info", error);
456
+ }
457
+ });
458
+ // Selection Tool
459
+ server.registerTool("get_selection", {
460
+ description: "Get information about the current selection in Figma (WebSocket mode only)"
461
+ }, async ()=>{
462
+ try {
463
+ requireWebSocket(context);
464
+ const result = await context.sendCommandToFigma("get_selection");
465
+ return formatObjectResponse(result);
466
+ } catch (error) {
467
+ return formatErrorResponse("get_selection", error);
468
+ }
469
+ });
470
+ // Annotation Tool
471
+ server.registerTool("add_annotations", {
472
+ description: "Add annotations to multiple nodes in Figma (WebSocket mode only)",
473
+ inputSchema: z.object({
474
+ annotations: z.array(z.object({
475
+ nodeId: z.string().describe("The ID of the node to add an annotation to"),
476
+ labelMarkdown: z.string().describe("The markdown label for the annotation, do not escape newlines")
477
+ }))
478
+ })
479
+ }, async ({ annotations })=>{
480
+ try {
481
+ requireWebSocket(context);
482
+ await context.sendCommandToFigma("add_annotations", {
483
+ annotations
484
+ });
485
+ return formatTextResponse(`Annotations added to nodes ${annotations.map((annotation)=>annotation.nodeId).join(", ")}`);
486
+ } catch (error) {
487
+ return formatErrorResponse("add_annotations", error);
488
+ }
489
+ });
490
+ // Get Annotations Tool
491
+ server.registerTool("get_annotations", {
492
+ description: "Get annotations for a specific node in Figma (WebSocket mode only)",
493
+ inputSchema: z.object({
494
+ nodeId: z.string().describe("The ID of the node to get annotations for")
495
+ })
496
+ }, async ({ nodeId })=>{
497
+ try {
498
+ requireWebSocket(context);
499
+ const result = await context.sendCommandToFigma("get_annotations", {
500
+ nodeId
501
+ });
502
+ return formatObjectResponse(result);
503
+ } catch (error) {
504
+ return formatErrorResponse("get_annotations", error);
505
+ }
506
+ });
507
+ // Export Node as Image Tool
508
+ server.registerTool("export_node_as_image", {
509
+ description: "Export a node as an image from Figma (WebSocket mode only)",
510
+ inputSchema: z.object({
511
+ nodeId: z.string().describe("The ID of the node to export"),
512
+ format: z.enum([
513
+ "PNG",
514
+ "JPG",
515
+ "SVG",
516
+ "PDF"
517
+ ]).optional().describe("Export format"),
518
+ scale: z.number().positive().optional().describe("Export scale")
519
+ })
520
+ }, async ({ nodeId, format, scale })=>{
521
+ try {
522
+ requireWebSocket(context);
523
+ const result = await context.sendCommandToFigma("export_node_as_image", {
524
+ nodeId,
525
+ format: format || "PNG",
526
+ scale: scale || 1
527
+ });
528
+ const typedResult = result;
529
+ return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
530
+ } catch (error) {
531
+ return formatErrorResponse("export_node_as_image", error);
532
+ }
533
+ });
534
+ }
535
+ }
536
+
537
+ export { createFigmaRestClient, parseFigmaUrl, registerTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/mcp",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -12,26 +12,34 @@
12
12
  "sideEffects": false,
13
13
  "files": [
14
14
  "bin",
15
+ "dist",
15
16
  "src",
16
17
  "package.json"
17
18
  ],
18
19
  "bin": "./bin/index.mjs",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
19
26
  "scripts": {
20
- "clean": "rm -rf lib",
27
+ "clean": "rm -rf dist bin",
21
28
  "build": "bunchee",
22
29
  "lint:publish": "bun publint"
23
30
  },
24
31
  "dependencies": {
25
- "@modelcontextprotocol/sdk": "^1.10.2",
32
+ "@modelcontextprotocol/sdk": "^1.25.3",
26
33
  "@seed-design/figma": "1.2.1",
27
34
  "cac": "^6.7.14",
35
+ "figma-api": "^2.1.0-beta",
28
36
  "uuid": "^13.0.0",
29
37
  "ws": "^8.18.1",
30
38
  "yargs": "^18.0.0",
31
- "zod": "^3.24.3"
39
+ "zod": "4.3.5"
32
40
  },
33
41
  "devDependencies": {
34
- "@types/bun": "^1.2.10",
42
+ "@figma/rest-api-spec": "^0.36.0",
35
43
  "@types/ws": "^8.18.1",
36
44
  "@types/yargs": "^17.0.33",
37
45
  "typescript": "^5.9.2"