Skip to main content
Glama
paragdesai1

Cursor Talk to Figma MCP

by paragdesai1

set_multiple_text_contents

Update multiple text elements simultaneously in a Figma design node to modify content across text nodes at once.

Instructions

Set multiple text contents parallelly in a node

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nodeIdYesThe ID of the node containing the text nodes to replace
textYesArray of text node IDs and their replacement texts

Implementation Reference

  • MCP tool registration for 'set_multiple_text_contents', including input schema (nodeId and array of {nodeId, text}) and handler that sends command to Figma plugin via WebSocket.
    server.tool(
      "set_multiple_text_contents",
      "Set multiple text contents parallelly in a node",
      {
        nodeId: z
          .string()
          .describe("The ID of the node containing the text nodes to replace"),
        text: z
          .array(
            z.object({
              nodeId: z.string().describe("The ID of the text node"),
              text: z.string().describe("The replacement text"),
            })
          )
          .describe("Array of text node IDs and their replacement texts"),
      },
      async ({ nodeId, text }, extra) => {
        try {
          if (!text || text.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: "No text provided",
                },
              ],
            };
          }
    
          // Initial response to indicate we're starting the process
          const initialStatus = {
            type: "text" as const,
            text: `Starting text replacement for ${text.length} nodes. This will be processed in batches of 5...`,
          };
    
          // Track overall progress
          let totalProcessed = 0;
          const totalToProcess = text.length;
    
          // Use the plugin's set_multiple_text_contents function with chunking
          const result = await sendCommandToFigma("set_multiple_text_contents", {
            nodeId,
            text,
          });
    
          // Cast the result to a specific type to work with it safely
          interface TextReplaceResult {
            success: boolean;
            nodeId: string;
            replacementsApplied?: number;
            replacementsFailed?: number;
            totalReplacements?: number;
            completedInChunks?: number;
            results?: Array<{
              success: boolean;
              nodeId: string;
              error?: string;
              originalText?: string;
              translatedText?: string;
            }>;
          }
    
          const typedResult = result as TextReplaceResult;
    
          // Format the results for display
          const success = typedResult.replacementsApplied && typedResult.replacementsApplied > 0;
          const progressText = `
          Text replacement completed:
          - ${typedResult.replacementsApplied || 0} of ${totalToProcess} successfully updated
          - ${typedResult.replacementsFailed || 0} failed
          - Processed in ${typedResult.completedInChunks || 1} batches
          `;
    
          // Detailed results
          const detailedResults = typedResult.results || [];
          const failedResults = detailedResults.filter(item => !item.success);
    
          // Create the detailed part of the response
          let detailedResponse = "";
          if (failedResults.length > 0) {
            detailedResponse = `\n\nNodes that failed:\n${failedResults.map(item =>
              `- ${item.nodeId}: ${item.error || "Unknown error"}`
            ).join('\n')}`;
          }
    
          return {
            content: [
              initialStatus,
              {
                type: "text" as const,
                text: progressText + detailedResponse,
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: "text",
                text: `Error setting multiple text contents: ${error instanceof Error ? error.message : String(error)
                  }`,
              },
            ],
          };
        }
      }
    );
  • Core handler implementation in Figma plugin: processes multiple text updates in batched chunks (size 5), handles font loading, visual feedback (highlighting), error handling, and detailed progress reporting.
    async function setMultipleTextContents(params) {
      const { nodeId, text } = params || {};
      const commandId = params.commandId || generateCommandId();
    
      if (!nodeId || !text || !Array.isArray(text)) {
        const errorMsg = "Missing required parameters: nodeId and text array";
    
        // Send error progress update
        sendProgressUpdate(
          commandId,
          "set_multiple_text_contents",
          "error",
          0,
          0,
          0,
          errorMsg,
          { error: errorMsg }
        );
    
        throw new Error(errorMsg);
      }
    
      console.log(
        `Starting text replacement for node: ${nodeId} with ${text.length} text replacements`
      );
    
      // Send started progress update
      sendProgressUpdate(
        commandId,
        "set_multiple_text_contents",
        "started",
        0,
        text.length,
        0,
        `Starting text replacement for ${text.length} nodes`,
        { totalReplacements: text.length }
      );
    
      // Define the results array and counters
      const results = [];
      let successCount = 0;
      let failureCount = 0;
    
      // Split text replacements into chunks of 5
      const CHUNK_SIZE = 5;
      const chunks = [];
    
      for (let i = 0; i < text.length; i += CHUNK_SIZE) {
        chunks.push(text.slice(i, i + CHUNK_SIZE));
      }
    
      console.log(`Split ${text.length} replacements into ${chunks.length} chunks`);
    
      // Send chunking info update
      sendProgressUpdate(
        commandId,
        "set_multiple_text_contents",
        "in_progress",
        5, // 5% progress for planning phase
        text.length,
        0,
        `Preparing to replace text in ${text.length} nodes using ${chunks.length} chunks`,
        {
          totalReplacements: text.length,
          chunks: chunks.length,
          chunkSize: CHUNK_SIZE,
        }
      );
    
      // Process each chunk sequentially
      for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
        const chunk = chunks[chunkIndex];
        console.log(
          `Processing chunk ${chunkIndex + 1}/${chunks.length} with ${
            chunk.length
          } replacements`
        );
    
        // Send chunk processing start update
        sendProgressUpdate(
          commandId,
          "set_multiple_text_contents",
          "in_progress",
          Math.round(5 + (chunkIndex / chunks.length) * 90), // 5-95% for processing
          text.length,
          successCount + failureCount,
          `Processing text replacements chunk ${chunkIndex + 1}/${chunks.length}`,
          {
            currentChunk: chunkIndex + 1,
            totalChunks: chunks.length,
            successCount,
            failureCount,
          }
        );
    
        // Process replacements within a chunk in parallel
        const chunkPromises = chunk.map(async (replacement) => {
          if (!replacement.nodeId || replacement.text === undefined) {
            console.error(`Missing nodeId or text for replacement`);
            return {
              success: false,
              nodeId: replacement.nodeId || "unknown",
              error: "Missing nodeId or text in replacement entry",
            };
          }
    
          try {
            console.log(
              `Attempting to replace text in node: ${replacement.nodeId}`
            );
    
            // Get the text node to update (just to check it exists and get original text)
            const textNode = await figma.getNodeByIdAsync(replacement.nodeId);
    
            if (!textNode) {
              console.error(`Text node not found: ${replacement.nodeId}`);
              return {
                success: false,
                nodeId: replacement.nodeId,
                error: `Node not found: ${replacement.nodeId}`,
              };
            }
    
            if (textNode.type !== "TEXT") {
              console.error(
                `Node is not a text node: ${replacement.nodeId} (type: ${textNode.type})`
              );
              return {
                success: false,
                nodeId: replacement.nodeId,
                error: `Node is not a text node: ${replacement.nodeId} (type: ${textNode.type})`,
              };
            }
    
            // Save original text for the result
            const originalText = textNode.characters;
            console.log(`Original text: "${originalText}"`);
            console.log(`Will translate to: "${replacement.text}"`);
    
            // Highlight the node before changing text
            let originalFills;
            try {
              // Save original fills for restoration later
              originalFills = JSON.parse(JSON.stringify(textNode.fills));
              // Apply highlight color (orange with 30% opacity)
              textNode.fills = [
                {
                  type: "SOLID",
                  color: { r: 1, g: 0.5, b: 0 },
                  opacity: 0.3,
                },
              ];
            } catch (highlightErr) {
              console.error(
                `Error highlighting text node: ${highlightErr.message}`
              );
              // Continue anyway, highlighting is just visual feedback
            }
    
            // Use the existing setTextContent function to handle font loading and text setting
            await setTextContent({
              nodeId: replacement.nodeId,
              text: replacement.text,
            });
    
            // Keep highlight for a moment after text change, then restore original fills
            if (originalFills) {
              try {
                // Use delay function for consistent timing
                await delay(500);
                textNode.fills = originalFills;
              } catch (restoreErr) {
                console.error(`Error restoring fills: ${restoreErr.message}`);
              }
            }
    
            console.log(
              `Successfully replaced text in node: ${replacement.nodeId}`
            );
            return {
              success: true,
              nodeId: replacement.nodeId,
              originalText: originalText,
              translatedText: replacement.text,
            };
          } catch (error) {
            console.error(
              `Error replacing text in node ${replacement.nodeId}: ${error.message}`
            );
            return {
              success: false,
              nodeId: replacement.nodeId,
              error: `Error applying replacement: ${error.message}`,
            };
          }
        });
    
        // Wait for all replacements in this chunk to complete
        const chunkResults = await Promise.all(chunkPromises);
    
        // Process results for this chunk
        chunkResults.forEach((result) => {
          if (result.success) {
            successCount++;
          } else {
            failureCount++;
          }
          results.push(result);
        });
    
        // Send chunk processing complete update with partial results
        sendProgressUpdate(
          commandId,
          "set_multiple_text_contents",
          "in_progress",
          Math.round(5 + ((chunkIndex + 1) / chunks.length) * 90), // 5-95% for processing
          text.length,
          successCount + failureCount,
          `Completed chunk ${chunkIndex + 1}/${
            chunks.length
          }. ${successCount} successful, ${failureCount} failed so far.`,
          {
            currentChunk: chunkIndex + 1,
            totalChunks: chunks.length,
            successCount,
            failureCount,
            chunkResults: chunkResults,
          }
        );
    
        // Add a small delay between chunks to avoid overloading Figma
        if (chunkIndex < chunks.length - 1) {
          console.log("Pausing between chunks to avoid overloading Figma...");
          await delay(1000); // 1 second delay between chunks
        }
      }
    
      console.log(
        `Replacement complete: ${successCount} successful, ${failureCount} failed`
      );
    
      // Send completed progress update
      sendProgressUpdate(
        commandId,
        "set_multiple_text_contents",
        "completed",
        100,
        text.length,
        successCount + failureCount,
        `Text replacement complete: ${successCount} successful, ${failureCount} failed`,
        {
          totalReplacements: text.length,
          replacementsApplied: successCount,
          replacementsFailed: failureCount,
          completedInChunks: chunks.length,
          results: results,
        }
      );
    
      return {
        success: successCount > 0,
        nodeId: nodeId,
        replacementsApplied: successCount,
        replacementsFailed: failureCount,
        totalReplacements: text.length,
        results: results,
        completedInChunks: chunks.length,
        commandId,
      };
    }
  • Helper function for safely setting characters/text in Figma text nodes, handling mixed fonts, font loading, and strategies to preserve typography.
    export const setCharacters = async (node, characters, options) => {
      const fallbackFont = options?.fallbackFont || {
        family: "Roboto",
        style: "Regular",
      };
      try {
        if (node.fontName === figma.mixed) {
          if (options?.smartStrategy === "prevail") {
            const fontHashTree = {};
            for (let i = 1; i < node.characters.length; i++) {
              const charFont = node.getRangeFontName(i - 1, i);
              const key = `${charFont.family}::${charFont.style}`;
              fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
            }
            const prevailedTreeItem = Object.entries(fontHashTree).sort(
              (a, b) => b[1] - a[1]
            )[0];
            const [family, style] = prevailedTreeItem[0].split("::");
            const prevailedFont = {
              family,
              style,
            };
            await figma.loadFontAsync(prevailedFont);
            node.fontName = prevailedFont;
          } else if (options?.smartStrategy === "strict") {
            return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
          } else if (options?.smartStrategy === "experimental") {
            return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
          } else {
            const firstCharFont = node.getRangeFontName(0, 1);
            await figma.loadFontAsync(firstCharFont);
            node.fontName = firstCharFont;
          }
        } else {
          await figma.loadFontAsync({
            family: node.fontName.family,
            style: node.fontName.style,
          });
        }
      } catch (err) {
        console.warn(
          `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
          err
        );
        await figma.loadFontAsync(fallbackFont);
        node.fontName = fallbackFont;
      }
      try {
        node.characters = characters;
        return true;
      } catch (err) {
        console.warn(`Failed to set characters. Skipped.`, err);
        return false;
      }
    };
  • Zod schema definition for tool input parameters.
      nodeId: z
        .string()
        .describe("The ID of the node containing the text nodes to replace"),
      text: z
        .array(
          z.object({
            nodeId: z.string().describe("The ID of the text node"),
            text: z.string().describe("The replacement text"),
          })
        )
        .describe("Array of text node IDs and their replacement texts"),
    },
  • Embedded helper for text setting (duplicate of setcharacters.js logic), used by setTextContent and setMultipleTextContents.
    const setCharacters = async (node, characters, options) => {
      const fallbackFont = (options && options.fallbackFont) || {
        family: "Inter",
        style: "Regular",
      };
      try {
        if (node.fontName === figma.mixed) {
          if (options && options.smartStrategy === "prevail") {
            const fontHashTree = {};
            for (let i = 1; i < node.characters.length; i++) {
              const charFont = node.getRangeFontName(i - 1, i);
              const key = `${charFont.family}::${charFont.style}`;
              fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
            }
            const prevailedTreeItem = Object.entries(fontHashTree).sort(
              (a, b) => b[1] - a[1]
            )[0];
            const [family, style] = prevailedTreeItem[0].split("::");
            const prevailedFont = {
              family,
              style,
            };
            await figma.loadFontAsync(prevailedFont);
            node.fontName = prevailedFont;
          } else if (options && options.smartStrategy === "strict") {
            return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
          } else if (options && options.smartStrategy === "experimental") {
            return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
          } else {
            const firstCharFont = node.getRangeFontName(0, 1);
            await figma.loadFontAsync(firstCharFont);
            node.fontName = firstCharFont;
          }
        } else {
          await figma.loadFontAsync({
            family: node.fontName.family,
            style: node.fontName.style,
          });
        }
      } catch (err) {
        console.warn(
          `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
          err
        );
        await figma.loadFontAsync(fallbackFont);
        node.fontName = fallbackFont;
      }
      try {
        node.characters = characters;
        return true;
      } catch (err) {
        console.warn(`Failed to set characters. Skipped.`, err);
        return false;
      }
    };

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