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