Files
kicad-mcp-server/src/tools/board.ts
Jeff Laflamme 6c44273d55 fix(board-view): rebase onto main — fold responseMode + restore jpg
Rebased onto current main (d765bfe). Merged changes:

Board view (kicad-cli, cffi-free):
- Replace pcbnew/PLOT_CONTROLLER + cairosvg with kicad-cli SVG export
  and cffi-free PNG conversion: pymupdf → inkscape → imagemagick chain
- pcbPath optional param with fallback to loaded board
- Restore jpg output via PIL post-processing on PNG bytes
- responseMode inline/file from main's #161: inline returns imageData,
  file writes <board>_2d_view.<ext> and returns filePath
- shutil.which for cross-platform kicad-cli lookup
- Explicit TimeoutExpired catch; errorDetails on all error returns
- Validate fmt strictly; TypeScript returns image type for inline png/jpg

Schematic view (kicad_interface.py):
- Same cffi-free _svg_to_png helper for get_schematic_view and
  get_schematic_view_region (pymupdf → inkscape → imagemagick)

Tests:
- Update test_get_board_2d_view_save_to_file.py to mock subprocess/
  kicad-cli and _svg_to_png instead of PLOT_CONTROLLER/cairosvg
- All 5 responseMode tests pass
2026-05-27 10:50:21 +07:00

493 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, unknown>) => Promise<any>;
/**
* 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 <board>_2d_view.<ext>; 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"}` },
],
};
}
},
);
}