@atezer/figma-mcp-bridge 1.9.8 → 1.9.11

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.
@@ -2323,6 +2323,347 @@ export async function main() {
2323
2323
  isError: true,
2324
2324
  };
2325
2325
  });
2326
+ // ---- v1.9.9: Figma Prototype Connections + Animations (FUTURE.md:176-254) ----
2327
+ server.registerTool("figma_create_prototype_connection", {
2328
+ description: "Create a prototype reaction between two nodes (source -> destination). " +
2329
+ "Maps Figma Prototype panel: trigger (On click/hover/press/drag, After delay, Mouse events, Key/gamepad) + " +
2330
+ "action (Navigate/Overlay/Swap/Back/Close/Change to/Scroll to/Open link) + " +
2331
+ "transition type (Dissolve/Smart animate/Scroll animate/Move in-out/Push/Slide in-out) with direction (LEFT/RIGHT/TOP/BOTTOM) + " +
2332
+ "easing + duration (ms, converted to seconds internally) + matchLayers (only on DirectionalTransition per Figma schema). " +
2333
+ "Uses Figma Plugin API setReactionsAsync (reactions property is readonly in 2024+ API). " +
2334
+ "v1 scope: SET_VARIABLE, SET_VARIABLE_MODE, UPDATE_MEDIA_RUNTIME, CONDITIONAL actions are NOT included (future release). " +
2335
+ "Overlay background/close-on-outside are readonly in Plugin API — configure in Figma UI (Prototype tab -> Advanced -> Overlay).",
2336
+ inputSchema: {
2337
+ sourceNodeId: z.string().describe("Source node id (FRAME/INSTANCE/COMPONENT/GROUP etc.) that will receive the reaction."),
2338
+ destinationNodeId: z.string().optional().describe("Destination FRAME id. Required for NAVIGATE/OVERLAY/SWAP/SCROLL_TO/CHANGE_TO; omitted for BACK/CLOSE/URL."),
2339
+ trigger: z.enum(["ON_CLICK", "ON_HOVER", "ON_PRESS", "ON_DRAG", "AFTER_TIMEOUT", "MOUSE_ENTER", "MOUSE_LEAVE", "MOUSE_UP", "MOUSE_DOWN", "ON_KEY_DOWN"]).optional().default("ON_CLICK"),
2340
+ timeout: z.number().optional().describe("Milliseconds — required when trigger=AFTER_TIMEOUT (default 1000)."),
2341
+ mouseDelay: z.number().optional().describe("Seconds — optional hold-delay for MOUSE_* triggers (e.g. 0.3 = 300ms hover before firing)."),
2342
+ keyCodes: z.array(z.number()).optional().describe("Required when trigger=ON_KEY_DOWN (e.g. [13]=Enter, [27]=Escape, [32]=Space)."),
2343
+ device: z.enum(["KEYBOARD", "XBOX_ONE", "PS4", "SWITCH_PRO", "UNKNOWN_CONTROLLER"]).optional().default("KEYBOARD").describe("Input device for ON_KEY_DOWN."),
2344
+ action: z.enum(["NAVIGATE", "OVERLAY", "SWAP", "BACK", "CLOSE", "SCROLL_TO", "CHANGE_TO", "URL"]).optional().default("NAVIGATE").describe("BACK action: Figma transition param'ını IGNORE eder ve önceki NAVIGATE'in yönünü otomatik ters uygular (audit'te transition: null görünür, Present modda doğru animasyon oynar)."),
2345
+ url: z.string().optional().describe("Required when action=URL."),
2346
+ transitionType: z.enum(["INSTANT", "DISSOLVE", "SMART_ANIMATE", "SCROLL_ANIMATE", "MOVE_IN", "MOVE_OUT", "PUSH", "SLIDE_IN", "SLIDE_OUT"]).optional().default("INSTANT").describe("INSTANT -> transition: null (no Figma INSTANT type). Directional types (MOVE_IN/OUT, PUSH, SLIDE_IN/OUT) require 'direction'."),
2347
+ direction: z.enum(["LEFT", "RIGHT", "TOP", "BOTTOM"]).optional().describe("Required for MOVE_IN/MOVE_OUT/PUSH/SLIDE_IN/SLIDE_OUT transitions."),
2348
+ matchLayers: z.boolean().optional().default(false).describe("DirectionalTransition only (SLIDE_IN/MOVE_IN/PUSH/...) — key is REQUIRED by Figma schema (always injected, value from this param). true = smart layer morph on top of directional transition. INVALID for SMART_ANIMATE (SimpleTransition rejects it)."),
2349
+ duration: z.number().optional().default(300).describe("Transition duration in ms; converted to seconds for Plugin API."),
2350
+ easing: z.enum(["EASE_IN", "EASE_OUT", "EASE_IN_AND_OUT", "LINEAR", "GENTLE", "QUICK", "BOUNCY", "SLOW", "EASE_IN_BACK", "EASE_OUT_BACK", "EASE_IN_AND_OUT_BACK"]).optional().default("EASE_OUT"),
2351
+ preserveScrollPosition: z.boolean().optional().default(false),
2352
+ overlayRelativePosition: z.object({ x: z.number(), y: z.number() }).optional().describe("OVERLAY action only — free overlay position. Requires destination frame's overlayPositionType=MANUAL (set in Figma UI)."),
2353
+ replace: z.boolean().optional().default(false).describe("true: replace reactions array; false (default): append."),
2354
+ },
2355
+ }, async ({ sourceNodeId, destinationNodeId, trigger, timeout, mouseDelay, keyCodes, device, action, url, transitionType, direction, matchLayers, duration, easing, preserveScrollPosition, overlayRelativePosition, replace }) => {
2356
+ try {
2357
+ invalidateCache();
2358
+ // TS-side param validation (fail fast, before hitting the plugin)
2359
+ if (trigger === "ON_KEY_DOWN" && (!keyCodes || keyCodes.length === 0)) {
2360
+ throw new Error("KEYCODES_REQUIRED: trigger=ON_KEY_DOWN için keyCodes (en az 1 tuş kodu) gerekli");
2361
+ }
2362
+ const directionalTypes = ["MOVE_IN", "MOVE_OUT", "PUSH", "SLIDE_IN", "SLIDE_OUT"];
2363
+ if (directionalTypes.includes(transitionType) && !direction) {
2364
+ throw new Error(`DIRECTION_REQUIRED: transitionType=${transitionType} için direction (LEFT/RIGHT/TOP/BOTTOM) gerekli`);
2365
+ }
2366
+ const conn = getConnector(bridge);
2367
+ // Trigger extras (inject into the generated JS as an object literal fragment)
2368
+ let triggerExtras = "";
2369
+ if (trigger === "AFTER_TIMEOUT") {
2370
+ triggerExtras = `, timeout: ${timeout ?? 1000}`;
2371
+ }
2372
+ else if (trigger === "ON_KEY_DOWN") {
2373
+ triggerExtras = `, device: ${JSON.stringify(device)}, keyCodes: ${JSON.stringify(keyCodes ?? [])}`;
2374
+ }
2375
+ else if (["MOUSE_UP", "MOUSE_DOWN", "MOUSE_ENTER", "MOUSE_LEAVE"].includes(trigger) && mouseDelay !== undefined) {
2376
+ triggerExtras = `, delay: ${mouseDelay}`;
2377
+ }
2378
+ const code = `
2379
+ const src = await figma.getNodeByIdAsync(${JSON.stringify(sourceNodeId)});
2380
+ if (!src) throw new Error("SOURCE_NOT_FOUND: " + ${JSON.stringify(sourceNodeId)});
2381
+ if (typeof src.setReactionsAsync !== "function") throw new Error("UNSUPPORTED_NODE_TYPE: " + src.type);
2382
+
2383
+ const NO_DEST = ["BACK","CLOSE","URL"].indexOf(${JSON.stringify(action)}) !== -1;
2384
+ let destId = null;
2385
+ if (!NO_DEST) {
2386
+ if (!${JSON.stringify(destinationNodeId || "")}) throw new Error("DESTINATION_REQUIRED: action=" + ${JSON.stringify(action)});
2387
+ const dst = await figma.getNodeByIdAsync(${JSON.stringify(destinationNodeId || "")});
2388
+ if (!dst) throw new Error("DESTINATION_NOT_FOUND: " + ${JSON.stringify(destinationNodeId || "")});
2389
+ if (${JSON.stringify(action)} === "NAVIGATE" && dst.type !== "FRAME")
2390
+ throw new Error("NAVIGATE_REQUIRES_FRAME: dst=" + dst.type);
2391
+ // v1.9.10 preflight: OVERLAY action destination frame needs overlayPositionType set in Figma UI
2392
+ // Plugin API can't write overlayPositionType/overlayBackgroundInteraction/overlayBackground (readonly).
2393
+ if (${JSON.stringify(action)} === "OVERLAY" && dst.type === "FRAME" && ("overlayPositionType" in dst) && dst.overlayPositionType === "NONE") {
2394
+ throw new Error("OVERLAY_FRAME_NOT_CONFIGURED: destinationNodeId=" + ${JSON.stringify(destinationNodeId || "")} + " Figma'da overlay olarak isaretli degil. Cozum: frame'i sec -> Prototype tab -> Advanced -> 'Overlay' ac -> Position sec. Plugin API bu ayari yazamiyor (readonly).");
2395
+ }
2396
+ destId = dst.id;
2397
+ }
2398
+
2399
+ const triggerObj = { type: ${JSON.stringify(trigger)}${triggerExtras} };
2400
+
2401
+ let actionObj;
2402
+ if (${JSON.stringify(action)} === "BACK") actionObj = { type: "BACK" };
2403
+ else if (${JSON.stringify(action)} === "CLOSE") actionObj = { type: "CLOSE" };
2404
+ else if (${JSON.stringify(action)} === "URL") actionObj = { type: "URL", url: ${JSON.stringify(url || "")} };
2405
+ else {
2406
+ const navMap = { NAVIGATE: "NAVIGATE", OVERLAY: "OVERLAY", SWAP: "SWAP", SCROLL_TO: "SCROLL_TO", CHANGE_TO: "CHANGE_TO" };
2407
+ const tranType = ${JSON.stringify(transitionType)};
2408
+ const isInstant = tranType === "INSTANT";
2409
+ const isDirectional = ["MOVE_IN","MOVE_OUT","PUSH","SLIDE_IN","SLIDE_OUT"].indexOf(tranType) !== -1;
2410
+ let transitionObj = null;
2411
+ if (!isInstant) {
2412
+ transitionObj = { type: tranType, easing: { type: ${JSON.stringify(easing)} }, duration: ${duration} / 1000 };
2413
+ if (isDirectional) {
2414
+ transitionObj.direction = ${JSON.stringify(direction || "RIGHT")};
2415
+ // v1.9.11 fix: matchLayers key REQUIRED on DirectionalTransition (Figma schema).
2416
+ // Value can be true OR false, but the key must be present (canlı test bulgusu).
2417
+ // SMART_ANIMATE and other SimpleTransition: matchLayers NOT allowed.
2418
+ transitionObj.matchLayers = ${matchLayers};
2419
+ }
2420
+ // SMART_ANIMATE and other SimpleTransition types: do NOT inject matchLayers.
2421
+ }
2422
+ actionObj = {
2423
+ type: "NODE",
2424
+ destinationId: destId,
2425
+ navigation: navMap[${JSON.stringify(action)}],
2426
+ transition: transitionObj,
2427
+ preserveScrollPosition: ${preserveScrollPosition}
2428
+ };
2429
+ ${overlayRelativePosition ? `if (${JSON.stringify(action)} === "OVERLAY") actionObj.overlayRelativePosition = ${JSON.stringify(overlayRelativePosition)};` : ""}
2430
+ // v1.9.10 NOT: overlayCloseOnClickOutside and overlayBackgroundColor params removed
2431
+ // — FrameNode.overlayBackgroundInteraction and overlayBackground are readonly in Plugin API.
2432
+ // User must configure these in Figma UI (Prototype tab -> Advanced -> Overlay).
2433
+ }
2434
+
2435
+ const reaction = { trigger: triggerObj, actions: [actionObj] };
2436
+ const current = ${replace} ? [] : ((src.reactions) || []).slice();
2437
+ current.push(reaction);
2438
+ await src.setReactionsAsync(current);
2439
+ const after = (src.reactions) || [];
2440
+ return { id: src.id, reactionsCount: after.length, trigger: triggerObj.type, action: actionObj.type, destinationId: destId, transitionType: ${JSON.stringify(transitionType)}, direction: ${JSON.stringify(direction || null)} };
2441
+ `;
2442
+ const result = await conn.executeCodeViaUI(code, 10000);
2443
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
2444
+ }
2445
+ catch (err) {
2446
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
2447
+ }
2448
+ });
2449
+ server.registerTool("figma_get_prototype_connections", {
2450
+ description: "Read prototype reactions on a node subtree or the whole current page. " +
2451
+ "Read-only audit — returns trigger, action, navigation, destinationId, transition, flowStartingPoints. " +
2452
+ "Uses getReactionsAsync if available, falls back to node.reactions getter. " +
2453
+ "At least one of nodeId or pageScope=true must be provided.",
2454
+ inputSchema: {
2455
+ nodeId: z.string().optional().describe("Scans this node and its descendants."),
2456
+ pageScope: z.boolean().optional().default(false).describe("true: scan the entire current page. Either nodeId or pageScope is required."),
2457
+ includeFlowStartingPoints: z.boolean().optional().default(true),
2458
+ },
2459
+ annotations: { readOnlyHint: true },
2460
+ }, async ({ nodeId, pageScope, includeFlowStartingPoints }) => {
2461
+ try {
2462
+ const conn = getConnector(bridge);
2463
+ const code = `
2464
+ if (!${JSON.stringify(nodeId || "")} && !${pageScope}) throw new Error("MISSING_SCOPE: nodeId veya pageScope=true gerekli");
2465
+
2466
+ const getR = async (n) => {
2467
+ if (typeof n.getReactionsAsync === "function") return await n.getReactionsAsync();
2468
+ return n.reactions || [];
2469
+ };
2470
+
2471
+ const summarize = (n, reactions) => ({
2472
+ nodeId: n.id, name: n.name, type: n.type,
2473
+ reactions: (reactions || []).map(r => ({
2474
+ trigger: r.trigger,
2475
+ actions: (r.actions || []).map(a => ({
2476
+ type: a.type,
2477
+ navigation: a.navigation || null,
2478
+ destinationId: a.destinationId || null,
2479
+ transition: a.transition ? { type: a.transition.type, direction: a.transition.direction || null, matchLayers: a.transition.matchLayers || false, easing: a.transition.easing && a.transition.easing.type, duration: a.transition.duration } : null,
2480
+ url: a.url || null,
2481
+ preserveScrollPosition: a.preserveScrollPosition || false,
2482
+ overlayRelativePosition: a.overlayRelativePosition || null
2483
+ }))
2484
+ }))
2485
+ });
2486
+
2487
+ const results = [];
2488
+ const hasReactions = (n) => typeof n.setReactionsAsync === "function" || "reactions" in n;
2489
+
2490
+ if (${JSON.stringify(nodeId || "")}) {
2491
+ const root = await figma.getNodeByIdAsync(${JSON.stringify(nodeId || "")});
2492
+ if (!root) throw new Error("NODE_NOT_FOUND: " + ${JSON.stringify(nodeId || "")});
2493
+ if (hasReactions(root)) {
2494
+ const r = await getR(root);
2495
+ if (r.length) results.push(summarize(root, r));
2496
+ }
2497
+ if ("findAll" in root) {
2498
+ const descendants = root.findAll(n => hasReactions(n));
2499
+ for (const n of descendants) {
2500
+ const r = await getR(n);
2501
+ if (r.length) results.push(summarize(n, r));
2502
+ }
2503
+ }
2504
+ } else if (${pageScope}) {
2505
+ const all = figma.currentPage.findAll(n => hasReactions(n));
2506
+ for (const n of all) {
2507
+ const r = await getR(n);
2508
+ if (r.length) results.push(summarize(n, r));
2509
+ }
2510
+ }
2511
+ const fsps = ${includeFlowStartingPoints} ? (figma.currentPage.flowStartingPoints || []) : [];
2512
+ return { connections: results, totalReactions: results.reduce((s, r) => s + r.reactions.length, 0), flowStartingPoints: fsps };
2513
+ `;
2514
+ const result = await conn.executeCodeViaUI(code, 15000);
2515
+ return toolResult(result, "figma_get_prototype_connections");
2516
+ }
2517
+ catch (err) {
2518
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
2519
+ }
2520
+ });
2521
+ server.registerTool("figma_set_flow_starting_point", {
2522
+ description: "Mark a FRAME as a prototype flow starting point (shown in Figma Prototype panel). " +
2523
+ "Uses page.setFlowStartingPointsAsync if available (future API), falls back to direct assignment. " +
2524
+ "Description is stored via pluginData (Figma FlowStartingPoint shape is { nodeId, name } only).",
2525
+ inputSchema: {
2526
+ nodeId: z.string().describe("FRAME node id to mark as a starting point."),
2527
+ name: z.string().describe("Flow name shown in Prototype panel (e.g. 'Login Akışı')."),
2528
+ description: z.string().optional().default(""),
2529
+ replace: z.boolean().optional().default(false).describe("true: replace entire array; false (default): append or update same nodeId."),
2530
+ },
2531
+ }, async ({ nodeId, name, description, replace }) => {
2532
+ try {
2533
+ invalidateCache();
2534
+ const conn = getConnector(bridge);
2535
+ const code = `
2536
+ const node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
2537
+ if (!node) throw new Error("NODE_NOT_FOUND: " + ${JSON.stringify(nodeId)});
2538
+ if (node.type !== "FRAME") throw new Error("FLOW_REQUIRES_FRAME: " + node.type);
2539
+ const page = node.parent && node.parent.type === "PAGE" ? node.parent : figma.currentPage;
2540
+ const existing = (page.flowStartingPoints || []).slice();
2541
+ const entry = { nodeId: node.id, name: ${JSON.stringify(name)} };
2542
+ const idx = existing.findIndex(fsp => fsp.nodeId === node.id);
2543
+ let nextArr;
2544
+ if (${replace}) nextArr = [entry];
2545
+ else if (idx === -1) { existing.push(entry); nextArr = existing; }
2546
+ else { existing[idx] = entry; nextArr = existing; }
2547
+
2548
+ if (typeof page.setFlowStartingPointsAsync === "function") {
2549
+ await page.setFlowStartingPointsAsync(nextArr);
2550
+ } else {
2551
+ page.flowStartingPoints = nextArr;
2552
+ }
2553
+ if (${JSON.stringify(description)}) node.setPluginData("flow.description", ${JSON.stringify(description)});
2554
+ return { id: node.id, name: ${JSON.stringify(name)}, total: (page.flowStartingPoints || []).length };
2555
+ `;
2556
+ const result = await conn.executeCodeViaUI(code, 10000);
2557
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
2558
+ }
2559
+ catch (err) {
2560
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
2561
+ }
2562
+ });
2563
+ server.registerTool("figma_create_interaction", {
2564
+ description: "Create a variant state-change interaction on an INSTANCE/variant node (hover/press/focus -> variant). " +
2565
+ "Uses reactions API with navigation: CHANGE_TO. Target variant resolved by id or name within the same COMPONENT_SET. " +
2566
+ "Uses getMainComponentAsync for deprecation-safe main component access. " +
2567
+ "v1 supports INSTANT/DISSOLVE/SMART_ANIMATE transitions (DirectionalTransition not applicable to variants).",
2568
+ inputSchema: {
2569
+ nodeId: z.string().describe("INSTANCE or variant node id."),
2570
+ trigger: z.enum(["ON_HOVER", "ON_PRESS", "MOUSE_ENTER", "MOUSE_LEAVE", "MOUSE_DOWN", "MOUSE_UP", "ON_CLICK"]).optional().default("ON_HOVER"),
2571
+ targetVariantId: z.string().optional(),
2572
+ targetVariantName: z.string().optional().describe("Variant name (e.g. 'State=Hover') — resolved within the source's COMPONENT_SET."),
2573
+ transitionType: z.enum(["INSTANT", "DISSOLVE", "SMART_ANIMATE"]).optional().default("SMART_ANIMATE").describe("SMART_ANIMATE inherently matches layers — no explicit matchLayers param needed (Figma schema rejects it)."),
2574
+ duration: z.number().optional().default(150),
2575
+ easing: z.enum(["EASE_IN", "EASE_OUT", "EASE_IN_AND_OUT", "LINEAR", "GENTLE", "QUICK", "BOUNCY", "SLOW"]).optional().default("EASE_IN"),
2576
+ },
2577
+ }, async ({ nodeId, trigger, targetVariantId, targetVariantName, transitionType, duration, easing }) => {
2578
+ try {
2579
+ invalidateCache();
2580
+ const conn = getConnector(bridge);
2581
+ const code = `
2582
+ const node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
2583
+ if (!node) throw new Error("NODE_NOT_FOUND: " + ${JSON.stringify(nodeId)});
2584
+ if (typeof node.setReactionsAsync !== "function") throw new Error("UNSUPPORTED_NODE_TYPE: " + node.type);
2585
+
2586
+ let targetId = ${JSON.stringify(targetVariantId || "")};
2587
+ if (!targetId) {
2588
+ const mc = node.type === "INSTANCE" ? await node.getMainComponentAsync() : node;
2589
+ const set = mc && mc.parent && mc.parent.type === "COMPONENT_SET" ? mc.parent : null;
2590
+ if (!set) throw new Error("NO_COMPONENT_SET");
2591
+ const match = set.children.find(c => c.name === ${JSON.stringify(targetVariantName || "")});
2592
+ if (!match) throw new Error("VARIANT_NOT_FOUND: " + ${JSON.stringify(targetVariantName || "")});
2593
+ targetId = match.id;
2594
+ }
2595
+
2596
+ const tranType = ${JSON.stringify(transitionType)};
2597
+ let transitionObj = null;
2598
+ if (tranType !== "INSTANT") {
2599
+ // v1.9.10 fix: NO matchLayers for SMART_ANIMATE / DISSOLVE — Figma schema rejects it on SimpleTransition.
2600
+ transitionObj = { type: tranType, easing: { type: ${JSON.stringify(easing)} }, duration: ${duration} / 1000 };
2601
+ }
2602
+ const reaction = {
2603
+ trigger: { type: ${JSON.stringify(trigger)} },
2604
+ actions: [{
2605
+ type: "NODE",
2606
+ destinationId: targetId,
2607
+ navigation: "CHANGE_TO",
2608
+ transition: transitionObj,
2609
+ preserveScrollPosition: false
2610
+ }]
2611
+ };
2612
+ const current = (node.reactions || []).slice();
2613
+ current.push(reaction);
2614
+ await node.setReactionsAsync(current);
2615
+ const after = (node.reactions) || [];
2616
+ return { id: node.id, targetVariantId: targetId, reactionsCount: after.length };
2617
+ `;
2618
+ const result = await conn.executeCodeViaUI(code, 10000);
2619
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
2620
+ }
2621
+ catch (err) {
2622
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
2623
+ }
2624
+ });
2625
+ server.registerTool("figma_set_scroll_behavior", {
2626
+ description: "Set a node's scroll behavior (Figma Prototype panel > Scroll behavior). " +
2627
+ "overflowDirection (FrameNode): NONE/HORIZONTAL/VERTICAL/BOTH — defines prototype scroll axis. " +
2628
+ "scrollBehavior (SceneNode): SCROLLS/FIXED/STICKY_SCROLLS — FIXED = sticky header, STICKY_SCROLLS = becomes sticky after scrolling past. " +
2629
+ "At least one of overflowDirection or scrollBehavior must be provided.",
2630
+ inputSchema: {
2631
+ nodeId: z.string().describe("Target node id (FRAME/COMPONENT/COMPONENT_SET/INSTANCE for overflowDirection; any SceneNode for scrollBehavior)."),
2632
+ overflowDirection: z.enum(["NONE", "HORIZONTAL", "VERTICAL", "BOTH"]).optional(),
2633
+ scrollBehavior: z.enum(["SCROLLS", "FIXED", "STICKY_SCROLLS"]).optional(),
2634
+ },
2635
+ }, async ({ nodeId, overflowDirection, scrollBehavior }) => {
2636
+ try {
2637
+ if (!overflowDirection && !scrollBehavior) {
2638
+ throw new Error("MISSING_PARAM: overflowDirection veya scrollBehavior'dan en az biri verilmeli");
2639
+ }
2640
+ invalidateCache();
2641
+ const conn = getConnector(bridge);
2642
+ const code = `
2643
+ const node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
2644
+ if (!node) throw new Error("NODE_NOT_FOUND: " + ${JSON.stringify(nodeId)});
2645
+ const results = {};
2646
+ ${overflowDirection ? `
2647
+ if (node.type !== "FRAME" && node.type !== "COMPONENT" && node.type !== "COMPONENT_SET" && node.type !== "INSTANCE") {
2648
+ throw new Error("OVERFLOW_REQUIRES_FRAME_LIKE: " + node.type);
2649
+ }
2650
+ node.overflowDirection = ${JSON.stringify(overflowDirection)};
2651
+ results.overflowDirection = node.overflowDirection;
2652
+ ` : ""}
2653
+ ${scrollBehavior ? `
2654
+ if (!("scrollBehavior" in node)) throw new Error("SCROLL_BEHAVIOR_UNSUPPORTED: " + node.type);
2655
+ node.scrollBehavior = ${JSON.stringify(scrollBehavior)};
2656
+ results.scrollBehavior = node.scrollBehavior;
2657
+ ` : ""}
2658
+ return { id: node.id, ...results };
2659
+ `;
2660
+ const result = await conn.executeCodeViaUI(code, 10000);
2661
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
2662
+ }
2663
+ catch (err) {
2664
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
2665
+ }
2666
+ });
2326
2667
  server.registerTool("figma_get_rest_token_status", {
2327
2668
  description: "Check if a Figma REST API token is set and view rate limit usage.",
2328
2669
  inputSchema: {},