feat: add snap_to_grid schematic tool

Adds a new MCP tool that snaps wire endpoints, junction positions, and
net label coordinates to the nearest grid point (default 2.54 mm). Off-grid
coordinates cause wires that appear visually connected to fail ERC checks
because KiCAD uses exact IU integer matching internally; this tool eliminates
that class of error before running ERC.

- python/commands/schematic_snap.py: core snap logic with in-place sexp mutation
- python/kicad_interface.py: route + handler
- python/schemas/tool_schemas.py: JSON schema (gridSize, elements params)
- src/tools/schematic.ts: TypeScript MCP tool registration
- tests/test_snap_to_grid.py: 20 unit + integration tests (all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-04-12 15:03:35 +01:00
parent 58bb08a252
commit 9387683368
5 changed files with 539 additions and 0 deletions

View File

@@ -1392,4 +1392,36 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
};
},
);
// Snap schematic elements to grid
server.tool(
"snap_to_grid",
"Snap schematic element coordinates to the nearest grid point. " +
"KiCAD uses exact integer matching for connectivity, so off-grid coordinates cause wires " +
"that look connected to fail ERC checks. " +
"Modifies the .kicad_sch file in place. Does not require the KiCAD UI to be running.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
gridSize: z
.number()
.optional()
.describe("Grid spacing in mm (default: 2.54 — standard KiCAD schematic grid)"),
elements: z
.array(z.enum(["wires", "junctions", "labels", "components"]))
.optional()
.describe(
'Element types to snap (default: ["wires", "junctions", "labels"]). ' +
'"components" is opt-in — moving a component without re-routing wires creates new mismatches.',
),
},
async (args: { schematicPath: string; gridSize?: number; elements?: string[] }) => {
const result = await callKicadScript("snap_to_grid", args);
if (result.success) {
return { content: [{ type: "text", text: result.message }] };
}
return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
};
},
);
}