/** * Board management tools for KiCAD MCP server * * These tools handle board setup, layer management, and board properties */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { logger } from "../logger.js"; // Command function type for KiCAD script calls type CommandFunction = (command: string, params: Record) => Promise; /** * Register board management tools with the MCP server * * @param server MCP server instance * @param callKicadScript Function to call KiCAD script commands */ export function registerBoardTools(server: McpServer, callKicadScript: CommandFunction): void { logger.info("Registering board management tools"); // ------------------------------------------------------ // Set Board Size Tool // ------------------------------------------------------ server.tool( "set_board_size", "Set the PCB board dimensions (width and height) in the specified unit.", { width: z.number().describe("Board width"), height: z.number().describe("Board height"), unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"), }, async ({ width, height, unit }) => { logger.debug(`Setting board size to ${width}x${height} ${unit}`); const result = await callKicadScript("set_board_size", { width, height, unit, }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Add Layer Tool // ------------------------------------------------------ server.tool( "add_layer", "Add a new copper or technical layer to the PCB stackup.", { name: z.string().describe("Layer name"), type: z.enum(["copper", "technical", "user", "signal"]).describe("Layer type"), position: z.enum(["top", "bottom", "inner"]).describe("Layer position"), number: z.number().optional().describe("Layer number (for inner layers)"), }, async ({ name, type, position, number }) => { logger.debug(`Adding ${type} layer: ${name}`); const result = await callKicadScript("add_layer", { name, type, position, number, }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Set Active Layer Tool // ------------------------------------------------------ server.tool( "set_active_layer", "Set the currently active PCB layer by name (e.g. F.Cu, B.Cu).", { layer: z.string().describe("Layer name to set as active"), }, async ({ layer }) => { logger.debug(`Setting active layer to: ${layer}`); const result = await callKicadScript("set_active_layer", { layer }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Get Board Info Tool // ------------------------------------------------------ server.tool( "get_board_info", "Retrieve general information about the current PCB board (dimensions, layer count, DRC status).", {}, async () => { logger.debug("Getting board information"); const result = await callKicadScript("get_board_info", {}); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Get Layer List Tool // ------------------------------------------------------ server.tool( "get_layer_list", "Return the list of all layers defined in the current PCB board.", {}, async () => { logger.debug("Getting layer list"); const result = await callKicadScript("get_layer_list", {}); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Add Board Outline Tool // ------------------------------------------------------ server.tool( "add_board_outline", "Draw the PCB board outline (Edge.Cuts layer) as a rectangle, rounded rectangle, circle or polygon.", { shape: z .enum(["rectangle", "circle", "polygon", "rounded_rectangle"]) .describe("Shape of the outline"), params: z .object({ // For rectangle / rounded_rectangle width: z.number().optional().describe("Width of rectangle"), height: z.number().optional().describe("Height of rectangle"), cornerRadius: z.number().optional().describe("Corner radius for rounded_rectangle (mm)"), // For circle radius: z.number().optional().describe("Radius of circle"), // For polygon points: z .array( z.object({ x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), }), ) .optional() .describe("Points of polygon"), // Position: top-left corner for rectangles/rounded_rectangle, center for circle x: z.number().describe("X coordinate of top-left corner for rectangles (default: 0)"), y: z.number().describe("Y coordinate of top-left corner for rectangles (default: 0)"), unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"), }) .describe("Parameters for the outline shape"), }, async ({ shape, params }) => { logger.debug(`Adding ${shape} board outline`); // Pass x/y as-is to Python; outline.py treats them as top-left corner // and computes the center internally (center = x + width/2, y + height/2). const result = await callKicadScript("add_board_outline", { shape, ...params, }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Add Mounting Hole Tool // ------------------------------------------------------ server.tool( "add_mounting_hole", "Place a mounting hole (NPTH or PTH) at the specified position on the PCB.", { position: z .object({ x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"), }) .describe("Position of the mounting hole"), diameter: z.number().describe("Diameter of the hole"), padDiameter: z.number().optional().describe("Optional diameter of the pad around the hole"), }, async ({ position, diameter, padDiameter }) => { logger.debug(`Adding mounting hole at (${position.x},${position.y}) ${position.unit}`); const result = await callKicadScript("add_mounting_hole", { position, diameter, padDiameter, }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Add Text Tool // ------------------------------------------------------ server.tool( "add_board_text", "Add a text label to a PCB layer (e.g. silkscreen, fab, courtyard).", { text: z.string().describe("Text content"), position: z .object({ x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"), }) .describe("Position of the text"), layer: z.string().describe("Layer to place the text on"), size: z.number().describe("Text size"), thickness: z.number().optional().describe("Line thickness"), rotation: z.number().optional().describe("Rotation angle in degrees"), style: z.enum(["normal", "italic", "bold"]).optional().describe("Text style"), }, async ({ text, position, layer, size, thickness, rotation, style }) => { logger.debug(`Adding text "${text}" at (${position.x},${position.y}) ${position.unit}`); const result = await callKicadScript("add_board_text", { text, position, layer, size, thickness, rotation, style, }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Add Zone Tool // ------------------------------------------------------ server.tool( "add_zone", "Create a copper fill zone (pour) on a PCB layer for a specified net.", { layer: z.string().describe("Layer for the zone"), net: z.string().describe("Net name for the zone"), points: z .array( z.object({ x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"), }), ) .describe("Points defining the zone outline"), unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"), clearance: z.number().optional().describe("Clearance value"), minWidth: z.number().optional().describe("Minimum width"), padConnection: z .enum(["thermal", "solid", "none"]) .optional() .describe("Pad connection type"), }, async ({ layer, net, points, unit, clearance, minWidth, padConnection }) => { logger.debug(`Adding zone on layer ${layer} for net ${net}`); const result = await callKicadScript("add_zone", { layer, net, points, unit, clearance, minWidth, padConnection, }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Get Board Extents Tool // ------------------------------------------------------ server.tool( "get_board_extents", "Return the bounding box (min/max X and Y) of all objects on the current PCB board.", { unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit of measurement for the result"), }, async ({ unit }) => { logger.debug("Getting board extents"); const result = await callKicadScript("get_board_extents", { unit }); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }, ); // ------------------------------------------------------ // Get Board 2D View Tool // ------------------------------------------------------ server.tool( "get_board_2d_view", [ "Render a 2D image of the PCB using kicad-cli. Returns PNG, JPG, or SVG.", "Use layers to filter — e.g. [\"F.Cu\",\"B.Cu\",\"Edge.Cuts\"] for copper + outline only.", "Use responseMode to choose delivery:", ' "inline" (default) — PNG/JPG rendered as an image visible to Claude; SVG returned as text.', ' "file" — image written next to the .kicad_pcb as _2d_view.; filePath is returned.', "Use file mode for large boards to avoid MCP message-size limits.", ].join(" "), { pcbPath: z.string().optional().describe("Absolute path to the .kicad_pcb file. Falls back to the currently loaded board if omitted."), layers: z.array(z.string()).optional().describe("Layer names to include, e.g. [\"F.Cu\",\"B.Cu\",\"Edge.Cuts\"]. Omit for all layers."), width: z.number().optional().describe("Output image width in pixels (default: 1600)"), height: z.number().optional().describe("Output image height in pixels (default: 1200)"), format: z.enum(["png", "jpg", "svg"]).optional().describe("Output format (default: png)"), responseMode: z .enum(["inline", "file"]) .optional() .describe( '"inline" (default): image returned directly; "file": written to disk, filePath returned', ), }, async ({ pcbPath, layers, width, height, format, responseMode }) => { logger.debug("Getting 2D board view"); const result = await callKicadScript("get_board_2d_view", { pcbPath, layers, width, height, format, responseMode, }); if (result.success) { // file mode — just return the path as text if (responseMode === "file" || result.filePath) { return { content: [{ type: "text" as const, text: result.message || result.filePath }], }; } // inline svg (or fallback svg) — return as text, prepend any notice if (result.format === "svg") { const parts: { type: "text"; text: string }[] = []; if (result.message) parts.push({ type: "text" as const, text: result.message }); parts.push({ type: "text" as const, text: Buffer.from(result.imageData, "base64").toString("utf-8") }); return { content: parts }; } // inline png/jpg — return as renderable image return { content: [ { type: "image" as const, data: result.imageData, mimeType: result.format === "jpg" ? "image/jpeg" : "image/png", }, ], }; } return { content: [ { type: "text" as const, text: `Failed to get board view: ${result.message || result.errorDetails || "Unknown error"}`, }, ], isError: true, }; }, ); logger.info("Board management tools registered"); // Import SVG logo onto PCB layer (silkscreen) server.tool( "import_svg_logo", "Imports an SVG file as filled graphic polygons onto a KiCAD PCB layer (default F.SilkS / front silkscreen). Curves are linearised automatically. Ideal for placing a company or project logo on the board.", { pcbPath: z.string().describe("Path to the .kicad_pcb file"), svgPath: z.string().describe("Path to the SVG logo file"), x: z.number().describe("X position of the logo top-left corner in mm"), y: z.number().describe("Y position of the logo top-left corner in mm"), width: z .number() .describe("Target width of the logo in mm (height is scaled to preserve aspect ratio)"), layer: z .string() .optional() .describe("PCB layer name, e.g. F.SilkS or B.SilkS (default: F.SilkS)"), strokeWidth: z .number() .optional() .describe("Outline stroke width in mm (0 = no outline, default 0)"), filled: z.boolean().optional().describe("Fill polygons with solid colour (default true)"), }, async (args: { pcbPath: string; svgPath: string; x: number; y: number; width: number; layer?: string; strokeWidth?: number; filled?: boolean; }) => { const result = await callKicadScript("import_svg_logo", args); if (result.success) { return { content: [ { type: "text", text: [ result.message, `Polygons: ${result.polygon_count}`, `Size: ${result.logo_width_mm?.toFixed(2)} × ${result.logo_height_mm?.toFixed(2)} mm`, `Layer: ${result.layer}`, ].join("\n"), }, ], }; } else { return { content: [ { type: "text", text: `SVG import failed: ${result.message || "Unknown error"}` }, ], }; } }, ); }