fix: register sync_schematic_to_board + snapshot_project as MCP tools; auto-save board after SWIG mutations; boardPath reload in place_component; add_schematic_connection warns about connect_passthrough; via.GetWidth(F_Cu) for KiCAD 9.0
This commit is contained in:
@@ -367,7 +367,7 @@ class RoutingCommands:
|
|||||||
"y": position["y"],
|
"y": position["y"],
|
||||||
"unit": position["unit"],
|
"unit": position["unit"],
|
||||||
},
|
},
|
||||||
"size": via.GetWidth() / 1000000,
|
"size": via.GetWidth(pcbnew.F_Cu) / 1000000,
|
||||||
"drill": via.GetDrill() / 1000000,
|
"drill": via.GetDrill() / 1000000,
|
||||||
"from_layer": from_layer,
|
"from_layer": from_layer,
|
||||||
"to_layer": to_layer,
|
"to_layer": to_layer,
|
||||||
@@ -950,7 +950,7 @@ class RoutingCommands:
|
|||||||
# Create new via
|
# Create new via
|
||||||
new_via = pcbnew.PCB_VIA(self.board)
|
new_via = pcbnew.PCB_VIA(self.board)
|
||||||
new_via.SetPosition(pcbnew.VECTOR2I(pos.x + offset_x, pos.y + offset_y))
|
new_via.SetPosition(pcbnew.VECTOR2I(pos.x + offset_x, pos.y + offset_y))
|
||||||
new_via.SetWidth(via.GetWidth())
|
new_via.SetWidth(via.GetWidth(pcbnew.F_Cu))
|
||||||
new_via.SetDrill(via.GetDrillValue())
|
new_via.SetDrill(via.GetDrillValue())
|
||||||
new_via.SetViaType(via.GetViaType())
|
new_via.SetViaType(via.GetViaType())
|
||||||
|
|
||||||
|
|||||||
@@ -496,6 +496,11 @@ class KiCADInterface:
|
|||||||
# Get board from the project commands handler
|
# Get board from the project commands handler
|
||||||
self.board = self.project_commands.board
|
self.board = self.project_commands.board
|
||||||
self._update_command_handlers()
|
self._update_command_handlers()
|
||||||
|
elif command in self._BOARD_MUTATING_COMMANDS:
|
||||||
|
# Auto-save after every board mutation via SWIG.
|
||||||
|
# Prevents data loss if Claude hits context limit before
|
||||||
|
# an explicit save_project call.
|
||||||
|
self._auto_save_board()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
@@ -516,6 +521,29 @@ class KiCADInterface:
|
|||||||
"errorDetails": f"{str(e)}\n{traceback_str}",
|
"errorDetails": f"{str(e)}\n{traceback_str}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Board-mutating commands that trigger auto-save on SWIG path
|
||||||
|
_BOARD_MUTATING_COMMANDS = {
|
||||||
|
"place_component", "move_component", "rotate_component", "delete_component",
|
||||||
|
"route_trace", "route_pad_to_pad", "add_via", "delete_trace", "add_net",
|
||||||
|
"add_board_outline", "add_mounting_hole", "add_text", "add_board_text",
|
||||||
|
"add_copper_pour", "refill_zones", "import_svg_logo",
|
||||||
|
"sync_schematic_to_board", "connect_passthrough",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _auto_save_board(self):
|
||||||
|
"""Save board to disk after SWIG mutations.
|
||||||
|
Called automatically after every board-mutating SWIG command so that
|
||||||
|
data is not lost if Claude hits the context limit before save_project.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.board:
|
||||||
|
board_path = self.board.GetFileName()
|
||||||
|
if board_path:
|
||||||
|
pcbnew.SaveBoard(board_path, self.board)
|
||||||
|
logger.debug(f"Auto-saved board to: {board_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Auto-save failed: {e}")
|
||||||
|
|
||||||
def _update_command_handlers(self):
|
def _update_command_handlers(self):
|
||||||
"""Update board reference in all command handlers"""
|
"""Update board reference in all command handlers"""
|
||||||
logger.debug("Updating board reference in command handlers")
|
logger.debug("Updating board reference in command handlers")
|
||||||
@@ -588,11 +616,35 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_place_component(self, params):
|
def _handle_place_component(self, params):
|
||||||
"""Place a component on the PCB, with project-local fp-lib-table support."""
|
"""Place a component on the PCB, with project-local fp-lib-table support.
|
||||||
|
If boardPath is given and differs from the currently loaded board, the
|
||||||
|
board is reloaded from boardPath before placing — prevents silent failures
|
||||||
|
when Claude provides a boardPath that was not yet loaded.
|
||||||
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
board_path = params.get("boardPath")
|
board_path = params.get("boardPath")
|
||||||
if board_path:
|
if board_path:
|
||||||
|
board_path_norm = str(Path(board_path).resolve())
|
||||||
|
current_board_file = (
|
||||||
|
str(Path(self.board.GetFileName()).resolve()) if self.board else ""
|
||||||
|
)
|
||||||
|
if board_path_norm != current_board_file:
|
||||||
|
logger.info(
|
||||||
|
f"boardPath differs from current board — reloading: {board_path}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.board = pcbnew.LoadBoard(board_path)
|
||||||
|
self._update_command_handlers()
|
||||||
|
logger.info("Board reloaded from boardPath")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to reload board from boardPath: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Could not load board from boardPath: {board_path}",
|
||||||
|
"errorDetails": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
project_path = Path(board_path).parent
|
project_path = Path(board_path).parent
|
||||||
if project_path != getattr(self, "_current_project_path", None):
|
if project_path != getattr(self, "_current_project_path", None):
|
||||||
self._current_project_path = project_path
|
self._current_project_path = project_path
|
||||||
|
|||||||
@@ -76,4 +76,23 @@ export function registerProjectTools(server: McpServer, callKicadScript: Functio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Snapshot project tool — saves a named checkpoint as PDF/image
|
||||||
|
server.tool(
|
||||||
|
"snapshot_project",
|
||||||
|
"Save a named checkpoint snapshot of the current project state (renders board to PDF and records step label). Call after completing each major step — e.g. after Step 1 (schematic_ok) and Step 2 (layout_ok). Required by the demo workflow before waiting for user confirmation.",
|
||||||
|
{
|
||||||
|
step: z.string().describe("Step number or identifier, e.g. '1' or '2'"),
|
||||||
|
label: z.string().describe("Short label for this checkpoint, e.g. 'schematic_ok' or 'layout_ok'"),
|
||||||
|
},
|
||||||
|
async (args: { step: string; label: string }) => {
|
||||||
|
const result = await callKicadScript("snapshot_project", args);
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
// Add pin-to-pin connection
|
// Add pin-to-pin connection
|
||||||
server.tool(
|
server.tool(
|
||||||
"add_schematic_connection",
|
"add_schematic_connection",
|
||||||
"Connect two component pins with a wire",
|
"Connect two component pins with a wire. Use this for individual connections between components with different pin roles (e.g. U1.SDA → J3.2). WARNING: Do NOT use this in a loop to wire N passthrough pins — use connect_passthrough instead (single call, cleaner layout, far fewer tokens).",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
||||||
@@ -527,4 +527,20 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sync schematic to PCB board (equivalent to KiCAD F8 / "Update PCB from Schematic")
|
||||||
|
server.tool(
|
||||||
|
"sync_schematic_to_board",
|
||||||
|
"Import the schematic netlist into the PCB board — equivalent to pressing F8 in KiCAD (Tools → Update PCB from Schematic). MUST be called after the schematic is complete and before placing or routing components on the PCB. Without this step, the board has no footprints and no net assignments — place_component and route_pad_to_pad will produce an empty, unroutable board.",
|
||||||
|
{
|
||||||
|
schematicPath: z.string().describe("Absolute path to the .kicad_sch schematic file"),
|
||||||
|
boardPath: z.string().describe("Absolute path to the .kicad_pcb board file"),
|
||||||
|
},
|
||||||
|
async (args: { schematicPath: string; boardPath: string }) => {
|
||||||
|
const result = await callKicadScript("sync_schematic_to_board", args);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user