Figma MCP Server

by GLips
Verified
MIT License
10,300
3,672
  • Linux
  • Apple
import { SimplifiedLayout, buildSimplifiedLayout } from "~/transformers/layout.js"; import type { GetFileNodesResponse, Node as FigmaDocumentNode, Paint, Vector, GetFileResponse, } from "@figma/rest-api-spec"; import { hasValue, isRectangleCornerRadii, isTruthy } from "~/utils/identity.js"; import { removeEmptyKeys, generateVarId, StyleId, parsePaint, isVisible } from "~/utils/common.js"; import { buildSimplifiedStrokes, SimplifiedStroke } from "~/transformers/style.js"; import { buildSimplifiedEffects, SimplifiedEffects } from "~/transformers/effects.js"; /** * TODO ITEMS * * - Improve layout handling—translate from Figma vocabulary to CSS * - Pull image fills/vectors out to top level for better AI visibility * ? Implement vector parents again for proper downloads * ? Look up existing styles in new MCP endpoint—Figma supports individual lookups without enterprise /v1/styles/:key * ? Parse out and save .cursor/rules/design-tokens file on command **/ // -------------------- SIMPLIFIED STRUCTURES -------------------- export type TextStyle = Partial<{ fontFamily: string; fontWeight: number; fontSize: number; lineHeight: string; letterSpacing: string; textCase: string; textAlignHorizontal: string; textAlignVertical: string; }>; export type StrokeWeights = { top: number; right: number; bottom: number; left: number; }; type StyleTypes = | TextStyle | SimplifiedFill[] | SimplifiedLayout | SimplifiedStroke | SimplifiedEffects | string; type GlobalVars = { styles: Record<StyleId, StyleTypes>; }; export interface SimplifiedDesign { name: string; lastModified: string; thumbnailUrl: string; nodes: SimplifiedNode[]; globalVars: GlobalVars; } export interface SimplifiedNode { id: string; name: string; type: string; // e.g. FRAME, TEXT, INSTANCE, RECTANGLE, etc. // geometry boundingBox?: BoundingBox; // text text?: string; textStyle?: string; // appearance fills?: string; styles?: string; strokes?: string; effects?: string; opacity?: number; borderRadius?: string; // layout & alignment layout?: string; // backgroundColor?: ColorValue; // Deprecated by Figma API // for rect-specific strokes, etc. // children children?: SimplifiedNode[]; } export interface BoundingBox { x: number; y: number; width: number; height: number; } export type CSSRGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`; export type CSSHexColor = `#${string}`; export type SimplifiedFill = | { type?: Paint["type"]; hex?: string; rgba?: string; opacity?: number; imageRef?: string; scaleMode?: string; gradientHandlePositions?: Vector[]; gradientStops?: { position: number; color: ColorValue | string; }[]; } | CSSRGBAColor | CSSHexColor; export interface ColorValue { hex: string; opacity: number; } // ---------------------- PARSING ---------------------- export function parseFigmaResponse(data: GetFileResponse | GetFileNodesResponse): SimplifiedDesign { const { name, lastModified, thumbnailUrl } = data; let nodes: FigmaDocumentNode[]; if ("document" in data) { nodes = Object.values(data.document.children); } else { nodes = Object.values(data.nodes).map((n) => n.document); } let globalVars: GlobalVars = { styles: {}, }; const simplifiedNodes: SimplifiedNode[] = nodes .filter(isVisible) .map((n) => parseNode(globalVars, n)) .filter((child) => child !== null && child !== undefined); return { name, lastModified, thumbnailUrl: thumbnailUrl || "", nodes: simplifiedNodes, globalVars, }; } // Helper function to find node by ID const findNodeById = (id: string, nodes: SimplifiedNode[]): SimplifiedNode | undefined => { for (const node of nodes) { if (node?.id === id) { return node; } if (node?.children && node.children.length > 0) { const foundInChildren = findNodeById(id, node.children); if (foundInChildren) { return foundInChildren; } } } return undefined; }; /** * Find or create global variables * @param globalVars - Global variables object * @param value - Value to store * @param prefix - Variable ID prefix * @returns Variable ID */ function findOrCreateVar(globalVars: GlobalVars, value: any, prefix: string): StyleId { // Check if the same value already exists const [existingVarId] = Object.entries(globalVars.styles).find( ([_, existingValue]) => JSON.stringify(existingValue) === JSON.stringify(value), ) ?? []; if (existingVarId) { return existingVarId as StyleId; } // Create a new variable if it doesn't exist const varId = generateVarId(prefix); globalVars.styles[varId] = value; return varId; } function parseNode( globalVars: GlobalVars, n: FigmaDocumentNode, parent?: FigmaDocumentNode, ): SimplifiedNode | null { const { id, name, type } = n; const simplified: SimplifiedNode = { id, name, type, }; // text if (hasValue("style", n) && Object.keys(n.style).length) { const style = n.style; const textStyle = { fontFamily: style.fontFamily, fontWeight: style.fontWeight, fontSize: style.fontSize, lineHeight: style.lineHeightPx && style.fontSize ? `${style.lineHeightPx / style.fontSize}em` : undefined, letterSpacing: style.letterSpacing && style.letterSpacing !== 0 && style.fontSize ? `${(style.letterSpacing / style.fontSize) * 100}%` : undefined, textCase: style.textCase, textAlignHorizontal: style.textAlignHorizontal, textAlignVertical: style.textAlignVertical, }; simplified.textStyle = findOrCreateVar(globalVars, textStyle, "style"); } // fills & strokes if (hasValue("fills", n) && Array.isArray(n.fills) && n.fills.length) { // const fills = simplifyFills(n.fills.map(parsePaint)); const fills = n.fills.map(parsePaint); simplified.fills = findOrCreateVar(globalVars, fills, "fill"); } const strokes = buildSimplifiedStrokes(n); if (strokes.colors.length) { simplified.strokes = findOrCreateVar(globalVars, strokes, "stroke"); } const effects = buildSimplifiedEffects(n); if (Object.keys(effects).length) { simplified.effects = findOrCreateVar(globalVars, effects, "effect"); } // Process layout const layout = buildSimplifiedLayout(n, parent); if (Object.keys(layout).length > 1) { simplified.layout = findOrCreateVar(globalVars, layout, "layout"); } // Keep other simple properties directly if (hasValue("characters", n, isTruthy)) { simplified.text = n.characters; } // border/corner // opacity if (hasValue("opacity", n) && typeof n.opacity === "number" && n.opacity !== 1) { simplified.opacity = n.opacity; } if (hasValue("cornerRadius", n) && typeof n.cornerRadius === "number") { simplified.borderRadius = `${n.cornerRadius}px`; } if (hasValue("rectangleCornerRadii", n, isRectangleCornerRadii)) { simplified.borderRadius = `${n.rectangleCornerRadii[0]}px ${n.rectangleCornerRadii[1]}px ${n.rectangleCornerRadii[2]}px ${n.rectangleCornerRadii[3]}px`; } // Recursively process child nodes if (hasValue("children", n) && n.children.length > 0) { let children = n.children .filter(isVisible) .map((child) => parseNode(globalVars, child, n)) .filter((child) => child !== null && child !== undefined); if (children.length) { simplified.children = children; } } // Convert VECTOR to IMAGE if (type === "VECTOR") { simplified.type = "IMAGE-SVG"; } return removeEmptyKeys(simplified); }