@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/src/tools.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { GetFileNodesResponse } from "@figma/rest-api-spec";
|
|
2
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
2
|
import { createRestNormalizer, figma, getFigmaColorVariableNames, react } from "@seed-design/figma";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import type { McpConfig } from "./config";
|
|
5
|
+
import { parseFigmaUrl } from "./figma-rest-client";
|
|
6
6
|
import { formatError } from "./logger";
|
|
7
7
|
import {
|
|
8
8
|
formatErrorResponse,
|
|
@@ -10,128 +10,146 @@ import {
|
|
|
10
10
|
formatObjectResponse,
|
|
11
11
|
formatTextResponse,
|
|
12
12
|
} from "./responses";
|
|
13
|
+
import type { FigmaRestClient } from "./figma-rest-client";
|
|
14
|
+
import {
|
|
15
|
+
createToolContext,
|
|
16
|
+
fetchMultipleNodesData,
|
|
17
|
+
fetchNodeData,
|
|
18
|
+
requireWebSocket,
|
|
19
|
+
type ToolMode,
|
|
20
|
+
} from "./tools-helpers";
|
|
13
21
|
import type { FigmaWebSocketClient } from "./websocket";
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const singleNodeBaseSchema = z.object({
|
|
24
|
+
figmaUrl: z
|
|
25
|
+
.url()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Figma node URL. Extracts fileKey and nodeId automatically when provided."),
|
|
28
|
+
fileKey: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Figma file key. Use with nodeId when not using figmaUrl."),
|
|
32
|
+
nodeId: z.string().optional().describe("Node ID (e.g., '0:1')."),
|
|
33
|
+
personalAccessToken: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided."),
|
|
37
|
+
});
|
|
22
38
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
followUp: {
|
|
37
|
-
tool: "join_channel",
|
|
38
|
-
description: "Join the specified channel",
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
39
|
+
const multiNodeBaseSchema = z.object({
|
|
40
|
+
fileKey: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Figma file key. Required when WebSocket connection is not available."),
|
|
44
|
+
nodeIds: z.array(z.string()).describe("Array of node IDs (e.g., ['0:1', '0:2'])"),
|
|
45
|
+
personalAccessToken: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
"Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided to be used when WebSocket connection is not available.",
|
|
50
|
+
),
|
|
51
|
+
});
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
function getSingleNodeParamsSchema(mode: ToolMode) {
|
|
54
|
+
switch (mode) {
|
|
55
|
+
case "websocket":
|
|
56
|
+
return singleNodeBaseSchema.pick({ nodeId: true }).required();
|
|
57
|
+
default:
|
|
58
|
+
return singleNodeBaseSchema;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return formatErrorResponse("get_document_info", error);
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
);
|
|
62
|
+
function getMultiNodeParamsSchema(mode: ToolMode) {
|
|
63
|
+
switch (mode) {
|
|
64
|
+
case "websocket":
|
|
65
|
+
return multiNodeBaseSchema.pick({ nodeIds: true });
|
|
66
|
+
case "rest":
|
|
67
|
+
return multiNodeBaseSchema.required({ fileKey: true });
|
|
68
|
+
default:
|
|
69
|
+
return multiNodeBaseSchema;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const result = await sendCommandToFigma("get_selection");
|
|
74
|
-
return formatObjectResponse(result);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
return formatErrorResponse("get_selection", error);
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
);
|
|
73
|
+
function resolveSingleNodeParams(params: z.infer<typeof singleNodeBaseSchema>): {
|
|
74
|
+
fileKey: string | undefined;
|
|
75
|
+
nodeId: string;
|
|
76
|
+
personalAccessToken: string | undefined;
|
|
77
|
+
} {
|
|
78
|
+
if (params.figmaUrl) {
|
|
79
|
+
const parsed = parseFigmaUrl(params.figmaUrl);
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
z.object({
|
|
88
|
-
nodeId: z.string().describe("The ID of the node to add an annotation to"),
|
|
89
|
-
labelMarkdown: z
|
|
90
|
-
.string()
|
|
91
|
-
.describe("The markdown label for the annotation, do not escape newlines"),
|
|
92
|
-
}),
|
|
93
|
-
),
|
|
94
|
-
},
|
|
95
|
-
async ({ annotations }) => {
|
|
96
|
-
try {
|
|
97
|
-
await sendCommandToFigma("add_annotations", { annotations });
|
|
81
|
+
return {
|
|
82
|
+
fileKey: parsed.fileKey,
|
|
83
|
+
nodeId: parsed.nodeId,
|
|
84
|
+
personalAccessToken: params.personalAccessToken,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
98
87
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
);
|
|
88
|
+
if (!params.nodeId) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Either figmaUrl or nodeId must be provided. Use figmaUrl for automatic parsing, or provide fileKey + nodeId directly.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
107
93
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
94
|
+
return {
|
|
95
|
+
fileKey: params.fileKey,
|
|
96
|
+
nodeId: params.nodeId,
|
|
97
|
+
personalAccessToken: params.personalAccessToken,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getSingleNodeDescription(baseDescription: string, mode: ToolMode): string {
|
|
102
|
+
switch (mode) {
|
|
103
|
+
case "rest":
|
|
104
|
+
return `${baseDescription} Provide either: (1) figmaUrl (e.g., https://www.figma.com/design/ABC/Name?node-id=0-1), or (2) fileKey + nodeId.`;
|
|
105
|
+
case "websocket":
|
|
106
|
+
return `${baseDescription} Provide nodeId. Requires WebSocket connection with Figma Plugin.`;
|
|
107
|
+
case "all":
|
|
108
|
+
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.`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
112
|
+
function getMultiNodeDescription(baseDescription: string, mode: ToolMode): string {
|
|
113
|
+
switch (mode) {
|
|
114
|
+
case "rest":
|
|
115
|
+
return `${baseDescription} Provide fileKey + nodeIds.`;
|
|
116
|
+
case "websocket":
|
|
117
|
+
return `${baseDescription} Provide nodeIds. Requires WebSocket connection with Figma Plugin.`;
|
|
118
|
+
case "all":
|
|
119
|
+
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.`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function registerTools(
|
|
124
|
+
server: McpServer,
|
|
125
|
+
figmaClient: FigmaWebSocketClient | null,
|
|
126
|
+
restClient: FigmaRestClient | null,
|
|
127
|
+
config: McpConfig | null,
|
|
128
|
+
mode: ToolMode,
|
|
129
|
+
): void {
|
|
130
|
+
const context = createToolContext(figmaClient, restClient, config, mode);
|
|
131
|
+
const singleNodeParamsSchema = getSingleNodeParamsSchema(mode);
|
|
132
|
+
const multiNodeParamsSchema = getMultiNodeParamsSchema(mode);
|
|
133
|
+
|
|
134
|
+
const shouldRegisterWebSocketOnlyTools = mode === "websocket" || mode === "all";
|
|
135
|
+
|
|
136
|
+
// REST API + WebSocket Tools (hybrid)
|
|
137
|
+
// These tools support both REST API and WebSocket modes
|
|
138
|
+
|
|
139
|
+
// Component Info Tool (REST API + WebSocket)
|
|
140
|
+
server.registerTool(
|
|
125
141
|
"get_component_info",
|
|
126
|
-
"Get detailed information about a specific component node in Figma",
|
|
127
142
|
{
|
|
128
|
-
|
|
143
|
+
description: getSingleNodeDescription(
|
|
144
|
+
"Get detailed information about a specific component node in Figma.",
|
|
145
|
+
mode,
|
|
146
|
+
),
|
|
147
|
+
inputSchema: singleNodeParamsSchema,
|
|
129
148
|
},
|
|
130
|
-
async (
|
|
149
|
+
async (params: z.infer<typeof singleNodeBaseSchema>) => {
|
|
131
150
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
})) as GetFileNodesResponse["nodes"][string];
|
|
151
|
+
const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
|
|
152
|
+
const result = await fetchNodeData({ fileKey, nodeId, personalAccessToken }, context);
|
|
135
153
|
|
|
136
154
|
const node = result.document;
|
|
137
155
|
if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") {
|
|
@@ -155,21 +173,26 @@ export function registerTools(
|
|
|
155
173
|
componentPropertyDefinitions: node.componentPropertyDefinitions,
|
|
156
174
|
});
|
|
157
175
|
} catch (error) {
|
|
158
|
-
return formatErrorResponse("
|
|
176
|
+
return formatErrorResponse("get_component_info", error);
|
|
159
177
|
}
|
|
160
178
|
},
|
|
161
179
|
);
|
|
162
180
|
|
|
163
|
-
// Node Info Tool
|
|
164
|
-
server.
|
|
181
|
+
// Node Info Tool (REST API + WebSocket)
|
|
182
|
+
server.registerTool(
|
|
165
183
|
"get_node_info",
|
|
166
|
-
"Get detailed information about a specific node in Figma",
|
|
167
184
|
{
|
|
168
|
-
|
|
185
|
+
description: getSingleNodeDescription(
|
|
186
|
+
"Get detailed information about a specific node in Figma.",
|
|
187
|
+
mode,
|
|
188
|
+
),
|
|
189
|
+
inputSchema: singleNodeParamsSchema,
|
|
169
190
|
},
|
|
170
|
-
async (
|
|
191
|
+
async (params: z.infer<typeof singleNodeBaseSchema>) => {
|
|
171
192
|
try {
|
|
172
|
-
const
|
|
193
|
+
const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
|
|
194
|
+
const result = await fetchNodeData({ fileKey, nodeId, personalAccessToken }, context);
|
|
195
|
+
|
|
173
196
|
const normalizer = createRestNormalizer(result);
|
|
174
197
|
const node = normalizer(result.document);
|
|
175
198
|
|
|
@@ -182,99 +205,105 @@ export function registerTools(
|
|
|
182
205
|
shouldInferVariableName: true,
|
|
183
206
|
});
|
|
184
207
|
const original =
|
|
185
|
-
noInferPipeline.generateCode(node, {
|
|
186
|
-
|
|
187
|
-
})?.jsx ?? "Failed to generate summarized node info";
|
|
208
|
+
noInferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
|
|
209
|
+
"Failed to generate summarized node info";
|
|
188
210
|
const inferred =
|
|
189
|
-
inferPipeline.generateCode(node, {
|
|
190
|
-
|
|
191
|
-
})?.jsx ?? "Failed to generate summarized node info";
|
|
211
|
+
inferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
|
|
212
|
+
"Failed to generate summarized node info";
|
|
192
213
|
|
|
193
214
|
return formatObjectResponse({
|
|
194
|
-
original: {
|
|
195
|
-
|
|
196
|
-
description: "Original Figma node info",
|
|
197
|
-
},
|
|
198
|
-
inferred: {
|
|
199
|
-
data: inferred,
|
|
200
|
-
description: "AutoLayout Inferred Figma node info (fix suggestions)",
|
|
201
|
-
},
|
|
215
|
+
original: { data: original, description: "Original Figma node info" },
|
|
216
|
+
inferred: { data: inferred, description: "AutoLayout Inferred Figma node info" },
|
|
202
217
|
});
|
|
203
218
|
} catch (error) {
|
|
204
219
|
return formatTextResponse(
|
|
205
|
-
`Error in get_node_info: ${formatError(error)}\n\n⚠️
|
|
220
|
+
`Error in get_node_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`,
|
|
206
221
|
);
|
|
207
222
|
}
|
|
208
223
|
},
|
|
209
224
|
);
|
|
210
225
|
|
|
211
|
-
// Nodes Info Tool
|
|
212
|
-
server.
|
|
226
|
+
// Nodes Info Tool (REST API + WebSocket)
|
|
227
|
+
server.registerTool(
|
|
213
228
|
"get_nodes_info",
|
|
214
|
-
"Get detailed information about multiple nodes in Figma",
|
|
215
229
|
{
|
|
216
|
-
|
|
230
|
+
description: getMultiNodeDescription(
|
|
231
|
+
"Get detailed information about multiple nodes in Figma.",
|
|
232
|
+
mode,
|
|
233
|
+
),
|
|
234
|
+
inputSchema: multiNodeParamsSchema,
|
|
217
235
|
},
|
|
218
|
-
async ({ nodeIds }) => {
|
|
236
|
+
async ({ fileKey, nodeIds, personalAccessToken }: z.infer<typeof multiNodeBaseSchema>) => {
|
|
219
237
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const normalizer = createRestNormalizer(result);
|
|
224
|
-
const node = normalizer(result.document);
|
|
225
|
-
|
|
226
|
-
const noInferPipeline = figma.createPipeline({
|
|
227
|
-
shouldInferAutoLayout: false,
|
|
228
|
-
shouldInferVariableName: false,
|
|
229
|
-
});
|
|
230
|
-
const inferPipeline = figma.createPipeline({
|
|
231
|
-
shouldInferAutoLayout: true,
|
|
232
|
-
shouldInferVariableName: true,
|
|
233
|
-
});
|
|
234
|
-
const original =
|
|
235
|
-
noInferPipeline.generateCode(node, {
|
|
236
|
-
shouldPrintSource: true,
|
|
237
|
-
})?.jsx ?? "Failed to generate summarized node info";
|
|
238
|
-
const inferred =
|
|
239
|
-
inferPipeline.generateCode(node, {
|
|
240
|
-
shouldPrintSource: true,
|
|
241
|
-
})?.jsx ?? "Failed to generate summarized node info";
|
|
238
|
+
if (nodeIds.length === 0) {
|
|
239
|
+
return formatErrorResponse("get_nodes_info", new Error("No node IDs provided"));
|
|
240
|
+
}
|
|
242
241
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
data: original,
|
|
247
|
-
description: "Original Figma node info",
|
|
248
|
-
},
|
|
249
|
-
inferred: {
|
|
250
|
-
data: inferred,
|
|
251
|
-
description: "AutoLayout Inferred Figma node info (fix suggestions)",
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
}),
|
|
242
|
+
const nodesData = await fetchMultipleNodesData(
|
|
243
|
+
{ fileKey, nodeIds, personalAccessToken },
|
|
244
|
+
context,
|
|
255
245
|
);
|
|
246
|
+
|
|
247
|
+
const results = nodeIds.map((nodeId) => {
|
|
248
|
+
const nodeData = nodesData[nodeId];
|
|
249
|
+
if (!nodeData) {
|
|
250
|
+
return { nodeId, error: `Node ${nodeId} not found` };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const normalizer = createRestNormalizer(nodeData);
|
|
254
|
+
const node = normalizer(nodeData.document);
|
|
255
|
+
|
|
256
|
+
const noInferPipeline = figma.createPipeline({
|
|
257
|
+
shouldInferAutoLayout: false,
|
|
258
|
+
shouldInferVariableName: false,
|
|
259
|
+
});
|
|
260
|
+
const inferPipeline = figma.createPipeline({
|
|
261
|
+
shouldInferAutoLayout: true,
|
|
262
|
+
shouldInferVariableName: true,
|
|
263
|
+
});
|
|
264
|
+
const original =
|
|
265
|
+
noInferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
|
|
266
|
+
"Failed to generate summarized node info";
|
|
267
|
+
const inferred =
|
|
268
|
+
inferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
|
|
269
|
+
"Failed to generate summarized node info";
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
nodeId,
|
|
273
|
+
original: { data: original, description: "Original Figma node info" },
|
|
274
|
+
inferred: { data: inferred, description: "AutoLayout Inferred Figma node info" },
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
|
|
256
278
|
return formatObjectResponse(results);
|
|
257
279
|
} catch (error) {
|
|
258
280
|
return formatTextResponse(
|
|
259
|
-
`Error in get_nodes_info: ${formatError(error)}\n\n⚠️
|
|
281
|
+
`Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`,
|
|
260
282
|
);
|
|
261
283
|
}
|
|
262
284
|
},
|
|
263
285
|
);
|
|
264
286
|
|
|
265
|
-
|
|
287
|
+
// Get Node React Code Tool (REST API + WebSocket)
|
|
288
|
+
server.registerTool(
|
|
266
289
|
"get_node_react_code",
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
290
|
+
{
|
|
291
|
+
description: getSingleNodeDescription(
|
|
292
|
+
"Get the React code for a specific node in Figma.",
|
|
293
|
+
mode,
|
|
294
|
+
),
|
|
295
|
+
inputSchema: singleNodeParamsSchema,
|
|
296
|
+
},
|
|
297
|
+
async (params: z.infer<typeof singleNodeBaseSchema>) => {
|
|
270
298
|
try {
|
|
271
|
-
const
|
|
272
|
-
const
|
|
299
|
+
const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
|
|
300
|
+
const result = await fetchNodeData({ fileKey, nodeId, personalAccessToken }, context);
|
|
273
301
|
|
|
302
|
+
const normalizer = createRestNormalizer(result);
|
|
274
303
|
const pipeline = react.createPipeline({
|
|
275
304
|
shouldInferAutoLayout: true,
|
|
276
305
|
shouldInferVariableName: true,
|
|
277
|
-
extend,
|
|
306
|
+
extend: context.extend,
|
|
278
307
|
});
|
|
279
308
|
const generated = pipeline.generateCode(normalizer(result.document), {
|
|
280
309
|
shouldPrintSource: false,
|
|
@@ -282,75 +311,216 @@ export function registerTools(
|
|
|
282
311
|
|
|
283
312
|
if (!generated) {
|
|
284
313
|
return formatTextResponse(
|
|
285
|
-
"Failed to generate code\n\n⚠️
|
|
314
|
+
"Failed to generate code\n\n⚠️ Please make sure you have the latest version of the Figma library.",
|
|
286
315
|
);
|
|
287
316
|
}
|
|
288
317
|
|
|
289
318
|
return formatTextResponse(`${generated.imports}\n\n${generated.jsx}`);
|
|
290
319
|
} catch (error) {
|
|
291
320
|
return formatTextResponse(
|
|
292
|
-
`Error in get_node_react_code: ${formatError(error)}\n\n⚠️
|
|
321
|
+
`Error in get_node_react_code: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`,
|
|
293
322
|
);
|
|
294
323
|
}
|
|
295
324
|
},
|
|
296
325
|
);
|
|
297
326
|
|
|
298
|
-
//
|
|
299
|
-
server.tool(
|
|
300
|
-
"export_node_as_image",
|
|
301
|
-
"Export a node as an image from Figma",
|
|
302
|
-
{
|
|
303
|
-
nodeId: z.string().describe("The ID of the node to export"),
|
|
304
|
-
format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
|
|
305
|
-
scale: z.number().positive().optional().describe("Export scale"),
|
|
306
|
-
},
|
|
307
|
-
async ({ nodeId, format, scale }) => {
|
|
308
|
-
try {
|
|
309
|
-
const result = await sendCommandToFigma("export_node_as_image", {
|
|
310
|
-
nodeId,
|
|
311
|
-
format: format || "PNG",
|
|
312
|
-
scale: scale || 1,
|
|
313
|
-
});
|
|
314
|
-
const typedResult = result as { base64: string; mimeType: string };
|
|
315
|
-
return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
|
|
316
|
-
} catch (error) {
|
|
317
|
-
return formatErrorResponse("export_node_as_image", error);
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
);
|
|
327
|
+
// Utility Tools (No Figma connection required)
|
|
321
328
|
|
|
322
329
|
// Retrieve Color Variable Names Tool
|
|
323
|
-
server.
|
|
330
|
+
server.registerTool(
|
|
324
331
|
"retrieve_color_variable_names",
|
|
325
|
-
"Retrieve available color variable names in scope",
|
|
326
332
|
{
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
333
|
+
description:
|
|
334
|
+
"Retrieve available SEED Design color variable names by scope. No Figma connection required.",
|
|
335
|
+
inputSchema: z.object({
|
|
336
|
+
scope: z
|
|
337
|
+
.enum(["fg", "bg", "stroke", "palette"])
|
|
338
|
+
.array()
|
|
339
|
+
.describe("The scope of the color variable names to retrieve"),
|
|
340
|
+
}),
|
|
331
341
|
},
|
|
332
342
|
async ({ scope }) => {
|
|
333
343
|
try {
|
|
334
344
|
const result = getFigmaColorVariableNames(scope);
|
|
345
|
+
|
|
335
346
|
return formatObjectResponse(result);
|
|
336
347
|
} catch (error) {
|
|
337
348
|
return formatErrorResponse("retrieve_color_variable_names", error);
|
|
338
349
|
}
|
|
339
350
|
},
|
|
340
351
|
);
|
|
352
|
+
|
|
353
|
+
if (shouldRegisterWebSocketOnlyTools) {
|
|
354
|
+
// WebSocket Only Tools
|
|
355
|
+
|
|
356
|
+
server.registerTool(
|
|
357
|
+
"join_channel",
|
|
358
|
+
{
|
|
359
|
+
description: "Join a specific channel to communicate with Figma (WebSocket mode only)",
|
|
360
|
+
inputSchema: z.object({
|
|
361
|
+
channel: z.string().describe("The name of the channel to join").default(""),
|
|
362
|
+
}),
|
|
363
|
+
},
|
|
364
|
+
async ({ channel }) => {
|
|
365
|
+
try {
|
|
366
|
+
if (!figmaClient)
|
|
367
|
+
return formatErrorResponse(
|
|
368
|
+
"join_channel",
|
|
369
|
+
new Error("WebSocket not available. This tool requires Figma Plugin connection."),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (!channel)
|
|
373
|
+
// If no channel provided, ask the user for input
|
|
374
|
+
return {
|
|
375
|
+
...formatTextResponse("Please provide a channel name to join:"),
|
|
376
|
+
followUp: {
|
|
377
|
+
tool: "join_channel",
|
|
378
|
+
description: "Join the specified channel",
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
await figmaClient.joinChannel(channel);
|
|
383
|
+
|
|
384
|
+
return formatTextResponse(`Successfully joined channel: ${channel}`);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
return formatErrorResponse("join_channel", error);
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Document Info Tool
|
|
392
|
+
server.registerTool(
|
|
393
|
+
"get_document_info",
|
|
394
|
+
{
|
|
395
|
+
description:
|
|
396
|
+
"Get detailed information about the current Figma document (WebSocket mode only)",
|
|
397
|
+
},
|
|
398
|
+
async () => {
|
|
399
|
+
try {
|
|
400
|
+
requireWebSocket(context);
|
|
401
|
+
const result = await context.sendCommandToFigma("get_document_info");
|
|
402
|
+
|
|
403
|
+
return formatObjectResponse(result);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
return formatErrorResponse("get_document_info", error);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Selection Tool
|
|
411
|
+
server.registerTool(
|
|
412
|
+
"get_selection",
|
|
413
|
+
{
|
|
414
|
+
description: "Get information about the current selection in Figma (WebSocket mode only)",
|
|
415
|
+
},
|
|
416
|
+
async () => {
|
|
417
|
+
try {
|
|
418
|
+
requireWebSocket(context);
|
|
419
|
+
const result = await context.sendCommandToFigma("get_selection");
|
|
420
|
+
|
|
421
|
+
return formatObjectResponse(result);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return formatErrorResponse("get_selection", error);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Annotation Tool
|
|
429
|
+
server.registerTool(
|
|
430
|
+
"add_annotations",
|
|
431
|
+
{
|
|
432
|
+
description: "Add annotations to multiple nodes in Figma (WebSocket mode only)",
|
|
433
|
+
inputSchema: z.object({
|
|
434
|
+
annotations: z.array(
|
|
435
|
+
z.object({
|
|
436
|
+
nodeId: z.string().describe("The ID of the node to add an annotation to"),
|
|
437
|
+
labelMarkdown: z
|
|
438
|
+
.string()
|
|
439
|
+
.describe("The markdown label for the annotation, do not escape newlines"),
|
|
440
|
+
}),
|
|
441
|
+
),
|
|
442
|
+
}),
|
|
443
|
+
},
|
|
444
|
+
async ({ annotations }) => {
|
|
445
|
+
try {
|
|
446
|
+
requireWebSocket(context);
|
|
447
|
+
await context.sendCommandToFigma("add_annotations", { annotations });
|
|
448
|
+
|
|
449
|
+
return formatTextResponse(
|
|
450
|
+
`Annotations added to nodes ${annotations.map((annotation) => annotation.nodeId).join(", ")}`,
|
|
451
|
+
);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return formatErrorResponse("add_annotations", error);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Get Annotations Tool
|
|
459
|
+
server.registerTool(
|
|
460
|
+
"get_annotations",
|
|
461
|
+
{
|
|
462
|
+
description: "Get annotations for a specific node in Figma (WebSocket mode only)",
|
|
463
|
+
inputSchema: z.object({
|
|
464
|
+
nodeId: z.string().describe("The ID of the node to get annotations for"),
|
|
465
|
+
}),
|
|
466
|
+
},
|
|
467
|
+
async ({ nodeId }) => {
|
|
468
|
+
try {
|
|
469
|
+
requireWebSocket(context);
|
|
470
|
+
const result = await context.sendCommandToFigma("get_annotations", { nodeId });
|
|
471
|
+
|
|
472
|
+
return formatObjectResponse(result);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
return formatErrorResponse("get_annotations", error);
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Export Node as Image Tool
|
|
480
|
+
server.registerTool(
|
|
481
|
+
"export_node_as_image",
|
|
482
|
+
{
|
|
483
|
+
description: "Export a node as an image from Figma (WebSocket mode only)",
|
|
484
|
+
inputSchema: z.object({
|
|
485
|
+
nodeId: z.string().describe("The ID of the node to export"),
|
|
486
|
+
format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
|
|
487
|
+
scale: z.number().positive().optional().describe("Export scale"),
|
|
488
|
+
}),
|
|
489
|
+
},
|
|
490
|
+
async ({ nodeId, format, scale }) => {
|
|
491
|
+
try {
|
|
492
|
+
requireWebSocket(context);
|
|
493
|
+
const result = await context.sendCommandToFigma("export_node_as_image", {
|
|
494
|
+
nodeId,
|
|
495
|
+
format: format || "PNG",
|
|
496
|
+
scale: scale || 1,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const typedResult = result as { base64: string; mimeType: string };
|
|
500
|
+
return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return formatErrorResponse("export_node_as_image", error);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
}
|
|
341
507
|
}
|
|
342
508
|
|
|
509
|
+
// editing tools require WebSocket client
|
|
510
|
+
|
|
343
511
|
export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSocketClient): void {
|
|
344
512
|
const { sendCommandToFigma } = figmaClient;
|
|
345
513
|
|
|
346
514
|
// Clone Node Tool
|
|
347
|
-
server.
|
|
515
|
+
server.registerTool(
|
|
348
516
|
"clone_node",
|
|
349
|
-
"Clone an existing node in Figma",
|
|
350
517
|
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
518
|
+
description: "Clone an existing node in Figma (WebSocket mode only)",
|
|
519
|
+
inputSchema: z.object({
|
|
520
|
+
nodeId: z.string().describe("The ID of the node to clone"),
|
|
521
|
+
x: z.number().optional().describe("New X position for the clone"),
|
|
522
|
+
y: z.number().optional().describe("New Y position for the clone"),
|
|
523
|
+
}),
|
|
354
524
|
},
|
|
355
525
|
async ({ nodeId, x, y }) => {
|
|
356
526
|
try {
|
|
@@ -362,6 +532,7 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
362
532
|
y?: number;
|
|
363
533
|
success: boolean;
|
|
364
534
|
};
|
|
535
|
+
|
|
365
536
|
return formatTextResponse(
|
|
366
537
|
`Cloned node with new ID: ${typedResult.id}${x !== undefined && y !== undefined ? ` at position (${x}, ${y})` : ""}`,
|
|
367
538
|
);
|
|
@@ -371,20 +542,23 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
371
542
|
},
|
|
372
543
|
);
|
|
373
544
|
|
|
374
|
-
server.
|
|
545
|
+
server.registerTool(
|
|
375
546
|
"set_fill_color",
|
|
376
|
-
"Set the fill color of a node",
|
|
377
547
|
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
.string()
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
548
|
+
description: "Set the fill color of a node (WebSocket mode only)",
|
|
549
|
+
inputSchema: z.object({
|
|
550
|
+
nodeId: z.string().describe("The ID of the node to set the fill color of"),
|
|
551
|
+
colorToken: z
|
|
552
|
+
.string()
|
|
553
|
+
.describe(
|
|
554
|
+
"The color token to set the fill color to. Format: `{category}/{name}`. Example: `bg/brand`",
|
|
555
|
+
),
|
|
556
|
+
}),
|
|
384
557
|
},
|
|
385
558
|
async ({ nodeId, colorToken }) => {
|
|
386
559
|
try {
|
|
387
560
|
await sendCommandToFigma("set_fill_color", { nodeId, colorToken });
|
|
561
|
+
|
|
388
562
|
return formatTextResponse(`Fill color set to ${colorToken}`);
|
|
389
563
|
} catch (error) {
|
|
390
564
|
return formatErrorResponse("set_fill_color", error);
|
|
@@ -392,20 +566,23 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
392
566
|
},
|
|
393
567
|
);
|
|
394
568
|
|
|
395
|
-
server.
|
|
569
|
+
server.registerTool(
|
|
396
570
|
"set_stroke_color",
|
|
397
|
-
"Set the stroke color of a node",
|
|
398
571
|
{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
.string()
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
572
|
+
description: "Set the stroke color of a node (WebSocket mode only)",
|
|
573
|
+
inputSchema: z.object({
|
|
574
|
+
nodeId: z.string().describe("The ID of the node to set the stroke color of"),
|
|
575
|
+
colorToken: z
|
|
576
|
+
.string()
|
|
577
|
+
.describe(
|
|
578
|
+
"The color token to set the stroke color to. Format: `{category}/{name}`. Example: `stroke/neutral`",
|
|
579
|
+
),
|
|
580
|
+
}),
|
|
405
581
|
},
|
|
406
582
|
async ({ nodeId, colorToken }) => {
|
|
407
583
|
try {
|
|
408
584
|
await sendCommandToFigma("set_stroke_color", { nodeId, colorToken });
|
|
585
|
+
|
|
409
586
|
return formatTextResponse(`Stroke color set to ${colorToken}`);
|
|
410
587
|
} catch (error) {
|
|
411
588
|
return formatErrorResponse("set_stroke_color", error);
|
|
@@ -413,31 +590,39 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
413
590
|
},
|
|
414
591
|
);
|
|
415
592
|
|
|
416
|
-
server.
|
|
593
|
+
server.registerTool(
|
|
417
594
|
"set_auto_layout",
|
|
418
|
-
"Set the auto layout of a node",
|
|
419
595
|
{
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
.number()
|
|
439
|
-
.optional()
|
|
440
|
-
|
|
596
|
+
description: "Set the auto layout of a node (WebSocket mode only)",
|
|
597
|
+
inputSchema: z.object({
|
|
598
|
+
nodeId: z.string().describe("The ID of the node to set the auto layout of"),
|
|
599
|
+
layoutMode: z
|
|
600
|
+
.enum(["HORIZONTAL", "VERTICAL"])
|
|
601
|
+
.optional()
|
|
602
|
+
.describe("The layout mode to set"),
|
|
603
|
+
layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("The layout wrap to set"),
|
|
604
|
+
primaryAxisAlignItems: z
|
|
605
|
+
.enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"])
|
|
606
|
+
.optional()
|
|
607
|
+
.describe("The primary axis align items to set"),
|
|
608
|
+
counterAxisAlignItems: z
|
|
609
|
+
.enum(["MIN", "MAX", "CENTER", "BASELINE"])
|
|
610
|
+
.optional()
|
|
611
|
+
.describe("The counter axis align items to set"),
|
|
612
|
+
itemSpacing: z.number().optional().describe("The item spacing to set"),
|
|
613
|
+
horizontalPadding: z.number().optional().describe("The horizontal padding to set"),
|
|
614
|
+
verticalPadding: z.number().optional().describe("The vertical padding to set"),
|
|
615
|
+
paddingLeft: z.number().optional().describe("The padding left to set (when left != right)"),
|
|
616
|
+
paddingRight: z
|
|
617
|
+
.number()
|
|
618
|
+
.optional()
|
|
619
|
+
.describe("The padding right to set (when left != right)"),
|
|
620
|
+
paddingTop: z.number().optional().describe("The padding top to set (when top != bottom)"),
|
|
621
|
+
paddingBottom: z
|
|
622
|
+
.number()
|
|
623
|
+
.optional()
|
|
624
|
+
.describe("The padding bottom to set (when top != bottom)"),
|
|
625
|
+
}),
|
|
441
626
|
},
|
|
442
627
|
async ({
|
|
443
628
|
nodeId,
|
|
@@ -468,6 +653,7 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
468
653
|
paddingTop,
|
|
469
654
|
paddingBottom,
|
|
470
655
|
});
|
|
656
|
+
|
|
471
657
|
return formatTextResponse(`Layout set to ${layoutMode}`);
|
|
472
658
|
} catch (error) {
|
|
473
659
|
return formatErrorResponse("set_auto_layout", error);
|
|
@@ -475,21 +661,23 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
475
661
|
},
|
|
476
662
|
);
|
|
477
663
|
|
|
478
|
-
server.
|
|
664
|
+
server.registerTool(
|
|
479
665
|
"set_size",
|
|
480
|
-
"Set the size of a node",
|
|
481
666
|
{
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
667
|
+
description: "Set the size of a node (WebSocket mode only)",
|
|
668
|
+
inputSchema: z.object({
|
|
669
|
+
nodeId: z.string().describe("The ID of the node to set the size of"),
|
|
670
|
+
layoutSizingHorizontal: z
|
|
671
|
+
.enum(["HUG", "FILL"])
|
|
672
|
+
.optional()
|
|
673
|
+
.describe("The horizontal layout sizing to set (exclusive with width)"),
|
|
674
|
+
layoutSizingVertical: z
|
|
675
|
+
.enum(["HUG", "FILL"])
|
|
676
|
+
.optional()
|
|
677
|
+
.describe("The vertical layout sizing to set (exclusive with height)"),
|
|
678
|
+
width: z.number().optional().describe("The width to set (raw value)"),
|
|
679
|
+
height: z.number().optional().describe("The height to set (raw value)"),
|
|
680
|
+
}),
|
|
493
681
|
},
|
|
494
682
|
async ({ nodeId, layoutSizingHorizontal, layoutSizingVertical, width, height }) => {
|
|
495
683
|
try {
|
|
@@ -500,6 +688,7 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
|
|
|
500
688
|
width,
|
|
501
689
|
height,
|
|
502
690
|
});
|
|
691
|
+
|
|
503
692
|
return formatTextResponse(
|
|
504
693
|
`Size set to ${width ?? layoutSizingHorizontal}x${height ?? layoutSizingVertical}`,
|
|
505
694
|
);
|