feat: add 11 missing schematic inspection & editing tools

Add all missing schematic operations identified in the gap analysis:

Inspection (P0/P2):
- list_schematic_components: enumerate all components with refs, values, pins
- list_schematic_nets: list all nets with their connections
- list_schematic_wires: list all wire geometry
- list_schematic_labels: list net labels, global labels, power flags

Editing (P0/P1):
- annotate_schematic: assign ref designators to unannotated components (R? → R1)
- move_schematic_component: reposition placed symbols
- rotate_schematic_component: rotate/mirror placed symbols
- delete_schematic_wire: remove wires by coordinates
- delete_schematic_net_label: remove labels by name/position

Export (P1):
- export_schematic_svg: schematic SVG export via kicad-cli
- export_schematic_pdf: enhanced with file path return & blackAndWhite support

Also registers list_schematic_components and annotate_schematic as
direct tools (always visible) since they're prerequisites for
reference-based workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-12 23:52:11 +00:00
parent 914ef92c0f
commit b2a547e624
3 changed files with 1018 additions and 18 deletions

View File

@@ -383,7 +383,17 @@ class KiCADInterface:
"generate_netlist": self._handle_generate_netlist, "generate_netlist": self._handle_generate_netlist,
"sync_schematic_to_board": self._handle_sync_schematic_to_board, "sync_schematic_to_board": self._handle_sync_schematic_to_board,
"list_schematic_libraries": self._handle_list_schematic_libraries, "list_schematic_libraries": self._handle_list_schematic_libraries,
"list_schematic_components": self._handle_list_schematic_components,
"list_schematic_nets": self._handle_list_schematic_nets,
"list_schematic_wires": self._handle_list_schematic_wires,
"list_schematic_labels": self._handle_list_schematic_labels,
"move_schematic_component": self._handle_move_schematic_component,
"rotate_schematic_component": self._handle_rotate_schematic_component,
"annotate_schematic": self._handle_annotate_schematic,
"delete_schematic_wire": self._handle_delete_schematic_wire,
"delete_schematic_net_label": self._handle_delete_schematic_net_label,
"export_schematic_pdf": self._handle_export_schematic_pdf, "export_schematic_pdf": self._handle_export_schematic_pdf,
"export_schematic_svg": self._handle_export_schematic_svg,
"import_svg_logo": self._handle_import_svg_logo, "import_svg_logo": self._handle_import_svg_logo,
# UI/Process management commands # UI/Process management commands
"check_kicad_ui": self._handle_check_kicad_ui, "check_kicad_ui": self._handle_check_kicad_ui,
@@ -1175,26 +1185,33 @@ class KiCADInterface:
if not output_path: if not output_path:
return {"success": False, "message": "Output path is required"} return {"success": False, "message": "Output path is required"}
if not os.path.exists(schematic_path):
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
import subprocess import subprocess
result = subprocess.run( cmd = [
[ "kicad-cli",
"kicad-cli", "sch",
"sch", "export",
"export", "pdf",
"pdf", "--output",
"--output", output_path,
output_path, schematic_path,
schematic_path, ]
],
capture_output=True,
text=True,
)
success = result.returncode == 0 if params.get("blackAndWhite"):
message = result.stderr if not success else "" cmd.insert(-1, "--black-and-white")
return {"success": success, "message": message} result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode == 0:
return {"success": True, "file": {"path": output_path}}
else:
return {"success": False, "message": f"kicad-cli failed: {result.stderr}"}
except FileNotFoundError:
return {"success": False, "message": "kicad-cli not found in PATH"}
except Exception as e: except Exception as e:
logger.error(f"Error exporting schematic to PDF: {str(e)}") logger.error(f"Error exporting schematic to PDF: {str(e)}")
return {"success": False, "message": str(e)} return {"success": False, "message": str(e)}
@@ -1403,6 +1420,533 @@ class KiCADInterface:
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return {"success": False, "message": str(e)} return {"success": False, "message": str(e)}
def _handle_list_schematic_components(self, params):
"""List all components in a schematic"""
logger.info("Listing schematic components")
try:
from pathlib import Path
from commands.pin_locator import PinLocator
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
sch_file = Path(schematic_path)
if not sch_file.exists():
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
# Optional filters
filter_params = params.get("filter", {})
lib_id_filter = filter_params.get("libId", "")
ref_prefix_filter = filter_params.get("referencePrefix", "")
locator = PinLocator()
components = []
for symbol in schematic.symbol:
if not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
# Skip template symbols
if ref.startswith("_TEMPLATE"):
continue
lib_id = symbol.lib_id.value if hasattr(symbol, "lib_id") else ""
# Apply filters
if lib_id_filter and lib_id_filter not in lib_id:
continue
if ref_prefix_filter and not ref.startswith(ref_prefix_filter):
continue
value = symbol.property.Value.value if hasattr(symbol.property, "Value") else ""
footprint = symbol.property.Footprint.value if hasattr(symbol.property, "Footprint") else ""
position = symbol.at.value if hasattr(symbol, "at") else [0, 0, 0]
uuid_val = symbol.uuid.value if hasattr(symbol, "uuid") else ""
comp = {
"reference": ref,
"libId": lib_id,
"value": value,
"footprint": footprint,
"position": {"x": float(position[0]), "y": float(position[1])},
"rotation": float(position[2]) if len(position) > 2 else 0,
"uuid": str(uuid_val),
}
# Get pins if available
try:
all_pins = locator.get_all_symbol_pins(sch_file, ref)
if all_pins:
pins_def = locator.get_symbol_pins(sch_file, lib_id) or {}
pin_list = []
for pin_num, coords in all_pins.items():
pin_info = {
"number": pin_num,
"position": {"x": coords[0], "y": coords[1]},
}
if pin_num in pins_def:
pin_info["name"] = pins_def[pin_num].get("name", pin_num)
pin_list.append(pin_info)
comp["pins"] = pin_list
except Exception:
pass # Pin lookup is best-effort
components.append(comp)
return {"success": True, "components": components, "count": len(components)}
except Exception as e:
logger.error(f"Error listing schematic components: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_list_schematic_nets(self, params):
"""List all nets in a schematic with their connections"""
logger.info("Listing schematic nets")
try:
from pathlib import Path
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
# Get all net names from labels
net_names = set()
if hasattr(schematic, "label"):
for label in schematic.label:
if hasattr(label, "value"):
net_names.add(label.value)
nets = []
for net_name in sorted(net_names):
connections = ConnectionManager.get_net_connections(
schematic, net_name, Path(schematic_path)
)
nets.append({
"name": net_name,
"connections": connections,
})
return {"success": True, "nets": nets, "count": len(nets)}
except Exception as e:
logger.error(f"Error listing schematic nets: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_list_schematic_wires(self, params):
"""List all wires in a schematic"""
logger.info("Listing schematic wires")
try:
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
wires = []
if hasattr(schematic, "wire"):
for wire in schematic.wire:
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
points = []
for point in wire.pts.xy:
if hasattr(point, "value"):
points.append({
"x": float(point.value[0]),
"y": float(point.value[1]),
})
if len(points) >= 2:
wires.append({
"start": points[0],
"end": points[-1],
})
return {"success": True, "wires": wires, "count": len(wires)}
except Exception as e:
logger.error(f"Error listing schematic wires: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_list_schematic_labels(self, params):
"""List all net labels and power flags in a schematic"""
logger.info("Listing schematic labels")
try:
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
labels = []
# Regular labels
if hasattr(schematic, "label"):
for label in schematic.label:
if hasattr(label, "value"):
pos = label.at.value if hasattr(label, "at") and hasattr(label.at, "value") else [0, 0]
labels.append({
"name": label.value,
"type": "net",
"position": {"x": float(pos[0]), "y": float(pos[1])},
})
# Global labels
if hasattr(schematic, "global_label"):
for label in schematic.global_label:
if hasattr(label, "value"):
pos = label.at.value if hasattr(label, "at") and hasattr(label.at, "value") else [0, 0]
labels.append({
"name": label.value,
"type": "global",
"position": {"x": float(pos[0]), "y": float(pos[1])},
})
# Power symbols (components with power flag)
if hasattr(schematic, "symbol"):
for symbol in schematic.symbol:
if not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("#PWR") or ref.startswith("_TEMPLATE"):
if ref.startswith("_TEMPLATE"):
continue
value = symbol.property.Value.value if hasattr(symbol.property, "Value") else ref
pos = symbol.at.value if hasattr(symbol, "at") else [0, 0, 0]
labels.append({
"name": value,
"type": "power",
"position": {"x": float(pos[0]), "y": float(pos[1])},
})
return {"success": True, "labels": labels, "count": len(labels)}
except Exception as e:
logger.error(f"Error listing schematic labels: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_move_schematic_component(self, params):
"""Move a schematic component to a new position"""
logger.info("Moving schematic component")
try:
from pathlib import Path
import re
schematic_path = params.get("schematicPath")
reference = params.get("reference")
position = params.get("position", {})
new_x = position.get("x")
new_y = position.get("y")
if not schematic_path or not reference:
return {"success": False, "message": "schematicPath and reference are required"}
if new_x is None or new_y is None:
return {"success": False, "message": "position with x and y is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
# Find the symbol
for symbol in schematic.symbol:
if not hasattr(symbol.property, "Reference"):
continue
if symbol.property.Reference.value == reference:
old_pos = list(symbol.at.value)
old_position = {"x": float(old_pos[0]), "y": float(old_pos[1])}
# Preserve rotation (third element)
rotation = float(old_pos[2]) if len(old_pos) > 2 else 0
symbol.at.value = [new_x, new_y, rotation]
SchematicManager.save_schematic(schematic, schematic_path)
return {
"success": True,
"oldPosition": old_position,
"newPosition": {"x": new_x, "y": new_y},
}
return {"success": False, "message": f"Component {reference} not found"}
except Exception as e:
logger.error(f"Error moving schematic component: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_rotate_schematic_component(self, params):
"""Rotate a schematic component"""
logger.info("Rotating schematic component")
try:
schematic_path = params.get("schematicPath")
reference = params.get("reference")
angle = params.get("angle", 0)
mirror = params.get("mirror")
if not schematic_path or not reference:
return {"success": False, "message": "schematicPath and reference are required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
for symbol in schematic.symbol:
if not hasattr(symbol.property, "Reference"):
continue
if symbol.property.Reference.value == reference:
pos = list(symbol.at.value)
pos[2] = angle if len(pos) > 2 else angle
while len(pos) < 3:
pos.append(0)
pos[2] = angle
symbol.at.value = pos
# Handle mirror if specified
if mirror and hasattr(symbol, "mirror"):
symbol.mirror.value = mirror
SchematicManager.save_schematic(schematic, schematic_path)
return {"success": True, "reference": reference, "angle": angle}
return {"success": False, "message": f"Component {reference} not found"}
except Exception as e:
logger.error(f"Error rotating schematic component: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_annotate_schematic(self, params):
"""Annotate unannotated components in schematic (R? -> R1, R2, ...)"""
logger.info("Annotating schematic")
try:
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
# Collect existing references by prefix
existing_refs = {} # prefix -> set of numbers
unannotated = [] # (symbol, prefix)
for symbol in schematic.symbol:
if not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
# Split reference into prefix and number
import re
match = re.match(r'^([A-Za-z_]+)(\d+)$', ref)
if match:
prefix = match.group(1)
num = int(match.group(2))
if prefix not in existing_refs:
existing_refs[prefix] = set()
existing_refs[prefix].add(num)
elif ref.endswith("?"):
prefix = ref[:-1]
unannotated.append((symbol, prefix))
if not unannotated:
return {"success": True, "annotated": [], "message": "All components already annotated"}
annotated = []
for symbol, prefix in unannotated:
if prefix not in existing_refs:
existing_refs[prefix] = set()
# Find next available number
next_num = 1
while next_num in existing_refs[prefix]:
next_num += 1
old_ref = symbol.property.Reference.value
new_ref = f"{prefix}{next_num}"
symbol.property.Reference.value = new_ref
existing_refs[prefix].add(next_num)
uuid_val = str(symbol.uuid.value) if hasattr(symbol, "uuid") else ""
annotated.append({
"uuid": uuid_val,
"oldReference": old_ref,
"newReference": new_ref,
})
SchematicManager.save_schematic(schematic, schematic_path)
return {"success": True, "annotated": annotated}
except Exception as e:
logger.error(f"Error annotating schematic: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_delete_schematic_wire(self, params):
"""Delete a wire from the schematic matching start/end points"""
logger.info("Deleting schematic wire")
try:
schematic_path = params.get("schematicPath")
start = params.get("start", {})
end = params.get("end", {})
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
if not hasattr(schematic, "wire"):
return {"success": False, "message": "Schematic has no wires"}
tolerance = 0.5
wire_to_remove = None
for wire in schematic.wire:
if not hasattr(wire, "pts") or not hasattr(wire.pts, "xy"):
continue
points = []
for point in wire.pts.xy:
if hasattr(point, "value"):
points.append([float(point.value[0]), float(point.value[1])])
if len(points) < 2:
continue
sx, sy = start.get("x", 0), start.get("y", 0)
ex, ey = end.get("x", 0), end.get("y", 0)
# Check both directions
match_fwd = (abs(points[0][0] - sx) < tolerance and abs(points[0][1] - sy) < tolerance and
abs(points[-1][0] - ex) < tolerance and abs(points[-1][1] - ey) < tolerance)
match_rev = (abs(points[0][0] - ex) < tolerance and abs(points[0][1] - ey) < tolerance and
abs(points[-1][0] - sx) < tolerance and abs(points[-1][1] - sy) < tolerance)
if match_fwd or match_rev:
wire_to_remove = wire
break
if wire_to_remove:
schematic.wire.remove(wire_to_remove)
SchematicManager.save_schematic(schematic, schematic_path)
return {"success": True}
else:
return {"success": False, "message": "No matching wire found"}
except Exception as e:
logger.error(f"Error deleting schematic wire: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_delete_schematic_net_label(self, params):
"""Delete a net label from the schematic"""
logger.info("Deleting schematic net label")
try:
schematic_path = params.get("schematicPath")
net_name = params.get("netName")
position = params.get("position")
if not schematic_path or not net_name:
return {"success": False, "message": "schematicPath and netName are required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
if not hasattr(schematic, "label"):
return {"success": False, "message": "Schematic has no labels"}
tolerance = 0.5
label_to_remove = None
for label in schematic.label:
if not hasattr(label, "value") or label.value != net_name:
continue
if position:
# Match by position too
pos = label.at.value if hasattr(label, "at") and hasattr(label.at, "value") else None
if pos:
px, py = position.get("x", 0), position.get("y", 0)
if abs(float(pos[0]) - px) < tolerance and abs(float(pos[1]) - py) < tolerance:
label_to_remove = label
break
else:
# First match
label_to_remove = label
break
if label_to_remove:
schematic.label.remove(label_to_remove)
SchematicManager.save_schematic(schematic, schematic_path)
return {"success": True}
else:
return {"success": False, "message": f"Label '{net_name}' not found"}
except Exception as e:
logger.error(f"Error deleting schematic net label: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_export_schematic_svg(self, params):
"""Export schematic to SVG using kicad-cli"""
logger.info("Exporting schematic SVG")
import subprocess
try:
schematic_path = params.get("schematicPath")
output_path = params.get("outputPath")
if not schematic_path or not output_path:
return {"success": False, "message": "schematicPath and outputPath are required"}
if not os.path.exists(schematic_path):
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
cmd = ["kicad-cli", "sch", "export", "svg", schematic_path, "-o", output_path]
if params.get("blackAndWhite"):
cmd.append("--black-and-white")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode == 0:
return {"success": True, "file": {"path": output_path}}
else:
return {"success": False, "message": f"kicad-cli failed: {result.stderr}"}
except FileNotFoundError:
return {"success": False, "message": "kicad-cli not found in PATH"}
except Exception as e:
logger.error(f"Error exporting schematic SVG: {e}")
return {"success": False, "message": str(e)}
def _handle_get_net_connections(self, params): def _handle_get_net_connections(self, params):
"""Get all connections for a named net""" """Get all connections for a named net"""
logger.info("Getting net connections") logger.info("Getting net connections")

View File

@@ -83,17 +83,28 @@ export const toolCategories: ToolCategory[] = [
}, },
{ {
name: "schematic", name: "schematic",
description: "Schematic operations: create, add components, wire connections, netlists", description: "Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation",
tools: [ tools: [
"create_schematic", "create_schematic",
"add_schematic_component", "add_schematic_component",
"list_schematic_components",
"move_schematic_component",
"rotate_schematic_component",
"annotate_schematic",
"add_wire", "add_wire",
"delete_schematic_wire",
"add_schematic_connection", "add_schematic_connection",
"add_schematic_net_label", "add_schematic_net_label",
"delete_schematic_net_label",
"connect_to_net", "connect_to_net",
"get_net_connections", "get_net_connections",
"list_schematic_nets",
"list_schematic_wires",
"list_schematic_labels",
"generate_netlist", "generate_netlist",
"sync_schematic_to_board" "sync_schematic_to_board",
"export_schematic_svg",
"export_schematic_pdf"
] ]
}, },
{ {
@@ -141,6 +152,8 @@ export const directToolNames = [
// Schematic essentials (always visible so AI uses them correctly) // Schematic essentials (always visible so AI uses them correctly)
"add_schematic_component", "add_schematic_component",
"list_schematic_components",
"annotate_schematic",
"connect_passthrough", "connect_passthrough",
"connect_to_net", "connect_to_net",
"add_schematic_net_label", "add_schematic_net_label",

View File

@@ -445,6 +445,449 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
}, },
); );
// 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"}`,
},
],
};
},
);
// 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"}` },
],
};
},
);
// 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"}` },
],
};
},
);
// 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"}` },
],
};
},
);
// Move schematic component
server.tool(
"move_schematic_component",
"Move a placed symbol to a new position in the schematic.",
{
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"),
},
async (args: {
schematicPath: string;
reference: string;
position: { x: number; y: number };
}) => {
const result = await callKicadScript("move_schematic_component", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Moved ${args.reference} from (${result.oldPosition.x}, ${result.oldPosition.y}) to (${result.newPosition.x}, ${result.newPosition.y})`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to move component: ${result.message || "Unknown error"}`,
},
],
};
},
);
// 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"}`,
},
],
};
},
);
// 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"}`,
},
],
};
},
);
// 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"}`,
},
],
};
},
);
// 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"}`,
},
],
};
},
);
// 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"}`,
},
],
};
},
);
// 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"}`,
},
],
};
},
);
// Run Electrical Rules Check (ERC) // Run Electrical Rules Check (ERC)
server.tool( server.tool(
"run_erc", "run_erc",