figma-copilot
Version:
Enhanced Figma MCP server with improved font handling and additional features. Unofficial tool, not affiliated with Figma, Inc.
1,527 lines (1,523 loc) • 153 kB
JavaScript
#!/usr/bin/env node
// src/talk_to_figma_mcp/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import WebSocket from "ws";
import { v4 as uuidv4 } from "uuid";
// src/talk_to_figma_mcp/errors.ts
var ErrorCodes = {
// Connection errors
CONNECTION_FAILED: "CONNECTION_FAILED",
WEBSOCKET_NOT_CONNECTED: "WEBSOCKET_NOT_CONNECTED",
CHANNEL_NOT_JOINED: "CHANNEL_NOT_JOINED",
// Tool errors
TOOL_DEPRECATED: "TOOL_DEPRECATED",
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
// Node operation errors
NODE_NOT_FOUND: "NODE_NOT_FOUND",
NODE_ACCESS_DENIED: "NODE_ACCESS_DENIED",
INVALID_NODE_TYPE: "INVALID_NODE_TYPE",
// Operation errors
OPERATION_TIMEOUT: "OPERATION_TIMEOUT",
OPERATION_FAILED: "OPERATION_FAILED",
INVALID_PARAMETERS: "INVALID_PARAMETERS",
// Batch operation errors
BATCH_PARTIAL_FAILURE: "BATCH_PARTIAL_FAILURE",
BATCH_SIZE_EXCEEDED: "BATCH_SIZE_EXCEEDED",
// Font/text errors
MIXED_FONTS_ERROR: "MIXED_FONTS_ERROR",
FONT_NOT_AVAILABLE: "FONT_NOT_AVAILABLE",
// Component errors
COMPONENT_NOT_FOUND: "COMPONENT_NOT_FOUND",
INVALID_COMPONENT_OVERRIDE: "INVALID_COMPONENT_OVERRIDE",
// Generic errors
UNKNOWN_ERROR: "UNKNOWN_ERROR",
INTERNAL_ERROR: "INTERNAL_ERROR"
};
function createErrorResponse(code, message, options) {
const error = {
code,
message
};
if (options?.nodeId) {
error.nodeId = options.nodeId;
}
if (options?.suggestions && options.suggestions.length > 0) {
error.suggestions = options.suggestions;
}
const response = { error };
if (options?.partialResults && options.partialResults.length > 0) {
response.partialResults = options.partialResults;
}
return response;
}
var CommonErrors = {
connectionFailed: (details) => createErrorResponse(
ErrorCodes.CONNECTION_FAILED,
`Failed to connect to Figma${details ? `: ${details}` : ""}`,
{
suggestions: [
"Ensure the Figma plugin is running",
"Check if the WebSocket server is accessible",
"Verify you have joined the correct channel"
]
}
),
nodeNotFound: (nodeId) => createErrorResponse(
ErrorCodes.NODE_NOT_FOUND,
`Node with ID "${nodeId}" not found`,
{
nodeId,
suggestions: [
"Verify the node ID is correct",
"Check if the node still exists in the document",
"Ensure you have access to the node"
]
}
),
invalidParameters: (details) => createErrorResponse(
ErrorCodes.INVALID_PARAMETERS,
`Invalid parameters: ${details}`,
{
suggestions: [
"Check the parameter types and values",
"Refer to the tool documentation for correct usage"
]
}
),
operationTimeout: (operation, nodeId) => createErrorResponse(
ErrorCodes.OPERATION_TIMEOUT,
`Operation "${operation}" timed out`,
{
nodeId,
suggestions: [
"Try with a smaller selection or fewer nodes",
"Use options like maxDepth or timeout to limit scope",
"Consider breaking the operation into smaller chunks"
]
}
),
mixedFonts: (nodeId) => createErrorResponse(
ErrorCodes.MIXED_FONTS_ERROR,
"Text node contains mixed fonts",
{
nodeId,
suggestions: [
"Use update_text_preserve_formatting to maintain text styles",
"Apply consistent font family before updating",
"Use set_multiple_text_contents_with_styles for styled updates"
]
}
)
};
function formatErrorForMCP(error) {
return {
content: [
{
type: "text",
text: JSON.stringify(error, null, 2)
}
],
isError: true
};
}
// src/talk_to_figma_mcp/server.ts
var logger = {
info: (message) => process.stderr.write(`[INFO] ${message}
`),
debug: (message) => process.stderr.write(`[DEBUG] ${message}
`),
warn: (message) => process.stderr.write(`[WARN] ${message}
`),
error: (message) => process.stderr.write(`[ERROR] ${message}
`),
log: (message) => process.stderr.write(`[LOG] ${message}
`)
};
var ws = null;
var pendingRequests = /* @__PURE__ */ new Map();
var currentChannel = null;
var server = new McpServer({
name: "TalkToFigmaMCP",
version: "1.0.0"
});
var args = process.argv.slice(2);
var serverArg = args.find((arg) => arg.startsWith("--server="));
var serverUrl = serverArg ? serverArg.split("=")[1] : "localhost";
var WS_URL = serverUrl === "localhost" ? `ws://${serverUrl}` : `wss://${serverUrl}`;
server.tool(
"get_document_info",
"Get detailed information about the current Figma document",
{},
async () => {
try {
const result = await sendCommandToFigma("get_document_info");
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get document info: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Ensure you are connected to Figma",
"Check if a document is open in Figma",
"Verify the WebSocket connection is active"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_selection",
"[DEPRECATED] Get information about the current selection in Figma. Use 'get_current_context' instead.",
{},
async () => {
console.warn("[DEPRECATION] get_selection is deprecated. Use get_current_context instead.");
try {
const result = await sendCommandToFigma("get_selection");
return {
content: [
{
type: "text",
text: JSON.stringify(result)
},
{
type: "text",
text: "\n\u26A0\uFE0F DEPRECATION WARNING: get_selection is deprecated. Please use get_current_context() instead for more comprehensive context information."
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get selection: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Use get_current_context instead of get_selection",
"The new tool provides selection plus additional context",
"Example: get_current_context()"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_current_context",
"Get comprehensive context about the current state including selection, focused slide (if in Slides mode), and optionally document info",
{
includeDocument: z.boolean().optional().describe("Include document information (default: false)"),
includeSlideDetails: z.boolean().optional().describe("Include detailed slide information if in Slides mode (default: true)"),
includeSelectionDetails: z.boolean().optional().describe("Include detailed selection information (default: false)")
},
async ({ includeDocument = false, includeSlideDetails = true, includeSelectionDetails = false }) => {
try {
const context = {};
try {
const selection = await sendCommandToFigma("get_selection");
context.selection = selection;
} catch (error) {
context.selection = { error: "Failed to get selection", selectionCount: 0 };
}
try {
const status = await sendCommandToFigma("get_connection_status", {});
context.editorType = status.editorType || "figma";
if (status.editorType === "slides" && includeSlideDetails) {
try {
const focusedSlide = await sendCommandToFigma("get_focused_slide", {});
context.focusedSlide = focusedSlide;
} catch (error) {
context.focusedSlide = null;
}
try {
const slidesMode = await sendCommandToFigma("get_slides_mode", {});
context.slidesMode = slidesMode;
} catch (error) {
context.slidesMode = null;
}
}
} catch (error) {
context.editorType = "unknown";
}
if (includeDocument) {
try {
const docInfo = await sendCommandToFigma("get_document_info");
context.document = docInfo;
} catch (error) {
context.document = { error: "Failed to get document info" };
}
}
if (includeSelectionDetails && context.selection && context.selection.selectionCount > 0) {
try {
const detailedSelection = await sendCommandToFigma("read_my_design", {});
context.selectionDetails = detailedSelection;
} catch (error) {
context.selectionDetails = null;
}
}
return {
content: [{
type: "text",
text: JSON.stringify(context, null, 2)
}]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get current context: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Ensure you are connected to Figma",
"Check if the Figma plugin is running",
"Try with different option parameters"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"read_my_design",
"Get detailed information about the current selection in Figma, including all node details",
{},
async () => {
try {
const result = await sendCommandToFigma("read_my_design", {});
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to read design: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Select one or more nodes in Figma first",
"Ensure you are connected to a Figma document",
"Check if the Figma plugin is running"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_node_info",
"[DEPRECATED] Get detailed information about a specific node in Figma. Use 'get_nodes' instead.",
{
nodeId: z.string().describe("The ID of the node to get information about")
},
async ({ nodeId }) => {
console.warn("[DEPRECATION] get_node_info is deprecated. Use get_nodes instead.");
try {
const result = await sendCommandToFigma("get_node_info", { nodeId });
const filtered = filterFigmaNode(result);
return {
content: [
{
type: "text",
text: JSON.stringify(filtered)
},
{
type: "text",
text: "\n\u26A0\uFE0F DEPRECATION WARNING: get_node_info is deprecated. Please use get_nodes({nodeIds: '" + nodeId + "'}) instead."
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("not found") || errorMessage.includes("does not exist")) {
return formatErrorForMCP(CommonErrors.nodeNotFound(nodeId));
}
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get node info: ${errorMessage}`,
{
nodeId,
suggestions: [
"Use get_nodes instead of get_node_info",
'Example: get_nodes({nodeIds: "' + nodeId + '"})',
"The new tool supports both single and multiple nodes"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
function rgbaToHex(color) {
if (color.startsWith("#")) {
return color;
}
const r = Math.round(color.r * 255);
const g = Math.round(color.g * 255);
const b = Math.round(color.b * 255);
const a = Math.round(color.a * 255);
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}${a === 255 ? "" : a.toString(16).padStart(2, "0")}`;
}
function filterFigmaNode(node) {
if (node.type === "VECTOR") {
return null;
}
const filtered = {
id: node.id,
name: node.name,
type: node.type
};
if (node.fills && node.fills.length > 0) {
filtered.fills = node.fills.map((fill) => {
const processedFill = { ...fill };
delete processedFill.boundVariables;
delete processedFill.imageRef;
if (processedFill.gradientStops) {
processedFill.gradientStops = processedFill.gradientStops.map((stop) => {
const processedStop = { ...stop };
if (processedStop.color) {
processedStop.color = rgbaToHex(processedStop.color);
}
delete processedStop.boundVariables;
return processedStop;
});
}
if (processedFill.color) {
processedFill.color = rgbaToHex(processedFill.color);
}
return processedFill;
});
}
if (node.strokes && node.strokes.length > 0) {
filtered.strokes = node.strokes.map((stroke) => {
const processedStroke = { ...stroke };
delete processedStroke.boundVariables;
if (processedStroke.color) {
processedStroke.color = rgbaToHex(processedStroke.color);
}
return processedStroke;
});
}
if (node.cornerRadius !== void 0) {
filtered.cornerRadius = node.cornerRadius;
}
if (node.absoluteBoundingBox) {
filtered.absoluteBoundingBox = node.absoluteBoundingBox;
}
if (node.characters) {
filtered.characters = node.characters;
}
if (node.style) {
filtered.style = {
fontFamily: node.style.fontFamily,
fontStyle: node.style.fontStyle,
fontWeight: node.style.fontWeight,
fontSize: node.style.fontSize,
textAlignHorizontal: node.style.textAlignHorizontal,
letterSpacing: node.style.letterSpacing,
lineHeightPx: node.style.lineHeightPx
};
}
if (node.children) {
filtered.children = node.children.map((child) => filterFigmaNode(child)).filter((child) => child !== null);
}
return filtered;
}
server.tool(
"get_nodes",
"Get detailed information about one or more nodes in Figma. Accepts either a single node ID or array of IDs.",
{
nodeIds: z.any().describe("Node ID(s) to retrieve - can be a single string ID or an array of string IDs"),
includeChildren: z.boolean().optional().describe("Whether to include child nodes (default: true)"),
maxDepth: z.number().optional().describe("Maximum depth for child traversal (-1 for unlimited, default: -1)")
},
async ({ nodeIds, includeChildren = true, maxDepth = -1 }) => {
try {
if (!nodeIds) {
throw new Error("nodeIds parameter is required");
}
let nodeIdArray;
let isSingleNode = false;
if (typeof nodeIds === "string") {
if (nodeIds.startsWith("[") && nodeIds.endsWith("]")) {
try {
const parsed = JSON.parse(nodeIds);
if (Array.isArray(parsed)) {
nodeIdArray = parsed;
isSingleNode = false;
} else {
isSingleNode = true;
nodeIdArray = [nodeIds];
}
} catch (e) {
isSingleNode = true;
nodeIdArray = [nodeIds];
}
} else {
isSingleNode = true;
nodeIdArray = [nodeIds];
}
} else if (Array.isArray(nodeIds)) {
nodeIdArray = nodeIds;
isSingleNode = nodeIdArray.length === 1;
} else {
throw new Error("nodeIds must be a string or array of strings");
}
if (nodeIdArray.length === 0) {
return {
content: [{
type: "text",
text: "No node IDs provided"
}]
};
}
if (nodeIdArray.length === 1) {
const result = await sendCommandToFigma("get_node_info", {
nodeId: nodeIdArray[0],
includeChildren,
maxDepth
});
const filtered = filterFigmaNode(result);
return {
content: [{
type: "text",
text: JSON.stringify(isSingleNode ? filtered : [filtered])
}]
};
} else {
const result = await sendCommandToFigma("get_multiple_nodes_info", {
nodeIds: nodeIdArray,
includeChildren,
maxDepth
});
const processedResults = result.results.map((nodeResult) => {
if (nodeResult.found) {
return filterFigmaNode(nodeResult.node);
}
return null;
}).filter((node) => node !== null);
return {
content: [{
type: "text",
text: JSON.stringify(processedResults)
}]
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("not found") || errorMessage.includes("does not exist")) {
const nodeId = typeof nodeIds === "string" ? nodeIds : nodeIds[0];
return formatErrorForMCP(CommonErrors.nodeNotFound(nodeId));
}
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get node(s) info: ${errorMessage}`,
{
suggestions: [
"Verify all node IDs are valid",
"Check if nodes exist in the current document",
"Ensure you have access to view these nodes"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_nodes_info",
"[DEPRECATED] Get detailed information about multiple nodes in Figma. Use 'get_nodes' instead.",
{
nodeIds: z.array(z.string()).describe("Array of node IDs to get information about")
},
async ({ nodeIds }) => {
console.warn("[DEPRECATION] get_nodes_info is deprecated. Use get_nodes instead.");
try {
const results = await Promise.all(
nodeIds.map(async (nodeId) => {
const result = await sendCommandToFigma("get_node_info", { nodeId });
return { nodeId, info: result };
})
);
return {
content: [
{
type: "text",
text: JSON.stringify(results.map((result) => filterFigmaNode(result.info)))
},
{
type: "text",
text: "\n\u26A0\uFE0F DEPRECATION WARNING: get_nodes_info is deprecated. Please use get_nodes({nodeIds: [" + nodeIds.map((id) => '"' + id + '"').join(", ") + "]}) instead."
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get multiple nodes info: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Use get_nodes instead of get_nodes_info",
'Example: get_nodes({nodeIds: ["id1", "id2"]})',
"The new tool supports both single and multiple nodes"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"create_rectangle",
"Create a new rectangle in Figma",
{
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
width: z.number().describe("Width of the rectangle"),
height: z.number().describe("Height of the rectangle"),
name: z.string().optional().describe("Optional name for the rectangle"),
parentId: z.string().optional().describe("Optional parent node ID to append the rectangle to")
},
async ({ x, y, width, height, name, parentId }) => {
try {
const result = await sendCommandToFigma("create_rectangle", {
x,
y,
width,
height,
name: name || "Rectangle",
parentId
});
return {
content: [
{
type: "text",
text: `Created rectangle "${JSON.stringify(result)}"`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to create rectangle: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Check if the parent node ID is valid",
"Ensure coordinates and dimensions are valid numbers",
"Verify you have edit access to the document"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"create_frame",
"Create a new frame in Figma",
{
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
width: z.number().describe("Width of the frame"),
height: z.number().describe("Height of the frame"),
name: z.string().optional().describe("Optional name for the frame"),
parentId: z.string().optional().describe("Optional parent node ID to append the frame to"),
fillColor: z.object({
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
}).optional().describe("Fill color in RGBA format"),
strokeColor: z.object({
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
}).optional().describe("Stroke color in RGBA format"),
strokeWeight: z.number().positive().optional().describe("Stroke weight"),
layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout mode for the frame"),
layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Whether the auto-layout frame wraps its children"),
paddingTop: z.number().optional().describe("Top padding for auto-layout frame"),
paddingRight: z.number().optional().describe("Right padding for auto-layout frame"),
paddingBottom: z.number().optional().describe("Bottom padding for auto-layout frame"),
paddingLeft: z.number().optional().describe("Left padding for auto-layout frame"),
primaryAxisAlignItems: z.enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]).optional().describe("Primary axis alignment for auto-layout frame. Note: When set to SPACE_BETWEEN, itemSpacing will be ignored as children will be evenly spaced."),
counterAxisAlignItems: z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional().describe("Counter axis alignment for auto-layout frame"),
layoutSizingHorizontal: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing mode for auto-layout frame"),
layoutSizingVertical: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing mode for auto-layout frame"),
itemSpacing: z.number().optional().describe("Distance between children in auto-layout frame. Note: This value will be ignored if primaryAxisAlignItems is set to SPACE_BETWEEN.")
},
async ({
x,
y,
width,
height,
name,
parentId,
fillColor,
strokeColor,
strokeWeight,
layoutMode,
layoutWrap,
paddingTop,
paddingRight,
paddingBottom,
paddingLeft,
primaryAxisAlignItems,
counterAxisAlignItems,
layoutSizingHorizontal,
layoutSizingVertical,
itemSpacing
}) => {
try {
const result = await sendCommandToFigma("create_frame", {
x,
y,
width,
height,
name: name || "Frame",
parentId,
fillColor: fillColor || { r: 1, g: 1, b: 1, a: 1 },
strokeColor,
strokeWeight,
layoutMode,
layoutWrap,
paddingTop,
paddingRight,
paddingBottom,
paddingLeft,
primaryAxisAlignItems,
counterAxisAlignItems,
layoutSizingHorizontal,
layoutSizingVertical,
itemSpacing
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Created frame "${typedResult.name}" with ID: ${typedResult.id}. Use the ID as the parentId to appendChild inside this frame.`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to create frame: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Check if the parent node ID is valid",
"Ensure coordinates and dimensions are valid numbers",
"Verify you have edit access to the document"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"create_text",
"Create a new text element in Figma",
{
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
text: z.string().describe("Text content"),
fontSize: z.number().optional().describe("Font size (default: 14)"),
fontWeight: z.number().optional().describe("Font weight (e.g., 400 for Regular, 700 for Bold)"),
fontColor: z.object({
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
}).optional().describe("Font color in RGBA format"),
name: z.string().optional().describe("Semantic layer name for the text node"),
parentId: z.string().optional().describe("Optional parent node ID to append the text to")
},
async ({ x, y, text, fontSize, fontWeight, fontColor, name, parentId }) => {
try {
const result = await sendCommandToFigma("create_text", {
x,
y,
text,
fontSize: fontSize || 14,
fontWeight: fontWeight || 400,
fontColor: fontColor || { r: 0, g: 0, b: 0, a: 1 },
name: name || "Text",
parentId
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Created text "${typedResult.name}" with ID: ${typedResult.id}`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to create text: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Check if the parent node ID is valid",
"Ensure coordinates and dimensions are valid numbers",
"Verify you have edit access to the document",
"Use update_text_preserve_formatting to maintain text styles"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_fill_color",
"Set the fill color of a node in Figma can be TextNode or FrameNode",
{
nodeId: z.string().describe("The ID of the node to modify"),
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
},
async ({ nodeId, r, g, b, a }) => {
try {
const result = await sendCommandToFigma("set_fill_color", {
nodeId,
color: { r, g, b, a: a || 1 }
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Set fill color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1})`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to set fill color: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Check parameter values are in the correct format",
"Ensure the node supports this operation"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_stroke_color",
"Set the stroke color of a node in Figma",
{
nodeId: z.string().describe("The ID of the node to modify"),
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"),
weight: z.number().positive().optional().describe("Stroke weight")
},
async ({ nodeId, r, g, b, a, weight }) => {
try {
const result = await sendCommandToFigma("set_stroke_color", {
nodeId,
color: { r, g, b, a: a || 1 },
weight: weight || 1
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Set stroke color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1}) with weight ${weight || 1}`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to set stroke color: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Check parameter values are in the correct format",
"Ensure the node supports this operation"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"move_node",
"Move a node to a new position in Figma",
{
nodeId: z.string().describe("The ID of the node to move"),
x: z.number().describe("New X position"),
y: z.number().describe("New Y position")
},
async ({ nodeId, x, y }) => {
try {
const result = await sendCommandToFigma("move_node", { nodeId, x, y });
const typedResult = result;
return {
content: [
{
type: "text",
text: `Moved node "${typedResult.name}" to position (${x}, ${y})`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to move node: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Ensure x and y coordinates are valid numbers",
"Check if the node is locked or constrained"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"clone_node",
"Clone an existing node in Figma",
{
nodeId: z.string().describe("The ID of the node to clone"),
x: z.number().optional().describe("New X position for the clone"),
y: z.number().optional().describe("New Y position for the clone")
},
async ({ nodeId, x, y }) => {
try {
const result = await sendCommandToFigma("clone_node", { nodeId, x, y });
const typedResult = result;
return {
content: [
{
type: "text",
text: `Cloned node "${typedResult.name}" with new ID: ${typedResult.id}${x !== void 0 && y !== void 0 ? ` at position (${x}, ${y})` : ""}`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to clone node: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the source node ID is valid",
"Ensure you have permission to duplicate the node",
"Check if the node type supports cloning"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"resize_node",
"Resize a node in Figma",
{
nodeId: z.string().describe("The ID of the node to resize"),
width: z.number().positive().describe("New width"),
height: z.number().positive().describe("New height")
},
async ({ nodeId, width, height }) => {
try {
const result = await sendCommandToFigma("resize_node", {
nodeId,
width,
height
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Resized node "${typedResult.name}" to width ${width} and height ${height}`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to resize node: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Ensure width and height are positive numbers",
"Check if the node has size constraints"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"delete_node",
"Delete a node from Figma",
{
nodeId: z.string().describe("The ID of the node to delete")
},
async ({ nodeId }) => {
try {
await sendCommandToFigma("delete_node", { nodeId });
return {
content: [
{
type: "text",
text: `Deleted node with ID: ${nodeId}`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Ensure you have permission to delete the node",
"Check if the node is locked or protected"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"delete_multiple_nodes",
"Delete multiple nodes from Figma at once",
{
nodeIds: z.array(z.string()).describe("Array of node IDs to delete")
},
async ({ nodeIds }) => {
try {
const result = await sendCommandToFigma("delete_multiple_nodes", { nodeIds });
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.BATCH_PARTIAL_FAILURE,
`Failed to delete multiple nodes: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Verify all node IDs in the batch are valid",
"Check if some operations may have partially succeeded",
"Ensure you have permissions for all items"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"export_node_as_image",
"Export a node as an image from Figma",
{
nodeId: z.string().describe("The ID of the node to export"),
format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
scale: z.number().positive().optional().describe("Export scale")
},
async ({ nodeId, format, scale }) => {
try {
const result = await sendCommandToFigma("export_node_as_image", {
nodeId,
format: format || "PNG",
scale: scale || 1
});
const typedResult = result;
return {
content: [
{
type: "image",
data: typedResult.imageData,
mimeType: typedResult.mimeType || "image/png"
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to export node as image: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Check if the node is visible and not empty",
"Ensure the export format is supported (PNG, JPG, SVG, PDF)"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_text_content",
"Set the text content of an existing text node in Figma",
{
nodeId: z.string().describe("The ID of the text node to modify"),
text: z.string().describe("New text content")
},
async ({ nodeId, text }) => {
try {
const result = await sendCommandToFigma("set_text_content", {
nodeId,
text
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Updated text content of node "${typedResult.name}" to "${text}"`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to set text content: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Check parameter values are in the correct format",
"Ensure the node supports this operation"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_styles",
"Get all styles from the current Figma document",
{},
async () => {
try {
const result = await sendCommandToFigma("get_styles");
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get styles: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Verify the node ID is correct",
"Check if the node exists in the current document",
"Ensure you have read access"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_local_components",
"Get all local components from the Figma document",
{},
async () => {
try {
const result = await sendCommandToFigma("get_local_components");
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.COMPONENT_NOT_FOUND,
`Failed to get local components: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Verify the node ID is correct",
"Check if the node exists in the current document",
"Ensure you have read access"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_annotations",
"Get all annotations in the current document or specific node",
{
nodeId: z.string().optional().describe("Optional node ID to get annotations for specific node"),
includeCategories: z.boolean().optional().default(true).describe("Whether to include category information")
},
async ({ nodeId, includeCategories }) => {
try {
const result = await sendCommandToFigma("get_annotations", {
nodeId,
includeCategories
});
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to get annotations: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is correct",
"Check if the node exists in the current document",
"Ensure you have read access"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_annotation",
"Create or update an annotation",
{
nodeId: z.string().describe("The ID of the node to annotate"),
annotationId: z.string().optional().describe("The ID of the annotation to update (if updating existing annotation)"),
labelMarkdown: z.string().describe("The annotation text in markdown format"),
categoryId: z.string().optional().describe("The ID of the annotation category"),
properties: z.array(z.object({
type: z.string()
})).optional().describe("Additional properties for the annotation")
},
async ({ nodeId, annotationId, labelMarkdown, categoryId, properties }) => {
try {
const result = await sendCommandToFigma("set_annotation", {
nodeId,
annotationId,
labelMarkdown,
categoryId,
properties
});
return {
content: [
{
type: "text",
text: JSON.stringify(result)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to set annotation: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Check parameter values are in the correct format",
"Ensure the node supports this operation"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_multiple_annotations",
"Set multiple annotations parallelly in a node",
{
nodeId: z.string().describe("The ID of the node containing the elements to annotate"),
annotations: z.array(
z.object({
nodeId: z.string().describe("The ID of the node to annotate"),
labelMarkdown: z.string().describe("The annotation text in markdown format"),
categoryId: z.string().optional().describe("The ID of the annotation category"),
annotationId: z.string().optional().describe("The ID of the annotation to update (if updating existing annotation)"),
properties: z.array(z.object({
type: z.string()
})).optional().describe("Additional properties for the annotation")
})
).describe("Array of annotations to apply")
},
async ({ nodeId, annotations }, extra) => {
try {
if (!annotations || annotations.length === 0) {
return {
content: [
{
type: "text",
text: "No annotations provided"
}
]
};
}
const initialStatus = {
type: "text",
text: `Starting annotation process for ${annotations.length} nodes. This will be processed in batches of 5...`
};
let totalProcessed = 0;
const totalToProcess = annotations.length;
const result = await sendCommandToFigma("set_multiple_annotations", {
nodeId,
annotations
});
const typedResult = result;
const success = typedResult.annotationsApplied && typedResult.annotationsApplied > 0;
const progressText = `
Annotation process completed:
- ${typedResult.annotationsApplied || 0} of ${totalToProcess} successfully applied
- ${typedResult.annotationsFailed || 0} failed
- Processed in ${typedResult.completedInChunks || 1} batches
`;
const detailedResults = typedResult.results || [];
const failedResults = detailedResults.filter((item) => !item.success);
let detailedResponse = "";
if (failedResults.length > 0) {
detailedResponse = `
Nodes that failed:
${failedResults.map(
(item) => `- ${item.nodeId}: ${item.error || "Unknown error"}`
).join("\n")}`;
}
return {
content: [
initialStatus,
{
type: "text",
text: progressText + detailedResponse
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to set multiple annotations: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Check parameter values are in the correct format",
"Ensure the node supports this operation"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"create_component_instance",
"Create an instance of a component in Figma",
{
componentKey: z.string().describe("Key of the component to instantiate"),
x: z.number().describe("X position"),
y: z.number().describe("Y position")
},
async ({ componentKey, x, y }) => {
try {
const result = await sendCommandToFigma("create_component_instance", {
componentKey,
x,
y
});
const typedResult = result;
return {
content: [
{
type: "text",
text: JSON.stringify(typedResult)
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.COMPONENT_NOT_FOUND,
`Failed to create component instance: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Check if the parent node ID is valid",
"Ensure coordinates and dimensions are valid numbers",
"Verify you have edit access to the document"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"get_instance_overrides",
"Get all override properties from a selected component instance. These overrides can be applied to other instances, which will swap them to match the source component.",
{
nodeId: z.string().optional().describe("Optional ID of the component instance to get overrides from. If not provided, currently selected instance will be used.")
},
async ({ nodeId }) => {
try {
const result = await sendCommandToFigma("get_instance_overrides", {
instanceNodeId: nodeId || null
});
const typedResult = result;
return {
content: [
{
type: "text",
text: typedResult.success ? `Successfully got instance overrides: ${typedResult.message}` : `Failed to get instance overrides: ${typedResult.message}`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to copy instance overrides: ${error instanceof Error ? error.message : String(error)}`,
{
nodeId,
suggestions: [
"Verify the node ID is valid",
"Ensure the node is a component instance",
"Check if the instance has any overrides"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_instance_overrides",
"Apply previously copied overrides to selected component instances. Target instances will be swapped to the source component and all copied override properties will be applied.",
{
sourceInstanceId: z.string().describe("ID of the source component instance"),
targetNodeIds: z.array(z.string()).describe("Array of target instance IDs. Currently selected instances will be used.")
},
async ({ sourceInstanceId, targetNodeIds }) => {
try {
const result = await sendCommandToFigma("set_instance_overrides", {
sourceInstanceId,
targetNodeIds: targetNodeIds || []
});
const typedResult = result;
if (typedResult.success) {
const successCount = typedResult.results?.filter((r) => r.success).length || 0;
return {
content: [
{
type: "text",
text: `Successfully applied ${typedResult.totalCount || 0} overrides to ${successCount} instances.`
}
]
};
} else {
return {
content: [
{
type: "text",
text: `Failed to set instance overrides: ${typedResult.message}`
}
]
};
}
} catch (error) {
const errorResponse = createErrorResponse(
ErrorCodes.OPERATION_FAILED,
`Failed to set instance overrides: ${error instanceof Error ? error.message : String(error)}`,
{
suggestions: [
"Verify the node ID is valid",
"Check parameter values are in the correct format",
"Ensure the node supports this operation"
]
}
);
return formatErrorForMCP(errorResponse);
}
}
);
server.tool(
"set_corner_radius",
"Set the corner radius of a node in Figma",
{
nodeId: z.string().describe("The ID of the node to modify"),
radius: z.number().min(0).describe("Corner radius value"),
corners: z.array(z.boolean()).length(4).optional().describe(
"Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]"
)
},
async ({ nodeId, radius, corners }) => {
try {
const result = await sendCommandToFigma("set_corner_radius", {
nodeId,
radius,
corners: corners || [true, true, true, true]
});
const typedResult = result;
return {
content: [
{
type: "text",
text: `Set corner radius of node "${typedResult.name}" to ${radius}px`
}
]
};
} catch (error) {
const errorResponse = createErrorResponse(