From a11dd5fac965bc27de159bddc6e95c13813dca65 Mon Sep 17 00:00:00 2001 From: Eugene Mikhantyev Date: Sat, 28 Mar 2026 13:11:17 +0000 Subject: [PATCH] style: apply Black and Prettier formatting to PR-changed files Co-Authored-By: Claude Sonnet 4.6 --- python/commands/wire_manager.py | 300 +++++++++++----- src/tools/registry.ts | 615 ++++++++++++++++---------------- src/tools/schematic.ts | 239 +++++++++---- 3 files changed, 687 insertions(+), 467 deletions(-) diff --git a/python/commands/wire_manager.py b/python/commands/wire_manager.py index 5e30cf8..679ec2e 100644 --- a/python/commands/wire_manager.py +++ b/python/commands/wire_manager.py @@ -15,25 +15,30 @@ from typing import List, Tuple, Optional import sexpdata from sexpdata import Symbol -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") # Module-level Symbol constants — avoids repeated allocation on every call -_SYM_WIRE = Symbol('wire') -_SYM_PTS = Symbol('pts') -_SYM_XY = Symbol('xy') -_SYM_STROKE = Symbol('stroke') -_SYM_WIDTH = Symbol('width') -_SYM_TYPE = Symbol('type') -_SYM_UUID = Symbol('uuid') -_SYM_SHEET_INSTANCES = Symbol('sheet_instances') +_SYM_WIRE = Symbol("wire") +_SYM_PTS = Symbol("pts") +_SYM_XY = Symbol("xy") +_SYM_STROKE = Symbol("stroke") +_SYM_WIDTH = Symbol("width") +_SYM_TYPE = Symbol("type") +_SYM_UUID = Symbol("uuid") +_SYM_SHEET_INSTANCES = Symbol("sheet_instances") class WireManager: """Manage wires in KiCad schematics using S-expression manipulation""" @staticmethod - def add_wire(schematic_path: Path, start_point: List[float], end_point: List[float], - stroke_width: float = 0, stroke_type: str = 'default') -> bool: + def add_wire( + schematic_path: Path, + start_point: List[float], + end_point: List[float], + stroke_width: float = 0, + stroke_type: str = "default", + ) -> bool: """ Add a wire to the schematic using S-expression manipulation @@ -49,7 +54,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -62,12 +67,18 @@ class WireManager: # Create wire S-expression # Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...)) - wire_sexp = WireManager._make_wire_sexp(start_point, end_point, stroke_width, stroke_type) + wire_sexp = WireManager._make_wire_sexp( + start_point, end_point, stroke_width, stroke_type + ) # Find insertion point (before sheet_instances) sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -80,7 +91,7 @@ class WireManager: logger.info(f"Injected wire from {start_point} to {end_point}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -90,12 +101,17 @@ class WireManager: except Exception as e: logger.error(f"Error adding wire: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def add_polyline_wire(schematic_path: Path, points: List[List[float]], - stroke_width: float = 0, stroke_type: str = 'default') -> bool: + def add_polyline_wire( + schematic_path: Path, + points: List[List[float]], + stroke_width: float = 0, + stroke_type: str = "default", + ) -> bool: """ Add a multi-segment wire (polyline) to the schematic @@ -114,7 +130,7 @@ class WireManager: return False # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -128,14 +144,20 @@ class WireManager: # KiCAD wire elements only support exactly 2 pts each. # Split N waypoints into N-1 individual wire segments. wire_sexps = [ - WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type) + WireManager._make_wire_sexp( + points[i], points[i + 1], stroke_width, stroke_type + ) for i in range(len(points) - 1) ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -146,10 +168,12 @@ class WireManager: # Insert all segments (in reverse so order is preserved after inserts) for wire_sexp in reversed(wire_sexps): sch_data.insert(sheet_instances_index, wire_sexp) - logger.info(f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline") + logger.info( + f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline" + ) # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -159,12 +183,18 @@ class WireManager: except Exception as e: logger.error(f"Error adding polyline wire: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def add_label(schematic_path: Path, text: str, position: List[float], - label_type: str = 'label', orientation: int = 0) -> bool: + def add_label( + schematic_path: Path, + text: str, + position: List[float], + label_type: str = "label", + orientation: int = 0, + ) -> bool: """ Add a net label to the schematic @@ -180,7 +210,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -190,19 +220,24 @@ class WireManager: label_sexp = [ Symbol(label_type), text, - [Symbol('at'), position[0], position[1], orientation], - [Symbol('fields_autoplaced'), Symbol('yes')], - [Symbol('effects'), - [Symbol('font'), [Symbol('size'), 1.27, 1.27]], - [Symbol('justify'), Symbol('left'), Symbol('bottom')] + [Symbol("at"), position[0], position[1], orientation], + [Symbol("fields_autoplaced"), Symbol("yes")], + [ + Symbol("effects"), + [Symbol("font"), [Symbol("size"), 1.27, 1.27]], + [Symbol("justify"), Symbol("left"), Symbol("bottom")], ], - [Symbol('uuid'), str(uuid.uuid4())] + [Symbol("uuid"), str(uuid.uuid4())], ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -215,7 +250,7 @@ class WireManager: logger.info(f"Injected label '{text}' at {position}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -225,21 +260,27 @@ class WireManager: except Exception as e: logger.error(f"Error adding label: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def _parse_wire(wire_item) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]: + def _parse_wire( + wire_item, + ) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]: """ Parse a wire S-expression item in a single pass. Returns ((x1,y1), (x2,y2), stroke_width, stroke_type), or None if not a valid wire. """ - if not (isinstance(wire_item, list) and len(wire_item) >= 2 - and wire_item[0] == _SYM_WIRE): + if not ( + isinstance(wire_item, list) + and len(wire_item) >= 2 + and wire_item[0] == _SYM_WIRE + ): return None start = end = None stroke_width: float = 0 - stroke_type: str = 'default' + stroke_type: str = "default" for part in wire_item[1:]: if not isinstance(part, list) or not part: continue @@ -265,10 +306,15 @@ class WireManager: return None @staticmethod - def _point_strictly_on_wire(px: float, py: float, - x1: float, y1: float, - x2: float, y2: float, - eps: float = 1e-6) -> bool: + def _point_strictly_on_wire( + px: float, + py: float, + x1: float, + y1: float, + x2: float, + y2: float, + eps: float = 1e-6, + ) -> bool: """ Return True if (px, py) lies strictly between (x1,y1) and (x2,y2) on a horizontal or vertical wire segment (not at either endpoint). @@ -286,17 +332,17 @@ class WireManager: return False @staticmethod - def _make_wire_sexp(start: List[float], end: List[float], - stroke_width: float = 0, stroke_type: str = 'default') -> list: + def _make_wire_sexp( + start: List[float], + end: List[float], + stroke_width: float = 0, + stroke_type: str = "default", + ) -> list: return [ _SYM_WIRE, - [_SYM_PTS, - [_SYM_XY, start[0], start[1]], - [_SYM_XY, end[0], end[1]]], - [_SYM_STROKE, - [_SYM_WIDTH, stroke_width], - [_SYM_TYPE, Symbol(stroke_type)]], - [_SYM_UUID, str(uuid.uuid4())] + [_SYM_PTS, [_SYM_XY, start[0], start[1]], [_SYM_XY, end[0], end[1]]], + [_SYM_STROKE, [_SYM_WIDTH, stroke_width], [_SYM_TYPE, Symbol(stroke_type)]], + [_SYM_UUID, str(uuid.uuid4())], ] @staticmethod @@ -316,9 +362,13 @@ class WireManager: if parsed is not None: (x1, y1), (x2, y2), stroke_width, stroke_type = parsed if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2): - seg_a = WireManager._make_wire_sexp([x1, y1], [px, py], stroke_width, stroke_type) - seg_b = WireManager._make_wire_sexp([px, py], [x2, y2], stroke_width, stroke_type) - sch_data[i:i + 1] = [seg_a, seg_b] + seg_a = WireManager._make_wire_sexp( + [x1, y1], [px, py], stroke_width, stroke_type + ) + seg_b = WireManager._make_wire_sexp( + [px, py], [x2, y2], stroke_width, stroke_type + ) + sch_data[i : i + 1] = [seg_a, seg_b] logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})") splits += 1 i += 2 # skip the two new segments @@ -327,7 +377,9 @@ class WireManager: return splits @staticmethod - def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool: + def add_junction( + schematic_path: Path, position: List[float], diameter: float = 0 + ) -> bool: """ Add a junction (connection dot) to the schematic. @@ -345,7 +397,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -359,17 +411,21 @@ class WireManager: # Create junction S-expression # Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...)) junction_sexp = [ - Symbol('junction'), - [Symbol('at'), position[0], position[1]], - [Symbol('diameter'), diameter], - [Symbol('color'), 0, 0, 0, 0], - [Symbol('uuid'), str(uuid.uuid4())] + Symbol("junction"), + [Symbol("at"), position[0], position[1]], + [Symbol("diameter"), diameter], + [Symbol("color"), 0, 0, 0, 0], + [Symbol("uuid"), str(uuid.uuid4())], ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -382,7 +438,7 @@ class WireManager: logger.info(f"Injected junction at {position}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -392,6 +448,7 @@ class WireManager: except Exception as e: logger.error(f"Error adding junction: {e}") import traceback + logger.error(traceback.format_exc()) return False @@ -409,7 +466,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -417,15 +474,19 @@ class WireManager: # Create no_connect S-expression # Format: (no_connect (at x y) (uuid ...)) no_connect_sexp = [ - Symbol('no_connect'), - [Symbol('at'), position[0], position[1]], - [Symbol('uuid'), str(uuid.uuid4())] + Symbol("no_connect"), + [Symbol("at"), position[0], position[1]], + [Symbol("uuid"), str(uuid.uuid4())], ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -438,7 +499,7 @@ class WireManager: logger.info(f"Injected no-connect at {position}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -448,12 +509,17 @@ class WireManager: except Exception as e: logger.error(f"Error adding no-connect: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def delete_wire(schematic_path: Path, start_point: List[float], end_point: List[float], - tolerance: float = 0.5) -> bool: + def delete_wire( + schematic_path: Path, + start_point: List[float], + end_point: List[float], + tolerance: float = 0.5, + ) -> bool: """ Delete a wire from the schematic matching given start/end coordinates. @@ -467,7 +533,7 @@ class WireManager: True if a wire was found and removed, False otherwise """ try: - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -476,34 +542,54 @@ class WireManager: ex, ey = end_point for i, item in enumerate(sch_data): - if not (isinstance(item, list) and len(item) > 0 and item[0] == Symbol('wire')): + if not ( + isinstance(item, list) + and len(item) > 0 + and item[0] == Symbol("wire") + ): continue # Extract pts from the wire s-expression pts_list = None for part in item[1:]: - if isinstance(part, list) and len(part) > 0 and part[0] == Symbol('pts'): + if ( + isinstance(part, list) + and len(part) > 0 + and part[0] == Symbol("pts") + ): pts_list = part break if pts_list is None: continue - xy_points = [p for p in pts_list[1:] if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol('xy')] + xy_points = [ + p + for p in pts_list[1:] + if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol("xy") + ] if len(xy_points) < 2: continue x1, y1 = float(xy_points[0][1]), float(xy_points[0][2]) x2, y2 = float(xy_points[-1][1]), float(xy_points[-1][2]) - match_fwd = (abs(x1 - sx) < tolerance and abs(y1 - sy) < tolerance and - abs(x2 - ex) < tolerance and abs(y2 - ey) < tolerance) - match_rev = (abs(x1 - ex) < tolerance and abs(y1 - ey) < tolerance and - abs(x2 - sx) < tolerance and abs(y2 - sy) < tolerance) + match_fwd = ( + abs(x1 - sx) < tolerance + and abs(y1 - sy) < tolerance + and abs(x2 - ex) < tolerance + and abs(y2 - ey) < tolerance + ) + match_rev = ( + abs(x1 - ex) < tolerance + and abs(y1 - ey) < tolerance + and abs(x2 - sx) < tolerance + and abs(y2 - sy) < tolerance + ) if match_fwd or match_rev: del sch_data[i] - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: f.write(sexpdata.dumps(sch_data)) logger.info(f"Deleted wire from {start_point} to {end_point}") return True @@ -514,13 +600,17 @@ class WireManager: except Exception as e: logger.error(f"Error deleting wire: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def delete_label(schematic_path: Path, net_name: str, - position: Optional[List[float]] = None, - tolerance: float = 0.5) -> bool: + def delete_label( + schematic_path: Path, + net_name: str, + position: Optional[List[float]] = None, + tolerance: float = 0.5, + ) -> bool: """ Delete a net label from the schematic by name (and optionally position). @@ -534,14 +624,17 @@ class WireManager: True if a label was found and removed, False otherwise """ try: - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) for i, item in enumerate(sch_data): - if not (isinstance(item, list) and len(item) > 0 - and item[0] == Symbol('label')): + if not ( + isinstance(item, list) + and len(item) > 0 + and item[0] == Symbol("label") + ): continue # Second element is the label text @@ -551,19 +644,26 @@ class WireManager: if position is not None: # Find (at x y ...) sub-expression and check coordinates at_entry = next( - (p for p in item[1:] if isinstance(p, list) - and len(p) >= 3 and p[0] == Symbol('at')), + ( + p + for p in item[1:] + if isinstance(p, list) + and len(p) >= 3 + and p[0] == Symbol("at") + ), None, ) if at_entry is None: continue lx, ly = float(at_entry[1]), float(at_entry[2]) - if not (abs(lx - position[0]) < tolerance - and abs(ly - position[1]) < tolerance): + if not ( + abs(lx - position[0]) < tolerance + and abs(ly - position[1]) < tolerance + ): continue del sch_data[i] - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: f.write(sexpdata.dumps(sch_data)) logger.info(f"Deleted label '{net_name}'") return True @@ -574,12 +674,14 @@ class WireManager: except Exception as e: logger.error(f"Error deleting label: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def create_orthogonal_path(start: List[float], end: List[float], - prefer_horizontal_first: bool = True) -> List[List[float]]: + def create_orthogonal_path( + start: List[float], end: List[float], prefer_horizontal_first: bool = True + ) -> List[List[float]]: """ Create an orthogonal (right-angle) path between two points @@ -608,10 +710,11 @@ class WireManager: return [start, corner, end] -if __name__ == '__main__': +if __name__ == "__main__": # Test wire creation import sys - sys.path.insert(0, '/home/chris/MCP/KiCAD-MCP-Server/python') + + sys.path.insert(0, "/home/chris/MCP/KiCAD-MCP-Server/python") from pathlib import Path import shutil @@ -621,8 +724,10 @@ if __name__ == '__main__': print("=" * 80) # Create test schematic (cross-platform temp directory) - test_path = Path(tempfile.gettempdir()) / 'test_wire_manager.kicad_sch' - template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch') + test_path = Path(tempfile.gettempdir()) / "test_wire_manager.kicad_sch" + template_path = Path( + "/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch" + ) shutil.copy(template_path, test_path) print(f"\n✓ Created test schematic: {test_path}") @@ -658,8 +763,9 @@ if __name__ == '__main__': print("\n[Verification] Loading with kicad-skip...") try: from skip import Schematic + sch = Schematic(str(test_path)) - wire_count = len(list(sch.wire)) if hasattr(sch, 'wire') else 0 + wire_count = len(list(sch.wire)) if hasattr(sch, "wire") else 0 print(f" ✓ Loaded successfully") print(f" ✓ Wire count: {wire_count}") except Exception as e: diff --git a/src/tools/registry.ts b/src/tools/registry.ts index 4799052..41e7ad4 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -1,308 +1,307 @@ -/** - * Tool Registry for KiCAD MCP Server - * - * Centralizes all tool definitions and provides lookup/search functionality - */ - -import { z } from 'zod'; - -export interface ToolDefinition { - name: string; - description: string; - inputSchema: z.ZodObject | z.ZodType; - // Handler will be registered separately in the existing tool files -} - -export interface ToolCategory { - name: string; - description: string; - tools: string[]; // Tool names in this category -} - -/** - * Tool category definitions - * Each category groups related tools for better organization - */ -export const toolCategories: ToolCategory[] = [ - { - name: "board", - description: "Board configuration: layers, mounting holes, zones, visualization", - tools: [ - "add_layer", - "set_active_layer", - "get_layer_list", - "add_mounting_hole", - "add_board_text", - "add_zone", - "get_board_extents", - "get_board_2d_view", - "launch_kicad_ui" - ] - }, - { - name: "component", - description: "Advanced component operations: edit, delete, search, group, annotate", - tools: [ - "rotate_component", - "delete_component", - "edit_component", - "find_component", - "get_component_properties", - "add_component_annotation", - "group_components", - "replace_component" - ] - }, - { - name: "export", - description: "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models", - tools: [ - "export_gerber", - "export_pdf", - "export_svg", - "export_3d", - "export_bom", - "export_netlist", - "export_position_file", - "export_vrml" - ] - }, - { - name: "drc", - description: "Design rule checking and electrical validation: DRC, net classes, clearances", - tools: [ - "set_design_rules", - "get_design_rules", - "run_drc", - "add_net_class", - "assign_net_to_class", - "set_layer_constraints", - "check_clearance", - "get_drc_violations" - ] - }, - { - name: "schematic", - description: "Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation", - tools: [ - "create_schematic", - "add_schematic_component", - "list_schematic_components", - "move_schematic_component", - "rotate_schematic_component", - "annotate_schematic", - "add_schematic_wire", - "delete_schematic_wire", - "add_schematic_junction", - "add_schematic_net_label", - "delete_schematic_net_label", - "connect_to_net", - "connect_passthrough", - "get_net_connections", - "list_schematic_nets", - "list_schematic_wires", - "list_schematic_labels", - "get_wire_connections", - "generate_netlist", - "sync_schematic_to_board", - "get_schematic_view", - "export_schematic_svg", - "export_schematic_pdf" - ] - }, - { - name: "library", - description: "Footprint library access: search, browse, get footprint information", - tools: [ - "list_libraries", - "search_footprints", - "list_library_footprints", - "get_footprint_info" - ] - }, - { - name: "routing", - description: "Advanced routing operations: vias, copper pours", - tools: [ - "add_via", - "add_copper_pour" - ] - }, - { - name: "autoroute", - description: "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES", - tools: [ - "autoroute", - "export_dsn", - "import_ses", - "check_freerouting" - ] - } -]; - -/** - * Direct tools that are always visible (not routed) - * These are the most frequently used tools - */ -export const directToolNames = [ - // Project lifecycle - "create_project", - "open_project", - "save_project", - "snapshot_project", - "get_project_info", - - // Core PCB operations - "place_component", - "move_component", - "add_net", - "route_trace", - "get_board_info", - "set_board_size", - - // Board setup - "add_board_outline", - - // Schematic essentials (always visible so AI uses them correctly) - "add_schematic_component", - "list_schematic_components", - "annotate_schematic", - "connect_passthrough", - "connect_to_net", - "add_schematic_net_label", - - // Schematic <-> PCB sync (F8 equivalent) - "sync_schematic_to_board", - - // UI management - "check_kicad_ui" -]; - -// Build lookup maps at module load time -const categoryMap = new Map(); -const toolCategoryMap = new Map(); - -export function initializeRegistry() { - // Build category map - for (const category of toolCategories) { - categoryMap.set(category.name, category); - - // Build tool -> category map - for (const toolName of category.tools) { - toolCategoryMap.set(toolName, category.name); - } - } -} - -/** - * Get a category by name - */ -export function getCategory(name: string): ToolCategory | undefined { - return categoryMap.get(name); -} - -/** - * Get the category name for a tool - */ -export function getToolCategory(toolName: string): string | undefined { - return toolCategoryMap.get(toolName); -} - -/** - * Get all categories - */ -export function getAllCategories(): ToolCategory[] { - return toolCategories; -} - -/** - * Get all routed tool names (excludes direct tools) - */ -export function getRoutedToolNames(): string[] { - const allRoutedTools: string[] = []; - for (const category of toolCategories) { - allRoutedTools.push(...category.tools); - } - return allRoutedTools; -} - -/** - * Check if a tool is a direct tool - */ -export function isDirectTool(toolName: string): boolean { - return directToolNames.includes(toolName); -} - -/** - * Check if a tool is a routed tool - */ -export function isRoutedTool(toolName: string): boolean { - return toolCategoryMap.has(toolName); -} - -/** - * Search for tools by keyword - * Searches tool names, descriptions, and category names - */ -export interface SearchResult { - category: string; - tool: string; - description: string; -} - -export function searchTools(query: string): SearchResult[] { - const q = query.toLowerCase(); - const matches: SearchResult[] = []; - - // Search direct tools first - for (const toolName of directToolNames) { - if (toolName.toLowerCase().includes(q)) { - matches.push({ - category: "direct", - tool: toolName, - description: `${toolName} (direct tool — call directly, no execute_tool needed)` - }); - } - } - - // Search routed tools by name and category - for (const category of toolCategories) { - const categoryMatch = - category.name.toLowerCase().includes(q) || - category.description.toLowerCase().includes(q); - - for (const toolName of category.tools) { - if (toolName.toLowerCase().includes(q) || categoryMatch) { - matches.push({ - category: category.name, - tool: toolName, - description: `${toolName} (${category.name})` - }); - } - } - } - - return matches.slice(0, 20); // Limit results -} - -/** - * Get statistics about the tool registry - */ -export function getRegistryStats() { - const routedToolCount = getRoutedToolNames().length; - const directToolCount = directToolNames.length; - - return { - total_categories: toolCategories.length, - total_routed_tools: routedToolCount, - total_direct_tools: directToolCount, - total_tools: routedToolCount + directToolCount, - categories: toolCategories.map(c => ({ - name: c.name, - tool_count: c.tools.length - })) - }; -} - -// Initialize on module load -initializeRegistry(); +/** + * Tool Registry for KiCAD MCP Server + * + * Centralizes all tool definitions and provides lookup/search functionality + */ + +import { z } from "zod"; + +export interface ToolDefinition { + name: string; + description: string; + inputSchema: z.ZodObject | z.ZodType; + // Handler will be registered separately in the existing tool files +} + +export interface ToolCategory { + name: string; + description: string; + tools: string[]; // Tool names in this category +} + +/** + * Tool category definitions + * Each category groups related tools for better organization + */ +export const toolCategories: ToolCategory[] = [ + { + name: "board", + description: + "Board configuration: layers, mounting holes, zones, visualization", + tools: [ + "add_layer", + "set_active_layer", + "get_layer_list", + "add_mounting_hole", + "add_board_text", + "add_zone", + "get_board_extents", + "get_board_2d_view", + "launch_kicad_ui", + ], + }, + { + name: "component", + description: + "Advanced component operations: edit, delete, search, group, annotate", + tools: [ + "rotate_component", + "delete_component", + "edit_component", + "find_component", + "get_component_properties", + "add_component_annotation", + "group_components", + "replace_component", + ], + }, + { + name: "export", + description: + "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models", + tools: [ + "export_gerber", + "export_pdf", + "export_svg", + "export_3d", + "export_bom", + "export_netlist", + "export_position_file", + "export_vrml", + ], + }, + { + name: "drc", + description: + "Design rule checking and electrical validation: DRC, net classes, clearances", + tools: [ + "set_design_rules", + "get_design_rules", + "run_drc", + "add_net_class", + "assign_net_to_class", + "set_layer_constraints", + "check_clearance", + "get_drc_violations", + ], + }, + { + name: "schematic", + description: + "Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation", + tools: [ + "create_schematic", + "add_schematic_component", + "list_schematic_components", + "move_schematic_component", + "rotate_schematic_component", + "annotate_schematic", + "add_schematic_wire", + "delete_schematic_wire", + "add_schematic_junction", + "add_schematic_net_label", + "delete_schematic_net_label", + "connect_to_net", + "connect_passthrough", + "get_net_connections", + "list_schematic_nets", + "list_schematic_wires", + "list_schematic_labels", + "get_wire_connections", + "generate_netlist", + "sync_schematic_to_board", + "get_schematic_view", + "export_schematic_svg", + "export_schematic_pdf", + ], + }, + { + name: "library", + description: + "Footprint library access: search, browse, get footprint information", + tools: [ + "list_libraries", + "search_footprints", + "list_library_footprints", + "get_footprint_info", + ], + }, + { + name: "routing", + description: "Advanced routing operations: vias, copper pours", + tools: ["add_via", "add_copper_pour"], + }, + { + name: "autoroute", + description: + "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES", + tools: ["autoroute", "export_dsn", "import_ses", "check_freerouting"], + }, +]; + +/** + * Direct tools that are always visible (not routed) + * These are the most frequently used tools + */ +export const directToolNames = [ + // Project lifecycle + "create_project", + "open_project", + "save_project", + "snapshot_project", + "get_project_info", + + // Core PCB operations + "place_component", + "move_component", + "add_net", + "route_trace", + "get_board_info", + "set_board_size", + + // Board setup + "add_board_outline", + + // Schematic essentials (always visible so AI uses them correctly) + "add_schematic_component", + "list_schematic_components", + "annotate_schematic", + "connect_passthrough", + "connect_to_net", + "add_schematic_net_label", + + // Schematic <-> PCB sync (F8 equivalent) + "sync_schematic_to_board", + + // UI management + "check_kicad_ui", +]; + +// Build lookup maps at module load time +const categoryMap = new Map(); +const toolCategoryMap = new Map(); + +export function initializeRegistry() { + // Build category map + for (const category of toolCategories) { + categoryMap.set(category.name, category); + + // Build tool -> category map + for (const toolName of category.tools) { + toolCategoryMap.set(toolName, category.name); + } + } +} + +/** + * Get a category by name + */ +export function getCategory(name: string): ToolCategory | undefined { + return categoryMap.get(name); +} + +/** + * Get the category name for a tool + */ +export function getToolCategory(toolName: string): string | undefined { + return toolCategoryMap.get(toolName); +} + +/** + * Get all categories + */ +export function getAllCategories(): ToolCategory[] { + return toolCategories; +} + +/** + * Get all routed tool names (excludes direct tools) + */ +export function getRoutedToolNames(): string[] { + const allRoutedTools: string[] = []; + for (const category of toolCategories) { + allRoutedTools.push(...category.tools); + } + return allRoutedTools; +} + +/** + * Check if a tool is a direct tool + */ +export function isDirectTool(toolName: string): boolean { + return directToolNames.includes(toolName); +} + +/** + * Check if a tool is a routed tool + */ +export function isRoutedTool(toolName: string): boolean { + return toolCategoryMap.has(toolName); +} + +/** + * Search for tools by keyword + * Searches tool names, descriptions, and category names + */ +export interface SearchResult { + category: string; + tool: string; + description: string; +} + +export function searchTools(query: string): SearchResult[] { + const q = query.toLowerCase(); + const matches: SearchResult[] = []; + + // Search direct tools first + for (const toolName of directToolNames) { + if (toolName.toLowerCase().includes(q)) { + matches.push({ + category: "direct", + tool: toolName, + description: `${toolName} (direct tool — call directly, no execute_tool needed)`, + }); + } + } + + // Search routed tools by name and category + for (const category of toolCategories) { + const categoryMatch = + category.name.toLowerCase().includes(q) || + category.description.toLowerCase().includes(q); + + for (const toolName of category.tools) { + if (toolName.toLowerCase().includes(q) || categoryMatch) { + matches.push({ + category: category.name, + tool: toolName, + description: `${toolName} (${category.name})`, + }); + } + } + } + + return matches.slice(0, 20); // Limit results +} + +/** + * Get statistics about the tool registry + */ +export function getRegistryStats() { + const routedToolCount = getRoutedToolNames().length; + const directToolCount = directToolNames.length; + + return { + total_categories: toolCategories.length, + total_routed_tools: routedToolCount, + total_direct_tools: directToolCount, + total_tools: routedToolCount + directToolCount, + categories: toolCategories.map((c) => ({ + name: c.name, + tool_count: c.tools.length, + })), + }; +} + +// Initialize on module load +initializeRegistry(); diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index 1fcf905..a021f3f 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -161,15 +161,37 @@ 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}})"), + 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; @@ -210,7 +232,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp "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)"), + 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); @@ -220,20 +244,24 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp : "unknown"; const fieldLines = Object.entries(result.fields ?? {}).map( ([name, f]: [string, any]) => - ` ${name}: "${f.value}" @ (${f.x}, ${f.y}, angle=${f.angle}°)` + ` ${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")}`, - }], + 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"}`, - }], + content: [ + { + type: "text", + text: `Failed to get component: ${result.message || "Unknown error"}`, + }, + ], }; }, ); @@ -580,8 +608,14 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp 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')"), + 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"), @@ -595,7 +629,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp const comps = result.components || []; if (comps.length === 0) { return { - content: [{ type: "text", text: "No components found in schematic." }], + content: [ + { type: "text", text: "No components found in schematic." }, + ], }; } const lines = comps.map( @@ -656,7 +692,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp } return { content: [ - { type: "text", text: `Failed to list nets: ${result.message || "Unknown error"}` }, + { + type: "text", + text: `Failed to list nets: ${result.message || "Unknown error"}`, + }, ], isError: true, }; @@ -694,7 +733,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp } return { content: [ - { type: "text", text: `Failed to list wires: ${result.message || "Unknown error"}` }, + { + type: "text", + text: `Failed to list wires: ${result.message || "Unknown error"}`, + }, ], isError: true, }; @@ -732,7 +774,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp } return { content: [ - { type: "text", text: `Failed to list labels: ${result.message || "Unknown error"}` }, + { + type: "text", + text: `Failed to list labels: ${result.message || "Unknown error"}`, + }, ], isError: true, }; @@ -789,10 +834,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp 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"), + mirror: z.enum(["x", "y"]).optional().describe("Optional mirror axis"), }, async (args: { schematicPath: string; @@ -1036,8 +1078,14 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp .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)"), + 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; @@ -1207,19 +1255,35 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp "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"), + 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)"), + 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; + 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) { @@ -1227,27 +1291,40 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp return { content: [{ type: "text", text: result.imageData }] }; } return { - content: [{ - type: "image", - data: result.imageData, - mimeType: "image/png", - }], + content: [ + { + type: "image", + data: result.imageData, + mimeType: "image/png", + }, + ], }; } return { - content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }], + 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)"), + 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); @@ -1259,25 +1336,36 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp 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}]`); + 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)`); + 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`); + 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"}` }], + content: [ + { + type: "text", + text: `Failed: ${result.message || "Unknown error"}`, + }, + ], }; }, ); @@ -1287,7 +1375,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp "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"), + 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"), @@ -1295,39 +1385,56 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp }, async (args: { schematicPath: string; - x1: number; y1: number; x2: number; y2: number; + 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 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]`); + 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})`); + 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`); + 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})`); + 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"}` }], + content: [ + { + type: "text", + text: `Failed: ${result.message || "Unknown error"}`, + }, + ], }; }, ); @@ -1337,7 +1444,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp "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"), + schematicPath: z + .string() + .describe("Path to the .kicad_sch schematic file"), }, async (args: { schematicPath: string }) => { const result = await callKicadScript("find_wires_crossing_symbols", args); @@ -1346,14 +1455,20 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp 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})` + ` ${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`); + 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"}` }], + content: [ + { + type: "text", + text: `Failed: ${result.message || "Unknown error"}`, + }, + ], }; }, );