modification-tools.ts•13.4 kB
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { sendCommandToFigma } from "../utils/websocket";
import { applyColorDefaults, applyDefault, FIGMA_DEFAULTS } from "../utils/defaults";
import { Color } from "../types/color";
/**
* Register modification tools to the MCP server
* This module contains tools for modifying existing elements in Figma
* @param server - The MCP server instance
*/
export function registerModificationTools(server: McpServer): void {
// Set Fill Color Tool
server.tool(
"set_fill_color",
"Set the fill color of a node in Figma. Alpha component defaults to 1 (fully opaque) if not specified. Use alpha 0 for fully transparent.",
{
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, defaults to 1 if not specified)"),
},
async ({ nodeId, r, g, b, a }) => {
try {
// Additional validation: Ensure RGB values are provided (they should not be undefined)
if (r === undefined || g === undefined || b === undefined) {
throw new Error("RGB components (r, g, b) are required and cannot be undefined");
}
// Apply default values safely - preserves opacity 0 for transparency
const colorInput: Color = { r, g, b, a };
const colorWithDefaults = applyColorDefaults(colorInput);
const result = await sendCommandToFigma("set_fill_color", {
nodeId,
color: colorWithDefaults,
});
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Set fill color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${colorWithDefaults.a})`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting fill color: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Set Stroke Color Tool
server.tool(
"set_stroke_color",
"Set the stroke color of a node in Figma (defaults: opacity 1, weight 1)",
{
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)"),
strokeWeight: z.number().min(0).optional().describe("Stroke weight >= 0)"),
},
async ({ nodeId, r, g, b, a, strokeWeight }) => {
try {
if (r === undefined || g === undefined || b === undefined) {
throw new Error("RGB components (r, g, b) are required and cannot be undefined");
}
const colorInput: Color = { r, g, b, a };
const colorWithDefaults = applyColorDefaults(colorInput);
const strokeWeightWithDefault = applyDefault(strokeWeight, FIGMA_DEFAULTS.stroke.weight);
const result = await sendCommandToFigma("set_stroke_color", {
nodeId,
color: colorWithDefaults,
strokeWeight: strokeWeightWithDefault,
});
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Set stroke color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${colorWithDefaults.a}) with weight ${strokeWeightWithDefault}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting stroke color: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Move Node Tool
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 as { name: string };
return {
content: [
{
type: "text",
text: `Moved node "${typedResult.name}" to position (${x}, ${y})`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error moving node: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Resize Node Tool
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 as { name: string };
return {
content: [
{
type: "text",
text: `Resized node "${typedResult.name}" to width ${width} and height ${height}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error resizing node: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Delete Node Tool
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) {
return {
content: [
{
type: "text",
text: `Error deleting node: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Set Corner Radius Tool
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 as { name: string };
return {
content: [
{
type: "text",
text: `Set corner radius of node "${typedResult.name}" to ${radius}px`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting corner radius: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Auto Layout Tool
server.tool(
"set_auto_layout",
"Configure auto layout properties for a node in Figma",
{
nodeId: z.string().describe("The ID of the node to configure auto layout"),
layoutMode: z.enum(["HORIZONTAL", "VERTICAL", "NONE"]).describe("Layout direction"),
paddingTop: z.number().optional().describe("Top padding in pixels"),
paddingBottom: z.number().optional().describe("Bottom padding in pixels"),
paddingLeft: z.number().optional().describe("Left padding in pixels"),
paddingRight: z.number().optional().describe("Right padding in pixels"),
itemSpacing: z.number().optional().describe("Spacing between items in pixels"),
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"),
layoutWrap: z.enum(["WRAP", "NO_WRAP"]).optional().describe("Whether items wrap to new lines"),
strokesIncludedInLayout: z.boolean().optional().describe("Whether strokes are included in layout calculations")
},
async ({ nodeId, layoutMode, paddingTop, paddingBottom, paddingLeft, paddingRight,
itemSpacing, primaryAxisAlignItems, counterAxisAlignItems, layoutWrap, strokesIncludedInLayout }) => {
try {
const result = await sendCommandToFigma("set_auto_layout", {
nodeId,
layoutMode,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
itemSpacing,
primaryAxisAlignItems,
counterAxisAlignItems,
layoutWrap,
strokesIncludedInLayout
});
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Applied auto layout to node "${typedResult.name}" with mode: ${layoutMode}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting auto layout: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Set Effects Tool
server.tool(
"set_effects",
"Set the visual effects of a node in Figma",
{
nodeId: z.string().describe("The ID of the node to modify"),
effects: z.array(
z.object({
type: z.enum(["DROP_SHADOW", "INNER_SHADOW", "LAYER_BLUR", "BACKGROUND_BLUR"]).describe("Effect type"),
color: z.object({
r: z.number().min(0).max(1).describe("Red (0-1)"),
g: z.number().min(0).max(1).describe("Green (0-1)"),
b: z.number().min(0).max(1).describe("Blue (0-1)"),
a: z.number().min(0).max(1).describe("Alpha (0-1)")
}).optional().describe("Effect color (for shadows)"),
offset: z.object({
x: z.number().describe("X offset"),
y: z.number().describe("Y offset")
}).optional().describe("Offset (for shadows)"),
radius: z.number().optional().describe("Effect radius"),
spread: z.number().optional().describe("Shadow spread (for shadows)"),
visible: z.boolean().optional().describe("Whether the effect is visible"),
blendMode: z.string().optional().describe("Blend mode")
})
).describe("Array of effects to apply")
},
async ({ nodeId, effects }) => {
try {
const result = await sendCommandToFigma("set_effects", {
nodeId,
effects
});
const typedResult = result as { name: string, effects: any[] };
return {
content: [
{
type: "text",
text: `Successfully applied ${effects.length} effect(s) to node "${typedResult.name}"`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting effects: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Set Effect Style ID Tool
server.tool(
"set_effect_style_id",
"Apply an effect style to a node in Figma",
{
nodeId: z.string().describe("The ID of the node to modify"),
effectStyleId: z.string().describe("The ID of the effect style to apply")
},
async ({ nodeId, effectStyleId }) => {
try {
const result = await sendCommandToFigma("set_effect_style_id", {
nodeId,
effectStyleId
});
const typedResult = result as { name: string, effectStyleId: string };
return {
content: [
{
type: "text",
text: `Successfully applied effect style to node "${typedResult.name}"`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting effect style: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
}