From 4004b5aed47d414aeb3b6cbf834cf05eef586222 Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Wed, 28 May 2025 22:38:06 +0200 Subject: [PATCH 1/7] add variables and paint support --- src/cursor_mcp_plugin/code.js | 88 +++++++++++- src/talk_to_figma_mcp/package.json | 4 +- src/talk_to_figma_mcp/server.ts | 208 ++++++++++++++++++++++++++++- 3 files changed, 292 insertions(+), 8 deletions(-) diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index b5fb794..f848d43 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -229,6 +229,16 @@ async function handleCommand(command, params) { return await setDefaultConnector(params); case "create_connections": return await createConnections(params); + case "list_variables": + return await listVariables(); + case "get_node_variables": + return await getNodeVariables(params); + case "set_node_variable": + return await setNodeVariable(params); + case "set_node_paints": + return await setNodePaints(params); + case "get_node_paints": + return await getNodePaints(params); default: throw new Error(`Unknown command: ${command}`); } @@ -1387,6 +1397,78 @@ async function setTextContent(params) { } } +// === Figma Variables Support === + +// List all local variables in the document +async function listVariables() { + if (!figma.variables || !figma.variables.getLocalVariablesAsync) { + throw new Error("Figma Variables API not available"); + } + const variables = await figma.variables.getLocalVariablesAsync(); + return variables.map(v => ({ + id: v.id, + name: v.name, + key: v.key, + resolvedType: v.resolvedType, + valuesByMode: v.valuesByMode, + scopes: v.scopes, + description: v.description + })); +} + +// Get variable bindings for a node +async function getNodeVariables(params) { + const { nodeId } = params || {}; + if (!nodeId) throw new Error("Missing nodeId parameter"); + const node = await figma.getNodeByIdAsync(nodeId); + if (!node) throw new Error(`Node not found: ${nodeId}`); + if (!node.boundVariables) return { nodeId, boundVariables: null }; + return { nodeId, boundVariables: node.boundVariables }; +} + +// Set a variable binding on a node +async function setNodeVariable(params) { + const { nodeId, property, variableId } = params || {}; + if (!nodeId || !property || !variableId) throw new Error("Missing nodeId, property, or variableId"); + const node = await figma.getNodeByIdAsync(nodeId); + if (!node) throw new Error(`Node not found: ${nodeId}`); + if (!node.setBoundVariable) throw new Error("Node does not support variable binding"); + await node.setBoundVariable(property, variableId); + return { nodeId, property, variableId, success: true }; +} + +// --- setNodePaints: Set fills or strokes on a node --- +async function setNodePaints(params) { + const { nodeId, paints, paintsType = "fills" } = params || {}; + if (!nodeId) throw new Error("Missing nodeId parameter"); + if (!Array.isArray(paints)) throw new Error("'paints' must be an array"); + if (paintsType !== "fills" && paintsType !== "strokes") throw new Error("paintsType must be 'fills' or 'strokes'"); + const node = await figma.getNodeByIdAsync(nodeId); + if (!node) throw new Error(`Node not found with ID: ${nodeId}`); + if (!(paintsType in node)) throw new Error(`Node does not support ${paintsType}: ${nodeId}`); + node[paintsType] = paints; + return { + id: node.id, + name: node.name, + [paintsType]: node[paintsType], + }; +} + +// --- getNodePaints: Get fills or strokes from a node --- +async function getNodePaints(params) { + const { nodeId, paintsType = "fills" } = params || {}; + if (!nodeId) throw new Error("Missing nodeId parameter"); + if (paintsType !== "fills" && paintsType !== "strokes") throw new Error("paintsType must be 'fills' or 'strokes'"); + const node = await figma.getNodeByIdAsync(nodeId); + if (!node) throw new Error(`Node not found with ID: ${nodeId}`); + if (!(paintsType in node)) throw new Error(`Node does not support ${paintsType}: ${nodeId}`); + return { + id: node.id, + name: node.name, + [paintsType]: node[paintsType], + }; +} + // Initialize settings on load (async function initializePlugin() { try { @@ -2386,17 +2468,17 @@ async function getAnnotations(params) { throw new Error(`Node type ${node.type} does not support annotations`); } - const result = { + const response = { nodeId: node.id, name: node.name, annotations: node.annotations || [], }; if (includeCategories) { - result.categories = Object.values(categoriesMap); + response.categories = Object.values(categoriesMap); } - return result; + return response; } else { // Get all annotations in the current page const annotations = []; diff --git a/src/talk_to_figma_mcp/package.json b/src/talk_to_figma_mcp/package.json index b7bfb6a..3deb1e6 100644 --- a/src/talk_to_figma_mcp/package.json +++ b/src/talk_to_figma_mcp/package.json @@ -24,8 +24,8 @@ "devDependencies": { "@types/node": "^20.10.5", "@types/uuid": "^9.0.7", - "@types/ws": "^8.5.10", + "@types/ws": "^8.18.1", "ts-node": "^10.9.2", "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index e7ad940..63edcdf 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -685,6 +685,98 @@ server.tool( } ); +// Get Node Paints Tool +server.tool( + "get_node_paints", + "Retrieve the Paint[] definition (either fills or strokes) from a node in Figma. The returned array conforms to the Figma Plugin API Paint interface.", + { + nodeId: z.string().describe("The ID of the node whose paints to retrieve"), + paintsType: z + .enum(["fills", "strokes"]) + .optional() + .default("fills") + .describe("Which paint list to return. Defaults to 'fills'."), + }, + async ({ nodeId, paintsType }) => { + try { + const result = await sendCommandToFigma("get_node_paints", { + nodeId, + paintsType: paintsType || "fills", + }); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error getting node paints: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + }; + } + } +); + +// Set Node Paints Tool +server.tool( + "set_node_paints", + "Replace the Paint[] definition (either fills or strokes) on a node in Figma. Supply an array of objects that follow the Figma Plugin API Paint specification (https://www.figma.com/plugin-docs/api/Paint/).", + { + nodeId: z.string().describe("The ID of the node to modify"), +paints: z + .array( + // an open object is enough to satisfy “items must have schema” + z.object({}).catchall(z.unknown()) + ) + .describe( + "Array of Paint objects. Each object must conform to the Paint interface: type, opacity, color, gradientStops, scaleMode, imageHash, etc." + ), + paintsType: z + .enum(["fills", "strokes"]) + .optional() + .default("fills") + .describe("Whether to apply the paints to 'fills' (default) or 'strokes'."), + }, + async ({ nodeId, paints, paintsType }) => { + try { + const result = await sendCommandToFigma("set_node_paints", { + nodeId, + paints, + paintsType: paintsType || "fills", + }); + const typedResult = result as { name: string }; + return { + content: [ + { + type: "text", + text: `Updated ${paintsType || "fills"} on node "${typedResult.name}".`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error setting node paints: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + }; + } + } +); + // Move Node Tool server.tool( "move_node", @@ -2542,6 +2634,99 @@ This detailed process ensures you correctly interpret the reaction data, prepare } ); +// Figma Variables: List all variables +server.tool( + "list_variables", + "List all local variables in the current Figma document. Returns an array of variable objects, including their id, name, type, and values.", + {}, + async (): Promise => { + try { + const result = await sendCommandToFigma("list_variables"); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2) + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error listing variables: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + } +); + +// Figma Variables: Get variable bindings for a node +server.tool( + "get_node_variables", + "Get all variable bindings for a specific node. Returns an object mapping property types (e.g., 'fills', 'strokes', 'opacity', etc.) to variable binding info.", + { + nodeId: z.string().describe("The ID of the node to get variable bindings for") + }, + async ({ nodeId }: { nodeId: string }): Promise => { + try { + const result = await sendCommandToFigma("get_node_variables", { nodeId }); + return { + content: [ + { + type: "text", + text: `These are the variables for the node: ${JSON.stringify(result, null, 2)}, you may use the 'list_variables' tool to find the name of the variables.`, + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error getting node variables: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + } +); + +// Figma Variables: Set a variable binding on a node +server.tool( + "set_node_variable", + "Set a variable binding on a node. You must specify the node ID, the property type (e.g., 'fills', 'strokes', 'opacity', etc.), the variable ID, and the optional collection mode (e.g., 'MODE_ID').", + { + nodeId: z.string().describe("The ID of the node to set the variable binding on"), + property: z.string().describe("The property to bind the variable to (e.g., 'fills', 'strokes', 'opacity', etc.)"), + variableId: z.string().describe("The ID of the variable to bind"), + modeId: z.string().optional().describe("Optional: The mode ID for the variable collection (for multi-mode variables)") + }, + async ({ nodeId, property, variableId, modeId }: { nodeId: string; property: string; variableId: string; modeId?: string }): Promise => { + try { + const result = await sendCommandToFigma("set_node_variable", { nodeId, property, variableId, modeId }); + return { + content: [ + { + type: "text", + text: `Set variable '${variableId}' for property '${property}' on node '${nodeId}'. Result: ${JSON.stringify(result)}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error setting node variable: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + } +); // Define command types and parameters type FigmaCommand = @@ -2582,7 +2767,12 @@ type FigmaCommand = | "set_item_spacing" | "get_reactions" | "set_default_connector" - | "create_connections"; + | "create_connections" + | "list_variables" + | "get_node_variables" + | "set_node_variable" + | "get_node_paints" + | "set_node_paints"; type CommandParams = { get_document_info: Record; @@ -2725,11 +2915,23 @@ type CommandParams = { text?: string; }>; }; - + list_variables: Record; + get_node_variables: { nodeId: string }; + set_node_variable: { nodeId: string; property: string; variableId: string; modeId?: string }; + get_node_paints: { nodeId: string }; + set_node_paints: { + nodeId: string; + paints: Array<{ + type: string; + color?: { r: number; g: number; b: number }; + gradientStops?: Array<{ color: { r: number; g: number; b: number; a?: number }; position: number }>; + imageRef?: string; + }>; + }; }; - // Helper function to process Figma node responses +// Helper function to process Figma node responses function processFigmaNodeResponse(result: unknown): any { if (!result || typeof result !== "object") { return result; From 4816bf0c25e748130148af698423d8de44499882 Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Fri, 30 May 2025 09:12:51 +0200 Subject: [PATCH 2/7] wip --- src/cursor_mcp_plugin/code.js | 155 ++++++++++++++++------ src/talk_to_figma_mcp/server.ts | 222 ++++++++++++++++++-------------- 2 files changed, 247 insertions(+), 130 deletions(-) diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index f848d43..f9e6447 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -1,6 +1,5 @@ // This is the main code file for the Cursor MCP Figma plugin // It handles Figma API commands - // Plugin state const state = { serverPort: 3055, // Default port @@ -143,8 +142,8 @@ async function handleCommand(command, params) { return await getStyles(); case "get_local_components": return await getLocalComponents(); - // case "get_team_components": - // return await getTeamComponents(); + case "get_team_components": + return await getTeamComponents(); case "create_component_instance": return await createComponentInstance(params); case "export_node_as_image": @@ -233,8 +232,6 @@ async function handleCommand(command, params) { return await listVariables(); case "get_node_variables": return await getNodeVariables(params); - case "set_node_variable": - return await setNodeVariable(params); case "set_node_paints": return await setNodePaints(params); case "get_node_paints": @@ -1150,24 +1147,24 @@ async function getLocalComponents() { }; } -// async function getTeamComponents() { -// try { -// const teamComponents = -// await figma.teamLibrary.getAvailableComponentsAsync(); - -// return { -// count: teamComponents.length, -// components: teamComponents.map((component) => ({ -// key: component.key, -// name: component.name, -// description: component.description, -// libraryName: component.libraryName, -// })), -// }; -// } catch (error) { -// throw new Error(`Error getting team components: ${error.message}`); -// } -// } +async function getTeamComponents() { + try { + const teamComponents = + await figma.teamLibrary.getAvailableComponentsAsync(); + + return { + count: teamComponents.length, + components: teamComponents.map((component) => ({ + key: component.key, + name: component.name, + description: component.description, + libraryName: component.libraryName, + })), + }; + } catch (error) { + throw new Error(`Error getting team components: ${error.message}`); + } +} async function createComponentInstance(params) { const { componentKey, x = 0, y = 0 } = params || {}; @@ -1426,31 +1423,117 @@ async function getNodeVariables(params) { return { nodeId, boundVariables: node.boundVariables }; } -// Set a variable binding on a node -async function setNodeVariable(params) { - const { nodeId, property, variableId } = params || {}; - if (!nodeId || !property || !variableId) throw new Error("Missing nodeId, property, or variableId"); - const node = await figma.getNodeByIdAsync(nodeId); - if (!node) throw new Error(`Node not found: ${nodeId}`); - if (!node.setBoundVariable) throw new Error("Node does not support variable binding"); - await node.setBoundVariable(property, variableId); - return { nodeId, property, variableId, success: true }; -} - // --- setNodePaints: Set fills or strokes on a node --- async function setNodePaints(params) { const { nodeId, paints, paintsType = "fills" } = params || {}; if (!nodeId) throw new Error("Missing nodeId parameter"); if (!Array.isArray(paints)) throw new Error("'paints' must be an array"); if (paintsType !== "fills" && paintsType !== "strokes") throw new Error("paintsType must be 'fills' or 'strokes'"); + + // Get target node const node = await figma.getNodeByIdAsync(nodeId); if (!node) throw new Error(`Node not found with ID: ${nodeId}`); if (!(paintsType in node)) throw new Error(`Node does not support ${paintsType}: ${nodeId}`); - node[paintsType] = paints; + + // Validate and format each paint object asynchronously + const validatedPaints = await Promise.all(paints.map(async paint => { + // Validate paint type + if (!paint.type || !['SOLID', 'GRADIENT_LINEAR', 'GRADIENT_RADIAL', + 'GRADIENT_ANGULAR', 'GRADIENT_DIAMOND', 'IMAGE', 'VIDEO'].includes(paint.type)) { + throw new Error(`Invalid paint type: ${paint.type}`); + } + + // Format based on paint type + let formattedPaint; + switch (paint.type) { + case 'SOLID': + if (!paint.color && paint.boundVariables && paint.boundVariables.color) { + // get variable color and return as formatted paint + console.log(`Using bound variable color for SOLID paint ${paint.boundVariables.color}`); + const variableColor = await figma.variables.getVariableByIdAsync(paint.boundVariables.color.variableId); + //get value from variable for default mode + console.log('variableColor', variableColor); + + const variableColorValue = Object.values(variableColor.valuesByMode)[0]; + console.log('variableColorValue', variableColorValue); + + console.log(variableColor); + formattedPaint = { + type: 'SOLID', + color: { + r: Number(variableColorValue.r || 0), + g: Number(variableColorValue.g || 0), + b: Number(variableColorValue.b || 0) + }, + opacity: Number(paint.opacity || 1) + }; + } else { + formattedPaint = { + type: 'SOLID', + color: { + r: Number(paint.color.r || 0), + g: Number(paint.color.g || 0), + b: Number(paint.color.b || 0) + }, + opacity: Number(paint.opacity || 1) + }; + } + break; + + case 'GRADIENT_LINEAR': + case 'GRADIENT_RADIAL': + case 'GRADIENT_ANGULAR': + case 'GRADIENT_DIAMOND': + if (!paint.gradientStops || !Array.isArray(paint.gradientStops)) { + throw new Error('Gradient requires gradientStops array'); + } + formattedPaint = { + type: paint.type, + gradientStops: paint.gradientStops.map(stop => ({ + position: Number(stop.position || 0), + color: { + r: Number(stop.color.r || 0), + g: Number(stop.color.g || 0), + b: Number(stop.color.b || 0), + a: Number(stop.color.a || 1) + } + })), + gradientTransform: paint.gradientTransform || [[1,0,0], [0,1,0]] + }; + break; + + case 'IMAGE': + formattedPaint = { + type: 'IMAGE', + scaleMode: paint.scaleMode || 'FILL', + imageHash: paint.imageHash, + opacity: Number(paint.opacity || 1) + }; + break; + + default: + throw new Error(`Unsupported paint type: ${paint.type}`); + } + + if (paint.boundVariables && paint.boundVariables.color) { + const variableColor = await figma.variables.getVariableByIdAsync(paint.boundVariables.color.variableId); + return figma.variables.setBoundVariableForPaint(formattedPaint, 'color', variableColor); + } else { + return formattedPaint; + } + })); + + // Apply validated paints + try { + node[paintsType] = validatedPaints; + } catch (error) { + throw new Error(`Error setting ${paintsType}: ${error.message}, ${JSON.stringify(validatedPaints, null, 2)}`); + } + return { id: node.id, name: node.name, - [paintsType]: node[paintsType], + [paintsType]: node[paintsType] }; } diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index 63edcdf..96ef838 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -729,22 +729,40 @@ server.tool( // Set Node Paints Tool server.tool( "set_node_paints", - "Replace the Paint[] definition (either fills or strokes) on a node in Figma. Supply an array of objects that follow the Figma Plugin API Paint specification (https://www.figma.com/plugin-docs/api/Paint/).", + "Replace the Paint definition (either fills or strokes) on a node and set color variables to the node.", { nodeId: z.string().describe("The ID of the node to modify"), -paints: z - .array( - // an open object is enough to satisfy “items must have schema” - z.object({}).catchall(z.unknown()) - ) - .describe( - "Array of Paint objects. Each object must conform to the Paint interface: type, opacity, color, gradientStops, scaleMode, imageHash, etc." - ), + paints: z + .array( + z.object({ + type: z.enum([ + 'SOLID', + 'GRADIENT_LINEAR', + 'GRADIENT_RADIAL', + 'GRADIENT_ANGULAR', + 'GRADIENT_DIAMOND', + 'IMAGE', + 'VIDEO', + 'VARIABLE_ALIAS', + ]), + visible: z.boolean().optional(), + opacity: z.number().min(0).max(1).optional(), + blendMode: z.string().optional(), + boundVariables: z.object({ + color: z.object({ + type: z.string().optional(), + variableId: z.string().describe("The ID of the variable to bind to the color in the format like VariableID:3:4"), + }).describe("Optional bound variables for the paint").optional(), + }).catchall(z.unknown()) + }) + .describe( + "Array of Paint objects. Each object must conform to the Paint interface: type, opacity, color, gradientStops, scaleMode, imageHash, etc." + )), paintsType: z - .enum(["fills", "strokes"]) - .optional() - .default("fills") - .describe("Whether to apply the paints to 'fills' (default) or 'strokes'."), + .enum(["fills", "strokes"]) + .optional() + .default("fills") + .describe("Whether to apply the paints to 'fills' (default) or 'strokes'."), }, async ({ nodeId, paints, paintsType }) => { try { @@ -1082,7 +1100,37 @@ server.tool( { type: "text", text: `Error getting local components: ${error instanceof Error ? error.message : String(error) - }`, + }`, + }, + ], + }; + } + } +); + +// Get Team Components Tool +server.tool( + "get_team_components", + "Get all team components from the Figma document", + {}, + async () => { + try { + const result = await sendCommandToFigma("get_team_components"); + return { + content: [ + { + type: "text", + text: JSON.stringify(result) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error getting team components: ${error instanceof Error ? error.message : String(error) + }`, }, ], }; @@ -2694,85 +2742,51 @@ server.tool( } ); -// Figma Variables: Set a variable binding on a node -server.tool( - "set_node_variable", - "Set a variable binding on a node. You must specify the node ID, the property type (e.g., 'fills', 'strokes', 'opacity', etc.), the variable ID, and the optional collection mode (e.g., 'MODE_ID').", - { - nodeId: z.string().describe("The ID of the node to set the variable binding on"), - property: z.string().describe("The property to bind the variable to (e.g., 'fills', 'strokes', 'opacity', etc.)"), - variableId: z.string().describe("The ID of the variable to bind"), - modeId: z.string().optional().describe("Optional: The mode ID for the variable collection (for multi-mode variables)") - }, - async ({ nodeId, property, variableId, modeId }: { nodeId: string; property: string; variableId: string; modeId?: string }): Promise => { - try { - const result = await sendCommandToFigma("set_node_variable", { nodeId, property, variableId, modeId }); - return { - content: [ - { - type: "text", - text: `Set variable '${variableId}' for property '${property}' on node '${nodeId}'. Result: ${JSON.stringify(result)}` - } - ] - }; - } catch (error: any) { - return { - content: [ - { - type: "text", - text: `Error setting node variable: ${error instanceof Error ? error.message : String(error)}` - } - ] - }; - } - } -); - // Define command types and parameters type FigmaCommand = - | "get_document_info" - | "get_selection" - | "get_node_info" - | "get_nodes_info" - | "read_my_design" - | "create_rectangle" - | "create_frame" - | "create_text" - | "set_fill_color" - | "set_stroke_color" - | "move_node" - | "resize_node" - | "delete_node" - | "delete_multiple_nodes" - | "get_styles" - | "get_local_components" - | "create_component_instance" - | "get_instance_overrides" - | "set_instance_overrides" - | "export_node_as_image" - | "join" - | "set_corner_radius" - | "clone_node" - | "set_text_content" - | "scan_text_nodes" - | "set_multiple_text_contents" - | "get_annotations" - | "set_annotation" - | "set_multiple_annotations" - | "scan_nodes_by_types" - | "set_layout_mode" - | "set_padding" - | "set_axis_align" - | "set_layout_sizing" - | "set_item_spacing" - | "get_reactions" - | "set_default_connector" - | "create_connections" - | "list_variables" - | "get_node_variables" - | "set_node_variable" - | "get_node_paints" - | "set_node_paints"; +| "get_document_info" +| "get_selection" +| "get_node_info" +| "get_nodes_info" +| "read_my_design" +| "create_rectangle" +| "create_frame" +| "create_text" +| "set_fill_color" +| "set_stroke_color" +| "move_node" +| "resize_node" +| "delete_node" +| "delete_multiple_nodes" +| "get_styles" +| "get_local_components" +| "get_team_components" +| "create_component_instance" +| "get_instance_overrides" +| "set_instance_overrides" +| "export_node_as_image" +| "join" +| "set_corner_radius" +| "clone_node" +| "set_text_content" +| "scan_text_nodes" +| "set_multiple_text_contents" +| "get_annotations" +| "set_annotation" +| "set_multiple_annotations" +| "scan_nodes_by_types" +| "set_layout_mode" +| "set_padding" +| "set_axis_align" +| "set_layout_sizing" +| "set_item_spacing" +| "get_reactions" +| "set_default_connector" +| "create_connections" +| "list_variables" +| "get_node_variables" +| "get_node_paints" +| "set_node_paints"; type CommandParams = { get_document_info: Record; @@ -2917,16 +2931,36 @@ type CommandParams = { }; list_variables: Record; get_node_variables: { nodeId: string }; - set_node_variable: { nodeId: string; property: string; variableId: string; modeId?: string }; get_node_paints: { nodeId: string }; set_node_paints: { nodeId: string; paints: Array<{ - type: string; - color?: { r: number; g: number; b: number }; + type: + | 'SOLID' + | 'GRADIENT_LINEAR' + | 'GRADIENT_RADIAL' + | 'GRADIENT_ANGULAR' + | 'GRADIENT_DIAMOND' + | 'IMAGE' + | 'VIDEO' + | 'VARIABLE_ALIAS'; + visible?: boolean; + opacity?: number; + blendMode?: string; + boundVariables?: { + color?: { + type: string; + variableId: string; + }; + [key: string]: unknown; + }; + color?: { r: number; g: number; b: number; a?: number }; gradientStops?: Array<{ color: { r: number; g: number; b: number; a?: number }; position: number }>; imageRef?: string; + // Allow additional properties as per Paint interface + [key: string]: unknown; }>; + paintsType?: "fills" | "strokes"; }; }; From 15a65176d6175bb03d5540d2eaad416c847fd79c Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Fri, 6 Jun 2025 12:53:03 +0200 Subject: [PATCH 3/7] wip --- src/cursor_mcp_plugin/code.js | 114 +++++++++++++++++ src/talk_to_figma_mcp/server.ts | 216 +++++++++++++++++++++++++++++++- 2 files changed, 328 insertions(+), 2 deletions(-) diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index f9e6447..15b9ff5 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -232,6 +232,12 @@ async function handleCommand(command, params) { return await listVariables(); case "get_node_variables": return await getNodeVariables(params); + case "create_variable": + return await createVariable(params); + case "set_variable_value": + return await setVariableValue(params); + case "list_collections": + return await listCollections(); case "set_node_paints": return await setNodePaints(params); case "get_node_paints": @@ -1423,6 +1429,114 @@ async function getNodeVariables(params) { return { nodeId, boundVariables: node.boundVariables }; } +async function listCollections(params) { + if (!figma.variables || !figma.variables.getLocalVariableCollectionsAsync) { + throw new Error("Figma Variables API not available"); + } + const collections = await figma.variables.getLocalVariableCollectionsAsync(); + return collections.map(c => ({ + id: c.id, + name: c.name, + key: c.key, + description: c.description + })); +} + +/** + * Creates a new variable in the Figma document. + * @param {Object} params + * @param {string} params.name - The name of the variable + * @param {"FLOAT"|"STRING"|"BOOLEAN"|"COLOR"} params.resolvedType - The type of the variable + * @param {string} [params.description] - Optional description + * @param {string} collectionId - The Figma collection to contain the variable. + * @returns {Promise} - The created variable object + */ +async function createVariable(params) { + if (!figma.variables || !figma.variables.createVariable) { + throw new Error("Figma Variables API not available"); + } + const { name, resolvedType, description, collectionId } = params || {}; + if (!name || !resolvedType) { + throw new Error("Missing required parameters: name, resolvedType"); + } + const collection = await figma.variables.getVariableCollectionByIdAsync(collectionId); + if (!collection) { + throw new Error(`Variable collection not found: ${collectionId}`); + } + const variable = figma.variables.createVariable(name, collection, resolvedType); + if (description) variable.description = description; + return { + id: variable.id, + name: variable.name, + key: variable.key, + resolvedType: variable.resolvedType, + valuesByMode: variable.valuesByMode, + scopes: variable.scopes, + description: variable.description + }; +} + +/** + * Sets a value for a Figma variable in a specific mode. + * Supports setting direct values (FLOAT, STRING, BOOLEAN, COLOR) or referencing another variable (alias). + * + * @async + * @function setVariableValue + * @param {Object} params - Parameters for setting the variable value. + * @param {string} params.variableId - The ID of the variable to update. + * @param {string} params.modeId - The ID of the mode to set the value for. + * @param {*} params.value - The value to set. For COLOR, should be an object { r, g, b, a }. + * @param {"FLOAT"|"STRING"|"BOOLEAN"|"COLOR"} params.valueType - The type of the value to set. + * @param {string} [params.variableReferenceId] - Optional. If provided, sets the value as an alias to another variable. + * @returns {Promise} Result object with success status and details of the operation. + * @throws {Error} If required parameters are missing, or if the Figma Variables API is not available, or if the value is invalid. + */ + +async function setVariableValue(params) { + const { variableId, modeId, value, valueType, variableReferenceId } = params || {}; + if (!variableId) { + throw new Error("Missing variableId or parameter"); + } + if (!figma.variables || !figma.variables.getVariableByIdAsync) { + throw new Error("Figma Variables API not available"); + } + const variable = await figma.variables.getVariableByIdAsync(variableId); + if (!variable) throw new Error(`Variable not found: ${variableId}`); + + let mode = modeId || Object.keys(variable.valuesByMode)[0]; + + // If variableReferenceId is provided, set value as a reference to another variable + if (variableReferenceId) { + const refVariable = await figma.variables.getVariableByIdAsync(variableReferenceId); + if (!refVariable) throw new Error(`Reference variable not found: ${variableReferenceId}`); + variable.setValueForMode(mode, { type: "VARIABLE_ALIAS", id: variableReferenceId }); + return { success: true, variableId, mode, value: { type: "VARIABLE_ALIAS", id: variableReferenceId } }; + } + + // Otherwise, set the value directly based on valueType + if (valueType === "COLOR") { + // value should be { r, g, b, a } + if (!value || typeof value !== "object" || value.r === undefined || value.g === undefined || value.b === undefined || value.a === undefined) { + throw new Error("Invalid color value"); + } + variable.setValueForMode(mode, { + r: Number(value.r), + g: Number(value.g), + b: Number(value.b), + a: Number(value.a) + }); + } else if (valueType === "FLOAT") { + variable.setValueForMode(mode, Number(value)); + } else if (valueType === "STRING") { + variable.setValueForMode(mode, String(value)); + } else if (valueType === "BOOLEAN") { + variable.setValueForMode(mode, Boolean(value)); + } else { + throw new Error("Unsupported valueType"); + } + return { success: true, variableId, mode, value }; +} + // --- setNodePaints: Set fills or strokes on a node --- async function setNodePaints(params) { const { nodeId, paints, paintsType = "fills" } = params || {}; diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index 96ef838..8e35d68 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -1623,6 +1623,86 @@ server.prompt( } ); +server.prompt( + "create_third_level_tokens", + "Best practices for creating third-level tokens for selection", + (extra) => { + return { + messages: [ + { + role: "assistant", + content: { + type: "text", + text: ` +#### 1. Audit the Document +- **get_selection()** → collect all selected components. +- For each component ID + • **get_node_info(componentId)** → store name, type, and child hierarchy. + +#### 2. Inventory Existing Colour Bindings +- For every component’s root node + • **scan_nodes_by_types(nodeId, ["TEXT","FRAME","RECTANGLE","ELLIPSE","VECTOR"])** + → iterate through each leaf node. + • On each leaf + – **get_node_paints(nodeId, "fills")** + – **get_node_paints(nodeId, "strokes")** + → If any Paint object is "type":"VARIABLE_REFERENCE", record: + oldVarId, oldVarName, nodeId, property ("fills"/"strokes"). + +#### 3. Derive New Variable Names +_Nomenclature: **(Component-Type)-Element-What-State**_ +- **Component-Type** = first segment of component.name ("Button / Primary" → “Button”). +- **Element** heuristics on node.name + • “label” → "Label" + • “icon” → "Icon" + • default → "Background" +- **State** heuristics on node.name + • “hover” → "Hover" • “pressed” → "Pressed" • “disabled” → "Disabled" • else → "Enabled". +- Assemble **newVarName** = "///". + +#### 4. Create or Re-use 3-Level Variables +- **list_variables(collectionName:"Components")** → see if *newVarName* already exists. +- If missing → **create_variable(collectionId, newVarName, "COLOR")** → returns *newVarId*. +- **set_variable_value(newVarId, {mode:"REFERENCE", id:oldVarId})** + → makes the new variable an **alias** of the old one (no visual change). + +#### 5. Re-bind Nodes to New Variables +- For each recorded nodeId/property pair + • Build a copy of the paint array, swapping "variableId: oldVarId → newVarId". + • **set_node_paints(nodeId, property, updatedPaints)**. + +#### 6. Verification +- On completion, loop through the touched nodes again: + • **get_node_paints(nodeId, property)** → confirm *variableId = newVarId*. +- Optionally print a Markdown report listing: + • Total components scanned + • New variables created + • Mapping table "oldVarName → newVarName". + +#### 7. Safety / Rollback (optional) +- Wrap Steps 4-5 in try/catch. If any error: + • **set_node_paints(nodeId, property, originalPaints)** to restore. + • (If you created variables this run) consider deleting them. + +--- +##### Example Mapping +| Component | Node Name | Old Var | New Var (3-Level) | +|-----------|-----------|---------|--------------------| +| Button | Primary BG | Button/Background | **Button-Primary-Background-Enabled** | +| TextInput | Label Text | Input/Label | **TextInput-Label-Text-Active** | +| Toggle | Track BG | Toggle/Track | **Toggle-Track-Background-Enabled** | + +> Run the steps in order; they're idempotent, so you can re-run safely. +> Adjust the Element/State heuristics if your layer naming differs.`, + }, + }, + ], + description: "Best practices for creating third level tokens for selection", + }; + } +); + + // Text Node Scanning Tool server.tool( "scan_text_nodes", @@ -2042,7 +2122,7 @@ server.prompt( The process of converting manual annotations (numbered/alphabetical indicators with connected descriptions) to Figma's native annotations: -1. Get selected frame/component information +1. Get the selected frame/component that contains annotations 2. Scan and collect all annotation text nodes 3. Scan target UI elements (components, instances, frames) 4. Match annotations to appropriate UI elements @@ -2742,6 +2822,119 @@ server.tool( } ); +// Figma Variables: Create a new variable +server.tool( + "create_variable", + "Create a new variable inside a collection. Returns the created variable object.", + { + name: z.string().describe("The name of the variable"), + resolvedType: z.enum(["FLOAT", "STRING", "BOOLEAN", "COLOR"]).describe("The type of the variable"), + description: z.string().optional().describe("Optional description for the variable"), + collectionId: z.string().describe("Collection ID to create the variable in you may use the 'list_collections' tool to find the collection ID") + }, + async ({ name, resolvedType, description, collectionId }) => { + try { + // Structure matches Figma plugin API: https://www.figma.com/plugin-docs/api/VariableCollection/ + const params: any = { + name, + resolvedType, + description, + collectionId + }; + + const result = await sendCommandToFigma("create_variable", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error creating variable: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + } +); + +server.tool( + "set_variable_value", + "Set the value of a variable in the Figma document. Returns the updated variable object.", + { + variableId: z.string().describe("The ID of the variable to update"), + modeId: z.string().optional().describe("Optional mode ID for the variable, if applicable"), + value: z.object({}).optional().describe("The value for the variable"), + valueType: z.enum(["FLOAT", "STRING", "BOOLEAN", "COLOR"]).describe("The type of the value to set"), + variableReferenceId: z.string().optional().describe("Optional reference to another variable") + }, + async ({ variableId, modeId, value, valueType, variableReferenceId }) => { + try { + const result = await sendCommandToFigma("set_variable_value", { + variableId, + modeId, + value, + valueType, + variableReferenceId + }); + return { + content: [ + { + type: "text", + text: JSON.stringify(result) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error setting variable value: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + } +); + +server.tool( + "list_collections", + "List all variable collections in the Figma document. Returns an array of collection objects, including their id, name, and type.", + + {}, + async (): Promise => { + try { + const result = await sendCommandToFigma("list_collections"); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2) + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error listing collections: ${error instanceof Error ? error.message : String(error)}` + } + ] + }; + } + } +); + + + // Define command types and parameters type FigmaCommand = | "get_document_info" @@ -2784,10 +2977,14 @@ type FigmaCommand = | "set_default_connector" | "create_connections" | "list_variables" +| "list_collections" | "get_node_variables" | "get_node_paints" -| "set_node_paints"; +| "set_node_paints" +| "create_variable" +| "set_variable_value"; +// Define the parameters for each command type CommandParams = { get_document_info: Record; get_selection: Record; @@ -2930,6 +3127,7 @@ type CommandParams = { }>; }; list_variables: Record; + list_collections: Record; get_node_variables: { nodeId: string }; get_node_paints: { nodeId: string }; set_node_paints: { @@ -2962,6 +3160,20 @@ type CommandParams = { }>; paintsType?: "fills" | "strokes"; }; + create_variable: { + name: string; + resolvedType: "FLOAT" | "STRING" | "BOOLEAN" | "COLOR"; + scopes: string[]; + description?: string; + }; + set_variable_value: { + variableId: string; + modeId?: string; + collectionId?: string; + valueType: "FLOAT" | "STRING" | "BOOLEAN" | "COLOR"; + value?: any; // Value can be of any type depending on the variable type + variableReferenceId?: string; // Optional reference to another variable + }; }; From e4922524449b28a71f5d15c3cb9487658d063a4a Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Fri, 6 Jun 2025 12:55:03 +0200 Subject: [PATCH 4/7] wip --- src/talk_to_figma_mcp/server.ts | 80 --------------------------------- 1 file changed, 80 deletions(-) diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index 8e35d68..c41e04a 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -1623,86 +1623,6 @@ server.prompt( } ); -server.prompt( - "create_third_level_tokens", - "Best practices for creating third-level tokens for selection", - (extra) => { - return { - messages: [ - { - role: "assistant", - content: { - type: "text", - text: ` -#### 1. Audit the Document -- **get_selection()** → collect all selected components. -- For each component ID - • **get_node_info(componentId)** → store name, type, and child hierarchy. - -#### 2. Inventory Existing Colour Bindings -- For every component’s root node - • **scan_nodes_by_types(nodeId, ["TEXT","FRAME","RECTANGLE","ELLIPSE","VECTOR"])** - → iterate through each leaf node. - • On each leaf - – **get_node_paints(nodeId, "fills")** - – **get_node_paints(nodeId, "strokes")** - → If any Paint object is "type":"VARIABLE_REFERENCE", record: - oldVarId, oldVarName, nodeId, property ("fills"/"strokes"). - -#### 3. Derive New Variable Names -_Nomenclature: **(Component-Type)-Element-What-State**_ -- **Component-Type** = first segment of component.name ("Button / Primary" → “Button”). -- **Element** heuristics on node.name - • “label” → "Label" - • “icon” → "Icon" - • default → "Background" -- **State** heuristics on node.name - • “hover” → "Hover" • “pressed” → "Pressed" • “disabled” → "Disabled" • else → "Enabled". -- Assemble **newVarName** = "///". - -#### 4. Create or Re-use 3-Level Variables -- **list_variables(collectionName:"Components")** → see if *newVarName* already exists. -- If missing → **create_variable(collectionId, newVarName, "COLOR")** → returns *newVarId*. -- **set_variable_value(newVarId, {mode:"REFERENCE", id:oldVarId})** - → makes the new variable an **alias** of the old one (no visual change). - -#### 5. Re-bind Nodes to New Variables -- For each recorded nodeId/property pair - • Build a copy of the paint array, swapping "variableId: oldVarId → newVarId". - • **set_node_paints(nodeId, property, updatedPaints)**. - -#### 6. Verification -- On completion, loop through the touched nodes again: - • **get_node_paints(nodeId, property)** → confirm *variableId = newVarId*. -- Optionally print a Markdown report listing: - • Total components scanned - • New variables created - • Mapping table "oldVarName → newVarName". - -#### 7. Safety / Rollback (optional) -- Wrap Steps 4-5 in try/catch. If any error: - • **set_node_paints(nodeId, property, originalPaints)** to restore. - • (If you created variables this run) consider deleting them. - ---- -##### Example Mapping -| Component | Node Name | Old Var | New Var (3-Level) | -|-----------|-----------|---------|--------------------| -| Button | Primary BG | Button/Background | **Button-Primary-Background-Enabled** | -| TextInput | Label Text | Input/Label | **TextInput-Label-Text-Active** | -| Toggle | Track BG | Toggle/Track | **Toggle-Track-Background-Enabled** | - -> Run the steps in order; they're idempotent, so you can re-run safely. -> Adjust the Element/State heuristics if your layer naming differs.`, - }, - }, - ], - description: "Best practices for creating third level tokens for selection", - }; - } -); - - // Text Node Scanning Tool server.tool( "scan_text_nodes", From 39d5f1189a5805a16fdd00ea95731533d03cfd12 Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Fri, 6 Jun 2025 13:23:31 +0200 Subject: [PATCH 5/7] refactor: streamline paint type handling in setNodePaints function --- src/cursor_mcp_plugin/code.js | 14 +++++++------- src/talk_to_figma_mcp/server.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index 15b9ff5..31bd174 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -1563,15 +1563,10 @@ async function setNodePaints(params) { case 'SOLID': if (!paint.color && paint.boundVariables && paint.boundVariables.color) { // get variable color and return as formatted paint - console.log(`Using bound variable color for SOLID paint ${paint.boundVariables.color}`); const variableColor = await figma.variables.getVariableByIdAsync(paint.boundVariables.color.variableId); + //get value from variable for default mode - console.log('variableColor', variableColor); - const variableColorValue = Object.values(variableColor.valuesByMode)[0]; - console.log('variableColorValue', variableColorValue); - - console.log(variableColor); formattedPaint = { type: 'SOLID', color: { @@ -1631,7 +1626,12 @@ async function setNodePaints(params) { if (paint.boundVariables && paint.boundVariables.color) { const variableColor = await figma.variables.getVariableByIdAsync(paint.boundVariables.color.variableId); - return figma.variables.setBoundVariableForPaint(formattedPaint, 'color', variableColor); + try { + return figma.variables.setBoundVariableForPaint(formattedPaint, 'color', variableColor); + } catch (error) { + console.error(`Error setting bound variable for paint: ${error.message}`); + throw new Error(`Error setting ${formattedPaint}: ${error.message}`); + } } else { return formattedPaint; } diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index c41e04a..d923a67 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -701,7 +701,7 @@ server.tool( try { const result = await sendCommandToFigma("get_node_paints", { nodeId, - paintsType: paintsType || "fills", + paintsType, }); return { content: [ From 1f74b439df13ccdc9399ba7b0dd217f582f1d128 Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Fri, 6 Jun 2025 13:27:58 +0200 Subject: [PATCH 6/7] wip --- src/cursor_mcp_plugin/code.js | 47 ++++++++------- src/talk_to_figma_mcp/package.json | 4 +- src/talk_to_figma_mcp/server.ts | 96 +++++++++++++++--------------- 3 files changed, 74 insertions(+), 73 deletions(-) diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index 31bd174..b22ee45 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -1,5 +1,6 @@ // This is the main code file for the Cursor MCP Figma plugin // It handles Figma API commands + // Plugin state const state = { serverPort: 3055, // Default port @@ -142,8 +143,8 @@ async function handleCommand(command, params) { return await getStyles(); case "get_local_components": return await getLocalComponents(); - case "get_team_components": - return await getTeamComponents(); + // case "get_team_components": + // return await getTeamComponents(); case "create_component_instance": return await createComponentInstance(params); case "export_node_as_image": @@ -1153,24 +1154,24 @@ async function getLocalComponents() { }; } -async function getTeamComponents() { - try { - const teamComponents = - await figma.teamLibrary.getAvailableComponentsAsync(); - - return { - count: teamComponents.length, - components: teamComponents.map((component) => ({ - key: component.key, - name: component.name, - description: component.description, - libraryName: component.libraryName, - })), - }; - } catch (error) { - throw new Error(`Error getting team components: ${error.message}`); - } -} +// async function getTeamComponents() { +// try { +// const teamComponents = +// await figma.teamLibrary.getAvailableComponentsAsync(); + +// return { +// count: teamComponents.length, +// components: teamComponents.map((component) => ({ +// key: component.key, +// name: component.name, +// description: component.description, +// libraryName: component.libraryName, +// })), +// }; +// } catch (error) { +// throw new Error(`Error getting team components: ${error.message}`); +// } +// } async function createComponentInstance(params) { const { componentKey, x = 0, y = 0 } = params || {}; @@ -2665,17 +2666,17 @@ async function getAnnotations(params) { throw new Error(`Node type ${node.type} does not support annotations`); } - const response = { + const result = { nodeId: node.id, name: node.name, annotations: node.annotations || [], }; if (includeCategories) { - response.categories = Object.values(categoriesMap); + result.categories = Object.values(categoriesMap); } - return response; + return result; } else { // Get all annotations in the current page const annotations = []; diff --git a/src/talk_to_figma_mcp/package.json b/src/talk_to_figma_mcp/package.json index 3deb1e6..b7bfb6a 100644 --- a/src/talk_to_figma_mcp/package.json +++ b/src/talk_to_figma_mcp/package.json @@ -24,8 +24,8 @@ "devDependencies": { "@types/node": "^20.10.5", "@types/uuid": "^9.0.7", - "@types/ws": "^8.18.1", + "@types/ws": "^8.5.10", "ts-node": "^10.9.2", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index d923a67..e83fcbb 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -2042,7 +2042,7 @@ server.prompt( The process of converting manual annotations (numbered/alphabetical indicators with connected descriptions) to Figma's native annotations: -1. Get the selected frame/component that contains annotations +1. Get selected frame/component information 2. Scan and collect all annotation text nodes 3. Scan target UI elements (components, instances, frames) 4. Match annotations to appropriate UI elements @@ -2857,52 +2857,52 @@ server.tool( // Define command types and parameters type FigmaCommand = -| "get_document_info" -| "get_selection" -| "get_node_info" -| "get_nodes_info" -| "read_my_design" -| "create_rectangle" -| "create_frame" -| "create_text" -| "set_fill_color" -| "set_stroke_color" -| "move_node" -| "resize_node" -| "delete_node" -| "delete_multiple_nodes" -| "get_styles" -| "get_local_components" -| "get_team_components" -| "create_component_instance" -| "get_instance_overrides" -| "set_instance_overrides" -| "export_node_as_image" -| "join" -| "set_corner_radius" -| "clone_node" -| "set_text_content" -| "scan_text_nodes" -| "set_multiple_text_contents" -| "get_annotations" -| "set_annotation" -| "set_multiple_annotations" -| "scan_nodes_by_types" -| "set_layout_mode" -| "set_padding" -| "set_axis_align" -| "set_layout_sizing" -| "set_item_spacing" -| "get_reactions" -| "set_default_connector" -| "create_connections" -| "list_variables" -| "list_collections" -| "get_node_variables" -| "get_node_paints" -| "set_node_paints" -| "create_variable" -| "set_variable_value"; + | "get_document_info" + | "get_selection" + | "get_node_info" + | "get_nodes_info" + | "read_my_design" + | "create_rectangle" + | "create_frame" + | "create_text" + | "set_fill_color" + | "set_stroke_color" + | "move_node" + | "resize_node" + | "delete_node" + | "delete_multiple_nodes" + | "get_styles" + | "get_local_components" + | "get_team_components" + | "create_component_instance" + | "get_instance_overrides" + | "set_instance_overrides" + | "export_node_as_image" + | "join" + | "set_corner_radius" + | "clone_node" + | "set_text_content" + | "scan_text_nodes" + | "set_multiple_text_contents" + | "get_annotations" + | "set_annotation" + | "set_multiple_annotations" + | "scan_nodes_by_types" + | "set_layout_mode" + | "set_padding" + | "set_axis_align" + | "set_layout_sizing" + | "set_item_spacing" + | "get_reactions" + | "set_default_connector" + | "create_connections" + | "list_variables" + | "list_collections" + | "get_node_variables" + | "get_node_paints" + | "set_node_paints" + | "create_variable" + | "set_variable_value"; // Define the parameters for each command type CommandParams = { @@ -3097,7 +3097,7 @@ type CommandParams = { }; -// Helper function to process Figma node responses + // Helper function to process Figma node responses function processFigmaNodeResponse(result: unknown): any { if (!result || typeof result !== "object") { return result; From 082181bae28da07bd7561a49c03d3897ff202a47 Mon Sep 17 00:00:00 2001 From: Luciano Gucciardo Date: Wed, 11 Jun 2025 11:27:15 +0200 Subject: [PATCH 7/7] refactor: improve color value handling in setVariableValue function and update tool descriptions --- src/cursor_mcp_plugin/code.js | 4 ++-- src/talk_to_figma_mcp/server.ts | 25 ++++++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index b22ee45..fc370ed 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -1517,14 +1517,14 @@ async function setVariableValue(params) { // Otherwise, set the value directly based on valueType if (valueType === "COLOR") { // value should be { r, g, b, a } - if (!value || typeof value !== "object" || value.r === undefined || value.g === undefined || value.b === undefined || value.a === undefined) { + if (!value || typeof value !== "object" || value.r === undefined || value.g === undefined || value.b === undefined) { throw new Error("Invalid color value"); } variable.setValueForMode(mode, { r: Number(value.r), g: Number(value.g), b: Number(value.b), - a: Number(value.a) + a: Number(value.a) || 1 }); } else if (valueType === "FLOAT") { variable.setValueForMode(mode, Number(value)); diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index e83fcbb..07c9be7 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -729,7 +729,7 @@ server.tool( // Set Node Paints Tool server.tool( "set_node_paints", - "Replace the Paint definition (either fills or strokes) on a node and set color variables to the node.", + "Bind the fills or strokes of a node to a variable.", { nodeId: z.string().describe("The ID of the node to modify"), paints: z @@ -2767,7 +2767,7 @@ server.tool( content: [ { type: "text", - text: JSON.stringify(result, null, 2) + text: `The variable has been created ${JSON.stringify(result, null, 2)} now you must 'set_variable_value' to assign the proper value to the variable. The variable will not be usable until it has a value assigned to it.` } ] }; @@ -2790,19 +2790,34 @@ server.tool( { variableId: z.string().describe("The ID of the variable to update"), modeId: z.string().optional().describe("Optional mode ID for the variable, if applicable"), - value: z.object({}).optional().describe("The value for the variable"), + value: z.object({ + r: z.number().optional(), + g: z.number().optional(), + b: z.number().optional(), + a: z.number().optional() + }).optional().describe("The value for the variable"), valueType: z.enum(["FLOAT", "STRING", "BOOLEAN", "COLOR"]).describe("The type of the value to set"), variableReferenceId: z.string().optional().describe("Optional reference to another variable") }, async ({ variableId, modeId, value, valueType, variableReferenceId }) => { try { + const formattedValue = valueType === "COLOR" && value + ? { + r: value.r || 0, + g: value.g || 0, + b: value.b || 0, + a: value.a || 1 + } + : value; + const result = await sendCommandToFigma("set_variable_value", { variableId, modeId, - value, + value: formattedValue, valueType, variableReferenceId }); + return { content: [ { @@ -3097,7 +3112,7 @@ type CommandParams = { }; - // Helper function to process Figma node responses +// Helper function to process Figma node responses function processFigmaNodeResponse(result: unknown): any { if (!result || typeof result !== "object") { return result;