This is page 2 of 2. Use http://codebase.md/sichang824/mcp-figma?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .envrc ├── .gitignore ├── .mcp.pid ├── bun.lockb ├── docs │ ├── 01-overview.md │ ├── 02-implementation-steps.md │ ├── 03-components-and-features.md │ ├── 04-usage-guide.md │ ├── 05-project-status.md │ ├── image.png │ ├── README.md │ └── widget-tools-guide.md ├── Makefile ├── manifest.json ├── package.json ├── prompt.md ├── README.md ├── README.zh.md ├── src │ ├── config │ │ └── env.ts │ ├── index.ts │ ├── plugin │ │ ├── code.js │ │ ├── code.ts │ │ ├── creators │ │ │ ├── componentCreators.ts │ │ │ ├── containerCreators.ts │ │ │ ├── elementCreator.ts │ │ │ ├── imageCreators.ts │ │ │ ├── shapeCreators.ts │ │ │ ├── sliceCreators.ts │ │ │ ├── specialCreators.ts │ │ │ └── textCreator.ts │ │ ├── manifest.json │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── ui.html │ │ └── utils │ │ ├── colorUtils.ts │ │ └── nodeUtils.ts │ ├── resources.ts │ ├── services │ │ ├── figma-api.ts │ │ ├── websocket.ts │ │ └── widget-api.ts │ ├── tools │ │ ├── canvas.ts │ │ ├── comment.ts │ │ ├── component.ts │ │ ├── file.ts │ │ ├── frame.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── node.ts │ │ ├── page.ts │ │ ├── search.ts │ │ ├── utils │ │ │ └── widget-utils.ts │ │ ├── version.ts │ │ ├── widget │ │ │ ├── analyze-widget-structure.ts │ │ │ ├── get-widget-sync-data.ts │ │ │ ├── get-widget.ts │ │ │ ├── get-widgets.ts │ │ │ ├── index.ts │ │ │ ├── README.md │ │ │ ├── search-widgets.ts │ │ │ └── widget-tools.ts │ │ └── zod-schemas.ts │ ├── utils │ │ ├── figma-utils.ts │ │ └── widget-utils.ts │ ├── utils.ts │ ├── widget │ │ └── utils │ │ └── widget-tools.ts │ └── widget-tools.ts ├── tsconfig.json └── tsconfig.widget.json ``` # Files -------------------------------------------------------------------------------- /src/plugin/creators/shapeCreators.ts: -------------------------------------------------------------------------------- ```typescript /** * Shape creation functions for Figma plugin */ import { createSolidPaint } from "../utils/colorUtils"; import { selectAndFocusNodes } from "../utils/nodeUtils"; /** * Create a rectangle from data * @param data Rectangle configuration data * @returns Created rectangle node */ export function createRectangleFromData(data: any): RectangleNode { const rect = figma.createRectangle(); // Size rect.resize(data.width || 100, data.height || 100); // Fill if (data.fills) { rect.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { // Handle hex color rect.fills = [createSolidPaint(data.fill)]; } else { // Handle fill object rect.fills = [data.fill]; } } // Stroke if (data.strokes) rect.strokes = data.strokes; if (data.strokeWeight !== undefined) rect.strokeWeight = data.strokeWeight; if (data.strokeAlign) rect.strokeAlign = data.strokeAlign; if (data.strokeCap) rect.strokeCap = data.strokeCap; if (data.strokeJoin) rect.strokeJoin = data.strokeJoin; if (data.dashPattern) rect.dashPattern = data.dashPattern; // Corner radius if (data.cornerRadius !== undefined) rect.cornerRadius = data.cornerRadius; if (data.topLeftRadius !== undefined) rect.topLeftRadius = data.topLeftRadius; if (data.topRightRadius !== undefined) rect.topRightRadius = data.topRightRadius; if (data.bottomLeftRadius !== undefined) rect.bottomLeftRadius = data.bottomLeftRadius; if (data.bottomRightRadius !== undefined) rect.bottomRightRadius = data.bottomRightRadius; return rect; } /** * Create a simple rectangle * Convenient function for basic rectangle creation * * @param x X coordinate * @param y Y coordinate * @param width Width of rectangle * @param height Height of rectangle * @param color Fill color as hex string * @returns Created rectangle node */ export function createRectangle( x: number, y: number, width: number, height: number, color: string ): RectangleNode { // Use the new data-driven function const rect = createRectangleFromData({ width, height, fill: color, }); // Set position rect.x = x; rect.y = y; // Select and focus selectAndFocusNodes(rect); return rect; } /** * Create an ellipse/circle from data * @param data Ellipse configuration data * @returns Created ellipse node */ export function createEllipseFromData(data: any): EllipseNode { const ellipse = figma.createEllipse(); // Size ellipse.resize(data.width || 100, data.height || 100); // Fill if (data.fills) { ellipse.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { // Handle hex color ellipse.fills = [createSolidPaint(data.fill)]; } else { // Handle fill object ellipse.fills = [data.fill]; } } // Arc data for partial ellipses (arcs/donuts) if (data.arcData) { ellipse.arcData = { startingAngle: data.arcData.startingAngle !== undefined ? data.arcData.startingAngle : 0, endingAngle: data.arcData.endingAngle !== undefined ? data.arcData.endingAngle : 360, innerRadius: data.arcData.innerRadius !== undefined ? data.arcData.innerRadius : 0 }; } // Stroke if (data.strokes) ellipse.strokes = data.strokes; if (data.strokeWeight !== undefined) ellipse.strokeWeight = data.strokeWeight; if (data.strokeAlign) ellipse.strokeAlign = data.strokeAlign; if (data.strokeCap) ellipse.strokeCap = data.strokeCap; if (data.strokeJoin) ellipse.strokeJoin = data.strokeJoin; if (data.dashPattern) ellipse.dashPattern = data.dashPattern; return ellipse; } /** * Create a simple circle or ellipse * @param x X coordinate * @param y Y coordinate * @param width Width of ellipse * @param height Height of ellipse * @param color Fill color as hex string * @returns Created ellipse node */ export function createCircle( x: number, y: number, width: number, height: number, color: string ): EllipseNode { // Use the data-driven function const ellipse = createEllipseFromData({ width, height, fill: color, }); // Set position ellipse.x = x; ellipse.y = y; // Select and focus selectAndFocusNodes(ellipse); return ellipse; } /** * Create a polygon from data * @param data Polygon configuration data * @returns Created polygon node */ export function createPolygonFromData(data: any): PolygonNode { const polygon = figma.createPolygon(); polygon.resize(data.width || 100, data.height || 100); // Set number of sides if (data.pointCount) polygon.pointCount = data.pointCount; // Fill if (data.fills) { polygon.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { polygon.fills = [createSolidPaint(data.fill)]; } else { polygon.fills = [data.fill]; } } else if (data.color) { // For consistency with other shape creation functions polygon.fills = [createSolidPaint(data.color)]; } // Stroke if (data.strokes) polygon.strokes = data.strokes; if (data.strokeWeight !== undefined) polygon.strokeWeight = data.strokeWeight; if (data.strokeAlign) polygon.strokeAlign = data.strokeAlign; if (data.strokeCap) polygon.strokeCap = data.strokeCap; if (data.strokeJoin) polygon.strokeJoin = data.strokeJoin; if (data.dashPattern) polygon.dashPattern = data.dashPattern; return polygon; } /** * Create a simple polygon * @param x X coordinate * @param y Y coordinate * @param width Width of polygon * @param height Height of polygon * @param sides Number of sides (≥ 3) * @param color Fill color as hex string * @returns Created polygon node */ export function createPolygon( x: number, y: number, width: number, height: number, sides: number = 3, color: string ): PolygonNode { // Use the data-driven function const polygon = createPolygonFromData({ width, height, pointCount: sides, fill: color }); // Set position polygon.x = x; polygon.y = y; // Select and focus selectAndFocusNodes(polygon); return polygon; } /** * Create a star from data * @param data Star configuration data * @returns Created star node */ export function createStarFromData(data: any): StarNode { const star = figma.createStar(); star.resize(data.width || 100, data.height || 100); // Star specific properties if (data.pointCount) star.pointCount = data.pointCount; if (data.innerRadius) star.innerRadius = data.innerRadius; // Fill if (data.fills) { star.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { star.fills = [createSolidPaint(data.fill)]; } else { star.fills = [data.fill]; } } return star; } /** * Create a line from data * @param data Line configuration data * @returns Created line node */ export function createLineFromData(data: any): LineNode { const line = figma.createLine(); // Set line length (width) line.resize(data.width || 100, 0); // Line height is always 0 // Set rotation if provided if (data.rotation !== undefined) line.rotation = data.rotation; // Stroke properties if (data.strokeWeight) line.strokeWeight = data.strokeWeight; if (data.strokeAlign) line.strokeAlign = data.strokeAlign; if (data.strokeCap) line.strokeCap = data.strokeCap; if (data.strokeJoin) line.strokeJoin = data.strokeJoin; if (data.dashPattern) line.dashPattern = data.dashPattern; // Stroke color if (data.strokes) { line.strokes = data.strokes; } else if (data.stroke) { if (typeof data.stroke === "string") { line.strokes = [createSolidPaint(data.stroke)]; } else { line.strokes = [data.stroke]; } } else if (data.color) { // For consistency with other shape creation functions line.strokes = [createSolidPaint(data.color)]; } return line; } /** * Create a simple line * @param x X coordinate * @param y Y coordinate * @param length Length of the line (width) * @param color Stroke color as hex string * @param rotation Rotation in degrees * @param strokeWeight Stroke thickness * @returns Created line node */ export function createLine( x: number, y: number, length: number, color: string, rotation: number = 0, strokeWeight: number = 1 ): LineNode { // Use the data-driven function const line = createLineFromData({ width: length, stroke: color, strokeWeight: strokeWeight, rotation: rotation }); // Set position line.x = x; line.y = y; // Select and focus selectAndFocusNodes(line); return line; } /** * Create a simple arc (partial ellipse) * @param x X coordinate * @param y Y coordinate * @param width Width of ellipse * @param height Height of ellipse * @param startAngle Starting angle in degrees * @param endAngle Ending angle in degrees * @param innerRadius Inner radius ratio (0-1) for donut shapes * @param color Fill color as hex string * @returns Created ellipse node as an arc */ export function createArc( x: number, y: number, width: number, height: number, startAngle: number, endAngle: number, innerRadius: number = 0, color: string ): EllipseNode { // Use the data-driven function const arc = createEllipseFromData({ width, height, fill: color, arcData: { startingAngle: startAngle, endingAngle: endAngle, innerRadius: innerRadius } }); // Set position arc.x = x; arc.y = y; // Select and focus selectAndFocusNodes(arc); return arc; } /** * Create a vector from data * @param data Vector configuration data * @returns Created vector node */ export function createVectorFromData(data: any): VectorNode { const vector = figma.createVector(); try { // Resize the vector vector.resize(data.width || 100, data.height || 100); // Set vector-specific properties if (data.vectorNetwork) { vector.vectorNetwork = data.vectorNetwork; } if (data.vectorPaths) { vector.vectorPaths = data.vectorPaths; } if (data.handleMirroring) { vector.handleMirroring = data.handleMirroring; } // Fill if (data.fills) { vector.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { vector.fills = [createSolidPaint(data.fill)]; } else { vector.fills = [data.fill]; } } else if (data.color) { // For consistency with other shape creation functions vector.fills = [createSolidPaint(data.color)]; } // Stroke if (data.strokes) vector.strokes = data.strokes; if (data.strokeWeight !== undefined) vector.strokeWeight = data.strokeWeight; if (data.strokeAlign) vector.strokeAlign = data.strokeAlign; if (data.strokeCap) vector.strokeCap = data.strokeCap; if (data.strokeJoin) vector.strokeJoin = data.strokeJoin; if (data.dashPattern) vector.dashPattern = data.dashPattern; if (data.strokeMiterLimit) vector.strokeMiterLimit = data.strokeMiterLimit; // Corner properties if (data.cornerRadius !== undefined) vector.cornerRadius = data.cornerRadius; if (data.cornerSmoothing !== undefined) vector.cornerSmoothing = data.cornerSmoothing; // Blend properties if (data.opacity !== undefined) vector.opacity = data.opacity; if (data.blendMode) vector.blendMode = data.blendMode; if (data.isMask !== undefined) vector.isMask = data.isMask; if (data.effects) vector.effects = data.effects; // Layout properties if (data.constraints) vector.constraints = data.constraints; if (data.layoutAlign) vector.layoutAlign = data.layoutAlign; if (data.layoutGrow !== undefined) vector.layoutGrow = data.layoutGrow; if (data.layoutPositioning) vector.layoutPositioning = data.layoutPositioning; if (data.rotation !== undefined) vector.rotation = data.rotation; if (data.layoutSizingHorizontal) vector.layoutSizingHorizontal = data.layoutSizingHorizontal; if (data.layoutSizingVertical) vector.layoutSizingVertical = data.layoutSizingVertical; console.log("Vector created successfully:", vector); } catch (error) { console.error("Error creating vector:", error); } return vector; } /** * Create a simple vector * @param x X coordinate * @param y Y coordinate * @param width Width of vector * @param height Height of vector * @param color Fill color as hex string * @returns Created vector node */ export function createVector( x: number, y: number, width: number, height: number, color: string ): VectorNode { // Use the data-driven function const vector = createVectorFromData({ width, height, fill: color }); // Set position vector.x = x; vector.y = y; // Select and focus selectAndFocusNodes(vector); return vector; } ``` -------------------------------------------------------------------------------- /src/plugin/ui.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html> <head> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; } .container { padding: 20px; } h2 { font-size: 16px; margin-bottom: 15px; } .control-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-size: 12px; } input { width: 100%; padding: 6px; margin-bottom: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; } .button-group { display: flex; gap: 8px; margin-top: 15px; } button { background-color: #18a0fb; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; flex: 1; font-size: 12px; } button:hover { background-color: #0d8ee3; } button.cancel { background-color: #f24822; } button.cancel:hover { background-color: #d83b17; } .connection-status { margin-top: 15px; padding: 8px; border-radius: 4px; font-size: 12px; text-align: center; } .status-connected { background-color: #ecfdf5; color: #047857; } .status-disconnected { background-color: #fef2f2; color: #b91c1c; } .status-connecting { background-color: #fef3c7; color: #92400e; } .mcp-section { margin-top: 20px; padding-top: 15px; border-top: 1px solid #e5e7eb; } .log-area { margin-top: 10px; height: 100px; overflow-y: auto; border: 1px solid #ccc; border-radius: 4px; padding: 8px; font-size: 11px; font-family: monospace; background-color: #f9fafb; } .log-item { margin-bottom: 4px; } .server-input { display: flex; gap: 8px; margin-bottom: 10px; } .server-input input { flex: 1; } .checkbox-group { display: flex; align-items: center; margin-bottom: 10px; } .checkbox-group input[type="checkbox"] { width: auto; margin-right: 8px; } .checkbox-group label { display: inline; font-size: 12px; } </style> </head> <body> <div class="container"> <h2>Figma MCP 画布操作工具</h2> <div class="control-group"> <label for="x">X 位置:</label> <input type="number" id="x" value="100" /> <label for="y">Y 位置:</label> <input type="number" id="y" value="100" /> </div> <div class="control-group"> <label for="width">宽度:</label> <input type="number" id="width" value="150" /> <label for="height">高度:</label> <input type="number" id="height" value="150" /> </div> <div class="control-group"> <label for="color">颜色:</label> <input type="color" id="color" value="#ff0000" /> </div> <div class="control-group"> <label for="text">文本:</label> <input type="text" id="text" value="Hello Figma!" /> <label for="fontSize">字体大小:</label> <input type="number" id="fontSize" value="24" /> </div> <div class="button-group"> <button id="create-rectangle">矩形</button> <button id="create-circle">圆形</button> <button id="create-text">文本</button> </div> <div class="mcp-section"> <h2>MCP 连接</h2> <div class="server-input"> <input type="text" id="server-url" value="ws://localhost:3001/ws" placeholder="输入 MCP 服务器 WebSocket URL" /> <button id="connect-button">连接</button> </div> <div class="checkbox-group"> <input type="checkbox" id="auto-reconnect" checked /> <label for="auto-reconnect">自动重连</label> </div> <div id="connection-status" class="connection-status status-disconnected" > 未连接到 MCP </div> <div class="log-area" id="log-area"> <div class="log-item">等待 MCP 连接和命令...</div> </div> </div> <div class="button-group"> <button class="cancel" id="cancel">关闭</button> </div> </div> <script> // 获取所有输入元素 const xInput = document.getElementById("x"); const yInput = document.getElementById("y"); const widthInput = document.getElementById("width"); const heightInput = document.getElementById("height"); const colorInput = document.getElementById("color"); const textInput = document.getElementById("text"); const fontSizeInput = document.getElementById("fontSize"); const connectionStatus = document.getElementById("connection-status"); const logArea = document.getElementById("log-area"); const serverUrlInput = document.getElementById("server-url"); const connectButton = document.getElementById("connect-button"); const autoReconnectCheckbox = document.getElementById("auto-reconnect"); // MCP 连接状态和 WebSocket 对象 let mcpConnected = false; let ws = null; let isConnecting = false; let isManualDisconnect = false; let retryCount = 0; let maxRetries = 10; let reconnectTimer = null; // 添加按钮事件监听器 document.getElementById("create-rectangle").onclick = () => { parent.postMessage( { pluginMessage: { type: "create-rectangle", x: parseInt(xInput.value), y: parseInt(yInput.value), width: parseInt(widthInput.value), height: parseInt(heightInput.value), color: colorInput.value, }, }, "*" ); }; document.getElementById("create-circle").onclick = () => { parent.postMessage( { pluginMessage: { type: "create-circle", x: parseInt(xInput.value), y: parseInt(yInput.value), width: parseInt(widthInput.value), height: parseInt(heightInput.value), color: colorInput.value, }, }, "*" ); }; document.getElementById("create-text").onclick = () => { parent.postMessage( { pluginMessage: { type: "create-text", x: parseInt(xInput.value), y: parseInt(yInput.value), text: textInput.value, fontSize: parseInt(fontSizeInput.value), }, }, "*" ); }; document.getElementById("cancel").onclick = () => { parent.postMessage( { pluginMessage: { type: "cancel" }, }, "*" ); }; // 添加日志条目 function addLogEntry(message) { const logItem = document.createElement("div"); logItem.classList.add("log-item"); logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logArea.appendChild(logItem); logArea.scrollTop = logArea.scrollHeight; } // 设置 MCP 连接状态 function setMcpConnectionStatus(status) { if (status === "connected") { mcpConnected = true; isConnecting = false; connectionStatus.className = "connection-status status-connected"; connectionStatus.textContent = "已连接到 MCP"; connectButton.textContent = "断开"; addLogEntry("MCP 已连接"); retryCount = 0; } else if (status === "connecting") { mcpConnected = false; isConnecting = true; connectionStatus.className = "connection-status status-connecting"; connectionStatus.textContent = `正在连接 MCP (尝试 ${ retryCount + 1 }/${maxRetries})`; connectButton.textContent = "取消"; addLogEntry(`尝试连接 MCP (${retryCount + 1}/${maxRetries})...`); } else { // disconnected mcpConnected = false; isConnecting = false; connectionStatus.className = "connection-status status-disconnected"; connectionStatus.textContent = "未连接到 MCP"; connectButton.textContent = "连接"; addLogEntry("MCP 已断开连接"); } } // 计算重连延迟,使用指数退避策略 function getReconnectDelay() { // 1秒, 2秒, 4秒, 8秒... return Math.min(1000 * Math.pow(2, retryCount), 30000); } // 清除所有重连定时器 function clearReconnectTimer() { if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } } // 重置连接状态 function resetConnectionState() { isConnecting = false; isManualDisconnect = false; clearReconnectTimer(); if (ws) { try { ws.close(); } catch (e) { // 忽略关闭错误 } ws = null; } } // 尝试重连 function attemptReconnect() { if ( isManualDisconnect || !autoReconnectCheckbox.checked || retryCount >= maxRetries ) { if (retryCount >= maxRetries) { addLogEntry(`已达到最大重试次数 (${maxRetries}),停止重连`); } setMcpConnectionStatus("disconnected"); retryCount = 0; return; } retryCount++; setMcpConnectionStatus("connecting"); const delay = getReconnectDelay(); addLogEntry(`将在 ${delay / 1000}秒后重新连接...`); clearReconnectTimer(); reconnectTimer = setTimeout(() => { connectToMcp(); }, delay); } // 连接到 MCP 服务器 function connectToMcp() { // 如果已经连接或正在连接,返回 if (mcpConnected) { // 如果已连接,则断开 isManualDisconnect = true; resetConnectionState(); setMcpConnectionStatus("disconnected"); return; } // 如果正在尝试连接,则取消 if (isConnecting) { isManualDisconnect = true; resetConnectionState(); setMcpConnectionStatus("disconnected"); return; } // 清除之前的连接 resetConnectionState(); isManualDisconnect = false; const serverUrl = serverUrlInput.value.trim(); if (!serverUrl) { addLogEntry("错误: 服务器 URL 不能为空"); return; } try { setMcpConnectionStatus("connecting"); // 创建 WebSocket 连接 ws = new WebSocket(serverUrl); ws.onopen = function () { setMcpConnectionStatus("connected"); // 发送初始化消息 ws.send( JSON.stringify({ type: "figma-plugin-connected", pluginId: "figma-mcp-canvas-tools", }) ); }; ws.onmessage = function (event) { try { const message = JSON.parse(event.data); addLogEntry(`收到 MCP 命令: ${message.command}`); // 转发给插件代码 parent.postMessage( { pluginMessage: { type: "mcp-command", command: message.command, params: message.params || {}, }, }, "*" ); } catch (error) { addLogEntry(`解析消息错误: ${error.message}`); } }; ws.onclose = function () { // 只有在不是手动断开连接的情况下才尝试重连 if (!isManualDisconnect) { addLogEntry("与 MCP 服务器的连接已关闭"); attemptReconnect(); } else { setMcpConnectionStatus("disconnected"); } ws = null; }; ws.onerror = function (error) { addLogEntry(`WebSocket 错误: ${error.message || "未知错误"}`); // 错误会触发关闭事件,关闭事件会处理重连 }; } catch (error) { addLogEntry(`连接错误: ${error.message}`); attemptReconnect(); } } // 连接按钮点击事件 connectButton.addEventListener("click", connectToMcp); // 自动重连选项变更 autoReconnectCheckbox.addEventListener("change", function () { if (this.checked) { addLogEntry("自动重连已启用"); // 如果目前未连接并且不是手动断开,尝试立即连接 if (!mcpConnected && !isManualDisconnect && !isConnecting) { retryCount = 0; connectToMcp(); } } else { addLogEntry("自动重连已禁用"); isManualDisconnect = true; clearReconnectTimer(); } }); // 监听来自插件代码的消息 window.addEventListener("message", (event) => { const message = event.data.pluginMessage; console.log("Received message from plugin:", message); // 处理来自插件代码的消息 if (message && message.type === "mcp-response") { addLogEntry( `命令 ${message.command} ${ message.success ? "成功执行" : "执行失败" }` ); // 如果连接了 MCP 服务器,则将响应发送给服务器 if (ws && ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: "figma-plugin-response", success: message.success, command: message.command, result: message.result, error: message.error, }) ); } } }); // 页面加载后自动尝试连接 window.addEventListener("load", () => { if (autoReconnectCheckbox.checked) { // 小延迟后开始连接,给 UI 渲染一些时间 setTimeout(() => { connectToMcp(); }, 1000); } }); </script> </body> </html> ``` -------------------------------------------------------------------------------- /src/tools/canvas.ts: -------------------------------------------------------------------------------- ```typescript /** * Canvas Tools - MCP server tools for interacting with Figma canvas elements */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { isPluginConnected, sendCommandToPlugin, } from "../services/websocket.js"; import { logError } from "../utils.js"; import { arcParams, elementParams, elementsParams, ellipseParams, lineParams, polygonParams, rectangleParams, starParams, textParams, vectorParams, } from "./zod-schemas.js"; /** * Register canvas-related tools with the MCP server * @param server The MCP server instance */ export function registerCanvasTools(server: McpServer) { // Create a rectangle in Figma server.tool("create_rectangle", rectangleParams, async (params) => { try { // Send command to Figma plugin const response = await sendCommandToPlugin( "create-rectangle", params ).catch((error: Error) => { throw error; }); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Rectangle Created Successfully` }, { type: "text", text: `A new rectangle has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Color: ${params.color}`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating rectangle in Figma", error); return { content: [ { type: "text", text: `Error creating rectangle: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create a circle in Figma server.tool("create_circle", ellipseParams, async (params) => { try { // Send command to Figma plugin const response = await sendCommandToPlugin("create-circle", params).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Circle Created Successfully` }, { type: "text", text: `A new circle has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Color: ${params.color}`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating circle in Figma", error); return { content: [ { type: "text", text: `Error creating circle: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create an arc (partial ellipse) in Figma server.tool("create_arc", arcParams, async (params) => { try { // Prepare parameters for the plugin const arcParams = { ...params, startAngle: params.startAngle, endAngle: params.endAngle, innerRadius: params.innerRadius, }; // Send command to Figma plugin const response = await sendCommandToPlugin("create-arc", arcParams).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Arc Created Successfully` }, { type: "text", text: `A new arc has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Angles: ${params.startAngle}° to ${params.endAngle}°\n- Inner radius: ${params.innerRadius}\n- Color: ${params.color}`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating arc in Figma", error); return { content: [ { type: "text", text: `Error creating arc: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create a polygon in Figma server.tool("create_polygon", polygonParams, async (params) => { try { // Prepare parameters for the plugin const polygonParams = { ...params, pointCount: params.pointCount, }; // Send command to Figma plugin const response = await sendCommandToPlugin( "create-polygon", polygonParams ).catch((error: Error) => { throw error; }); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Polygon Created Successfully` }, { type: "text", text: `A new polygon has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Sides: ${params.pointCount}\n- Color: ${params.color}`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating polygon in Figma", error); return { content: [ { type: "text", text: `Error creating polygon: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create a star in Figma server.tool("create_star", starParams, async (params) => { try { // Prepare parameters for the plugin const starParams = { ...params, pointCount: params.pointCount, innerRadius: params.innerRadius, }; // Send command to Figma plugin const response = await sendCommandToPlugin( "create-star", starParams ).catch((error: Error) => { throw error; }); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Star Created Successfully` }, { type: "text", text: `A new star has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Points: ${params.pointCount}\n- Inner Radius: ${params.innerRadius}\n- Color: ${params.color}`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating star in Figma", error); return { content: [ { type: "text", text: `Error creating star: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create a vector in Figma server.tool("create_vector", vectorParams, async (params) => { try { // Prepare parameters for the plugin const vectorParams = { ...params, vectorNetwork: params.vectorNetwork, vectorPaths: params.vectorPaths, handleMirroring: params.handleMirroring, }; // Send command to Figma plugin const response = await sendCommandToPlugin( "create-vector", vectorParams ).catch((error: Error) => { throw error; }); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Vector Created Successfully` }, { type: "text", text: `A new vector has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Color: ${params.color}`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating vector in Figma", error); return { content: [ { type: "text", text: `Error creating vector: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create a line in Figma server.tool("create_line", lineParams, async (params) => { try { // Send command to Figma plugin const response = await sendCommandToPlugin("create-line", params).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Line Created Successfully` }, { type: "text", text: `A new line has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Length: ${params.width}px\n- Color: ${params.color}`, }, { type: "text", text: `- Rotation: ${params.rotation || 0}°`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating line in Figma", error); return { content: [ { type: "text", text: `Error creating line: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Create text in Figma server.tool("create_text", textParams, async (params) => { try { // Send command to Figma plugin const response = await sendCommandToPlugin("create-text", params).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Text Created Successfully` }, { type: "text", text: `New text has been created in your Figma canvas.`, }, { type: "text", text: `- Position: (${params.x}, ${params.y})\n- Font Size: ${params.fontSize}px\n- Content: "${params.text}"`, }, { type: "text", text: response.result && response.result.id ? `Node ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error: unknown) { logError("Error creating text in Figma", error); return { content: [ { type: "text", text: `Error creating text: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Get current selection in Figma server.tool("get_selection", {}, async () => { try { // Send command to Figma plugin const response = await sendCommandToPlugin("get-selection", {}).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } return { content: [ { type: "text", text: `# Current Selection` }, { type: "text", text: `Information about currently selected elements in Figma:`, }, { type: "text", text: response.result ? JSON.stringify(response.result, null, 2) : "No selection information available", }, ], }; } catch (error: unknown) { logError("Error getting selection in Figma", error); return { content: [ { type: "text", text: `Error getting selection: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Check plugin connection status server.tool("check_connection", {}, async () => { return { content: [ { type: "text", text: `# Figma Plugin Connection Status` }, { type: "text", text: isPluginConnected() ? `✅ Figma plugin is connected to MCP server` : `❌ No Figma plugin is currently connected`, }, { type: "text", text: isPluginConnected() ? `You can now use MCP tools to interact with the Figma canvas.` : `Please make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; }); // Get all elements from current page or specified page server.tool("get_elements", elementsParams, async (params) => { try { // Send command to Figma plugin const response = await sendCommandToPlugin("get-elements", params).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } const elements = response.result; const count = Array.isArray(elements) ? elements.length : 0; const typeValue = params.type || "ALL"; const pageName = params.page_id ? `specified page` : "current page"; return { content: [ { type: "text", text: `# Elements Retrieved` }, { type: "text", text: `Found ${count} element${ count !== 1 ? "s" : "" } of type ${typeValue} on ${pageName}.`, }, { type: "text", text: count > 0 ? `Element information: ${JSON.stringify(elements, null, 2)}` : "No elements matched your criteria.", }, ], }; } catch (error: unknown) { logError("Error getting elements from Figma", error); return { content: [ { type: "text", text: `Error retrieving elements: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); // Get a specific element by ID server.tool("get_element", elementParams, async (params) => { try { // Send command to Figma plugin const response = await sendCommandToPlugin("get-element", params).catch( (error: Error) => { throw error; } ); if (!response.success) { throw new Error(response.error || "Unknown error"); } const element = response.result; const isArray = Array.isArray(element); const hasChildren = isArray && element.length > 1; return { content: [ { type: "text", text: `# Element Retrieved` }, { type: "text", text: `Successfully retrieved element with ID: ${params.node_id}`, }, { type: "text", text: hasChildren ? `Element and ${element.length - 1} children retrieved.` : `Element information:`, }, { type: "text", text: JSON.stringify(element, null, 2), }, ], }; } catch (error: unknown) { logError("Error getting element from Figma", error); return { content: [ { type: "text", text: `Error retrieving element: ${ error instanceof Error ? error.message : "Unknown error" }`, }, { type: "text", text: `Make sure the Figma plugin is running and connected to the MCP server.`, }, ], }; } }); } ``` -------------------------------------------------------------------------------- /src/tools/zod-schemas.ts: -------------------------------------------------------------------------------- ```typescript /** * Figma Zod Schemas * Zod schemas for validating Figma API parameters */ import { z } from "zod"; // Base types export const colorSchema = z.object({ r: z.number().min(0).max(1).describe("Red channel (0-1)"), g: z.number().min(0).max(1).describe("Green channel (0-1)"), b: z.number().min(0).max(1).describe("Blue channel (0-1)"), }); // Position and size base params const positionParams = { x: z.number().default(0).describe("X position of the element"), y: z.number().default(0).describe("Y position of the element"), }; const sizeParams = { width: z.number().min(1).default(100).describe("Width of the element in pixels"), height: z.number().min(1).default(100).describe("Height of the element in pixels"), }; // Base node properties const baseNodeParams = { name: z.string().optional().describe("Name of the node"), }; // Scene node properties const sceneNodeParams = { visible: z.boolean().optional().describe("Whether the node is visible"), locked: z.boolean().optional().describe("Whether the node is locked"), }; // Blend-related properties const blendParams = { opacity: z.number().min(0).max(1).optional().describe("Opacity of the node (0-1)"), blendMode: z.enum([ "NORMAL", "DARKEN", "MULTIPLY", "LINEAR_BURN", "COLOR_BURN", "LIGHTEN", "SCREEN", "LINEAR_DODGE", "COLOR_DODGE", "OVERLAY", "SOFT_LIGHT", "HARD_LIGHT", "DIFFERENCE", "EXCLUSION", "SUBTRACT", "DIVIDE", "HUE", "SATURATION", "COLOR", "LUMINOSITY", "PASS_THROUGH" ]).optional().describe("Blend mode of the node"), isMask: z.boolean().optional().describe("Whether this node is a mask"), maskType: z.enum(["ALPHA", "LUMINANCE"]).optional().describe("Type of masking to use if this node is a mask"), effects: z.array(z.any()).optional().describe("Array of effects"), effectStyleId: z.string().optional().describe("The id of the EffectStyle object"), }; // Corner-related properties const cornerParams = { cornerRadius: z.number().min(0).optional().describe("Rounds all corners by this amount"), cornerSmoothing: z.number().min(0).max(1).optional().describe("Corner smoothing between 0 and 1"), topLeftRadius: z.number().min(0).optional().describe("Top left corner radius override"), topRightRadius: z.number().min(0).optional().describe("Top right corner radius override"), bottomLeftRadius: z.number().min(0).optional().describe("Bottom left corner radius override"), bottomRightRadius: z.number().min(0).optional().describe("Bottom right corner radius override"), }; // Geometry-related properties const geometryParams = { fills: z.array(z.any()).optional().describe("The paints used to fill the area of the shape"), fillStyleId: z.string().optional().describe("The id of the PaintStyle object linked to fills"), strokes: z.array(z.any()).optional().describe("The paints used to fill the area of the shape's strokes"), strokeStyleId: z.string().optional().describe("The id of the PaintStyle object linked to strokes"), strokeWeight: z.number().min(0).optional().describe("The thickness of the stroke, in pixels"), strokeJoin: z.enum(["MITER", "BEVEL", "ROUND"]).optional().describe("The decoration applied to vertices"), strokeAlign: z.enum(["CENTER", "INSIDE", "OUTSIDE"]).optional().describe("The alignment of the stroke"), dashPattern: z.array(z.number().min(0)).optional().describe("Array of numbers for dash pattern"), strokeCap: z.enum(["NONE", "ROUND", "SQUARE", "ARROW_LINES", "ARROW_EQUILATERAL"]).optional().describe("The decoration applied to vertices"), strokeMiterLimit: z.number().min(0).optional().describe("The miter limit on the stroke"), color: z.string().regex(/^#([0-9A-F]{6}|[0-9A-F]{8})$/i).default("#ff0000").describe("Fill color as hex code (#RRGGBB or #RRGGBBAA)"), }; // Individual strokes-related properties const rectangleStrokeParams = { strokeTopWeight: z.number().min(0).optional().describe("Top stroke weight"), strokeBottomWeight: z.number().min(0).optional().describe("Bottom stroke weight"), strokeLeftWeight: z.number().min(0).optional().describe("Left stroke weight"), strokeRightWeight: z.number().min(0).optional().describe("Right stroke weight"), }; // Layout-related properties const layoutParams = { minWidth: z.number().nullable().optional().describe("Minimum width constraint"), maxWidth: z.number().nullable().optional().describe("Maximum width constraint"), minHeight: z.number().nullable().optional().describe("Minimum height constraint"), maxHeight: z.number().nullable().optional().describe("Maximum height constraint"), layoutAlign: z.enum(["MIN", "CENTER", "MAX", "STRETCH", "INHERIT"]).optional().describe("Alignment within parent"), layoutGrow: z.number().min(0).default(0).optional().describe("Stretch along parent's primary axis"), layoutPositioning: z.enum(["AUTO", "ABSOLUTE"]).optional().describe("Layout positioning mode"), constrainProportions: z.boolean().optional().describe("Whether to keep proportions when resizing"), rotation: z.number().min(-180).max(180).optional().describe("Rotation in degrees (-180 to 180)"), layoutSizingHorizontal: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing mode"), layoutSizingVertical: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing mode"), constraints: z.any().optional().describe("Constraints relative to containing frame"), }; // Common properties for all export settings const commonExportSettingsProps = { contentsOnly: z.boolean().optional().describe("Whether only the contents of the node are exported. Defaults to true."), useAbsoluteBounds: z.boolean().optional().describe("Use full dimensions regardless of cropping. Defaults to false."), suffix: z.string().optional().describe("Suffix appended to the file name when exporting."), colorProfile: z.enum(["DOCUMENT", "SRGB", "DISPLAY_P3_V4"]).optional().describe("Color profile of the export."), }; // Common SVG export properties const commonSvgExportProps = { ...commonExportSettingsProps, svgOutlineText: z.boolean().optional().describe("Whether text elements are rendered as outlines. Defaults to true."), svgIdAttribute: z.boolean().optional().describe("Whether to include layer names as ID attributes. Defaults to false."), svgSimplifyStroke: z.boolean().optional().describe("Whether to simplify inside and outside strokes. Defaults to true."), }; // Export constraints const exportConstraintsSchema = z.object({ type: z.enum(["SCALE", "WIDTH", "HEIGHT"]).describe("Type of constraint for the export"), value: z.number().positive().describe("Value for the constraint") }); // Export Settings Image (JPG/PNG) const exportSettingsImageSchema = z.object({ format: z.enum(["JPG", "PNG"]).describe("The export format (JPG or PNG)"), constraint: exportConstraintsSchema.optional().describe("Constraint on the image size when exporting"), ...commonExportSettingsProps }); // Export Settings SVG const exportSettingsSvgSchema = z.object({ format: z.literal("SVG").describe("The export format (SVG)"), ...commonSvgExportProps }); // Export Settings SVG String (for exportAsync only) const exportSettingsSvgStringSchema = z.object({ format: z.literal("SVG_STRING").describe("The export format (SVG_STRING)"), ...commonSvgExportProps }); // Export Settings PDF const exportSettingsPdfSchema = z.object({ format: z.literal("PDF").describe("The export format (PDF)"), ...commonExportSettingsProps }); // Export Settings REST const exportSettingsRestSchema = z.object({ format: z.literal("JSON_REST_V1").describe("The export format (JSON_REST_V1)"), ...commonExportSettingsProps }); // Combined Export Settings type const exportSettingsSchema = z.discriminatedUnion("format", [ exportSettingsImageSchema, exportSettingsSvgSchema, exportSettingsSvgStringSchema, exportSettingsPdfSchema, exportSettingsRestSchema ]); // Export-related properties const exportParams = { exportSettings: z.array(exportSettingsSchema).optional().describe("Export settings stored on the node"), }; // Prototyping - Trigger and Action types const triggerSchema = z.enum([ "ON_CLICK", "ON_HOVER", "ON_PRESS", "ON_DRAG", "AFTER_TIMEOUT", "MOUSE_ENTER", "MOUSE_LEAVE", "MOUSE_UP", "MOUSE_DOWN", "ON_KEY_DOWN" ]).nullable().describe("The trigger that initiates the prototype interaction"); // Action represents what happens when a trigger is activated const actionSchema = z.object({ type: z.enum([ "BACK", "CLOSE", "URL", "NODE", "SWAP", "OVERLAY", "SCROLL_TO", "OPEN_LINK" ]).describe("The type of action to perform"), url: z.string().optional().describe("URL to navigate to if action type is URL"), nodeID: z.string().optional().describe("ID of the node if action type is NODE"), destinationID: z.string().optional().describe("Destination node ID"), navigation: z.enum(["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO"]).optional().describe("Navigation type"), transitionNode: z.string().optional().describe("ID of the node to use for transition"), preserveScrollPosition: z.boolean().optional().describe("Whether to preserve scroll position"), overlayRelativePosition: z.object({ x: z.number(), y: z.number() }).optional().describe("Relative position for overlay"), // Additional properties can be added as needed based on Figma API }); // Reaction combines a trigger with actions for prototyping const reactionSchema = z.object({ action: actionSchema.optional().describe("DEPRECATED: The action triggered by this reaction"), actions: z.array(actionSchema).optional().describe("The actions triggered by this reaction"), trigger: triggerSchema.describe("The trigger that initiates this reaction") }); // Reaction properties const reactionParams = { reactions: z.array(reactionSchema).optional().describe("List of reactions for prototyping"), }; // Annotation properties const annotationPropertySchema = z.object({ type: z.enum([ "width", "height", "fills", "strokes", "strokeWeight", "cornerRadius", "opacity", "blendMode", "effects", "layoutConstraints", "padding", "itemSpacing", "layoutMode", "primaryAxisAlignment", "counterAxisAlignment", "fontName", "fontSize", "letterSpacing", "lineHeight", "textCase", "textDecoration", "textAlignHorizontal", "textAlignVertical", "characters" ]).describe("The type of property being annotated"), value: z.any().optional().describe("The value of the property (if applicable)") }); // Annotation schema const annotationSchema = z.object({ label: z.string().optional().describe("Text label for the annotation"), labelMarkdown: z.string().optional().describe("Markdown-formatted text label"), properties: z.array(annotationPropertySchema).optional().describe("Properties pinned in this annotation"), categoryId: z.string().optional().describe("ID of the annotation category") }); // Annotation properties const annotationParams = { annotations: z.array(annotationSchema).optional().describe("Annotations on the node"), }; // Line parameters (width represents length, height is always 0) export const lineParams = { ...positionParams, width: z.number().min(1).default(100).describe("Length of the line in pixels"), ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...geometryParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, }; // Combined parameters for rectangles export const rectangleParams = { ...positionParams, ...sizeParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...cornerParams, ...geometryParams, ...rectangleStrokeParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, }; // Ellipse Arc data for creating arcs and donuts const arcDataSchema = z.object({ startingAngle: z.number().describe("Starting angle in degrees from 0 to 360"), endingAngle: z.number().describe("Ending angle in degrees from 0 to 360"), innerRadius: z.number().min(0).max(1).describe("Inner radius ratio from 0 to 1") }); // Circle/Ellipse parameters export const ellipseParams = { ...positionParams, ...sizeParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...geometryParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, arcData: arcDataSchema.optional().describe("Arc data for creating partial ellipses and donuts") }; // Text parameters export const textParams = { ...positionParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, text: z.string().default("Hello Figma!").describe("The text content"), characters: z.string().optional().describe("Alternative for text content"), fontSize: z.number().min(1).default(24).describe("The font size in pixels"), fontFamily: z.string().optional().describe("Font family name"), fontStyle: z.string().optional().describe("Font style (e.g., 'Regular', 'Bold')"), fontName: z.object({ family: z.string().optional().describe("Font family name"), style: z.string().optional().describe("Font style (e.g., 'Regular', 'Bold')"), }).optional().describe("Font family and style"), textAlignHorizontal: z.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]).optional().describe("Horizontal text alignment"), textAlignVertical: z.enum(["TOP", "CENTER", "BOTTOM"]).optional().describe("Vertical text alignment"), textAutoResize: z.enum(["NONE", "WIDTH_AND_HEIGHT", "HEIGHT", "TRUNCATE"]).optional().describe("How text box adjusts to fit characters"), textTruncation: z.enum(["DISABLED", "ENDING"]).optional().describe("Whether text will truncate with ellipsis"), maxLines: z.number().nullable().optional().describe("Max number of lines before truncation"), paragraphIndent: z.number().optional().describe("Indentation of paragraphs"), paragraphSpacing: z.number().optional().describe("Vertical distance between paragraphs"), listSpacing: z.number().optional().describe("Vertical distance between lines of a list"), hangingPunctuation: z.boolean().optional().describe("Whether punctuation hangs outside the text box"), hangingList: z.boolean().optional().describe("Whether list counters/bullets hang outside the text box"), autoRename: z.boolean().optional().describe("Whether to update node name based on text content"), letterSpacing: z.union([ z.number(), z.object({ value: z.number(), unit: z.enum(["PIXELS", "PERCENT"]) }) ]).optional().describe("Letter spacing between characters"), lineHeight: z.union([ z.number(), z.object({ value: z.number(), unit: z.enum(["PIXELS", "PERCENT"]) }) ]).optional().describe("Line height"), leadingTrim: z.enum(["NONE", "CAP_HEIGHT", "BOTH"]).optional().describe("Removal of vertical space above/below text glyphs"), textCase: z.enum(["ORIGINAL", "UPPER", "LOWER", "TITLE"]).optional().describe("Text case transformation"), textDecoration: z.enum(["NONE", "UNDERLINE", "STRIKETHROUGH"]).optional().describe("Text decoration"), textDecorationStyle: z.enum(["SOLID", "DASHED", "DOTTED", "WAVY", "DOUBLE"]).optional().describe("Text decoration style"), textDecorationOffset: z.union([ z.number(), z.object({ value: z.number(), unit: z.enum(["PIXELS", "PERCENT"]) }) ]).optional().describe("Text decoration offset"), textDecorationThickness: z.union([ z.number(), z.object({ value: z.number(), unit: z.enum(["PIXELS", "PERCENT"]) }) ]).optional().describe("Text decoration thickness"), textDecorationColor: z.union([ z.object({ r: z.number().min(0).max(1), g: z.number().min(0).max(1), b: z.number().min(0).max(1), a: z.number().min(0).max(1).optional() }), z.string() ]).optional().describe("Text decoration color"), textDecorationSkipInk: z.boolean().optional().describe("Whether text decoration skips descenders"), textStyleId: z.string().optional().describe("ID of linked TextStyle object"), hyperlink: z.object({ type: z.enum(["URL", "NODE"]), url: z.string().optional(), nodeID: z.string().optional() }).nullable().optional().describe("Hyperlink target"), fill: z.string().optional().describe("Fill color as hex code (shorthand for fills)"), rangeStyles: z.array( z.object({ start: z.number().describe("Start index (inclusive)"), end: z.number().describe("End index (exclusive)"), style: z.object({}).passthrough().describe("Style properties to apply to range") }) ).optional().describe("Character-level styling for text ranges"), width: z.number().optional().describe("Width of the text box") }; // Frame parameters export const frameParams = { ...positionParams, ...sizeParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...cornerParams, ...geometryParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, itemSpacing: z.number().min(0).optional().describe("Space between children in auto-layout"), layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout direction"), primaryAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().describe("How frame sizes along primary axis"), counterAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().describe("How frame sizes along counter axis"), primaryAxisAlignItems: z.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]).optional().describe("Alignment along primary axis"), counterAxisAlignItems: z.enum(["MIN", "CENTER", "MAX"]).optional().describe("Alignment along counter axis"), paddingLeft: z.number().min(0).optional().describe("Padding on left side"), paddingRight: z.number().min(0).optional().describe("Padding on right side"), paddingTop: z.number().min(0).optional().describe("Padding on top side"), paddingBottom: z.number().min(0).optional().describe("Padding on bottom side"), }; // Arc parameters (based on ellipse parameters with added angle parameters) export const arcParams = { ...ellipseParams, startAngle: z.number().default(0).describe("Starting angle in degrees"), endAngle: z.number().default(180).describe("Ending angle in degrees"), innerRadius: z.number().min(0).max(1).default(0).describe("Inner radius ratio (0-1) for donut shapes") }; // Polygon parameters export const polygonParams = { ...positionParams, ...sizeParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...cornerParams, ...geometryParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, pointCount: z.number().int().min(3).default(3).describe("Number of sides of the polygon. Must be an integer >= 3.") }; // Star parameters export const starParams = { ...positionParams, ...sizeParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...geometryParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, pointCount: z.number().int().min(3).default(5).describe("Number of points on the star. Must be an integer >= 3."), innerRadius: z.number().min(0).max(1).default(0.5).describe("Ratio of inner radius to outer radius (0-1).") }; // Vector parameters for Vector Node export const vectorParams = { ...positionParams, ...sizeParams, ...baseNodeParams, ...sceneNodeParams, ...blendParams, ...cornerParams, ...geometryParams, ...layoutParams, ...exportParams, ...reactionParams, ...annotationParams, // Vector specific parameters vectorNetwork: z.any().optional().describe("Complete representation of vectors as a network of edges between vertices"), vectorPaths: z.any().optional().describe("Simple representation of vectors as paths"), handleMirroring: z.enum(["NONE", "ANGLE", "ANGLE_AND_LENGTH"]).optional().describe("Whether the vector handles are mirrored or independent") }; export const elementParams = { node_id: z.string().describe("ID of the element to retrieve"), include_children: z.boolean().optional().default(false).describe("Whether to include children of the element"), } export const elementsParams = { type: z.enum([ "ALL", "RECTANGLE", "ELLIPSE", "POLYGON", "STAR", "VECTOR", "TEXT", "FRAME", "COMPONENT", "INSTANCE", "BOOLEAN_OPERATION", "GROUP", "SECTION", "SLICE", "LINE", "CONNECTOR", "SHAPE_WITH_TEXT", "CODE_BLOCK", "STAMP", "WIDGET", "STICKY", "TABLE", "SECTION", "HIGHLIGHT" ]).optional().default("ALL").describe("Type of elements to filter (default: ALL)"), page_id: z.string().optional().describe("ID of page to get elements from (default: current page)"), limit: z.number().int().min(1).max(1000).optional().default(100).describe("Maximum number of elements to return"), include_hidden: z.boolean().optional().default(false).describe("Whether to include hidden elements"), }; ``` -------------------------------------------------------------------------------- /src/plugin/code.js: -------------------------------------------------------------------------------- ```javascript // src/plugin/utils/colorUtils.ts function hexToRgb(hex) { hex = hex.replace("#", ""); const r = parseInt(hex.substring(0, 2), 16) / 255; const g = parseInt(hex.substring(2, 4), 16) / 255; const b = parseInt(hex.substring(4, 6), 16) / 255; return { r, g, b }; } function createSolidPaint(color) { if (typeof color === "string") { return { type: "SOLID", color: hexToRgb(color) }; } return { type: "SOLID", color }; } // src/plugin/utils/nodeUtils.ts function applyCommonProperties(node, data) { if (data.x !== undefined) node.x = data.x; if (data.y !== undefined) node.y = data.y; if (data.name) node.name = data.name; if (data.opacity !== undefined && "opacity" in node) { node.opacity = data.opacity; } if (data.blendMode && "blendMode" in node) { node.blendMode = data.blendMode; } if (data.effects && "effects" in node) { node.effects = data.effects; } if (data.constraints && "constraints" in node) { node.constraints = data.constraints; } if (data.isMask !== undefined && "isMask" in node) { node.isMask = data.isMask; } if (data.visible !== undefined) node.visible = data.visible; if (data.locked !== undefined) node.locked = data.locked; } function selectAndFocusNodes(nodes) { const nodesToFocus = Array.isArray(nodes) ? nodes : [nodes]; figma.currentPage.selection = nodesToFocus; figma.viewport.scrollAndZoomIntoView(nodesToFocus); } function buildResultObject(result) { let resultObject = {}; if (!result) return resultObject; if (Array.isArray(result)) { resultObject.count = result.length; if (result.length > 0) { resultObject.items = result.map((node) => ({ id: node.id, type: node.type, name: node.name })); } } else { const node = result; resultObject.id = node.id; resultObject.type = node.type; resultObject.name = node.name; } return resultObject; } // src/plugin/creators/shapeCreators.ts function createRectangleFromData(data) { const rect = figma.createRectangle(); rect.resize(data.width || 100, data.height || 100); if (data.fills) { rect.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { rect.fills = [createSolidPaint(data.fill)]; } else { rect.fills = [data.fill]; } } if (data.strokes) rect.strokes = data.strokes; if (data.strokeWeight !== undefined) rect.strokeWeight = data.strokeWeight; if (data.strokeAlign) rect.strokeAlign = data.strokeAlign; if (data.strokeCap) rect.strokeCap = data.strokeCap; if (data.strokeJoin) rect.strokeJoin = data.strokeJoin; if (data.dashPattern) rect.dashPattern = data.dashPattern; if (data.cornerRadius !== undefined) rect.cornerRadius = data.cornerRadius; if (data.topLeftRadius !== undefined) rect.topLeftRadius = data.topLeftRadius; if (data.topRightRadius !== undefined) rect.topRightRadius = data.topRightRadius; if (data.bottomLeftRadius !== undefined) rect.bottomLeftRadius = data.bottomLeftRadius; if (data.bottomRightRadius !== undefined) rect.bottomRightRadius = data.bottomRightRadius; return rect; } function createEllipseFromData(data) { const ellipse = figma.createEllipse(); ellipse.resize(data.width || 100, data.height || 100); if (data.fills) { ellipse.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { ellipse.fills = [createSolidPaint(data.fill)]; } else { ellipse.fills = [data.fill]; } } if (data.arcData) { ellipse.arcData = { startingAngle: data.arcData.startingAngle !== undefined ? data.arcData.startingAngle : 0, endingAngle: data.arcData.endingAngle !== undefined ? data.arcData.endingAngle : 360, innerRadius: data.arcData.innerRadius !== undefined ? data.arcData.innerRadius : 0 }; } if (data.strokes) ellipse.strokes = data.strokes; if (data.strokeWeight !== undefined) ellipse.strokeWeight = data.strokeWeight; if (data.strokeAlign) ellipse.strokeAlign = data.strokeAlign; if (data.strokeCap) ellipse.strokeCap = data.strokeCap; if (data.strokeJoin) ellipse.strokeJoin = data.strokeJoin; if (data.dashPattern) ellipse.dashPattern = data.dashPattern; return ellipse; } function createPolygonFromData(data) { const polygon = figma.createPolygon(); polygon.resize(data.width || 100, data.height || 100); if (data.pointCount) polygon.pointCount = data.pointCount; if (data.fills) { polygon.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { polygon.fills = [createSolidPaint(data.fill)]; } else { polygon.fills = [data.fill]; } } else if (data.color) { polygon.fills = [createSolidPaint(data.color)]; } if (data.strokes) polygon.strokes = data.strokes; if (data.strokeWeight !== undefined) polygon.strokeWeight = data.strokeWeight; if (data.strokeAlign) polygon.strokeAlign = data.strokeAlign; if (data.strokeCap) polygon.strokeCap = data.strokeCap; if (data.strokeJoin) polygon.strokeJoin = data.strokeJoin; if (data.dashPattern) polygon.dashPattern = data.dashPattern; return polygon; } function createStarFromData(data) { const star = figma.createStar(); star.resize(data.width || 100, data.height || 100); if (data.pointCount) star.pointCount = data.pointCount; if (data.innerRadius) star.innerRadius = data.innerRadius; if (data.fills) { star.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { star.fills = [createSolidPaint(data.fill)]; } else { star.fills = [data.fill]; } } return star; } function createLineFromData(data) { const line = figma.createLine(); line.resize(data.width || 100, 0); if (data.rotation !== undefined) line.rotation = data.rotation; if (data.strokeWeight) line.strokeWeight = data.strokeWeight; if (data.strokeAlign) line.strokeAlign = data.strokeAlign; if (data.strokeCap) line.strokeCap = data.strokeCap; if (data.strokeJoin) line.strokeJoin = data.strokeJoin; if (data.dashPattern) line.dashPattern = data.dashPattern; if (data.strokes) { line.strokes = data.strokes; } else if (data.stroke) { if (typeof data.stroke === "string") { line.strokes = [createSolidPaint(data.stroke)]; } else { line.strokes = [data.stroke]; } } else if (data.color) { line.strokes = [createSolidPaint(data.color)]; } return line; } function createVectorFromData(data) { const vector = figma.createVector(); try { vector.resize(data.width || 100, data.height || 100); if (data.vectorNetwork) { vector.vectorNetwork = data.vectorNetwork; } if (data.vectorPaths) { vector.vectorPaths = data.vectorPaths; } if (data.handleMirroring) { vector.handleMirroring = data.handleMirroring; } if (data.fills) { vector.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { vector.fills = [createSolidPaint(data.fill)]; } else { vector.fills = [data.fill]; } } else if (data.color) { vector.fills = [createSolidPaint(data.color)]; } if (data.strokes) vector.strokes = data.strokes; if (data.strokeWeight !== undefined) vector.strokeWeight = data.strokeWeight; if (data.strokeAlign) vector.strokeAlign = data.strokeAlign; if (data.strokeCap) vector.strokeCap = data.strokeCap; if (data.strokeJoin) vector.strokeJoin = data.strokeJoin; if (data.dashPattern) vector.dashPattern = data.dashPattern; if (data.strokeMiterLimit) vector.strokeMiterLimit = data.strokeMiterLimit; if (data.cornerRadius !== undefined) vector.cornerRadius = data.cornerRadius; if (data.cornerSmoothing !== undefined) vector.cornerSmoothing = data.cornerSmoothing; if (data.opacity !== undefined) vector.opacity = data.opacity; if (data.blendMode) vector.blendMode = data.blendMode; if (data.isMask !== undefined) vector.isMask = data.isMask; if (data.effects) vector.effects = data.effects; if (data.constraints) vector.constraints = data.constraints; if (data.layoutAlign) vector.layoutAlign = data.layoutAlign; if (data.layoutGrow !== undefined) vector.layoutGrow = data.layoutGrow; if (data.layoutPositioning) vector.layoutPositioning = data.layoutPositioning; if (data.rotation !== undefined) vector.rotation = data.rotation; if (data.layoutSizingHorizontal) vector.layoutSizingHorizontal = data.layoutSizingHorizontal; if (data.layoutSizingVertical) vector.layoutSizingVertical = data.layoutSizingVertical; console.log("Vector created successfully:", vector); } catch (error) { console.error("Error creating vector:", error); } return vector; } // src/plugin/creators/containerCreators.ts function createFrameFromData(data) { const frame = figma.createFrame(); frame.resize(data.width || 100, data.height || 100); if (data.fills) { frame.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { frame.fills = [createSolidPaint(data.fill)]; } else { frame.fills = [data.fill]; } } if (data.layoutMode) frame.layoutMode = data.layoutMode; if (data.primaryAxisSizingMode) frame.primaryAxisSizingMode = data.primaryAxisSizingMode; if (data.counterAxisSizingMode) frame.counterAxisSizingMode = data.counterAxisSizingMode; if (data.primaryAxisAlignItems) frame.primaryAxisAlignItems = data.primaryAxisAlignItems; if (data.counterAxisAlignItems) frame.counterAxisAlignItems = data.counterAxisAlignItems; if (data.paddingLeft !== undefined) frame.paddingLeft = data.paddingLeft; if (data.paddingRight !== undefined) frame.paddingRight = data.paddingRight; if (data.paddingTop !== undefined) frame.paddingTop = data.paddingTop; if (data.paddingBottom !== undefined) frame.paddingBottom = data.paddingBottom; if (data.itemSpacing !== undefined) frame.itemSpacing = data.itemSpacing; if (data.cornerRadius !== undefined) frame.cornerRadius = data.cornerRadius; if (data.topLeftRadius !== undefined) frame.topLeftRadius = data.topLeftRadius; if (data.topRightRadius !== undefined) frame.topRightRadius = data.topRightRadius; if (data.bottomLeftRadius !== undefined) frame.bottomLeftRadius = data.bottomLeftRadius; if (data.bottomRightRadius !== undefined) frame.bottomRightRadius = data.bottomRightRadius; return frame; } function createComponentFromData(data) { const component = figma.createComponent(); component.resize(data.width || 100, data.height || 100); if (data.fills) { component.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { component.fills = [createSolidPaint(data.fill)]; } else { component.fills = [data.fill]; } } if (data.layoutMode) component.layoutMode = data.layoutMode; if (data.primaryAxisSizingMode) component.primaryAxisSizingMode = data.primaryAxisSizingMode; if (data.counterAxisSizingMode) component.counterAxisSizingMode = data.counterAxisSizingMode; if (data.primaryAxisAlignItems) component.primaryAxisAlignItems = data.primaryAxisAlignItems; if (data.counterAxisAlignItems) component.counterAxisAlignItems = data.counterAxisAlignItems; if (data.paddingLeft !== undefined) component.paddingLeft = data.paddingLeft; if (data.paddingRight !== undefined) component.paddingRight = data.paddingRight; if (data.paddingTop !== undefined) component.paddingTop = data.paddingTop; if (data.paddingBottom !== undefined) component.paddingBottom = data.paddingBottom; if (data.itemSpacing !== undefined) component.itemSpacing = data.itemSpacing; if (data.description) component.description = data.description; return component; } function createGroupFromData(data, children) { const group = figma.group(children, figma.currentPage); applyCommonProperties(group, data); return group; } function createInstanceFromData(data) { if (!data.componentId) { console.error("Cannot create instance: componentId is required"); return null; } const component = figma.getNodeById(data.componentId); if (!component || component.type !== "COMPONENT") { console.error(`Cannot create instance: component with id ${data.componentId} not found`); return null; } const instance = component.createInstance(); applyCommonProperties(instance, data); if (data.componentProperties) { for (const [key, value] of Object.entries(data.componentProperties)) { if (key in instance.componentProperties) { const prop = instance.componentProperties[key]; if (prop.type === "BOOLEAN") { instance.setProperties({ [key]: !!value }); } else if (prop.type === "TEXT") { instance.setProperties({ [key]: String(value) }); } else if (prop.type === "INSTANCE_SWAP") { instance.setProperties({ [key]: String(value) }); } else if (prop.type === "VARIANT") { instance.setProperties({ [key]: String(value) }); } } } } return instance; } function createSectionFromData(data) { const section = figma.createSection(); if (data.name) section.name = data.name; if (data.sectionContentsHidden !== undefined) section.sectionContentsHidden = data.sectionContentsHidden; if (data.x !== undefined) section.x = data.x; if (data.y !== undefined) section.y = data.y; return section; } // src/plugin/creators/textCreator.ts async function createTextFromData(data) { const text = figma.createText(); const fontFamily = data.fontFamily || data.fontName.family || "Inter"; const fontStyle = data.fontStyle || data.fontName.style || "Regular"; try { await figma.loadFontAsync({ family: fontFamily, style: fontStyle }); } catch (error) { console.warn(`Failed to load font ${fontFamily} ${fontStyle}. Falling back to Inter Regular.`); await figma.loadFontAsync({ family: "Inter", style: "Regular" }); } text.characters = data.text || data.characters || "Text"; if (data.x !== undefined) text.x = data.x; if (data.y !== undefined) text.y = data.y; if (data.fontSize) text.fontSize = data.fontSize; if (data.width) text.resize(data.width, text.height); if (data.fontName) text.fontName = data.fontName; if (data.textAlignHorizontal) text.textAlignHorizontal = data.textAlignHorizontal; if (data.textAlignVertical) text.textAlignVertical = data.textAlignVertical; if (data.textAutoResize) text.textAutoResize = data.textAutoResize; if (data.textTruncation) text.textTruncation = data.textTruncation; if (data.maxLines !== undefined) text.maxLines = data.maxLines; if (data.paragraphIndent) text.paragraphIndent = data.paragraphIndent; if (data.paragraphSpacing) text.paragraphSpacing = data.paragraphSpacing; if (data.listSpacing) text.listSpacing = data.listSpacing; if (data.hangingPunctuation !== undefined) text.hangingPunctuation = data.hangingPunctuation; if (data.hangingList !== undefined) text.hangingList = data.hangingList; if (data.autoRename !== undefined) text.autoRename = data.autoRename; if (data.letterSpacing) text.letterSpacing = data.letterSpacing; if (data.lineHeight) text.lineHeight = data.lineHeight; if (data.leadingTrim) text.leadingTrim = data.leadingTrim; if (data.textCase) text.textCase = data.textCase; if (data.textDecoration) text.textDecoration = data.textDecoration; if (data.textStyleId) text.textStyleId = data.textStyleId; if (data.textDecorationStyle) text.textDecorationStyle = data.textDecorationStyle; if (data.textDecorationOffset) text.textDecorationOffset = data.textDecorationOffset; if (data.textDecorationThickness) text.textDecorationThickness = data.textDecorationThickness; if (data.textDecorationColor) text.textDecorationColor = data.textDecorationColor; if (data.textDecorationSkipInk !== undefined) text.textDecorationSkipInk = data.textDecorationSkipInk; if (data.fills) { text.fills = data.fills; } else if (data.fill) { if (typeof data.fill === "string") { text.fills = [createSolidPaint(data.fill)]; } else { text.fills = [data.fill]; } } if (data.hyperlink) { text.hyperlink = data.hyperlink; } if (data.layoutAlign) text.layoutAlign = data.layoutAlign; if (data.layoutGrow !== undefined) text.layoutGrow = data.layoutGrow; if (data.layoutSizingHorizontal) text.layoutSizingHorizontal = data.layoutSizingHorizontal; if (data.layoutSizingVertical) text.layoutSizingVertical = data.layoutSizingVertical; if (data.rangeStyles && Array.isArray(data.rangeStyles)) { applyTextRangeStyles(text, data.rangeStyles); } if (data.name) text.name = data.name; if (data.visible !== undefined) text.visible = data.visible; if (data.locked !== undefined) text.locked = data.locked; if (data.opacity !== undefined) text.opacity = data.opacity; if (data.blendMode) text.blendMode = data.blendMode; if (data.effects) text.effects = data.effects; if (data.effectStyleId) text.effectStyleId = data.effectStyleId; if (data.exportSettings) text.exportSettings = data.exportSettings; if (data.constraints) text.constraints = data.constraints; return text; } function applyTextRangeStyles(textNode, ranges) { for (const range of ranges) { for (const [property, value] of Object.entries(range.style)) { if (property === "fills") { textNode.setRangeFills(range.start, range.end, value); } else if (property === "fillStyleId") { textNode.setRangeFillStyleId(range.start, range.end, value); } else if (property === "fontName") { textNode.setRangeFontName(range.start, range.end, value); } else if (property === "fontSize") { textNode.setRangeFontSize(range.start, range.end, value); } else if (property === "textCase") { textNode.setRangeTextCase(range.start, range.end, value); } else if (property === "textDecoration") { textNode.setRangeTextDecoration(range.start, range.end, value); } else if (property === "textDecorationStyle") { textNode.setRangeTextDecorationStyle(range.start, range.end, value); } else if (property === "textDecorationOffset") { textNode.setRangeTextDecorationOffset(range.start, range.end, value); } else if (property === "textDecorationThickness") { textNode.setRangeTextDecorationThickness(range.start, range.end, value); } else if (property === "textDecorationColor") { textNode.setRangeTextDecorationColor(range.start, range.end, value); } else if (property === "textDecorationSkipInk") { textNode.setRangeTextDecorationSkipInk(range.start, range.end, value); } else if (property === "letterSpacing") { textNode.setRangeLetterSpacing(range.start, range.end, value); } else if (property === "lineHeight") { textNode.setRangeLineHeight(range.start, range.end, value); } else if (property === "hyperlink") { textNode.setRangeHyperlink(range.start, range.end, value); } else if (property === "textStyleId") { textNode.setRangeTextStyleId(range.start, range.end, value); } else if (property === "indentation") { textNode.setRangeIndentation(range.start, range.end, value); } else if (property === "paragraphIndent") { textNode.setRangeParagraphIndent(range.start, range.end, value); } else if (property === "paragraphSpacing") { textNode.setRangeParagraphSpacing(range.start, range.end, value); } else if (property === "listOptions") { textNode.setRangeListOptions(range.start, range.end, value); } else if (property === "listSpacing") { textNode.setRangeListSpacing(range.start, range.end, value); } } } } // src/plugin/creators/specialCreators.ts function createBooleanOperationFromData(data) { if (!data.children || !Array.isArray(data.children) || data.children.length < 2) { console.error("Boolean operation requires at least 2 child nodes"); return null; } let childNodes = []; try { for (const childData of data.children) { const node = figma.createRectangle(); childNodes.push(node); } const booleanOperation = figma.createBooleanOperation(); if (data.booleanOperation) { booleanOperation.booleanOperation = data.booleanOperation; } applyCommonProperties(booleanOperation, data); return booleanOperation; } catch (error) { console.error("Failed to create boolean operation:", error); childNodes.forEach((node) => node.remove()); return null; } } function createConnectorFromData(data) { const connector = figma.createConnector(); if (data.connectorStart) connector.connectorStart = data.connectorStart; if (data.connectorEnd) connector.connectorEnd = data.connectorEnd; if (data.connectorStartStrokeCap) connector.connectorStartStrokeCap = data.connectorStartStrokeCap; if (data.connectorEndStrokeCap) connector.connectorEndStrokeCap = data.connectorEndStrokeCap; if (data.connectorLineType) connector.connectorLineType = data.connectorLineType; if (data.strokes) connector.strokes = data.strokes; if (data.strokeWeight) connector.strokeWeight = data.strokeWeight; applyCommonProperties(connector, data); return connector; } function createShapeWithTextFromData(data) { if (!("createShapeWithText" in figma)) { console.error("ShapeWithText creation is not supported in this Figma version"); return null; } try { const shapeWithText = figma.createShapeWithText(); if (data.shapeType) shapeWithText.shapeType = data.shapeType; if (data.text || data.characters) { shapeWithText.text.characters = data.text || data.characters; } try { if (data.fontSize) shapeWithText.text.fontSize = data.fontSize; if (data.fontName) shapeWithText.text.fontName = data.fontName; if (data.textAlignHorizontal && "textAlignHorizontal" in shapeWithText.text) { shapeWithText.text.textAlignHorizontal = data.textAlignHorizontal; } if (data.textAlignVertical && "textAlignVertical" in shapeWithText.text) { shapeWithText.text.textAlignVertical = data.textAlignVertical; } } catch (e) { console.warn("Some text properties could not be set on ShapeWithText:", e); } if (data.fills) shapeWithText.fills = data.fills; if (data.strokes) shapeWithText.strokes = data.strokes; applyCommonProperties(shapeWithText, data); return shapeWithText; } catch (error) { console.error("Failed to create shape with text:", error); return null; } } function createCodeBlockFromData(data) { const codeBlock = figma.createCodeBlock(); if (data.code) codeBlock.code = data.code; if (data.codeLanguage) codeBlock.codeLanguage = data.codeLanguage; applyCommonProperties(codeBlock, data); return codeBlock; } function createTableFromData(data) { const table = figma.createTable(data.numRows || 2, data.numColumns || 2); if (data.fills && "fills" in table) { table.fills = data.fills; } if (data.cells && Array.isArray(data.cells)) { for (const cellData of data.cells) { if (cellData.rowIndex !== undefined && cellData.columnIndex !== undefined) { try { let cell; if ("cellAt" in table) { cell = table.cellAt(cellData.rowIndex, cellData.columnIndex); } else if ("getCellAt" in table) { cell = table.getCellAt(cellData.rowIndex, cellData.columnIndex); } if (cell) { if (cellData.text && cell.text) cell.text.characters = cellData.text; if (cellData.fills && "fills" in cell) cell.fills = cellData.fills; if (cellData.rowSpan && "rowSpan" in cell) cell.rowSpan = cellData.rowSpan; if (cellData.columnSpan && "columnSpan" in cell) cell.columnSpan = cellData.columnSpan; } } catch (e) { console.warn(`Could not set properties for cell at ${cellData.rowIndex}, ${cellData.columnIndex}:`, e); } } } } applyCommonProperties(table, data); return table; } function createWidgetFromData(data) { if (!("createWidget" in figma)) { console.error("Widget creation is not supported in this Figma version"); return null; } if (!data.widgetId) { console.error("Widget creation requires a widgetId"); return null; } try { const widget = figma.createWidget(data.widgetId); if (data.widgetData) widget.widgetData = JSON.stringify(data.widgetData); if (data.width && data.height && "resize" in widget) widget.resize(data.width, data.height); applyCommonProperties(widget, data); return widget; } catch (error) { console.error("Failed to create widget:", error); return null; } } function createMediaFromData(data) { if (!("createMedia" in figma)) { console.error("Media creation is not supported in this Figma version"); return null; } if (!data.hash) { console.error("Media creation requires a valid media hash"); return null; } try { const media = figma.createMedia(data.hash); applyCommonProperties(media, data); return media; } catch (error) { console.error("Failed to create media:", error); return null; } } // src/plugin/creators/imageCreators.ts function createImageFromData(data) { try { if (!data.hash) { console.error("Image creation requires an image hash"); return null; } const image = figma.createImage(data.hash); const rect = figma.createRectangle(); if (data.width && data.height) { rect.resize(data.width, data.height); } rect.fills = [{ type: "IMAGE", scaleMode: data.scaleMode || "FILL", imageHash: image.hash }]; applyCommonProperties(rect, data); return rect; } catch (error) { console.error("Failed to create image:", error); return null; } } async function createImageFromBytesAsync(data) { try { if (!data.bytes && !data.file) { console.error("Image creation requires image bytes or file"); return null; } let image; if (data.bytes) { image = await figma.createImageAsync(data.bytes); } else if (data.file) { image = await figma.createImageAsync(data.file); } else { return null; } const rect = figma.createRectangle(); if (data.width && data.height) { rect.resize(data.width, data.height); } rect.fills = [{ type: "IMAGE", scaleMode: data.scaleMode || "FILL", imageHash: image.hash }]; applyCommonProperties(rect, data); return rect; } catch (error) { console.error("Failed to create image asynchronously:", error); return null; } } function createGifFromData(data) { console.error("createGif API is not directly available or implemented"); return null; } async function createVideoFromDataAsync(data) { if (!("createVideoAsync" in figma)) { console.error("Video creation is not supported in this Figma version"); return null; } try { if (!data.bytes) { console.error("Video creation requires video bytes"); return null; } const video = await figma.createVideoAsync(data.bytes); applyCommonProperties(video, data); return video; } catch (error) { console.error("Failed to create video:", error); return null; } } async function createLinkPreviewFromDataAsync(data) { if (!("createLinkPreviewAsync" in figma)) { console.error("Link preview creation is not supported in this Figma version"); return null; } try { if (!data.url) { console.error("Link preview creation requires a URL"); return null; } const linkPreview = await figma.createLinkPreviewAsync(data.url); applyCommonProperties(linkPreview, data); return linkPreview; } catch (error) { console.error("Failed to create link preview:", error); return null; } } // src/plugin/creators/sliceCreators.ts function createSliceFromData(data) { const slice = figma.createSlice(); if (data.width && data.height) { slice.resize(data.width, data.height); } if (data.x !== undefined) slice.x = data.x; if (data.y !== undefined) slice.y = data.y; if (data.exportSettings && Array.isArray(data.exportSettings)) { slice.exportSettings = data.exportSettings; } if (data.name) slice.name = data.name; if (data.visible !== undefined) slice.visible = data.visible; return slice; } function createPageFromData(data) { const page = figma.createPage(); if (data.name) page.name = data.name; if (data.backgrounds) page.backgrounds = data.backgrounds; return page; } function createPageDividerFromData(data) { if (!("createPageDivider" in figma)) { console.error("createPageDivider is not supported in this Figma version"); return null; } try { const pageDivider = figma.createPageDivider(); if (data.name) pageDivider.name = data.name; return pageDivider; } catch (error) { console.error("Failed to create page divider:", error); return null; } } function createSlideFromData(data) { if (!("createSlide" in figma)) { console.error("createSlide is not supported in this Figma version"); return null; } try { const slide = figma.createSlide(); if (data.name) slide.name = data.name; applyCommonProperties(slide, data); return slide; } catch (error) { console.error("Failed to create slide:", error); return null; } } function createSlideRowFromData(data) { if (!("createSlideRow" in figma)) { console.error("createSlideRow is not supported in this Figma version"); return null; } try { const slideRow = figma.createSlideRow(); if (data.name) slideRow.name = data.name; applyCommonProperties(slideRow, data); return slideRow; } catch (error) { console.error("Failed to create slide row:", error); return null; } } // src/plugin/creators/componentCreators.ts function createComponentFromNodeData(data) { if (!data.sourceNode) { console.error("createComponentFromNode requires a sourceNode"); return null; } try { let sourceNode; if (typeof data.sourceNode === "string") { sourceNode = figma.getNodeById(data.sourceNode); if (!sourceNode || !("type" in sourceNode)) { console.error(`Node with ID ${data.sourceNode} not found or is not a valid node`); return null; } } else { sourceNode = data.sourceNode; } const component = figma.createComponentFromNode(sourceNode); if (data.description) component.description = data.description; applyCommonProperties(component, data); return component; } catch (error) { console.error("Failed to create component from node:", error); return null; } } function createComponentSetFromData(data) { try { if (!data.components || !Array.isArray(data.components) || data.components.length === 0) { console.error("Component set creation requires component nodes"); return null; } const componentNodes = []; for (const component of data.components) { let node; if (typeof component === "string") { node = figma.getNodeById(component); } else { node = component; } if (node && node.type === "COMPONENT") { componentNodes.push(node); } } if (componentNodes.length === 0) { console.error("No valid component nodes provided"); return null; } const componentSet = figma.combineAsVariants(componentNodes, figma.currentPage); if (data.name) componentSet.name = data.name; applyCommonProperties(componentSet, data); return componentSet; } catch (error) { console.error("Failed to create component set:", error); return null; } } // src/plugin/creators/elementCreator.ts async function createElementFromData(data) { if (!data || !data.type) { console.error("Invalid element data: missing type"); return null; } let element = null; try { switch (data.type.toLowerCase()) { case "rectangle": element = createRectangleFromData(data); break; case "ellipse": case "circle": element = createEllipseFromData(data); break; case "polygon": element = createPolygonFromData(data); break; case "star": element = createStarFromData(data); break; case "line": element = createLineFromData(data); break; case "vector": element = createVectorFromData(data); break; case "frame": element = createFrameFromData(data); break; case "component": element = createComponentFromData(data); break; case "componentfromnode": element = createComponentFromNodeData(data); break; case "componentset": element = createComponentSetFromData(data); break; case "instance": element = createInstanceFromData(data); break; case "section": element = createSectionFromData(data); break; case "text": element = await createTextFromData(data); break; case "boolean": case "booleanoperation": element = createBooleanOperationFromData(data); break; case "connector": element = createConnectorFromData(data); break; case "shapewithtext": element = createShapeWithTextFromData(data); break; case "codeblock": element = createCodeBlockFromData(data); break; case "table": element = createTableFromData(data); break; case "widget": element = createWidgetFromData(data); break; case "media": element = createMediaFromData(data); break; case "image": if (data.bytes || data.file) { element = await createImageFromBytesAsync(data); } else { element = createImageFromData(data); } break; case "gif": element = createGifFromData(data); break; case "video": element = await createVideoFromDataAsync(data); break; case "linkpreview": element = await createLinkPreviewFromDataAsync(data); break; case "slice": element = createSliceFromData(data); break; case "page": const page = createPageFromData(data); console.log(`Created page: ${page.name}`); return null; case "pagedivider": element = createPageDividerFromData(data); break; case "slide": element = createSlideFromData(data); break; case "sliderow": element = createSlideRowFromData(data); break; case "group": if (!data.children || !Array.isArray(data.children) || data.children.length < 1) { console.error("Cannot create group: children array is required"); return null; } const childNodes = []; for (const childData of data.children) { const child = await createElementFromData(childData); if (child) childNodes.push(child); } if (childNodes.length > 0) { element = createGroupFromData(data, childNodes); } else { console.error("Cannot create group: no valid children were created"); return null; } break; default: console.error(`Unsupported element type: ${data.type}`); return null; } if (element) { applyCommonProperties(element, data); if (data.select !== false) { selectAndFocusNodes(element); } } return element; } catch (error) { console.error(`Error creating element: ${error instanceof Error ? error.message : "Unknown error"}`); return null; } } async function createElementsFromDataArray(dataArray) { const createdNodes = []; for (const data of dataArray) { const node = await createElementFromData(data); if (node) createdNodes.push(node); } if (createdNodes.length > 0) { selectAndFocusNodes(createdNodes); } return createdNodes; } // src/plugin/code.ts figma.showUI(__html__, { width: 320, height: 500 }); console.log("Figma MCP Plugin loaded"); var elementCreators = { "create-rectangle": createRectangleFromData, "create-circle": createEllipseFromData, "create-ellipse": createEllipseFromData, "create-polygon": createPolygonFromData, "create-line": createLineFromData, "create-text": createTextFromData, "create-star": createStarFromData, "create-vector": createVectorFromData, "create-arc": (params) => { const ellipse = createEllipseFromData(params); if (params.arcData || params.startAngle !== undefined && params.endAngle !== undefined) { ellipse.arcData = { startingAngle: params.startAngle || params.arcData.startingAngle || 0, endingAngle: params.endAngle || params.arcData.endingAngle || 360, innerRadius: params.innerRadius || params.arcData.innerRadius || 0 }; } return ellipse; } }; async function createElement(type, params) { console.log(`Creating ${type} with params:`, params); const creator = elementCreators[type]; if (!creator) { console.error(`Unknown element type: ${type}`); return null; } try { const element = await Promise.resolve(creator(params)); if (element && params) { if (params.x !== undefined) element.x = params.x; if (params.y !== undefined) element.y = params.y; } if (element) { selectAndFocusNodes(element); } return element; } catch (error) { console.error(`Error creating ${type}:`, error); return null; } } figma.ui.onmessage = async function(msg) { console.log("Received message from UI:", msg); if (elementCreators[msg.type]) { await createElement(msg.type, msg); } else if (msg.type === "create-element") { console.log("Creating element with data:", msg.data); createElementFromData(msg.data); } else if (msg.type === "create-elements") { console.log("Creating multiple elements with data:", msg.data); createElementsFromDataArray(msg.data); } else if (msg.type === "mcp-command") { console.log("Received MCP command:", msg.command, "with params:", msg.params); handleMcpCommand(msg.command, msg.params); } else if (msg.type === "cancel") { console.log("Closing plugin"); figma.closePlugin(); } else { console.log("Unknown message type:", msg.type); } }; async function handleMcpCommand(command, params) { let result = null; try { const pluginCommand = command.replace(/_/g, "-"); switch (pluginCommand) { case "create-rectangle": case "create-circle": case "create-polygon": case "create-line": case "create-arc": case "create-vector": console.log(`MCP command: Creating ${pluginCommand.substring(7)} with params:`, params); result = await createElement(pluginCommand, params); break; case "create-text": console.log("MCP command: Creating text with params:", params); result = await createElement(pluginCommand, params); break; case "create-element": console.log("MCP command: Creating element with params:", params); result = await createElementFromData(params); break; case "create-elements": console.log("MCP command: Creating multiple elements with params:", params); result = await createElementsFromDataArray(params); break; case "get-selection": console.log("MCP command: Getting current selection"); result = figma.currentPage.selection; break; case "get-elements": console.log("MCP command: Getting elements with params:", params); const page = params.page_id ? figma.getNodeById(params.page_id) : figma.currentPage; if (!page || page.type !== "PAGE") { throw new Error("Invalid page ID or node is not a page"); } const nodeType = params.type || "ALL"; const limit = params.limit || 100; const includeHidden = params.include_hidden || false; if (nodeType === "ALL") { result = includeHidden ? page.children.slice(0, limit) : page.children.filter((node2) => node2.visible).slice(0, limit); } else { result = page.findAll((node2) => { const typeMatch = node2.type === nodeType; const visibilityMatch = includeHidden || node2.visible; return typeMatch && visibilityMatch; }).slice(0, limit); } break; case "get-element": console.log("MCP command: Getting element with ID:", params.node_id); const node = figma.getNodeById(params.node_id); if (!node) { throw new Error("Element not found with ID: " + params.node_id); } if (!["DOCUMENT", "PAGE"].includes(node.type)) { if (params.include_children && "children" in node) { result = [node, ...node.children || []]; } else { result = node; } } else if (node.type === "PAGE") { result = node; } else { throw new Error("Unsupported node type: " + node.type); } break; case "get-pages": console.log("MCP command: Getting all pages"); result = figma.root.children; break; case "get-page": console.log("MCP command: Getting page with ID:", params.page_id); if (!params.page_id) { console.log("No page_id provided, using current page"); result = figma.currentPage; } else { const pageNode = figma.getNodeById(params.page_id); if (!pageNode || pageNode.type !== "PAGE") throw new Error("Invalid page ID or node is not a page"); result = pageNode; } break; case "create-page": console.log("MCP command: Creating new page with name:", params.name); const newPage = figma.createPage(); newPage.name = params.name || "New Page"; result = newPage; break; case "switch-page": console.log("MCP command: Switching to page with ID:", params.id); if (!params.id) throw new Error("Page ID is required"); const switchPageNode = figma.getNodeById(params.id); if (!switchPageNode || switchPageNode.type !== "PAGE") throw new Error("Invalid page ID"); figma.currentPage = switchPageNode; result = switchPageNode; break; case "modify-rectangle": console.log("MCP command: Modifying rectangle with ID:", params.id); if (!params.id) throw new Error("Rectangle ID is required"); const modifyNode = figma.getNodeById(params.id); if (!modifyNode || modifyNode.type !== "RECTANGLE") throw new Error("Invalid rectangle ID"); const rect = modifyNode; if (params.x !== undefined) rect.x = params.x; if (params.y !== undefined) rect.y = params.y; if (params.width !== undefined && params.height !== undefined) rect.resize(params.width, params.height); if (params.cornerRadius !== undefined) rect.cornerRadius = params.cornerRadius; if (params.color) rect.fills = [{ type: "SOLID", color: hexToRgb(params.color) }]; result = rect; break; default: console.log("Unknown MCP command:", command); throw new Error("Unknown command: " + command); } let resultForBuilder = null; if (result === null) { resultForBuilder = null; } else if (Array.isArray(result)) { resultForBuilder = result; } else if ("type" in result && result.type === "PAGE") { resultForBuilder = result; } else { resultForBuilder = result; } const resultObject = buildResultObject(resultForBuilder); console.log("Command result:", resultObject); figma.ui.postMessage({ type: "mcp-response", success: true, command, result: resultObject }); console.log("Response sent to UI"); return resultObject; } catch (error) { console.error("Error handling MCP command:", error); figma.ui.postMessage({ type: "mcp-response", success: false, command, error: error instanceof Error ? error.message : "Unknown error" }); console.log("Error response sent to UI"); throw error; } } ```