feat: add schematic analysis tools (read-only)
Add five new read-only schematic analysis MCP tools: - get_schematic_view_region: export cropped schematic region as PNG/SVG - find_unconnected_pins: list pins with no wire/label/power connection - find_overlapping_elements: detect duplicate symbols, stacked labels, collinear wire overlaps - get_elements_in_region: list all symbols/wires/labels in a bounding box - check_wire_collisions: detect wires passing through component bodies Includes Python handler dispatch, tool schemas, TypeScript server bindings, the schematic_analysis command module, and a full test suite (28 tests passing). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1092,4 +1092,187 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// Schematic Analysis Tools (read-only)
|
||||
// ============================================================
|
||||
|
||||
// Get a zoomed view of a schematic region
|
||||
server.tool(
|
||||
"get_schematic_view_region",
|
||||
"Export a cropped region of the schematic as an image (PNG or SVG). Specify bounding box coordinates in schematic mm. Useful for zooming into a specific area to inspect wiring or layout.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
x1: z.number().describe("Left X coordinate of the region in mm"),
|
||||
y1: z.number().describe("Top Y coordinate of the region in mm"),
|
||||
x2: z.number().describe("Right X coordinate of the region in mm"),
|
||||
y2: z.number().describe("Bottom Y coordinate of the region in mm"),
|
||||
format: z.enum(["png", "svg"]).optional().describe("Output image format (default: png)"),
|
||||
width: z.number().optional().describe("Output image width in pixels (default: 800)"),
|
||||
height: z.number().optional().describe("Output image height in pixels (default: 600)"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
x1: number; y1: number; x2: number; y2: number;
|
||||
format?: string; width?: number; height?: number;
|
||||
}) => {
|
||||
const result = await callKicadScript("get_schematic_view_region", args);
|
||||
if (result.success && result.imageData) {
|
||||
if (result.format === "svg") {
|
||||
return { content: [{ type: "text", text: result.imageData }] };
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "image",
|
||||
data: result.imageData,
|
||||
mimeType: "image/png",
|
||||
}],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Find unconnected pins
|
||||
server.tool(
|
||||
"find_unconnected_pins",
|
||||
"List all component pins in the schematic that have no wire, label, or power symbol touching them. Useful for checking connectivity before running ERC.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
},
|
||||
async (args: { schematicPath: string }) => {
|
||||
const result = await callKicadScript("find_unconnected_pins", args);
|
||||
if (result.success) {
|
||||
const pins: any[] = result.unconnectedPins || [];
|
||||
const lines = [`Found ${pins.length} unconnected pin(s):`];
|
||||
pins.slice(0, 50).forEach((p: any) => {
|
||||
lines.push(` ${p.reference} pin ${p.pinNumber} (${p.pinName}) @ (${p.position.x}, ${p.position.y})`);
|
||||
});
|
||||
if (pins.length > 50) lines.push(` ... and ${pins.length - 50} more`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Find overlapping elements
|
||||
server.tool(
|
||||
"find_overlapping_elements",
|
||||
"Detect spatially overlapping symbols, wires, and labels in the schematic. Finds duplicate power symbols at the same position, collinear overlapping wires, and labels stacked on top of each other.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
tolerance: z.number().optional().describe("Distance in mm below which elements are considered overlapping (default: 0.5)"),
|
||||
},
|
||||
async (args: { schematicPath: string; tolerance?: number }) => {
|
||||
const result = await callKicadScript("find_overlapping_elements", args);
|
||||
if (result.success) {
|
||||
const lines = [`Found ${result.totalOverlaps} overlap(s):`];
|
||||
const syms: any[] = result.overlappingSymbols || [];
|
||||
const lbls: any[] = result.overlappingLabels || [];
|
||||
const wires: any[] = result.overlappingWires || [];
|
||||
if (syms.length) {
|
||||
lines.push(`\nOverlapping symbols (${syms.length}):`);
|
||||
syms.slice(0, 20).forEach((o: any) => {
|
||||
lines.push(` ${o.element1.reference} ↔ ${o.element2.reference} (${o.distance}mm) [${o.type}]`);
|
||||
});
|
||||
}
|
||||
if (lbls.length) {
|
||||
lines.push(`\nOverlapping labels (${lbls.length}):`);
|
||||
lbls.slice(0, 20).forEach((o: any) => {
|
||||
lines.push(` "${o.element1.name}" ↔ "${o.element2.name}" (${o.distance}mm)`);
|
||||
});
|
||||
}
|
||||
if (wires.length) {
|
||||
lines.push(`\nOverlapping wires (${wires.length}):`);
|
||||
wires.slice(0, 20).forEach((o: any) => {
|
||||
lines.push(` wire @ (${o.wire1.start.x},${o.wire1.start.y})→(${o.wire1.end.x},${o.wire1.end.y}) overlaps with another`);
|
||||
});
|
||||
}
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Get elements in a region
|
||||
server.tool(
|
||||
"get_elements_in_region",
|
||||
"List all symbols, wires, and labels within a rectangular region of the schematic. Useful for understanding what is in a specific area before modifying it.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
x1: z.number().describe("Left X coordinate of the region in mm"),
|
||||
y1: z.number().describe("Top Y coordinate of the region in mm"),
|
||||
x2: z.number().describe("Right X coordinate of the region in mm"),
|
||||
y2: z.number().describe("Bottom Y coordinate of the region in mm"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
x1: number; y1: number; x2: number; y2: number;
|
||||
}) => {
|
||||
const result = await callKicadScript("get_elements_in_region", args);
|
||||
if (result.success) {
|
||||
const c = result.counts;
|
||||
const lines = [`Region (${args.x1},${args.y1})→(${args.x2},${args.y2}): ${c.symbols} symbols, ${c.wires} wires, ${c.labels} labels`];
|
||||
const syms: any[] = result.symbols || [];
|
||||
if (syms.length) {
|
||||
lines.push("\nSymbols:");
|
||||
syms.forEach((s: any) => {
|
||||
const pinCount = s.pins ? Object.keys(s.pins).length : 0;
|
||||
lines.push(` ${s.reference} (${s.libId}) @ (${s.position.x}, ${s.position.y}) [${pinCount} pins]`);
|
||||
});
|
||||
}
|
||||
const wires: any[] = result.wires || [];
|
||||
if (wires.length) {
|
||||
lines.push(`\nWires (${wires.length}):`);
|
||||
wires.slice(0, 30).forEach((w: any) => {
|
||||
lines.push(` (${w.start.x},${w.start.y}) → (${w.end.x},${w.end.y})`);
|
||||
});
|
||||
if (wires.length > 30) lines.push(` ... and ${wires.length - 30} more`);
|
||||
}
|
||||
const labels: any[] = result.labels || [];
|
||||
if (labels.length) {
|
||||
lines.push(`\nLabels (${labels.length}):`);
|
||||
labels.forEach((l: any) => {
|
||||
lines.push(` "${l.name}" [${l.type}] @ (${l.position.x}, ${l.position.y})`);
|
||||
});
|
||||
}
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Check wire collisions
|
||||
server.tool(
|
||||
"check_wire_collisions",
|
||||
"Detect wires that pass through component bodies without connecting to their pins. These are usually routing mistakes where a wire crosses over a symbol instead of connecting to it.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
},
|
||||
async (args: { schematicPath: string }) => {
|
||||
const result = await callKicadScript("check_wire_collisions", args);
|
||||
if (result.success) {
|
||||
const collisions: any[] = result.collisions || [];
|
||||
const lines = [`Found ${collisions.length} wire collision(s):`];
|
||||
collisions.slice(0, 30).forEach((c: any, i: number) => {
|
||||
lines.push(
|
||||
` ${i + 1}. Wire (${c.wire.start.x},${c.wire.start.y})→(${c.wire.end.x},${c.wire.end.y}) passes through ${c.component.reference} (${c.component.libId})`
|
||||
);
|
||||
});
|
||||
if (collisions.length > 30) lines.push(` ... and ${collisions.length - 30} more`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user