This is page 2 of 2. Use http://codebase.md/sichang824/mcp-figma?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;
}
}
```