set_text_content
Update the text content of a text node in Figma designs programmatically using the Cursor AI and Figma integration.
Instructions
Set the text content of an existing text node in Figma
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Input Schema (JSON Schema)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {},
"type": "object"
}
Implementation Reference
- src/cursor_mcp_plugin/code.js:1116-1150 (handler)Main handler function for 'set_text_content' command. Validates parameters, retrieves the text node, loads its font, calls setCharacters helper to update text, and returns updated node info.async function setTextContent(params) { const { nodeId, text } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } if (text === undefined) { throw new Error("Missing text parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (node.type !== "TEXT") { throw new Error(`Node is not a text node: ${nodeId}`); } try { await figma.loadFontAsync(node.fontName); await setCharacters(node, text); return { id: node.id, name: node.name, characters: node.characters, fontName: node.fontName, }; } catch (error) { throw new Error(`Error setting text content: ${error.message}`); } }
- Core helper function that intelligently handles font loading (including mixed fonts) and sets the characters property on a Figma text node. Used by setTextContent.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; } };
- src/talk_to_figma_mcp/server.ts:875-909 (registration)MCP server.tool registration for 'set_text_content'. Defines input schema (nodeId, text) and forwards the command to the Figma plugin via WebSocket.server.tool( "set_text_content", "Set the text content of an existing text node in Figma", { nodeId: z.string().describe("The ID of the text node to modify"), text: z.string().describe("New text content"), }, async ({ nodeId, text }) => { try { const result = await sendCommandToFigma("set_text_content", { nodeId, text, }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Updated text content of node "${typedResult.name}" to "${text}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting text content: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } );
- src/cursor_mcp_plugin/code.js:153-154 (handler)Dispatch case in handleCommand switch statement that routes 'set_text_content' command to the setTextContent handler function.case "set_text_content": return await setTextContent(params);
- Supporting helper functions for advanced font handling in mixed-font scenarios, used by setCharacters depending on options.smartStrategy.const setCharactersWithStrictMatchFont = async ( node, characters, fallbackFont ) => { const fontHashTree = {}; for (let i = 1; i < node.characters.length; i++) { const startIdx = i - 1; const startCharFont = node.getRangeFontName(startIdx, i); const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`; while (i < node.characters.length) { i++; const charFont = node.getRangeFontName(i - 1, i); if (startCharFontVal !== `${charFont.family}::${charFont.style}`) { break; } } fontHashTree[`${startIdx}_${i}`] = startCharFontVal; } await figma.loadFontAsync(fallbackFont); node.fontName = fallbackFont; node.characters = characters; console.log(fontHashTree); await Promise.all( Object.keys(fontHashTree).map(async (range) => { console.log(range, fontHashTree[range]); const [start, end] = range.split("_"); const [family, style] = fontHashTree[range].split("::"); const matchedFont = { family, style, }; await figma.loadFontAsync(matchedFont); return node.setRangeFontName(Number(start), Number(end), matchedFont); }) ); return true; }; const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => { const indices = []; let temp = startIdx; for (let i = startIdx; i < endIdx; i++) { if ( str[i] === delimiter && i + startIdx !== endIdx && temp !== i + startIdx ) { indices.push([temp, i + startIdx]); temp = i + startIdx + 1; } } temp !== endIdx && indices.push([temp, endIdx]); return indices.filter(Boolean); }; const buildLinearOrder = (node) => { const fontTree = []; const newLinesPos = getDelimiterPos(node.characters, "\n"); newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => { const newLinesRangeFont = node.getRangeFontName( newLinesRangeStart, newLinesRangeEnd ); if (newLinesRangeFont === figma.mixed) { const spacesPos = getDelimiterPos( node.characters, " ", newLinesRangeStart, newLinesRangeEnd ); spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => { const spacesRangeFont = node.getRangeFontName( spacesRangeStart, spacesRangeEnd ); if (spacesRangeFont === figma.mixed) { const spacesRangeFont = node.getRangeFontName( spacesRangeStart, spacesRangeStart[0] ); fontTree.push({ start: spacesRangeStart, delimiter: " ", family: spacesRangeFont.family, style: spacesRangeFont.style, }); } else { fontTree.push({ start: spacesRangeStart, delimiter: " ", family: spacesRangeFont.family, style: spacesRangeFont.style, }); } }); } else { fontTree.push({ start: newLinesRangeStart, delimiter: "\n", family: newLinesRangeFont.family, style: newLinesRangeFont.style, }); } }); return fontTree .sort((a, b) => +a.start - +b.start) .map(({ family, style, delimiter }) => ({ family, style, delimiter })); }; const setCharactersWithSmartMatchFont = async ( node, characters, fallbackFont ) => { const rangeTree = buildLinearOrder(node); const fontsToLoad = uniqBy( rangeTree, ({ family, style }) => `${family}::${style}` ).map(({ family, style }) => ({ family, style, })); await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync)); node.fontName = fallbackFont; node.characters = characters; let prevPos = 0; rangeTree.forEach(({ family, style, delimiter }) => { if (prevPos < node.characters.length) { const delimeterPos = node.characters.indexOf(delimiter, prevPos); const endPos = delimeterPos > prevPos ? delimeterPos : node.characters.length; const matchedFont = { family, style, }; node.setRangeFontName(prevPos, endPos, matchedFont); prevPos = endPos + 1; } }); return true; };