@atezer/figma-mcp-bridge 1.7.30 → 1.9.0

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 (56) hide show
  1. package/CHANGELOG.md +366 -0
  2. package/README.md +3 -2
  3. package/agents/_orchestrator-protocol.md +185 -0
  4. package/agents/ds-auditor.md +73 -22
  5. package/agents/screen-builder.md +60 -22
  6. package/agents/token-syncer.md +63 -19
  7. package/dist/core/code-warnings.d.ts +38 -0
  8. package/dist/core/code-warnings.d.ts.map +1 -0
  9. package/dist/core/code-warnings.js +191 -0
  10. package/dist/core/code-warnings.js.map +1 -0
  11. package/dist/core/device-presets.d.ts +49 -0
  12. package/dist/core/device-presets.d.ts.map +1 -0
  13. package/dist/core/device-presets.js +141 -0
  14. package/dist/core/device-presets.js.map +1 -0
  15. package/dist/core/instructions.d.ts +4 -2
  16. package/dist/core/instructions.d.ts.map +1 -1
  17. package/dist/core/instructions.js +239 -29
  18. package/dist/core/instructions.js.map +1 -1
  19. package/dist/core/plugin-bridge-connector.d.ts +26 -0
  20. package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
  21. package/dist/core/plugin-bridge-connector.js +18 -2
  22. package/dist/core/plugin-bridge-connector.js.map +1 -1
  23. package/dist/core/plugin-bridge-server.d.ts +2 -0
  24. package/dist/core/plugin-bridge-server.d.ts.map +1 -1
  25. package/dist/core/plugin-bridge-server.js +5 -1
  26. package/dist/core/plugin-bridge-server.js.map +1 -1
  27. package/dist/core/response-guard.d.ts +23 -0
  28. package/dist/core/response-guard.d.ts.map +1 -1
  29. package/dist/core/response-guard.js +113 -0
  30. package/dist/core/response-guard.js.map +1 -1
  31. package/dist/core/version.d.ts +1 -1
  32. package/dist/core/version.d.ts.map +1 -1
  33. package/dist/core/version.js +1 -1
  34. package/dist/core/version.js.map +1 -1
  35. package/dist/local-plugin-only.d.ts.map +1 -1
  36. package/dist/local-plugin-only.js +334 -101
  37. package/dist/local-plugin-only.js.map +1 -1
  38. package/f-mcp-plugin/code.js +514 -29
  39. package/f-mcp-plugin/ui.html +62 -6
  40. package/package.json +1 -1
  41. package/skills/SKILL_INDEX.md +13 -1
  42. package/skills/apply-figma-design-system/SKILL.md +37 -0
  43. package/skills/audit-figma-design-system/SKILL.md +38 -0
  44. package/skills/code-design-mapper/SKILL.md +37 -0
  45. package/skills/design-token-pipeline/SKILL.md +44 -0
  46. package/skills/figma-canvas-ops/SKILL.md +200 -243
  47. package/skills/fmcp-ds-audit-orchestrator/SKILL.md +205 -0
  48. package/skills/fmcp-intent-router/SKILL.md +574 -0
  49. package/skills/fmcp-screen-orchestrator/SKILL.md +166 -0
  50. package/skills/fmcp-screen-recipes/SKILL.md +528 -0
  51. package/skills/fmcp-token-sync-orchestrator/SKILL.md +198 -0
  52. package/skills/generate-figma-library/SKILL.md +38 -0
  53. package/skills/generate-figma-screen/SKILL.md +360 -6
  54. package/skills/implement-design/SKILL.md +32 -0
  55. package/skills/inspiration-intake/SKILL.md +220 -0
  56. package/skills/visual-qa-compare/SKILL.md +33 -0
@@ -17,11 +17,22 @@ 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
19
  import { parseFigmaUrl } from "./core/figma-url.js";
20
- import { truncateRestResponse } from "./core/response-guard.js";
20
+ import { truncateRestResponse, truncatePluginResponse } from "./core/response-guard.js";
21
+ import { analyzeCodeForWarnings } from "./core/code-warnings.js";
22
+ import { resolveDevice, DEVICE_PRESETS } from "./core/device-presets.js";
21
23
  import { closeAuditLog } from "./core/audit-log.js";
22
24
  import { FMCP_VERSION } from "./core/version.js";
23
25
  import { ResponseCache } from "./core/response-cache.js";
24
26
  const logger = createChildLogger({ component: "plugin-only-mcp" });
27
+ /**
28
+ * Legacy default flag — when set, restores pre-v1.8.0 default values for
29
+ * read-only tools (depth=2, verbosity="standard", scale=2, format="PNG").
30
+ * Allows downstream consumers to opt back into the heavier defaults during
31
+ * the v1.8.0 → v1.9.0 transition. Will be removed in v1.9.0.
32
+ *
33
+ * Set: FMCP_LEGACY_DEFAULTS=1
34
+ */
35
+ const LEGACY_DEFAULTS = process.env.FMCP_LEGACY_DEFAULTS === "1";
25
36
  /** Resolve fileKey from figmaUrl (parse) or explicit fileKey. Returns undefined if neither yields a key. */
26
37
  function resolveFileKey(figmaUrl, explicitFileKey) {
27
38
  if (explicitFileKey && explicitFileKey.trim())
@@ -98,42 +109,8 @@ function getErrorHint(category) {
98
109
  default: return "Hata mesajini kontrol et.";
99
110
  }
100
111
  }
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
- }
112
+ // analyzeCodeForWarnings + CodeWarning type moved to ./core/code-warnings.ts (v1.8.1)
113
+ // Imported at the top of this file — this comment marks where the helper used to live.
137
114
  /** Wrap a tool handler with try-catch to prevent unhandled rejections. */
138
115
  function safeToolHandler(handler) {
139
116
  return async (params) => {
@@ -172,6 +149,49 @@ export async function main() {
172
149
  const cache = new ResponseCache();
173
150
  /** Invalidate cache after any mutating operation. */
174
151
  function invalidateCache() { cache.invalidate(); }
152
+ /**
153
+ * Build a stable cache key from tool name + params. Sort keys for determinism.
154
+ * fileKey is included to avoid cross-file leakage in multi-file sessions.
155
+ */
156
+ function makeCacheKey(toolName, params) {
157
+ const sortedEntries = Object.entries(params)
158
+ .filter(([_, v]) => v !== undefined)
159
+ .sort(([a], [b]) => a.localeCompare(b));
160
+ return `${toolName}::${JSON.stringify(sortedEntries)}`;
161
+ }
162
+ /**
163
+ * Shared envelope wrapper for plugin tool results. Applies truncatePluginResponse
164
+ * unless skipGuard is true (for cache hits whose data was already guarded).
165
+ * When debug=true, the _responseGuard marker is preserved; otherwise stripped.
166
+ */
167
+ function toolResult(data, toolName, opts) {
168
+ let payload;
169
+ if (data === undefined || data === null) {
170
+ payload = { success: false, error: "No data from plugin" };
171
+ }
172
+ else if (opts?.skipGuard) {
173
+ payload = data;
174
+ }
175
+ else {
176
+ const result = truncatePluginResponse(data, toolName);
177
+ payload = result.data;
178
+ // Strip _responseGuard marker unless debug=true
179
+ if (!opts?.debug && payload && typeof payload === "object" && payload._responseGuard) {
180
+ const stripped = { ...payload };
181
+ delete stripped._responseGuard;
182
+ payload = stripped;
183
+ }
184
+ }
185
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload);
186
+ return { content: [{ type: "text", text }] };
187
+ }
188
+ /** Helper for error responses with consistent shape. */
189
+ function errorResult(msg) {
190
+ return {
191
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
192
+ isError: true,
193
+ };
194
+ }
175
195
  const server = new McpServer({
176
196
  name: "F-MCP ATezer Bridge (Plugin-only)",
177
197
  version: FMCP_VERSION,
@@ -198,8 +218,9 @@ export async function main() {
198
218
  };
199
219
  });
200
220
  // ---- figma_get_file_data_plugin (no REST, no token) ----
221
+ // v1.8.0: Defaults already conservative (depth=1, verbosity="summary"). Cached + truncated.
201
222
  server.registerTool("figma_get_file_data", {
202
- 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.",
223
+ 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. Defaults to depth=1, verbosity='summary'. Cached 60s per session.",
203
224
  inputSchema: {
204
225
  figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey is extracted from the link for routing."),
205
226
  fileKey: z.string().optional().describe("Target a specific connected file. Use figma_list_connected_files to see available files."),
@@ -210,16 +231,14 @@ export async function main() {
210
231
  includeTypography: z.boolean().optional(),
211
232
  includeCodeReady: z.boolean().optional(),
212
233
  outputHint: z.enum(["react", "tailwind"]).optional(),
234
+ debug: z.boolean().optional().describe("Bypass cache and include _responseGuard fields."),
213
235
  },
214
236
  annotations: { readOnlyHint: true },
215
- }, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
237
+ }, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint, debug }) => {
216
238
  try {
217
239
  const resolvedKey = resolveFileKey(figmaUrl, fileKey);
218
240
  if (figmaUrl && !resolvedKey) {
219
- return {
220
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
221
- isError: true,
222
- };
241
+ return errorResult("Invalid Figma/FigJam URL: could not extract file key.");
223
242
  }
224
243
  const conn = getConnector(bridge, resolvedKey);
225
244
  const opts = includeLayout !== undefined ||
@@ -229,13 +248,12 @@ export async function main() {
229
248
  outputHint !== undefined
230
249
  ? { includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
231
250
  : undefined;
232
- const data = await conn.getDocumentStructure(depth, verbosity, opts);
233
- const text = data === undefined || data === null
234
- ? JSON.stringify({ success: false, error: "No data from plugin" })
235
- : typeof data === "string"
236
- ? data
237
- : JSON.stringify(data);
238
- return { content: [{ type: "text", text }] };
251
+ const cacheK = makeCacheKey("figma_get_file_data", { resolvedKey, depth, verbosity, opts });
252
+ const cached = debug ? null : cache.get(cacheK, 60_000);
253
+ const data = cached ?? await conn.getDocumentStructure(depth, verbosity, opts);
254
+ if (!cached)
255
+ cache.set(cacheK, data);
256
+ return toolResult(data, "figma_get_file_data", { skipGuard: !!cached, debug });
239
257
  }
240
258
  catch (err) {
241
259
  const msg = err instanceof Error ? err.message : String(err);
@@ -246,23 +264,26 @@ export async function main() {
246
264
  }
247
265
  });
248
266
  // ---- figma_get_design_context (get_design_context tarzı, token tasarruflu, Figma token yok) ----
267
+ // v1.8.0: Context-safe defaults — depth=1, verbosity="summary"
268
+ // Override with FMCP_LEGACY_DEFAULTS=1 env var or pass explicit params.
249
269
  server.registerTool("figma_get_design_context", {
250
- 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.",
270
+ description: "Design context for a node or whole file: structure + text, layout/visual/typography. Defaults to depth=1, verbosity='summary' for context safety. Pass depth/verbosity explicitly for deeper data. Cached 60s per session.",
251
271
  inputSchema: {
252
272
  figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey and optional node-id are extracted for routing."),
253
273
  fileKey: z.string().optional().describe("Target a specific connected file."),
254
274
  nodeId: z.string().optional(),
255
- depth: z.number().min(0).max(3).optional().default(2),
256
- verbosity: z.enum(["summary", "standard", "full"]).optional().default("standard"),
257
- excludeScreenshot: z.boolean().optional(),
275
+ depth: z.number().min(0).max(3).optional().default(LEGACY_DEFAULTS ? 2 : 1),
276
+ verbosity: z.enum(["summary", "standard", "full"]).optional().default(LEGACY_DEFAULTS ? "standard" : "summary"),
277
+ excludeScreenshot: z.boolean().optional().describe("Reserved for future use; plugin currently does not embed screenshots in design_context."),
258
278
  includeLayout: z.boolean().optional(),
259
279
  includeVisual: z.boolean().optional(),
260
280
  includeTypography: z.boolean().optional(),
261
281
  includeCodeReady: z.boolean().optional(),
262
282
  outputHint: z.enum(["react", "tailwind"]).optional(),
283
+ debug: z.boolean().optional().describe("Bypass cache and include _responseGuard/_metrics fields."),
263
284
  },
264
285
  annotations: { readOnlyHint: true },
265
- }, async ({ figmaUrl, fileKey, nodeId, depth, verbosity, excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
286
+ }, async ({ figmaUrl, fileKey, nodeId, depth, verbosity, excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint, debug }) => {
266
287
  try {
267
288
  const { fileKey: resolvedKey, nodeId: resolvedNodeId } = resolveDesignContextParams({ figmaUrl, fileKey, nodeId });
268
289
  if (figmaUrl && !resolvedKey) {
@@ -281,15 +302,15 @@ export async function main() {
281
302
  ? { excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
282
303
  : undefined;
283
304
  const effectiveNodeId = resolvedNodeId ?? nodeId?.trim();
284
- const data = effectiveNodeId
305
+ // Cache lookup (60s TTL) — bypassed when debug=true
306
+ const cacheK = makeCacheKey("figma_get_design_context", { resolvedKey, effectiveNodeId, depth, verbosity, opts });
307
+ const cached = debug ? null : cache.get(cacheK, 60_000);
308
+ const data = cached ?? (effectiveNodeId
285
309
  ? await conn.getNodeContext(effectiveNodeId, depth, verbosity, opts)
286
- : await conn.getDocumentStructure(depth, verbosity, opts);
287
- const text = data === undefined || data === null
288
- ? JSON.stringify({ success: false, error: "No data from plugin" })
289
- : typeof data === "string"
290
- ? data
291
- : JSON.stringify(data);
292
- return { content: [{ type: "text", text }] };
310
+ : await conn.getDocumentStructure(depth, verbosity, opts));
311
+ if (!cached)
312
+ cache.set(cacheK, data);
313
+ return toolResult(data, "figma_get_design_context", { skipGuard: !!cached, debug });
293
314
  }
294
315
  catch (err) {
295
316
  const msg = err instanceof Error ? err.message : String(err);
@@ -365,11 +386,9 @@ export async function main() {
365
386
  // ---- figma_execute ----
366
387
  server.registerTool("figma_execute", {
367
388
  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=. " +
389
+ "v1.8.1+: Static analysis detects design-system discipline violations (hardcoded colors, missing token bindings, no-instance usage, hardcoded typography). " +
390
+ "SEVERE warnings are promoted to the top of the response as _designSystemViolations — Claude must read and self-correct. " +
391
+ "Also detects gotchas: FILL/ABSOLUTE before appendChild, sync API usage, missing loadFontAsync, sync currentPage assignment. " +
373
392
  "For component instances: use setProperties({...}), NOT findAll(TEXT).",
374
393
  inputSchema: {
375
394
  figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
@@ -385,11 +404,26 @@ export async function main() {
385
404
  isError: true,
386
405
  };
387
406
  }
388
- const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 120000));
407
+ const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 30000));
389
408
  invalidateCache();
390
- // Run static analysis BEFORE execution so warnings are available in ALL response paths
409
+ // v1.8.1: Structured warnings with SEVERE vs ADVISORY severity
391
410
  const codeWarnings = analyzeCodeForWarnings(code);
392
- const warningsField = codeWarnings.length > 0 ? { _warnings: codeWarnings } : {};
411
+ const severeWarnings = codeWarnings.filter((w) => w.severity === "SEVERE");
412
+ const advisoryWarnings = codeWarnings.filter((w) => w.severity === "ADVISORY");
413
+ // SEVERE warnings go to a prominent field that Claude cannot ignore
414
+ const dsViolations = severeWarnings.length > 0
415
+ ? {
416
+ _designSystemViolations: {
417
+ count: severeWarnings.length,
418
+ message: "⚠️ DESIGN SYSTEM DISIPLIN IHLALI TESPIT EDILDI — kodu duzelt ve tekrar calistir. Bu ekran kabul edilmez.",
419
+ violations: severeWarnings.map((w) => ({ category: w.category, message: w.message })),
420
+ action: "Her ihlal icin uygun DS token binding veya component instance kullan. Detay: figma-canvas-ops SKILL Kural 10.",
421
+ },
422
+ }
423
+ : {};
424
+ const warningsField = advisoryWarnings.length > 0
425
+ ? { _warnings: advisoryWarnings.map((w) => w.message) }
426
+ : {};
393
427
  const startTime = Date.now();
394
428
  try {
395
429
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
@@ -407,6 +441,7 @@ export async function main() {
407
441
  catch { /* safe fallback */ }
408
442
  return {
409
443
  content: [{ type: "text", text: JSON.stringify({
444
+ ...dsViolations, // v1.8.1: SEVERE warnings FIRST, before anything else
410
445
  ...result,
411
446
  errorCategory: category,
412
447
  _metrics: { durationMs, timeoutMs: clampedTimeout },
@@ -419,8 +454,15 @@ export async function main() {
419
454
  let enriched;
420
455
  try {
421
456
  enriched = typeof result === "object" && result !== null
422
- ? { ...result, _metrics: { durationMs, timeoutMs: clampedTimeout }, ...warningsField }
423
- : result;
457
+ ? {
458
+ ...dsViolations, // v1.8.1: SEVERE warnings at top level
459
+ ...result,
460
+ _metrics: { durationMs, timeoutMs: clampedTimeout },
461
+ ...warningsField,
462
+ }
463
+ : severeWarnings.length > 0 || advisoryWarnings.length > 0
464
+ ? { ...dsViolations, result, ...warningsField }
465
+ : result;
424
466
  }
425
467
  catch {
426
468
  enriched = result;
@@ -451,21 +493,116 @@ export async function main() {
451
493
  };
452
494
  }
453
495
  }));
496
+ // ============================================================================
497
+ // v1.8.1: HIGH-LEVEL "FAST AND CORRECT" TOOLS
498
+ // ============================================================================
499
+ //
500
+ // These tools sit above figma_execute and provide one-call solutions for
501
+ // common workflows. Claude should prefer these over hand-written execute
502
+ // code when possible — they handle token binding, auto-layout preservation,
503
+ // and DS instance preservation automatically.
504
+ // ============================================================================
505
+ // ---- figma_clone_screen_to_device ----
506
+ // v1.8.2: NARROW USE CASE ONLY — device migration.
507
+ // For alternatives/variations/new designs, use figma_execute with the
508
+ // generate-figma-screen SKILL Step 5 "build from scratch" pattern.
509
+ // Clone will copy benchmark's existing mistakes (hardcoded rectangles,
510
+ // missing token bindings, non-responsive layouts).
511
+ server.registerTool("figma_clone_screen_to_device", {
512
+ description: "⚠️ NARROW USE CASE — Device migration ONLY. Clone a Figma screen to a target device " +
513
+ "dimension, preserving library instances, bound variables, and auto-layout. " +
514
+ "USE ONLY WHEN: same design system + same layout structure + only screen size changes. " +
515
+ "DO NOT USE FOR: creating alternatives, variations, or new designs — these REQUIRE " +
516
+ "building from scratch with figma_execute following the generate-figma-screen SKILL " +
517
+ "Step 5 pattern (search_assets → instantiate_component → setBoundVariable → auto-layout FILL). " +
518
+ "Clone copies benchmark's EXISTING mistakes (hardcoded rectangles, missing token bindings, " +
519
+ "non-responsive layouts). Benchmark is INSPIRATION, not a copy source for variations. " +
520
+ "If the user says 'alternatif', 'varyasyon', 'farklı', 'yeni', 'tasarla' — USE figma_execute + Step 5, NOT this tool. " +
521
+ "Device presets: iPhone 17, iPhone 16 Pro Max, Android Compact, iPad Pro 11, Desktop, " +
522
+ "and more. Custom: 'WxH' format.",
523
+ inputSchema: {
524
+ figmaUrl: z.string().optional().describe("Figma file URL for routing."),
525
+ fileKey: z.string().optional().describe("Target a specific connected file."),
526
+ sourceNodeId: z.string().describe("Node ID of the source screen to clone (e.g. '139:3407')"),
527
+ targetDevice: z.string().describe("Device preset name (e.g. 'iPhone 17', 'Android Compact') or custom 'WxH' (e.g. '1200x800')"),
528
+ newName: z.string().optional().describe("Name for the cloned screen (default: source name + device suffix)"),
529
+ targetParentId: z.string().optional().describe("Parent node to place the clone under (default: current page)"),
530
+ position: z
531
+ .object({ x: z.number(), y: z.number() })
532
+ .optional()
533
+ .describe("Explicit position for the clone (default: auto-placed right of source)"),
534
+ },
535
+ annotations: { destructiveHint: true },
536
+ }, safeToolHandler(async ({ figmaUrl, fileKey, sourceNodeId, targetDevice, newName, targetParentId, position, }) => {
537
+ // Resolve device name to concrete dimensions
538
+ const resolved = resolveDevice(targetDevice);
539
+ if (!resolved) {
540
+ return errorResult(`Unknown device preset '${targetDevice}'. Supported presets: ${DEVICE_PRESETS.map((p) => p.name).join(", ")}. For custom, use 'WxH' format (e.g. '1200x800').`);
541
+ }
542
+ invalidateCache();
543
+ const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
544
+ const result = await conn.cloneScreenToDevice({
545
+ sourceNodeId,
546
+ targetWidth: resolved.width,
547
+ targetHeight: resolved.height,
548
+ targetDeviceName: resolved.name,
549
+ newName,
550
+ targetParentId,
551
+ position,
552
+ });
553
+ return toolResult(result, "figma_clone_screen_to_device");
554
+ }));
555
+ // ---- figma_validate_screen ----
556
+ // Post-creation audit tool. Walks a node tree and scores its DS compliance
557
+ // across 3 dimensions: instance coverage, token binding coverage, auto-layout
558
+ // coverage. Returns pass/fail + actionable violations.
559
+ server.registerTool("figma_validate_screen", {
560
+ description: "Validate a screen against design-system discipline criteria. Returns a compliance score (0-100) " +
561
+ "across 3 dimensions: instance coverage (library usage), token binding coverage (bound variables), " +
562
+ "and auto-layout coverage. Use this AFTER creating a screen to verify DS compliance. " +
563
+ "If score < minScore, Claude should delete the screen and rebuild it using DS components + token bindings. " +
564
+ "Read-only — never mutates the file.",
565
+ inputSchema: {
566
+ figmaUrl: z.string().optional().describe("Figma file URL for routing."),
567
+ fileKey: z.string().optional().describe("Target a specific connected file."),
568
+ nodeId: z.string().describe("Node ID of the screen to validate"),
569
+ expectedDs: z.string().optional().describe("Expected DS library name (e.g. '❖ SUI') for library match scoring"),
570
+ minScore: z.number().min(0).max(100).optional().default(80).describe("Minimum acceptable score (0-100). Below this, the screen is considered non-compliant."),
571
+ },
572
+ annotations: { readOnlyHint: true },
573
+ }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, expectedDs, minScore, }) => {
574
+ const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
575
+ const result = await conn.validateScreen({ nodeId, expectedDs, minScore });
576
+ return toolResult(result, "figma_validate_screen");
577
+ }));
454
578
  // ---- figma_capture_screenshot ----
579
+ // v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x). Override via params.
455
580
  server.registerTool("figma_capture_screenshot", {
456
- description: "Capture screenshot of a node or current view from the plugin. No REST API. Use fileKey or figmaUrl to target a specific file.",
581
+ description: "Capture screenshot of a node or current view from the plugin. No REST API. Defaults to JPG@1x (q70) for context safety. Use scale/format/jpegQuality to override.",
457
582
  inputSchema: {
458
583
  figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
459
584
  fileKey: z.string().optional().describe("Target a specific connected file."),
460
585
  nodeId: z.string().optional(),
461
- format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
462
- scale: z.number().optional().default(2),
586
+ format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
587
+ scale: z.number().optional().default(LEGACY_DEFAULTS ? 2 : 1),
588
+ jpegQuality: z.number().min(30).max(100).optional().default(70).describe("JPEG quality 30-100. Ignored when format=PNG."),
463
589
  },
464
590
  annotations: { readOnlyHint: true },
465
- }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
591
+ }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality }) => {
466
592
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
467
- const result = await conn.captureScreenshot(nodeId ?? null, { format, scale });
468
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
593
+ const result = await conn.captureScreenshot(nodeId ?? null, { format, scale, jpegQuality });
594
+ // v1.9.6 Fix M1 — skipGuard: true truncatePluginResponse'u bypass et.
595
+ // Gerekçe: screenshot response {success, image: {base64, ...}} şeklinde.
596
+ // pruneNodeTree screenshot payload'una (node tree değil) uymadığı için
597
+ // final fallback truncateResponse({maxStringLength: 200}) devreye girip
598
+ // base64 string'i 200 karaktere kırpıyordu → Claude Desktop `{}` empty
599
+ // object görüyordu (FP-1-R-v2 Known Limitation #8 root cause).
600
+ // skipGuard: true ile payload olduğu gibi JSON.stringify edilir, base64
601
+ // intact kalır. Claude image render etmez (text content block) ama
602
+ // base64 string'i görür, kullanabilir, böylece empty object problemi
603
+ // çözülür. Image content block upgrade'i (MCP image type) Part 5
604
+ // sonraki adımı — safeToolHandler generic refactor'u gerektirir.
605
+ return toolResult(result, "figma_capture_screenshot", { skipGuard: true });
469
606
  }));
470
607
  // ---- figma_set_instance_properties ----
471
608
  server.registerTool("figma_set_instance_properties", {
@@ -733,35 +870,37 @@ export async function main() {
733
870
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
734
871
  }));
735
872
  server.registerTool("figma_get_component_image", {
736
- description: "Get screenshot of a node (component/frame). Returns base64 image. No REST API.",
873
+ description: "Get screenshot of a node (component/frame). Returns base64 image. Defaults to JPG@1x q70 (v1.8.0 context-safe).",
737
874
  inputSchema: {
738
875
  nodeId: z.string(),
739
- scale: z.number().min(0.5).max(4).optional().default(2),
740
- format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
876
+ scale: z.number().min(0.5).max(4).optional().default(LEGACY_DEFAULTS ? 2 : 1),
877
+ format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
878
+ jpegQuality: z.number().min(30).max(100).optional().default(70),
741
879
  },
742
880
  annotations: { readOnlyHint: true },
743
- }, safeToolHandler(async ({ nodeId, scale, format }) => {
881
+ }, safeToolHandler(async ({ nodeId, scale, format, jpegQuality }) => {
744
882
  const conn = getConnector(bridge);
745
- const result = await conn.captureScreenshot(nodeId, { scale, format });
746
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
883
+ const result = await conn.captureScreenshot(nodeId, { scale, format, jpegQuality });
884
+ return toolResult(result, "figma_get_component_image");
747
885
  }));
748
886
  server.registerTool("figma_get_component_for_development", {
749
- description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows.",
887
+ description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows. Defaults to JPG@1x q70 (v1.8.0 context-safe).",
750
888
  inputSchema: {
751
889
  nodeId: z.string(),
752
- scale: z.number().min(0.5).max(4).optional().default(2),
753
- format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
890
+ scale: z.number().min(0.5).max(4).optional().default(LEGACY_DEFAULTS ? 2 : 1),
891
+ format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
892
+ jpegQuality: z.number().min(30).max(100).optional().default(70),
754
893
  },
755
894
  annotations: { readOnlyHint: true },
756
- }, safeToolHandler(async ({ nodeId, scale, format }) => {
895
+ }, safeToolHandler(async ({ nodeId, scale, format, jpegQuality }) => {
757
896
  const conn = getConnector(bridge);
758
897
  const [component, screenshot] = await Promise.all([
759
898
  conn.getComponentFromPluginUI(nodeId),
760
- conn.captureScreenshot(nodeId, { scale, format }),
899
+ conn.captureScreenshot(nodeId, { scale, format, jpegQuality }),
761
900
  ]);
762
901
  const comp = component?.component ?? component;
763
902
  const out = { success: true, component: comp, image: screenshot?.image ?? screenshot?.data };
764
- return { content: [{ type: "text", text: JSON.stringify(out) }] };
903
+ return toolResult(out, "figma_get_component_for_development");
765
904
  }));
766
905
  // ---- Batch variables & setup_design_tokens & arrange_component_set ----
767
906
  server.registerTool("figma_batch_create_variables", {
@@ -1038,6 +1177,11 @@ export async function main() {
1038
1177
  const listening = bridge.isListening();
1039
1178
  const currentPort = bridge.getPort();
1040
1179
  const startError = bridge.getStartError();
1180
+ // v1.8.0+: detect plugin/server version mismatch
1181
+ const outdatedPlugins = connectedFiles.filter((f) => f.pluginVersion === null || (f.pluginVersion && f.pluginVersion !== FMCP_VERSION));
1182
+ const versionWarning = outdatedPlugins.length > 0
1183
+ ? `⚠️ Plugin version mismatch detected: ${outdatedPlugins.map(f => `${f.fileName || "?"} (plugin v${f.pluginVersion ?? "<1.8.0"}, server v${FMCP_VERSION})`).join(", ")}. Reinstall the plugin from f-mcp-plugin/ for full v1.8.0 context-safe defaults.`
1184
+ : undefined;
1041
1185
  let msg;
1042
1186
  if (!listening) {
1043
1187
  msg = startError
@@ -1046,6 +1190,8 @@ export async function main() {
1046
1190
  }
1047
1191
  else if (connected) {
1048
1192
  msg = `F-MCP ATezer Bridge: ${clientCount} plugin(s) connected on port ${currentPort}. You can use all figma_* tools.`;
1193
+ if (versionWarning)
1194
+ msg += " " + versionWarning;
1049
1195
  }
1050
1196
  else {
1051
1197
  msg = PLUGIN_NOT_CONNECTED;
@@ -1067,30 +1213,57 @@ export async function main() {
1067
1213
  connectedClients: clientCount,
1068
1214
  connectedFiles,
1069
1215
  bridgePort: currentPort,
1216
+ serverVersion: FMCP_VERSION,
1070
1217
  ...(autoIncremented && { preferredPort: bridge.getPreferredPort(), autoIncremented }),
1071
1218
  message: msg,
1072
1219
  ...(startError && { startError }),
1073
1220
  ...(portHint && { portHint }),
1221
+ ...(versionWarning && { versionWarning }),
1074
1222
  }),
1075
1223
  }],
1076
1224
  };
1077
1225
  });
1078
1226
  // ---- Node Creation Tools ----
1227
+ // v1.8.0: figma_create_frame now supports auto-layout out of the box.
1228
+ // Default layoutMode="VERTICAL" with sensible padding/gap. Pass layoutMode="NONE"
1229
+ // for a free-form frame (legacy behavior).
1079
1230
  server.registerTool("figma_create_frame", {
1080
- description: "Create a new frame node on the current page. Returns the created node ID.",
1231
+ description: "Create a new frame node with optional auto-layout. Returns the created node ID. " +
1232
+ "v1.8.0: defaults to layoutMode='VERTICAL' with paddingTop/Bottom=16, paddingLeft/Right=16, itemSpacing=12, " +
1233
+ "primaryAxisSizingMode='AUTO', counterAxisSizingMode='AUTO'. Pass layoutMode='NONE' for legacy free-form frames.",
1081
1234
  inputSchema: {
1082
1235
  name: z.string().optional().default("Frame").describe("Frame name"),
1083
1236
  x: z.number().optional().describe("X position. If omitted, auto-positions to the right of existing content"),
1084
1237
  y: z.number().optional().default(0),
1085
1238
  width: z.number().optional().default(200),
1086
1239
  height: z.number().optional().default(200),
1087
- fillColor: z.string().optional().describe("Hex color e.g. '#ffffff'"),
1240
+ fillColor: z.string().optional().describe("Hex color e.g. '#ffffff'. DEPRECATED — prefer fillVariableKey for DS token binding (v1.8.1+)."),
1088
1241
  parentId: z.string().optional().describe("Parent node ID (default: current page)"),
1242
+ // Auto-layout parameters (v1.8.0)
1243
+ layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().default("VERTICAL").describe("Auto-layout direction. VERTICAL by default; pass 'NONE' for free-form frames."),
1244
+ paddingTop: z.number().optional().default(16),
1245
+ paddingBottom: z.number().optional().default(16),
1246
+ paddingLeft: z.number().optional().default(16),
1247
+ paddingRight: z.number().optional().default(16),
1248
+ itemSpacing: z.number().optional().default(12).describe("Gap between auto-layout children"),
1249
+ primaryAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().default("AUTO").describe("AUTO = hug contents, FIXED = use width/height"),
1250
+ counterAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().default("AUTO"),
1251
+ primaryAxisAlignItems: z.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]).optional().describe("Main-axis alignment (MIN=top/left, MAX=bottom/right)"),
1252
+ counterAxisAlignItems: z.enum(["MIN", "CENTER", "MAX", "BASELINE"]).optional().describe("Cross-axis alignment"),
1253
+ layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Wrap children when they exceed primary axis"),
1254
+ // v1.8.1+: DS token binding params — PREFER over hardcoded fillColor / padding / radius
1255
+ fillVariableKey: z.string().optional().describe("DS variable key for fill binding (from figma_get_library_variables). Takes precedence over fillColor."),
1256
+ paddingVariableKey: z.string().optional().describe("DS spacing variable key — applies to all 4 paddings via setBoundVariable."),
1257
+ itemSpacingVariableKey: z.string().optional().describe("DS spacing variable key for itemSpacing via setBoundVariable."),
1258
+ cornerRadiusVariableKey: z.string().optional().describe("DS radius variable key for cornerRadius via setBoundVariable."),
1259
+ cornerRadius: z.number().optional().describe("Hardcoded corner radius in px. DEPRECATED — prefer cornerRadiusVariableKey."),
1089
1260
  },
1090
- }, async ({ name, x, y, width, height, fillColor, parentId }) => {
1261
+ }, async ({ name, x, y, width, height, fillColor, parentId, layoutMode, paddingTop, paddingBottom, paddingLeft, paddingRight, itemSpacing, primaryAxisSizingMode, counterAxisSizingMode, primaryAxisAlignItems, counterAxisAlignItems, layoutWrap, fillVariableKey, paddingVariableKey, itemSpacingVariableKey, cornerRadiusVariableKey, cornerRadius }) => {
1091
1262
  try {
1263
+ invalidateCache();
1092
1264
  const conn = getConnector(bridge);
1093
1265
  const autoPosition = x === undefined && !parentId;
1266
+ const useAutoLayout = layoutMode !== "NONE";
1094
1267
  const code = `
1095
1268
  ${autoPosition ? `
1096
1269
  let posX = 0;
@@ -1108,9 +1281,64 @@ export async function main() {
1108
1281
  frame.name = ${JSON.stringify(name)};
1109
1282
  frame.x = posX; frame.y = ${y};
1110
1283
  frame.resize(${width}, ${height});
1111
- ${fillColor ? `frame.fills = [{ type: 'SOLID', color: { r: parseInt('${fillColor}'.slice(1,3),16)/255, g: parseInt('${fillColor}'.slice(3,5),16)/255, b: parseInt('${fillColor}'.slice(5,7),16)/255 } }];` : ""}
1284
+ ${useAutoLayout ? `
1285
+ frame.layoutMode = ${JSON.stringify(layoutMode)};
1286
+ frame.paddingTop = ${paddingTop};
1287
+ frame.paddingBottom = ${paddingBottom};
1288
+ frame.paddingLeft = ${paddingLeft};
1289
+ frame.paddingRight = ${paddingRight};
1290
+ frame.itemSpacing = ${itemSpacing};
1291
+ frame.primaryAxisSizingMode = ${JSON.stringify(primaryAxisSizingMode)};
1292
+ frame.counterAxisSizingMode = ${JSON.stringify(counterAxisSizingMode)};
1293
+ ${primaryAxisAlignItems ? `frame.primaryAxisAlignItems = ${JSON.stringify(primaryAxisAlignItems)};` : ""}
1294
+ ${counterAxisAlignItems ? `frame.counterAxisAlignItems = ${JSON.stringify(counterAxisAlignItems)};` : ""}
1295
+ ${layoutWrap ? `frame.layoutWrap = ${JSON.stringify(layoutWrap)};` : ""}
1296
+ ` : ""}
1297
+ ${cornerRadius != null ? `frame.cornerRadius = ${cornerRadius};` : ""}
1298
+ // v1.8.1: DS token binding takes precedence over hardcoded values
1299
+ ${fillVariableKey ? `
1300
+ try {
1301
+ const fillVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(fillVariableKey)});
1302
+ const baseFill = { type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 1 };
1303
+ const boundFill = figma.variables.setBoundVariableForPaint(baseFill, 'color', fillVar);
1304
+ frame.fills = [boundFill];
1305
+ } catch (fillBindErr) {
1306
+ console.warn('[figma_create_frame] fillVariableKey binding failed:', fillBindErr.message);
1307
+ }
1308
+ ` : fillColor ? `frame.fills = [{ type: 'SOLID', color: { r: parseInt('${fillColor}'.slice(1,3),16)/255, g: parseInt('${fillColor}'.slice(3,5),16)/255, b: parseInt('${fillColor}'.slice(5,7),16)/255 } }];` : ""}
1309
+ ${paddingVariableKey ? `
1310
+ try {
1311
+ const padVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(paddingVariableKey)});
1312
+ frame.setBoundVariable('paddingTop', padVar);
1313
+ frame.setBoundVariable('paddingBottom', padVar);
1314
+ frame.setBoundVariable('paddingLeft', padVar);
1315
+ frame.setBoundVariable('paddingRight', padVar);
1316
+ } catch (padBindErr) {
1317
+ console.warn('[figma_create_frame] paddingVariableKey binding failed:', padBindErr.message);
1318
+ }
1319
+ ` : ""}
1320
+ ${itemSpacingVariableKey ? `
1321
+ try {
1322
+ const gapVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(itemSpacingVariableKey)});
1323
+ frame.setBoundVariable('itemSpacing', gapVar);
1324
+ } catch (gapBindErr) {
1325
+ console.warn('[figma_create_frame] itemSpacingVariableKey binding failed:', gapBindErr.message);
1326
+ }
1327
+ ` : ""}
1328
+ ${cornerRadiusVariableKey ? `
1329
+ try {
1330
+ const radVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(cornerRadiusVariableKey)});
1331
+ frame.setBoundVariable('topLeftRadius', radVar);
1332
+ frame.setBoundVariable('topRightRadius', radVar);
1333
+ frame.setBoundVariable('bottomLeftRadius', radVar);
1334
+ frame.setBoundVariable('bottomRightRadius', radVar);
1335
+ } catch (radBindErr) {
1336
+ console.warn('[figma_create_frame] cornerRadiusVariableKey binding failed:', radBindErr.message);
1337
+ }
1338
+ ` : ""}
1112
1339
  ${parentId ? `const parent = await figma.getNodeByIdAsync(${JSON.stringify(parentId)}); if (parent && 'appendChild' in parent) parent.appendChild(frame);` : ""}
1113
- return { id: frame.id, name: frame.name, width: frame.width, height: frame.height, x: frame.x, y: frame.y };
1340
+ const boundCount = frame.boundVariables ? Object.keys(frame.boundVariables).length : 0;
1341
+ return { id: frame.id, name: frame.name, width: frame.width, height: frame.height, x: frame.x, y: frame.y, layoutMode: frame.layoutMode, boundVariableCount: boundCount };
1114
1342
  `;
1115
1343
  const result = await conn.executeCodeViaUI(code, 10000);
1116
1344
  return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
@@ -1218,11 +1446,12 @@ export async function main() {
1218
1446
  server.registerTool("figma_export_nodes", {
1219
1447
  description: "Export one or multiple nodes as SVG, PNG, JPG, or PDF. Returns base64-encoded data for each node. " +
1220
1448
  "Supports batch export (up to 50 nodes). No REST API token needed — uses plugin exportAsync. " +
1221
- "SVG preserves vectors; PNG/JPG are rasterized at configurable scale.",
1449
+ "SVG preserves vectors; PNG/JPG are rasterized at configurable scale. " +
1450
+ "v1.8.0: default scale=1 for context safety (was 2). Override for high-DPI exports.",
1222
1451
  inputSchema: {
1223
1452
  nodeIds: z.array(z.string()).min(1).max(50).describe("Node IDs to export (1-50)"),
1224
1453
  format: z.enum(["PNG", "SVG", "JPG", "PDF"]).optional().default("PNG").describe("Export format"),
1225
- scale: z.number().min(0.5).max(4).optional().default(2).describe("Scale factor (0.5-4, default 2)"),
1454
+ scale: z.number().min(0.5).max(4).optional().default(LEGACY_DEFAULTS ? 2 : 1).describe("Scale factor (0.5-4, default 1)"),
1226
1455
  svgOutlineText: z.boolean().optional().describe("SVG: render text as outlines (default true)"),
1227
1456
  svgIncludeId: z.boolean().optional().describe("SVG: include node IDs in attributes"),
1228
1457
  },
@@ -1277,9 +1506,13 @@ export async function main() {
1277
1506
  });
1278
1507
  // ---- figma_search_assets (team library search via plugin) ----
1279
1508
  server.registerTool("figma_search_assets", {
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. " +
1509
+ description: "Search for design system assets in the current Figma file. Returns: " +
1510
+ "(1) team library VARIABLES via figma.teamLibrary API (all enabled libraries), " +
1511
+ "(2) file-local COMPONENTS / COMPONENT_SETS, and " +
1512
+ "(3) v1.8.0+: REMOTE LIBRARY COMPONENTS discovered by scanning existing INSTANCE nodes (returned as 'libraryComponents'). " +
1513
+ "For library components to appear, at least one DS instance must exist in the file — place one manually first if empty. " +
1514
+ "Pass currentPageOnly=false to scan all pages for instance discovery. " +
1515
+ "Use the returned componentKey with figma_instantiate_component to place new instances. " +
1283
1516
  "Pass assetTypes to filter: ['variables'], ['components'], or both (default).",
1284
1517
  inputSchema: {
1285
1518
  figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),