Skip to main content
Glama
paragdesai1

Cursor Talk to Figma MCP

by paragdesai1

scan_text_nodes

Extract all text content from a selected Figma design element for analysis or processing.

Instructions

Scan all text nodes in the selected Figma node

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nodeIdYesID of the node to scan

Implementation Reference

  • Core handler function that implements scan_text_nodes by recursively traversing Figma node tree, collecting all TEXT nodes with their properties (id, name, characters, fonts, bounds, hierarchy path). Supports chunked processing for large designs with progress reporting via sendProgressUpdate.
    async function scanTextNodes(params) {
      console.log(`Starting to scan text nodes from node ID: ${params.nodeId}`);
      const {
        nodeId,
        useChunking = true,
        chunkSize = 10,
        commandId = generateCommandId(),
      } = params || {};
    
      const node = await figma.getNodeByIdAsync(nodeId);
    
      if (!node) {
        console.error(`Node with ID ${nodeId} not found`);
        // Send error progress update
        sendProgressUpdate(
          commandId,
          "scan_text_nodes",
          "error",
          0,
          0,
          0,
          `Node with ID ${nodeId} not found`,
          { error: `Node not found: ${nodeId}` }
        );
        throw new Error(`Node with ID ${nodeId} not found`);
      }
    
      // If chunking is not enabled, use the original implementation
      if (!useChunking) {
        const textNodes = [];
        try {
          // Send started progress update
          sendProgressUpdate(
            commandId,
            "scan_text_nodes",
            "started",
            0,
            1, // Not known yet how many nodes there are
            0,
            `Starting scan of node "${node.name || nodeId}" without chunking`,
            null
          );
    
          await findTextNodes(node, [], 0, textNodes);
    
          // Send completed progress update
          sendProgressUpdate(
            commandId,
            "scan_text_nodes",
            "completed",
            100,
            textNodes.length,
            textNodes.length,
            `Scan complete. Found ${textNodes.length} text nodes.`,
            { textNodes }
          );
    
          return {
            success: true,
            message: `Scanned ${textNodes.length} text nodes.`,
            count: textNodes.length,
            textNodes: textNodes,
            commandId,
          };
        } catch (error) {
          console.error("Error scanning text nodes:", error);
    
          // Send error progress update
          sendProgressUpdate(
            commandId,
            "scan_text_nodes",
            "error",
            0,
            0,
            0,
            `Error scanning text nodes: ${error.message}`,
            { error: error.message }
          );
    
          throw new Error(`Error scanning text nodes: ${error.message}`);
        }
      }
    
      // Chunked implementation
      console.log(`Using chunked scanning with chunk size: ${chunkSize}`);
    
      // First, collect all nodes to process (without processing them yet)
      const nodesToProcess = [];
    
      // Send started progress update
      sendProgressUpdate(
        commandId,
        "scan_text_nodes",
        "started",
        0,
        0, // Not known yet how many nodes there are
        0,
        `Starting chunked scan of node "${node.name || nodeId}"`,
        { chunkSize }
      );
    
      await collectNodesToProcess(node, [], 0, nodesToProcess);
    
      const totalNodes = nodesToProcess.length;
      console.log(`Found ${totalNodes} total nodes to process`);
    
      // Calculate number of chunks needed
      const totalChunks = Math.ceil(totalNodes / chunkSize);
      console.log(`Will process in ${totalChunks} chunks`);
    
      // Send update after node collection
      sendProgressUpdate(
        commandId,
        "scan_text_nodes",
        "in_progress",
        5, // 5% progress for collection phase
        totalNodes,
        0,
        `Found ${totalNodes} nodes to scan. Will process in ${totalChunks} chunks.`,
        {
          totalNodes,
          totalChunks,
          chunkSize,
        }
      );
    
      // Process nodes in chunks
      const allTextNodes = [];
      let processedNodes = 0;
      let chunksProcessed = 0;
    
      for (let i = 0; i < totalNodes; i += chunkSize) {
        const chunkEnd = Math.min(i + chunkSize, totalNodes);
        console.log(
          `Processing chunk ${chunksProcessed + 1}/${totalChunks} (nodes ${i} to ${
            chunkEnd - 1
          })`
        );
    
        // Send update before processing chunk
        sendProgressUpdate(
          commandId,
          "scan_text_nodes",
          "in_progress",
          Math.round(5 + (chunksProcessed / totalChunks) * 90), // 5-95% for processing
          totalNodes,
          processedNodes,
          `Processing chunk ${chunksProcessed + 1}/${totalChunks}`,
          {
            currentChunk: chunksProcessed + 1,
            totalChunks,
            textNodesFound: allTextNodes.length,
          }
        );
    
        const chunkNodes = nodesToProcess.slice(i, chunkEnd);
        const chunkTextNodes = [];
    
        // Process each node in this chunk
        for (const nodeInfo of chunkNodes) {
          if (nodeInfo.node.type === "TEXT") {
            try {
              const textNodeInfo = await processTextNode(
                nodeInfo.node,
                nodeInfo.parentPath,
                nodeInfo.depth
              );
              if (textNodeInfo) {
                chunkTextNodes.push(textNodeInfo);
              }
            } catch (error) {
              console.error(`Error processing text node: ${error.message}`);
              // Continue with other nodes
            }
          }
    
          // Brief delay to allow UI updates and prevent freezing
          await delay(5);
        }
    
        // Add results from this chunk
        allTextNodes.push(...chunkTextNodes);
        processedNodes += chunkNodes.length;
        chunksProcessed++;
    
        // Send update after processing chunk
        sendProgressUpdate(
          commandId,
          "scan_text_nodes",
          "in_progress",
          Math.round(5 + (chunksProcessed / totalChunks) * 90), // 5-95% for processing
          totalNodes,
          processedNodes,
          `Processed chunk ${chunksProcessed}/${totalChunks}. Found ${allTextNodes.length} text nodes so far.`,
          {
            currentChunk: chunksProcessed,
            totalChunks,
            processedNodes,
            textNodesFound: allTextNodes.length,
            chunkResult: chunkTextNodes,
          }
        );
    
        // Small delay between chunks to prevent UI freezing
        if (i + chunkSize < totalNodes) {
          await delay(50);
        }
      }
    
      // Send completed progress update
      sendProgressUpdate(
        commandId,
        "scan_text_nodes",
        "completed",
        100,
        totalNodes,
        processedNodes,
        `Scan complete. Found ${allTextNodes.length} text nodes.`,
        {
          textNodes: allTextNodes,
          processedNodes,
          chunks: chunksProcessed,
        }
      );
    
      return {
        success: true,
        message: `Chunked scan complete. Found ${allTextNodes.length} text nodes.`,
        totalNodes: allTextNodes.length,
        processedNodes: processedNodes,
        chunks: chunksProcessed,
        textNodes: allTextNodes,
        commandId,
      };
    }
  • MCP tool registration and thin handler proxy that forwards the call to the Figma plugin via WebSocket (sendCommandToFigma), enabling the actual implementation. Includes input schema validation.
      "scan_text_nodes",
      "Scan all text nodes in the selected Figma node",
      {
        nodeId: z.string().describe("ID of the node to scan"),
      },
      async ({ nodeId }) => {
        try {
          // Initial response to indicate we're starting the process
          const initialStatus = {
            type: "text" as const,
            text: "Starting text node scanning. This may take a moment for large designs...",
          };
    
          // Use the plugin's scan_text_nodes function with chunking flag
          const result = await sendCommandToFigma("scan_text_nodes", {
            nodeId,
            useChunking: true,  // Enable chunking on the plugin side
            chunkSize: 10       // Process 10 nodes at a time
          });
    
          // If the result indicates chunking was used, format the response accordingly
          if (result && typeof result === 'object' && 'chunks' in result) {
            const typedResult = result as {
              success: boolean,
              totalNodes: number,
              processedNodes: number,
              chunks: number,
              textNodes: Array<any>
            };
    
            const summaryText = `
            Scan completed:
            - Found ${typedResult.totalNodes} text nodes
            - Processed in ${typedResult.chunks} chunks
            `;
    
            return {
              content: [
                initialStatus,
                {
                  type: "text" as const,
                  text: summaryText
                },
                {
                  type: "text" as const,
                  text: JSON.stringify(typedResult.textNodes, null, 2)
                }
              ],
            };
          }
    
          // If chunking wasn't used or wasn't reported in the result format, return the result as is
          return {
            content: [
              initialStatus,
              {
                type: "text",
                text: JSON.stringify(result, null, 2),
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: "text",
                text: `Error scanning text nodes: ${error instanceof Error ? error.message : String(error)
                  }`,
              },
            ],
          };
        }
      }
    );
  • Legacy helper function for scanning text nodes without chunking, used as fallback or in non-chunked mode.
    async function findTextNodes(node, parentPath = [], depth = 0, textNodes = []) {
      // Skip invisible nodes
      if (node.visible === false) return;
    
      // Get the path to this node including its name
      const nodePath = [...parentPath, node.name || `Unnamed ${node.type}`];
    
      if (node.type === "TEXT") {
        try {
          // Safely extract font information to avoid Symbol serialization issues
          let fontFamily = "";
          let fontStyle = "";
    
          if (node.fontName) {
            if (typeof node.fontName === "object") {
              if ("family" in node.fontName) fontFamily = node.fontName.family;
              if ("style" in node.fontName) fontStyle = node.fontName.style;
            }
          }
    
          // Create a safe representation of the text node with only serializable properties
          const safeTextNode = {
            id: node.id,
            name: node.name || "Text",
            type: node.type,
            characters: node.characters,
            fontSize: typeof node.fontSize === "number" ? node.fontSize : 0,
            fontFamily: fontFamily,
            fontStyle: fontStyle,
            x: typeof node.x === "number" ? node.x : 0,
            y: typeof node.y === "number" ? node.y : 0,
            width: typeof node.width === "number" ? node.width : 0,
            height: typeof node.height === "number" ? node.height : 0,
            path: nodePath.join(" > "),
            depth: depth,
          };
    
          // Only highlight the node if it's not being done via API
          try {
            // Safe way to create a temporary highlight without causing serialization issues
            const originalFills = JSON.parse(JSON.stringify(node.fills));
            node.fills = [
              {
                type: "SOLID",
                color: { r: 1, g: 0.5, b: 0 },
                opacity: 0.3,
              },
            ];
    
            // Promise-based delay instead of setTimeout
            await delay(500);
    
            try {
              node.fills = originalFills;
            } catch (err) {
              console.error("Error resetting fills:", err);
            }
          } catch (highlightErr) {
            console.error("Error highlighting text node:", highlightErr);
            // Continue anyway, highlighting is just visual feedback
          }
    
          textNodes.push(safeTextNode);
        } catch (nodeErr) {
          console.error("Error processing text node:", nodeErr);
          // Skip this node but continue with others
        }
      }
    
      // Recursively process children of container nodes
      if ("children" in node) {
        for (const child of node.children) {
          await findTextNodes(child, nodePath, depth + 1, textNodes);
        }
      }
    }
  • Helper for chunked mode: pre-collects all descendant nodes to process, building hierarchy paths.
    async function collectNodesToProcess(
      node,
      parentPath = [],
      depth = 0,
      nodesToProcess = []
    ) {
      // Skip invisible nodes
      if (node.visible === false) return;
    
      // Get the path to this node
      const nodePath = [...parentPath, node.name || `Unnamed ${node.type}`];
    
      // Add this node to the processing list
      nodesToProcess.push({
        node: node,
        parentPath: nodePath,
        depth: depth,
      });
    
      // Recursively add children
      if ("children" in node) {
        for (const child of node.children) {
          await collectNodesToProcess(child, nodePath, depth + 1, nodesToProcess);
        }
      }
    }
  • Helper to safely extract and serialize TEXT node properties (avoids Symbol issues), temporarily highlights node for visual feedback.
    async function processTextNode(node, parentPath, depth) {
      if (node.type !== "TEXT") return null;
    
      try {
        // Safely extract font information
        let fontFamily = "";
        let fontStyle = "";
    
        if (node.fontName) {
          if (typeof node.fontName === "object") {
            if ("family" in node.fontName) fontFamily = node.fontName.family;
            if ("style" in node.fontName) fontStyle = node.fontName.style;
          }
        }
    
        // Create a safe representation of the text node
        const safeTextNode = {
          id: node.id,
          name: node.name || "Text",
          type: node.type,
          characters: node.characters,
          fontSize: typeof node.fontSize === "number" ? node.fontSize : 0,
          fontFamily: fontFamily,
          fontStyle: fontStyle,
          x: typeof node.x === "number" ? node.x : 0,
          y: typeof node.y === "number" ? node.y : 0,
          width: typeof node.width === "number" ? node.width : 0,
          height: typeof node.height === "number" ? node.height : 0,
          path: parentPath.join(" > "),
          depth: depth,
        };
    
        // Highlight the node briefly (optional visual feedback)
        try {
          const originalFills = JSON.parse(JSON.stringify(node.fills));
          node.fills = [
            {
              type: "SOLID",
              color: { r: 1, g: 0.5, b: 0 },
              opacity: 0.3,
            },
          ];
    
          // Brief delay for the highlight to be visible
          await delay(100);
    
          try {
            node.fills = originalFills;
          } catch (err) {
            console.error("Error resetting fills:", err);
          }
        } catch (highlightErr) {
          console.error("Error highlighting text node:", highlightErr);
          // Continue anyway, highlighting is just visual feedback
        }
    
        return safeTextNode;
      } catch (nodeErr) {
        console.error("Error processing text node:", nodeErr);
        return null;
      }
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/paragdesai1/parag-Figma-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server