From b37ca6a225e9dbe4f954990077c53f4fdfb9707e Mon Sep 17 00:00:00 2001 From: Gavin Colonese Date: Fri, 12 Jun 2026 10:53:45 -0400 Subject: [PATCH] feat: add export_drill tool (kicad-cli, full drill option set) New MCP tool `export_drill` wrapping `kicad-cli pcb export drill`: format (excellon/gerber), drill origin, Excellon zero-suppression and oval formats, units, mirror-Y, minimal header, separate PTH/NPTH files, drill map generation + map format, and Gerber precision. Self-contained interface handler reading the saved .kicad_pcb. Co-Authored-By: Claude Opus 4.8 (1M context) --- python/kicad_interface.py | 85 +++++++++++++++++++++++++++++++++++++++ src/tools/export.ts | 56 ++++++++++++++++++++++++++ src/tools/registry.ts | 1 + 3 files changed, 142 insertions(+) diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 0ecb4fd..a998cfe 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -588,6 +588,7 @@ class KiCADInterface: "run_erc": self._handle_run_erc, "export_netlist": self._handle_export_netlist, "export_gerbers": self._handle_export_gerbers, + "export_drill": self._handle_export_drill, "generate_netlist": self._handle_generate_netlist, "sync_schematic_to_board": self._handle_sync_schematic_to_board, "list_schematic_libraries": self._handle_list_schematic_libraries, @@ -4600,6 +4601,90 @@ class KiCADInterface: logger.error(f"Error exporting Gerbers: {e}") return {"success": False, "message": str(e)} + def _handle_export_drill(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Generate drill files via kicad-cli (`pcb export drill`). + + Exposes the full Excellon/Gerber drill option set. Reads the saved + .kicad_pcb on disk. + """ + import subprocess + + logger.info("Exporting drill files via kicad-cli") + try: + board_path = params.get("boardPath") or self._current_board_path() + output_dir = params.get("outputDir") + + 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_dir: + return {"success": False, "message": "outputDir is required"} + + kicad_cli = self._find_kicad_cli_static() + if not kicad_cli: + return {"success": False, "message": "kicad-cli not found in PATH"} + + output_dir = os.path.abspath(os.path.expanduser(output_dir)) + # kicad-cli drill requires the output dir path to end with a separator + os.makedirs(output_dir, exist_ok=True) + + cmd = [kicad_cli, "pcb", "export", "drill", "--output", output_dir + os.sep] + + # Valued options (omit to use kicad-cli defaults) + value_map = { + "format": "--format", + "drillOrigin": "--drill-origin", + "excellonZerosFormat": "--excellon-zeros-format", + "excellonOvalFormat": "--excellon-oval-format", + "excellonUnits": "--excellon-units", + "mapFormat": "--map-format", + "gerberPrecision": "--gerber-precision", + } + for key, flag in value_map.items(): + val = params.get(key) + if val is not None: + cmd += [flag, str(val)] + + # Boolean flags + flag_map = { + "excellonMirrorY": "--excellon-mirror-y", + "excellonMinHeader": "--excellon-min-header", + "excellonSeparateTh": "--excellon-separate-th", + "generateMap": "--generate-map", + } + for key, flag in flag_map.items(): + if params.get(key): + cmd.append(flag) + + cmd.append(board_path) + + logger.info(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + + if result.returncode != 0: + return { + "success": False, + "message": f"kicad-cli failed (exit {result.returncode}): " + f"{result.stderr.strip()}", + } + + files = sorted( + f for f in os.listdir(output_dir) if os.path.isfile(os.path.join(output_dir, f)) + ) + return {"success": True, "outputDir": output_dir, "files": files} + + except FileNotFoundError: + return {"success": False, "message": "kicad-cli not found in PATH"} + except subprocess.TimeoutExpired: + return {"success": False, "message": "kicad-cli timed out after 120 seconds"} + except Exception as e: + logger.error(f"Error exporting drill files: {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 a03588d..39e9573 100644 --- a/src/tools/export.ts +++ b/src/tools/export.ts @@ -393,5 +393,61 @@ export function registerExportTools(server: McpServer, callKicadScript: CommandF }, ); + // ------------------------------------------------------ + // Export Drill Files Tool (kicad-cli) + // ------------------------------------------------------ + server.tool( + "export_drill", + "Generate drill files for a PCB via kicad-cli, exposing the full Excellon/Gerber drill option set (format, drill origin, zero suppression, oval format, units, mirror-Y, minimal header, separate PTH/NPTH files, drill map + map format). Reads the last SAVED state of the .kicad_pcb.", + { + outputDir: z.string().describe("Output directory for the drill files"), + boardPath: z.string().optional().describe("Path to the .kicad_pcb (default: current board)"), + format: z + .enum(["excellon", "gerber"]) + .optional() + .describe("Drill file format (default excellon)"), + drillOrigin: z + .enum(["absolute", "plot"]) + .optional() + .describe("Drill coordinate origin (default absolute)"), + excellonZerosFormat: z + .enum(["decimal", "suppressleading", "suppresstrailing", "keep"]) + .optional() + .describe("Excellon zero-suppression format (default decimal)"), + excellonOvalFormat: z + .enum(["route", "alternate"]) + .optional() + .describe("Excellon oval hole format (default alternate)"), + excellonUnits: z.enum(["in", "mm"]).optional().describe("Excellon output units (default mm)"), + excellonMirrorY: z.boolean().optional().describe("Mirror the Y axis (Excellon)"), + excellonMinHeader: z.boolean().optional().describe("Use a minimal Excellon header"), + excellonSeparateTh: z + .boolean() + .optional() + .describe("Generate independent files for NPTH and PTH holes"), + generateMap: z.boolean().optional().describe("Generate a drill map / summary file"), + mapFormat: z + .enum(["pdf", "gerberx2", "ps", "dxf", "svg"]) + .optional() + .describe("Drill map format when generateMap is set (default pdf)"), + gerberPrecision: z + .number() + .optional() + .describe("Gerber coordinate precision (5 or 6) when format=gerber"), + }, + async (args) => { + logger.debug(`Exporting drill files to: ${args.outputDir}`); + const result = await callKicadScript("export_drill", 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 482ede6..6e4e4da 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -59,6 +59,7 @@ export const toolCategories: ToolCategory[] = [ tools: [ "export_gerber", "export_gerbers", + "export_drill", "export_pdf", "export_svg", "export_3d",