Skip to main content
Glama

Figma MCP Server

by xxflux
// This code runs in the Figma environment // Reference the Figma Plugin API types /// <reference types="@figma/plugin-typings" /> // Constants const DEFAULT_FONT = "Inter"; const WEBSOCKET_SERVER_URL = "ws://localhost:8080"; // Show UI window for WebSocket communication figma.showUI(__html__, { width: 400, height: 350 }); // WebSocket connection is managed in the UI thread figma.ui.postMessage({ type: 'connect-to-server', serverUrl: WEBSOCKET_SERVER_URL }); // Handle messages from the UI thread figma.ui.onmessage = async (msg) => { console.log('Received message in plugin:', msg); try { // Handle different operation types switch (msg.type) { case 'create-rectangle': await createRectangle(msg.position, msg.size, msg.color); break; case 'create-text': { try { console.log("Creating text:", msg.text, "at position:", msg.position); const textId = await createText( msg.text, msg.position, msg.fontSize, msg.color, msg.fontFamily || DEFAULT_FONT, msg.resizeMode || 'AUTO_WIDTH' ); figma.ui.postMessage({ type: 'operation-completed', status: 'success', originalOperation: 'create-text', data: { nodeId: textId, text: msg.text } }); } catch (error) { console.error("Error creating text:", error); figma.ui.postMessage({ type: 'operation-error', originalOperation: 'create-text', error: error instanceof Error ? error.message : String(error) }); } break; } case 'create-page': await createPage(msg.pageName, msg.description, msg.styleGuide); break; // New operations case 'select-node': await selectNode(msg.nodeId); break; case 'change-color': await changeColor(msg.color, msg.nodeId); break; case 'change-radius': await changeRadius(msg.radius, msg.nodeId); break; case 'change-typeface': await changeTypeface(msg.fontFamily, msg.nodeId); break; case 'change-font-style': await changeFontStyle(msg.fontSize, msg.fontWeight, msg.italic, msg.nodeId); break; case 'change-alignment': await changeAlignment(msg.horizontal, msg.vertical, msg.nodeId); break; case 'change-spacing': await changeSpacing(msg.padding, msg.itemSpacing, msg.nodeId); break; case 'list-fonts': await listAvailableFonts(); break; case 'list-nodes': await listAvailableNodes(msg.includeDetails); break; case 'change-text-resize': await changeTextResize(msg.resizeMode, msg.width, msg.height, msg.nodeId); break; case 'delete-node': await deleteNode(msg.nodeId); break; case 'move-node': await moveNode(msg.position, msg.nodeId); break; case 'create-icon': { try { console.log("Creating icon:", msg.iconName, "at position:", msg.position); const iconId = await createIcon( msg.iconName, msg.svgData, msg.position, msg.size || 24, msg.color, msg.strokeWidth || 2 ); figma.ui.postMessage({ type: 'operation-completed', status: 'success', originalOperation: 'create-icon', data: { nodeId: iconId, iconName: msg.iconName } }); // We return here to prevent the generic operation-completed message below return; } catch (error) { console.error("Error creating icon:", error); figma.ui.postMessage({ type: 'operation-error', originalOperation: 'create-icon', error: error instanceof Error ? error.message : String(error) }); // We return here to prevent the generic operation-completed message below return; } } case 'create-border-box': { try { console.log("Creating border box:", msg.options?.type, "at position:", msg.position); const boxId = await createBorderBox( msg.position, msg.size, msg.options ); figma.ui.postMessage({ type: 'operation-completed', status: 'success', originalOperation: 'create-border-box', data: { nodeId: boxId, type: msg.options?.type } }); // We return here to prevent the generic operation-completed message below return; } catch (error) { console.error("Error creating border box:", error); figma.ui.postMessage({ type: 'operation-error', originalOperation: 'create-border-box', error: error instanceof Error ? error.message : String(error) }); // We return here to prevent the generic operation-completed message below return; } } case 'connection-status': // Log connection status changes console.log(`WebSocket connection status: ${msg.status}`); break; case 'draw-line': { try { console.log("Drawing line from:", msg.start, "to:", msg.end); const lineId = await drawLine( msg.start, msg.end, msg.color, msg.thickness || 1 ); figma.ui.postMessage({ type: 'operation-completed', status: 'success', originalOperation: 'draw-line', data: { nodeId: lineId } }); // Return to prevent generic message return; } catch (error) { console.error("Error drawing line:", error); figma.ui.postMessage({ type: 'operation-error', originalOperation: 'draw-line', error: error instanceof Error ? error.message : String(error) }); // Return to prevent generic message return; } } default: console.error(`Unknown operation type: ${msg.type}`); } // Notify UI that operation is complete figma.ui.postMessage({ type: 'operation-completed', originalOperation: msg.type, status: 'success' }); } catch (error: unknown) { console.error('Error executing operation:', error); figma.ui.postMessage({ type: 'operation-error', originalOperation: msg.type, error: error instanceof Error ? error.message : 'Unknown error' }); } }; // Function to list available fonts in Figma async function listAvailableFonts(): Promise<void> { try { // Get all available fonts (this might be slow) const fontList = await figma.listAvailableFontsAsync(); // Extract unique font family names const fontFamilies = new Set<string>(); for (const font of fontList) { fontFamilies.add(font.fontName.family); } // Convert to array and sort alphabetically const uniqueFamilies = Array.from(fontFamilies).sort(); // Send the list back to the UI figma.ui.postMessage({ type: 'fonts-list', fonts: uniqueFamilies }); return; } catch (error) { console.error('Error listing fonts:', error); throw new Error('Failed to list available fonts'); } } // Function to create a rectangle async function createRectangle(position: {x: number, y: number}, size: {width: number, height: number}, color?: {r: number, g: number, b: number}) { const rect = figma.createRectangle(); // Set position rect.x = position.x; rect.y = position.y; // Set size rect.resize(size.width, size.height); // Set color if (color) { const rgbColor = { r: color.r, g: color.g, b: color.b }; const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; rect.fills = [solidPaint]; } // Select the created rectangle figma.currentPage.selection = [rect]; figma.viewport.scrollAndZoomIntoView([rect]); return rect.id; } // Function to create text async function createText( text: string, position: {x: number, y: number}, fontSize: number = 24, color?: {r: number, g: number, b: number}, fontFamily: string = DEFAULT_FONT, resizeMode: 'AUTO_WIDTH' | 'AUTO_HEIGHT' | 'FIXED_SIZE' = 'AUTO_WIDTH' ): Promise<string> { // Create a text node const textNode = figma.createText(); // Set position textNode.x = position.x; textNode.y = position.y; try { // Load a font to use console.log(`Loading font: ${fontFamily} Regular`); try { await figma.loadFontAsync({ family: fontFamily, style: "Regular" }); } catch (fontError) { console.warn(`Failed to load ${fontFamily} Regular, falling back to Inter:`, fontError); // If specified font fails, try using Inter as fallback fontFamily = DEFAULT_FONT; await figma.loadFontAsync({ family: DEFAULT_FONT, style: "Regular" }); } // Set the text content and font size textNode.fontName = { family: fontFamily, style: "Regular" }; textNode.characters = text; textNode.fontSize = fontSize; // Set resize behavior switch (resizeMode) { case 'AUTO_WIDTH': textNode.textAutoResize = 'WIDTH_AND_HEIGHT'; break; case 'AUTO_HEIGHT': textNode.textAutoResize = 'HEIGHT'; textNode.resize(300, textNode.height); // Set a default width break; case 'FIXED_SIZE': textNode.textAutoResize = 'NONE'; textNode.resize(300, 100); // Set default dimensions break; } // Set color if provided if (color) { const rgbColor = { r: color.r, g: color.g, b: color.b }; const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; textNode.fills = [solidPaint]; } // Select the created text figma.currentPage.selection = [textNode]; figma.viewport.scrollAndZoomIntoView([textNode]); return textNode.id; } catch (error) { // Clean up the node if there was an error textNode.remove(); console.error('Error creating text:', error); throw new Error(`Failed to create text: ${error instanceof Error ? error.message : String(error)}`); } } // Function to create a page based on description async function createPage(pageName: string, description: string, styleGuide: any = {}): Promise<any> { // Use font from style guide if provided, otherwise use default const fontFamily = styleGuide?.typography?.fontFamily || DEFAULT_FONT; // Create a new page const page = figma.createPage(); page.name = pageName; // Set as current page figma.currentPage = page; // Create a main frame for the page content const frame = figma.createFrame(); frame.name = "Main Content"; frame.resize(1440, 900); // Default size, adjust as needed // Parse the description to determine what to create // This is a simplified implementation - a more sophisticated one would use // natural language processing or predefined templates // For demonstration, let's create a simple layout based on keywords in the description if (description.toLowerCase().includes('header')) { await createHeader(frame, styleGuide); } if (description.toLowerCase().includes('hero')) { await createHeroSection(frame, description, styleGuide); } if (description.toLowerCase().includes('features') || description.toLowerCase().includes('benefits')) { await createFeaturesSection(frame, description, styleGuide); } if (description.toLowerCase().includes('footer')) { await createFooter(frame, styleGuide); } // Select the frame to show it figma.currentPage.selection = [frame]; figma.viewport.scrollAndZoomIntoView([frame]); return { pageId: page.id, frameId: frame.id }; } // Helper function to create a header section async function createHeader(parent: FrameNode, styleGuide: any) { const fontFamily = styleGuide?.typography?.fontFamily || DEFAULT_FONT; const header = figma.createFrame(); header.name = "Header"; header.resize(parent.width, 80); header.x = 0; header.y = 0; // Add a logo await figma.loadFontAsync({ family: fontFamily, style: "Bold" }); const logo = figma.createText(); logo.characters = "LOGO"; logo.fontSize = 24; logo.x = 40; logo.y = 28; // Add navigation items const navItems = ["Home", "Features", "Pricing", "Contact"]; let xOffset = parent.width - 400; for (const item of navItems) { await figma.loadFontAsync({ family: fontFamily, style: "Regular" }); const navItem = figma.createText(); navItem.characters = item; navItem.fontSize = 16; navItem.x = xOffset; navItem.y = 32; xOffset += 100; header.appendChild(navItem); } header.appendChild(logo); parent.appendChild(header); return header.id; } // Helper function to create a hero section async function createHeroSection(parent: FrameNode, description: string, styleGuide: any) { const fontFamily = styleGuide?.typography?.fontFamily || DEFAULT_FONT; const hero = figma.createFrame(); hero.name = "Hero Section"; hero.resize(parent.width, 500); hero.x = 0; hero.y = 90; // Below header // Add a heading await figma.loadFontAsync({ family: fontFamily, style: "Bold" }); const heading = figma.createText(); // Extract heading from description or use default const headingMatch = description.match(/heading ['"]([^'"]+)['"]/i); heading.characters = headingMatch ? headingMatch[1] : "Welcome to Our Platform"; heading.fontSize = 48; heading.x = parent.width / 2 - 400; heading.y = 100; heading.resize(800, heading.height); heading.textAlignHorizontal = "CENTER"; // Add a subheading await figma.loadFontAsync({ family: fontFamily, style: "Regular" }); const subheading = figma.createText(); // Extract subheading from description or use default const subheadingMatch = description.match(/subheading ['"]([^'"]+)['"]/i); subheading.characters = subheadingMatch ? subheadingMatch[1] : "The best solution for your design and productivity needs"; subheading.fontSize = 24; subheading.x = parent.width / 2 - 400; subheading.y = 170; subheading.resize(800, subheading.height); subheading.textAlignHorizontal = "CENTER"; // Add a CTA button const ctaButton = figma.createRectangle(); ctaButton.resize(200, 50); ctaButton.x = parent.width / 2 - 100; ctaButton.y = 250; ctaButton.cornerRadius = 8; // Set button color from style guide or default const buttonColor = styleGuide?.colors?.primary ? styleGuide.colors.primary : { r: 0.2, g: 0.4, b: 0.9 }; const buttonSolidPaint: SolidPaint = { type: 'SOLID', color: buttonColor }; ctaButton.fills = [buttonSolidPaint]; // Add button text await figma.loadFontAsync({ family: fontFamily, style: "SemiBold" }); const buttonText = figma.createText(); buttonText.characters = "Get Started"; buttonText.fontSize = 16; const textSolidPaint: SolidPaint = { type: 'SOLID', color: { r: 1, g: 1, b: 1 } }; buttonText.fills = [textSolidPaint]; buttonText.x = parent.width / 2 - 50; buttonText.y = 265; // Add elements to hero hero.appendChild(heading); hero.appendChild(subheading); hero.appendChild(ctaButton); hero.appendChild(buttonText); // Add hero to parent parent.appendChild(hero); return hero.id; } // Helper function to create a features section async function createFeaturesSection(parent: FrameNode, description: string, styleGuide: any) { const fontFamily = styleGuide?.typography?.fontFamily || DEFAULT_FONT; const features = figma.createFrame(); features.name = "Features Section"; features.resize(parent.width, 600); features.x = 0; features.y = 600; // Below hero // Section title await figma.loadFontAsync({ family: fontFamily, style: "Bold" }); const title = figma.createText(); title.characters = "Key Features"; title.fontSize = 36; title.x = parent.width / 2 - 150; title.y = 40; title.resize(300, title.height); title.textAlignHorizontal = "CENTER"; features.appendChild(title); // Create feature cards const numberOfFeatures = description.toLowerCase().includes("3 features") ? 3 : description.toLowerCase().includes("4 features") ? 4 : 3; const cardWidth = 320; const spacing = 40; const totalWidth = numberOfFeatures * cardWidth + (numberOfFeatures - 1) * spacing; let xOffset = (parent.width - totalWidth) / 2; for (let i = 0; i < numberOfFeatures; i++) { // Create a card const card = figma.createFrame(); card.name = `Feature ${i + 1}`; card.resize(cardWidth, 400); card.x = xOffset; card.y = 120; card.cornerRadius = 8; const cardSolidPaint: SolidPaint = { type: 'SOLID', color: { r: 0.98, g: 0.98, b: 0.98 } }; card.fills = [cardSolidPaint]; // Create icon placeholder const icon = figma.createEllipse(); icon.resize(80, 80); icon.x = cardWidth / 2 - 40; icon.y = 40; const iconSolidPaint: SolidPaint = { type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }; icon.fills = [iconSolidPaint]; // Create feature title await figma.loadFontAsync({ family: fontFamily, style: "SemiBold" }); const featureTitle = figma.createText(); featureTitle.characters = `Feature ${i + 1}`; featureTitle.fontSize = 24; featureTitle.x = 20; featureTitle.y = 150; featureTitle.resize(cardWidth - 40, featureTitle.height); featureTitle.textAlignHorizontal = "CENTER"; // Create feature description await figma.loadFontAsync({ family: fontFamily, style: "Regular" }); const featureDesc = figma.createText(); featureDesc.characters = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean gravida est nec finibus dictum."; featureDesc.fontSize = 16; featureDesc.x = 20; featureDesc.y = 200; featureDesc.resize(cardWidth - 40, featureDesc.height); featureDesc.textAlignHorizontal = "CENTER"; // Add elements to card card.appendChild(icon); card.appendChild(featureTitle); card.appendChild(featureDesc); // Add card to features section features.appendChild(card); // Update xOffset for next card xOffset += cardWidth + spacing; } // Add features to parent parent.appendChild(features); return features.id; } // Helper function to create a footer async function createFooter(parent: FrameNode, styleGuide: any) { const fontFamily = styleGuide?.typography?.fontFamily || DEFAULT_FONT; const footer = figma.createFrame(); footer.name = "Footer"; footer.resize(parent.width, 200); footer.x = 0; footer.y = 1200; // Below features const footerSolidPaint: SolidPaint = { type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1 } }; footer.fills = [footerSolidPaint]; // Add copyright text await figma.loadFontAsync({ family: fontFamily, style: "Regular" }); const copyright = figma.createText(); copyright.characters = "© 2023 Company Name. All rights reserved."; copyright.fontSize = 14; const copyrightSolidPaint: SolidPaint = { type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }; copyright.fills = [copyrightSolidPaint]; copyright.x = parent.width / 2 - 150; copyright.y = 150; // Add footer links const links = ["Privacy Policy", "Terms of Service", "Contact Us"]; let xOffset = parent.width / 2 - 200; for (const linkText of links) { const link = figma.createText(); link.characters = linkText; link.fontSize = 14; const linkSolidPaint: SolidPaint = { type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }; link.fills = [linkSolidPaint]; link.x = xOffset; link.y = 100; footer.appendChild(link); xOffset += 150; } footer.appendChild(copyright); parent.appendChild(footer); return footer.id; } // Helper functions for node operations function getTargetNodes(nodeId?: string): readonly SceneNode[] { if (nodeId) { const node = figma.getNodeById(nodeId); if (!node) { throw new Error(`Node with ID ${nodeId} not found`); } return [node as SceneNode]; } else { // Use current selection if no nodeId is specified return figma.currentPage.selection; } } // Function to select a node by ID async function selectNode(nodeId: string): Promise<void> { // First check if the node exists const node = figma.getNodeById(nodeId); if (!node) { // Try to get more context about why the node wasn't found const currentPage = figma.currentPage; const allNodes = currentPage.findAll(); const nodeIds = allNodes.map(n => n.id); throw new Error( `Node with ID "${nodeId}" not found. ` + `Current page has ${allNodes.length} nodes. ` + `Available node IDs: ${nodeIds.join(', ')}` ); } // Check if the node is a valid SceneNode if (!('type' in node)) { throw new Error(`Node "${nodeId}" is not a valid Figma node type`); } // Select the node figma.currentPage.selection = [node as SceneNode]; // Scroll and zoom to show the node figma.viewport.scrollAndZoomIntoView([node as SceneNode]); // Log success for debugging console.log(`Successfully selected node: ${nodeId} (Type: ${node.type})`); } // Function to change the color of selected nodes async function changeColor(color: {r: number, g: number, b: number}, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for color change'); } const rgbColor = { r: color.r, g: color.g, b: color.b }; const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; for (const node of targetNodes) { if ('fills' in node) { // We need to create a new array because the fills property is readonly const newFills = [...(node.fills as readonly Paint[])]; // Replace first fill or add one if none exists if (newFills.length > 0) { newFills[0] = solidPaint; } else { newFills.push(solidPaint); } node.fills = newFills; } } } // Function to change the corner radius of selected nodes async function changeRadius(radius: number, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for radius change'); } for (const node of targetNodes) { if ('cornerRadius' in node) { // For Rectangle, Ellipse, etc. that support setRangeCornerRadius if ('setRangeCornerRadius' in node && typeof node.setRangeCornerRadius === 'function') { node.setRangeCornerRadius(0, 3, radius); } // For other nodes, use corner specific properties if available else if ('topLeftRadius' in node) { node.topLeftRadius = radius; node.topRightRadius = radius; node.bottomLeftRadius = radius; node.bottomRightRadius = radius; } } } } // Function to change the typeface of selected text nodes async function changeTypeface(fontFamily: string, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for typeface change'); } // Try to load the font try { // Load the font with Regular style first await figma.loadFontAsync({ family: fontFamily, style: "Regular" }); for (const node of targetNodes) { if (node.type === 'TEXT') { // Handle text nodes const textNode = node as TextNode; const currentFontName = textNode.fontName as FontName; // Create a new FontName object const newFont = { family: fontFamily, style: currentFontName.style }; try { // Try to load the font with the current style await figma.loadFontAsync(newFont); textNode.fontName = newFont; } catch (e) { console.warn(`Could not load ${fontFamily} with style ${currentFontName.style}, falling back to Regular style`); // If specific style isn't available, try with Regular style textNode.fontName = { family: fontFamily, style: "Regular" }; } } } } catch (error) { console.error(`Failed to load font family "${fontFamily}":`, error); throw new Error(`Failed to load font family "${fontFamily}". Make sure it exists in Figma.`); } } // Function to change font style (size, weight, italic) async function changeFontStyle(fontSize?: number, fontWeight?: string, italic?: boolean, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for font style change'); } for (const node of targetNodes) { if (node.type === 'TEXT') { const textNode = node as TextNode; // Change font size if specified if (fontSize !== undefined) { textNode.fontSize = fontSize; } // Change font weight if specified if (fontWeight !== undefined) { const currentFontName = textNode.fontName as FontName; let newStyle = fontWeight; // Add Italic if needed if (italic === true && !newStyle.includes('Italic')) { newStyle += ' Italic'; } else if (italic === false && newStyle.includes('Italic')) { // Remove Italic if present newStyle = newStyle.replace('Italic', '').trim(); } try { // Try to load the font with the new style await figma.loadFontAsync({ family: currentFontName.family, style: newStyle }); textNode.fontName = { family: currentFontName.family, style: newStyle }; } catch (e) { console.warn(`Could not load font with style ${newStyle}, skipping weight change`); } } // Handle just italic change if fontWeight isn't specified else if (italic !== undefined) { const currentFontName = textNode.fontName as FontName; let newStyle = currentFontName.style; if (italic && !newStyle.includes('Italic')) { newStyle += ' Italic'; } else if (!italic && newStyle.includes('Italic')) { newStyle = newStyle.replace('Italic', '').trim(); } try { // Try to load the font with the new style await figma.loadFontAsync({ family: currentFontName.family, style: newStyle }); textNode.fontName = { family: currentFontName.family, style: newStyle }; } catch (e) { console.warn(`Could not load font with style ${newStyle}, skipping italic change`); } } } } } // Function to change text alignment async function changeAlignment(horizontal?: string, vertical?: string, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for alignment change'); } for (const node of targetNodes) { if (node.type === 'TEXT') { // Change horizontal alignment if specified if (horizontal) { switch (horizontal.toLowerCase()) { case 'left': node.textAlignHorizontal = 'LEFT'; break; case 'center': node.textAlignHorizontal = 'CENTER'; break; case 'right': node.textAlignHorizontal = 'RIGHT'; break; case 'justified': node.textAlignHorizontal = 'JUSTIFIED'; break; default: console.warn(`Unknown horizontal alignment: ${horizontal}`); } } // Change vertical alignment if specified if (vertical) { switch (vertical.toLowerCase()) { case 'top': node.textAlignVertical = 'TOP'; break; case 'center': node.textAlignVertical = 'CENTER'; break; case 'bottom': node.textAlignVertical = 'BOTTOM'; break; default: console.warn(`Unknown vertical alignment: ${vertical}`); } } } } } // Function to change spacing (padding and item spacing) for auto layout frames async function changeSpacing(padding?: number | {top?: number, right?: number, bottom?: number, left?: number}, itemSpacing?: number, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for spacing change'); } for (const node of targetNodes) { if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') { // Change item spacing if specified and the node has auto layout if (itemSpacing !== undefined && node.layoutMode !== 'NONE') { node.itemSpacing = itemSpacing; } // Change padding if specified if (padding !== undefined) { if (typeof padding === 'number') { // Apply uniform padding node.paddingTop = padding; node.paddingRight = padding; node.paddingBottom = padding; node.paddingLeft = padding; } else { // Apply individual padding values if (padding.top !== undefined) node.paddingTop = padding.top; if (padding.right !== undefined) node.paddingRight = padding.right; if (padding.bottom !== undefined) node.paddingBottom = padding.bottom; if (padding.left !== undefined) node.paddingLeft = padding.left; } } } } } // Function to list all available nodes in the current page async function listAvailableNodes(includeDetails: boolean = false): Promise<void> { try { const currentPage = figma.currentPage; const allNodes = currentPage.findAll(); // Create a list of node information const nodesList = allNodes.map(node => { if (includeDetails) { return { id: node.id, name: node.name, type: node.type, visible: 'visible' in node ? node.visible : true, parent: node.parent ? { id: node.parent.id, type: node.parent.type, name: node.parent.name } : null }; } else { return { id: node.id, type: node.type }; } }); // Send the node list back to the UI figma.ui.postMessage({ type: 'nodes-list', nodes: nodesList, count: nodesList.length, currentPageId: currentPage.id, currentPageName: currentPage.name }); } catch (error) { console.error('Error listing nodes:', error); throw new Error('Failed to list available nodes'); } } // Function to change text resize mode async function changeTextResize(resizeMode: 'AUTO_WIDTH' | 'AUTO_HEIGHT' | 'FIXED_SIZE', width?: number, height?: number, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for text resize change'); } for (const node of targetNodes) { if (node.type === 'TEXT') { const textNode = node as TextNode; // First, we need to load the font that the text is currently using try { const currentFont = textNode.fontName as FontName; await figma.loadFontAsync(currentFont); console.log(`Font loaded successfully: ${currentFont.family} ${currentFont.style}`); // Now we can safely change the resize behavior switch (resizeMode) { case 'AUTO_WIDTH': textNode.textAutoResize = 'WIDTH_AND_HEIGHT'; break; case 'AUTO_HEIGHT': textNode.textAutoResize = 'HEIGHT'; // Set width if provided, otherwise keep current width if (width !== undefined) { textNode.resize(width, textNode.height); } break; case 'FIXED_SIZE': textNode.textAutoResize = 'NONE'; // Set dimensions if provided, otherwise keep current dimensions if (width !== undefined && height !== undefined) { textNode.resize(width, height); } else if (width !== undefined) { textNode.resize(width, textNode.height); } else if (height !== undefined) { textNode.resize(textNode.width, height); } break; } } catch (error) { console.error('Error loading font:', error); throw new Error(`Failed to change text resize mode: ${error instanceof Error ? error.message : String(error)}`); } } } } // Function to delete nodes by ID or selection async function deleteNode(nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for deletion'); } // Store the IDs of nodes that will be deleted const deletedIds = targetNodes.map(node => node.id); // Delete each node for (const node of targetNodes) { node.remove(); } console.log(`Successfully deleted ${targetNodes.length} node(s): ${deletedIds.join(', ')}`); // First send detailed information about deleted nodes figma.ui.postMessage({ type: 'nodes-deleted', count: targetNodes.length, nodeIds: deletedIds }); // Then send the operation-completed message that the server is specifically listening for figma.ui.postMessage({ type: 'operation-completed', originalOperation: 'delete-node', status: 'success', data: { count: targetNodes.length, nodeIds: deletedIds } }); } // Function to move nodes to a specific position async function moveNode(position: {x: number, y: number}, nodeId?: string): Promise<void> { const targetNodes = getTargetNodes(nodeId); if (targetNodes.length === 0) { throw new Error('No nodes selected for moving'); } // Store original positions for reporting const originalPositions = targetNodes.map(node => ({ id: node.id, name: node.name, oldX: node.x, oldY: node.y })); // Move each node to the new position // If multiple nodes are selected, offset them to prevent overlapping let offsetX = 0; let offsetY = 0; const offsetStep = 20; // Pixels to offset each node by for (const node of targetNodes) { // Set the new position node.x = position.x + offsetX; node.y = position.y + offsetY; // Increment offset for next node if multiple nodes are selected if (targetNodes.length > 1) { offsetX += offsetStep; offsetY += offsetStep; } } // If only one node is moved, center the view on it if (targetNodes.length === 1) { figma.viewport.scrollAndZoomIntoView(targetNodes); } console.log(`Successfully moved ${targetNodes.length} node(s) to (${position.x}, ${position.y})`); // Send information about moved nodes figma.ui.postMessage({ type: 'nodes-moved', count: targetNodes.length, nodes: targetNodes.map(node => ({ id: node.id, name: node.name, newX: node.x, newY: node.y })), originalPositions }); } // Function to create an icon from Lucide Icons async function createIcon( iconName: string, svgData: string, position: {x: number, y: number}, size: number = 24, color?: {r: number, g: number, b: number}, strokeWidth: number = 2 ): Promise<string> { try { console.log(`Creating icon: ${iconName}`); if (!svgData) { throw new Error(`Missing SVG data for icon: ${iconName}`); } // Create a node from the SVG string const svgNode = figma.createNodeFromSvg(svgData); // Create a frame to group the SVG elements const frame = figma.createFrame(); frame.name = `Lucide Icon: ${iconName}`; frame.x = position.x; frame.y = position.y; frame.fills = []; // Make the frame background transparent // Calculate sizing for the outer frame const aspectRatio = svgNode.width / svgNode.height; let frameWidth, frameHeight; if (aspectRatio >= 1) { frameWidth = size; frameHeight = size / aspectRatio; } else { frameWidth = size * aspectRatio; frameHeight = size; } // Resize the frame frame.resize(frameWidth, frameHeight); // Move the SVG elements into the frame const svgElements = [...svgNode.children]; // Make a copy of the children // Process all SVG elements before adding to the frame for (const element of svgElements) { // Detach from original SVG node and add to our frame frame.appendChild(element); // Apply color if provided if (color && 'strokes' in element) { const rgbColor = { r: color.r, g: color.g, b: color.b }; const strokePaint: SolidPaint = { type: 'SOLID', color: rgbColor }; element.strokes = [strokePaint]; // Set stroke width if the element has strokes if ('strokeWeight' in element) { element.strokeWeight = strokeWidth; } } } // Clean up the original SVG node since we've moved all its children svgNode.remove(); // Group the elements within the frame if (svgElements.length > 0) { const group = figma.group(svgElements, frame); group.name = "IconContent"; // Name the group for clarity // Calculate target size (90% of frame) const targetWidth = frame.width * 0.9; const targetHeight = frame.height * 0.9; // Calculate scale factor to fit within target size, maintaining aspect ratio const groupWidth = group.width; const groupHeight = group.height; if (groupWidth > 0 && groupHeight > 0) { const scaleX = targetWidth / groupWidth; const scaleY = targetHeight / groupHeight; const scale = Math.min(scaleX, scaleY); // Resize the group group.resize(groupWidth * scale, groupHeight * scale); // Center the resized group within the frame group.x = (frame.width - group.width) / 2; group.y = (frame.height - group.height) / 2; } else { console.warn("Icon content group has zero width or height, skipping resize/centering."); // Fallback centering for empty/invalid groups group.x = (frame.width - group.width) / 2; group.y = (frame.height - group.height) / 2; } } // Select the created frame with the icon figma.currentPage.selection = [frame]; figma.viewport.scrollAndZoomIntoView([frame]); return frame.id; } catch (error) { console.error('Error creating icon:', error); throw new Error(`Failed to create icon: ${error instanceof Error ? error.message : String(error)}`); } } // Function to create a border line box or button async function createBorderBox( position: {x: number, y: number}, size: {width: number, height: number}, options: { type: 'box' | 'button', borderColor?: {r: number, g: number, b: number}, borderWidth?: number, fillColor?: {r: number, g: number, b: number}, cornerRadius?: number, text?: string, textColor?: {r: number, g: number, b: number}, fontSize?: number, fontFamily?: string } ): Promise<string> { try { console.log(`Creating ${options.type} at position:`, position); // Create a frame const frame = figma.createFrame(); frame.name = options.type === 'button' ? 'Button' : 'Border Box'; frame.x = position.x; frame.y = position.y; frame.resize(size.width, size.height); // Set corner radius if provided if (options.cornerRadius !== undefined) { frame.cornerRadius = options.cornerRadius; } // Set fill color if provided if (options.fillColor) { const rgbColor = { r: options.fillColor.r, g: options.fillColor.g, b: options.fillColor.b }; const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; frame.fills = [solidPaint]; } else { // Make the frame background transparent frame.fills = []; } // Set border color and width if provided if (options.borderColor) { const rgbColor = { r: options.borderColor.r, g: options.borderColor.g, b: options.borderColor.b }; const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; frame.strokes = [solidPaint]; frame.strokeWeight = options.borderWidth || 1; } // Add text if provided if (options.text) { const textNode = figma.createText(); // Load font let actualFontFamily = options.fontFamily || DEFAULT_FONT; try { await figma.loadFontAsync({ family: actualFontFamily, style: "Regular" }); } catch (fontError) { console.warn(`Failed to load ${actualFontFamily} Regular, falling back to Inter:`, fontError); actualFontFamily = DEFAULT_FONT; await figma.loadFontAsync({ family: DEFAULT_FONT, style: "Regular" }); } // Set text properties textNode.fontName = { family: actualFontFamily, style: "Regular" }; textNode.characters = options.text; textNode.fontSize = options.fontSize || 16; // Set text color if provided if (options.textColor) { const rgbColor = { r: options.textColor.r, g: options.textColor.g, b: options.textColor.b }; const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; textNode.fills = [solidPaint]; } // Center the text in the frame textNode.textAlignHorizontal = 'CENTER'; textNode.textAlignVertical = 'CENTER'; textNode.resize(frame.width, frame.height); // Make text node same size as frame for centering textNode.x = 0; // Position relative to frame textNode.y = 0; // Position relative to frame frame.appendChild(textNode); // Reset frame layout mode if we added text // frame.layoutMode = 'VERTICAL'; // frame.primaryAxisAlignItems = 'CENTER'; // frame.counterAxisAlignItems = 'CENTER'; } // Select the created frame figma.currentPage.selection = [frame]; figma.viewport.scrollAndZoomIntoView([frame]); return frame.id; } catch (error) { console.error('Error creating border box:', error); throw new Error(`Failed to create border box: ${error instanceof Error ? error.message : String(error)}`); } } // Function to draw a line async function drawLine( start: {x: number, y: number}, end: {x: number, y: number}, color?: {r: number, g: number, b: number}, thickness: number = 1 ): Promise<string> { try { console.log(`Drawing line from (${start.x}, ${start.y}) to (${end.x}, ${end.y})`); const line = figma.createLine(); // Set line coordinates line.x = start.x; line.y = start.y; // Adjust the endpoint relative to the start point for the line's internal coordinate system line.resize(end.x - start.x, end.y - start.y); // Set line color const rgbColor = color ? { r: color.r, g: color.g, b: color.b } : { r: 0, g: 0, b: 0 }; // Default black const solidPaint: SolidPaint = { type: 'SOLID', color: rgbColor }; line.strokes = [solidPaint]; line.strokeWeight = thickness; // Select the created line figma.currentPage.selection = [line]; figma.viewport.scrollAndZoomIntoView([line]); return line.id; } catch (error) { console.error('Error drawing line:', error); throw new Error(`Failed to draw line: ${error instanceof Error ? error.message : String(error)}`); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/xxflux/figma_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server