@seed-design/mcp 0.0.0-alpha-20260324091316

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 ADDED
@@ -0,0 +1,1295 @@
1
+ #!/usr/bin/env node
2
+ import { cac } from 'cac';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
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';
8
+ import { v4 } from 'uuid';
9
+ import WebSocket$1 from 'ws';
10
+ import { createRestNormalizer, figma, react, getFigmaColorVariableNames } from '@seed-design/figma';
11
+ import { z } from 'zod';
12
+
13
+ var version = "1.3.8";
14
+
15
+ /**
16
+ * Custom logging module that writes to stderr instead of stdout
17
+ * to avoid being captured in MCP protocol communication
18
+ */ const logger = {
19
+ /**
20
+ * Log an informational message
21
+ */ info: (message)=>process.stderr.write(`[INFO] ${message}\n`),
22
+ /**
23
+ * Log a debug message
24
+ */ debug: (message)=>process.stderr.write(`[DEBUG] ${message}\n`),
25
+ /**
26
+ * Log a warning message
27
+ */ warn: (message)=>process.stderr.write(`[WARN] ${message}\n`),
28
+ /**
29
+ * Log an error message
30
+ */ error: (message)=>process.stderr.write(`[ERROR] ${message}\n`),
31
+ /**
32
+ * Log a general message
33
+ */ log: (message)=>process.stderr.write(`[LOG] ${message}\n`)
34
+ };
35
+ /**
36
+ * Format an error for logging
37
+ */ function formatError(error) {
38
+ if (error instanceof Error) {
39
+ return error.message;
40
+ }
41
+ return String(error);
42
+ }
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
+
114
+ function createFigmaWebSocketClient(serverUrl) {
115
+ const WS_URL = serverUrl === "localhost" ? `ws://${serverUrl}` : `wss://${serverUrl}`;
116
+ // Track which channel each client is in
117
+ let currentChannel = null;
118
+ // WebSocket connection and request tracking
119
+ let ws = null;
120
+ const pendingRequests = new Map();
121
+ // Update the connectToFigma function
122
+ function connectToFigma(port = 3055) {
123
+ // If already connected, do nothing
124
+ if (ws && ws.readyState === WebSocket$1.OPEN) {
125
+ logger.info("Already connected to Figma");
126
+ return;
127
+ }
128
+ const wsUrl = serverUrl === "localhost" ? `${WS_URL}:${port}` : WS_URL;
129
+ logger.info(`Connecting to Figma socket server at ${wsUrl}...`);
130
+ ws = new WebSocket$1(wsUrl);
131
+ ws.on("open", ()=>{
132
+ logger.info("Connected to Figma socket server");
133
+ // Reset channel on new connection
134
+ joinChannel("local-default");
135
+ });
136
+ ws.on("message", (data)=>{
137
+ try {
138
+ const json = JSON.parse(data);
139
+ // Handle progress updates
140
+ if (json.type === "progress_update") {
141
+ const progressData = json.message.data;
142
+ const requestId = json.id || "";
143
+ if (requestId && pendingRequests.has(requestId)) {
144
+ const request = pendingRequests.get(requestId);
145
+ // Update last activity timestamp
146
+ request.lastActivity = Date.now();
147
+ // Reset the timeout to prevent timeouts during long-running operations
148
+ clearTimeout(request.timeout);
149
+ // Create a new timeout
150
+ request.timeout = setTimeout(()=>{
151
+ if (pendingRequests.has(requestId)) {
152
+ logger.error(`Request ${requestId} timed out after extended period of inactivity`);
153
+ pendingRequests.delete(requestId);
154
+ request.reject(new Error("Request to Figma timed out"));
155
+ }
156
+ }, 60000); // 60 second timeout for inactivity
157
+ // Log progress
158
+ logger.info(`Progress update for ${progressData.commandType}: ${progressData.progress}% - ${progressData.message}`);
159
+ // For completed updates, we could resolve the request early if desired
160
+ if (progressData.status === "completed" && progressData.progress === 100) {
161
+ // Optionally resolve early with partial data
162
+ // request.resolve(progressData.payload);
163
+ // pendingRequests.delete(requestId);
164
+ // Instead, just log the completion, wait for final result from Figma
165
+ logger.info(`Operation ${progressData.commandType} completed, waiting for final result`);
166
+ }
167
+ }
168
+ return;
169
+ }
170
+ // Handle regular responses
171
+ const myResponse = json.message;
172
+ logger.debug(`Received message: ${JSON.stringify(myResponse)}`);
173
+ logger.log("myResponse" + JSON.stringify(myResponse));
174
+ // Handle response to a request
175
+ if (myResponse.id && pendingRequests.has(myResponse.id) && (myResponse.result || myResponse.error)) {
176
+ const request = pendingRequests.get(myResponse.id);
177
+ clearTimeout(request.timeout);
178
+ if (myResponse.error) {
179
+ logger.error(`Error from Figma: ${myResponse.error}`);
180
+ request.reject(new Error(myResponse.error));
181
+ } else {
182
+ if (myResponse.result) {
183
+ request.resolve(myResponse.result);
184
+ }
185
+ }
186
+ pendingRequests.delete(myResponse.id);
187
+ } else {
188
+ // Handle broadcast messages or events not associated with a request ID
189
+ logger.info(`Received broadcast message: ${JSON.stringify(myResponse)}`);
190
+ }
191
+ } catch (error) {
192
+ logger.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`);
193
+ }
194
+ });
195
+ ws.on("error", (error)=>{
196
+ logger.error(`Socket error: ${error}`);
197
+ });
198
+ ws.on("close", ()=>{
199
+ logger.info("Disconnected from Figma socket server");
200
+ ws = null;
201
+ // Reject all pending requests
202
+ for (const [id, request] of pendingRequests.entries()){
203
+ clearTimeout(request.timeout);
204
+ request.reject(new Error("Connection closed"));
205
+ pendingRequests.delete(id);
206
+ }
207
+ // Attempt to reconnect
208
+ logger.info("Attempting to reconnect in 2 seconds...");
209
+ setTimeout(()=>connectToFigma(port), 2000);
210
+ });
211
+ }
212
+ // Function to join a channel
213
+ async function joinChannel(channelName) {
214
+ if (!ws || ws.readyState !== WebSocket$1.OPEN) {
215
+ throw new Error("Not connected to Figma");
216
+ }
217
+ try {
218
+ await sendCommandToFigma("join", {
219
+ channel: channelName
220
+ });
221
+ currentChannel = channelName;
222
+ logger.info(`Joined channel: ${channelName}`);
223
+ } catch (error) {
224
+ logger.error(`Failed to join channel: ${error instanceof Error ? error.message : String(error)}`);
225
+ throw error;
226
+ }
227
+ }
228
+ // Function to send commands to Figma
229
+ function sendCommandToFigma(command, params = {}, timeoutMs = 30000) {
230
+ return new Promise((resolve, reject)=>{
231
+ // If not connected, try to connect first
232
+ if (!ws || ws.readyState !== WebSocket$1.OPEN) {
233
+ connectToFigma();
234
+ reject(new Error("Not connected to Figma. Attempting to connect..."));
235
+ return;
236
+ }
237
+ // Check if we need a channel for this command
238
+ const requiresChannel = command !== "join";
239
+ if (requiresChannel && !currentChannel) {
240
+ reject(new Error("Must join a channel before sending commands"));
241
+ return;
242
+ }
243
+ const id = v4();
244
+ const request = {
245
+ id,
246
+ type: command === "join" ? "join" : "message",
247
+ ...command === "join" ? {
248
+ channel: params.channel
249
+ } : {
250
+ channel: currentChannel
251
+ },
252
+ message: {
253
+ id,
254
+ command,
255
+ params: {
256
+ ...params,
257
+ commandId: id
258
+ }
259
+ }
260
+ };
261
+ // Set timeout for request
262
+ const timeout = setTimeout(()=>{
263
+ if (pendingRequests.has(id)) {
264
+ pendingRequests.delete(id);
265
+ logger.error(`Request ${id} to Figma timed out after ${timeoutMs / 1000} seconds`);
266
+ reject(new Error("Request to Figma timed out"));
267
+ }
268
+ }, timeoutMs);
269
+ // Store the promise callbacks to resolve/reject later
270
+ pendingRequests.set(id, {
271
+ resolve,
272
+ reject,
273
+ timeout,
274
+ lastActivity: Date.now()
275
+ });
276
+ // Send the request
277
+ logger.info(`Sending command to Figma: ${command}`);
278
+ logger.debug(`Request details: ${JSON.stringify(request)}`);
279
+ ws.send(JSON.stringify(request));
280
+ });
281
+ }
282
+ return {
283
+ connectToFigma,
284
+ joinChannel,
285
+ sendCommandToFigma
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Format an object response
291
+ */ function formatObjectResponse(result) {
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: JSON.stringify(result)
297
+ }
298
+ ]
299
+ };
300
+ }
301
+ /**
302
+ * Format a text response
303
+ */ function formatTextResponse(text) {
304
+ return {
305
+ content: [
306
+ {
307
+ type: "text",
308
+ text
309
+ }
310
+ ]
311
+ };
312
+ }
313
+ /**
314
+ * Format an image response
315
+ */ function formatImageResponse(imageData, mimeType = "image/png") {
316
+ return {
317
+ content: [
318
+ {
319
+ type: "image",
320
+ data: imageData,
321
+ mimeType
322
+ }
323
+ ]
324
+ };
325
+ }
326
+ /**
327
+ * Format an error response
328
+ */ function formatErrorResponse(toolName, error) {
329
+ return {
330
+ content: [
331
+ {
332
+ type: "text",
333
+ text: `Error in ${toolName}: ${formatError(error)}`
334
+ }
335
+ ]
336
+ };
337
+ }
338
+
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", {
387
+ nodeId
388
+ });
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
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);
491
+ const node = result.document;
492
+ if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") {
493
+ return formatErrorResponse("get_component_info", new Error(`Node with ID ${nodeId} is not a component node`));
494
+ }
495
+ const key = result.componentSets[nodeId]?.key ?? result.components[nodeId]?.key;
496
+ if (!key) {
497
+ return formatErrorResponse("get_component_info", new Error(`${nodeId} is not present in exported component data`));
498
+ }
499
+ return formatObjectResponse({
500
+ name: node.name,
501
+ key,
502
+ componentPropertyDefinitions: node.componentPropertyDefinitions
503
+ });
504
+ } catch (error) {
505
+ return formatErrorResponse("get_component_info", error);
506
+ }
507
+ });
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)=>{
513
+ try {
514
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
515
+ const result = await fetchNodeData({
516
+ fileKey,
517
+ nodeId,
518
+ personalAccessToken
519
+ }, context);
520
+ const normalizer = createRestNormalizer(result);
521
+ const node = normalizer(result.document);
522
+ const noInferPipeline = figma.createPipeline({
523
+ shouldInferAutoLayout: false,
524
+ shouldInferVariableName: false
525
+ });
526
+ const inferPipeline = figma.createPipeline({
527
+ shouldInferAutoLayout: true,
528
+ shouldInferVariableName: true
529
+ });
530
+ const original = noInferPipeline.generateCode(node, {
531
+ shouldPrintSource: true
532
+ })?.jsx ?? "Failed to generate summarized node info";
533
+ const inferred = inferPipeline.generateCode(node, {
534
+ shouldPrintSource: true
535
+ })?.jsx ?? "Failed to generate summarized node info";
536
+ return formatObjectResponse({
537
+ original: {
538
+ data: original,
539
+ description: "Original Figma node info"
540
+ },
541
+ inferred: {
542
+ data: inferred,
543
+ description: "AutoLayout Inferred Figma node info"
544
+ }
545
+ });
546
+ } catch (error) {
547
+ return formatTextResponse(`Error in get_node_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
548
+ }
549
+ });
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 })=>{
555
+ try {
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);
574
+ const noInferPipeline = figma.createPipeline({
575
+ shouldInferAutoLayout: false,
576
+ shouldInferVariableName: false
577
+ });
578
+ const inferPipeline = figma.createPipeline({
579
+ shouldInferAutoLayout: true,
580
+ shouldInferVariableName: true
581
+ });
582
+ const original = noInferPipeline.generateCode(node, {
583
+ shouldPrintSource: true
584
+ })?.jsx ?? "Failed to generate summarized node info";
585
+ const inferred = inferPipeline.generateCode(node, {
586
+ shouldPrintSource: true
587
+ })?.jsx ?? "Failed to generate summarized node info";
588
+ return {
589
+ nodeId,
590
+ original: {
591
+ data: original,
592
+ description: "Original Figma node info"
593
+ },
594
+ inferred: {
595
+ data: inferred,
596
+ description: "AutoLayout Inferred Figma node info"
597
+ }
598
+ };
599
+ });
600
+ return formatObjectResponse(results);
601
+ } catch (error) {
602
+ return formatTextResponse(`Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`);
603
+ }
604
+ });
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)=>{
610
+ try {
611
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
612
+ const result = await fetchNodeData({
613
+ fileKey,
614
+ nodeId,
615
+ personalAccessToken
616
+ }, context);
617
+ const normalizer = createRestNormalizer(result);
618
+ const pipeline = react.createPipeline({
619
+ shouldInferAutoLayout: true,
620
+ shouldInferVariableName: true,
621
+ extend: context.extend
622
+ });
623
+ const generated = pipeline.generateCode(normalizer(result.document), {
624
+ shouldPrintSource: false
625
+ });
626
+ if (!generated) {
627
+ return formatTextResponse("Failed to generate code\n\n⚠️ Please make sure you have the latest version of the Figma library.");
628
+ }
629
+ return formatTextResponse(`${generated.imports}\n\n${generated.jsx}`);
630
+ } catch (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.`);
632
+ }
633
+ });
634
+ // Find Layers Tool (REST API + WebSocket)
635
+ server.registerTool("find_nodes", {
636
+ description: getSingleNodeDescription("Find layers by name within a Figma node's subtree. Returns a flat array of matching nodes with their IDs.", mode),
637
+ inputSchema: singleNodeParamsSchema.extend({
638
+ name: z.string().describe("Regex pattern to match layer names (e.g., 'Usage', 'Do$').")
639
+ })
640
+ }, async (params)=>{
641
+ try {
642
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
643
+ const result = await fetchNodeData({
644
+ fileKey,
645
+ nodeId,
646
+ personalAccessToken
647
+ }, context);
648
+ const normalizer = createRestNormalizer(result);
649
+ const node = normalizer(result.document);
650
+ let pattern;
651
+ try {
652
+ pattern = new RegExp(params.name);
653
+ } catch {
654
+ return formatErrorResponse("find_nodes", new Error(`Invalid regex pattern: ${params.name}`));
655
+ }
656
+ const matches = collectMatchingNodes(node, pattern);
657
+ return formatObjectResponse(matches);
658
+ } catch (error) {
659
+ return formatErrorResponse("find_nodes", error);
660
+ }
661
+ });
662
+ // Utility Tools (No Figma connection required)
663
+ // Retrieve Color Variable Names Tool
664
+ server.registerTool("retrieve_color_variable_names", {
665
+ description: "Retrieve available SEED Design color variable names by scope. No Figma connection required.",
666
+ inputSchema: z.object({
667
+ scope: z.enum([
668
+ "fg",
669
+ "bg",
670
+ "stroke",
671
+ "palette"
672
+ ]).array().describe("The scope of the color variable names to retrieve")
673
+ })
674
+ }, async ({ scope })=>{
675
+ try {
676
+ const result = getFigmaColorVariableNames(scope);
677
+ return formatObjectResponse(result);
678
+ } catch (error) {
679
+ return formatErrorResponse("retrieve_color_variable_names", error);
680
+ }
681
+ });
682
+ if (shouldRegisterWebSocketOnlyTools) {
683
+ // WebSocket Only Tools
684
+ server.registerTool("join_channel", {
685
+ description: "Join a specific channel to communicate with Figma (WebSocket mode only)",
686
+ inputSchema: z.object({
687
+ channel: z.string().describe("The name of the channel to join").default("")
688
+ })
689
+ }, async ({ channel })=>{
690
+ try {
691
+ if (!figmaClient) return formatErrorResponse("join_channel", new Error("WebSocket not available. This tool requires Figma Plugin connection."));
692
+ if (!channel) // If no channel provided, ask the user for input
693
+ return {
694
+ ...formatTextResponse("Please provide a channel name to join:"),
695
+ followUp: {
696
+ tool: "join_channel",
697
+ description: "Join the specified channel"
698
+ }
699
+ };
700
+ await figmaClient.joinChannel(channel);
701
+ return formatTextResponse(`Successfully joined channel: ${channel}`);
702
+ } catch (error) {
703
+ return formatErrorResponse("join_channel", error);
704
+ }
705
+ });
706
+ // Document Info Tool
707
+ server.registerTool("get_document_info", {
708
+ description: "Get detailed information about the current Figma document (WebSocket mode only)"
709
+ }, async ()=>{
710
+ try {
711
+ requireWebSocket(context);
712
+ const result = await context.sendCommandToFigma("get_document_info");
713
+ return formatObjectResponse(result);
714
+ } catch (error) {
715
+ return formatErrorResponse("get_document_info", error);
716
+ }
717
+ });
718
+ // Selection Tool
719
+ server.registerTool("get_selection", {
720
+ description: "Get information about the current selection in Figma (WebSocket mode only)"
721
+ }, async ()=>{
722
+ try {
723
+ requireWebSocket(context);
724
+ const result = await context.sendCommandToFigma("get_selection");
725
+ return formatObjectResponse(result);
726
+ } catch (error) {
727
+ return formatErrorResponse("get_selection", error);
728
+ }
729
+ });
730
+ // Annotation Tool
731
+ server.registerTool("add_annotations", {
732
+ description: "Add annotations to multiple nodes in Figma (WebSocket mode only)",
733
+ inputSchema: z.object({
734
+ annotations: z.array(z.object({
735
+ nodeId: z.string().describe("The ID of the node to add an annotation to"),
736
+ labelMarkdown: z.string().describe("The markdown label for the annotation, do not escape newlines")
737
+ }))
738
+ })
739
+ }, async ({ annotations })=>{
740
+ try {
741
+ requireWebSocket(context);
742
+ await context.sendCommandToFigma("add_annotations", {
743
+ annotations
744
+ });
745
+ return formatTextResponse(`Annotations added to nodes ${annotations.map((annotation)=>annotation.nodeId).join(", ")}`);
746
+ } catch (error) {
747
+ return formatErrorResponse("add_annotations", error);
748
+ }
749
+ });
750
+ // Get Annotations Tool
751
+ server.registerTool("get_annotations", {
752
+ description: "Get annotations for a specific node in Figma (WebSocket mode only)",
753
+ inputSchema: z.object({
754
+ nodeId: z.string().describe("The ID of the node to get annotations for")
755
+ })
756
+ }, async ({ nodeId })=>{
757
+ try {
758
+ requireWebSocket(context);
759
+ const result = await context.sendCommandToFigma("get_annotations", {
760
+ nodeId
761
+ });
762
+ return formatObjectResponse(result);
763
+ } catch (error) {
764
+ return formatErrorResponse("get_annotations", error);
765
+ }
766
+ });
767
+ // Export Node as Image Tool
768
+ server.registerTool("export_node_as_image", {
769
+ description: "Export a node as an image from Figma (WebSocket mode only)",
770
+ inputSchema: z.object({
771
+ nodeId: z.string().describe("The ID of the node to export"),
772
+ format: z.enum([
773
+ "PNG",
774
+ "JPG",
775
+ "SVG",
776
+ "PDF"
777
+ ]).optional().describe("Export format"),
778
+ scale: z.number().positive().optional().describe("Export scale")
779
+ })
780
+ }, async ({ nodeId, format, scale })=>{
781
+ try {
782
+ requireWebSocket(context);
783
+ const result = await context.sendCommandToFigma("export_node_as_image", {
784
+ nodeId,
785
+ format: format || "PNG",
786
+ scale: scale || 1
787
+ });
788
+ const typedResult = result;
789
+ return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
790
+ } catch (error) {
791
+ return formatErrorResponse("export_node_as_image", error);
792
+ }
793
+ });
794
+ }
795
+ }
796
+ function collectMatchingNodes(node, pattern) {
797
+ const results = [];
798
+ if ("name" in node && pattern.test(node.name)) {
799
+ results.push({
800
+ id: node.id,
801
+ name: node.name,
802
+ type: node.type
803
+ });
804
+ }
805
+ if ("children" in node && node.children) {
806
+ for (const child of node.children){
807
+ results.push(...collectMatchingNodes(child, pattern));
808
+ }
809
+ }
810
+ return results;
811
+ }
812
+ // editing tools require WebSocket client
813
+ function registerEditingTools(server, figmaClient) {
814
+ const { sendCommandToFigma } = figmaClient;
815
+ // Clone Node Tool
816
+ server.registerTool("clone_node", {
817
+ description: "Clone an existing node in Figma (WebSocket mode only)",
818
+ inputSchema: z.object({
819
+ nodeId: z.string().describe("The ID of the node to clone"),
820
+ x: z.number().optional().describe("New X position for the clone"),
821
+ y: z.number().optional().describe("New Y position for the clone")
822
+ })
823
+ }, async ({ nodeId, x, y })=>{
824
+ try {
825
+ const result = await sendCommandToFigma("clone_node", {
826
+ nodeId,
827
+ x,
828
+ y
829
+ });
830
+ const typedResult = result;
831
+ return formatTextResponse(`Cloned node with new ID: ${typedResult.id}${x !== undefined && y !== undefined ? ` at position (${x}, ${y})` : ""}`);
832
+ } catch (error) {
833
+ return formatErrorResponse("clone_node", error);
834
+ }
835
+ });
836
+ server.registerTool("set_fill_color", {
837
+ description: "Set the fill color of a node (WebSocket mode only)",
838
+ inputSchema: z.object({
839
+ nodeId: z.string().describe("The ID of the node to set the fill color of"),
840
+ colorToken: z.string().describe("The color token to set the fill color to. Format: `{category}/{name}`. Example: `bg/brand`")
841
+ })
842
+ }, async ({ nodeId, colorToken })=>{
843
+ try {
844
+ await sendCommandToFigma("set_fill_color", {
845
+ nodeId,
846
+ colorToken
847
+ });
848
+ return formatTextResponse(`Fill color set to ${colorToken}`);
849
+ } catch (error) {
850
+ return formatErrorResponse("set_fill_color", error);
851
+ }
852
+ });
853
+ server.registerTool("set_stroke_color", {
854
+ description: "Set the stroke color of a node (WebSocket mode only)",
855
+ inputSchema: z.object({
856
+ nodeId: z.string().describe("The ID of the node to set the stroke color of"),
857
+ colorToken: z.string().describe("The color token to set the stroke color to. Format: `{category}/{name}`. Example: `stroke/neutral`")
858
+ })
859
+ }, async ({ nodeId, colorToken })=>{
860
+ try {
861
+ await sendCommandToFigma("set_stroke_color", {
862
+ nodeId,
863
+ colorToken
864
+ });
865
+ return formatTextResponse(`Stroke color set to ${colorToken}`);
866
+ } catch (error) {
867
+ return formatErrorResponse("set_stroke_color", error);
868
+ }
869
+ });
870
+ server.registerTool("set_auto_layout", {
871
+ description: "Set the auto layout of a node (WebSocket mode only)",
872
+ inputSchema: z.object({
873
+ nodeId: z.string().describe("The ID of the node to set the auto layout of"),
874
+ layoutMode: z.enum([
875
+ "HORIZONTAL",
876
+ "VERTICAL"
877
+ ]).optional().describe("The layout mode to set"),
878
+ layoutWrap: z.enum([
879
+ "NO_WRAP",
880
+ "WRAP"
881
+ ]).optional().describe("The layout wrap to set"),
882
+ primaryAxisAlignItems: z.enum([
883
+ "MIN",
884
+ "MAX",
885
+ "CENTER",
886
+ "SPACE_BETWEEN"
887
+ ]).optional().describe("The primary axis align items to set"),
888
+ counterAxisAlignItems: z.enum([
889
+ "MIN",
890
+ "MAX",
891
+ "CENTER",
892
+ "BASELINE"
893
+ ]).optional().describe("The counter axis align items to set"),
894
+ itemSpacing: z.number().optional().describe("The item spacing to set"),
895
+ horizontalPadding: z.number().optional().describe("The horizontal padding to set"),
896
+ verticalPadding: z.number().optional().describe("The vertical padding to set"),
897
+ paddingLeft: z.number().optional().describe("The padding left to set (when left != right)"),
898
+ paddingRight: z.number().optional().describe("The padding right to set (when left != right)"),
899
+ paddingTop: z.number().optional().describe("The padding top to set (when top != bottom)"),
900
+ paddingBottom: z.number().optional().describe("The padding bottom to set (when top != bottom)")
901
+ })
902
+ }, async ({ nodeId, layoutMode, layoutWrap, primaryAxisAlignItems, counterAxisAlignItems, itemSpacing, horizontalPadding, verticalPadding, paddingLeft, paddingRight, paddingTop, paddingBottom })=>{
903
+ try {
904
+ await sendCommandToFigma("set_auto_layout", {
905
+ nodeId,
906
+ layoutMode,
907
+ layoutWrap,
908
+ primaryAxisAlignItems,
909
+ counterAxisAlignItems,
910
+ itemSpacing,
911
+ horizontalPadding,
912
+ verticalPadding,
913
+ paddingLeft,
914
+ paddingRight,
915
+ paddingTop,
916
+ paddingBottom
917
+ });
918
+ return formatTextResponse(`Layout set to ${layoutMode}`);
919
+ } catch (error) {
920
+ return formatErrorResponse("set_auto_layout", error);
921
+ }
922
+ });
923
+ server.registerTool("set_size", {
924
+ description: "Set the size of a node (WebSocket mode only)",
925
+ inputSchema: z.object({
926
+ nodeId: z.string().describe("The ID of the node to set the size of"),
927
+ layoutSizingHorizontal: z.enum([
928
+ "HUG",
929
+ "FILL"
930
+ ]).optional().describe("The horizontal layout sizing to set (exclusive with width)"),
931
+ layoutSizingVertical: z.enum([
932
+ "HUG",
933
+ "FILL"
934
+ ]).optional().describe("The vertical layout sizing to set (exclusive with height)"),
935
+ width: z.number().optional().describe("The width to set (raw value)"),
936
+ height: z.number().optional().describe("The height to set (raw value)")
937
+ })
938
+ }, async ({ nodeId, layoutSizingHorizontal, layoutSizingVertical, width, height })=>{
939
+ try {
940
+ await sendCommandToFigma("set_size", {
941
+ nodeId,
942
+ layoutSizingHorizontal,
943
+ layoutSizingVertical,
944
+ width,
945
+ height
946
+ });
947
+ return formatTextResponse(`Size set to ${width ?? layoutSizingHorizontal}x${height ?? layoutSizingVertical}`);
948
+ } catch (error) {
949
+ return formatErrorResponse("set_size", error);
950
+ }
951
+ });
952
+ }
953
+
954
+ function registerPrompts(server) {
955
+ server.prompt("react_implementation_strategy", "Best practices for implementing React components", (_extra)=>{
956
+ return {
957
+ messages: [
958
+ {
959
+ role: "assistant",
960
+ content: {
961
+ type: "text",
962
+ text: `When implementing React components, follow these best practices:
963
+
964
+ 1. Start with selection:
965
+ - First use get_selection() to understand the current selection
966
+ - If no selection ask user to select single node
967
+
968
+ 2. Get React code of the selected node:
969
+ - Use get_node_react_code() to get the React code of the selected node
970
+ - If no selection ask user to select single node
971
+ `
972
+ }
973
+ }
974
+ ],
975
+ description: "Best practices for implementing React components"
976
+ };
977
+ });
978
+ server.prompt("read_design_strategy", "Best practices for reading Figma designs", (_extra)=>{
979
+ return {
980
+ messages: [
981
+ {
982
+ role: "assistant",
983
+ content: {
984
+ type: "text",
985
+ text: `When reading Figma designs, follow these best practices:
986
+
987
+ 1. Start with selection:
988
+ - First use get_selection() to understand the current selection
989
+ - If no selection ask user to select single or multiple nodes
990
+
991
+ 2. Get node infos of the selected nodes:
992
+ - Use get_nodes_info() to get the information of the selected nodes
993
+ - If no selection ask user to select single or multiple nodes
994
+ `
995
+ }
996
+ }
997
+ ],
998
+ description: "Best practices for reading Figma designs"
999
+ };
1000
+ });
1001
+ }
1002
+
1003
+ const channels = new Map();
1004
+ function sendJson(ws, data) {
1005
+ ws.send(JSON.stringify(data));
1006
+ }
1007
+ function broadcastToChannel(channelName, message, excludeWs) {
1008
+ const clients = channels.get(channelName);
1009
+ if (!clients) return;
1010
+ for (const client of clients){
1011
+ if (client !== excludeWs && client.readyState === WebSocket.OPEN) {
1012
+ sendJson(client, message);
1013
+ }
1014
+ }
1015
+ }
1016
+ function handleJoin(ws, data) {
1017
+ const { channel: channelName, id } = data;
1018
+ if (!channelName || typeof channelName !== "string") {
1019
+ sendJson(ws, {
1020
+ type: "error",
1021
+ message: "Channel name is required"
1022
+ });
1023
+ return;
1024
+ }
1025
+ if (!channels.has(channelName)) {
1026
+ channels.set(channelName, new Set());
1027
+ }
1028
+ const channelClients = channels.get(channelName);
1029
+ if (!channelClients) {
1030
+ sendJson(ws, {
1031
+ type: "error",
1032
+ message: "Failed to join channel"
1033
+ });
1034
+ return;
1035
+ }
1036
+ channelClients.add(ws);
1037
+ // Notify client they joined successfully
1038
+ sendJson(ws, {
1039
+ type: "system",
1040
+ message: `Joined channel: ${channelName}`,
1041
+ channel: channelName
1042
+ });
1043
+ // Send connection confirmation
1044
+ console.log("Sending message to client:", id);
1045
+ sendJson(ws, {
1046
+ type: "system",
1047
+ message: {
1048
+ id,
1049
+ result: `Connected to channel: ${channelName}`
1050
+ },
1051
+ channel: channelName
1052
+ });
1053
+ // Notify other clients in channel
1054
+ broadcastToChannel(channelName, {
1055
+ type: "system",
1056
+ message: "A new user has joined the channel",
1057
+ channel: channelName
1058
+ }, ws);
1059
+ }
1060
+ function handleMessage(ws, data) {
1061
+ const { channel: channelName, message } = data;
1062
+ if (!channelName || typeof channelName !== "string") {
1063
+ sendJson(ws, {
1064
+ type: "error",
1065
+ message: "Channel name is required"
1066
+ });
1067
+ return;
1068
+ }
1069
+ const channelClients = channels.get(channelName);
1070
+ if (!channelClients?.has(ws)) {
1071
+ sendJson(ws, {
1072
+ type: "error",
1073
+ message: "You must join the channel first"
1074
+ });
1075
+ return;
1076
+ }
1077
+ // Broadcast to all clients in the channel
1078
+ for (const client of channelClients){
1079
+ if (client.readyState === WebSocket.OPEN) {
1080
+ console.log("Broadcasting message to client:", message);
1081
+ sendJson(client, {
1082
+ type: "broadcast",
1083
+ message,
1084
+ sender: client === ws ? "You" : "User",
1085
+ channel: channelName
1086
+ });
1087
+ }
1088
+ }
1089
+ }
1090
+ // WebSocket Event Handlers
1091
+ function handleConnection(ws) {
1092
+ console.log("New client connected");
1093
+ sendJson(ws, {
1094
+ type: "system",
1095
+ message: "Please join a channel to start chatting"
1096
+ });
1097
+ }
1098
+ function handleWebSocketMessage(ws, rawMessage) {
1099
+ try {
1100
+ console.log("Received message from client:", rawMessage);
1101
+ const data = JSON.parse(rawMessage);
1102
+ switch(data.type){
1103
+ case "join":
1104
+ handleJoin(ws, data);
1105
+ break;
1106
+ case "message":
1107
+ handleMessage(ws, data);
1108
+ break;
1109
+ default:
1110
+ console.warn(`Unknown message type: ${data.type}`);
1111
+ }
1112
+ } catch (err) {
1113
+ console.error("Error handling message:", err);
1114
+ }
1115
+ }
1116
+ function handleClose(ws) {
1117
+ console.log("Client disconnected");
1118
+ for (const [channelName, clients] of channels){
1119
+ if (clients.has(ws)) {
1120
+ clients.delete(ws);
1121
+ broadcastToChannel(channelName, {
1122
+ type: "system",
1123
+ message: "A user has left the channel",
1124
+ channel: channelName
1125
+ });
1126
+ }
1127
+ }
1128
+ }
1129
+ // Server
1130
+ async function startWebSocketServer(port) {
1131
+ const server = Bun.serve({
1132
+ port,
1133
+ // uncomment this to allow connections in windows wsl
1134
+ // hostname: "0.0.0.0",
1135
+ fetch (req, server) {
1136
+ // Handle CORS preflight
1137
+ if (req.method === "OPTIONS") {
1138
+ return new Response(null, {
1139
+ headers: {
1140
+ "Access-Control-Allow-Origin": "*",
1141
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
1142
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1143
+ }
1144
+ });
1145
+ }
1146
+ // Handle WebSocket upgrade
1147
+ if (server.upgrade(req, {
1148
+ headers: {
1149
+ "Access-Control-Allow-Origin": "*"
1150
+ },
1151
+ data: {}
1152
+ })) {
1153
+ return;
1154
+ }
1155
+ // Return response for non-WebSocket requests
1156
+ return new Response("WebSocket server running", {
1157
+ headers: {
1158
+ "Access-Control-Allow-Origin": "*"
1159
+ }
1160
+ });
1161
+ },
1162
+ websocket: {
1163
+ open: handleConnection,
1164
+ message: handleWebSocketMessage,
1165
+ close: handleClose
1166
+ }
1167
+ });
1168
+ console.log(`WebSocket server running on port ${server.port}`);
1169
+ return server;
1170
+ }
1171
+
1172
+ // Helper Functions
1173
+ function getFigmaAccessToken() {
1174
+ return process.env["FIGMA_PERSONAL_ACCESS_TOKEN"]?.trim();
1175
+ }
1176
+ function createFigmaClient(serverUrl, mode) {
1177
+ const pat = getFigmaAccessToken();
1178
+ const resolvedUrl = serverUrl ?? "localhost";
1179
+ switch(mode){
1180
+ case "rest":
1181
+ {
1182
+ if (!pat) {
1183
+ logger.warn("REST mode requires FIGMA_PERSONAL_ACCESS_TOKEN. Running without Figma client.");
1184
+ } else {
1185
+ logger.info("REST mode enabled. Using REST API only.");
1186
+ }
1187
+ return null;
1188
+ }
1189
+ case "websocket":
1190
+ {
1191
+ logger.info(`WebSocket mode enabled. Client connecting to: ${resolvedUrl}`);
1192
+ return createFigmaWebSocketClient(resolvedUrl);
1193
+ }
1194
+ case "all":
1195
+ {
1196
+ if (pat) {
1197
+ logger.info("FIGMA_PERSONAL_ACCESS_TOKEN found. REST API enabled for figmaUrl/fileKey requests.");
1198
+ }
1199
+ logger.info(`WebSocket client connecting to: ${resolvedUrl}`);
1200
+ return createFigmaWebSocketClient(resolvedUrl);
1201
+ }
1202
+ }
1203
+ }
1204
+ function createRestClient(mode) {
1205
+ if (mode === "websocket") {
1206
+ return null;
1207
+ }
1208
+ const pat = getFigmaAccessToken();
1209
+ if (!pat) {
1210
+ if (mode === "rest") {
1211
+ logger.warn("REST mode requires FIGMA_PERSONAL_ACCESS_TOKEN. REST API will not be available.");
1212
+ }
1213
+ return null;
1214
+ }
1215
+ logger.info("Initializing REST API client with PAT from environment");
1216
+ return createFigmaRestClient(pat);
1217
+ }
1218
+ async function loadMcpConfig(configPath) {
1219
+ if (!configPath) return null;
1220
+ const config = await loadConfig(configPath);
1221
+ if (!config) return null;
1222
+ logger.info(`Loaded configuration from: ${configPath}`);
1223
+ if (config.extend?.componentHandlers?.length) {
1224
+ logger.info(`Found ${config.extend.componentHandlers.length} custom component handlers`);
1225
+ }
1226
+ return config;
1227
+ }
1228
+ function connectFigmaClient(figmaClient) {
1229
+ if (!figmaClient) return;
1230
+ try {
1231
+ figmaClient.connectToFigma();
1232
+ } catch (error) {
1233
+ const message = error instanceof Error ? error.message : String(error);
1234
+ logger.warn(`Could not connect to Figma initially: ${message}`);
1235
+ if (getFigmaAccessToken()) {
1236
+ logger.info("REST API fallback available via FIGMA_PERSONAL_ACCESS_TOKEN");
1237
+ } else {
1238
+ logger.warn("Will try to connect when the first command is sent");
1239
+ }
1240
+ }
1241
+ }
1242
+ async function startMcpServer(options = {}) {
1243
+ const { serverUrl, experimental, configPath, mode = "all" } = options;
1244
+ const config = await loadMcpConfig(configPath);
1245
+ const figmaClient = createFigmaClient(serverUrl, mode);
1246
+ const restClient = createRestClient(mode);
1247
+ const server = new McpServer({
1248
+ name: "SEED Design MCP",
1249
+ version
1250
+ });
1251
+ registerTools(server, figmaClient, restClient, config, mode);
1252
+ registerPrompts(server);
1253
+ if (experimental) {
1254
+ if (mode === "rest") {
1255
+ logger.warn("Experimental editing tools not available in REST mode. Skipping.");
1256
+ } else if (figmaClient) {
1257
+ registerEditingTools(server, figmaClient);
1258
+ } else {
1259
+ logger.warn("Experimental editing tools require WebSocket connection. Skipping.");
1260
+ }
1261
+ }
1262
+ connectFigmaClient(figmaClient);
1263
+ const transport = new StdioServerTransport();
1264
+ await server.connect(transport);
1265
+ logger.info(`FigmaMCP server running on stdio (mode: ${mode})`);
1266
+ }
1267
+ // CLI
1268
+ const cli = cac("@seed-design/mcp");
1269
+ 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", {
1270
+ default: false
1271
+ }).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)=>{
1272
+ const mode = options.mode;
1273
+ if (mode && ![
1274
+ "rest",
1275
+ "websocket",
1276
+ "all"
1277
+ ].includes(mode)) {
1278
+ console.error(`Invalid mode: ${mode}. Use 'rest', 'websocket', or 'all'.`);
1279
+ process.exit(1);
1280
+ }
1281
+ await startMcpServer({
1282
+ serverUrl: options.server,
1283
+ experimental: options.experimental,
1284
+ configPath: options.config,
1285
+ mode
1286
+ });
1287
+ });
1288
+ cli.command("socket", "Start the WebSocket server").option("--port <port>", "Port number", {
1289
+ default: 3055
1290
+ }).action(async (options)=>{
1291
+ await startWebSocketServer(options.port);
1292
+ });
1293
+ cli.help();
1294
+ cli.version(version);
1295
+ cli.parse();