From 88226d567bbd22fae28b3bf02760ebca933fe01b Mon Sep 17 00:00:00 2001 From: Gavin Colonese Date: Fri, 12 Jun 2026 10:54:11 -0400 Subject: [PATCH] feat: add export_ipc2581 tool (kicad-cli, with BOM column mapping) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New MCP tool `export_ipc2581` wrapping `kicad-cli pcb export ipc2581`: precision, compression, standard version, units, and the five --bom-col-* field mappings (internal id, mfg P/N, mfg, distributor P/N, distributor) that control the BOM part data embedded in the IPC-2581 file — directly useful for MES/assembly imports that need a real internal P/N rather than the description. Self-contained interface handler reading the saved .kicad_pcb. Co-Authored-By: Claude Opus 4.8 (1M context) --- python/kicad_interface.py | 78 +++++++++++++++++++++++++++++++++++++++ src/tools/export.ts | 50 +++++++++++++++++++++++++ src/tools/registry.ts | 1 + 3 files changed, 129 insertions(+) diff --git a/python/kicad_interface.py b/python/kicad_interface.py index a998cfe..fdc680d 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -589,6 +589,7 @@ class KiCADInterface: "export_netlist": self._handle_export_netlist, "export_gerbers": self._handle_export_gerbers, "export_drill": self._handle_export_drill, + "export_ipc2581": self._handle_export_ipc2581, "generate_netlist": self._handle_generate_netlist, "sync_schematic_to_board": self._handle_sync_schematic_to_board, "list_schematic_libraries": self._handle_list_schematic_libraries, @@ -4685,6 +4686,83 @@ class KiCADInterface: logger.error(f"Error exporting drill files: {e}") return {"success": False, "message": str(e)} + def _handle_export_ipc2581(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Export the PCB in IPC-2581 format via kicad-cli (`pcb export ipc2581`). + + IPC-2581 is a single-file MES/CAD interchange format carrying placement, + nets, and BOM part data inline. The bom-col-* params map schematic fields + to the BOM columns embedded in the file. Reads the saved .kicad_pcb. + """ + import subprocess + + logger.info("Exporting IPC-2581 via kicad-cli") + try: + board_path = params.get("boardPath") or self._current_board_path() + output_path = params.get("outputPath") + + if not board_path: + return { + "success": False, + "message": "boardPath is required (no current board could be resolved)", + } + if not os.path.exists(board_path): + return {"success": False, "message": f"Board not found: {board_path}"} + if not output_path: + return {"success": False, "message": "outputPath is required"} + + kicad_cli = self._find_kicad_cli_static() + if not kicad_cli: + return {"success": False, "message": "kicad-cli not found in PATH"} + + output_path = os.path.abspath(os.path.expanduser(output_path)) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + cmd = [kicad_cli, "pcb", "export", "ipc2581", "--output", output_path] + + if params.get("drawingSheet"): + cmd += ["--drawing-sheet", params["drawingSheet"]] + for kv in params.get("defineVar", []) or []: + cmd += ["--define-var", kv] + + value_map = { + "precision": "--precision", + "version": "--version", + "units": "--units", + "bomColIntId": "--bom-col-int-id", + "bomColMfgPn": "--bom-col-mfg-pn", + "bomColMfg": "--bom-col-mfg", + "bomColDistPn": "--bom-col-dist-pn", + "bomColDist": "--bom-col-dist", + } + for key, flag in value_map.items(): + val = params.get(key) + if val is not None and val != "": + cmd += [flag, str(val)] + + if params.get("compress"): + cmd.append("--compress") + + cmd.append(board_path) + + logger.info(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True, timeout=180) + + if result.returncode != 0: + return { + "success": False, + "message": f"kicad-cli failed (exit {result.returncode}): " + f"{result.stderr.strip()}", + } + return {"success": True, "outputPath": output_path} + + except FileNotFoundError: + return {"success": False, "message": "kicad-cli not found in PATH"} + except subprocess.TimeoutExpired: + return {"success": False, "message": "kicad-cli timed out after 180 seconds"} + except Exception as e: + logger.error(f"Error exporting IPC-2581: {e}") + return {"success": False, "message": str(e)} + def _handle_generate_netlist(self, params: Dict[str, Any]) -> Dict[str, Any]: """Generate netlist from schematic and return structured JSON. diff --git a/src/tools/export.ts b/src/tools/export.ts index 39e9573..911d548 100644 --- a/src/tools/export.ts +++ b/src/tools/export.ts @@ -449,5 +449,55 @@ export function registerExportTools(server: McpServer, callKicadScript: CommandF }, ); + // ------------------------------------------------------ + // Export IPC-2581 Tool (kicad-cli) + // ------------------------------------------------------ + server.tool( + "export_ipc2581", + "Export the PCB in IPC-2581 format via kicad-cli. Single-file MES/CAD interchange carrying placement, nets and BOM part data inline. The bomCol* params map schematic fields to the embedded BOM columns (e.g. internal P/N, manufacturer P/N) — useful for assembly/MES imports. Reads the last SAVED state of the .kicad_pcb.", + { + outputPath: z.string().describe("Output .xml file path"), + boardPath: z.string().optional().describe("Path to the .kicad_pcb (default: current board)"), + drawingSheet: z.string().optional().describe("Path to a drawing sheet override"), + defineVar: z + .array(z.string()) + .optional() + .describe("Project variable overrides as 'KEY=VALUE' strings"), + precision: z.number().optional().describe("Coordinate precision (default 6)"), + compress: z.boolean().optional().describe("Compress the output"), + version: z.string().optional().describe("IPC-2581 standard version (default 'C')"), + units: z.enum(["mm", "in"]).optional().describe("Units (default mm)"), + bomColIntId: z + .string() + .optional() + .describe("Schematic field to use for the BOM Internal Id column"), + bomColMfgPn: z + .string() + .optional() + .describe("Schematic field to use for the BOM Manufacturer Part Number column"), + bomColMfg: z + .string() + .optional() + .describe("Schematic field to use for the BOM Manufacturer column"), + bomColDistPn: z + .string() + .optional() + .describe("Schematic field to use for the BOM Distributor Part Number column"), + bomColDist: z.string().optional().describe("Value to insert into the BOM Distributor column"), + }, + async (args) => { + logger.debug(`Exporting IPC-2581 to: ${args.outputPath}`); + const result = await callKicadScript("export_ipc2581", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + logger.info("Export tools registered"); } diff --git a/src/tools/registry.ts b/src/tools/registry.ts index 6e4e4da..d5742c6 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -60,6 +60,7 @@ export const toolCategories: ToolCategory[] = [ "export_gerber", "export_gerbers", "export_drill", + "export_ipc2581", "export_pdf", "export_svg", "export_3d",