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
This commit is contained in:
Jeff Laflamme
2026-05-27 10:50:21 +07:00
parent d765bfec78
commit 6c44273d55
5 changed files with 349 additions and 188 deletions

View File

@@ -363,27 +363,30 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
server.tool(
"get_board_2d_view",
[
"Render a 2D image of the current PCB board and return it as PNG, JPG or SVG.",
"Use responseMode to choose how the image is delivered:",
' "inline" (default) — base64-encoded bytes returned in imageData; works well for small boards.',
"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 hitting MCP message-size limits.",
"Use file mode for large boards to avoid MCP message-size limits.",
].join(" "),
{
layers: z.array(z.string()).optional().describe("Optional array of layer names to include"),
width: z.number().optional().describe("Optional width of the image in pixels"),
height: z.number().optional().describe("Optional height of the image in pixels"),
format: z.enum(["png", "jpg", "svg"]).optional().describe("Image format"),
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(
'How to return the image: "inline" (default) returns base64 imageData; "file" writes to disk and returns filePath',
'"inline" (default): image returned directly; "file": written to disk, filePath returned',
),
},
async ({ layers, width, height, format, responseMode }) => {
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,
@@ -391,13 +394,39 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
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",
text: JSON.stringify(result),
type: "text" as const,
text: `Failed to get board view: ${result.message || result.errorDetails || "Unknown error"}`,
},
],
isError: true,
};
},
);