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:
@@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user