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
| Name | Required | Description | Default |
|---|---|---|---|
| nodeId | Yes | The ID of the node containing the text nodes to replace | |
| text | Yes | Array of text node IDs and their replacement texts |
Implementation Reference
- src/talk_to_figma_mcp/server.ts:1663-1769 (registration)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) }`, }, ], }; } } );
- src/cursor_mcp_plugin/code.js:1844-2113 (handler)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; } };