@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/LICENSE +0 -11
  3. package/README.md +107 -10
  4. package/dist/cloudflare/core/config.js +1 -1
  5. package/dist/cloudflare/core/figma-tools.js +290 -254
  6. package/dist/cloudflare/core/figma-url.js +48 -0
  7. package/dist/cloudflare/core/plugin-bridge-connector.js +52 -43
  8. package/dist/cloudflare/core/plugin-bridge-server.js +247 -75
  9. package/dist/core/config.js +1 -1
  10. package/dist/core/config.js.map +1 -1
  11. package/dist/core/figma-tools.d.ts.map +1 -1
  12. package/dist/core/figma-tools.js +290 -254
  13. package/dist/core/figma-tools.js.map +1 -1
  14. package/dist/core/figma-url.d.ts +10 -0
  15. package/dist/core/figma-url.d.ts.map +1 -0
  16. package/dist/core/figma-url.js +49 -0
  17. package/dist/core/figma-url.js.map +1 -0
  18. package/dist/core/plugin-bridge-connector.d.ts +6 -1
  19. package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
  20. package/dist/core/plugin-bridge-connector.js +52 -43
  21. package/dist/core/plugin-bridge-connector.js.map +1 -1
  22. package/dist/core/plugin-bridge-server.d.ts +49 -12
  23. package/dist/core/plugin-bridge-server.d.ts.map +1 -1
  24. package/dist/core/plugin-bridge-server.js +247 -75
  25. package/dist/core/plugin-bridge-server.js.map +1 -1
  26. package/dist/local-plugin-only.d.ts.map +1 -1
  27. package/dist/local-plugin-only.js +391 -139
  28. package/dist/local-plugin-only.js.map +1 -1
  29. package/dist/local.js +323 -183
  30. package/dist/local.js.map +1 -1
  31. package/f-mcp-plugin/README.md +5 -5
  32. package/f-mcp-plugin/code.js +243 -2
  33. package/f-mcp-plugin/manifest.json +3 -1
  34. package/f-mcp-plugin/ui.html +359 -64
  35. 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
- return new PluginBridgeConnector(bridge);
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.0.0",
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.tool("figma_get_file_data", "Get file structure and document tree from the open Figma file. No REST API or token needed. Uses plugin only. Start with depth=1 and verbosity=summary for minimal tokens. Use includeLayout/includeVisual/includeTypography for pixel-perfect spec (auto-layout, constraints, fills, typography).", {
63
- depth: z.number().min(0).max(3).optional().default(1),
64
- verbosity: z.enum(["summary", "standard", "full"]).optional().default("summary"),
65
- includeLayout: z.boolean().optional(),
66
- includeVisual: z.boolean().optional(),
67
- includeTypography: z.boolean().optional(),
68
- includeCodeReady: z.boolean().optional(),
69
- outputHint: z.enum(["react", "tailwind"]).optional(),
70
- }, async ({ depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
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 conn = getConnector(bridge);
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.tool("figma_get_design_context", "Design context for a node or whole file: structure + text, and optionally layout/visual/typography. Returns roleHint/suiComponent (SUI-style name), layoutSummary, colorHex, variantSummary/suggestedProps for instances, incompleteReasons, hasImageFill. Use outputHint: react or tailwind for code-ready layoutSummary. No Figma REST API, no token.", {
98
- nodeId: z.string().optional(),
99
- depth: z.number().min(0).max(3).optional().default(2),
100
- verbosity: z.enum(["summary", "standard", "full"]).optional().default("standard"),
101
- excludeScreenshot: z.boolean().optional(),
102
- includeLayout: z.boolean().optional(),
103
- includeVisual: z.boolean().optional(),
104
- includeTypography: z.boolean().optional(),
105
- includeCodeReady: z.boolean().optional(),
106
- outputHint: z.enum(["react", "tailwind"]).optional(),
107
- }, async ({ nodeId, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
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 conn = getConnector(bridge);
110
- const opts = includeLayout !== undefined ||
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 data = nodeId
118
- ? await conn.getNodeContext(nodeId, depth, verbosity, opts)
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.tool("figma_get_variables", "Get design tokens and variables from the open Figma file. No REST API or token. Returns summary by default to save tokens.", {
137
- verbosity: z.enum(["inventory", "summary", "standard", "full"]).optional().default("summary"),
138
- }, async ({ verbosity }) => {
139
- const conn = getConnector(bridge);
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.tool("figma_get_component", "Get component metadata by node ID from the open Figma file. No REST API. Use figma_get_file_data or figma_search_components to find nodeIds.", { nodeId: z.string() }, async ({ nodeId }) => {
166
- const conn = getConnector(bridge);
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.tool("figma_get_styles", "Get local paint, text, and effect styles from the open Figma file. No REST API. Default verbosity=summary for token saving.", { verbosity: z.enum(["summary", "full"]).optional().default("summary") }, async ({ verbosity }) => {
172
- const conn = getConnector(bridge);
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.tool("figma_execute", "Run JavaScript in the Figma plugin context. Full Plugin API available (figma.root, figma.createFrame, etc.). No token needed.", {
178
- code: z.string(),
179
- timeout: z.number().optional().default(5000),
180
- }, async ({ code, timeout }) => {
181
- const conn = getConnector(bridge);
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.tool("figma_capture_screenshot", "Capture screenshot of a node or current view from the plugin. No REST API.", {
187
- nodeId: z.string().optional(),
188
- format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
189
- scale: z.number().optional().default(2),
190
- }, async ({ nodeId, format, scale }) => {
191
- const conn = getConnector(bridge);
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.tool("figma_set_instance_properties", "Set component instance properties (TEXT, BOOLEAN, VARIANT, etc.).", {
197
- nodeId: z.string(),
198
- properties: z.record(z.union([z.string(), z.boolean()])),
199
- }, async ({ nodeId, properties }) => {
200
- const conn = getConnector(bridge);
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.tool("figma_update_variable", "Update a variable value in a mode. Get IDs from figma_get_variables.", {
206
- variableId: z.string(),
207
- modeId: z.string(),
208
- value: z.union([z.string(), z.number(), z.boolean()]),
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.tool("figma_create_variable", "Create a variable in a collection. Get collectionId from figma_get_variables.", {
215
- name: z.string(),
216
- collectionId: z.string(),
217
- resolvedType: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]),
218
- options: z.record(z.any()).optional(),
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.tool("figma_create_variable_collection", "Create a variable collection.", { name: z.string(), options: z.record(z.any()).optional() }, async (p) => {
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.tool("figma_delete_variable", "Delete a variable.", { variableId: z.string() }, async (p) => {
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.tool("figma_delete_variable_collection", "Delete a variable collection.", { collectionId: z.string() }, async (p) => {
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.tool("figma_rename_variable", "Rename a variable.", { variableId: z.string(), newName: z.string() }, async (p) => {
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.tool("figma_add_mode", "Add a mode to a collection.", { collectionId: z.string(), modeName: z.string() }, async (p) => {
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.tool("figma_rename_mode", "Rename a mode in a collection.", { collectionId: z.string(), modeId: z.string(), newName: z.string() }, async (p) => {
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.tool("figma_get_design_system_summary", "Get a compact overview: variable collection names and component counts. Minimal tokens. No REST API. Uses currentPageOnly: true by default to avoid timeout on large files (SUI); set currentPageOnly: false to scan entire file.", {
256
- currentPageOnly: z.boolean().optional().default(true),
257
- limit: z.number().min(0).optional(),
258
- }, async ({ currentPageOnly, limit }) => {
259
- const conn = getConnector(bridge);
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.tool("figma_search_components", "Search local components by name. Returns nodeIds and names. No REST API. Uses currentPageOnly: true by default to avoid timeout on large files; set currentPageOnly: false to search entire file.", {
277
- query: z.string().optional(),
278
- currentPageOnly: z.boolean().optional().default(true),
279
- limit: z.number().min(0).optional(),
280
- }, async ({ query, currentPageOnly, limit }) => {
281
- const conn = getConnector(bridge);
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.tool("figma_instantiate_component", "Create a component instance. Use componentKey from figma_search_components or nodeId for local components.", {
297
- componentKey: z.string(),
298
- options: z
299
- .object({
300
- nodeId: z.string().optional(),
301
- position: z.object({ x: z.number(), y: z.number() }).optional(),
302
- parentId: z.string().optional(),
303
- overrides: z.record(z.any()).optional(),
304
- })
305
- .optional(),
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.tool("figma_refresh_variables", "Refresh variables from the file.", {}, async () => {
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.tool("figma_get_console_logs", "Get plugin console logs (log/warn/error) from the F-MCP plugin buffer. No CDP. Limit default 50.", { limit: z.number().min(1).max(200).optional().default(50) }, async ({ limit }) => {
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.tool("figma_watch_console", "Stream new plugin console logs until timeout. Polls the plugin buffer. Timeout default 30s.", { timeoutSeconds: z.number().min(1).max(120).optional().default(30) }, async ({ timeoutSeconds }) => {
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.tool("figma_clear_console", "Clear the plugin console log buffer.", {}, async () => {
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.tool("figma_set_description", "Set description on a component, component set, or style node. Supports markdown (descriptionMarkdown).", {
349
- nodeId: z.string(),
350
- description: z.string(),
351
- descriptionMarkdown: z.string().optional(),
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.tool("figma_get_component_image", "Get screenshot of a node (component/frame). Returns base64 image. No REST API.", {
358
- nodeId: z.string(),
359
- scale: z.number().min(0.5).max(4).optional().default(2),
360
- format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
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.tool("figma_get_component_for_development", "Get component metadata plus base64 screenshot in one call. For design-to-code workflows.", {
367
- nodeId: z.string(),
368
- scale: z.number().min(0.5).max(4).optional().default(2),
369
- format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
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.tool("figma_batch_create_variables", "Create up to 100 variables in one call. Each item: collectionId, name, resolvedType (COLOR/FLOAT/STRING/BOOLEAN), value, modeId. Returns created and failed lists.", {
382
- items: z.array(z.object({
383
- collectionId: z.string(),
384
- name: z.string(),
385
- resolvedType: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]),
386
- value: z.unknown().optional(),
387
- modeId: z.string().optional(),
388
- valuesByMode: z.record(z.unknown()).optional(),
389
- })).max(100),
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.tool("figma_batch_update_variables", "Update up to 100 variables. Each item: variableId, modeId, value. Returns updated and failed lists.", {
396
- items: z.array(z.object({
397
- variableId: z.string(),
398
- modeId: z.string(),
399
- value: z.union([z.string(), z.number(), z.boolean()]),
400
- })).max(100),
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.tool("figma_setup_design_tokens", "Atomically create a variable collection + modes + variables. Rollback on any error. Params: collectionName, modes (array), tokens (array of { name, type?, value? or values? }).", {
407
- collectionName: z.string(),
408
- modes: z.array(z.string()).min(1),
409
- tokens: z.array(z.object({
410
- name: z.string(),
411
- type: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]).optional(),
412
- value: z.unknown().optional(),
413
- values: z.record(z.unknown()).optional(),
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.tool("figma_arrange_component_set", "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.", { nodeIds: z.array(z.string()).min(2) }, async ({ nodeIds }) => {
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.tool("figma_check_design_parity", "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\"}}.", {
427
- codeTokens: z.string().optional(),
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.tool("figma_get_token_browser", "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.", {
550
- verbosity: z.enum(["summary", "full"]).optional().default("summary"),
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.tool("figma_get_status", "Check if F-MCP ATezer Bridge plugin is connected. No REST API or token.", {}, async () => {
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
- ? "F-MCP ATezer Bridge plugin is connected. You can use all figma_* tools."
844
+ ? `F-MCP ATezer Bridge: ${clientCount} plugin(s) connected. You can use all figma_* tools.`
618
845
  : PLUGIN_NOT_CONNECTED;
619
- return { content: [{ type: "text", text: JSON.stringify({ pluginConnected: connected, message: msg }, null, 0) }] };
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);