@atezer/figma-mcp-bridge 1.7.29 → 1.7.30

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.
@@ -98,6 +98,42 @@ function getErrorHint(category) {
98
98
  default: return "Hata mesajini kontrol et.";
99
99
  }
100
100
  }
101
+ /** Analyze figma_execute code for common mistakes. Returns advisory warnings (never blocks execution). */
102
+ function analyzeCodeForWarnings(code) {
103
+ const warnings = [];
104
+ // 1. FILL before appendChild — must set FILL *after* node is in auto-layout parent
105
+ if (/layoutSizing(?:Horizontal|Vertical)\s*=\s*['"]FILL['"]/i.test(code)) {
106
+ const fillIdx = code.search(/layoutSizing(?:Horizontal|Vertical)\s*=\s*['"]FILL['"]/i);
107
+ const appendIdx = code.indexOf("appendChild");
108
+ if (appendIdx === -1 || fillIdx < appendIdx) {
109
+ warnings.push("layoutSizingHorizontal/Vertical = 'FILL' appendChild'dan ONCE ayarlanmis. " +
110
+ "Oncesinde hata verir. FILL'i appendChild SONRASINA tasi.");
111
+ }
112
+ }
113
+ // 2. Sync API usage — should use Async versions
114
+ const syncApis = [
115
+ { sync: "getLocalPaintStyles(", async: "getLocalPaintStylesAsync(" },
116
+ { sync: "getLocalTextStyles(", async: "getLocalTextStylesAsync(" },
117
+ { sync: "getLocalEffectStyles(", async: "getLocalEffectStylesAsync(" },
118
+ { sync: "getLocalGridStyles(", async: "getLocalGridStylesAsync(" },
119
+ ];
120
+ for (const api of syncApis) {
121
+ if (code.includes(api.sync) && !code.includes(api.async)) {
122
+ warnings.push(`Sync API '${api.sync.slice(0, -1)}' tespit edildi. 'await ${api.async.slice(0, -1)}' kullanin — dynamic-page modunda sync API'ler calismaz.`);
123
+ }
124
+ }
125
+ // 3. Font not loaded before text modification
126
+ if ((/\.characters\s*=/.test(code) || code.includes(".insertCharacters") || code.includes(".deleteCharacters")) &&
127
+ !code.includes("loadFontAsync")) {
128
+ warnings.push("Text icerik degisikligi (characters) tespit edildi, ancak loadFontAsync cagrisi yok. " +
129
+ "Metin degistirmeden once 'await figma.loadFontAsync(node.fontName)' ekleyin.");
130
+ }
131
+ // 4. Sync page assignment — does not work
132
+ if (/figma\.currentPage\s*=/.test(code) && !code.includes("setCurrentPageAsync")) {
133
+ warnings.push("'figma.currentPage = ...' calismaz. 'await figma.setCurrentPageAsync(page)' kullanin.");
134
+ }
135
+ return warnings;
136
+ }
101
137
  /** Wrap a tool handler with try-catch to prevent unhandled rejections. */
102
138
  function safeToolHandler(handler) {
103
139
  return async (params) => {
@@ -328,7 +364,13 @@ export async function main() {
328
364
  }));
329
365
  // ---- figma_execute ----
330
366
  server.registerTool("figma_execute", {
331
- description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file.",
367
+ description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file. " +
368
+ "Common mistakes are detected and returned as _warnings: " +
369
+ "(1) layoutSizingHorizontal/Vertical='FILL' must be set AFTER appendChild, " +
370
+ "(2) use getLocalPaintStylesAsync not getLocalPaintStyles, " +
371
+ "(3) call loadFontAsync before .characters=, " +
372
+ "(4) use setCurrentPageAsync not figma.currentPage=. " +
373
+ "For component instances: use setProperties({...}), NOT findAll(TEXT).",
332
374
  inputSchema: {
333
375
  figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
334
376
  fileKey: z.string().optional().describe("Target a specific connected file."),
@@ -345,6 +387,9 @@ export async function main() {
345
387
  }
346
388
  const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 120000));
347
389
  invalidateCache();
390
+ // Run static analysis BEFORE execution so warnings are available in ALL response paths
391
+ const codeWarnings = analyzeCodeForWarnings(code);
392
+ const warningsField = codeWarnings.length > 0 ? { _warnings: codeWarnings } : {};
348
393
  const startTime = Date.now();
349
394
  try {
350
395
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
@@ -366,6 +411,7 @@ export async function main() {
366
411
  errorCategory: category,
367
412
  _metrics: { durationMs, timeoutMs: clampedTimeout },
368
413
  hint,
414
+ ...warningsField,
369
415
  }) }],
370
416
  isError: true,
371
417
  };
@@ -373,7 +419,7 @@ export async function main() {
373
419
  let enriched;
374
420
  try {
375
421
  enriched = typeof result === "object" && result !== null
376
- ? { ...result, _metrics: { durationMs, timeoutMs: clampedTimeout } }
422
+ ? { ...result, _metrics: { durationMs, timeoutMs: clampedTimeout }, ...warningsField }
377
423
  : result;
378
424
  }
379
425
  catch {
@@ -399,6 +445,7 @@ export async function main() {
399
445
  error: msg,
400
446
  _metrics: { durationMs, timeoutMs: clampedTimeout },
401
447
  hint,
448
+ ...warningsField,
402
449
  }) }],
403
450
  isError: true,
404
451
  };
@@ -581,7 +628,9 @@ export async function main() {
581
628
  }));
582
629
  // ---- Node operations (short list) ----
583
630
  server.registerTool("figma_instantiate_component", {
584
- description: "Create a component instance. Use componentKey from figma_search_components or nodeId for local components.",
631
+ description: "Create a component instance. Use componentKey from figma_search_components, figma_search_assets, or REST API. " +
632
+ "Supports library components (importComponentByKeyAsync) and local components (by nodeId). " +
633
+ "After creation: use overrides with setProperties({...}) for component properties — do NOT use findAll(TEXT) to modify instance text.",
585
634
  inputSchema: {
586
635
  componentKey: z.string(),
587
636
  options: z
@@ -1071,14 +1120,16 @@ export async function main() {
1071
1120
  }
1072
1121
  });
1073
1122
  server.registerTool("figma_create_text", {
1074
- description: "Create a new text node on the current page. Returns the created node ID.",
1123
+ description: "Create a new text node on the current page. Returns the created node ID. " +
1124
+ "IMPORTANT: fontFamily defaults to 'Inter' — if using a design system (e.g. SUI uses SHBGrotesk), specify the DS font. " +
1125
+ "For DS text with proper token binding, prefer figma_execute with importStyleByKeyAsync + setTextStyleIdAsync instead.",
1075
1126
  inputSchema: {
1076
1127
  text: z.string().describe("Text content"),
1077
1128
  x: z.number().optional().default(0),
1078
1129
  y: z.number().optional().default(0),
1079
1130
  name: z.string().optional().describe("Node name (default: text content)"),
1080
1131
  fontSize: z.number().optional().default(16),
1081
- fontFamily: z.string().optional().default("Inter"),
1132
+ fontFamily: z.string().optional().default("Inter").describe("Font family — defaults to Inter. Specify DS font if using a design system (e.g. SHBGrotesk for SUI)."),
1082
1133
  fontStyle: z.string().optional().default("Regular"),
1083
1134
  fillColor: z.string().optional().describe("Text color hex e.g. '#000000'"),
1084
1135
  parentId: z.string().optional().describe("Parent node ID"),
@@ -1226,36 +1277,171 @@ export async function main() {
1226
1277
  });
1227
1278
  // ---- figma_search_assets (team library search via plugin) ----
1228
1279
  server.registerTool("figma_search_assets", {
1229
- description: "Search for published team library components and styles available in the current file. " +
1230
- "Uses Figma's teamLibrary API via plugin. Returns available components from enabled libraries.",
1280
+ description: "Search for team library variable collections (with import keys) and file-local components/component sets. " +
1281
+ "Variables come from enabled team libraries via figma.teamLibrary API. " +
1282
+ "Components are file-local only — for remote library component discovery, use figma_rest_api GET /v1/files/{fileKey}/components. " +
1283
+ "Pass assetTypes to filter: ['variables'], ['components'], or both (default).",
1231
1284
  inputSchema: {
1285
+ figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
1286
+ fileKey: z.string().optional().describe("Target a specific connected file."),
1232
1287
  query: z.string().optional().describe("Search query to filter by name"),
1288
+ assetTypes: z.array(z.string()).optional().describe("Asset types to search: 'variables', 'components'. Default: both."),
1289
+ limit: z.number().min(1).max(80).optional().describe("Max results per asset type (default 25, max 80)"),
1290
+ currentPageOnly: z.boolean().optional().describe("For components: search current page only (default true)"),
1233
1291
  },
1234
1292
  annotations: { readOnlyHint: true },
1235
- }, async ({ query }) => {
1236
- try {
1237
- const conn = getConnector(bridge);
1238
- const code = `
1239
- if (!figma.teamLibrary) return { success: false, error: "teamLibrary API not available" };
1240
- const availableLibs = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
1241
- const availableComps = typeof figma.teamLibrary.getAvailableLibraryComponentsAsync === 'function' ? await figma.teamLibrary.getAvailableLibraryComponentsAsync() : [];
1242
- return {
1243
- variableCollections: availableLibs.map(c => ({ name: c.name, key: c.key, libraryName: c.libraryName })),
1244
- note: "Use figma_search_components for file-local components. Team library component search requires REST API (figma_rest_api)."
1245
- };
1246
- `;
1247
- const result = await conn.executeCodeViaUI(code, 15000);
1248
- const data = result;
1249
- if (query && data.variableCollections && Array.isArray(data.variableCollections)) {
1250
- const q = query.toLowerCase();
1251
- data.variableCollections = data.variableCollections.filter((c) => (c.name || "").toLowerCase().includes(q) || (c.libraryName || "").toLowerCase().includes(q));
1252
- }
1253
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
1254
- }
1255
- catch (err) {
1256
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
1257
- }
1258
- });
1293
+ }, safeToolHandler(async ({ figmaUrl, fileKey, query, assetTypes, limit, currentPageOnly }) => {
1294
+ const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
1295
+ const result = await conn.searchLibraryAssets({
1296
+ query: query || undefined,
1297
+ assetTypes: assetTypes?.length ? assetTypes : undefined,
1298
+ limit: limit ?? undefined,
1299
+ currentPageOnly,
1300
+ });
1301
+ const data = result;
1302
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
1303
+ }));
1304
+ // ---- figma_get_library_variables (team library variable discovery with import keys) ----
1305
+ server.registerTool("figma_get_library_variables", {
1306
+ description: "List variables from team library collections with import keys. " +
1307
+ "Uses figma.teamLibrary API works in the TARGET file, no need to connect the DS source file. " +
1308
+ "Returns variable name, key (for importVariableByKeyAsync), resolvedType, collection, and library name. " +
1309
+ "Use the returned keys with figma_bind_variable or figma.variables.importVariableByKeyAsync() in figma_execute.",
1310
+ inputSchema: {
1311
+ figmaUrl: z.string().optional().describe("Figma file URL for routing."),
1312
+ fileKey: z.string().optional().describe("Target a specific connected file."),
1313
+ query: z.string().optional().describe("Filter variables by name (case-insensitive contains)"),
1314
+ collectionName: z.string().optional().describe("Filter by collection name (exact match)"),
1315
+ libraryName: z.string().optional().describe("Filter by library name (exact match, e.g. '❖ SUI')"),
1316
+ limit: z.number().min(1).max(500).optional().describe("Max results (default 100)"),
1317
+ },
1318
+ annotations: { readOnlyHint: true },
1319
+ }, safeToolHandler(async ({ figmaUrl, fileKey, query, collectionName, libraryName, limit }) => {
1320
+ const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
1321
+ const maxResults = limit ?? 100;
1322
+ const q = query ? query.toLowerCase() : "";
1323
+ const code = `
1324
+ if (!figma.teamLibrary) return { success: false, error: "teamLibrary API not available" };
1325
+ var cols = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
1326
+ var filtered = cols;
1327
+ ${collectionName ? `filtered = filtered.filter(function(c) { return c.name === ${JSON.stringify(collectionName)}; });` : ""}
1328
+ ${libraryName ? `filtered = filtered.filter(function(c) { return c.libraryName === ${JSON.stringify(libraryName)}; });` : ""}
1329
+ var results = [];
1330
+ for (var ci = 0; ci < filtered.length && results.length < ${maxResults}; ci++) {
1331
+ var col = filtered[ci];
1332
+ var vars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(col.key);
1333
+ for (var vi = 0; vi < vars.length && results.length < ${maxResults}; vi++) {
1334
+ var v = vars[vi];
1335
+ var nm = (v.name || "").toLowerCase();
1336
+ if (!${JSON.stringify(q)} || nm.indexOf(${JSON.stringify(q)}) >= 0) {
1337
+ results.push({ name: v.name, key: v.key, resolvedType: v.resolvedType, collection: col.name, library: col.libraryName });
1338
+ }
1339
+ }
1340
+ }
1341
+ return { success: true, count: results.length, variables: results };
1342
+ `;
1343
+ const result = await conn.executeCodeViaUI(code, 30000);
1344
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1345
+ }));
1346
+ // ---- figma_bind_variable (import variable and bind to node property) ----
1347
+ server.registerTool("figma_bind_variable", {
1348
+ description: "Import a library variable by key and bind it to a node property. " +
1349
+ "For colors: binds to fills or strokes via setBoundVariableForPaint. " +
1350
+ "For spacing/sizing: binds via setBoundVariable (paddingLeft, itemSpacing, cornerRadius, etc.). " +
1351
+ "Get variableKey from figma_get_library_variables. " +
1352
+ "The node's fill/spacing will dynamically update when the DS token changes.",
1353
+ inputSchema: {
1354
+ figmaUrl: z.string().optional().describe("Figma file URL for routing."),
1355
+ fileKey: z.string().optional().describe("Target a specific connected file."),
1356
+ nodeId: z.string().describe("Target node ID"),
1357
+ variableKey: z.string().describe("Variable import key from figma_get_library_variables"),
1358
+ property: z.enum([
1359
+ "fills", "strokes",
1360
+ "paddingLeft", "paddingRight", "paddingTop", "paddingBottom",
1361
+ "itemSpacing", "counterAxisSpacing",
1362
+ "topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius", "cornerRadius",
1363
+ "strokeWeight", "opacity",
1364
+ "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
1365
+ ]).describe("Node property to bind the variable to"),
1366
+ paintIndex: z.number().optional().default(0).describe("For fills/strokes: which paint index (default 0)"),
1367
+ },
1368
+ annotations: { destructiveHint: true },
1369
+ }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, variableKey, property, paintIndex }) => {
1370
+ invalidateCache();
1371
+ const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
1372
+ const idx = paintIndex ?? 0;
1373
+ const code = `
1374
+ var variable = await figma.variables.importVariableByKeyAsync(${JSON.stringify(variableKey)});
1375
+ var node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
1376
+ if (!node) throw new Error("Node not found: " + ${JSON.stringify(nodeId)});
1377
+ var prop = ${JSON.stringify(property)};
1378
+ if (prop === "fills" || prop === "strokes") {
1379
+ var paints = [];
1380
+ for (var i = 0; i < node[prop].length; i++) paints.push(node[prop][i]);
1381
+ if (!paints[${idx}]) throw new Error("No paint at index ${idx} on " + prop);
1382
+ var boundPaint = figma.variables.setBoundVariableForPaint(paints[${idx}], "color", variable);
1383
+ paints[${idx}] = boundPaint;
1384
+ node[prop] = paints;
1385
+ } else {
1386
+ node.setBoundVariable(prop, variable);
1387
+ }
1388
+ return { success: true, nodeId: ${JSON.stringify(nodeId)}, property: prop, variableName: variable.name, variableId: variable.id };
1389
+ `;
1390
+ const result = await conn.executeCodeViaUI(code, 10000);
1391
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1392
+ }));
1393
+ // ---- figma_import_style (import text/paint/effect style from library) ----
1394
+ server.registerTool("figma_import_style", {
1395
+ description: "Import a text, paint, or effect style from a team library by key, and optionally apply it to a node. " +
1396
+ "IMPORTANT: This API only imports PUBLISHED LIBRARY styles, NOT local file styles. " +
1397
+ "For local styles, use 'node.fillStyleId = style.id' (or textStyleId/effectStyleId) directly via figma_execute. " +
1398
+ "Get library style keys from .claude/libraries/ cache or REST API: figma_rest_api GET /v1/files/{fileKey}/styles. " +
1399
+ "For TEXT styles: applies via setTextStyleIdAsync (includes font, size, weight). " +
1400
+ "For PAINT styles: applies via fillStyleId. For EFFECT styles: applies via effectStyleId.",
1401
+ inputSchema: {
1402
+ figmaUrl: z.string().optional().describe("Figma file URL for routing."),
1403
+ fileKey: z.string().optional().describe("Target a specific connected file."),
1404
+ styleKey: z.string().describe("Library style key (must be from a PUBLISHED team library, not a local style)"),
1405
+ nodeId: z.string().optional().describe("Node ID to apply the style to (optional — omit to just import)"),
1406
+ },
1407
+ annotations: { destructiveHint: true },
1408
+ }, safeToolHandler(async ({ figmaUrl, fileKey, styleKey, nodeId }) => {
1409
+ invalidateCache();
1410
+ const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
1411
+ const code = `
1412
+ var style;
1413
+ try {
1414
+ style = await figma.importStyleByKeyAsync(${JSON.stringify(styleKey)});
1415
+ } catch (e) {
1416
+ var origMsg = e && e.message ? e.message : String(e);
1417
+ throw new Error(
1418
+ "importStyleByKeyAsync failed for key '" + ${JSON.stringify(styleKey)} + "'. " +
1419
+ "This API only works with PUBLISHED LIBRARY styles. " +
1420
+ "Local file styles cannot be imported this way — use 'node.fillStyleId/textStyleId/effectStyleId = <localStyleId>' directly via figma_execute. " +
1421
+ "To find library style keys, use REST API: GET /v1/files/{fileKey}/styles. " +
1422
+ "Original error: " + origMsg
1423
+ );
1424
+ }
1425
+ var applied = false;
1426
+ ${nodeId ? `
1427
+ var node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
1428
+ if (!node) throw new Error("Node not found: " + ${JSON.stringify(nodeId)});
1429
+ if (style.type === "TEXT" && node.type === "TEXT") {
1430
+ await node.setTextStyleIdAsync(style.id);
1431
+ applied = true;
1432
+ } else if (style.type === "PAINT") {
1433
+ node.fillStyleId = style.id;
1434
+ applied = true;
1435
+ } else if (style.type === "EFFECT") {
1436
+ node.effectStyleId = style.id;
1437
+ applied = true;
1438
+ }
1439
+ ` : ""}
1440
+ return { success: true, styleId: style.id, styleName: style.name, styleType: style.type, applied: applied };
1441
+ `;
1442
+ const result = await conn.executeCodeViaUI(code, 10000);
1443
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1444
+ }));
1259
1445
  // ---- figma_plugin_diagnostics ----
1260
1446
  server.registerTool("figma_plugin_diagnostics", {
1261
1447
  description: "Get diagnostic info about plugin connection health: uptime, connected clients, " +