diff --git a/readme.md b/readme.md index fe8502f..6cea53a 100644 --- a/readme.md +++ b/readme.md @@ -146,8 +146,6 @@ The MCP server provides the following tools for interacting with Figma: ### Prototyping & Connections - `get_reactions` - Get all prototype reactions from nodes with visual highlight animation -- `set_default_connector` - Set a copied FigJam connector as the default connector style for creating connections (must be set before creating connections) -- `create_connections` - Create FigJam connector lines between nodes, based on prototype flows or custom mapping ### Creating Elements @@ -208,7 +206,6 @@ The MCP server includes several helper prompts to guide you through complex desi - `text_replacement_strategy` - Systematic approach for replacing text in Figma designs - `annotation_conversion_strategy` - Strategy for converting manual annotations to Figma's native annotations - `swap_overrides_instances` - Strategy for transferring overrides between component instances in Figma -- `reaction_to_connector_strategy` - Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions', and guiding the use 'create_connections' in sequence ## Development @@ -252,11 +249,6 @@ When working with the Figma MCP: - Create native annotations with `set_multiple_annotations` in batches - Verify all annotations are properly linked to their targets - Delete legacy annotation nodes after successful conversion -11. Visualize prototype noodles as FigJam connectors: - -- Use `get_reactions` to extract prototype flows, -- set a default connector with `set_default_connector`, -- and generate connector lines with `create_connections` for clear visual flow mapping. ## License diff --git a/src/cursor_mcp_plugin/code.js b/src/cursor_mcp_plugin/code.js index 51b0b57..d487dd2 100644 --- a/src/cursor_mcp_plugin/code.js +++ b/src/cursor_mcp_plugin/code.js @@ -224,11 +224,7 @@ async function handleCommand(command, params) { if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) { throw new Error("Missing or invalid nodeIds parameter"); } - return await getReactions(params.nodeIds); - case "set_default_connector": - return await setDefaultConnector(params); - case "create_connections": - return await createConnections(params); + return await getReactions(params.nodeIds); case "set_focus": return await setFocus(params); case "set_selections": @@ -3562,92 +3558,6 @@ async function setItemSpacing(params) { }; } -async function setDefaultConnector(params) { - const { connectorId } = params || {}; - - // If connectorId is provided, search and set by that ID (do not check existing storage) - if (connectorId) { - // Get node by specified ID - const node = await figma.getNodeByIdAsync(connectorId); - if (!node) { - throw new Error(`Connector node not found with ID: ${connectorId}`); - } - - // Check node type - if (node.type !== 'CONNECTOR') { - throw new Error(`Node is not a connector: ${connectorId}`); - } - - // Set the found connector as the default connector - await figma.clientStorage.setAsync('defaultConnectorId', connectorId); - - return { - success: true, - message: `Default connector set to: ${connectorId}`, - connectorId: connectorId - }; - } - // If connectorId is not provided, check existing storage - else { - // Check if there is an existing default connector in client storage - try { - const existingConnectorId = await figma.clientStorage.getAsync('defaultConnectorId'); - - // If there is an existing connector ID, check if the node is still valid - if (existingConnectorId) { - try { - const existingConnector = await figma.getNodeByIdAsync(existingConnectorId); - - // If the stored connector still exists and is of type CONNECTOR - if (existingConnector && existingConnector.type === 'CONNECTOR') { - return { - success: true, - message: `Default connector is already set to: ${existingConnectorId}`, - connectorId: existingConnectorId, - exists: true - }; - } - // The stored connector is no longer valid - find a new connector - else { - console.log(`Stored connector ID ${existingConnectorId} is no longer valid, finding a new connector...`); - } - } catch (error) { - console.log(`Error finding stored connector: ${error.message}. Will try to set a new one.`); - } - } - } catch (error) { - console.log(`Error checking for existing connector: ${error.message}`); - } - - // If there is no stored default connector or it is invalid, find one in the current page - try { - // Find CONNECTOR type nodes in the current page - const currentPageConnectors = figma.currentPage.findAllWithCriteria({ types: ['CONNECTOR'] }); - - if (currentPageConnectors && currentPageConnectors.length > 0) { - // Use the first connector found - const foundConnector = currentPageConnectors[0]; - const autoFoundId = foundConnector.id; - - // Set the found connector as the default connector - await figma.clientStorage.setAsync('defaultConnectorId', autoFoundId); - - return { - success: true, - message: `Automatically found and set default connector to: ${autoFoundId}`, - connectorId: autoFoundId, - autoSelected: true - }; - } else { - // If no connector is found in the current page, show a guide message - throw new Error('No connector found in the current page. Please create a connector in Figma first or specify a connector ID.'); - } - } catch (error) { - // Error occurred while running findAllWithCriteria - throw new Error(`Failed to find a connector: ${error.message}`); - } - } -} async function createCursorNode(targetNodeId) { const svgString = ` @@ -3756,202 +3666,76 @@ async function createCursorNode(targetNodeId) { } } -async function createConnections(params) { - if (!params || !params.connections || !Array.isArray(params.connections)) { - throw new Error('Missing or invalid connections parameter'); + +// Set focus on a specific node +async function setFocus(params) { + if (!params || !params.nodeId) { + throw new Error("Missing nodeId parameter"); } - - const { connections } = params; - - // Command ID for progress tracking - const commandId = generateCommandId(); - sendProgressUpdate( - commandId, - "create_connections", - "started", - 0, - connections.length, - 0, - `Starting to create ${connections.length} connections` - ); - - // Get default connector ID from client storage - const defaultConnectorId = await figma.clientStorage.getAsync('defaultConnectorId'); - if (!defaultConnectorId) { - throw new Error('No default connector set. Please try one of the following options to create connections:\n1. Create a connector in FigJam and copy/paste it to your current page, then run the "set_default_connector" command.\n2. Select an existing connector on the current page, then run the "set_default_connector" command.'); + + const node = await figma.getNodeByIdAsync(params.nodeId); + if (!node) { + throw new Error(`Node with ID ${params.nodeId} not found`); } + + // Set selection to the node + figma.currentPage.selection = [node]; - // Get the default connector - const defaultConnector = await figma.getNodeByIdAsync(defaultConnectorId); - if (!defaultConnector) { - throw new Error(`Default connector not found with ID: ${defaultConnectorId}`); + // Scroll and zoom to show the node in viewport + figma.viewport.scrollAndZoomIntoView([node]); + + return { + success: true, + name: node.name, + id: node.id, + message: `Focused on node "${node.name}"` + }; +} + +// Set selection to multiple nodes +async function setSelections(params) { + if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) { + throw new Error("Missing or invalid nodeIds parameter"); } - if (defaultConnector.type !== 'CONNECTOR') { - throw new Error(`Node is not a connector: ${defaultConnectorId}`); + + if (params.nodeIds.length === 0) { + throw new Error("nodeIds array cannot be empty"); } - - // Results array for connection creation - const results = []; - let processedCount = 0; - const totalCount = connections.length; - - // Preload fonts (used for text if provided) - let fontLoaded = false; - - for (let i = 0; i < connections.length; i++) { - try { - const { startNodeId: originalStartId, endNodeId: originalEndId, text } = connections[i]; - let startId = originalStartId; - let endId = originalEndId; - - // Check and potentially replace start node ID - if (startId.includes(';')) { - console.log(`Nested start node detected: ${startId}. Creating cursor node.`); - const cursorResult = await createCursorNode(startId); - if (!cursorResult || !cursorResult.id) { - throw new Error(`Failed to create cursor node for nested start node: ${startId}`); - } - startId = cursorResult.id; - } - - const startNode = await figma.getNodeByIdAsync(startId); - if (!startNode) throw new Error(`Start node not found with ID: ${startId}`); - - // Check and potentially replace end node ID - if (endId.includes(';')) { - console.log(`Nested end node detected: ${endId}. Creating cursor node.`); - const cursorResult = await createCursorNode(endId); - if (!cursorResult || !cursorResult.id) { - throw new Error(`Failed to create cursor node for nested end node: ${endId}`); - } - endId = cursorResult.id; - } - const endNode = await figma.getNodeByIdAsync(endId); - if (!endNode) throw new Error(`End node not found with ID: ${endId}`); - - // Clone the default connector - const clonedConnector = defaultConnector.clone(); - - // Update connector name using potentially replaced node names - clonedConnector.name = `TTF_Connector/${startNode.id}/${endNode.id}`; - - // Set start and end points using potentially replaced IDs - clonedConnector.connectorStart = { - endpointNodeId: startId, - magnet: 'AUTO' - }; - - clonedConnector.connectorEnd = { - endpointNodeId: endId, - magnet: 'AUTO' - }; - - // Add text (if provided) - if (text) { - try { - // Try to load the necessary fonts - try { - // First check if default connector has font and use the same - if (defaultConnector.text && defaultConnector.text.fontName) { - const fontName = defaultConnector.text.fontName; - await figma.loadFontAsync(fontName); - clonedConnector.text.fontName = fontName; - } else { - // Try default Inter font - await figma.loadFontAsync({ family: "Inter", style: "Regular" }); - } - } catch (fontError) { - // If first font load fails, try another font style - try { - await figma.loadFontAsync({ family: "Inter", style: "Medium" }); - } catch (mediumFontError) { - // If second font fails, try system font - try { - await figma.loadFontAsync({ family: "System", style: "Regular" }); - } catch (systemFontError) { - // If all font loading attempts fail, throw error - throw new Error(`Failed to load any font: ${fontError.message}`); - } - } - } - - // Set the text - clonedConnector.text.characters = text; - } catch (textError) { - console.error("Error setting text:", textError); - // Continue with connection even if text setting fails - results.push({ - id: clonedConnector.id, - startNodeId: startNodeId, - endNodeId: endNodeId, - text: "", - textError: textError.message - }); - - // Continue to next connection - continue; - } - } - - // Add to results (using the *original* IDs for reference if needed) - results.push({ - id: clonedConnector.id, - originalStartNodeId: originalStartId, - originalEndNodeId: originalEndId, - usedStartNodeId: startId, // ID actually used for connection - usedEndNodeId: endId, // ID actually used for connection - text: text || "" - }); - - // Update progress - processedCount++; - sendProgressUpdate( - commandId, - "create_connections", - "in_progress", - processedCount / totalCount, - totalCount, - processedCount, - `Created connection ${processedCount}/${totalCount}` - ); - - } catch (error) { - console.error("Error creating connection", error); - // Continue processing remaining connections even if an error occurs - processedCount++; - sendProgressUpdate( - commandId, - "create_connections", - "in_progress", - processedCount / totalCount, - totalCount, - processedCount, - `Error creating connection: ${error.message}` - ); - - results.push({ - error: error.message, - connectionInfo: connections[i] - }); + // Get all valid nodes + const nodes = []; + const notFoundIds = []; + + for (const nodeId of params.nodeIds) { + const node = await figma.getNodeByIdAsync(nodeId); + if (node) { + nodes.push(node); + } else { + notFoundIds.push(nodeId); } } + + if (nodes.length === 0) { + throw new Error(`No valid nodes found for the provided IDs: ${params.nodeIds.join(', ')}`); + } + + // Set selection to the nodes + figma.currentPage.selection = nodes; - // Completion update - sendProgressUpdate( - commandId, - "create_connections", - "completed", - 1, - totalCount, - totalCount, - `Completed creating ${results.length} connections` - ); - + // Scroll and zoom to show all nodes in viewport + figma.viewport.scrollAndZoomIntoView(nodes); + + const selectedNodes = nodes.map(node => ({ + name: node.name, + id: node.id + })); + return { success: true, - count: results.length, - connections: results + count: nodes.length, + selectedNodes: selectedNodes, + notFoundIds: notFoundIds, + message: `Selected ${nodes.length} nodes${notFoundIds.length > 0 ? ` (${notFoundIds.length} not found)` : ''}` }; } diff --git a/src/talk_to_figma_mcp/server.ts b/src/talk_to_figma_mcp/server.ts index 7b16785..0f0e97a 100644 --- a/src/talk_to_figma_mcp/server.ts +++ b/src/talk_to_figma_mcp/server.ts @@ -2338,7 +2338,7 @@ server.tool( // A tool to get Figma Prototyping Reactions from multiple nodes server.tool( "get_reactions", - "Get Figma Prototyping Reactions from multiple nodes. CRITICAL: The output MUST be processed using the 'reaction_to_connector_strategy' prompt IMMEDIATELY to generate parameters for connector lines via the 'create_connections' tool.", + "Get Figma Prototyping Reactions from multiple nodes for analyzing prototype flows and interactions.", { nodeIds: z.array(z.string()).describe("Array of node IDs to get reactions from"), }, @@ -2350,16 +2350,8 @@ server.tool( { type: "text", text: JSON.stringify(result) - }, - { - type: "text", - text: "IMPORTANT: You MUST now use the reaction data above and follow the `reaction_to_connector_strategy` prompt to prepare the parameters for the `create_connections` tool call. This is a required next step." } ], - followUp: { - type: "prompt", - prompt: "reaction_to_connector_strategy", - }, }; } catch (error) { return { @@ -2375,105 +2367,56 @@ server.tool( } ); -// Create Connectors Tool -server.tool( - "set_default_connector", - "Set a copied connector node as the default connector", - { - connectorId: z.string().optional().describe("The ID of the connector node to set as default") - }, - async ({ connectorId }: any) => { - try { - const result = await sendCommandToFigma("set_default_connector", { - connectorId - }); - return { - content: [ - { - type: "text", - text: `Default connector set: ${JSON.stringify(result)}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting default connector: ${error instanceof Error ? error.message : String(error)}` - } - ] - }; - } - } -); -// Connect Nodes Tool +// Set Focus Tool server.tool( - "create_connections", - "Create connections between nodes using the default connector style", + "set_focus", + "Set focus on a specific node in Figma by selecting it and scrolling viewport to it", { - connections: z.array(z.object({ - startNodeId: z.string().describe("ID of the starting node"), - endNodeId: z.string().describe("ID of the ending node"), - text: z.string().optional().describe("Optional text to display on the connector") - })).describe("Array of node connections to create") + nodeId: z.string().describe("The ID of the node to focus on"), }, - async ({ connections }: any) => { + async ({ nodeId }: any) => { try { - if (!connections || connections.length === 0) { - return { - content: [ - { - type: "text", - text: "No connections provided" - } - ] - }; - } - - const result = await sendCommandToFigma("create_connections", { - connections - }); - + const result = await sendCommandToFigma("set_focus", { nodeId }); + const typedResult = result as { name: string; id: string }; return { content: [ { type: "text", - text: `Created ${connections.length} connections: ${JSON.stringify(result)}` - } - ] + text: `Focused on node "${typedResult.name}" (ID: ${typedResult.id})`, + }, + ], }; } catch (error) { return { content: [ { type: "text", - text: `Error creating connections: ${error instanceof Error ? error.message : String(error)}` - } - ] + text: `Error setting focus: ${error instanceof Error ? error.message : String(error)}`, + }, + ], }; } } ); -// Set Focus Tool +// Set Selections Tool server.tool( - "set_focus", - "Set focus on a specific node in Figma by selecting it and scrolling viewport to it", + "set_selections", + "Set selection to multiple nodes in Figma and scroll viewport to show them", { - nodeId: z.string().describe("The ID of the node to focus on"), + nodeIds: z.array(z.string()).describe("Array of node IDs to select"), }, - async ({ nodeId }: any) => { + async ({ nodeIds }: any) => { try { - const result = await sendCommandToFigma("set_focus", { nodeId }); - const typedResult = result as { name: string; id: string }; + const result = await sendCommandToFigma("set_selections", { nodeIds }); + const typedResult = result as { selectedNodes: Array<{ name: string; id: string }>; count: number }; return { content: [ { type: "text", - text: `Focused on node "${typedResult.name}" (ID: ${typedResult.id})`, + text: `Selected ${typedResult.count} nodes: ${typedResult.selectedNodes.map(node => `"${node.name}" (${node.id})`).join(', ')}`, }, ], }; @@ -2482,7 +2425,7 @@ server.tool( content: [ { type: "text", - text: `Error setting focus: ${error instanceof Error ? error.message : String(error)}`, + text: `Error setting selections: ${error instanceof Error ? error.message : String(error)}`, }, ], }; @@ -2490,6 +2433,7 @@ server.tool( } ); + // Set Selections Tool server.tool( "set_selections", @@ -2522,89 +2466,6 @@ server.tool( } ); -// Strategy for converting Figma prototype reactions to connector lines -server.prompt( - "reaction_to_connector_strategy", - "Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions'", - (extra) => { - return { - messages: [ - { - role: "assistant", - content: { - type: "text", - text: `# Strategy: Convert Figma Prototype Reactions to Connector Lines - -## Goal -Process the JSON output from the \`get_reactions\` tool to generate an array of connection objects suitable for the \`create_connections\` tool. This visually represents prototype flows as connector lines on the Figma canvas. - -## Input Data -You will receive JSON data from the \`get_reactions\` tool. This data contains an array of nodes, each with potential reactions. A typical reaction object looks like this: -\`\`\`json -{ - "trigger": { "type": "ON_CLICK" }, - "action": { - "type": "NAVIGATE", - "destinationId": "destination-node-id", - "navigationTransition": { ... }, - "preserveScrollPosition": false - } -} -\`\`\` - -## Step-by-Step Process - -### 1. Preparation & Context Gathering - - **Action:** Call \`read_my_design\` on the relevant node(s) to get context about the nodes involved (names, types, etc.). This helps in generating meaningful connector labels later. - - **Action:** Call \`set_default_connector\` **without** the \`connectorId\` parameter. - - **Check Result:** Analyze the response from \`set_default_connector\`. - - If it confirms a default connector is already set (e.g., "Default connector is already set"), proceed to Step 2. - - If it indicates no default connector is set (e.g., "No default connector set..."), you **cannot** proceed with \`create_connections\` yet. Inform the user they need to manually copy a connector from FigJam, paste it onto the current page, select it, and then you can run \`set_default_connector({ connectorId: "SELECTED_NODE_ID" })\` before attempting \`create_connections\`. **Do not proceed to Step 2 until a default connector is confirmed.** - -### 2. Filter and Transform Reactions from \`get_reactions\` Output - - **Iterate:** Go through the JSON array provided by \`get_reactions\`. For each node in the array: - - Iterate through its \`reactions\` array. - - **Filter:** Keep only reactions where the \`action\` meets these criteria: - - Has a \`type\` that implies a connection (e.g., \`NAVIGATE\`, \`OPEN_OVERLAY\`, \`SWAP_OVERLAY\`). **Ignore** types like \`CHANGE_TO\`, \`CLOSE_OVERLAY\`, etc. - - Has a valid \`destinationId\` property. - - **Extract:** For each valid reaction, extract the following information: - - \`sourceNodeId\`: The ID of the node the reaction belongs to (from the outer loop). - - \`destinationNodeId\`: The value of \`action.destinationId\`. - - \`actionType\`: The value of \`action.type\`. - - \`triggerType\`: The value of \`trigger.type\`. - -### 3. Generate Connector Text Labels - - **For each extracted connection:** Create a concise, descriptive text label string. - - **Combine Information:** Use the \`actionType\`, \`triggerType\`, and potentially the names of the source/destination nodes (obtained from Step 1's \`read_my_design\` or by calling \`get_node_info\` if necessary) to generate the label. - - **Example Labels:** - - If \`triggerType\` is "ON\_CLICK" and \`actionType\` is "NAVIGATE": "On click, navigate to [Destination Node Name]" - - If \`triggerType\` is "ON\_DRAG" and \`actionType\` is "OPEN\_OVERLAY": "On drag, open [Destination Node Name] overlay" - - **Keep it brief and informative.** Let this generated string be \`generatedText\`. - -### 4. Prepare the \`connections\` Array for \`create_connections\` - - **Structure:** Create a JSON array where each element is an object representing a connection. - - **Format:** Each object in the array must have the following structure: - \`\`\`json - { - "startNodeId": "sourceNodeId_from_step_2", - "endNodeId": "destinationNodeId_from_step_2", - "text": "generatedText_from_step_3" - } - \`\`\` - - **Result:** This final array is the value you will pass to the \`connections\` parameter when calling the \`create_connections\` tool. - -### 5. Execute Connection Creation - - **Action:** Call the \`create_connections\` tool, passing the array generated in Step 4 as the \`connections\` argument. - - **Verify:** Check the response from \`create_connections\` to confirm success or failure. - -This detailed process ensures you correctly interpret the reaction data, prepare the necessary information, and use the appropriate tools to create the connector lines.` - }, - }, - ], - description: "Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions'", - }; - } -); // Define command types and parameters @@ -2645,8 +2506,6 @@ type FigmaCommand = | "set_layout_sizing" | "set_item_spacing" | "get_reactions" - | "set_default_connector" - | "create_connections" | "set_focus" | "set_selections"; @@ -2781,15 +2640,11 @@ type CommandParams = { types: Array; }; get_reactions: { nodeIds: string[] }; - set_default_connector: { - connectorId?: string | undefined; + set_focus: { + nodeId: string; }; - create_connections: { - connections: Array<{ - startNodeId: string; - endNodeId: string; - text?: string; - }>; + set_selections: { + nodeIds: string[]; }; set_focus: { nodeId: string;