diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index b5fb794..fc370ed 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -229,6 +229,20 @@ 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 "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": + return await getNodePaints(params); default: throw new Error(`Unknown command: ${command}`); } @@ -1387,6 +1401,272 @@ 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 }; +} + +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) { + 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) || 1 + }); + } 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 || {}; + 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}`); + + // 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 + const variableColor = await figma.variables.getVariableByIdAsync(paint.boundVariables.color.variableId); + + //get value from variable for default mode + const variableColorValue = Object.values(variableColor.valuesByMode)[0]; + 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); + 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; + } + })); + + // 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] + }; +} + +// --- 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 { diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index e7ad940..07c9be7 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -685,6 +685,116 @@ 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, + }); + 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", + "Bind the fills or strokes of a node to a variable.", + { + nodeId: z.string().describe("The ID of the node to modify"), + 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'."), + }, + 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", @@ -990,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) + }`, }, ], }; @@ -2542,6 +2682,193 @@ 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: 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: `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.` + } + ] + }; + } 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({ + 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: formattedValue, + 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 = @@ -2561,6 +2888,7 @@ type FigmaCommand = | "delete_multiple_nodes" | "get_styles" | "get_local_components" + | "get_team_components" | "create_component_instance" | "get_instance_overrides" | "set_instance_overrides" @@ -2582,8 +2910,16 @@ type FigmaCommand = | "set_item_spacing" | "get_reactions" | "set_default_connector" - | "create_connections"; - + | "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 = { get_document_info: Record; get_selection: Record; @@ -2725,11 +3061,58 @@ type CommandParams = { text?: string; }>; }; - + list_variables: Record; + list_collections: Record; + get_node_variables: { nodeId: string }; + get_node_paints: { nodeId: string }; + set_node_paints: { + nodeId: string; + paints: Array<{ + 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"; + }; + 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 + }; }; - // 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;