@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/bin/index.mjs CHANGED
@@ -2,12 +2,15 @@
2
2
  import { cac } from 'cac';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { Api } from 'figma-api';
5
8
  import { v4 } from 'uuid';
6
9
  import WebSocket$1 from 'ws';
7
10
  import { createRestNormalizer, figma, react, getFigmaColorVariableNames } from '@seed-design/figma';
8
11
  import { z } from 'zod';
9
- import fs from 'node:fs';
10
- import path from 'node:path';
12
+
13
+ var version = "1.3.1";
11
14
 
12
15
  /**
13
16
  * Custom logging module that writes to stderr instead of stdout
@@ -38,6 +41,76 @@ import path from 'node:path';
38
41
  return String(error);
39
42
  }
40
43
 
44
+ // Config loader
45
+ async function loadConfig(configPath) {
46
+ try {
47
+ const resolvedPath = path.resolve(process.cwd(), configPath);
48
+ if (!fs.existsSync(resolvedPath)) {
49
+ logger.error(`Config file not found: ${resolvedPath}`);
50
+ return null;
51
+ }
52
+ // Handle different file types
53
+ if (resolvedPath.endsWith(".json")) {
54
+ const content = fs.readFileSync(resolvedPath, "utf-8");
55
+ return JSON.parse(content);
56
+ }
57
+ if (resolvedPath.endsWith(".js") || resolvedPath.endsWith(".mjs") || resolvedPath.endsWith(".ts") || resolvedPath.endsWith(".mts")) {
58
+ // For JS/MJS/TS/MTS files, we can dynamically import with Bun
59
+ // Bun has built-in TypeScript support without requiring transpilation
60
+ const config = await import(resolvedPath);
61
+ return config.default || config;
62
+ }
63
+ logger.error(`Unsupported config file format: ${resolvedPath}`);
64
+ return null;
65
+ } catch (error) {
66
+ logger.error(`Failed to load config file: ${error instanceof Error ? error.message : String(error)}`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function createFigmaRestClient(personalAccessToken) {
72
+ const api = new Api({
73
+ personalAccessToken
74
+ });
75
+ return {
76
+ async getFileNodes (fileKey, nodeIds) {
77
+ const response = await api.getFileNodes({
78
+ file_key: fileKey
79
+ }, {
80
+ ids: nodeIds.join(",")
81
+ });
82
+ return response;
83
+ }
84
+ };
85
+ }
86
+ /**
87
+ * https://www.figma.com/:file_type/:file_key/:file_name?node-id=:id
88
+ *
89
+ * file_type:
90
+ * - design
91
+ * - file (legacy)
92
+ *
93
+ * Note: While node-id is separated by hyphens ('-') in the URL,
94
+ * it must be converted to colons (':') when making API calls.
95
+ * e.g. URL "node-id=794-1987" → API "794:1987"
96
+ */ function parseFigmaUrl(url) {
97
+ const __url = (()=>{
98
+ try {
99
+ return new URL(url);
100
+ } catch {
101
+ throw new Error(`Invalid URL format: ${url}`);
102
+ }
103
+ })();
104
+ const pathMatch = __url.pathname.match(/^\/(design|file)\/([A-Za-z0-9]+)/);
105
+ const rawNodeId = __url.searchParams.get("node-id");
106
+ if (!pathMatch) throw new Error("Invalid Figma URL: Expected format https://www.figma.com/design/{fileKey}/... or /file/{fileKey}/...");
107
+ if (!rawNodeId) throw new Error("Invalid Figma URL: Missing node-id query parameter");
108
+ return {
109
+ fileKey: pathMatch[2],
110
+ nodeId: rawNodeId.replace(/-/g, ":")
111
+ };
112
+ }
113
+
41
114
  function createFigmaWebSocketClient(serverUrl) {
42
115
  const WS_URL = serverUrl === "localhost" ? `ws://${serverUrl}` : `wss://${serverUrl}`;
43
116
  // Track which channel each client is in
@@ -263,85 +336,158 @@ function createFigmaWebSocketClient(serverUrl) {
263
336
  };
264
337
  }
265
338
 
266
- function registerTools(server, figmaClient, config = {}) {
267
- const { joinChannel, sendCommandToFigma } = figmaClient;
268
- const { extend } = config ?? {};
269
- // join_channel tool
270
- server.tool("join_channel", "Join a specific channel to communicate with Figma", {
271
- channel: z.string().describe("The name of the channel to join").default("")
272
- }, async ({ channel })=>{
273
- try {
274
- if (!channel) {
275
- // If no channel provided, ask the user for input
276
- return {
277
- ...formatTextResponse("Please provide a channel name to join:"),
278
- followUp: {
279
- tool: "join_channel",
280
- description: "Join the specified channel"
281
- }
282
- };
283
- }
284
- await joinChannel(channel);
285
- return formatTextResponse(`Successfully joined channel: ${channel}`);
286
- } catch (error) {
287
- return formatErrorResponse("join_channel", error);
288
- }
289
- });
290
- // Document Info Tool
291
- server.tool("get_document_info", "Get detailed information about the current Figma document", {}, async ()=>{
292
- try {
293
- const result = await sendCommandToFigma("get_document_info");
294
- return formatObjectResponse(result);
295
- } catch (error) {
296
- return formatErrorResponse("get_document_info", error);
297
- }
298
- });
299
- // Selection Tool
300
- server.tool("get_selection", "Get information about the current selection in Figma", {}, async ()=>{
301
- try {
302
- const result = await sendCommandToFigma("get_selection");
303
- return formatObjectResponse(result);
304
- } catch (error) {
305
- return formatErrorResponse("get_selection", error);
306
- }
307
- });
308
- // Annotation Tool
309
- server.tool("add_annotations", "Add annotations to multiple nodes in Figma", {
310
- annotations: z.array(z.object({
311
- nodeId: z.string().describe("The ID of the node to add an annotation to"),
312
- labelMarkdown: z.string().describe("The markdown label for the annotation, do not escape newlines")
313
- }))
314
- }, async ({ annotations })=>{
315
- try {
316
- await sendCommandToFigma("add_annotations", {
317
- annotations
318
- });
319
- return formatTextResponse(`Annotations added to nodes ${annotations.map((annotation)=>annotation.nodeId).join(", ")}`);
320
- } catch (error) {
321
- return formatErrorResponse("add_annotations", error);
322
- }
323
- });
324
- // Get Annotations Tool
325
- server.tool("get_annotations", "Get annotations for a specific node in Figma", {
326
- nodeId: z.string().describe("The ID of the node to get annotations for")
327
- }, async ({ nodeId })=>{
328
- try {
329
- const result = await sendCommandToFigma("get_annotations", {
339
+ function createToolContext(figmaClient, restClient, config, mode) {
340
+ return {
341
+ sendCommandToFigma: figmaClient?.sendCommandToFigma ?? null,
342
+ restClient,
343
+ mode,
344
+ extend: config?.extend
345
+ };
346
+ }
347
+ function resolveRestClient(personalAccessToken, context) {
348
+ if (context.mode === "websocket") {
349
+ return null;
350
+ }
351
+ if (personalAccessToken) {
352
+ return createFigmaRestClient(personalAccessToken);
353
+ }
354
+ return context.restClient;
355
+ }
356
+ async function fetchNodeData(params, context) {
357
+ const { fileKey, nodeId, personalAccessToken } = params;
358
+ const restClient = resolveRestClient(personalAccessToken, context);
359
+ const { sendCommandToFigma } = context;
360
+ if (restClient && fileKey) {
361
+ const response = await restClient.getFileNodes(fileKey, [
362
+ nodeId
363
+ ]);
364
+ const nodeData = response.nodes[nodeId];
365
+ if (!nodeData) throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
366
+ return nodeData;
367
+ }
368
+ if (sendCommandToFigma) {
369
+ return await sendCommandToFigma("get_node_info", {
370
+ nodeId
371
+ });
372
+ }
373
+ throw new Error("No connection available. Provide figmaUrl/fileKey with personalAccessToken or FIGMA_PERSONAL_ACCESS_TOKEN, or use WebSocket mode with Figma Plugin.");
374
+ }
375
+ async function fetchMultipleNodesData(params, context) {
376
+ const { fileKey, nodeIds, personalAccessToken } = params;
377
+ const restClient = resolveRestClient(personalAccessToken, context);
378
+ const { sendCommandToFigma } = context;
379
+ if (restClient && fileKey) {
380
+ const response = await restClient.getFileNodes(fileKey, nodeIds);
381
+ return response.nodes;
382
+ }
383
+ if (sendCommandToFigma) {
384
+ const results = {};
385
+ await Promise.all(nodeIds.map(async (nodeId)=>{
386
+ const data = await sendCommandToFigma("get_node_info", {
330
387
  nodeId
331
388
  });
332
- return formatObjectResponse(result);
333
- } catch (error) {
334
- return formatErrorResponse("get_annotations", error);
335
- }
336
- });
337
- // Component Info Tool
338
- server.tool("get_component_info", "Get detailed information about a specific component node in Figma", {
339
- nodeId: z.string().describe("The ID of the component node to get information about")
340
- }, async ({ nodeId })=>{
341
- try {
342
- const result = await sendCommandToFigma("get_node_info", {
343
- nodeId
389
+ results[nodeId] = data;
390
+ }));
391
+ return results;
392
+ }
393
+ throw new Error("No connection available. Provide figmaUrl/fileKey with personalAccessToken or FIGMA_PERSONAL_ACCESS_TOKEN, or use WebSocket mode with Figma Plugin.");
394
+ }
395
+ function requireWebSocket(context) {
396
+ if (!context.sendCommandToFigma) throw new Error("WebSocket not available. This tool requires Figma Plugin connection.");
397
+ }
398
+
399
+ const singleNodeBaseSchema = z.object({
400
+ figmaUrl: z.url().optional().describe("Figma node URL. Extracts fileKey and nodeId automatically when provided."),
401
+ fileKey: z.string().optional().describe("Figma file key. Use with nodeId when not using figmaUrl."),
402
+ nodeId: z.string().optional().describe("Node ID (e.g., '0:1')."),
403
+ personalAccessToken: z.string().optional().describe("Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided.")
404
+ });
405
+ const multiNodeBaseSchema = z.object({
406
+ fileKey: z.string().optional().describe("Figma file key. Required when WebSocket connection is not available."),
407
+ nodeIds: z.array(z.string()).describe("Array of node IDs (e.g., ['0:1', '0:2'])"),
408
+ 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.")
409
+ });
410
+ function getSingleNodeParamsSchema(mode) {
411
+ switch(mode){
412
+ case "websocket":
413
+ return singleNodeBaseSchema.pick({
414
+ nodeId: true
415
+ }).required();
416
+ default:
417
+ return singleNodeBaseSchema;
418
+ }
419
+ }
420
+ function getMultiNodeParamsSchema(mode) {
421
+ switch(mode){
422
+ case "websocket":
423
+ return multiNodeBaseSchema.pick({
424
+ nodeIds: true
425
+ });
426
+ case "rest":
427
+ return multiNodeBaseSchema.required({
428
+ fileKey: true
344
429
  });
430
+ default:
431
+ return multiNodeBaseSchema;
432
+ }
433
+ }
434
+ function resolveSingleNodeParams(params) {
435
+ if (params.figmaUrl) {
436
+ const parsed = parseFigmaUrl(params.figmaUrl);
437
+ return {
438
+ fileKey: parsed.fileKey,
439
+ nodeId: parsed.nodeId,
440
+ personalAccessToken: params.personalAccessToken
441
+ };
442
+ }
443
+ if (!params.nodeId) {
444
+ throw new Error("Either figmaUrl or nodeId must be provided. Use figmaUrl for automatic parsing, or provide fileKey + nodeId directly.");
445
+ }
446
+ return {
447
+ fileKey: params.fileKey,
448
+ nodeId: params.nodeId,
449
+ personalAccessToken: params.personalAccessToken
450
+ };
451
+ }
452
+ function getSingleNodeDescription(baseDescription, mode) {
453
+ switch(mode){
454
+ case "rest":
455
+ return `${baseDescription} Provide either: (1) figmaUrl (e.g., https://www.figma.com/design/ABC/Name?node-id=0-1), or (2) fileKey + nodeId.`;
456
+ case "websocket":
457
+ return `${baseDescription} Provide nodeId. Requires WebSocket connection with Figma Plugin.`;
458
+ case "all":
459
+ 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.`;
460
+ }
461
+ }
462
+ function getMultiNodeDescription(baseDescription, mode) {
463
+ switch(mode){
464
+ case "rest":
465
+ return `${baseDescription} Provide fileKey + nodeIds.`;
466
+ case "websocket":
467
+ return `${baseDescription} Provide nodeIds. Requires WebSocket connection with Figma Plugin.`;
468
+ case "all":
469
+ 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.`;
470
+ }
471
+ }
472
+ function registerTools(server, figmaClient, restClient, config, mode) {
473
+ const context = createToolContext(figmaClient, restClient, config, mode);
474
+ const singleNodeParamsSchema = getSingleNodeParamsSchema(mode);
475
+ const multiNodeParamsSchema = getMultiNodeParamsSchema(mode);
476
+ const shouldRegisterWebSocketOnlyTools = mode === "websocket" || mode === "all";
477
+ // REST API + WebSocket Tools (hybrid)
478
+ // These tools support both REST API and WebSocket modes
479
+ // Component Info Tool (REST API + WebSocket)
480
+ server.registerTool("get_component_info", {
481
+ description: getSingleNodeDescription("Get detailed information about a specific component node in Figma.", mode),
482
+ inputSchema: singleNodeParamsSchema
483
+ }, async (params)=>{
484
+ try {
485
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
486
+ const result = await fetchNodeData({
487
+ fileKey,
488
+ nodeId,
489
+ personalAccessToken
490
+ }, context);
345
491
  const node = result.document;
346
492
  if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") {
347
493
  return formatErrorResponse("get_component_info", new Error(`Node with ID ${nodeId} is not a component node`));
@@ -356,17 +502,21 @@ function registerTools(server, figmaClient, config = {}) {
356
502
  componentPropertyDefinitions: node.componentPropertyDefinitions
357
503
  });
358
504
  } catch (error) {
359
- return formatErrorResponse("get_node_info", error);
505
+ return formatErrorResponse("get_component_info", error);
360
506
  }
361
507
  });
362
- // Node Info Tool
363
- server.tool("get_node_info", "Get detailed information about a specific node in Figma", {
364
- nodeId: z.string().describe("The ID of the node to get information about")
365
- }, async ({ nodeId })=>{
508
+ // Node Info Tool (REST API + WebSocket)
509
+ server.registerTool("get_node_info", {
510
+ description: getSingleNodeDescription("Get detailed information about a specific node in Figma.", mode),
511
+ inputSchema: singleNodeParamsSchema
512
+ }, async (params)=>{
366
513
  try {
367
- const result = await sendCommandToFigma("get_node_info", {
368
- nodeId
369
- });
514
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
515
+ const result = await fetchNodeData({
516
+ fileKey,
517
+ nodeId,
518
+ personalAccessToken
519
+ }, context);
370
520
  const normalizer = createRestNormalizer(result);
371
521
  const node = normalizer(result.document);
372
522
  const noInferPipeline = figma.createPipeline({
@@ -390,24 +540,37 @@ function registerTools(server, figmaClient, config = {}) {
390
540
  },
391
541
  inferred: {
392
542
  data: inferred,
393
- description: "AutoLayout Inferred Figma node info (fix suggestions)"
543
+ description: "AutoLayout Inferred Figma node info"
394
544
  }
395
545
  });
396
546
  } catch (error) {
397
- return formatTextResponse(`Error in get_node_info: ${formatError(error)}\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.`);
547
+ return formatTextResponse(`Error in get_node_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
398
548
  }
399
549
  });
400
- // Nodes Info Tool
401
- server.tool("get_nodes_info", "Get detailed information about multiple nodes in Figma", {
402
- nodeIds: z.array(z.string()).describe("Array of node IDs to get information about")
403
- }, async ({ nodeIds })=>{
550
+ // Nodes Info Tool (REST API + WebSocket)
551
+ server.registerTool("get_nodes_info", {
552
+ description: getMultiNodeDescription("Get detailed information about multiple nodes in Figma.", mode),
553
+ inputSchema: multiNodeParamsSchema
554
+ }, async ({ fileKey, nodeIds, personalAccessToken })=>{
404
555
  try {
405
- const results = await Promise.all(nodeIds.map(async (nodeId)=>{
406
- const result = await sendCommandToFigma("get_node_info", {
407
- nodeId
408
- });
409
- const normalizer = createRestNormalizer(result);
410
- const node = normalizer(result.document);
556
+ if (nodeIds.length === 0) {
557
+ return formatErrorResponse("get_nodes_info", new Error("No node IDs provided"));
558
+ }
559
+ const nodesData = await fetchMultipleNodesData({
560
+ fileKey,
561
+ nodeIds,
562
+ personalAccessToken
563
+ }, context);
564
+ const results = nodeIds.map((nodeId)=>{
565
+ const nodeData = nodesData[nodeId];
566
+ if (!nodeData) {
567
+ return {
568
+ nodeId,
569
+ error: `Node ${nodeId} not found`
570
+ };
571
+ }
572
+ const normalizer = createRestNormalizer(nodeData);
573
+ const node = normalizer(nodeData.document);
411
574
  const noInferPipeline = figma.createPipeline({
412
575
  shouldInferAutoLayout: false,
413
576
  shouldInferVariableName: false
@@ -430,70 +593,56 @@ function registerTools(server, figmaClient, config = {}) {
430
593
  },
431
594
  inferred: {
432
595
  data: inferred,
433
- description: "AutoLayout Inferred Figma node info (fix suggestions)"
596
+ description: "AutoLayout Inferred Figma node info"
434
597
  }
435
598
  };
436
- }));
599
+ });
437
600
  return formatObjectResponse(results);
438
601
  } catch (error) {
439
- return formatTextResponse(`Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.`);
602
+ return formatTextResponse(`Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
440
603
  }
441
604
  });
442
- server.tool("get_node_react_code", "Get the React code for a specific node in Figma", {
443
- nodeId: z.string().describe("The ID of the node to get code for")
444
- }, async ({ nodeId })=>{
605
+ // Get Node React Code Tool (REST API + WebSocket)
606
+ server.registerTool("get_node_react_code", {
607
+ description: getSingleNodeDescription("Get the React code for a specific node in Figma.", mode),
608
+ inputSchema: singleNodeParamsSchema
609
+ }, async (params)=>{
445
610
  try {
446
- const result = await sendCommandToFigma("get_node_info", {
447
- nodeId
448
- });
611
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
612
+ const result = await fetchNodeData({
613
+ fileKey,
614
+ nodeId,
615
+ personalAccessToken
616
+ }, context);
449
617
  const normalizer = createRestNormalizer(result);
450
618
  const pipeline = react.createPipeline({
451
619
  shouldInferAutoLayout: true,
452
620
  shouldInferVariableName: true,
453
- extend
621
+ extend: context.extend
454
622
  });
455
623
  const generated = pipeline.generateCode(normalizer(result.document), {
456
624
  shouldPrintSource: false
457
625
  });
458
626
  if (!generated) {
459
- return formatTextResponse("Failed to generate code\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.");
627
+ return formatTextResponse("Failed to generate code\n\n⚠️ Please make sure you have the latest version of the Figma library.");
460
628
  }
461
629
  return formatTextResponse(`${generated.imports}\n\n${generated.jsx}`);
462
630
  } catch (error) {
463
- return formatTextResponse(`Error in get_node_react_code: ${formatError(error)}\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.`);
464
- }
465
- });
466
- // Export Node as Image Tool
467
- server.tool("export_node_as_image", "Export a node as an image from Figma", {
468
- nodeId: z.string().describe("The ID of the node to export"),
469
- format: z.enum([
470
- "PNG",
471
- "JPG",
472
- "SVG",
473
- "PDF"
474
- ]).optional().describe("Export format"),
475
- scale: z.number().positive().optional().describe("Export scale")
476
- }, async ({ nodeId, format, scale })=>{
477
- try {
478
- const result = await sendCommandToFigma("export_node_as_image", {
479
- nodeId,
480
- format: format || "PNG",
481
- scale: scale || 1
482
- });
483
- const typedResult = result;
484
- return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
485
- } catch (error) {
486
- return formatErrorResponse("export_node_as_image", error);
631
+ return formatTextResponse(`Error in get_node_react_code: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
487
632
  }
488
633
  });
634
+ // Utility Tools (No Figma connection required)
489
635
  // Retrieve Color Variable Names Tool
490
- server.tool("retrieve_color_variable_names", "Retrieve available color variable names in scope", {
491
- scope: z.enum([
492
- "fg",
493
- "bg",
494
- "stroke",
495
- "palette"
496
- ]).array().describe("The scope of the color variable names to retrieve")
636
+ server.registerTool("retrieve_color_variable_names", {
637
+ description: "Retrieve available SEED Design color variable names by scope. No Figma connection required.",
638
+ inputSchema: z.object({
639
+ scope: z.enum([
640
+ "fg",
641
+ "bg",
642
+ "stroke",
643
+ "palette"
644
+ ]).array().describe("The scope of the color variable names to retrieve")
645
+ })
497
646
  }, async ({ scope })=>{
498
647
  try {
499
648
  const result = getFigmaColorVariableNames(scope);
@@ -502,14 +651,131 @@ function registerTools(server, figmaClient, config = {}) {
502
651
  return formatErrorResponse("retrieve_color_variable_names", error);
503
652
  }
504
653
  });
654
+ if (shouldRegisterWebSocketOnlyTools) {
655
+ // WebSocket Only Tools
656
+ server.registerTool("join_channel", {
657
+ description: "Join a specific channel to communicate with Figma (WebSocket mode only)",
658
+ inputSchema: z.object({
659
+ channel: z.string().describe("The name of the channel to join").default("")
660
+ })
661
+ }, async ({ channel })=>{
662
+ try {
663
+ if (!figmaClient) return formatErrorResponse("join_channel", new Error("WebSocket not available. This tool requires Figma Plugin connection."));
664
+ if (!channel) // If no channel provided, ask the user for input
665
+ return {
666
+ ...formatTextResponse("Please provide a channel name to join:"),
667
+ followUp: {
668
+ tool: "join_channel",
669
+ description: "Join the specified channel"
670
+ }
671
+ };
672
+ await figmaClient.joinChannel(channel);
673
+ return formatTextResponse(`Successfully joined channel: ${channel}`);
674
+ } catch (error) {
675
+ return formatErrorResponse("join_channel", error);
676
+ }
677
+ });
678
+ // Document Info Tool
679
+ server.registerTool("get_document_info", {
680
+ description: "Get detailed information about the current Figma document (WebSocket mode only)"
681
+ }, async ()=>{
682
+ try {
683
+ requireWebSocket(context);
684
+ const result = await context.sendCommandToFigma("get_document_info");
685
+ return formatObjectResponse(result);
686
+ } catch (error) {
687
+ return formatErrorResponse("get_document_info", error);
688
+ }
689
+ });
690
+ // Selection Tool
691
+ server.registerTool("get_selection", {
692
+ description: "Get information about the current selection in Figma (WebSocket mode only)"
693
+ }, async ()=>{
694
+ try {
695
+ requireWebSocket(context);
696
+ const result = await context.sendCommandToFigma("get_selection");
697
+ return formatObjectResponse(result);
698
+ } catch (error) {
699
+ return formatErrorResponse("get_selection", error);
700
+ }
701
+ });
702
+ // Annotation Tool
703
+ server.registerTool("add_annotations", {
704
+ description: "Add annotations to multiple nodes in Figma (WebSocket mode only)",
705
+ inputSchema: z.object({
706
+ annotations: z.array(z.object({
707
+ nodeId: z.string().describe("The ID of the node to add an annotation to"),
708
+ labelMarkdown: z.string().describe("The markdown label for the annotation, do not escape newlines")
709
+ }))
710
+ })
711
+ }, async ({ annotations })=>{
712
+ try {
713
+ requireWebSocket(context);
714
+ await context.sendCommandToFigma("add_annotations", {
715
+ annotations
716
+ });
717
+ return formatTextResponse(`Annotations added to nodes ${annotations.map((annotation)=>annotation.nodeId).join(", ")}`);
718
+ } catch (error) {
719
+ return formatErrorResponse("add_annotations", error);
720
+ }
721
+ });
722
+ // Get Annotations Tool
723
+ server.registerTool("get_annotations", {
724
+ description: "Get annotations for a specific node in Figma (WebSocket mode only)",
725
+ inputSchema: z.object({
726
+ nodeId: z.string().describe("The ID of the node to get annotations for")
727
+ })
728
+ }, async ({ nodeId })=>{
729
+ try {
730
+ requireWebSocket(context);
731
+ const result = await context.sendCommandToFigma("get_annotations", {
732
+ nodeId
733
+ });
734
+ return formatObjectResponse(result);
735
+ } catch (error) {
736
+ return formatErrorResponse("get_annotations", error);
737
+ }
738
+ });
739
+ // Export Node as Image Tool
740
+ server.registerTool("export_node_as_image", {
741
+ description: "Export a node as an image from Figma (WebSocket mode only)",
742
+ inputSchema: z.object({
743
+ nodeId: z.string().describe("The ID of the node to export"),
744
+ format: z.enum([
745
+ "PNG",
746
+ "JPG",
747
+ "SVG",
748
+ "PDF"
749
+ ]).optional().describe("Export format"),
750
+ scale: z.number().positive().optional().describe("Export scale")
751
+ })
752
+ }, async ({ nodeId, format, scale })=>{
753
+ try {
754
+ requireWebSocket(context);
755
+ const result = await context.sendCommandToFigma("export_node_as_image", {
756
+ nodeId,
757
+ format: format || "PNG",
758
+ scale: scale || 1
759
+ });
760
+ const typedResult = result;
761
+ return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
762
+ } catch (error) {
763
+ return formatErrorResponse("export_node_as_image", error);
764
+ }
765
+ });
766
+ }
505
767
  }
768
+ // editing tools require WebSocket client
506
769
  function registerEditingTools(server, figmaClient) {
507
770
  const { sendCommandToFigma } = figmaClient;
508
771
  // Clone Node Tool
509
- server.tool("clone_node", "Clone an existing node in Figma", {
510
- nodeId: z.string().describe("The ID of the node to clone"),
511
- x: z.number().optional().describe("New X position for the clone"),
512
- y: z.number().optional().describe("New Y position for the clone")
772
+ server.registerTool("clone_node", {
773
+ description: "Clone an existing node in Figma (WebSocket mode only)",
774
+ inputSchema: z.object({
775
+ nodeId: z.string().describe("The ID of the node to clone"),
776
+ x: z.number().optional().describe("New X position for the clone"),
777
+ y: z.number().optional().describe("New Y position for the clone")
778
+ })
513
779
  }, async ({ nodeId, x, y })=>{
514
780
  try {
515
781
  const result = await sendCommandToFigma("clone_node", {
@@ -523,9 +789,12 @@ function registerEditingTools(server, figmaClient) {
523
789
  return formatErrorResponse("clone_node", error);
524
790
  }
525
791
  });
526
- server.tool("set_fill_color", "Set the fill color of a node", {
527
- nodeId: z.string().describe("The ID of the node to set the fill color of"),
528
- colorToken: z.string().describe("The color token to set the fill color to. Format: `{category}/{name}`. Example: `bg/brand`")
792
+ server.registerTool("set_fill_color", {
793
+ description: "Set the fill color of a node (WebSocket mode only)",
794
+ inputSchema: z.object({
795
+ nodeId: z.string().describe("The ID of the node to set the fill color of"),
796
+ colorToken: z.string().describe("The color token to set the fill color to. Format: `{category}/{name}`. Example: `bg/brand`")
797
+ })
529
798
  }, async ({ nodeId, colorToken })=>{
530
799
  try {
531
800
  await sendCommandToFigma("set_fill_color", {
@@ -537,9 +806,12 @@ function registerEditingTools(server, figmaClient) {
537
806
  return formatErrorResponse("set_fill_color", error);
538
807
  }
539
808
  });
540
- server.tool("set_stroke_color", "Set the stroke color of a node", {
541
- nodeId: z.string().describe("The ID of the node to set the stroke color of"),
542
- colorToken: z.string().describe("The color token to set the stroke color to. Format: `{category}/{name}`. Example: `stroke/neutral`")
809
+ server.registerTool("set_stroke_color", {
810
+ description: "Set the stroke color of a node (WebSocket mode only)",
811
+ inputSchema: z.object({
812
+ nodeId: z.string().describe("The ID of the node to set the stroke color of"),
813
+ colorToken: z.string().describe("The color token to set the stroke color to. Format: `{category}/{name}`. Example: `stroke/neutral`")
814
+ })
543
815
  }, async ({ nodeId, colorToken })=>{
544
816
  try {
545
817
  await sendCommandToFigma("set_stroke_color", {
@@ -551,35 +823,38 @@ function registerEditingTools(server, figmaClient) {
551
823
  return formatErrorResponse("set_stroke_color", error);
552
824
  }
553
825
  });
554
- server.tool("set_auto_layout", "Set the auto layout of a node", {
555
- nodeId: z.string().describe("The ID of the node to set the auto layout of"),
556
- layoutMode: z.enum([
557
- "HORIZONTAL",
558
- "VERTICAL"
559
- ]).optional().describe("The layout mode to set"),
560
- layoutWrap: z.enum([
561
- "NO_WRAP",
562
- "WRAP"
563
- ]).optional().describe("The layout wrap to set"),
564
- primaryAxisAlignItems: z.enum([
565
- "MIN",
566
- "MAX",
567
- "CENTER",
568
- "SPACE_BETWEEN"
569
- ]).optional().describe("The primary axis align items to set"),
570
- counterAxisAlignItems: z.enum([
571
- "MIN",
572
- "MAX",
573
- "CENTER",
574
- "BASELINE"
575
- ]).optional().describe("The counter axis align items to set"),
576
- itemSpacing: z.number().optional().describe("The item spacing to set"),
577
- horizontalPadding: z.number().optional().describe("The horizontal padding to set"),
578
- verticalPadding: z.number().optional().describe("The vertical padding to set"),
579
- paddingLeft: z.number().optional().describe("The padding left to set (when left != right)"),
580
- paddingRight: z.number().optional().describe("The padding right to set (when left != right)"),
581
- paddingTop: z.number().optional().describe("The padding top to set (when top != bottom)"),
582
- paddingBottom: z.number().optional().describe("The padding bottom to set (when top != bottom)")
826
+ server.registerTool("set_auto_layout", {
827
+ description: "Set the auto layout of a node (WebSocket mode only)",
828
+ inputSchema: z.object({
829
+ nodeId: z.string().describe("The ID of the node to set the auto layout of"),
830
+ layoutMode: z.enum([
831
+ "HORIZONTAL",
832
+ "VERTICAL"
833
+ ]).optional().describe("The layout mode to set"),
834
+ layoutWrap: z.enum([
835
+ "NO_WRAP",
836
+ "WRAP"
837
+ ]).optional().describe("The layout wrap to set"),
838
+ primaryAxisAlignItems: z.enum([
839
+ "MIN",
840
+ "MAX",
841
+ "CENTER",
842
+ "SPACE_BETWEEN"
843
+ ]).optional().describe("The primary axis align items to set"),
844
+ counterAxisAlignItems: z.enum([
845
+ "MIN",
846
+ "MAX",
847
+ "CENTER",
848
+ "BASELINE"
849
+ ]).optional().describe("The counter axis align items to set"),
850
+ itemSpacing: z.number().optional().describe("The item spacing to set"),
851
+ horizontalPadding: z.number().optional().describe("The horizontal padding to set"),
852
+ verticalPadding: z.number().optional().describe("The vertical padding to set"),
853
+ paddingLeft: z.number().optional().describe("The padding left to set (when left != right)"),
854
+ paddingRight: z.number().optional().describe("The padding right to set (when left != right)"),
855
+ paddingTop: z.number().optional().describe("The padding top to set (when top != bottom)"),
856
+ paddingBottom: z.number().optional().describe("The padding bottom to set (when top != bottom)")
857
+ })
583
858
  }, async ({ nodeId, layoutMode, layoutWrap, primaryAxisAlignItems, counterAxisAlignItems, itemSpacing, horizontalPadding, verticalPadding, paddingLeft, paddingRight, paddingTop, paddingBottom })=>{
584
859
  try {
585
860
  await sendCommandToFigma("set_auto_layout", {
@@ -601,18 +876,21 @@ function registerEditingTools(server, figmaClient) {
601
876
  return formatErrorResponse("set_auto_layout", error);
602
877
  }
603
878
  });
604
- server.tool("set_size", "Set the size of a node", {
605
- nodeId: z.string().describe("The ID of the node to set the size of"),
606
- layoutSizingHorizontal: z.enum([
607
- "HUG",
608
- "FILL"
609
- ]).optional().describe("The horizontal layout sizing to set (exclusive with width)"),
610
- layoutSizingVertical: z.enum([
611
- "HUG",
612
- "FILL"
613
- ]).optional().describe("The vertical layout sizing to set (exclusive with height)"),
614
- width: z.number().optional().describe("The width to set (raw value)"),
615
- height: z.number().optional().describe("The height to set (raw value)")
879
+ server.registerTool("set_size", {
880
+ description: "Set the size of a node (WebSocket mode only)",
881
+ inputSchema: z.object({
882
+ nodeId: z.string().describe("The ID of the node to set the size of"),
883
+ layoutSizingHorizontal: z.enum([
884
+ "HUG",
885
+ "FILL"
886
+ ]).optional().describe("The horizontal layout sizing to set (exclusive with width)"),
887
+ layoutSizingVertical: z.enum([
888
+ "HUG",
889
+ "FILL"
890
+ ]).optional().describe("The vertical layout sizing to set (exclusive with height)"),
891
+ width: z.number().optional().describe("The width to set (raw value)"),
892
+ height: z.number().optional().describe("The height to set (raw value)")
893
+ })
616
894
  }, async ({ nodeId, layoutSizingHorizontal, layoutSizingVertical, width, height })=>{
617
895
  try {
618
896
  await sendCommandToFigma("set_size", {
@@ -678,63 +956,135 @@ function registerPrompts(server) {
678
956
  });
679
957
  }
680
958
 
681
- var version = "1.2.1";
682
-
683
- // Config loader
684
- async function loadConfig(configPath) {
685
- try {
686
- const resolvedPath = path.resolve(process.cwd(), configPath);
687
- if (!fs.existsSync(resolvedPath)) {
688
- logger.error(`Config file not found: ${resolvedPath}`);
689
- return null;
690
- }
691
- // Handle different file types
692
- if (resolvedPath.endsWith(".json")) {
693
- const content = fs.readFileSync(resolvedPath, "utf-8");
694
- return JSON.parse(content);
959
+ const channels = new Map();
960
+ function sendJson(ws, data) {
961
+ ws.send(JSON.stringify(data));
962
+ }
963
+ function broadcastToChannel(channelName, message, excludeWs) {
964
+ const clients = channels.get(channelName);
965
+ if (!clients) return;
966
+ for (const client of clients){
967
+ if (client !== excludeWs && client.readyState === WebSocket.OPEN) {
968
+ sendJson(client, message);
695
969
  }
696
- if (resolvedPath.endsWith(".js") || resolvedPath.endsWith(".mjs") || resolvedPath.endsWith(".ts") || resolvedPath.endsWith(".mts")) {
697
- // For JS/MJS/TS/MTS files, we can dynamically import with Bun
698
- // Bun has built-in TypeScript support without requiring transpilation
699
- const config = await import(resolvedPath);
700
- return config.default || config;
970
+ }
971
+ }
972
+ function handleJoin(ws, data) {
973
+ const { channel: channelName, id } = data;
974
+ if (!channelName || typeof channelName !== "string") {
975
+ sendJson(ws, {
976
+ type: "error",
977
+ message: "Channel name is required"
978
+ });
979
+ return;
980
+ }
981
+ if (!channels.has(channelName)) {
982
+ channels.set(channelName, new Set());
983
+ }
984
+ const channelClients = channels.get(channelName);
985
+ if (!channelClients) {
986
+ sendJson(ws, {
987
+ type: "error",
988
+ message: "Failed to join channel"
989
+ });
990
+ return;
991
+ }
992
+ channelClients.add(ws);
993
+ // Notify client they joined successfully
994
+ sendJson(ws, {
995
+ type: "system",
996
+ message: `Joined channel: ${channelName}`,
997
+ channel: channelName
998
+ });
999
+ // Send connection confirmation with ID if provided
1000
+ if (id) {
1001
+ console.log("Sending message to client:", id);
1002
+ sendJson(ws, {
1003
+ type: "system",
1004
+ message: {
1005
+ id,
1006
+ result: `Connected to channel: ${channelName}`
1007
+ },
1008
+ channel: channelName
1009
+ });
1010
+ }
1011
+ // Notify other clients in channel
1012
+ broadcastToChannel(channelName, {
1013
+ type: "system",
1014
+ message: "A new user has joined the channel",
1015
+ channel: channelName
1016
+ }, ws);
1017
+ }
1018
+ function handleMessage(ws, data) {
1019
+ const { channel: channelName, message } = data;
1020
+ if (!channelName || typeof channelName !== "string") {
1021
+ sendJson(ws, {
1022
+ type: "error",
1023
+ message: "Channel name is required"
1024
+ });
1025
+ return;
1026
+ }
1027
+ const channelClients = channels.get(channelName);
1028
+ if (!channelClients?.has(ws)) {
1029
+ sendJson(ws, {
1030
+ type: "error",
1031
+ message: "You must join the channel first"
1032
+ });
1033
+ return;
1034
+ }
1035
+ // Broadcast to all clients in the channel
1036
+ for (const client of channelClients){
1037
+ if (client.readyState === WebSocket.OPEN) {
1038
+ console.log("Broadcasting message to client:", message);
1039
+ sendJson(client, {
1040
+ type: "broadcast",
1041
+ message,
1042
+ sender: client === ws ? "You" : "User",
1043
+ channel: channelName
1044
+ });
701
1045
  }
702
- logger.error(`Unsupported config file format: ${resolvedPath}`);
703
- return null;
704
- } catch (error) {
705
- logger.error(`Failed to load config file: ${error instanceof Error ? error.message : String(error)}`);
706
- return null;
707
1046
  }
708
1047
  }
709
-
710
- // Initialize CLI
711
- const cli = cac("@seed-design/mcp");
712
- // Store WebSocket clients by channel
713
- const channels = new Map();
714
- function handleWebSocketConnection(ws) {
1048
+ // WebSocket Event Handlers
1049
+ function handleConnection(ws) {
715
1050
  console.log("New client connected");
716
- ws.send(JSON.stringify({
1051
+ sendJson(ws, {
717
1052
  type: "system",
718
1053
  message: "Please join a channel to start chatting"
719
- }));
720
- ws.close = ()=>{
721
- console.log("Client disconnected");
722
- channels.forEach((clients, channelName)=>{
723
- if (clients.has(ws)) {
724
- clients.delete(ws);
725
- clients.forEach((client)=>{
726
- if (client.readyState === WebSocket.OPEN) {
727
- client.send(JSON.stringify({
728
- type: "system",
729
- message: "A user has left the channel",
730
- channel: channelName
731
- }));
732
- }
733
- });
734
- }
735
- });
736
- };
1054
+ });
1055
+ }
1056
+ function handleWebSocketMessage(ws, rawMessage) {
1057
+ try {
1058
+ console.log("Received message from client:", rawMessage);
1059
+ const data = JSON.parse(rawMessage);
1060
+ switch(data.type){
1061
+ case "join":
1062
+ handleJoin(ws, data);
1063
+ break;
1064
+ case "message":
1065
+ handleMessage(ws, data);
1066
+ break;
1067
+ default:
1068
+ console.warn(`Unknown message type: ${data.type}`);
1069
+ }
1070
+ } catch (err) {
1071
+ console.error("Error handling message:", err);
1072
+ }
1073
+ }
1074
+ function handleClose(ws) {
1075
+ console.log("Client disconnected");
1076
+ for (const [channelName, clients] of channels){
1077
+ if (clients.has(ws)) {
1078
+ clients.delete(ws);
1079
+ broadcastToChannel(channelName, {
1080
+ type: "system",
1081
+ message: "A user has left the channel",
1082
+ channel: channelName
1083
+ });
1084
+ }
1085
+ }
737
1086
  }
1087
+ // Server
738
1088
  async function startWebSocketServer(port) {
739
1089
  const server = Bun.serve({
740
1090
  port,
@@ -752,12 +1102,14 @@ async function startWebSocketServer(port) {
752
1102
  });
753
1103
  }
754
1104
  // Handle WebSocket upgrade
755
- const success = server.upgrade(req, {
1105
+ if (server.upgrade(req, {
756
1106
  headers: {
757
1107
  "Access-Control-Allow-Origin": "*"
758
- }
759
- });
760
- if (success) return;
1108
+ },
1109
+ data: {}
1110
+ })) {
1111
+ return;
1112
+ }
761
1113
  // Return response for non-WebSocket requests
762
1114
  return new Response("WebSocket server running", {
763
1115
  headers: {
@@ -766,143 +1118,130 @@ async function startWebSocketServer(port) {
766
1118
  });
767
1119
  },
768
1120
  websocket: {
769
- open: handleWebSocketConnection,
770
- message (ws, message) {
771
- try {
772
- console.log("Received message from client:", message);
773
- const data = JSON.parse(message);
774
- if (data.type === "join") {
775
- const channelName = data.channel;
776
- if (!channelName || typeof channelName !== "string") {
777
- ws.send(JSON.stringify({
778
- type: "error",
779
- message: "Channel name is required"
780
- }));
781
- return;
782
- }
783
- // Create channel if it doesn't exist
784
- if (!channels.has(channelName)) {
785
- channels.set(channelName, new Set());
786
- }
787
- // Add client to channel
788
- const channelClients = channels.get(channelName);
789
- channelClients.add(ws);
790
- // Notify client they joined successfully
791
- ws.send(JSON.stringify({
792
- type: "system",
793
- message: `Joined channel: ${channelName}`,
794
- channel: channelName
795
- }));
796
- console.log("Sending message to client:", data.id);
797
- ws.send(JSON.stringify({
798
- type: "system",
799
- message: {
800
- id: data.id,
801
- result: "Connected to channel: " + channelName
802
- },
803
- channel: channelName
804
- }));
805
- // Notify other clients in channel
806
- channelClients.forEach((client)=>{
807
- if (client !== ws && client.readyState === WebSocket.OPEN) {
808
- client.send(JSON.stringify({
809
- type: "system",
810
- message: "A new user has joined the channel",
811
- channel: channelName
812
- }));
813
- }
814
- });
815
- return;
816
- }
817
- // Handle regular messages
818
- if (data.type === "message") {
819
- const channelName = data.channel;
820
- if (!channelName || typeof channelName !== "string") {
821
- ws.send(JSON.stringify({
822
- type: "error",
823
- message: "Channel name is required"
824
- }));
825
- return;
826
- }
827
- const channelClients = channels.get(channelName);
828
- if (!channelClients || !channelClients.has(ws)) {
829
- ws.send(JSON.stringify({
830
- type: "error",
831
- message: "You must join the channel first"
832
- }));
833
- return;
834
- }
835
- // Broadcast to all clients in the channel
836
- channelClients.forEach((client)=>{
837
- if (client.readyState === WebSocket.OPEN) {
838
- console.log("Broadcasting message to client:", data.message);
839
- client.send(JSON.stringify({
840
- type: "broadcast",
841
- message: data.message,
842
- sender: client === ws ? "You" : "User",
843
- channel: channelName
844
- }));
845
- }
846
- });
847
- }
848
- } catch (err) {
849
- console.error("Error handling message:", err);
850
- }
851
- },
852
- close (ws) {
853
- // Remove client from their channel
854
- channels.forEach((clients)=>{
855
- clients.delete(ws);
856
- });
857
- }
1121
+ open: handleConnection,
1122
+ message: handleWebSocketMessage,
1123
+ close: handleClose
858
1124
  }
859
1125
  });
860
1126
  console.log(`WebSocket server running on port ${server.port}`);
861
1127
  return server;
862
1128
  }
863
- async function startMcpServer(serverUrl, experimental, configPath) {
864
- // Load config if provided
865
- let configData = null;
866
- if (configPath) {
867
- configData = await loadConfig(configPath);
868
- if (configData) {
869
- logger.info(`Loaded configuration from: ${configPath}`);
870
- // Log component transformers if present
871
- if (configData.extend?.componentHandlers) {
872
- const handlers = configData.extend.componentHandlers;
873
- if (handlers.length > 0) {
874
- logger.info(`Found ${handlers.length} custom component handlers`);
1129
+
1130
+ // Helper Functions
1131
+ function getFigmaAccessToken() {
1132
+ return process.env["FIGMA_PERSONAL_ACCESS_TOKEN"]?.trim();
1133
+ }
1134
+ function createFigmaClient(serverUrl, mode) {
1135
+ const pat = getFigmaAccessToken();
1136
+ const resolvedUrl = serverUrl ?? "localhost";
1137
+ switch(mode){
1138
+ case "rest":
1139
+ {
1140
+ if (!pat) {
1141
+ logger.warn("REST mode requires FIGMA_PERSONAL_ACCESS_TOKEN. Running without Figma client.");
1142
+ } else {
1143
+ logger.info("REST mode enabled. Using REST API only.");
1144
+ }
1145
+ return null;
1146
+ }
1147
+ case "websocket":
1148
+ {
1149
+ logger.info(`WebSocket mode enabled. Client connecting to: ${resolvedUrl}`);
1150
+ return createFigmaWebSocketClient(resolvedUrl);
1151
+ }
1152
+ case "all":
1153
+ {
1154
+ if (pat) {
1155
+ logger.info("FIGMA_PERSONAL_ACCESS_TOKEN found. REST API enabled for figmaUrl/fileKey requests.");
875
1156
  }
1157
+ logger.info(`WebSocket client connecting to: ${resolvedUrl}`);
1158
+ return createFigmaWebSocketClient(resolvedUrl);
876
1159
  }
1160
+ }
1161
+ }
1162
+ function createRestClient(mode) {
1163
+ if (mode === "websocket") {
1164
+ return null;
1165
+ }
1166
+ const pat = getFigmaAccessToken();
1167
+ if (!pat) {
1168
+ if (mode === "rest") {
1169
+ logger.warn("REST mode requires FIGMA_PERSONAL_ACCESS_TOKEN. REST API will not be available.");
877
1170
  }
1171
+ return null;
1172
+ }
1173
+ logger.info("Initializing REST API client with PAT from environment");
1174
+ return createFigmaRestClient(pat);
1175
+ }
1176
+ async function loadMcpConfig(configPath) {
1177
+ if (!configPath) return null;
1178
+ const config = await loadConfig(configPath);
1179
+ if (!config) return null;
1180
+ logger.info(`Loaded configuration from: ${configPath}`);
1181
+ if (config.extend?.componentHandlers?.length) {
1182
+ logger.info(`Found ${config.extend.componentHandlers.length} custom component handlers`);
878
1183
  }
879
- const figmaClient = createFigmaWebSocketClient(serverUrl);
1184
+ return config;
1185
+ }
1186
+ function connectFigmaClient(figmaClient) {
1187
+ if (!figmaClient) return;
1188
+ try {
1189
+ figmaClient.connectToFigma();
1190
+ } catch (error) {
1191
+ const message = error instanceof Error ? error.message : String(error);
1192
+ logger.warn(`Could not connect to Figma initially: ${message}`);
1193
+ if (getFigmaAccessToken()) {
1194
+ logger.info("REST API fallback available via FIGMA_PERSONAL_ACCESS_TOKEN");
1195
+ } else {
1196
+ logger.warn("Will try to connect when the first command is sent");
1197
+ }
1198
+ }
1199
+ }
1200
+ async function startMcpServer(options = {}) {
1201
+ const { serverUrl, experimental, configPath, mode = "all" } = options;
1202
+ const config = await loadMcpConfig(configPath);
1203
+ const figmaClient = createFigmaClient(serverUrl, mode);
1204
+ const restClient = createRestClient(mode);
880
1205
  const server = new McpServer({
881
1206
  name: "SEED Design MCP",
882
1207
  version
883
1208
  });
884
- registerTools(server, figmaClient, configData);
885
- if (experimental) {
886
- registerEditingTools(server, figmaClient);
887
- }
1209
+ registerTools(server, figmaClient, restClient, config, mode);
888
1210
  registerPrompts(server);
889
- try {
890
- figmaClient.connectToFigma();
891
- } catch (error) {
892
- logger.warn(`Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`);
893
- logger.warn("Will try to connect when the first command is sent");
1211
+ if (experimental) {
1212
+ if (mode === "rest") {
1213
+ logger.warn("Experimental editing tools not available in REST mode. Skipping.");
1214
+ } else if (figmaClient) {
1215
+ registerEditingTools(server, figmaClient);
1216
+ } else {
1217
+ logger.warn("Experimental editing tools require WebSocket connection. Skipping.");
1218
+ }
894
1219
  }
1220
+ connectFigmaClient(figmaClient);
895
1221
  const transport = new StdioServerTransport();
896
1222
  await server.connect(transport);
897
- logger.info("FigmaMCP server running on stdio");
1223
+ logger.info(`FigmaMCP server running on stdio (mode: ${mode})`);
898
1224
  }
899
- // Define CLI commands
900
- cli.command("", "Start the MCP server").option("--server <server>", "Server URL", {
901
- default: "localhost"
902
- }).option("--experimental", "Enable experimental features", {
1225
+ // CLI
1226
+ const cli = cac("@seed-design/mcp");
1227
+ cli.command("", "Start the MCP server").option("--server <server>", "WebSocket server URL. If not provided and FIGMA_PERSONAL_ACCESS_TOKEN is set, REST API mode will be used.").option("--experimental", "Enable experimental features", {
903
1228
  default: false
904
- }).option("--config <config>", "Path to configuration file (.js, .mjs, .ts, .mts)").action(async (options)=>{
905
- await startMcpServer(options.server, options.experimental, options.config);
1229
+ }).option("--config <config>", "Path to configuration file (.js, .mjs, .ts, .mts)").option("--mode <mode>", "Tool registration mode: 'rest' (REST API tools only), 'websocket' (WebSocket tools only), or 'all' (default)").action(async (options)=>{
1230
+ const mode = options.mode;
1231
+ if (mode && ![
1232
+ "rest",
1233
+ "websocket",
1234
+ "all"
1235
+ ].includes(mode)) {
1236
+ console.error(`Invalid mode: ${mode}. Use 'rest', 'websocket', or 'all'.`);
1237
+ process.exit(1);
1238
+ }
1239
+ await startMcpServer({
1240
+ serverUrl: options.server,
1241
+ experimental: options.experimental,
1242
+ configPath: options.config,
1243
+ mode
1244
+ });
906
1245
  });
907
1246
  cli.command("socket", "Start the WebSocket server").option("--port <port>", "Port number", {
908
1247
  default: 3055
@@ -911,5 +1250,4 @@ cli.command("socket", "Start the WebSocket server").option("--port <port>", "Por
911
1250
  });
912
1251
  cli.help();
913
1252
  cli.version(version);
914
- // Parse CLI args
915
1253
  cli.parse();