Skip to content
Open
8 changes: 0 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
336 changes: 60 additions & 276 deletions src/cursor_mcp_plugin/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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 = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand Down Expand Up @@ -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)` : ''}`
};
}

Expand Down
Loading