@atezer/figma-mcp-bridge 1.1.2 → 1.2.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/CHANGELOG.md +82 -0
- package/LICENSE +0 -11
- package/README.md +107 -10
- package/dist/cloudflare/core/config.js +1 -1
- package/dist/cloudflare/core/figma-tools.js +290 -254
- package/dist/cloudflare/core/figma-url.js +48 -0
- package/dist/cloudflare/core/plugin-bridge-connector.js +52 -43
- package/dist/cloudflare/core/plugin-bridge-server.js +247 -75
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +290 -254
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/figma-url.d.ts +10 -0
- package/dist/core/figma-url.d.ts.map +1 -0
- package/dist/core/figma-url.js +49 -0
- package/dist/core/figma-url.js.map +1 -0
- package/dist/core/plugin-bridge-connector.d.ts +6 -1
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js +52 -43
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/core/plugin-bridge-server.d.ts +49 -12
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +247 -75
- package/dist/core/plugin-bridge-server.js.map +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +391 -139
- package/dist/local-plugin-only.js.map +1 -1
- package/dist/local.js +323 -183
- package/dist/local.js.map +1 -1
- package/f-mcp-plugin/README.md +5 -5
- package/f-mcp-plugin/code.js +243 -2
- package/f-mcp-plugin/manifest.json +3 -1
- package/f-mcp-plugin/ui.html +359 -64
- package/package.json +8 -8
|
@@ -16,7 +16,30 @@ import { getConfig } from "./core/config.js";
|
|
|
16
16
|
import { createChildLogger } from "./core/logger.js";
|
|
17
17
|
import { PluginBridgeServer } from "./core/plugin-bridge-server.js";
|
|
18
18
|
import { PluginBridgeConnector } from "./core/plugin-bridge-connector.js";
|
|
19
|
+
import { parseFigmaUrl } from "./core/figma-url.js";
|
|
19
20
|
const logger = createChildLogger({ component: "plugin-only-mcp" });
|
|
21
|
+
/** Resolve fileKey from figmaUrl (parse) or explicit fileKey. Returns undefined if neither yields a key. */
|
|
22
|
+
function resolveFileKey(figmaUrl, explicitFileKey) {
|
|
23
|
+
if (explicitFileKey && explicitFileKey.trim())
|
|
24
|
+
return explicitFileKey.trim();
|
|
25
|
+
if (figmaUrl) {
|
|
26
|
+
const parsed = parseFigmaUrl(figmaUrl);
|
|
27
|
+
if (parsed?.fileKey)
|
|
28
|
+
return parsed.fileKey;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/** For figma_get_design_context: resolve fileKey and nodeId from figmaUrl or explicit params. */
|
|
33
|
+
function resolveDesignContextParams(params) {
|
|
34
|
+
const fileKey = resolveFileKey(params.figmaUrl, params.fileKey);
|
|
35
|
+
let nodeId = params.nodeId?.trim();
|
|
36
|
+
if (!nodeId && params.figmaUrl) {
|
|
37
|
+
const parsed = parseFigmaUrl(params.figmaUrl);
|
|
38
|
+
if (parsed?.nodeId)
|
|
39
|
+
nodeId = parsed.nodeId;
|
|
40
|
+
}
|
|
41
|
+
return { fileKey, nodeId: nodeId || undefined };
|
|
42
|
+
}
|
|
20
43
|
function rgbaToHex(color) {
|
|
21
44
|
if (!color || typeof color !== "object")
|
|
22
45
|
return "";
|
|
@@ -43,10 +66,19 @@ function normalizeForCompare(s) {
|
|
|
43
66
|
return s.replace(/\s/g, "");
|
|
44
67
|
}
|
|
45
68
|
const PLUGIN_NOT_CONNECTED = "F-MCP ATezer Bridge plugin not connected. Open Figma → Plugins → Development → F-MCP ATezer Bridge, wait for 'ready'.";
|
|
46
|
-
function getConnector(bridge) {
|
|
47
|
-
if (!bridge.isConnected())
|
|
69
|
+
function getConnector(bridge, fileKey) {
|
|
70
|
+
if (!bridge.isConnected(fileKey)) {
|
|
71
|
+
if (fileKey) {
|
|
72
|
+
const connected = bridge.listConnectedFiles();
|
|
73
|
+
const fileList = connected.length > 0
|
|
74
|
+
? ` Connected files: ${connected.map(f => `${f.fileName || "?"} (${f.fileKey || "?"})`).join(", ")}`
|
|
75
|
+
: "";
|
|
76
|
+
throw new Error(`No plugin connected for fileKey "${fileKey}".${fileList} ` +
|
|
77
|
+
"Open the target file in Figma and run the F-MCP ATezer Bridge plugin.");
|
|
78
|
+
}
|
|
48
79
|
throw new Error(PLUGIN_NOT_CONNECTED);
|
|
49
|
-
|
|
80
|
+
}
|
|
81
|
+
return new PluginBridgeConnector(bridge, fileKey);
|
|
50
82
|
}
|
|
51
83
|
export async function main() {
|
|
52
84
|
const config = getConfig();
|
|
@@ -56,20 +88,54 @@ export async function main() {
|
|
|
56
88
|
bridge.start();
|
|
57
89
|
const server = new McpServer({
|
|
58
90
|
name: "F-MCP ATezer Bridge (Plugin-only)",
|
|
59
|
-
version: "1.
|
|
91
|
+
version: "1.1.2",
|
|
92
|
+
});
|
|
93
|
+
// ---- figma_list_connected_files (multi-client discovery) ----
|
|
94
|
+
server.registerTool("figma_list_connected_files", {
|
|
95
|
+
description: "List all currently connected Figma/FigJam plugin instances (Figma Desktop, FigJam browser, Figma browser). Returns fileKey, fileName, and connection time for each. Use when multiple windows or agents are active. Pass the returned fileKey (or a Figma/FigJam URL via figmaUrl) to other tools to target a specific file.",
|
|
96
|
+
inputSchema: {},
|
|
97
|
+
annotations: { readOnlyHint: true },
|
|
98
|
+
}, async () => {
|
|
99
|
+
const files = bridge.listConnectedFiles();
|
|
100
|
+
return {
|
|
101
|
+
content: [{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: JSON.stringify({
|
|
104
|
+
success: true,
|
|
105
|
+
connectedFiles: files,
|
|
106
|
+
totalConnections: files.length,
|
|
107
|
+
message: files.length === 0
|
|
108
|
+
? "No plugins connected. Open Figma and run the F-MCP ATezer Bridge plugin."
|
|
109
|
+
: `${files.length} plugin(s) connected. Use fileKey parameter in other tools to target a specific file.`,
|
|
110
|
+
}, null, 0),
|
|
111
|
+
}],
|
|
112
|
+
};
|
|
60
113
|
});
|
|
61
114
|
// ---- figma_get_file_data_plugin (no REST, no token) ----
|
|
62
|
-
server.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
115
|
+
server.registerTool("figma_get_file_data", {
|
|
116
|
+
description: "Get file structure and document tree from the open Figma file. No REST API or token. Use fileKey or figmaUrl to target a specific file when multiple plugins are connected (Figma Desktop, FigJam browser, Figma browser). Pass a Figma/FigJam URL in figmaUrl to route by link.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey is extracted from the link for routing."),
|
|
119
|
+
fileKey: z.string().optional().describe("Target a specific connected file. Use figma_list_connected_files to see available files."),
|
|
120
|
+
depth: z.number().min(0).max(3).optional().default(1),
|
|
121
|
+
verbosity: z.enum(["summary", "standard", "full"]).optional().default("summary"),
|
|
122
|
+
includeLayout: z.boolean().optional(),
|
|
123
|
+
includeVisual: z.boolean().optional(),
|
|
124
|
+
includeTypography: z.boolean().optional(),
|
|
125
|
+
includeCodeReady: z.boolean().optional(),
|
|
126
|
+
outputHint: z.enum(["react", "tailwind"]).optional(),
|
|
127
|
+
},
|
|
128
|
+
annotations: { readOnlyHint: true },
|
|
129
|
+
}, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
|
|
71
130
|
try {
|
|
72
|
-
const
|
|
131
|
+
const resolvedKey = resolveFileKey(figmaUrl, fileKey);
|
|
132
|
+
if (figmaUrl && !resolvedKey) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }, null, 0) }],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const conn = getConnector(bridge, resolvedKey);
|
|
73
139
|
const opts = includeLayout !== undefined ||
|
|
74
140
|
includeVisual !== undefined ||
|
|
75
141
|
includeTypography !== undefined ||
|
|
@@ -94,28 +160,43 @@ export async function main() {
|
|
|
94
160
|
}
|
|
95
161
|
});
|
|
96
162
|
// ---- figma_get_design_context (get_design_context tarzı, token tasarruflu, Figma token yok) ----
|
|
97
|
-
server.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
163
|
+
server.registerTool("figma_get_design_context", {
|
|
164
|
+
description: "Design context for a node or whole file: structure + text, layout/visual/typography. Use fileKey or figmaUrl to target a file when multiple plugins are connected. Pass a Figma/FigJam URL in figmaUrl; fileKey and node-id (if present in the link) are extracted automatically.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey and optional node-id are extracted for routing."),
|
|
167
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
168
|
+
nodeId: z.string().optional(),
|
|
169
|
+
depth: z.number().min(0).max(3).optional().default(2),
|
|
170
|
+
verbosity: z.enum(["summary", "standard", "full"]).optional().default("standard"),
|
|
171
|
+
excludeScreenshot: z.boolean().optional(),
|
|
172
|
+
includeLayout: z.boolean().optional(),
|
|
173
|
+
includeVisual: z.boolean().optional(),
|
|
174
|
+
includeTypography: z.boolean().optional(),
|
|
175
|
+
includeCodeReady: z.boolean().optional(),
|
|
176
|
+
outputHint: z.enum(["react", "tailwind"]).optional(),
|
|
177
|
+
},
|
|
178
|
+
annotations: { readOnlyHint: true },
|
|
179
|
+
}, async ({ figmaUrl, fileKey, nodeId, depth, verbosity, excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
|
|
108
180
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
181
|
+
const { fileKey: resolvedKey, nodeId: resolvedNodeId } = resolveDesignContextParams({ figmaUrl, fileKey, nodeId });
|
|
182
|
+
if (figmaUrl && !resolvedKey) {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }, null, 0) }],
|
|
185
|
+
isError: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const conn = getConnector(bridge, resolvedKey);
|
|
189
|
+
const opts = excludeScreenshot !== undefined ||
|
|
190
|
+
includeLayout !== undefined ||
|
|
111
191
|
includeVisual !== undefined ||
|
|
112
192
|
includeTypography !== undefined ||
|
|
113
193
|
includeCodeReady !== undefined ||
|
|
114
194
|
outputHint !== undefined
|
|
115
|
-
? { includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
|
|
195
|
+
? { excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
|
|
116
196
|
: undefined;
|
|
117
|
-
const
|
|
118
|
-
|
|
197
|
+
const effectiveNodeId = resolvedNodeId ?? nodeId?.trim();
|
|
198
|
+
const data = effectiveNodeId
|
|
199
|
+
? await conn.getNodeContext(effectiveNodeId, depth, verbosity, opts)
|
|
119
200
|
: await conn.getDocumentStructure(depth, verbosity, opts);
|
|
120
201
|
const text = data === undefined || data === null
|
|
121
202
|
? JSON.stringify({ success: false, error: "No data from plugin" })
|
|
@@ -133,10 +214,16 @@ export async function main() {
|
|
|
133
214
|
}
|
|
134
215
|
});
|
|
135
216
|
// ---- figma_get_variables (plugin only, token-friendly default) ----
|
|
136
|
-
server.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
217
|
+
server.registerTool("figma_get_variables", {
|
|
218
|
+
description: "Get design tokens and variables from the open Figma file. No REST API or token. Use fileKey or figmaUrl to target a specific file when multiple plugins are connected.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
221
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
222
|
+
verbosity: z.enum(["inventory", "summary", "standard", "full"]).optional().default("summary"),
|
|
223
|
+
},
|
|
224
|
+
annotations: { readOnlyHint: true },
|
|
225
|
+
}, async ({ figmaUrl, fileKey, verbosity }) => {
|
|
226
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
140
227
|
const raw = await conn.getVariablesFromPluginUI();
|
|
141
228
|
if (!raw || !raw.variables) {
|
|
142
229
|
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "Variables not loaded" }) }] };
|
|
@@ -162,101 +249,173 @@ export async function main() {
|
|
|
162
249
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
|
|
163
250
|
});
|
|
164
251
|
// ---- figma_get_component ----
|
|
165
|
-
server.
|
|
166
|
-
|
|
252
|
+
server.registerTool("figma_get_component", {
|
|
253
|
+
description: "Get component metadata by node ID from the open Figma file. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
256
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
257
|
+
nodeId: z.string(),
|
|
258
|
+
},
|
|
259
|
+
annotations: { readOnlyHint: true },
|
|
260
|
+
}, async ({ figmaUrl, fileKey, nodeId }) => {
|
|
261
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
167
262
|
const result = await conn.getComponentFromPluginUI(nodeId);
|
|
168
263
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
169
264
|
});
|
|
170
265
|
// ---- figma_get_styles (plugin only) ----
|
|
171
|
-
server.
|
|
172
|
-
|
|
266
|
+
server.registerTool("figma_get_styles", {
|
|
267
|
+
description: "Get local paint, text, and effect styles from the open Figma file. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
268
|
+
inputSchema: {
|
|
269
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
270
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
271
|
+
verbosity: z.enum(["summary", "full"]).optional().default("summary"),
|
|
272
|
+
},
|
|
273
|
+
annotations: { readOnlyHint: true },
|
|
274
|
+
}, async ({ figmaUrl, fileKey, verbosity }) => {
|
|
275
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
173
276
|
const data = await conn.getLocalStyles(verbosity);
|
|
174
277
|
return { content: [{ type: "text", text: JSON.stringify(data || {}, null, 0) }] };
|
|
175
278
|
});
|
|
176
279
|
// ---- figma_execute ----
|
|
177
|
-
server.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
280
|
+
server.registerTool("figma_execute", {
|
|
281
|
+
description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
284
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
285
|
+
code: z.string(),
|
|
286
|
+
timeout: z.number().optional().default(5000),
|
|
287
|
+
},
|
|
288
|
+
annotations: { destructiveHint: true },
|
|
289
|
+
}, async ({ figmaUrl, fileKey, code, timeout }) => {
|
|
290
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
182
291
|
const result = await conn.executeCodeViaUI(code, timeout);
|
|
183
292
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
184
293
|
});
|
|
185
294
|
// ---- figma_capture_screenshot ----
|
|
186
|
-
server.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
295
|
+
server.registerTool("figma_capture_screenshot", {
|
|
296
|
+
description: "Capture screenshot of a node or current view from the plugin. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
299
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
300
|
+
nodeId: z.string().optional(),
|
|
301
|
+
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
302
|
+
scale: z.number().optional().default(2),
|
|
303
|
+
},
|
|
304
|
+
annotations: { readOnlyHint: true },
|
|
305
|
+
}, async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
|
|
306
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
192
307
|
const result = await conn.captureScreenshot(nodeId ?? null, { format, scale });
|
|
193
308
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
194
309
|
});
|
|
195
310
|
// ---- figma_set_instance_properties ----
|
|
196
|
-
server.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
311
|
+
server.registerTool("figma_set_instance_properties", {
|
|
312
|
+
description: "Set component instance properties (TEXT, BOOLEAN, VARIANT, etc.). Use fileKey or figmaUrl to target a specific file.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
315
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
316
|
+
nodeId: z.string(),
|
|
317
|
+
properties: z.record(z.union([z.string(), z.boolean()])),
|
|
318
|
+
},
|
|
319
|
+
annotations: { destructiveHint: true },
|
|
320
|
+
}, async ({ figmaUrl, fileKey, nodeId, properties }) => {
|
|
321
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
201
322
|
const result = await conn.setInstanceProperties(nodeId, properties);
|
|
202
323
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
203
324
|
});
|
|
204
325
|
// ---- Variable CRUD ----
|
|
205
|
-
server.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
326
|
+
server.registerTool("figma_update_variable", {
|
|
327
|
+
description: "Update a variable value in a mode. Get IDs from figma_get_variables.",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
variableId: z.string(),
|
|
330
|
+
modeId: z.string(),
|
|
331
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
332
|
+
},
|
|
333
|
+
annotations: { destructiveHint: true },
|
|
209
334
|
}, async (p) => {
|
|
210
335
|
const conn = getConnector(bridge);
|
|
211
336
|
const result = await conn.updateVariable(p.variableId, p.modeId, p.value);
|
|
212
337
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
213
338
|
});
|
|
214
|
-
server.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
339
|
+
server.registerTool("figma_create_variable", {
|
|
340
|
+
description: "Create a variable in a collection. Get collectionId from figma_get_variables.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
name: z.string(),
|
|
343
|
+
collectionId: z.string(),
|
|
344
|
+
resolvedType: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]),
|
|
345
|
+
options: z.record(z.any()).optional(),
|
|
346
|
+
},
|
|
347
|
+
annotations: { destructiveHint: true },
|
|
219
348
|
}, async (p) => {
|
|
220
349
|
const conn = getConnector(bridge);
|
|
221
350
|
const result = await conn.createVariable(p.name, p.collectionId, p.resolvedType, p.options);
|
|
222
351
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
223
352
|
});
|
|
224
|
-
server.
|
|
353
|
+
server.registerTool("figma_create_variable_collection", {
|
|
354
|
+
description: "Create a variable collection.",
|
|
355
|
+
inputSchema: { name: z.string(), options: z.record(z.any()).optional() },
|
|
356
|
+
annotations: { destructiveHint: true },
|
|
357
|
+
}, async (p) => {
|
|
225
358
|
const conn = getConnector(bridge);
|
|
226
359
|
const result = await conn.createVariableCollection(p.name, p.options);
|
|
227
360
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
228
361
|
});
|
|
229
|
-
server.
|
|
362
|
+
server.registerTool("figma_delete_variable", {
|
|
363
|
+
description: "Delete a variable.",
|
|
364
|
+
inputSchema: { variableId: z.string() },
|
|
365
|
+
annotations: { destructiveHint: true },
|
|
366
|
+
}, async (p) => {
|
|
230
367
|
const conn = getConnector(bridge);
|
|
231
368
|
const result = await conn.deleteVariable(p.variableId);
|
|
232
369
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
233
370
|
});
|
|
234
|
-
server.
|
|
371
|
+
server.registerTool("figma_delete_variable_collection", {
|
|
372
|
+
description: "Delete a variable collection.",
|
|
373
|
+
inputSchema: { collectionId: z.string() },
|
|
374
|
+
annotations: { destructiveHint: true },
|
|
375
|
+
}, async (p) => {
|
|
235
376
|
const conn = getConnector(bridge);
|
|
236
377
|
const result = await conn.deleteVariableCollection(p.collectionId);
|
|
237
378
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
238
379
|
});
|
|
239
|
-
server.
|
|
380
|
+
server.registerTool("figma_rename_variable", {
|
|
381
|
+
description: "Rename a variable.",
|
|
382
|
+
inputSchema: { variableId: z.string(), newName: z.string() },
|
|
383
|
+
annotations: { destructiveHint: true },
|
|
384
|
+
}, async (p) => {
|
|
240
385
|
const conn = getConnector(bridge);
|
|
241
386
|
const result = await conn.renameVariable(p.variableId, p.newName);
|
|
242
387
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
243
388
|
});
|
|
244
|
-
server.
|
|
389
|
+
server.registerTool("figma_add_mode", {
|
|
390
|
+
description: "Add a mode to a collection.",
|
|
391
|
+
inputSchema: { collectionId: z.string(), modeName: z.string() },
|
|
392
|
+
annotations: { destructiveHint: true },
|
|
393
|
+
}, async (p) => {
|
|
245
394
|
const conn = getConnector(bridge);
|
|
246
395
|
const result = await conn.addMode(p.collectionId, p.modeName);
|
|
247
396
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
248
397
|
});
|
|
249
|
-
server.
|
|
398
|
+
server.registerTool("figma_rename_mode", {
|
|
399
|
+
description: "Rename a mode in a collection.",
|
|
400
|
+
inputSchema: { collectionId: z.string(), modeId: z.string(), newName: z.string() },
|
|
401
|
+
annotations: { destructiveHint: true },
|
|
402
|
+
}, async (p) => {
|
|
250
403
|
const conn = getConnector(bridge);
|
|
251
404
|
const result = await conn.renameMode(p.collectionId, p.modeId, p.newName);
|
|
252
405
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
253
406
|
});
|
|
254
407
|
// ---- Design system summary (minimal tokens) ----
|
|
255
|
-
server.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
408
|
+
server.registerTool("figma_get_design_system_summary", {
|
|
409
|
+
description: "Get a compact overview: variable collection names and component counts. Minimal tokens. Use fileKey or figmaUrl to target a specific file.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
412
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
413
|
+
currentPageOnly: z.boolean().optional().default(true),
|
|
414
|
+
limit: z.number().min(0).optional(),
|
|
415
|
+
},
|
|
416
|
+
annotations: { readOnlyHint: true },
|
|
417
|
+
}, async ({ figmaUrl, fileKey, currentPageOnly, limit }) => {
|
|
418
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
260
419
|
const [vars, components] = await Promise.all([
|
|
261
420
|
conn.getVariablesFromPluginUI(),
|
|
262
421
|
conn.getLocalComponents({ currentPageOnly, limit }),
|
|
@@ -273,12 +432,18 @@ export async function main() {
|
|
|
273
432
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
|
|
274
433
|
});
|
|
275
434
|
// ---- figma_search_components ----
|
|
276
|
-
server.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
435
|
+
server.registerTool("figma_search_components", {
|
|
436
|
+
description: "Search local components by name. Returns nodeIds and names. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
437
|
+
inputSchema: {
|
|
438
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
439
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
440
|
+
query: z.string().optional(),
|
|
441
|
+
currentPageOnly: z.boolean().optional().default(true),
|
|
442
|
+
limit: z.number().min(0).optional(),
|
|
443
|
+
},
|
|
444
|
+
annotations: { readOnlyHint: true },
|
|
445
|
+
}, async ({ figmaUrl, fileKey, query, currentPageOnly, limit }) => {
|
|
446
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
282
447
|
const result = (await conn.getLocalComponents({ currentPageOnly, limit }));
|
|
283
448
|
const data = result?.data;
|
|
284
449
|
if (!data) {
|
|
@@ -289,37 +454,53 @@ export async function main() {
|
|
|
289
454
|
const q = query.trim().toLowerCase();
|
|
290
455
|
list = list.filter((c) => (c.name || "").toLowerCase().includes(q));
|
|
291
456
|
}
|
|
292
|
-
const summary = list.map((c) => ({ id: c.id, name: c.name, type: c.type }));
|
|
457
|
+
const summary = list.map((c) => ({ id: c.id, name: c.name, key: c.key, type: c.type }));
|
|
293
458
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, components: summary }, null, 0) }] };
|
|
294
459
|
});
|
|
295
460
|
// ---- Node operations (short list) ----
|
|
296
|
-
server.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
461
|
+
server.registerTool("figma_instantiate_component", {
|
|
462
|
+
description: "Create a component instance. Use componentKey from figma_search_components or nodeId for local components.",
|
|
463
|
+
inputSchema: {
|
|
464
|
+
componentKey: z.string(),
|
|
465
|
+
options: z
|
|
466
|
+
.object({
|
|
467
|
+
nodeId: z.string().optional(),
|
|
468
|
+
position: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
469
|
+
parentId: z.string().optional(),
|
|
470
|
+
overrides: z.record(z.any()).optional(),
|
|
471
|
+
})
|
|
472
|
+
.optional(),
|
|
473
|
+
},
|
|
474
|
+
annotations: { destructiveHint: true },
|
|
306
475
|
}, async (p) => {
|
|
307
476
|
const conn = getConnector(bridge);
|
|
308
477
|
const result = await conn.instantiateComponent(p.componentKey, p.options || {});
|
|
309
478
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
310
479
|
});
|
|
311
|
-
server.
|
|
480
|
+
server.registerTool("figma_refresh_variables", {
|
|
481
|
+
description: "Refresh variables from the file.",
|
|
482
|
+
inputSchema: {},
|
|
483
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
484
|
+
}, async () => {
|
|
312
485
|
const conn = getConnector(bridge);
|
|
313
486
|
const result = await conn.refreshVariables();
|
|
314
487
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
315
488
|
});
|
|
316
489
|
// ---- Console (plugin buffer, no CDP) ----
|
|
317
|
-
server.
|
|
490
|
+
server.registerTool("figma_get_console_logs", {
|
|
491
|
+
description: "Get plugin console logs (log/warn/error) from the F-MCP plugin buffer. No CDP. Limit default 50.",
|
|
492
|
+
inputSchema: { limit: z.number().min(1).max(200).optional().default(50) },
|
|
493
|
+
annotations: { readOnlyHint: true },
|
|
494
|
+
}, async ({ limit }) => {
|
|
318
495
|
const conn = getConnector(bridge);
|
|
319
496
|
const data = await conn.getConsoleLogs(limit);
|
|
320
497
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }, null, 0) }] };
|
|
321
498
|
});
|
|
322
|
-
server.
|
|
499
|
+
server.registerTool("figma_watch_console", {
|
|
500
|
+
description: "Stream new plugin console logs until timeout. Polls the plugin buffer. Timeout default 30s.",
|
|
501
|
+
inputSchema: { timeoutSeconds: z.number().min(1).max(120).optional().default(30) },
|
|
502
|
+
annotations: { readOnlyHint: true },
|
|
503
|
+
}, async ({ timeoutSeconds }) => {
|
|
323
504
|
const conn = getConnector(bridge);
|
|
324
505
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
325
506
|
const seen = new Set();
|
|
@@ -339,34 +520,50 @@ export async function main() {
|
|
|
339
520
|
content: [{ type: "text", text: JSON.stringify({ success: true, stream, count: stream.length }, null, 0) }],
|
|
340
521
|
};
|
|
341
522
|
});
|
|
342
|
-
server.
|
|
523
|
+
server.registerTool("figma_clear_console", {
|
|
524
|
+
description: "Clear the plugin console log buffer.",
|
|
525
|
+
inputSchema: {},
|
|
526
|
+
annotations: { destructiveHint: true },
|
|
527
|
+
}, async () => {
|
|
343
528
|
const conn = getConnector(bridge);
|
|
344
529
|
await conn.clearConsole();
|
|
345
530
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Console cleared" }, null, 0) }] };
|
|
346
531
|
});
|
|
347
532
|
// ---- set_description, get_component_image, get_component_for_development ----
|
|
348
|
-
server.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
533
|
+
server.registerTool("figma_set_description", {
|
|
534
|
+
description: "Set description on a component, component set, or style node. Supports markdown (descriptionMarkdown).",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
nodeId: z.string(),
|
|
537
|
+
description: z.string(),
|
|
538
|
+
descriptionMarkdown: z.string().optional(),
|
|
539
|
+
},
|
|
540
|
+
annotations: { destructiveHint: true },
|
|
352
541
|
}, async (p) => {
|
|
353
542
|
const conn = getConnector(bridge);
|
|
354
543
|
const result = await conn.setNodeDescription(p.nodeId, p.description, p.descriptionMarkdown);
|
|
355
544
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
356
545
|
});
|
|
357
|
-
server.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
546
|
+
server.registerTool("figma_get_component_image", {
|
|
547
|
+
description: "Get screenshot of a node (component/frame). Returns base64 image. No REST API.",
|
|
548
|
+
inputSchema: {
|
|
549
|
+
nodeId: z.string(),
|
|
550
|
+
scale: z.number().min(0.5).max(4).optional().default(2),
|
|
551
|
+
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
552
|
+
},
|
|
553
|
+
annotations: { readOnlyHint: true },
|
|
361
554
|
}, async ({ nodeId, scale, format }) => {
|
|
362
555
|
const conn = getConnector(bridge);
|
|
363
556
|
const result = await conn.captureScreenshot(nodeId, { scale, format });
|
|
364
557
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
365
558
|
});
|
|
366
|
-
server.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
559
|
+
server.registerTool("figma_get_component_for_development", {
|
|
560
|
+
description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows.",
|
|
561
|
+
inputSchema: {
|
|
562
|
+
nodeId: z.string(),
|
|
563
|
+
scale: z.number().min(0.5).max(4).optional().default(2),
|
|
564
|
+
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
565
|
+
},
|
|
566
|
+
annotations: { readOnlyHint: true },
|
|
370
567
|
}, async ({ nodeId, scale, format }) => {
|
|
371
568
|
const conn = getConnector(bridge);
|
|
372
569
|
const [component, screenshot] = await Promise.all([
|
|
@@ -378,53 +575,73 @@ export async function main() {
|
|
|
378
575
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
|
|
379
576
|
});
|
|
380
577
|
// ---- Batch variables & setup_design_tokens & arrange_component_set ----
|
|
381
|
-
server.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
578
|
+
server.registerTool("figma_batch_create_variables", {
|
|
579
|
+
description: "Create up to 100 variables in one call. Each item: collectionId, name, resolvedType (COLOR/FLOAT/STRING/BOOLEAN), value, modeId. Returns created and failed lists.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
items: z.array(z.object({
|
|
582
|
+
collectionId: z.string(),
|
|
583
|
+
name: z.string(),
|
|
584
|
+
resolvedType: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]),
|
|
585
|
+
value: z.unknown().optional(),
|
|
586
|
+
modeId: z.string().optional(),
|
|
587
|
+
valuesByMode: z.record(z.unknown()).optional(),
|
|
588
|
+
})).max(100),
|
|
589
|
+
},
|
|
590
|
+
annotations: { destructiveHint: true },
|
|
390
591
|
}, async ({ items }) => {
|
|
391
592
|
const conn = getConnector(bridge);
|
|
392
593
|
const result = await conn.batchCreateVariables(items);
|
|
393
594
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
|
|
394
595
|
});
|
|
395
|
-
server.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
596
|
+
server.registerTool("figma_batch_update_variables", {
|
|
597
|
+
description: "Update up to 100 variables. Each item: variableId, modeId, value. Returns updated and failed lists.",
|
|
598
|
+
inputSchema: {
|
|
599
|
+
items: z.array(z.object({
|
|
600
|
+
variableId: z.string(),
|
|
601
|
+
modeId: z.string(),
|
|
602
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
603
|
+
})).max(100),
|
|
604
|
+
},
|
|
605
|
+
annotations: { destructiveHint: true },
|
|
401
606
|
}, async ({ items }) => {
|
|
402
607
|
const conn = getConnector(bridge);
|
|
403
608
|
const result = await conn.batchUpdateVariables(items);
|
|
404
609
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
|
|
405
610
|
});
|
|
406
|
-
server.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
611
|
+
server.registerTool("figma_setup_design_tokens", {
|
|
612
|
+
description: "Atomically create a variable collection + modes + variables. Rollback on any error. Params: collectionName, modes (array), tokens (array of { name, type?, value? or values? }).",
|
|
613
|
+
inputSchema: {
|
|
614
|
+
collectionName: z.string(),
|
|
615
|
+
modes: z.array(z.string()).min(1),
|
|
616
|
+
tokens: z.array(z.object({
|
|
617
|
+
name: z.string(),
|
|
618
|
+
type: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]).optional(),
|
|
619
|
+
value: z.unknown().optional(),
|
|
620
|
+
values: z.record(z.unknown()).optional(),
|
|
621
|
+
})),
|
|
622
|
+
},
|
|
623
|
+
annotations: { destructiveHint: true },
|
|
415
624
|
}, async (p) => {
|
|
416
625
|
const conn = getConnector(bridge);
|
|
417
626
|
const result = await conn.setupDesignTokens(p);
|
|
418
627
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
|
|
419
628
|
});
|
|
420
|
-
server.
|
|
629
|
+
server.registerTool("figma_arrange_component_set", {
|
|
630
|
+
description: "Combine multiple component nodes into one Figma component set (combineAsVariants). Params: nodeIds (array of at least 2 component node IDs). Returns new component set nodeId.",
|
|
631
|
+
inputSchema: { nodeIds: z.array(z.string()).min(2) },
|
|
632
|
+
annotations: { destructiveHint: true },
|
|
633
|
+
}, async ({ nodeIds }) => {
|
|
421
634
|
const conn = getConnector(bridge);
|
|
422
635
|
const result = await conn.arrangeComponentSet(nodeIds);
|
|
423
636
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
|
|
424
637
|
});
|
|
425
638
|
// ---- figma_check_design_parity (design–code gap analysis) ----
|
|
426
|
-
server.
|
|
427
|
-
|
|
639
|
+
server.registerTool("figma_check_design_parity", {
|
|
640
|
+
description: "Compare Figma design tokens (variables + styles) with code-side tokens. Critical for design-code gap analysis. Returns matching, inFigmaOnly, inCodeOnly, and divergent (same name, different value). Optional codeTokens: JSON string of expected tokens, e.g. {\"primary\": \"#0066cc\", \"spacing.md\": 16} or {\"primary\": {\"value\": \"#0066cc\"}}.",
|
|
641
|
+
inputSchema: {
|
|
642
|
+
codeTokens: z.string().optional(),
|
|
643
|
+
},
|
|
644
|
+
annotations: { readOnlyHint: true },
|
|
428
645
|
}, async ({ codeTokens }) => {
|
|
429
646
|
try {
|
|
430
647
|
const conn = getConnector(bridge);
|
|
@@ -546,8 +763,12 @@ export async function main() {
|
|
|
546
763
|
}
|
|
547
764
|
});
|
|
548
765
|
// ---- figma_get_token_browser (Token Browser – kurulum özel MCP App) ----
|
|
549
|
-
server.
|
|
550
|
-
|
|
766
|
+
server.registerTool("figma_get_token_browser", {
|
|
767
|
+
description: "Token Browser: hierarchical view of design tokens for browsing. Returns variable collections with variables and modes, plus paint and text styles. Use for exploring and auditing tokens in the open Figma file. No REST API.",
|
|
768
|
+
inputSchema: {
|
|
769
|
+
verbosity: z.enum(["summary", "full"]).optional().default("summary"),
|
|
770
|
+
},
|
|
771
|
+
annotations: { readOnlyHint: true },
|
|
551
772
|
}, async ({ verbosity }) => {
|
|
552
773
|
try {
|
|
553
774
|
const conn = getConnector(bridge);
|
|
@@ -611,13 +832,44 @@ export async function main() {
|
|
|
611
832
|
}
|
|
612
833
|
});
|
|
613
834
|
// ---- figma_get_status (plugin-only) ----
|
|
614
|
-
server.
|
|
835
|
+
server.registerTool("figma_get_status", {
|
|
836
|
+
description: "Check if F-MCP ATezer Bridge plugin is connected and list all connected files. No REST API or token.",
|
|
837
|
+
inputSchema: {},
|
|
838
|
+
annotations: { readOnlyHint: true },
|
|
839
|
+
}, async () => {
|
|
615
840
|
const connected = bridge.isConnected();
|
|
841
|
+
const connectedFiles = bridge.listConnectedFiles();
|
|
842
|
+
const clientCount = bridge.connectedClientCount();
|
|
616
843
|
const msg = connected
|
|
617
|
-
?
|
|
844
|
+
? `F-MCP ATezer Bridge: ${clientCount} plugin(s) connected. You can use all figma_* tools.`
|
|
618
845
|
: PLUGIN_NOT_CONNECTED;
|
|
619
|
-
|
|
846
|
+
const portHint = clientCount === 0
|
|
847
|
+
? `Bu uygulama (Claude/Cursor) bridge'i port ${port}'ta dinliyor. Figma'da plugini açıp Port alanına ${port} yazın → "ready (:${port})" görünmeli. Claude ile 5455 kullanıyorsanız plugin'de Port: 5455 olmalı.`
|
|
848
|
+
: undefined;
|
|
849
|
+
return {
|
|
850
|
+
content: [{
|
|
851
|
+
type: "text",
|
|
852
|
+
text: JSON.stringify({
|
|
853
|
+
pluginConnected: connected,
|
|
854
|
+
connectedClients: clientCount,
|
|
855
|
+
connectedFiles,
|
|
856
|
+
bridgePort: port,
|
|
857
|
+
message: msg,
|
|
858
|
+
...(portHint && { portHint }),
|
|
859
|
+
}, null, 0),
|
|
860
|
+
}],
|
|
861
|
+
};
|
|
620
862
|
});
|
|
863
|
+
const shutdown = () => {
|
|
864
|
+
logger.info("Shutting down plugin-only MCP server...");
|
|
865
|
+
try {
|
|
866
|
+
bridge.stop();
|
|
867
|
+
}
|
|
868
|
+
catch { /* ignore */ }
|
|
869
|
+
process.exit(0);
|
|
870
|
+
};
|
|
871
|
+
process.on("SIGINT", shutdown);
|
|
872
|
+
process.on("SIGTERM", shutdown);
|
|
621
873
|
const transport = new StdioServerTransport();
|
|
622
874
|
await server.connect(transport);
|
|
623
875
|
logger.info({ port }, "F-MCP ATezer Bridge (plugin-only) MCP server running on stdio; WebSocket on port %s", port);
|