Files
kicad-mcp-server/src/tools/schematic.ts
Eugene Mikhantyev 5f3c20d308 feat: add get_pin_net tool for direct net/pin queries
Answers "what net is pin X of component Y on?" without requiring
callers to triangulate from list_schematic_nets or know a wire
coordinate first.

Accepts either {reference, pin} (resolved via PinLocator) or {x, y}
coordinate. Returns net label name (or null for unnamed nets), all
connected pins, wire segments, and the resolved query point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 15:19:14 +01:00

1485 lines
51 KiB
TypeScript

/**
* Schematic tools for KiCAD MCP server
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerSchematicTools(server: McpServer, callKicadScript: Function) {
// Create schematic tool
server.tool(
"create_schematic",
"Create a new schematic",
{
name: z.string().describe("Schematic name"),
path: z.string().optional().describe("Optional path"),
},
async (args: { name: string; path?: string }) => {
const result = await callKicadScript("create_schematic", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Add component to schematic
server.tool(
"add_schematic_component",
"Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')",
{
schematicPath: z.string().describe("Path to the schematic file"),
symbol: z
.string()
.describe("Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)"),
reference: z.string().describe("Component reference (e.g., R1, U1)"),
value: z.string().optional().describe("Component value"),
footprint: z
.string()
.optional()
.describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
position: z
.object({
x: z.number(),
y: z.number(),
})
.optional()
.describe("Position on schematic"),
},
async (args: {
schematicPath: string;
symbol: string;
reference: string;
value?: string;
footprint?: string;
position?: { x: number; y: number };
}) => {
// Transform to what Python backend expects
const [library, symbolName] = args.symbol.includes(":")
? args.symbol.split(":")
: ["Device", args.symbol];
const transformed = {
schematicPath: args.schematicPath,
component: {
library,
type: symbolName,
reference: args.reference,
value: args.value,
footprint: args.footprint ?? "",
// Python expects flat x, y not nested position
x: args.position?.x ?? 0,
y: args.position?.y ?? 0,
},
};
const result = await callKicadScript("add_schematic_component", transformed);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to add component: ${result.message || JSON.stringify(result)}`,
},
],
};
}
},
);
// Delete component from schematic
server.tool(
"delete_schematic_component",
`Remove a placed symbol from a KiCAD schematic (.kicad_sch).
This removes the symbol instance (the placed component) from the schematic.
It does NOT remove the symbol definition from lib_symbols.
Note: This tool operates on schematic files (.kicad_sch).
To remove a footprint from a PCB, use delete_component instead.`,
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z
.string()
.describe("Reference designator of the component to remove (e.g. R1, U3)"),
},
async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("delete_schematic_component", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully removed ${args.reference} from schematic`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to remove component: ${result.message || "Unknown error"}`,
},
],
};
},
);
// Edit component properties in schematic (footprint, value, reference)
server.tool(
"edit_schematic_component",
`Update properties of a placed symbol in a KiCAD schematic (.kicad_sch) in-place.
Use this tool to assign or update a footprint, change the value, or rename the reference
of an already-placed component. This is more efficient than delete + re-add because it
preserves the component's position and UUID.
Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_component.`,
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Current reference designator of the component (e.g. R1, U3)"),
footprint: z
.string()
.optional()
.describe("New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)"),
value: z.string().optional().describe("New value string (e.g. 10k, 100nF)"),
newReference: z
.string()
.optional()
.describe("Rename the reference designator (e.g. R1 → R10)"),
fieldPositions: z
.record(
z.object({
x: z.number(),
y: z.number(),
angle: z.number().optional().default(0),
}),
)
.optional()
.describe(
'Reposition field labels: map of field name to {x, y, angle} (e.g. {"Reference": {"x": 12.5, "y": 17.0}})',
),
},
async (args: {
schematicPath: string;
reference: string;
footprint?: string;
value?: string;
newReference?: string;
fieldPositions?: Record<string, { x: number; y: number; angle?: number }>;
}) => {
const result = await callKicadScript("edit_schematic_component", args);
if (result.success) {
const changes = Object.entries(result.updated ?? {})
.map(([k, v]) => `${k}=${v}`)
.join(", ");
return {
content: [
{
type: "text" as const,
text: `Successfully updated ${args.reference}: ${changes}`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: `Failed to edit component: ${result.message || "Unknown error"}`,
},
],
};
},
);
// Get component properties and field positions from schematic
server.tool(
"get_schematic_component",
"Get full component info from a schematic: position, field values, and each field's label position (at x/y/angle). Use this to inspect or prepare repositioning of Reference/Value labels.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Component reference designator (e.g. R1, U1)"),
},
async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("get_schematic_component", args);
if (result.success) {
const pos = result.position
? `(${result.position.x}, ${result.position.y}, angle=${result.position.angle}°)`
: "unknown";
const fieldLines = Object.entries(result.fields ?? {}).map(
([name, f]: [string, any]) =>
` ${name}: "${f.value}" @ (${f.x}, ${f.y}, angle=${f.angle}°)`,
);
return {
content: [
{
type: "text",
text: `Component ${result.reference} at ${pos}\nFields:\n${fieldLines.join("\n")}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to get component: ${result.message || "Unknown error"}`,
},
],
};
},
);
// Draw wire between coordinate waypoints with optional pin snapping
server.tool(
"add_schematic_wire",
"Draws a wire on the schematic between two or more coordinate points. Always call get_schematic_pin_locations first to get the approximate pin coordinates, then pass them as the first and last waypoints. snapToPins (on by default) will correct any float imprecision by snapping endpoints to the exact nearest pin coordinate. To route around components, add intermediate waypoints between the start and end: e.g. [[x1,y1], [xMid,y1], [xMid,y2], [x2,y2]] routes horizontally then vertically. Intermediate waypoints are never snapped.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
waypoints: z
.array(z.array(z.number()).length(2))
.min(2)
.describe("Ordered list of [x, y] coordinates. Minimum 2 points."),
snapToPins: z
.boolean()
.optional()
.describe("Snap the first and last waypoints to the nearest pin (default: true)"),
snapTolerance: z.number().optional().describe("Maximum snap distance in mm (default: 1.0)"),
},
async (args: {
schematicPath: string;
waypoints: number[][];
snapToPins?: boolean;
snapTolerance?: number;
}) => {
const result = await callKicadScript("add_schematic_wire", args);
if (result.success) {
return {
content: [
{
type: "text" as const,
text: result.message || "Wire added successfully",
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: `Failed to add wire: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Add junction dot at a T/X intersection
server.tool(
"add_schematic_junction",
"Place a junction dot at a wire intersection in the schematic. Required at T-branch and X-cross points so KiCAD recognises the electrical connection.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
position: z.array(z.number()).length(2).describe("Junction position [x, y] in mm"),
},
async (args: { schematicPath: string; position: number[] }) => {
const result = await callKicadScript("add_schematic_junction", args);
if (result.success) {
return {
content: [
{
type: "text" as const,
text: result.message || "Junction added successfully",
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: `Failed to add junction: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Add net label
server.tool(
"add_schematic_net_label",
"Add a net label to the schematic. " +
"PREFERRED: supply componentRef + pinNumber to snap the label to the exact pin endpoint — " +
"this guarantees an electrical connection. " +
"Alternatively supply position [x, y], but the coordinates must match the pin endpoint exactly " +
"(even a 0.01 mm offset breaks the connection). " +
"The response includes actual_position (coordinates actually used) and snapped_to_pin " +
"(present when a pin reference was resolved).",
{
schematicPath: z.string().describe("Path to the schematic file"),
netName: z.string().describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
position: z
.array(z.number())
.length(2)
.optional()
.describe(
"Position [x, y] for the label. Required when componentRef/pinNumber are not given.",
),
componentRef: z
.string()
.optional()
.describe("Component reference to snap label to (e.g. U1, R1). Use with pinNumber."),
pinNumber: z
.union([z.string(), z.number()])
.optional()
.describe(
"Pin number or name on componentRef to snap label to (e.g. '1', 'GND'). Use with componentRef.",
),
labelType: z
.enum(["label", "global_label", "hierarchical_label"])
.optional()
.describe("Label type (default: label)"),
orientation: z.number().optional().describe("Rotation angle 0/90/180/270 (default: 0)"),
},
async (args: {
schematicPath: string;
netName: string;
position?: number[];
componentRef?: string;
pinNumber?: string | number;
labelType?: string;
orientation?: number;
}) => {
const result = await callKicadScript("add_schematic_net_label", args);
if (result.success) {
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to add net label: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Connect pin to net
server.tool(
"connect_to_net",
"Connect a component pin to a named net by adding a wire stub and net label at the exact pin endpoint. " +
"The response includes pin_location (exact pin coords), label_location (where the label was placed), " +
"and wire_stub (the wire segment added) so you can confirm the placement.",
{
schematicPath: z.string().describe("Path to the schematic file"),
componentRef: z.string().describe("Component reference (e.g., U1, R1)"),
pinName: z.string().describe("Pin name/number to connect"),
netName: z.string().describe("Name of the net to connect to"),
},
async (args: {
schematicPath: string;
componentRef: string;
pinName: string;
netName: string;
}) => {
const result = await callKicadScript("connect_to_net", args);
if (result.success) {
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to connect to net: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Get net connections
server.tool(
"get_net_connections",
"Get all connections for a named net",
{
schematicPath: z.string().describe("Path to the schematic file"),
netName: z.string().describe("Name of the net to query"),
},
async (args: { schematicPath: string; netName: string }) => {
const result = await callKicadScript("get_net_connections", args);
if (result.success && result.connections) {
const connectionList = result.connections
.map((conn: any) => ` - ${conn.component}/${conn.pin}`)
.join("\n");
return {
content: [
{
type: "text",
text: `Net '${args.netName}' connections:\n${connectionList}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to get net connections: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Get wire connections
server.tool(
"get_wire_connections",
"Find all component pins reachable from a schematic point via connected wires, net labels, and power symbols. The query point must be at a wire endpoint or junction — midpoints of wire segments are not matched. Use get_schematic_pin_locations or list_schematic_wires to obtain exact endpoint coordinates first.",
{
schematicPath: z.string().describe("Path to the schematic file"),
x: z.number().describe("X coordinate of a wire endpoint or junction"),
y: z.number().describe("Y coordinate of a wire endpoint or junction"),
},
async (args: { schematicPath: string; x: number; y: number }) => {
const result = await callKicadScript("get_wire_connections", args);
if (result.success && result.pins) {
const pinList = result.pins.map((p: any) => ` - ${p.component}/${p.pin}`).join("\n");
const wireList = (result.wires ?? [])
.map((w: any) => ` - (${w.start.x},${w.start.y}) → (${w.end.x},${w.end.y})`)
.join("\n");
return {
content: [
{
type: "text",
text: `Pins connected at (${args.x},${args.y}):\n${pinList || " (none found)"}\n\nWire segments:\n${wireList || " (none)"}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to get wire connections: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Get pin locations for a schematic component
server.tool(
"get_schematic_pin_locations",
"Returns the exact x/y coordinates of every pin on a schematic component. Use this before add_schematic_net_label to place labels correctly on pin endpoints.",
{
schematicPath: z.string().describe("Path to the schematic file"),
reference: z.string().describe("Component reference designator (e.g. U1, R1, J2)"),
},
async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("get_schematic_pin_locations", args);
if (result.success && result.pins) {
const lines = Object.entries(result.pins as Record<string, any>).map(
([pinNum, data]: [string, any]) =>
` Pin ${pinNum} (${data.name || pinNum}): x=${data.x}, y=${data.y}, angle=${data.angle ?? 0}°`,
);
return {
content: [
{
type: "text",
text: `Pin locations for ${args.reference}:\n${lines.join("\n")}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to get pin locations: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Connect all pins of source connector to matching pins of target connector (passthrough)
server.tool(
"connect_passthrough",
"Connects all pins of a source connector (e.g. J1) to matching pins of a target connector (e.g. J2) via shared net labels — pin N gets net '{netPrefix}_{N}'. Use this for FFC/ribbon cable passthrough adapters instead of calling connect_to_net for every pin.",
{
schematicPath: z.string().describe("Path to the schematic file"),
sourceRef: z.string().describe("Source connector reference (e.g. J1)"),
targetRef: z.string().describe("Target connector reference (e.g. J2)"),
netPrefix: z
.string()
.optional()
.describe("Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN)"),
pinOffset: z
.number()
.optional()
.describe("Add to pin number when building net name (default: 0)"),
},
async (args: {
schematicPath: string;
sourceRef: string;
targetRef: string;
netPrefix?: string;
pinOffset?: number;
}) => {
const result = await callKicadScript("connect_passthrough", args);
if (result.success !== false || (result.connected && result.connected.length > 0)) {
const lines: string[] = [];
if (result.connected?.length)
lines.push(
`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`,
);
if (result.failed?.length)
lines.push(`Failed (${result.failed.length}): ${result.failed.join(", ")}`);
return {
content: [{ type: "text", text: result.message + "\n" + lines.join("\n") }],
};
} else {
return {
content: [
{
type: "text",
text: `Passthrough failed: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// List all components in schematic
server.tool(
"list_schematic_components",
"List all components in a schematic with their references, values, positions, and pins. Essential for inspecting what's on the schematic before making edits.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
filter: z
.object({
libId: z.string().optional().describe("Filter by library ID (e.g., 'Device:R')"),
referencePrefix: z
.string()
.optional()
.describe("Filter by reference prefix (e.g., 'R', 'C', 'U')"),
})
.optional()
.describe("Optional filters"),
},
async (args: {
schematicPath: string;
filter?: { libId?: string; referencePrefix?: string };
}) => {
const result = await callKicadScript("list_schematic_components", args);
if (result.success) {
const comps = result.components || [];
if (comps.length === 0) {
return {
content: [{ type: "text", text: "No components found in schematic." }],
};
}
const lines = comps.map(
(c: any) =>
` ${c.reference}: ${c.libId} = "${c.value}" at (${c.position.x}, ${c.position.y}) rot=${c.rotation}°${c.pins ? ` [${c.pins.length} pins]` : ""}`,
);
return {
content: [
{
type: "text",
text: `Components (${comps.length}):\n${lines.join("\n")}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to list components: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// List all nets in schematic
server.tool(
"list_schematic_nets",
"List all nets in the schematic with their connections.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("list_schematic_nets", args);
if (result.success) {
const nets = result.nets || [];
if (nets.length === 0) {
return {
content: [{ type: "text", text: "No nets found in schematic." }],
};
}
const lines = nets.map((n: any) => {
const conns = (n.connections || []).map((c: any) => `${c.component}/${c.pin}`).join(", ");
return ` ${n.name}: ${conns || "(no connections)"}`;
});
return {
content: [
{
type: "text",
text: `Nets (${nets.length}):\n${lines.join("\n")}`,
},
],
};
}
return {
content: [
{ type: "text", text: `Failed to list nets: ${result.message || "Unknown error"}` },
],
isError: true,
};
},
);
// List all wires in schematic
server.tool(
"list_schematic_wires",
"List all wires in the schematic with start/end coordinates.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("list_schematic_wires", args);
if (result.success) {
const wires = result.wires || [];
if (wires.length === 0) {
return {
content: [{ type: "text", text: "No wires found in schematic." }],
};
}
const lines = wires.map(
(w: any) => ` (${w.start.x}, ${w.start.y}) → (${w.end.x}, ${w.end.y})`,
);
return {
content: [
{
type: "text",
text: `Wires (${wires.length}):\n${lines.join("\n")}`,
},
],
};
}
return {
content: [
{ type: "text", text: `Failed to list wires: ${result.message || "Unknown error"}` },
],
isError: true,
};
},
);
// List all labels in schematic
server.tool(
"list_schematic_labels",
"List all net labels, global labels, and power flags in the schematic.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("list_schematic_labels", args);
if (result.success) {
const labels = result.labels || [];
if (labels.length === 0) {
return {
content: [{ type: "text", text: "No labels found in schematic." }],
};
}
const lines = labels.map(
(l: any) => ` [${l.type}] ${l.name} at (${l.position.x}, ${l.position.y})`,
);
return {
content: [
{
type: "text",
text: `Labels (${labels.length}):\n${lines.join("\n")}`,
},
],
};
}
return {
content: [
{ type: "text", text: `Failed to list labels: ${result.message || "Unknown error"}` },
],
isError: true,
};
},
);
// Move a placed symbol, dragging connected wires
server.tool(
"move_schematic_component",
"Move a placed symbol to a new position in the schematic. By default (preserveWires=true) wire endpoints touching the component's pins are stretched to follow the new position.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Reference designator (e.g., R1, U1)"),
position: z
.object({ x: z.number(), y: z.number() })
.describe("New position in schematic mm coordinates"),
preserveWires: z
.boolean()
.optional()
.describe("Stretch connected wire endpoints to follow the move (default true)"),
},
async (args: {
schematicPath: string;
reference: string;
position: { x: number; y: number };
preserveWires?: boolean;
}) => {
const result = await callKicadScript("move_schematic_component", args);
if (result.success) {
const moved = result.wiresMoved ?? 0;
const removed = result.wiresRemoved ?? 0;
return {
content: [
{
type: "text",
text:
`Moved ${args.reference} from (${result.oldPosition.x}, ${result.oldPosition.y}) ` +
`to (${result.newPosition.x}, ${result.newPosition.y})` +
(moved > 0 ? `, ${moved} wire endpoint(s) updated` : "") +
(removed > 0 ? `, ${removed} zero-length wire(s) removed` : ""),
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to move component: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Rotate schematic component
server.tool(
"rotate_schematic_component",
"Rotate a placed symbol in the schematic.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Reference designator (e.g., R1, U1)"),
angle: z.number().describe("Rotation angle in degrees (0, 90, 180, 270)"),
mirror: z.enum(["x", "y"]).optional().describe("Optional mirror axis"),
},
async (args: {
schematicPath: string;
reference: string;
angle: number;
mirror?: "x" | "y";
}) => {
const result = await callKicadScript("rotate_schematic_component", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Rotated ${args.reference} to ${args.angle}°${args.mirror ? ` (mirrored ${args.mirror})` : ""}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to rotate component: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Annotate schematic
server.tool(
"annotate_schematic",
"Assign reference designators to unannotated components (R? → R1, R2, ...). Must be called before tools that require known references.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("annotate_schematic", args);
if (result.success) {
const annotated = result.annotated || [];
if (annotated.length === 0) {
return {
content: [{ type: "text", text: "All components are already annotated." }],
};
}
const lines = annotated.map((a: any) => ` ${a.oldReference}${a.newReference}`);
return {
content: [
{
type: "text",
text: `Annotated ${annotated.length} component(s):\n${lines.join("\n")}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to annotate: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Delete wire from schematic
server.tool(
"delete_schematic_wire",
"Remove a wire from the schematic by start and end coordinates.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
start: z.object({ x: z.number(), y: z.number() }).describe("Wire start position"),
end: z.object({ x: z.number(), y: z.number() }).describe("Wire end position"),
},
async (args: {
schematicPath: string;
start: { x: number; y: number };
end: { x: number; y: number };
}) => {
const result = await callKicadScript("delete_schematic_wire", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Deleted wire from (${args.start.x}, ${args.start.y}) to (${args.end.x}, ${args.end.y})`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to delete wire: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Delete net label from schematic
server.tool(
"delete_schematic_net_label",
"Remove a net label from the schematic.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
netName: z.string().describe("Name of the net label to remove"),
position: z
.object({ x: z.number(), y: z.number() })
.optional()
.describe("Position to disambiguate if multiple labels with same name"),
},
async (args: {
schematicPath: string;
netName: string;
position?: { x: number; y: number };
}) => {
const result = await callKicadScript("delete_schematic_net_label", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Deleted net label '${args.netName}'`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to delete label: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Export schematic to SVG
server.tool(
"export_schematic_svg",
"Export schematic to SVG format using kicad-cli.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
outputPath: z.string().describe("Output SVG file path"),
blackAndWhite: z.boolean().optional().describe("Export in black and white"),
},
async (args: { schematicPath: string; outputPath: string; blackAndWhite?: boolean }) => {
const result = await callKicadScript("export_schematic_svg", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Exported schematic SVG to ${result.file?.path || args.outputPath}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to export SVG: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Export schematic to PDF
server.tool(
"export_schematic_pdf",
"Export schematic to PDF format using kicad-cli.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
outputPath: z.string().describe("Output PDF file path"),
blackAndWhite: z.boolean().optional().describe("Export in black and white"),
},
async (args: { schematicPath: string; outputPath: string; blackAndWhite?: boolean }) => {
const result = await callKicadScript("export_schematic_pdf", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Exported schematic PDF to ${result.file?.path || args.outputPath}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to export PDF: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Get schematic view (rasterized image)
server.tool(
"get_schematic_view",
"Return a rasterized image of the schematic (PNG by default, or SVG). Uses kicad-cli to export SVG, then converts to PNG via cairosvg. Use this for visual feedback after placing or wiring components.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
format: z.enum(["png", "svg"]).optional().describe("Output format (default: png)"),
width: z.number().optional().describe("Image width in pixels (default: 1200)"),
height: z.number().optional().describe("Image height in pixels (default: 900)"),
},
async (args: {
schematicPath: string;
format?: "png" | "svg";
width?: number;
height?: number;
}) => {
const result = await callKicadScript("get_schematic_view", args);
if (result.success) {
if (result.format === "svg") {
const parts: { type: "text"; text: string }[] = [];
if (result.message) {
parts.push({ type: "text", text: result.message });
}
parts.push({
type: "text",
text: result.imageData || "",
});
return { content: parts };
}
// PNG — return as base64 image
return {
content: [
{
type: "image" as const,
data: result.imageData,
mimeType: "image/png",
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to get schematic view: ${result.message || "Unknown error"}`,
},
],
isError: true,
};
},
);
// Run Electrical Rules Check (ERC)
server.tool(
"run_erc",
"Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("run_erc", args);
if (result.success) {
const violations: any[] = result.violations || [];
const lines: string[] = [`ERC result: ${violations.length} violation(s)`];
if (result.summary?.by_severity) {
const s = result.summary.by_severity;
lines.push(
` Errors: ${s.error ?? 0} Warnings: ${s.warning ?? 0} Info: ${s.info ?? 0}`,
);
}
if (violations.length > 0) {
lines.push("");
violations.slice(0, 30).forEach((v: any, i: number) => {
const loc =
v.location && v.location.x !== undefined
? ` @ (${v.location.x}, ${v.location.y})`
: "";
lines.push(`${i + 1}. [${v.severity}] ${v.message}${loc}`);
});
if (violations.length > 30) {
lines.push(`... and ${violations.length - 30} more`);
}
}
return { content: [{ type: "text", text: lines.join("\n") }] };
} else {
return {
content: [
{
type: "text",
text: `ERC failed: ${result.message || "Unknown error"}${result.errorDetails ? "\n" + result.errorDetails : ""}`,
},
],
};
}
},
);
// Generate netlist
server.tool(
"generate_netlist",
"Generate a netlist from the schematic",
{
schematicPath: z.string().describe("Path to the schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("generate_netlist", args);
if (result.success && result.netlist) {
const netlist = result.netlist;
const output = [
`=== Netlist for ${args.schematicPath} ===`,
`\nComponents (${netlist.components.length}):`,
...netlist.components.map(
(comp: any) =>
` ${comp.reference}: ${comp.value} (${comp.footprint || "No footprint"})`,
),
`\nNets (${netlist.nets.length}):`,
...netlist.nets.map((net: any) => {
const connections = net.connections
.map((conn: any) => `${conn.component}/${conn.pin}`)
.join(", ");
return ` ${net.name}: ${connections}`;
}),
].join("\n");
return {
content: [
{
type: "text",
text: output,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to generate netlist: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// 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) }],
};
},
);
// ============================================================
// 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 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 threshold in mm for label proximity and wire collinearity checks. Symbol overlap uses bounding-box intersection. (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"}` }],
};
},
);
// Find wires crossing symbols
server.tool(
"find_wires_crossing_symbols",
"Find all wires that cross over component symbol bodies. Wires passing over symbols are unacceptable in schematics — they indicate routing mistakes where a wire was drawn across a component instead of around it.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("find_wires_crossing_symbols", args);
if (result.success) {
const collisions: any[] = result.collisions || [];
const lines = [`Found ${collisions.length} wire(s) crossing symbols:`];
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}) crosses ${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"}` }],
};
},
);
// Find orphaned wires
server.tool(
"find_orphaned_wires",
"Find wire segments with at least one dangling endpoint — not connected to a component pin, " +
"net label, or another wire. Orphaned wires cause ERC 'wire end unconnected' errors. " +
"Does not require the KiCad UI to be running.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("find_orphaned_wires", args);
if (result.success) {
const wires: any[] = result.orphaned_wires || [];
if (wires.length === 0) {
return { content: [{ type: "text", text: "No orphaned wires found." }] };
}
const lines: string[] = [`Found ${wires.length} orphaned wire(s):\n`];
wires.slice(0, 50).forEach((w: any) => {
const dangling = w.dangling_ends.map((e: any) => `(${e.x}, ${e.y})`).join(", ");
lines.push(
` wire (${w.start.x}, ${w.start.y})→(${w.end.x}, ${w.end.y}) dangling end(s): ${dangling}`,
);
});
if (wires.length > 50) lines.push(` ... and ${wires.length - 50} more`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
};
},
);
// 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"}` }],
};
},
);
server.tool(
"get_pin_net",
"Returns the net name and all connected pins for a component pin (reference + pin number) " +
"or a schematic coordinate (x, y in mm). Use this instead of list_schematic_nets + " +
"get_wire_connections when you want to answer 'what net is pin 3 of U1 on?'. " +
"Returns net=null for unnamed (unlabelled) nets.",
{
schematicPath: z.string().describe("Path to the schematic file"),
reference: z
.string()
.optional()
.describe("Component reference (e.g. U1, R1). Pair with pin."),
pin: z
.string()
.optional()
.describe("Pin number or name (e.g. '3', 'SDA'). Pair with reference."),
x: z.number().optional().describe("X coordinate of a wire endpoint in mm. Pair with y."),
y: z.number().optional().describe("Y coordinate of a wire endpoint in mm. Pair with x."),
},
async (args: {
schematicPath: string;
reference?: string;
pin?: string;
x?: number;
y?: number;
}) => {
const result = await callKicadScript("get_pin_net", args);
if (result.success) {
const netLabel = result.net ?? "(unnamed)";
const pinList = (result.pins ?? [])
.map((p: any) => ` - ${p.component}/${p.pin}`)
.join("\n");
const qp = result.query_point;
return {
content: [
{
type: "text",
text:
`Net: ${netLabel}\n` +
`Query point: (${qp?.x ?? args.x}, ${qp?.y ?? args.y})\n` +
`Connected pins:\n${pinList || " (none found)"}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to get pin net: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
}