Merge pull request #44 from Kletternaut/feat/routing-component-tools-and-bugfixes
feat: Add missing routing/component tools and fix SWIG/UUID bugs
This commit is contained in:
96
CHANGELOG.md
96
CHANGELOG.md
@@ -2,6 +2,102 @@
|
|||||||
|
|
||||||
All notable changes to the KiCAD MCP Server project are documented here.
|
All notable changes to the KiCAD MCP Server project are documented here.
|
||||||
|
|
||||||
|
## [2.2.0-alpha] - 2026-02-27
|
||||||
|
|
||||||
|
### New MCP Tools (TypeScript layer – previously Python-only)
|
||||||
|
|
||||||
|
**Routing tools:**
|
||||||
|
- `delete_trace` - Delete traces by UUID, position or net name
|
||||||
|
- `query_traces` - Query/filter traces on the board
|
||||||
|
- `get_nets_list` - List all nets with net code and class
|
||||||
|
- `modify_trace` - Modify trace width or layer
|
||||||
|
- `create_netclass` - Create or update a net class
|
||||||
|
- `route_differential_pair` - Route a differential pair between two points
|
||||||
|
- `refill_zones` - Refill all copper zones ⚠️ SWIG segfault risk, prefer IPC/UI
|
||||||
|
|
||||||
|
**Component tools:**
|
||||||
|
- `get_component_pads` - Get all pad data for a component
|
||||||
|
- `get_component_list` - List all components on the board
|
||||||
|
- `get_pad_position` - Get absolute position of a specific pad
|
||||||
|
- `place_component_array` - Place components in a grid array
|
||||||
|
- `align_components` - Align components along an axis
|
||||||
|
- `duplicate_component` - Duplicate a component with offset
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- `routing.py`: Fix SwigPyObject UUID comparison (`str()` → `m_Uuid.AsString()`)
|
||||||
|
- `routing.py`: Fix SWIG iterator invalidation after `board.Remove()` by snapshotting `list(board.Tracks())`
|
||||||
|
- `routing.py`: Add `board.SetModified()` + `track = None` after `Remove()` to prevent dangling SWIG pointer crashes
|
||||||
|
- `routing.py`: Per-track `try/except` in `query_traces()` to skip invalid objects after bulk delete
|
||||||
|
- `routing.py`: Add missing return statement (mypy)
|
||||||
|
- `library.py`: Fix `search_footprints` parameter mapping (`search_term` → `pattern`)
|
||||||
|
- `library.py`: Fix field access (`fp.name` → `fp.full_name`)
|
||||||
|
- `library.py`: Accept both `pattern` and `search_term` parameter names
|
||||||
|
- `library.py`: Fix loop variable shadowing `Path` object (mypy)
|
||||||
|
- `design_rules.py`: Add type annotation for `violation_counts` (mypy)
|
||||||
|
|
||||||
|
### Pending additions (not yet committed)
|
||||||
|
|
||||||
|
**Datasheet tools:**
|
||||||
|
- `get_datasheet_url` - Return LCSC datasheet PDF URL and product page URL for a given
|
||||||
|
LCSC number (e.g. `C179739` → `https://www.lcsc.com/datasheet/C179739.pdf`).
|
||||||
|
No API key required – URL is constructed directly from the LCSC number.
|
||||||
|
- `enrich_datasheets` - Scan a `.kicad_sch` file and write LCSC datasheet URLs into
|
||||||
|
every symbol that has an `LCSC` property but an empty `Datasheet` field. After
|
||||||
|
enrichment the URL appears natively in KiCAD's symbol properties, footprint browser
|
||||||
|
and any other tool that reads the standard KiCAD `Datasheet` field.
|
||||||
|
Supports `dry_run=true` for preview without writing.
|
||||||
|
Implementation: `python/commands/datasheet_manager.py` (text-based, no `skip` writes)
|
||||||
|
|
||||||
|
**Schematic tools:**
|
||||||
|
- `delete_schematic_component` - Remove a placed symbol from a `.kicad_sch` file by
|
||||||
|
reference designator (e.g. `R1`, `U3`).
|
||||||
|
|
||||||
|
### Bug Fixes (pending)
|
||||||
|
|
||||||
|
- `schematic.ts` / `kicad_interface.py`: Fix missing `delete_schematic_component` MCP tool.
|
||||||
|
|
||||||
|
**Root cause (two separate issues):**
|
||||||
|
1. No MCP tool named `delete_schematic_component` existed. Claude had no way to call
|
||||||
|
it, so any "delete schematic component" request fell through to the PCB-only
|
||||||
|
`delete_component` tool, which searches `pcbnew.BOARD` and always returned
|
||||||
|
"Component not found" for schematic symbols.
|
||||||
|
2. `component_schematic.py::remove_component()` still used `skip` for writes.
|
||||||
|
PR #40 rewrote `DynamicSymbolLoader` (add path) to avoid `skip`-induced schematic
|
||||||
|
corruption, but `remove_component` (delete path) was not touched by that PR.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
- Added `delete_schematic_component` to the TypeScript tool layer (`schematic.ts`)
|
||||||
|
with clear docstring distinguishing it from the PCB `delete_component`.
|
||||||
|
- Implemented `_handle_delete_schematic_component` in `kicad_interface.py` using
|
||||||
|
direct text manipulation (parenthesis-depth tracking, same approach as PR #40).
|
||||||
|
Does not call `component_schematic.py::remove_component()` at all.
|
||||||
|
- Error message explicitly guides the user when the wrong tool is used:
|
||||||
|
*"note: this tool removes schematic symbols, use delete_component for PCB footprints"*
|
||||||
|
|
||||||
|
### Pending fixes (not yet committed)
|
||||||
|
|
||||||
|
- `connection_schematic.py` / `kicad_interface.py`: Fix `generate_netlist` missing
|
||||||
|
`schematic_path` parameter – without it `get_net_connections` always fell back to
|
||||||
|
proximity matching which only returns one connection per component (first wire hit,
|
||||||
|
then `break`). PinLocator was never invoked. Fix: added `schematic_path: Optional[Path]`
|
||||||
|
to `generate_netlist` signature and threaded it through to `get_net_connections`,
|
||||||
|
and updated `_handle_generate_netlist` in `kicad_interface.py` to pass `schematic_path`.
|
||||||
|
- `server.ts`: Fix KiCAD bundled Python (3.11.5) not being selected on Windows – the
|
||||||
|
detection condition `process.env.PYTHONPATH?.includes("KiCad")` was fragile and failed
|
||||||
|
in some environments, causing System Python 3.12 to be used instead. Since `pcbnew.pyd`
|
||||||
|
is compiled for KiCAD's Python 3.11.5, this resulted in `No module named 'pcbnew'`.
|
||||||
|
Fix: removed the condition, KiCAD bundled Python is now always preferred on Windows
|
||||||
|
when it exists at `C:\Program Files\KiCad\9.0\bin\python.exe`.
|
||||||
|
Also added `KICAD_PYTHON` to `claude_desktop_config.json` as explicit override.
|
||||||
|
- `pin_locator.py`: Fix `generate_netlist` timeout – `get_pin_location` and
|
||||||
|
`get_all_symbol_pins` called `Schematic(schematic_path)` on every single pin lookup,
|
||||||
|
causing O(nets × components × pins) schematic file loads (e.g. 400+ loads for a
|
||||||
|
medium schematic). Fix: added `_schematic_cache` dict to `PinLocator.__init__`,
|
||||||
|
schematic is now loaded once per path and reused.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.1.0-alpha] - 2026-01-10
|
## [2.1.0-alpha] - 2026-01-10
|
||||||
|
|
||||||
### Phase 1: Intelligent Schematic Wiring System - Core Infrastructure
|
### Phase 1: Intelligent Schematic Wiring System - Core Infrastructure
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ logger = logging.getLogger(__name__)
|
|||||||
try:
|
try:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
from commands.pin_locator import PinLocator
|
from commands.pin_locator import PinLocator
|
||||||
|
|
||||||
WIRE_MANAGER_AVAILABLE = True
|
WIRE_MANAGER_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("WireManager/PinLocator not available")
|
logger.warning("WireManager/PinLocator not available")
|
||||||
WIRE_MANAGER_AVAILABLE = False
|
WIRE_MANAGER_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
"""Manage connections between components in schematics"""
|
"""Manage connections between components in schematics"""
|
||||||
|
|
||||||
@@ -29,7 +31,12 @@ class ConnectionManager:
|
|||||||
return cls._pin_locator
|
return cls._pin_locator
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_wire(schematic_path: Path, start_point: list, end_point: list, properties: dict = None):
|
def add_wire(
|
||||||
|
schematic_path: Path,
|
||||||
|
start_point: list,
|
||||||
|
end_point: list,
|
||||||
|
properties: dict = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Add a wire between two points using WireManager
|
Add a wire between two points using WireManager
|
||||||
|
|
||||||
@@ -47,11 +54,18 @@ class ConnectionManager:
|
|||||||
logger.error("WireManager not available")
|
logger.error("WireManager not available")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
stroke_width = properties.get('stroke_width', 0) if properties else 0
|
stroke_width = properties.get("stroke_width", 0) if properties else 0
|
||||||
stroke_type = properties.get('stroke_type', 'default') if properties else 'default'
|
stroke_type = (
|
||||||
|
properties.get("stroke_type", "default") if properties else "default"
|
||||||
|
)
|
||||||
|
|
||||||
success = WireManager.add_wire(schematic_path, start_point, end_point,
|
success = WireManager.add_wire(
|
||||||
stroke_width=stroke_width, stroke_type=stroke_type)
|
schematic_path,
|
||||||
|
start_point,
|
||||||
|
end_point,
|
||||||
|
stroke_width=stroke_width,
|
||||||
|
stroke_type=stroke_type,
|
||||||
|
)
|
||||||
return success
|
return success
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding wire: {e}")
|
logger.error(f"Error adding wire: {e}")
|
||||||
@@ -70,7 +84,7 @@ class ConnectionManager:
|
|||||||
[x, y] coordinates or None if pin not found
|
[x, y] coordinates or None if pin not found
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not hasattr(symbol, 'pin'):
|
if not hasattr(symbol, "pin"):
|
||||||
logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
|
logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -82,7 +96,9 @@ class ConnectionManager:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not target_pin:
|
if not target_pin:
|
||||||
logger.warning(f"Pin '{pin_name}' not found on {symbol.property.Reference.value}")
|
logger.warning(
|
||||||
|
f"Pin '{pin_name}' not found on {symbol.property.Reference.value}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get pin location relative to symbol
|
# Get pin location relative to symbol
|
||||||
@@ -101,8 +117,14 @@ class ConnectionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_connection(schematic_path: Path, source_ref: str, source_pin: str,
|
def add_connection(
|
||||||
target_ref: str, target_pin: str, routing: str = 'direct'):
|
schematic_path: Path,
|
||||||
|
source_ref: str,
|
||||||
|
source_pin: str,
|
||||||
|
target_ref: str,
|
||||||
|
target_pin: str,
|
||||||
|
routing: str = "direct",
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Add a wire connection between two component pins
|
Add a wire connection between two component pins
|
||||||
|
|
||||||
@@ -128,31 +150,41 @@ class ConnectionManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get pin locations
|
# Get pin locations
|
||||||
source_loc = locator.get_pin_location(schematic_path, source_ref, source_pin)
|
source_loc = locator.get_pin_location(
|
||||||
target_loc = locator.get_pin_location(schematic_path, target_ref, target_pin)
|
schematic_path, source_ref, source_pin
|
||||||
|
)
|
||||||
|
target_loc = locator.get_pin_location(
|
||||||
|
schematic_path, target_ref, target_pin
|
||||||
|
)
|
||||||
|
|
||||||
if not source_loc or not target_loc:
|
if not source_loc or not target_loc:
|
||||||
logger.error("Could not determine pin locations")
|
logger.error("Could not determine pin locations")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create wire based on routing style
|
# Create wire based on routing style
|
||||||
if routing == 'direct':
|
if routing == "direct":
|
||||||
# Simple direct wire
|
# Simple direct wire
|
||||||
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
|
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
|
||||||
elif routing == 'orthogonal_h':
|
elif routing == "orthogonal_h":
|
||||||
# Orthogonal routing (horizontal first)
|
# Orthogonal routing (horizontal first)
|
||||||
path = WireManager.create_orthogonal_path(source_loc, target_loc, prefer_horizontal_first=True)
|
path = WireManager.create_orthogonal_path(
|
||||||
|
source_loc, target_loc, prefer_horizontal_first=True
|
||||||
|
)
|
||||||
success = WireManager.add_polyline_wire(schematic_path, path)
|
success = WireManager.add_polyline_wire(schematic_path, path)
|
||||||
elif routing == 'orthogonal_v':
|
elif routing == "orthogonal_v":
|
||||||
# Orthogonal routing (vertical first)
|
# Orthogonal routing (vertical first)
|
||||||
path = WireManager.create_orthogonal_path(source_loc, target_loc, prefer_horizontal_first=False)
|
path = WireManager.create_orthogonal_path(
|
||||||
|
source_loc, target_loc, prefer_horizontal_first=False
|
||||||
|
)
|
||||||
success = WireManager.add_polyline_wire(schematic_path, path)
|
success = WireManager.add_polyline_wire(schematic_path, path)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Unknown routing style: {routing}")
|
logger.error(f"Unknown routing style: {routing}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})")
|
logger.info(
|
||||||
|
f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -160,6 +192,7 @@ class ConnectionManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding connection: {e}")
|
logger.error(f"Error adding connection: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -177,13 +210,12 @@ class ConnectionManager:
|
|||||||
Label object or None on error
|
Label object or None on error
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not hasattr(schematic, 'label'):
|
if not hasattr(schematic, "label"):
|
||||||
logger.error("Schematic does not have label collection")
|
logger.error("Schematic does not have label collection")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
label = schematic.label.append(
|
label = schematic.label.append(
|
||||||
text=net_name,
|
text=net_name, at={"x": position[0], "y": position[1]}
|
||||||
at={'x': position[0], 'y': position[1]}
|
|
||||||
)
|
)
|
||||||
logger.info(f"Added net label '{net_name}' at {position}")
|
logger.info(f"Added net label '{net_name}' at {position}")
|
||||||
return label
|
return label
|
||||||
@@ -192,7 +224,9 @@ class ConnectionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def connect_to_net(schematic_path: Path, component_ref: str, pin_name: str, net_name: str):
|
def connect_to_net(
|
||||||
|
schematic_path: Path, component_ref: str, pin_name: str, net_name: str
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Connect a component pin to a named net using a wire stub and label
|
Connect a component pin to a named net using a wire stub and label
|
||||||
|
|
||||||
@@ -231,7 +265,9 @@ class ConnectionManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Add label at the end of the stub using WireManager
|
# Add label at the end of the stub using WireManager
|
||||||
label_success = WireManager.add_label(schematic_path, net_name, stub_end, label_type='label')
|
label_success = WireManager.add_label(
|
||||||
|
schematic_path, net_name, stub_end, label_type="label"
|
||||||
|
)
|
||||||
if not label_success:
|
if not label_success:
|
||||||
logger.error(f"Failed to add net label '{net_name}'")
|
logger.error(f"Failed to add net label '{net_name}'")
|
||||||
return False
|
return False
|
||||||
@@ -242,11 +278,14 @@ class ConnectionManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error connecting to net: {e}")
|
logger.error(f"Error connecting to net: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_net_connections(schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None):
|
def get_net_connections(
|
||||||
|
schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Get all connections for a named net using wire graph analysis
|
Get all connections for a named net using wire graph analysis
|
||||||
|
|
||||||
@@ -273,14 +312,14 @@ class ConnectionManager:
|
|||||||
return dx < tolerance and dy < tolerance
|
return dx < tolerance and dy < tolerance
|
||||||
|
|
||||||
# 1. Find all labels with this net name
|
# 1. Find all labels with this net name
|
||||||
if not hasattr(schematic, 'label'):
|
if not hasattr(schematic, "label"):
|
||||||
logger.warning("Schematic has no labels")
|
logger.warning("Schematic has no labels")
|
||||||
return connections
|
return connections
|
||||||
|
|
||||||
net_label_positions = []
|
net_label_positions = []
|
||||||
for label in schematic.label:
|
for label in schematic.label:
|
||||||
if hasattr(label, 'value') and label.value == net_name:
|
if hasattr(label, "value") and label.value == net_name:
|
||||||
if hasattr(label, 'at') and hasattr(label.at, 'value'):
|
if hasattr(label, "at") and hasattr(label.at, "value"):
|
||||||
pos = label.at.value
|
pos = label.at.value
|
||||||
net_label_positions.append([float(pos[0]), float(pos[1])])
|
net_label_positions.append([float(pos[0]), float(pos[1])])
|
||||||
|
|
||||||
@@ -288,21 +327,25 @@ class ConnectionManager:
|
|||||||
logger.info(f"No labels found for net '{net_name}'")
|
logger.info(f"No labels found for net '{net_name}'")
|
||||||
return connections
|
return connections
|
||||||
|
|
||||||
logger.debug(f"Found {len(net_label_positions)} labels for net '{net_name}'")
|
logger.debug(
|
||||||
|
f"Found {len(net_label_positions)} labels for net '{net_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
# 2. Find all wires connected to these label positions
|
# 2. Find all wires connected to these label positions
|
||||||
if not hasattr(schematic, 'wire'):
|
if not hasattr(schematic, "wire"):
|
||||||
logger.warning("Schematic has no wires")
|
logger.warning("Schematic has no wires")
|
||||||
return connections
|
return connections
|
||||||
|
|
||||||
connected_wire_points = set()
|
connected_wire_points = set()
|
||||||
for wire in schematic.wire:
|
for wire in schematic.wire:
|
||||||
if hasattr(wire, 'pts') and hasattr(wire.pts, 'xy'):
|
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
|
||||||
# Get all points in this wire (polyline)
|
# Get all points in this wire (polyline)
|
||||||
wire_points = []
|
wire_points = []
|
||||||
for point in wire.pts.xy:
|
for point in wire.pts.xy:
|
||||||
if hasattr(point, 'value'):
|
if hasattr(point, "value"):
|
||||||
wire_points.append([float(point.value[0]), float(point.value[1])])
|
wire_points.append(
|
||||||
|
[float(point.value[0]), float(point.value[1])]
|
||||||
|
)
|
||||||
|
|
||||||
# Check if any wire point touches a label
|
# Check if any wire point touches a label
|
||||||
wire_connected = False
|
wire_connected = False
|
||||||
@@ -323,10 +366,12 @@ class ConnectionManager:
|
|||||||
logger.debug(f"No wires connected to net '{net_name}' labels")
|
logger.debug(f"No wires connected to net '{net_name}' labels")
|
||||||
return connections
|
return connections
|
||||||
|
|
||||||
logger.debug(f"Found {len(connected_wire_points)} wire connection points for net '{net_name}'")
|
logger.debug(
|
||||||
|
f"Found {len(connected_wire_points)} wire connection points for net '{net_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Find component pins at wire endpoints
|
# 3. Find component pins at wire endpoints
|
||||||
if not hasattr(schematic, 'symbol'):
|
if not hasattr(schematic, "symbol"):
|
||||||
logger.warning("Schematic has no symbols")
|
logger.warning("Schematic has no symbols")
|
||||||
return connections
|
return connections
|
||||||
|
|
||||||
@@ -337,15 +382,15 @@ class ConnectionManager:
|
|||||||
|
|
||||||
for symbol in schematic.symbol:
|
for symbol in schematic.symbol:
|
||||||
# Skip template symbols
|
# Skip template symbols
|
||||||
if not hasattr(symbol.property, 'Reference'):
|
if not hasattr(symbol.property, "Reference"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ref = symbol.property.Reference.value
|
ref = symbol.property.Reference.value
|
||||||
if ref.startswith('_TEMPLATE'):
|
if ref.startswith("_TEMPLATE"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get lib_id for pin location lookup
|
# Get lib_id for pin location lookup
|
||||||
lib_id = symbol.lib_id.value if hasattr(symbol, 'lib_id') else None
|
lib_id = symbol.lib_id.value if hasattr(symbol, "lib_id") else None
|
||||||
if not lib_id:
|
if not lib_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -360,17 +405,18 @@ class ConnectionManager:
|
|||||||
# Check each pin
|
# Check each pin
|
||||||
for pin_num, pin_data in pins.items():
|
for pin_num, pin_data in pins.items():
|
||||||
# Get pin location
|
# Get pin location
|
||||||
pin_loc = locator.get_pin_location(schematic_path, ref, pin_num)
|
pin_loc = locator.get_pin_location(
|
||||||
|
schematic_path, ref, pin_num
|
||||||
|
)
|
||||||
if not pin_loc:
|
if not pin_loc:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if pin coincides with any wire point
|
# Check if pin coincides with any wire point
|
||||||
for wire_pt in connected_wire_points:
|
for wire_pt in connected_wire_points:
|
||||||
if points_coincide(pin_loc, list(wire_pt)):
|
if points_coincide(pin_loc, list(wire_pt)):
|
||||||
connections.append({
|
connections.append(
|
||||||
"component": ref,
|
{"component": ref, "pin": pin_num}
|
||||||
"pin": pin_num
|
)
|
||||||
})
|
|
||||||
break # Pin found, no need to check more wire points
|
break # Pin found, no need to check more wire points
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -380,7 +426,7 @@ class ConnectionManager:
|
|||||||
|
|
||||||
# Fallback: proximity-based matching if no PinLocator
|
# Fallback: proximity-based matching if no PinLocator
|
||||||
if not locator or not schematic_path:
|
if not locator or not schematic_path:
|
||||||
symbol_pos = symbol.at.value if hasattr(symbol, 'at') else None
|
symbol_pos = symbol.at.value if hasattr(symbol, "at") else None
|
||||||
if not symbol_pos:
|
if not symbol_pos:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -389,12 +435,11 @@ class ConnectionManager:
|
|||||||
|
|
||||||
# Check if symbol is near any wire point (within 10mm)
|
# Check if symbol is near any wire point (within 10mm)
|
||||||
for wire_pt in connected_wire_points:
|
for wire_pt in connected_wire_points:
|
||||||
dist = ((symbol_x - wire_pt[0])**2 + (symbol_y - wire_pt[1])**2)**0.5
|
dist = (
|
||||||
|
(symbol_x - wire_pt[0]) ** 2 + (symbol_y - wire_pt[1]) ** 2
|
||||||
|
) ** 0.5
|
||||||
if dist < 10.0: # 10mm proximity threshold
|
if dist < 10.0: # 10mm proximity threshold
|
||||||
connections.append({
|
connections.append({"component": ref, "pin": "unknown"})
|
||||||
"component": ref,
|
|
||||||
"pin": "unknown"
|
|
||||||
})
|
|
||||||
break # Only add once per component
|
break # Only add once per component
|
||||||
|
|
||||||
logger.info(f"Found {len(connections)} connections for net '{net_name}'")
|
logger.info(f"Found {len(connections)} connections for net '{net_name}'")
|
||||||
@@ -403,14 +448,20 @@ class ConnectionManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting net connections: {e}")
|
logger.error(f"Error getting net connections: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_netlist(schematic: Schematic):
|
def generate_netlist(schematic: Schematic, schematic_path: Optional[Path] = None):
|
||||||
"""
|
"""
|
||||||
Generate a netlist from the schematic
|
Generate a netlist from the schematic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schematic: Schematic object
|
||||||
|
schematic_path: Optional path to schematic file (enables accurate pin matching
|
||||||
|
via PinLocator; without it, only one connection per component is found)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with net information:
|
Dictionary with net information:
|
||||||
{
|
{
|
||||||
@@ -431,47 +482,58 @@ class ConnectionManager:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
netlist = {
|
netlist = {"nets": [], "components": []}
|
||||||
"nets": [],
|
|
||||||
"components": []
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gather all components
|
# Gather all components
|
||||||
if hasattr(schematic, 'symbol'):
|
if hasattr(schematic, "symbol"):
|
||||||
for symbol in schematic.symbol:
|
for symbol in schematic.symbol:
|
||||||
component_info = {
|
component_info = {
|
||||||
"reference": symbol.property.Reference.value,
|
"reference": symbol.property.Reference.value,
|
||||||
"value": symbol.property.Value.value if hasattr(symbol.property, 'Value') else "",
|
"value": (
|
||||||
"footprint": symbol.property.Footprint.value if hasattr(symbol.property, 'Footprint') else ""
|
symbol.property.Value.value
|
||||||
|
if hasattr(symbol.property, "Value")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"footprint": (
|
||||||
|
symbol.property.Footprint.value
|
||||||
|
if hasattr(symbol.property, "Footprint")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
netlist["components"].append(component_info)
|
netlist["components"].append(component_info)
|
||||||
|
|
||||||
# Gather all nets from labels
|
# Gather all nets from labels
|
||||||
if hasattr(schematic, 'label'):
|
if hasattr(schematic, "label"):
|
||||||
net_names = set()
|
net_names = set()
|
||||||
for label in schematic.label:
|
for label in schematic.label:
|
||||||
if hasattr(label, 'value'):
|
if hasattr(label, "value"):
|
||||||
net_names.add(label.value)
|
net_names.add(label.value)
|
||||||
|
|
||||||
# For each net, get connections
|
# For each net, get connections
|
||||||
for net_name in net_names:
|
for net_name in net_names:
|
||||||
connections = ConnectionManager.get_net_connections(schematic, net_name)
|
connections = ConnectionManager.get_net_connections(
|
||||||
|
schematic, net_name, schematic_path
|
||||||
|
)
|
||||||
if connections:
|
if connections:
|
||||||
netlist["nets"].append({
|
netlist["nets"].append(
|
||||||
"name": net_name,
|
{"name": net_name, "connections": connections}
|
||||||
"connections": connections
|
)
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components")
|
logger.info(
|
||||||
|
f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components"
|
||||||
|
)
|
||||||
return netlist
|
return netlist
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error generating netlist: {e}")
|
logger.error(f"Error generating netlist: {e}")
|
||||||
return {"nets": [], "components": []}
|
return {"nets": [], "components": []}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
# Example Usage (for testing)
|
# Example Usage (for testing)
|
||||||
from schematic import SchematicManager # Assuming schematic.py is in the same directory
|
from schematic import (
|
||||||
|
SchematicManager,
|
||||||
|
) # Assuming schematic.py is in the same directory
|
||||||
|
|
||||||
# Create a new schematic
|
# Create a new schematic
|
||||||
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
|
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
|
||||||
|
|||||||
289
python/commands/datasheet_manager.py
Normal file
289
python/commands/datasheet_manager.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
"""
|
||||||
|
Datasheet Manager for KiCAD MCP Server
|
||||||
|
|
||||||
|
Enriches KiCAD schematic symbols with datasheet URLs derived from LCSC part numbers.
|
||||||
|
Uses direct text manipulation (like dynamic_symbol_loader.py) to avoid
|
||||||
|
skip-library-induced schematic corruption.
|
||||||
|
|
||||||
|
URL schema: https://www.lcsc.com/datasheet/{LCSC#}.pdf
|
||||||
|
No API key required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
LCSC_DATASHEET_URL = "https://www.lcsc.com/datasheet/{lcsc}.pdf"
|
||||||
|
LCSC_PRODUCT_URL = "https://www.lcsc.com/product-detail/{lcsc}.html"
|
||||||
|
|
||||||
|
# Values treated as "empty" datasheet
|
||||||
|
EMPTY_DATASHEET_VALUES = {"~", "", "~{DATASHEET}"}
|
||||||
|
|
||||||
|
|
||||||
|
class DatasheetManager:
|
||||||
|
"""
|
||||||
|
Enriches KiCAD schematics with LCSC datasheet URLs.
|
||||||
|
|
||||||
|
Reads .kicad_sch files, finds symbol instances that have an LCSC property
|
||||||
|
but an empty Datasheet property, and fills in the LCSC datasheet URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_lcsc(lcsc: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Normalize LCSC number to standard format 'C123456'.
|
||||||
|
|
||||||
|
Accepts: 'C123456', '123456', 'c123456'
|
||||||
|
Returns: 'C123456' or None if invalid
|
||||||
|
"""
|
||||||
|
lcsc = lcsc.strip()
|
||||||
|
if not lcsc:
|
||||||
|
return None
|
||||||
|
# Remove leading C/c
|
||||||
|
without_prefix = lcsc.lstrip("Cc")
|
||||||
|
if without_prefix.isdigit():
|
||||||
|
return f"C{without_prefix}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_lib_symbols_range(lines: List[str]):
|
||||||
|
"""
|
||||||
|
Find the line range of the (lib_symbols ...) section.
|
||||||
|
Returns (start, end) line indices or (None, None) if not found.
|
||||||
|
These lines must be excluded from symbol-instance processing.
|
||||||
|
"""
|
||||||
|
lib_sym_start = None
|
||||||
|
lib_sym_end = None
|
||||||
|
depth = 0
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if "(lib_symbols" in line and lib_sym_start is None:
|
||||||
|
lib_sym_start = i
|
||||||
|
depth = 0
|
||||||
|
for ch in line:
|
||||||
|
if ch == "(":
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
elif lib_sym_start is not None and lib_sym_end is None:
|
||||||
|
for ch in line:
|
||||||
|
if ch == "(":
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
lib_sym_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
return lib_sym_start, lib_sym_end
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_symbol_block(
|
||||||
|
lines: List[str], block_start: int, block_end: int
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Extract LCSC and Datasheet info from a placed symbol block.
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
- lcsc: normalized LCSC number or None
|
||||||
|
- datasheet_line: line index of Datasheet property or None
|
||||||
|
- datasheet_value: current Datasheet value or None
|
||||||
|
"""
|
||||||
|
lcsc_value = None
|
||||||
|
datasheet_line_idx = None
|
||||||
|
datasheet_current = None
|
||||||
|
|
||||||
|
for k in range(block_start, block_end + 1):
|
||||||
|
line = lines[k]
|
||||||
|
|
||||||
|
lcsc_match = re.search(r'\(property\s+"LCSC"\s+"([^"]*)"', line)
|
||||||
|
if lcsc_match:
|
||||||
|
lcsc_value = lcsc_match.group(1)
|
||||||
|
|
||||||
|
ds_match = re.search(r'\(property\s+"Datasheet"\s+"([^"]*)"', line)
|
||||||
|
if ds_match:
|
||||||
|
datasheet_line_idx = k
|
||||||
|
datasheet_current = ds_match.group(1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"lcsc": lcsc_value,
|
||||||
|
"datasheet_line": datasheet_line_idx,
|
||||||
|
"datasheet_value": datasheet_current,
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrich_schematic(
|
||||||
|
self, schematic_path: Path, dry_run: bool = False
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Scan a .kicad_sch file and fill in missing LCSC datasheet URLs.
|
||||||
|
|
||||||
|
For each placed symbol that has:
|
||||||
|
- (property "LCSC" "C123456") set
|
||||||
|
- (property "Datasheet" "~") or empty
|
||||||
|
|
||||||
|
Sets:
|
||||||
|
- (property "Datasheet" "https://www.lcsc.com/datasheet/C123456.pdf")
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schematic_path: Path to .kicad_sch file
|
||||||
|
dry_run: If True, return what would be changed without writing
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"updated": <count>,
|
||||||
|
"already_set": <count>,
|
||||||
|
"no_lcsc": <count>,
|
||||||
|
"no_datasheet_field": <count>,
|
||||||
|
"details": [{"reference": "...", "lcsc": "...", "url": "..."}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
schematic_path = Path(schematic_path)
|
||||||
|
if not schematic_path.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Schematic not found: {schematic_path}",
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
lines = content.split("\n")
|
||||||
|
new_lines = list(lines)
|
||||||
|
|
||||||
|
lib_sym_start, lib_sym_end = self._find_lib_symbols_range(lines)
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
already_set = 0
|
||||||
|
no_lcsc = 0
|
||||||
|
no_datasheet_field = 0
|
||||||
|
details = []
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(new_lines):
|
||||||
|
line = new_lines[i]
|
||||||
|
|
||||||
|
# Skip lib_symbols section
|
||||||
|
if lib_sym_start is not None and lib_sym_end is not None:
|
||||||
|
if lib_sym_start <= i <= lib_sym_end:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detect placed symbol: (symbol (lib_id "...")
|
||||||
|
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", line):
|
||||||
|
block_start = i
|
||||||
|
block_depth = 0
|
||||||
|
for ch in line:
|
||||||
|
if ch == "(":
|
||||||
|
block_depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
block_depth -= 1
|
||||||
|
|
||||||
|
j = i + 1
|
||||||
|
while j < len(new_lines) and block_depth > 0:
|
||||||
|
for ch in new_lines[j]:
|
||||||
|
if ch == "(":
|
||||||
|
block_depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
block_depth -= 1
|
||||||
|
if block_depth > 0:
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
block_end = j
|
||||||
|
info = self._process_symbol_block(new_lines, block_start, block_end)
|
||||||
|
|
||||||
|
raw_lcsc = info["lcsc"]
|
||||||
|
ds_line = info["datasheet_line"]
|
||||||
|
ds_value = info["datasheet_value"]
|
||||||
|
|
||||||
|
# Extract reference for reporting
|
||||||
|
ref_match = None
|
||||||
|
for k in range(block_start, block_end + 1):
|
||||||
|
m = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', new_lines[k])
|
||||||
|
if m:
|
||||||
|
ref_match = m.group(1)
|
||||||
|
break
|
||||||
|
reference = ref_match or "?"
|
||||||
|
|
||||||
|
if not raw_lcsc:
|
||||||
|
no_lcsc += 1
|
||||||
|
elif ds_line is None:
|
||||||
|
no_datasheet_field += 1
|
||||||
|
logger.warning(
|
||||||
|
f"Symbol {reference} has LCSC={raw_lcsc} but no Datasheet property"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lcsc_norm = self._normalize_lcsc(raw_lcsc)
|
||||||
|
if not lcsc_norm:
|
||||||
|
no_lcsc += 1
|
||||||
|
elif ds_value not in EMPTY_DATASHEET_VALUES:
|
||||||
|
already_set += 1
|
||||||
|
logger.debug(
|
||||||
|
f"Symbol {reference}: Datasheet already set to {ds_value!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
url = LCSC_DATASHEET_URL.format(lcsc=lcsc_norm)
|
||||||
|
if not dry_run:
|
||||||
|
new_lines[ds_line] = re.sub(
|
||||||
|
r'(property\s+"Datasheet"\s+)"[^"]*"',
|
||||||
|
f'\\1"{url}"',
|
||||||
|
new_lines[ds_line],
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
details.append(
|
||||||
|
{
|
||||||
|
"reference": reference,
|
||||||
|
"lcsc": lcsc_norm,
|
||||||
|
"url": url,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"{'[DRY RUN] ' if dry_run else ''}Set Datasheet for "
|
||||||
|
f"{reference} ({lcsc_norm}): {url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
i = block_end + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not dry_run and updated > 0:
|
||||||
|
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(new_lines))
|
||||||
|
logger.info(
|
||||||
|
f"Saved {schematic_path.name}: {updated} datasheet URLs written"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"updated": updated,
|
||||||
|
"already_set": already_set,
|
||||||
|
"no_lcsc": no_lcsc,
|
||||||
|
"no_datasheet_field": no_datasheet_field,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
"details": details,
|
||||||
|
"schematic": str(schematic_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_datasheet_url(self, lcsc: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Return the LCSC datasheet URL for a given LCSC number.
|
||||||
|
No network request – pure URL construction.
|
||||||
|
"""
|
||||||
|
norm = self._normalize_lcsc(lcsc)
|
||||||
|
if norm:
|
||||||
|
return LCSC_DATASHEET_URL.format(lcsc=norm)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_product_url(self, lcsc: str) -> Optional[str]:
|
||||||
|
"""Return the LCSC product page URL."""
|
||||||
|
norm = self._normalize_lcsc(lcsc)
|
||||||
|
if norm:
|
||||||
|
return LCSC_PRODUCT_URL.format(lcsc=norm)
|
||||||
|
return None
|
||||||
@@ -7,7 +7,8 @@ import pcbnew
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional, List, Tuple
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class DesignRuleCommands:
|
class DesignRuleCommands:
|
||||||
"""Handles design rule checking and configuration"""
|
"""Handles design rule checking and configuration"""
|
||||||
@@ -23,7 +24,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
design_settings = self.board.GetDesignSettings()
|
design_settings = self.board.GetDesignSettings()
|
||||||
@@ -57,9 +58,13 @@ class DesignRuleCommands:
|
|||||||
|
|
||||||
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
|
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
|
||||||
if "microViaDiameter" in params:
|
if "microViaDiameter" in params:
|
||||||
design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale)
|
design_settings.m_MicroViasMinSize = int(
|
||||||
|
params["microViaDiameter"] * scale
|
||||||
|
)
|
||||||
if "microViaDrill" in params:
|
if "microViaDrill" in params:
|
||||||
design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale)
|
design_settings.m_MicroViasMinDrill = int(
|
||||||
|
params["microViaDrill"] * scale
|
||||||
|
)
|
||||||
|
|
||||||
# Set minimum values
|
# Set minimum values
|
||||||
if "minTrackWidth" in params:
|
if "minTrackWidth" in params:
|
||||||
@@ -72,13 +77,19 @@ class DesignRuleCommands:
|
|||||||
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
|
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
|
||||||
|
|
||||||
if "minMicroViaDiameter" in params:
|
if "minMicroViaDiameter" in params:
|
||||||
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
|
design_settings.m_MicroViasMinSize = int(
|
||||||
|
params["minMicroViaDiameter"] * scale
|
||||||
|
)
|
||||||
if "minMicroViaDrill" in params:
|
if "minMicroViaDrill" in params:
|
||||||
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
|
design_settings.m_MicroViasMinDrill = int(
|
||||||
|
params["minMicroViaDrill"] * scale
|
||||||
|
)
|
||||||
|
|
||||||
# KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
|
# KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
|
||||||
if "minHoleDiameter" in params:
|
if "minHoleDiameter" in params:
|
||||||
design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale)
|
design_settings.m_MinThroughDrill = int(
|
||||||
|
params["minHoleDiameter"] * scale
|
||||||
|
)
|
||||||
|
|
||||||
# KiCAD 9.0: Added hole clearance settings
|
# KiCAD 9.0: Added hole clearance settings
|
||||||
if "holeClearance" in params:
|
if "holeClearance" in params:
|
||||||
@@ -102,13 +113,13 @@ class DesignRuleCommands:
|
|||||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||||
"holeClearance": design_settings.m_HoleClearance / scale,
|
"holeClearance": design_settings.m_HoleClearance / scale,
|
||||||
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
||||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale
|
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "Updated design rules",
|
"message": "Updated design rules",
|
||||||
"rules": response_rules
|
"rules": response_rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -116,7 +127,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to set design rules",
|
"message": "Failed to set design rules",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -126,7 +137,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
design_settings = self.board.GetDesignSettings()
|
design_settings = self.board.GetDesignSettings()
|
||||||
@@ -138,42 +149,34 @@ class DesignRuleCommands:
|
|||||||
"clearance": design_settings.m_MinClearance / scale,
|
"clearance": design_settings.m_MinClearance / scale,
|
||||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||||
|
|
||||||
# Via settings (current values from methods)
|
# Via settings (current values from methods)
|
||||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||||
|
|
||||||
# Via minimum values
|
# Via minimum values
|
||||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
||||||
|
|
||||||
# Micro via settings
|
# Micro via settings
|
||||||
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||||
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||||
|
|
||||||
# KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
|
# KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
|
||||||
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
|
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
|
||||||
"holeClearance": design_settings.m_HoleClearance / scale,
|
"holeClearance": design_settings.m_HoleClearance / scale,
|
||||||
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
||||||
|
|
||||||
# Other constraints
|
# Other constraints
|
||||||
"copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
|
"copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
|
||||||
"silkClearance": design_settings.m_SilkClearance / scale,
|
"silkClearance": design_settings.m_SilkClearance / scale,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {"success": True, "rules": rules}
|
||||||
"success": True,
|
|
||||||
"rules": rules
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting design rules: {str(e)}")
|
logger.error(f"Error getting design rules: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get design rules",
|
"message": "Failed to get design rules",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -189,7 +192,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
report_path = params.get("reportPath")
|
report_path = params.get("reportPath")
|
||||||
@@ -200,7 +203,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Board file not found",
|
"message": "Board file not found",
|
||||||
"errorDetails": "Cannot run DRC without a saved board file"
|
"errorDetails": "Cannot run DRC without a saved board file",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find kicad-cli executable
|
# Find kicad-cli executable
|
||||||
@@ -209,23 +212,28 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "kicad-cli not found",
|
"message": "kicad-cli not found",
|
||||||
"errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH."
|
"errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create temporary JSON output file
|
# Create temporary JSON output file
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".json", delete=False
|
||||||
|
) as tmp:
|
||||||
json_output = tmp.name
|
json_output = tmp.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build command
|
# Build command
|
||||||
cmd = [
|
cmd = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb',
|
"pcb",
|
||||||
'drc',
|
"drc",
|
||||||
'--format', 'json',
|
"--format",
|
||||||
'--output', json_output,
|
"json",
|
||||||
'--units', 'mm',
|
"--output",
|
||||||
board_file
|
json_output,
|
||||||
|
"--units",
|
||||||
|
"mm",
|
||||||
|
board_file,
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info(f"Running DRC command: {' '.join(cmd)}")
|
logger.info(f"Running DRC command: {' '.join(cmd)}")
|
||||||
@@ -235,7 +243,7 @@ class DesignRuleCommands:
|
|||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=600 # 10 minute timeout for large boards (21MB PCB needs time)
|
timeout=600, # 10 minute timeout for large boards (21MB PCB needs time)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
@@ -243,32 +251,34 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "DRC command failed",
|
"message": "DRC command failed",
|
||||||
"errorDetails": result.stderr
|
"errorDetails": result.stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read JSON output
|
# Read JSON output
|
||||||
with open(json_output, 'r', encoding='utf-8') as f:
|
with open(json_output, "r", encoding="utf-8") as f:
|
||||||
drc_data = json.load(f)
|
drc_data = json.load(f)
|
||||||
|
|
||||||
# Parse violations from kicad-cli output
|
# Parse violations from kicad-cli output
|
||||||
violations = []
|
violations = []
|
||||||
violation_counts = {}
|
violation_counts: dict[str, int] = {}
|
||||||
severity_counts = {"error": 0, "warning": 0, "info": 0}
|
severity_counts = {"error": 0, "warning": 0, "info": 0}
|
||||||
|
|
||||||
for violation in drc_data.get('violations', []):
|
for violation in drc_data.get("violations", []):
|
||||||
vtype = violation.get("type", "unknown")
|
vtype = violation.get("type", "unknown")
|
||||||
vseverity = violation.get("severity", "error")
|
vseverity = violation.get("severity", "error")
|
||||||
|
|
||||||
violations.append({
|
violations.append(
|
||||||
"type": vtype,
|
{
|
||||||
"severity": vseverity,
|
"type": vtype,
|
||||||
"message": violation.get("description", ""),
|
"severity": vseverity,
|
||||||
"location": {
|
"message": violation.get("description", ""),
|
||||||
"x": violation.get("x", 0),
|
"location": {
|
||||||
"y": violation.get("y", 0),
|
"x": violation.get("x", 0),
|
||||||
"unit": "mm"
|
"y": violation.get("y", 0),
|
||||||
|
"unit": "mm",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
# Count violations by type
|
# Count violations by type
|
||||||
violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
|
violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
|
||||||
@@ -280,30 +290,39 @@ class DesignRuleCommands:
|
|||||||
# Determine where to save the violations file
|
# Determine where to save the violations file
|
||||||
board_dir = os.path.dirname(board_file)
|
board_dir = os.path.dirname(board_file)
|
||||||
board_name = os.path.splitext(os.path.basename(board_file))[0]
|
board_name = os.path.splitext(os.path.basename(board_file))[0]
|
||||||
violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json")
|
violations_file = os.path.join(
|
||||||
|
board_dir, f"{board_name}_drc_violations.json"
|
||||||
|
)
|
||||||
|
|
||||||
# Always save violations to JSON file (for large result sets)
|
# Always save violations to JSON file (for large result sets)
|
||||||
with open(violations_file, 'w', encoding='utf-8') as f:
|
with open(violations_file, "w", encoding="utf-8") as f:
|
||||||
json.dump({
|
json.dump(
|
||||||
"board": board_file,
|
{
|
||||||
"timestamp": drc_data.get("date", "unknown"),
|
"board": board_file,
|
||||||
"total_violations": len(violations),
|
"timestamp": drc_data.get("date", "unknown"),
|
||||||
"violation_counts": violation_counts,
|
"total_violations": len(violations),
|
||||||
"severity_counts": severity_counts,
|
"violation_counts": violation_counts,
|
||||||
"violations": violations
|
"severity_counts": severity_counts,
|
||||||
}, f, indent=2)
|
"violations": violations,
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
# Save text report if requested
|
# Save text report if requested
|
||||||
if report_path:
|
if report_path:
|
||||||
report_path = os.path.abspath(os.path.expanduser(report_path))
|
report_path = os.path.abspath(os.path.expanduser(report_path))
|
||||||
cmd_report = [
|
cmd_report = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb',
|
"pcb",
|
||||||
'drc',
|
"drc",
|
||||||
'--format', 'report',
|
"--format",
|
||||||
'--output', report_path,
|
"report",
|
||||||
'--units', 'mm',
|
"--output",
|
||||||
board_file
|
report_path,
|
||||||
|
"--units",
|
||||||
|
"mm",
|
||||||
|
board_file,
|
||||||
]
|
]
|
||||||
subprocess.run(cmd_report, capture_output=True, timeout=600)
|
subprocess.run(cmd_report, capture_output=True, timeout=600)
|
||||||
|
|
||||||
@@ -314,10 +333,10 @@ class DesignRuleCommands:
|
|||||||
"summary": {
|
"summary": {
|
||||||
"total": len(violations),
|
"total": len(violations),
|
||||||
"by_severity": severity_counts,
|
"by_severity": severity_counts,
|
||||||
"by_type": violation_counts
|
"by_type": violation_counts,
|
||||||
},
|
},
|
||||||
"violationsFile": violations_file,
|
"violationsFile": violations_file,
|
||||||
"reportPath": report_path if report_path else None
|
"reportPath": report_path if report_path else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@@ -330,14 +349,14 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "DRC command timed out",
|
"message": "DRC command timed out",
|
||||||
"errorDetails": "Command took longer than 600 seconds (10 minutes)"
|
"errorDetails": "Command took longer than 600 seconds (10 minutes)",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error running DRC: {str(e)}")
|
logger.error(f"Error running DRC: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to run DRC",
|
"message": "Failed to run DRC",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _find_kicad_cli(self) -> Optional[str]:
|
def _find_kicad_cli(self) -> Optional[str]:
|
||||||
@@ -399,7 +418,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
severity = params.get("severity", "all")
|
severity = params.get("severity", "all")
|
||||||
@@ -416,25 +435,27 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Violations file not found",
|
"message": "Violations file not found",
|
||||||
"errorDetails": "run_drc did not create violations file"
|
"errorDetails": "run_drc did not create violations file",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load violations from file
|
# Load violations from file
|
||||||
with open(violations_file, 'r', encoding='utf-8') as f:
|
with open(violations_file, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
all_violations = data.get("violations", [])
|
all_violations = data.get("violations", [])
|
||||||
|
|
||||||
# Filter by severity if specified
|
# Filter by severity if specified
|
||||||
if severity != "all":
|
if severity != "all":
|
||||||
filtered_violations = [v for v in all_violations if v.get("severity") == severity]
|
filtered_violations = [
|
||||||
|
v for v in all_violations if v.get("severity") == severity
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
filtered_violations = all_violations
|
filtered_violations = all_violations
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"violations": filtered_violations,
|
"violations": filtered_violations,
|
||||||
"violationsFile": violations_file # Include file path for reference
|
"violationsFile": violations_file, # Include file path for reference
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -442,5 +463,5 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get DRC violations",
|
"message": "Failed to get DRC violations",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class LibraryManager:
|
class LibraryManager:
|
||||||
@@ -85,7 +85,7 @@ class LibraryManager:
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(table_path, 'r') as f:
|
with open(table_path, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Simple regex-based parser for lib entries
|
# Simple regex-based parser for lib entries
|
||||||
@@ -103,7 +103,9 @@ class LibraryManager:
|
|||||||
self.libraries[nickname] = resolved_uri
|
self.libraries[nickname] = resolved_uri
|
||||||
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
|
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f" Could not resolve URI for library {nickname}: {uri}")
|
logger.warning(
|
||||||
|
f" Could not resolve URI for library {nickname}: {uri}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
|
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
|
||||||
@@ -124,23 +126,23 @@ class LibraryManager:
|
|||||||
|
|
||||||
# Common KiCAD environment variables
|
# Common KiCAD environment variables
|
||||||
env_vars = {
|
env_vars = {
|
||||||
'KICAD9_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
"KICAD9_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||||
'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
"KICAD8_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||||
'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
"KICAD_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||||
'KISYSMOD': self._find_kicad_footprint_dir(),
|
"KISYSMOD": self._find_kicad_footprint_dir(),
|
||||||
'KICAD9_3RD_PARTY': self._find_kicad_3rdparty_dir(),
|
"KICAD9_3RD_PARTY": self._find_kicad_3rdparty_dir(),
|
||||||
'KICAD8_3RD_PARTY': self._find_kicad_3rdparty_dir(),
|
"KICAD8_3RD_PARTY": self._find_kicad_3rdparty_dir(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Project directory
|
# Project directory
|
||||||
if self.project_path:
|
if self.project_path:
|
||||||
env_vars['KIPRJMOD'] = str(self.project_path)
|
env_vars["KIPRJMOD"] = str(self.project_path)
|
||||||
|
|
||||||
# Replace environment variables
|
# Replace environment variables
|
||||||
for var, value in env_vars.items():
|
for var, value in env_vars.items():
|
||||||
if value:
|
if value:
|
||||||
resolved = resolved.replace(f'${{{var}}}', value)
|
resolved = resolved.replace(f"${{{var}}}", value)
|
||||||
resolved = resolved.replace(f'${var}', value)
|
resolved = resolved.replace(f"${var}", value)
|
||||||
|
|
||||||
# Expand ~ to home directory
|
# Expand ~ to home directory
|
||||||
resolved = os.path.expanduser(resolved)
|
resolved = os.path.expanduser(resolved)
|
||||||
@@ -167,10 +169,10 @@ class LibraryManager:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Also check environment variable
|
# Also check environment variable
|
||||||
if 'KICAD9_FOOTPRINT_DIR' in os.environ:
|
if "KICAD9_FOOTPRINT_DIR" in os.environ:
|
||||||
possible_paths.insert(0, os.environ['KICAD9_FOOTPRINT_DIR'])
|
possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"])
|
||||||
if 'KICAD8_FOOTPRINT_DIR' in os.environ:
|
if "KICAD8_FOOTPRINT_DIR" in os.environ:
|
||||||
possible_paths.insert(0, os.environ['KICAD8_FOOTPRINT_DIR'])
|
possible_paths.insert(0, os.environ["KICAD8_FOOTPRINT_DIR"])
|
||||||
|
|
||||||
for path in possible_paths:
|
for path in possible_paths:
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
@@ -190,26 +192,36 @@ class LibraryManager:
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
# 1. Check shell environment variable first
|
# 1. Check shell environment variable first
|
||||||
if 'KICAD9_3RD_PARTY' in os.environ:
|
if "KICAD9_3RD_PARTY" in os.environ:
|
||||||
path = os.environ['KICAD9_3RD_PARTY']
|
path = os.environ["KICAD9_3RD_PARTY"]
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# 2. Check kicad_common.json for user-defined variables
|
# 2. Check kicad_common.json for user-defined variables
|
||||||
kicad_common_paths = [
|
kicad_common_paths = [
|
||||||
Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "kicad_common.json", # macOS
|
Path.home()
|
||||||
|
/ "Library"
|
||||||
|
/ "Preferences"
|
||||||
|
/ "kicad"
|
||||||
|
/ "9.0"
|
||||||
|
/ "kicad_common.json", # macOS
|
||||||
Path.home() / ".config" / "kicad" / "9.0" / "kicad_common.json", # Linux
|
Path.home() / ".config" / "kicad" / "9.0" / "kicad_common.json", # Linux
|
||||||
Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "kicad_common.json", # Windows
|
Path.home()
|
||||||
|
/ "AppData"
|
||||||
|
/ "Roaming"
|
||||||
|
/ "kicad"
|
||||||
|
/ "9.0"
|
||||||
|
/ "kicad_common.json", # Windows
|
||||||
]
|
]
|
||||||
|
|
||||||
for config_path in kicad_common_paths:
|
for config_path in kicad_common_paths:
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
try:
|
try:
|
||||||
with open(config_path, 'r') as f:
|
with open(config_path, "r") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
env_vars = config.get('environment', {}).get('vars', {})
|
env_vars = config.get("environment", {}).get("vars", {})
|
||||||
if env_vars and 'KICAD9_3RD_PARTY' in env_vars:
|
if env_vars and "KICAD9_3RD_PARTY" in env_vars:
|
||||||
path = env_vars['KICAD9_3RD_PARTY']
|
path = env_vars["KICAD9_3RD_PARTY"]
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
return path
|
return path
|
||||||
except (json.JSONDecodeError, KeyError, TypeError):
|
except (json.JSONDecodeError, KeyError, TypeError):
|
||||||
@@ -231,10 +243,10 @@ class LibraryManager:
|
|||||||
Path.home() / "Documents" / "KiCad" / version / "3rdparty",
|
Path.home() / "Documents" / "KiCad" / version / "3rdparty",
|
||||||
]
|
]
|
||||||
|
|
||||||
for path in possible_paths:
|
for candidate in possible_paths:
|
||||||
if path.exists():
|
if candidate.exists():
|
||||||
logger.info(f"Found KiCad 3rd party directory: {path}")
|
logger.info(f"Found KiCad 3rd party directory: {candidate}")
|
||||||
return str(path)
|
return str(candidate)
|
||||||
|
|
||||||
logger.warning("Could not find KiCad 3rd party directory")
|
logger.warning("Could not find KiCad 3rd party directory")
|
||||||
return None
|
return None
|
||||||
@@ -325,7 +337,9 @@ class LibraryManager:
|
|||||||
for library_nickname, library_path in self.libraries.items():
|
for library_nickname, library_path in self.libraries.items():
|
||||||
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
|
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
|
||||||
if fp_file.exists():
|
if fp_file.exists():
|
||||||
logger.info(f"Found footprint {footprint_name} in library {library_nickname}")
|
logger.info(
|
||||||
|
f"Found footprint {footprint_name} in library {library_nickname}"
|
||||||
|
)
|
||||||
return (library_path, footprint_name)
|
return (library_path, footprint_name)
|
||||||
|
|
||||||
logger.warning(f"Footprint not found in any library: {footprint_name}")
|
logger.warning(f"Footprint not found in any library: {footprint_name}")
|
||||||
@@ -354,18 +368,22 @@ class LibraryManager:
|
|||||||
|
|
||||||
for footprint in footprints:
|
for footprint in footprints:
|
||||||
if regex.search(footprint.lower()):
|
if regex.search(footprint.lower()):
|
||||||
results.append({
|
results.append(
|
||||||
'library': library_nickname,
|
{
|
||||||
'footprint': footprint,
|
"library": library_nickname,
|
||||||
'full_name': f"{library_nickname}:{footprint}"
|
"footprint": footprint,
|
||||||
})
|
"full_name": f"{library_nickname}:{footprint}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if len(results) >= limit:
|
if len(results) >= limit:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_footprint_info(self, library_nickname: str, footprint_name: str) -> Optional[Dict[str, str]]:
|
def get_footprint_info(
|
||||||
|
self, library_nickname: str, footprint_name: str
|
||||||
|
) -> Optional[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
Get information about a specific footprint
|
Get information about a specific footprint
|
||||||
|
|
||||||
@@ -385,11 +403,11 @@ class LibraryManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'library': library_nickname,
|
"library": library_nickname,
|
||||||
'footprint': footprint_name,
|
"footprint": footprint_name,
|
||||||
'full_name': f"{library_nickname}:{footprint_name}",
|
"full_name": f"{library_nickname}:{footprint_name}",
|
||||||
'path': str(fp_file),
|
"path": str(fp_file),
|
||||||
'library_path': library_path
|
"library_path": library_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -404,39 +422,48 @@ class LibraryCommands:
|
|||||||
"""List all available footprint libraries"""
|
"""List all available footprint libraries"""
|
||||||
try:
|
try:
|
||||||
libraries = self.library_manager.list_libraries()
|
libraries = self.library_manager.list_libraries()
|
||||||
return {
|
return {"success": True, "libraries": libraries, "count": len(libraries)}
|
||||||
"success": True,
|
|
||||||
"libraries": libraries,
|
|
||||||
"count": len(libraries)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing libraries: {e}")
|
logger.error(f"Error listing libraries: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to list libraries",
|
"message": "Failed to list libraries",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def search_footprints(self, params: Dict) -> Dict:
|
def search_footprints(self, params: Dict) -> Dict:
|
||||||
"""Search for footprints by pattern"""
|
"""Search for footprints by pattern"""
|
||||||
try:
|
try:
|
||||||
pattern = params.get("pattern", "*")
|
# Support both 'pattern' and 'search_term' parameter names
|
||||||
|
pattern = params.get("pattern") or params.get("search_term", "*")
|
||||||
limit = params.get("limit", 20)
|
limit = params.get("limit", 20)
|
||||||
|
library_filter = params.get("library")
|
||||||
|
|
||||||
results = self.library_manager.search_footprints(pattern, limit)
|
results = self.library_manager.search_footprints(
|
||||||
|
pattern, limit * 10 if library_filter else limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter by library if specified
|
||||||
|
if library_filter:
|
||||||
|
results = [
|
||||||
|
r
|
||||||
|
for r in results
|
||||||
|
if r.get("library", "").lower() == library_filter.lower()
|
||||||
|
]
|
||||||
|
results = results[:limit]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"footprints": results,
|
"footprints": results,
|
||||||
"count": len(results),
|
"count": len(results),
|
||||||
"pattern": pattern
|
"pattern": pattern,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error searching footprints: {e}")
|
logger.error(f"Error searching footprints: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to search footprints",
|
"message": "Failed to search footprints",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def list_library_footprints(self, params: Dict) -> Dict:
|
def list_library_footprints(self, params: Dict) -> Dict:
|
||||||
@@ -444,10 +471,7 @@ class LibraryCommands:
|
|||||||
try:
|
try:
|
||||||
library = params.get("library")
|
library = params.get("library")
|
||||||
if not library:
|
if not library:
|
||||||
return {
|
return {"success": False, "message": "Missing library parameter"}
|
||||||
"success": False,
|
|
||||||
"message": "Missing library parameter"
|
|
||||||
}
|
|
||||||
|
|
||||||
footprints = self.library_manager.list_footprints(library)
|
footprints = self.library_manager.list_footprints(library)
|
||||||
|
|
||||||
@@ -455,14 +479,14 @@ class LibraryCommands:
|
|||||||
"success": True,
|
"success": True,
|
||||||
"library": library,
|
"library": library,
|
||||||
"footprints": footprints,
|
"footprints": footprints,
|
||||||
"count": len(footprints)
|
"count": len(footprints),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing library footprints: {e}")
|
logger.error(f"Error listing library footprints: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to list library footprints",
|
"message": "Failed to list library footprints",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_footprint_info(self, params: Dict) -> Dict:
|
def get_footprint_info(self, params: Dict) -> Dict:
|
||||||
@@ -470,10 +494,7 @@ class LibraryCommands:
|
|||||||
try:
|
try:
|
||||||
footprint_spec = params.get("footprint")
|
footprint_spec = params.get("footprint")
|
||||||
if not footprint_spec:
|
if not footprint_spec:
|
||||||
return {
|
return {"success": False, "message": "Missing footprint parameter"}
|
||||||
"success": False,
|
|
||||||
"message": "Missing footprint parameter"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try to find the footprint
|
# Try to find the footprint
|
||||||
result = self.library_manager.find_footprint(footprint_spec)
|
result = self.library_manager.find_footprint(footprint_spec)
|
||||||
@@ -491,17 +512,14 @@ class LibraryCommands:
|
|||||||
"library": library_nickname,
|
"library": library_nickname,
|
||||||
"footprint": footprint_name,
|
"footprint": footprint_name,
|
||||||
"full_name": f"{library_nickname}:{footprint_name}",
|
"full_name": f"{library_nickname}:{footprint_name}",
|
||||||
"library_path": library_path
|
"library_path": library_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {"success": True, "footprint_info": info}
|
||||||
"success": True,
|
|
||||||
"footprint_info": info
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Footprint not found: {footprint_spec}"
|
"message": f"Footprint not found: {footprint_spec}",
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -509,5 +527,5 @@ class LibraryCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get footprint info",
|
"message": "Failed to get footprint info",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import sexpdata
|
|||||||
from sexpdata import Symbol
|
from sexpdata import Symbol
|
||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class PinLocator:
|
class PinLocator:
|
||||||
@@ -23,6 +23,7 @@ class PinLocator:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize pin locator with empty cache"""
|
"""Initialize pin locator with empty cache"""
|
||||||
self.pin_definition_cache = {} # Cache: "lib_id:symbol_name" -> pin_data
|
self.pin_definition_cache = {} # Cache: "lib_id:symbol_name" -> pin_data
|
||||||
|
self._schematic_cache: Dict[str, object] = {} # Cache: path -> loaded Schematic
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_symbol_definition(symbol_def: list) -> Dict[str, Dict]:
|
def parse_symbol_definition(symbol_def: list) -> Dict[str, Dict]:
|
||||||
@@ -47,39 +48,39 @@ class PinLocator:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Check if this is a pin definition
|
# Check if this is a pin definition
|
||||||
if len(sexp) > 0 and sexp[0] == Symbol('pin'):
|
if len(sexp) > 0 and sexp[0] == Symbol("pin"):
|
||||||
# Pin format: (pin type shape (at x y angle) (length len) (name "name") (number "num"))
|
# Pin format: (pin type shape (at x y angle) (length len) (name "name") (number "num"))
|
||||||
pin_data = {
|
pin_data = {
|
||||||
'x': 0,
|
"x": 0,
|
||||||
'y': 0,
|
"y": 0,
|
||||||
'angle': 0,
|
"angle": 0,
|
||||||
'length': 0,
|
"length": 0,
|
||||||
'name': '',
|
"name": "",
|
||||||
'number': '',
|
"number": "",
|
||||||
'type': str(sexp[1]) if len(sexp) > 1 else 'passive'
|
"type": str(sexp[1]) if len(sexp) > 1 else "passive",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract pin attributes
|
# Extract pin attributes
|
||||||
for item in sexp:
|
for item in sexp:
|
||||||
if isinstance(item, list) and len(item) > 0:
|
if isinstance(item, list) and len(item) > 0:
|
||||||
if item[0] == Symbol('at') and len(item) >= 3:
|
if item[0] == Symbol("at") and len(item) >= 3:
|
||||||
pin_data['x'] = float(item[1])
|
pin_data["x"] = float(item[1])
|
||||||
pin_data['y'] = float(item[2])
|
pin_data["y"] = float(item[2])
|
||||||
if len(item) >= 4:
|
if len(item) >= 4:
|
||||||
pin_data['angle'] = float(item[3])
|
pin_data["angle"] = float(item[3])
|
||||||
|
|
||||||
elif item[0] == Symbol('length') and len(item) >= 2:
|
elif item[0] == Symbol("length") and len(item) >= 2:
|
||||||
pin_data['length'] = float(item[1])
|
pin_data["length"] = float(item[1])
|
||||||
|
|
||||||
elif item[0] == Symbol('name') and len(item) >= 2:
|
elif item[0] == Symbol("name") and len(item) >= 2:
|
||||||
pin_data['name'] = str(item[1]).strip('"')
|
pin_data["name"] = str(item[1]).strip('"')
|
||||||
|
|
||||||
elif item[0] == Symbol('number') and len(item) >= 2:
|
elif item[0] == Symbol("number") and len(item) >= 2:
|
||||||
pin_data['number'] = str(item[1]).strip('"')
|
pin_data["number"] = str(item[1]).strip('"')
|
||||||
|
|
||||||
# Store by pin number
|
# Store by pin number
|
||||||
if pin_data['number']:
|
if pin_data["number"]:
|
||||||
pins[pin_data['number']] = pin_data
|
pins[pin_data["number"]] = pin_data
|
||||||
|
|
||||||
# Recurse into sublists
|
# Recurse into sublists
|
||||||
for item in sexp:
|
for item in sexp:
|
||||||
@@ -108,7 +109,7 @@ class PinLocator:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Read schematic
|
# 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_content = f.read()
|
||||||
|
|
||||||
sch_data = sexpdata.loads(sch_content)
|
sch_data = sexpdata.loads(sch_content)
|
||||||
@@ -116,7 +117,11 @@ class PinLocator:
|
|||||||
# Find lib_symbols section
|
# Find lib_symbols section
|
||||||
lib_symbols = None
|
lib_symbols = None
|
||||||
for item in sch_data:
|
for item in sch_data:
|
||||||
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('lib_symbols'):
|
if (
|
||||||
|
isinstance(item, list)
|
||||||
|
and len(item) > 0
|
||||||
|
and item[0] == Symbol("lib_symbols")
|
||||||
|
):
|
||||||
lib_symbols = item
|
lib_symbols = item
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -126,7 +131,11 @@ class PinLocator:
|
|||||||
|
|
||||||
# Find the specific symbol definition
|
# Find the specific symbol definition
|
||||||
for item in lib_symbols[1:]: # Skip 'lib_symbols' itself
|
for item in lib_symbols[1:]: # Skip 'lib_symbols' itself
|
||||||
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol('symbol'):
|
if (
|
||||||
|
isinstance(item, list)
|
||||||
|
and len(item) > 1
|
||||||
|
and item[0] == Symbol("symbol")
|
||||||
|
):
|
||||||
symbol_name = str(item[1]).strip('"')
|
symbol_name = str(item[1]).strip('"')
|
||||||
if symbol_name == lib_id:
|
if symbol_name == lib_id:
|
||||||
# Found the symbol, parse pins
|
# Found the symbol, parse pins
|
||||||
@@ -141,6 +150,7 @@ class PinLocator:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting symbol pins: {e}")
|
logger.error(f"Error getting symbol pins: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -169,8 +179,9 @@ class PinLocator:
|
|||||||
|
|
||||||
return (rotated_x, rotated_y)
|
return (rotated_x, rotated_y)
|
||||||
|
|
||||||
def get_pin_location(self, schematic_path: Path, symbol_reference: str,
|
def get_pin_location(
|
||||||
pin_number: str) -> Optional[List[float]]:
|
self, schematic_path: Path, symbol_reference: str, pin_number: str
|
||||||
|
) -> Optional[List[float]]:
|
||||||
"""
|
"""
|
||||||
Get the absolute location of a pin on a symbol instance
|
Get the absolute location of a pin on a symbol instance
|
||||||
|
|
||||||
@@ -184,7 +195,11 @@ class PinLocator:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load schematic with kicad-skip to get symbol instance
|
# Load schematic with kicad-skip to get symbol instance
|
||||||
sch = Schematic(str(schematic_path))
|
# Use cache to avoid reloading the file for every pin lookup
|
||||||
|
sch_key = str(schematic_path)
|
||||||
|
if sch_key not in self._schematic_cache:
|
||||||
|
self._schematic_cache[sch_key] = Schematic(sch_key)
|
||||||
|
sch = self._schematic_cache[sch_key]
|
||||||
|
|
||||||
# Find the symbol instance
|
# Find the symbol instance
|
||||||
target_symbol = None
|
target_symbol = None
|
||||||
@@ -205,12 +220,16 @@ class PinLocator:
|
|||||||
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
|
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
|
||||||
|
|
||||||
# Get symbol lib_id
|
# Get symbol lib_id
|
||||||
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, 'lib_id') else None
|
lib_id = (
|
||||||
|
target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
|
||||||
|
)
|
||||||
if not lib_id:
|
if not lib_id:
|
||||||
logger.error(f"Symbol {symbol_reference} has no lib_id")
|
logger.error(f"Symbol {symbol_reference} has no lib_id")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.debug(f"Symbol {symbol_reference}: pos=({symbol_x}, {symbol_y}), rot={symbol_rotation}, lib_id={lib_id}")
|
logger.debug(
|
||||||
|
f"Symbol {symbol_reference}: pos=({symbol_x}, {symbol_y}), rot={symbol_rotation}, lib_id={lib_id}"
|
||||||
|
)
|
||||||
|
|
||||||
# Get pin definitions for this symbol
|
# Get pin definitions for this symbol
|
||||||
pins = self.get_symbol_pins(schematic_path, lib_id)
|
pins = self.get_symbol_pins(schematic_path, lib_id)
|
||||||
@@ -220,36 +239,49 @@ class PinLocator:
|
|||||||
|
|
||||||
# Find the requested pin
|
# Find the requested pin
|
||||||
if pin_number not in pins:
|
if pin_number not in pins:
|
||||||
logger.error(f"Pin {pin_number} not found on {symbol_reference}. Available pins: {list(pins.keys())}")
|
logger.error(
|
||||||
|
f"Pin {pin_number} not found on {symbol_reference}. Available pins: {list(pins.keys())}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
pin_data = pins[pin_number]
|
pin_data = pins[pin_number]
|
||||||
|
|
||||||
# Get pin position relative to symbol origin
|
# Get pin position relative to symbol origin
|
||||||
pin_rel_x = pin_data['x']
|
pin_rel_x = pin_data["x"]
|
||||||
pin_rel_y = pin_data['y']
|
pin_rel_y = pin_data["y"]
|
||||||
|
|
||||||
logger.debug(f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})")
|
logger.debug(
|
||||||
|
f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})"
|
||||||
|
)
|
||||||
|
|
||||||
# Apply symbol rotation to pin position
|
# Apply symbol rotation to pin position
|
||||||
if symbol_rotation != 0:
|
if symbol_rotation != 0:
|
||||||
pin_rel_x, pin_rel_y = self.rotate_point(pin_rel_x, pin_rel_y, symbol_rotation)
|
pin_rel_x, pin_rel_y = self.rotate_point(
|
||||||
logger.debug(f"After rotation {symbol_rotation}°: ({pin_rel_x}, {pin_rel_y})")
|
pin_rel_x, pin_rel_y, symbol_rotation
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"After rotation {symbol_rotation}°: ({pin_rel_x}, {pin_rel_y})"
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate absolute position
|
# Calculate absolute position
|
||||||
abs_x = symbol_x + pin_rel_x
|
abs_x = symbol_x + pin_rel_x
|
||||||
abs_y = symbol_y + pin_rel_y
|
abs_y = symbol_y + pin_rel_y
|
||||||
|
|
||||||
logger.info(f"Pin {symbol_reference}/{pin_number} located at ({abs_x}, {abs_y})")
|
logger.info(
|
||||||
|
f"Pin {symbol_reference}/{pin_number} located at ({abs_x}, {abs_y})"
|
||||||
|
)
|
||||||
return [abs_x, abs_y]
|
return [abs_x, abs_y]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting pin location: {e}")
|
logger.error(f"Error getting pin location: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_all_symbol_pins(self, schematic_path: Path, symbol_reference: str) -> Dict[str, List[float]]:
|
def get_all_symbol_pins(
|
||||||
|
self, schematic_path: Path, symbol_reference: str
|
||||||
|
) -> Dict[str, List[float]]:
|
||||||
"""
|
"""
|
||||||
Get locations of all pins on a symbol instance
|
Get locations of all pins on a symbol instance
|
||||||
|
|
||||||
@@ -261,8 +293,11 @@ class PinLocator:
|
|||||||
Dictionary mapping pin number -> [x, y] coordinates
|
Dictionary mapping pin number -> [x, y] coordinates
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load schematic
|
# Load schematic (use cache)
|
||||||
sch = Schematic(str(schematic_path))
|
sch_key = str(schematic_path)
|
||||||
|
if sch_key not in self._schematic_cache:
|
||||||
|
self._schematic_cache[sch_key] = Schematic(sch_key)
|
||||||
|
sch = self._schematic_cache[sch_key]
|
||||||
|
|
||||||
# Find symbol
|
# Find symbol
|
||||||
target_symbol = None
|
target_symbol = None
|
||||||
@@ -276,7 +311,9 @@ class PinLocator:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Get lib_id
|
# Get lib_id
|
||||||
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, 'lib_id') else None
|
lib_id = (
|
||||||
|
target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
|
||||||
|
)
|
||||||
if not lib_id:
|
if not lib_id:
|
||||||
logger.error(f"Symbol {symbol_reference} has no lib_id")
|
logger.error(f"Symbol {symbol_reference} has no lib_id")
|
||||||
return {}
|
return {}
|
||||||
@@ -289,7 +326,9 @@ class PinLocator:
|
|||||||
# Calculate location for each pin
|
# Calculate location for each pin
|
||||||
result = {}
|
result = {}
|
||||||
for pin_num in pins.keys():
|
for pin_num in pins.keys():
|
||||||
location = self.get_pin_location(schematic_path, symbol_reference, pin_num)
|
location = self.get_pin_location(
|
||||||
|
schematic_path, symbol_reference, pin_num
|
||||||
|
)
|
||||||
if location:
|
if location:
|
||||||
result[pin_num] = location
|
result[pin_num] = location
|
||||||
|
|
||||||
@@ -301,10 +340,11 @@ class PinLocator:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
# Test pin location discovery
|
# Test pin location discovery
|
||||||
import sys
|
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
|
from pathlib import Path
|
||||||
from commands.component_schematic import ComponentManager
|
from commands.component_schematic import ComponentManager
|
||||||
@@ -316,8 +356,10 @@ if __name__ == '__main__':
|
|||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
# Create test schematic with components (cross-platform temp directory)
|
# Create test schematic with components (cross-platform temp directory)
|
||||||
test_path = Path(tempfile.gettempdir()) / 'test_pin_locator.kicad_sch'
|
test_path = Path(tempfile.gettempdir()) / "test_pin_locator.kicad_sch"
|
||||||
template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/template_with_symbols_expanded.kicad_sch')
|
template_path = Path(
|
||||||
|
"/home/chris/MCP/KiCAD-MCP-Server/python/templates/template_with_symbols_expanded.kicad_sch"
|
||||||
|
)
|
||||||
|
|
||||||
shutil.copy(template_path, test_path)
|
shutil.copy(template_path, test_path)
|
||||||
print(f"\n✓ Created test schematic: {test_path}")
|
print(f"\n✓ Created test schematic: {test_path}")
|
||||||
@@ -327,11 +369,25 @@ if __name__ == '__main__':
|
|||||||
sch = SchematicManager.load_schematic(str(test_path))
|
sch = SchematicManager.load_schematic(str(test_path))
|
||||||
|
|
||||||
# Add resistor at (100, 100), rotation 0
|
# Add resistor at (100, 100), rotation 0
|
||||||
r1_def = {'type': 'R', 'reference': 'R1', 'value': '10k', 'x': 100, 'y': 100, 'rotation': 0}
|
r1_def = {
|
||||||
|
"type": "R",
|
||||||
|
"reference": "R1",
|
||||||
|
"value": "10k",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"rotation": 0,
|
||||||
|
}
|
||||||
ComponentManager.add_component(sch, r1_def, test_path)
|
ComponentManager.add_component(sch, r1_def, test_path)
|
||||||
|
|
||||||
# Add capacitor at (150, 100), rotation 90
|
# Add capacitor at (150, 100), rotation 90
|
||||||
c1_def = {'type': 'C', 'reference': 'C1', 'value': '100nF', 'x': 150, 'y': 100, 'rotation': 90}
|
c1_def = {
|
||||||
|
"type": "C",
|
||||||
|
"reference": "C1",
|
||||||
|
"value": "100nF",
|
||||||
|
"x": 150,
|
||||||
|
"y": 100,
|
||||||
|
"rotation": 90,
|
||||||
|
}
|
||||||
ComponentManager.add_component(sch, c1_def, test_path)
|
ComponentManager.add_component(sch, c1_def, test_path)
|
||||||
|
|
||||||
SchematicManager.save_schematic(sch, str(test_path))
|
SchematicManager.save_schematic(sch, str(test_path))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
444
src/server.ts
444
src/server.ts
@@ -2,46 +2,47 @@
|
|||||||
* KiCAD MCP Server implementation
|
* KiCAD MCP Server implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import express from 'express';
|
import express from "express";
|
||||||
import { spawn, exec, execSync, ChildProcess } from 'child_process';
|
import { spawn, exec, execSync, ChildProcess } from "child_process";
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from "fs";
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from "path";
|
||||||
import { logger } from './logger.js';
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
// Import tool registration functions
|
// Import tool registration functions
|
||||||
import { registerProjectTools } from './tools/project.js';
|
import { registerProjectTools } from "./tools/project.js";
|
||||||
import { registerBoardTools } from './tools/board.js';
|
import { registerBoardTools } from "./tools/board.js";
|
||||||
import { registerComponentTools } from './tools/component.js';
|
import { registerComponentTools } from "./tools/component.js";
|
||||||
import { registerRoutingTools } from './tools/routing.js';
|
import { registerRoutingTools } from "./tools/routing.js";
|
||||||
import { registerDesignRuleTools } from './tools/design-rules.js';
|
import { registerDesignRuleTools } from "./tools/design-rules.js";
|
||||||
import { registerExportTools } from './tools/export.js';
|
import { registerExportTools } from "./tools/export.js";
|
||||||
import { registerSchematicTools } from './tools/schematic.js';
|
import { registerSchematicTools } from "./tools/schematic.js";
|
||||||
import { registerLibraryTools } from './tools/library.js';
|
import { registerLibraryTools } from "./tools/library.js";
|
||||||
import { registerSymbolLibraryTools } from './tools/library-symbol.js';
|
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
||||||
import { registerJLCPCBApiTools } from './tools/jlcpcb-api.js';
|
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
||||||
import { registerUITools } from './tools/ui.js';
|
import { registerDatasheetTools } from "./tools/datasheet.js";
|
||||||
import { registerRouterTools } from './tools/router.js';
|
import { registerUITools } from "./tools/ui.js";
|
||||||
|
import { registerRouterTools } from "./tools/router.js";
|
||||||
|
|
||||||
// Import resource registration functions
|
// Import resource registration functions
|
||||||
import { registerProjectResources } from './resources/project.js';
|
import { registerProjectResources } from "./resources/project.js";
|
||||||
import { registerBoardResources } from './resources/board.js';
|
import { registerBoardResources } from "./resources/board.js";
|
||||||
import { registerComponentResources } from './resources/component.js';
|
import { registerComponentResources } from "./resources/component.js";
|
||||||
import { registerLibraryResources } from './resources/library.js';
|
import { registerLibraryResources } from "./resources/library.js";
|
||||||
|
|
||||||
// Import prompt registration functions
|
// Import prompt registration functions
|
||||||
import { registerComponentPrompts } from './prompts/component.js';
|
import { registerComponentPrompts } from "./prompts/component.js";
|
||||||
import { registerRoutingPrompts } from './prompts/routing.js';
|
import { registerRoutingPrompts } from "./prompts/routing.js";
|
||||||
import { registerDesignPrompts } from './prompts/design.js';
|
import { registerDesignPrompts } from "./prompts/design.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the Python executable to use
|
* Find the Python executable to use
|
||||||
* Prioritizes virtual environment if available, falls back to system Python
|
* Prioritizes virtual environment if available, falls back to system Python
|
||||||
*/
|
*/
|
||||||
function findPythonExecutable(scriptPath: string): string {
|
function findPythonExecutable(scriptPath: string): string {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === "win32";
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === "darwin";
|
||||||
const isLinux = !isWindows && !isMac;
|
const isLinux = !isWindows && !isMac;
|
||||||
|
|
||||||
// Get the project root (parent of the python/ directory)
|
// Get the project root (parent of the python/ directory)
|
||||||
@@ -49,8 +50,18 @@ function findPythonExecutable(scriptPath: string): string {
|
|||||||
|
|
||||||
// Check for virtual environment
|
// Check for virtual environment
|
||||||
const venvPaths = [
|
const venvPaths = [
|
||||||
join(projectRoot, 'venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'),
|
join(
|
||||||
join(projectRoot, '.venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'),
|
projectRoot,
|
||||||
|
"venv",
|
||||||
|
isWindows ? "Scripts" : "bin",
|
||||||
|
isWindows ? "python.exe" : "python",
|
||||||
|
),
|
||||||
|
join(
|
||||||
|
projectRoot,
|
||||||
|
".venv",
|
||||||
|
isWindows ? "Scripts" : "bin",
|
||||||
|
isWindows ? "python.exe" : "python",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const venvPath of venvPaths) {
|
for (const venvPath of venvPaths) {
|
||||||
@@ -62,27 +73,29 @@ function findPythonExecutable(scriptPath: string): string {
|
|||||||
|
|
||||||
// Allow override via KICAD_PYTHON environment variable (any platform)
|
// Allow override via KICAD_PYTHON environment variable (any platform)
|
||||||
if (process.env.KICAD_PYTHON) {
|
if (process.env.KICAD_PYTHON) {
|
||||||
logger.info(`Using KICAD_PYTHON environment variable: ${process.env.KICAD_PYTHON}`);
|
logger.info(
|
||||||
|
`Using KICAD_PYTHON environment variable: ${process.env.KICAD_PYTHON}`,
|
||||||
|
);
|
||||||
return process.env.KICAD_PYTHON;
|
return process.env.KICAD_PYTHON;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platform-specific KiCAD bundled Python detection
|
// Platform-specific KiCAD bundled Python detection
|
||||||
if (isWindows && process.env.PYTHONPATH?.includes('KiCad')) {
|
if (isWindows) {
|
||||||
// Windows: Try KiCAD's bundled Python
|
// Windows: Always prefer KiCAD's bundled Python (pcbnew.pyd is compiled for it)
|
||||||
const kicadPython = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
|
const kicadPython = "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe";
|
||||||
if (existsSync(kicadPython)) {
|
if (existsSync(kicadPython)) {
|
||||||
logger.info(`Found KiCAD bundled Python at: ${kicadPython}`);
|
logger.info(`Found KiCAD bundled Python at: ${kicadPython}`);
|
||||||
return kicadPython;
|
return kicadPython;
|
||||||
}
|
}
|
||||||
} else if (isMac) {
|
} else if (isMac) {
|
||||||
// macOS: Try KiCAD's bundled Python (check multiple versions and locations)
|
// macOS: Try KiCAD's bundled Python (check multiple versions and locations)
|
||||||
const kicadPythonVersions = ['3.9', '3.10', '3.11', '3.12', '3.13'];
|
const kicadPythonVersions = ["3.9", "3.10", "3.11", "3.12", "3.13"];
|
||||||
|
|
||||||
// Standard KiCAD installation paths
|
// Standard KiCAD installation paths
|
||||||
const kicadAppPaths = [
|
const kicadAppPaths = [
|
||||||
'/Applications/KiCad/KiCad.app',
|
"/Applications/KiCad/KiCad.app",
|
||||||
'/Applications/KiCAD/KiCad.app', // Alternative capitalization
|
"/Applications/KiCAD/KiCad.app", // Alternative capitalization
|
||||||
`${process.env.HOME}/Applications/KiCad/KiCad.app`, // User Applications folder
|
`${process.env.HOME}/Applications/KiCad/KiCad.app`, // User Applications folder
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check all KiCAD app locations with all Python versions
|
// Check all KiCAD app locations with all Python versions
|
||||||
@@ -98,24 +111,26 @@ function findPythonExecutable(scriptPath: string): string {
|
|||||||
|
|
||||||
// Fallback to Homebrew Python (if pcbnew is installed via pip)
|
// Fallback to Homebrew Python (if pcbnew is installed via pip)
|
||||||
const homebrewPaths = [
|
const homebrewPaths = [
|
||||||
'/opt/homebrew/bin/python3', // Apple Silicon
|
"/opt/homebrew/bin/python3", // Apple Silicon
|
||||||
'/usr/local/bin/python3', // Intel Mac
|
"/usr/local/bin/python3", // Intel Mac
|
||||||
'/opt/homebrew/bin/python3.12',
|
"/opt/homebrew/bin/python3.12",
|
||||||
'/opt/homebrew/bin/python3.11',
|
"/opt/homebrew/bin/python3.11",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const path of homebrewPaths) {
|
for (const path of homebrewPaths) {
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
logger.info(`Found Homebrew Python at: ${path} (ensure pcbnew is importable)`);
|
logger.info(
|
||||||
|
`Found Homebrew Python at: ${path} (ensure pcbnew is importable)`,
|
||||||
|
);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isLinux) {
|
} else if (isLinux) {
|
||||||
// Linux: Try KiCAD bundled Python locations first
|
// Linux: Try KiCAD bundled Python locations first
|
||||||
const linuxKicadPaths = [
|
const linuxKicadPaths = [
|
||||||
'/usr/lib/kicad/bin/python3',
|
"/usr/lib/kicad/bin/python3",
|
||||||
'/usr/local/lib/kicad/bin/python3',
|
"/usr/local/lib/kicad/bin/python3",
|
||||||
'/opt/kicad/bin/python3',
|
"/opt/kicad/bin/python3",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const path of linuxKicadPaths) {
|
for (const path of linuxKicadPaths) {
|
||||||
@@ -127,17 +142,17 @@ function findPythonExecutable(scriptPath: string): string {
|
|||||||
|
|
||||||
// Resolve system python3 to full path using 'which'
|
// Resolve system python3 to full path using 'which'
|
||||||
try {
|
try {
|
||||||
const result = execSync('which python3', { encoding: 'utf-8' }).trim();
|
const result = execSync("which python3", { encoding: "utf-8" }).trim();
|
||||||
if (result && existsSync(result)) {
|
if (result && existsSync(result)) {
|
||||||
logger.info(`Resolved system Python via which: ${result}`);
|
logger.info(`Resolved system Python via which: ${result}`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to resolve python3 via which command');
|
logger.warn("Failed to resolve python3 via which command");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to common system paths
|
// Fallback to common system paths
|
||||||
const systemPaths = ['/usr/bin/python3', '/bin/python3'];
|
const systemPaths = ["/usr/bin/python3", "/bin/python3"];
|
||||||
for (const path of systemPaths) {
|
for (const path of systemPaths) {
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
logger.info(`Found system Python at: ${path}`);
|
logger.info(`Found system Python at: ${path}`);
|
||||||
@@ -147,8 +162,8 @@ function findPythonExecutable(scriptPath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default to system Python (last resort)
|
// Default to system Python (last resort)
|
||||||
logger.info('Using system Python (no venv found)');
|
logger.info("Using system Python (no venv found)");
|
||||||
return isWindows ? 'python.exe' : 'python3';
|
return isWindows ? "python.exe" : "python3";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,11 +174,19 @@ export class KiCADMcpServer {
|
|||||||
private pythonProcess: ChildProcess | null = null;
|
private pythonProcess: ChildProcess | null = null;
|
||||||
private kicadScriptPath: string;
|
private kicadScriptPath: string;
|
||||||
private stdioTransport!: StdioServerTransport;
|
private stdioTransport!: StdioServerTransport;
|
||||||
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
|
private requestQueue: Array<{
|
||||||
|
request: any;
|
||||||
|
resolve: Function;
|
||||||
|
reject: Function;
|
||||||
|
}> = [];
|
||||||
private processingRequest = false;
|
private processingRequest = false;
|
||||||
private responseBuffer: string = '';
|
private responseBuffer: string = "";
|
||||||
private currentRequestHandler: { resolve: Function, reject: Function, timeoutHandle: NodeJS.Timeout } | null = null;
|
private currentRequestHandler: {
|
||||||
|
resolve: Function;
|
||||||
|
reject: Function;
|
||||||
|
timeoutHandle: NodeJS.Timeout;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the KiCAD MCP Server
|
* Constructor for the KiCAD MCP Server
|
||||||
* @param kicadScriptPath Path to the Python KiCAD interface script
|
* @param kicadScriptPath Path to the Python KiCAD interface script
|
||||||
@@ -171,37 +194,39 @@ export class KiCADMcpServer {
|
|||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
kicadScriptPath: string,
|
kicadScriptPath: string,
|
||||||
logLevel: 'error' | 'warn' | 'info' | 'debug' = 'info'
|
logLevel: "error" | "warn" | "info" | "debug" = "info",
|
||||||
) {
|
) {
|
||||||
// Set up the logger
|
// Set up the logger
|
||||||
logger.setLogLevel(logLevel);
|
logger.setLogLevel(logLevel);
|
||||||
|
|
||||||
// Check if KiCAD script exists
|
// Check if KiCAD script exists
|
||||||
this.kicadScriptPath = kicadScriptPath;
|
this.kicadScriptPath = kicadScriptPath;
|
||||||
if (!existsSync(this.kicadScriptPath)) {
|
if (!existsSync(this.kicadScriptPath)) {
|
||||||
throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
|
throw new Error(
|
||||||
|
`KiCAD interface script not found: ${this.kicadScriptPath}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the MCP server
|
// Initialize the MCP server
|
||||||
this.server = new McpServer({
|
this.server = new McpServer({
|
||||||
name: 'kicad-mcp-server',
|
name: "kicad-mcp-server",
|
||||||
version: '1.0.0',
|
version: "1.0.0",
|
||||||
description: 'MCP server for KiCAD PCB design operations'
|
description: "MCP server for KiCAD PCB design operations",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize STDIO transport
|
// Initialize STDIO transport
|
||||||
this.stdioTransport = new StdioServerTransport();
|
this.stdioTransport = new StdioServerTransport();
|
||||||
logger.info('Using STDIO transport for local communication');
|
logger.info("Using STDIO transport for local communication");
|
||||||
|
|
||||||
// Register tools, resources, and prompts
|
// Register tools, resources, and prompts
|
||||||
this.registerAll();
|
this.registerAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all tools, resources, and prompts
|
* Register all tools, resources, and prompts
|
||||||
*/
|
*/
|
||||||
private registerAll(): void {
|
private registerAll(): void {
|
||||||
logger.info('Registering KiCAD tools, resources, and prompts...');
|
logger.info("Registering KiCAD tools, resources, and prompts...");
|
||||||
|
|
||||||
// Register router tools FIRST (for tool discovery and execution)
|
// Register router tools FIRST (for tool discovery and execution)
|
||||||
registerRouterTools(this.server, this.callKicadScript.bind(this));
|
registerRouterTools(this.server, this.callKicadScript.bind(this));
|
||||||
@@ -217,6 +242,7 @@ export class KiCADMcpServer {
|
|||||||
registerLibraryTools(this.server, this.callKicadScript.bind(this));
|
registerLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
||||||
|
registerDatasheetTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerUITools(this.server, this.callKicadScript.bind(this));
|
registerUITools(this.server, this.callKicadScript.bind(this));
|
||||||
|
|
||||||
// Register all resources
|
// Register all resources
|
||||||
@@ -230,20 +256,26 @@ export class KiCADMcpServer {
|
|||||||
registerRoutingPrompts(this.server);
|
registerRoutingPrompts(this.server);
|
||||||
registerDesignPrompts(this.server);
|
registerDesignPrompts(this.server);
|
||||||
|
|
||||||
logger.info('All KiCAD tools, resources, and prompts registered');
|
logger.info("All KiCAD tools, resources, and prompts registered");
|
||||||
logger.info('Router pattern enabled: 4 router tools + direct tools for discovery');
|
logger.info(
|
||||||
|
"Router pattern enabled: 4 router tools + direct tools for discovery",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate prerequisites before starting the server
|
* Validate prerequisites before starting the server
|
||||||
*/
|
*/
|
||||||
private async validatePrerequisites(pythonExe: string): Promise<boolean> {
|
private async validatePrerequisites(pythonExe: string): Promise<boolean> {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === "win32";
|
||||||
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
const isLinux =
|
||||||
|
process.platform !== "win32" && process.platform !== "darwin";
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Check if Python executable exists (for absolute paths) or is executable (for commands)
|
// Check if Python executable exists (for absolute paths) or is executable (for commands)
|
||||||
const isAbsolutePath = pythonExe.startsWith('/') || pythonExe.startsWith('C:') || pythonExe.startsWith('\\');
|
const isAbsolutePath =
|
||||||
|
pythonExe.startsWith("/") ||
|
||||||
|
pythonExe.startsWith("C:") ||
|
||||||
|
pythonExe.startsWith("\\");
|
||||||
|
|
||||||
if (isAbsolutePath) {
|
if (isAbsolutePath) {
|
||||||
// Absolute path: use existsSync
|
// Absolute path: use existsSync
|
||||||
@@ -251,42 +283,61 @@ export class KiCADMcpServer {
|
|||||||
errors.push(`Python executable not found: ${pythonExe}`);
|
errors.push(`Python executable not found: ${pythonExe}`);
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
errors.push('Windows: Install KiCAD 9.0+ from https://www.kicad.org/download/windows/');
|
errors.push(
|
||||||
errors.push('Or run: .\\setup-windows.ps1 for automatic configuration');
|
"Windows: Install KiCAD 9.0+ from https://www.kicad.org/download/windows/",
|
||||||
|
);
|
||||||
|
errors.push(
|
||||||
|
"Or run: .\\setup-windows.ps1 for automatic configuration",
|
||||||
|
);
|
||||||
} else if (isLinux) {
|
} else if (isLinux) {
|
||||||
errors.push('Linux: Install KiCAD 9.0+ or set KICAD_PYTHON environment variable');
|
errors.push(
|
||||||
errors.push('Set KICAD_PYTHON to specify a custom Python path');
|
"Linux: Install KiCAD 9.0+ or set KICAD_PYTHON environment variable",
|
||||||
|
);
|
||||||
|
errors.push("Set KICAD_PYTHON to specify a custom Python path");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Command name: verify it's executable via --version test
|
// Command name: verify it's executable via --version test
|
||||||
logger.info(`Validating command-based Python executable: ${pythonExe}`);
|
logger.info(`Validating command-based Python executable: ${pythonExe}`);
|
||||||
try {
|
try {
|
||||||
const { stdout } = await new Promise<{stdout: string, stderr: string}>((resolve, reject) => {
|
const { stdout } = await new Promise<{
|
||||||
exec(`"${pythonExe}" --version`, {
|
stdout: string;
|
||||||
timeout: 3000,
|
stderr: string;
|
||||||
env: { ...process.env }
|
}>((resolve, reject) => {
|
||||||
}, (error: any, stdout: string, stderr: string) => {
|
exec(
|
||||||
if (error) {
|
`"${pythonExe}" --version`,
|
||||||
reject(error);
|
{
|
||||||
} else {
|
timeout: 3000,
|
||||||
resolve({ stdout, stderr });
|
env: { ...process.env },
|
||||||
}
|
},
|
||||||
});
|
(error: any, stdout: string, stderr: string) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Python version check passed: ${stdout.trim()}`);
|
logger.info(`Python version check passed: ${stdout.trim()}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
errors.push(`Python executable not found in PATH: ${pythonExe}`);
|
errors.push(`Python executable not found in PATH: ${pythonExe}`);
|
||||||
errors.push(`Error: ${error.message}`);
|
errors.push(`Error: ${error.message}`);
|
||||||
errors.push('Set KICAD_PYTHON environment variable to specify full path');
|
errors.push(
|
||||||
|
"Set KICAD_PYTHON environment variable to specify full path",
|
||||||
|
);
|
||||||
|
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
errors.push('');
|
errors.push("");
|
||||||
errors.push('Linux troubleshooting:');
|
errors.push("Linux troubleshooting:");
|
||||||
errors.push('1. Check if python3 is installed: which python3');
|
errors.push("1. Check if python3 is installed: which python3");
|
||||||
errors.push('2. Install KiCAD: sudo apt install kicad (Ubuntu/Debian)');
|
errors.push(
|
||||||
errors.push('3. Set KICAD_PYTHON=/usr/bin/python3 in your MCP config');
|
"2. Install KiCAD: sudo apt install kicad (Ubuntu/Debian)",
|
||||||
|
);
|
||||||
|
errors.push(
|
||||||
|
"3. Set KICAD_PYTHON=/usr/bin/python3 in your MCP config",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,76 +348,91 @@ export class KiCADMcpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if dist/index.js exists (if running from compiled code)
|
// Check if dist/index.js exists (if running from compiled code)
|
||||||
const distPath = join(dirname(dirname(this.kicadScriptPath)), 'dist', 'index.js');
|
const distPath = join(
|
||||||
|
dirname(dirname(this.kicadScriptPath)),
|
||||||
|
"dist",
|
||||||
|
"index.js",
|
||||||
|
);
|
||||||
if (!existsSync(distPath)) {
|
if (!existsSync(distPath)) {
|
||||||
errors.push('Project not built. Run: npm run build');
|
errors.push("Project not built. Run: npm run build");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to test pcbnew import (quick validation)
|
// Try to test pcbnew import (quick validation)
|
||||||
if (existsSync(pythonExe) && existsSync(this.kicadScriptPath)) {
|
if (existsSync(pythonExe) && existsSync(this.kicadScriptPath)) {
|
||||||
logger.info('Validating pcbnew module access...');
|
logger.info("Validating pcbnew module access...");
|
||||||
|
|
||||||
const testCommand = `"${pythonExe}" -c "import pcbnew; print('OK')"`;
|
const testCommand = `"${pythonExe}" -c "import pcbnew; print('OK')"`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await new Promise<{stdout: string, stderr: string}>((resolve, reject) => {
|
const { stdout, stderr } = await new Promise<{
|
||||||
exec(testCommand, {
|
stdout: string;
|
||||||
timeout: 5000,
|
stderr: string;
|
||||||
env: { ...process.env }
|
}>((resolve, reject) => {
|
||||||
}, (error: any, stdout: string, stderr: string) => {
|
exec(
|
||||||
if (error) {
|
testCommand,
|
||||||
reject(error);
|
{
|
||||||
} else {
|
timeout: 5000,
|
||||||
resolve({ stdout, stderr });
|
env: { ...process.env },
|
||||||
}
|
},
|
||||||
});
|
(error: any, stdout: string, stderr: string) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!stdout.includes('OK')) {
|
if (!stdout.includes("OK")) {
|
||||||
errors.push('pcbnew module import test failed');
|
errors.push("pcbnew module import test failed");
|
||||||
errors.push(`Output: ${stdout}`);
|
errors.push(`Output: ${stdout}`);
|
||||||
errors.push(`Errors: ${stderr}`);
|
errors.push(`Errors: ${stderr}`);
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
errors.push('');
|
errors.push("");
|
||||||
errors.push('Windows troubleshooting:');
|
errors.push("Windows troubleshooting:");
|
||||||
errors.push('1. Set PYTHONPATH=C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages');
|
errors.push(
|
||||||
errors.push('2. Test: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"');
|
"1. Set PYTHONPATH=C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages",
|
||||||
errors.push('3. Run: .\\setup-windows.ps1 for automatic fix');
|
);
|
||||||
errors.push('4. See: docs/WINDOWS_TROUBLESHOOTING.md');
|
errors.push(
|
||||||
|
'2. Test: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"',
|
||||||
|
);
|
||||||
|
errors.push("3. Run: .\\setup-windows.ps1 for automatic fix");
|
||||||
|
errors.push("4. See: docs/WINDOWS_TROUBLESHOOTING.md");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info('✓ pcbnew module validated successfully');
|
logger.info("✓ pcbnew module validated successfully");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
errors.push(`pcbnew validation failed: ${error.message}`);
|
errors.push(`pcbnew validation failed: ${error.message}`);
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
errors.push('');
|
errors.push("");
|
||||||
errors.push('This usually means:');
|
errors.push("This usually means:");
|
||||||
errors.push('- KiCAD is not installed');
|
errors.push("- KiCAD is not installed");
|
||||||
errors.push('- PYTHONPATH is incorrect');
|
errors.push("- PYTHONPATH is incorrect");
|
||||||
errors.push('- Python cannot find pcbnew module');
|
errors.push("- Python cannot find pcbnew module");
|
||||||
errors.push('');
|
errors.push("");
|
||||||
errors.push('Quick fix: Run .\\setup-windows.ps1');
|
errors.push("Quick fix: Run .\\setup-windows.ps1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log all errors
|
// Log all errors
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
logger.error('='.repeat(70));
|
logger.error("=".repeat(70));
|
||||||
logger.error('STARTUP VALIDATION FAILED');
|
logger.error("STARTUP VALIDATION FAILED");
|
||||||
logger.error('='.repeat(70));
|
logger.error("=".repeat(70));
|
||||||
errors.forEach(err => logger.error(err));
|
errors.forEach((err) => logger.error(err));
|
||||||
logger.error('='.repeat(70));
|
logger.error("=".repeat(70));
|
||||||
|
|
||||||
// Also write to stderr for Claude Desktop to capture
|
// Also write to stderr for Claude Desktop to capture
|
||||||
process.stderr.write('\n' + '='.repeat(70) + '\n');
|
process.stderr.write("\n" + "=".repeat(70) + "\n");
|
||||||
process.stderr.write('KiCAD MCP Server - Startup Validation Failed\n');
|
process.stderr.write("KiCAD MCP Server - Startup Validation Failed\n");
|
||||||
process.stderr.write('='.repeat(70) + '\n');
|
process.stderr.write("=".repeat(70) + "\n");
|
||||||
errors.forEach(err => process.stderr.write(err + '\n'));
|
errors.forEach((err) => process.stderr.write(err + "\n"));
|
||||||
process.stderr.write('='.repeat(70) + '\n\n');
|
process.stderr.write("=".repeat(70) + "\n\n");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -379,10 +445,12 @@ export class KiCADMcpServer {
|
|||||||
*/
|
*/
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info('Starting KiCAD MCP server...');
|
logger.info("Starting KiCAD MCP server...");
|
||||||
|
|
||||||
// Start the Python process for KiCAD scripting
|
// Start the Python process for KiCAD scripting
|
||||||
logger.info(`Starting Python process with script: ${this.kicadScriptPath}`);
|
logger.info(
|
||||||
|
`Starting Python process with script: ${this.kicadScriptPath}`,
|
||||||
|
);
|
||||||
const pythonExe = findPythonExecutable(this.kicadScriptPath);
|
const pythonExe = findPythonExecutable(this.kicadScriptPath);
|
||||||
|
|
||||||
logger.info(`Using Python executable: ${pythonExe}`);
|
logger.info(`Using Python executable: ${pythonExe}`);
|
||||||
@@ -390,76 +458,82 @@ export class KiCADMcpServer {
|
|||||||
// Validate prerequisites
|
// Validate prerequisites
|
||||||
const isValid = await this.validatePrerequisites(pythonExe);
|
const isValid = await this.validatePrerequisites(pythonExe);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error('Prerequisites validation failed. See logs above for details.');
|
throw new Error(
|
||||||
|
"Prerequisites validation failed. See logs above for details.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
|
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
PYTHONPATH: process.env.PYTHONPATH || 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
|
PYTHONPATH:
|
||||||
}
|
process.env.PYTHONPATH ||
|
||||||
|
"C:/Program Files/KiCad/9.0/lib/python3/dist-packages",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for process exit
|
// Listen for process exit
|
||||||
this.pythonProcess.on('exit', (code, signal) => {
|
this.pythonProcess.on("exit", (code, signal) => {
|
||||||
logger.warn(`Python process exited with code ${code} and signal ${signal}`);
|
logger.warn(
|
||||||
|
`Python process exited with code ${code} and signal ${signal}`,
|
||||||
|
);
|
||||||
this.pythonProcess = null;
|
this.pythonProcess = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for process errors
|
// Listen for process errors
|
||||||
this.pythonProcess.on('error', (err) => {
|
this.pythonProcess.on("error", (err) => {
|
||||||
logger.error(`Python process error: ${err.message}`);
|
logger.error(`Python process error: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up error logging for stderr
|
// Set up error logging for stderr
|
||||||
if (this.pythonProcess.stderr) {
|
if (this.pythonProcess.stderr) {
|
||||||
this.pythonProcess.stderr.on('data', (data: Buffer) => {
|
this.pythonProcess.stderr.on("data", (data: Buffer) => {
|
||||||
logger.error(`Python stderr: ${data.toString()}`);
|
logger.error(`Python stderr: ${data.toString()}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up persistent stdout handler (instead of adding/removing per request)
|
// Set up persistent stdout handler (instead of adding/removing per request)
|
||||||
if (this.pythonProcess.stdout) {
|
if (this.pythonProcess.stdout) {
|
||||||
this.pythonProcess.stdout.on('data', (data: Buffer) => {
|
this.pythonProcess.stdout.on("data", (data: Buffer) => {
|
||||||
this.handlePythonResponse(data);
|
this.handlePythonResponse(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect server to STDIO transport
|
// Connect server to STDIO transport
|
||||||
logger.info('Connecting MCP server to STDIO transport...');
|
logger.info("Connecting MCP server to STDIO transport...");
|
||||||
try {
|
try {
|
||||||
await this.server.connect(this.stdioTransport);
|
await this.server.connect(this.stdioTransport);
|
||||||
logger.info('Successfully connected to STDIO transport');
|
logger.info("Successfully connected to STDIO transport");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to connect to STDIO transport: ${error}`);
|
logger.error(`Failed to connect to STDIO transport: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write a ready message to stderr (for debugging)
|
// Write a ready message to stderr (for debugging)
|
||||||
process.stderr.write('KiCAD MCP SERVER READY\n');
|
process.stderr.write("KiCAD MCP SERVER READY\n");
|
||||||
|
|
||||||
logger.info('KiCAD MCP server started and ready');
|
logger.info("KiCAD MCP server started and ready");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to start KiCAD MCP server: ${error}`);
|
logger.error(`Failed to start KiCAD MCP server: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the MCP server and clean up resources
|
* Stop the MCP server and clean up resources
|
||||||
*/
|
*/
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
logger.info('Stopping KiCAD MCP server...');
|
logger.info("Stopping KiCAD MCP server...");
|
||||||
|
|
||||||
// Kill the Python process if it's running
|
// Kill the Python process if it's running
|
||||||
if (this.pythonProcess) {
|
if (this.pythonProcess) {
|
||||||
this.pythonProcess.kill();
|
this.pythonProcess.kill();
|
||||||
this.pythonProcess = null;
|
this.pythonProcess = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('KiCAD MCP server stopped');
|
logger.info("KiCAD MCP server stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call the KiCAD scripting interface to execute commands
|
* Call the KiCAD scripting interface to execute commands
|
||||||
*
|
*
|
||||||
@@ -471,7 +545,7 @@ export class KiCADMcpServer {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Check if Python process is running
|
// Check if Python process is running
|
||||||
if (!this.pythonProcess) {
|
if (!this.pythonProcess) {
|
||||||
logger.error('Python process is not running');
|
logger.error("Python process is not running");
|
||||||
reject(new Error("Python process for KiCAD scripting is not running"));
|
reject(new Error("Python process for KiCAD scripting is not running"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -479,17 +553,24 @@ export class KiCADMcpServer {
|
|||||||
// Determine timeout based on command type
|
// Determine timeout based on command type
|
||||||
// DRC and export operations need longer timeouts for large boards
|
// DRC and export operations need longer timeouts for large boards
|
||||||
let commandTimeout = 30000; // Default 30 seconds
|
let commandTimeout = 30000; // Default 30 seconds
|
||||||
const longRunningCommands = ['run_drc', 'export_gerber', 'export_pdf', 'export_3d'];
|
const longRunningCommands = [
|
||||||
|
"run_drc",
|
||||||
|
"export_gerber",
|
||||||
|
"export_pdf",
|
||||||
|
"export_3d",
|
||||||
|
];
|
||||||
if (longRunningCommands.includes(command)) {
|
if (longRunningCommands.includes(command)) {
|
||||||
commandTimeout = 600000; // 10 minutes for long operations
|
commandTimeout = 600000; // 10 minutes for long operations
|
||||||
logger.info(`Using extended timeout (${commandTimeout/1000}s) for command: ${command}`);
|
logger.info(
|
||||||
|
`Using extended timeout (${commandTimeout / 1000}s) for command: ${command}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add request to queue with timeout info
|
// Add request to queue with timeout info
|
||||||
this.requestQueue.push({
|
this.requestQueue.push({
|
||||||
request: { command, params, timeout: commandTimeout },
|
request: { command, params, timeout: commandTimeout },
|
||||||
resolve,
|
resolve,
|
||||||
reject
|
reject,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process the queue if not already processing
|
// Process the queue if not already processing
|
||||||
@@ -498,7 +579,7 @@ export class KiCADMcpServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming data from Python process stdout
|
* Handle incoming data from Python process stdout
|
||||||
* This is a persistent handler that processes all responses
|
* This is a persistent handler that processes all responses
|
||||||
@@ -519,8 +600,10 @@ export class KiCADMcpServer {
|
|||||||
if (!this.currentRequestHandler) {
|
if (!this.currentRequestHandler) {
|
||||||
// No pending request, clear buffer if it has data (shouldn't happen)
|
// No pending request, clear buffer if it has data (shouldn't happen)
|
||||||
if (this.responseBuffer.trim()) {
|
if (this.responseBuffer.trim()) {
|
||||||
logger.warn(`Received data with no pending request: ${this.responseBuffer.substring(0, 100)}...`);
|
logger.warn(
|
||||||
this.responseBuffer = '';
|
`Received data with no pending request: ${this.responseBuffer.substring(0, 100)}...`,
|
||||||
|
);
|
||||||
|
this.responseBuffer = "";
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -530,7 +613,9 @@ export class KiCADMcpServer {
|
|||||||
const result = JSON.parse(this.responseBuffer);
|
const result = JSON.parse(this.responseBuffer);
|
||||||
|
|
||||||
// If we get here, we have a valid JSON response
|
// If we get here, we have a valid JSON response
|
||||||
logger.debug(`Completed KiCAD command with result: ${result.success ? 'success' : 'failure'}`);
|
logger.debug(
|
||||||
|
`Completed KiCAD command with result: ${result.success ? "success" : "failure"}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Clear the timeout since we got a response
|
// Clear the timeout since we got a response
|
||||||
if (this.currentRequestHandler.timeoutHandle) {
|
if (this.currentRequestHandler.timeoutHandle) {
|
||||||
@@ -541,7 +626,7 @@ export class KiCADMcpServer {
|
|||||||
const handler = this.currentRequestHandler;
|
const handler = this.currentRequestHandler;
|
||||||
|
|
||||||
// Clear state
|
// Clear state
|
||||||
this.responseBuffer = '';
|
this.responseBuffer = "";
|
||||||
this.currentRequestHandler = null;
|
this.currentRequestHandler = null;
|
||||||
this.processingRequest = false;
|
this.processingRequest = false;
|
||||||
|
|
||||||
@@ -550,7 +635,6 @@ export class KiCADMcpServer {
|
|||||||
|
|
||||||
// Process next request if any
|
// Process next request if any
|
||||||
setTimeout(() => this.processNextRequest(), 0);
|
setTimeout(() => this.processNextRequest(), 0);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not a complete JSON yet, keep collecting data
|
// Not a complete JSON yet, keep collecting data
|
||||||
// This is normal for large responses that come in chunks
|
// This is normal for large responses that come in chunks
|
||||||
@@ -579,21 +663,29 @@ export class KiCADMcpServer {
|
|||||||
const requestStr = JSON.stringify(request);
|
const requestStr = JSON.stringify(request);
|
||||||
|
|
||||||
// Clear response buffer for new request
|
// Clear response buffer for new request
|
||||||
this.responseBuffer = '';
|
this.responseBuffer = "";
|
||||||
|
|
||||||
// Set a timeout (use command-specific timeout or default)
|
// Set a timeout (use command-specific timeout or default)
|
||||||
const timeoutDuration = request.timeout || 30000;
|
const timeoutDuration = request.timeout || 30000;
|
||||||
const timeoutHandle = setTimeout(() => {
|
const timeoutHandle = setTimeout(() => {
|
||||||
logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`);
|
logger.error(
|
||||||
logger.error(`Buffer contents: ${this.responseBuffer.substring(0, 200)}...`);
|
`Command timeout after ${timeoutDuration / 1000}s: ${request.command}`,
|
||||||
|
);
|
||||||
|
logger.error(
|
||||||
|
`Buffer contents: ${this.responseBuffer.substring(0, 200)}...`,
|
||||||
|
);
|
||||||
|
|
||||||
// Clear state
|
// Clear state
|
||||||
this.responseBuffer = '';
|
this.responseBuffer = "";
|
||||||
this.currentRequestHandler = null;
|
this.currentRequestHandler = null;
|
||||||
this.processingRequest = false;
|
this.processingRequest = false;
|
||||||
|
|
||||||
// Reject the promise
|
// Reject the promise
|
||||||
reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`));
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Command timeout after ${timeoutDuration / 1000}s: ${request.command}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Process next request
|
// Process next request
|
||||||
setTimeout(() => this.processNextRequest(), 0);
|
setTimeout(() => this.processNextRequest(), 0);
|
||||||
@@ -604,7 +696,7 @@ export class KiCADMcpServer {
|
|||||||
|
|
||||||
// Write the request to the Python process
|
// Write the request to the Python process
|
||||||
logger.debug(`Sending request: ${requestStr}`);
|
logger.debug(`Sending request: ${requestStr}`);
|
||||||
this.pythonProcess?.stdin?.write(requestStr + '\n');
|
this.pythonProcess?.stdin?.write(requestStr + "\n");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error processing request: ${error}`);
|
logger.error(`Error processing request: ${error}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,291 +1,644 @@
|
|||||||
/**
|
/**
|
||||||
* Component management tools for KiCAD MCP server
|
* Component management tools for KiCAD MCP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { logger } from '../logger.js';
|
import { logger } from "../logger.js";
|
||||||
|
|
||||||
// Command function type for KiCAD script calls
|
// Command function type for KiCAD script calls
|
||||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
type CommandFunction = (
|
||||||
|
command: string,
|
||||||
/**
|
params: Record<string, unknown>,
|
||||||
* Register component management tools with the MCP server
|
) => Promise<any>;
|
||||||
*
|
|
||||||
* @param server MCP server instance
|
/**
|
||||||
* @param callKicadScript Function to call KiCAD script commands
|
* Register component management tools with the MCP server
|
||||||
*/
|
*
|
||||||
export function registerComponentTools(server: McpServer, callKicadScript: CommandFunction): void {
|
* @param server MCP server instance
|
||||||
logger.info('Registering component management tools');
|
* @param callKicadScript Function to call KiCAD script commands
|
||||||
|
*/
|
||||||
// ------------------------------------------------------
|
export function registerComponentTools(
|
||||||
// Place Component Tool
|
server: McpServer,
|
||||||
// ------------------------------------------------------
|
callKicadScript: CommandFunction,
|
||||||
server.tool(
|
): void {
|
||||||
"place_component",
|
logger.info("Registering component management tools");
|
||||||
{
|
|
||||||
componentId: z.string().describe("Identifier for the component to place (e.g., 'R_0603_10k')"),
|
// ------------------------------------------------------
|
||||||
position: z.object({
|
// Place Component Tool
|
||||||
x: z.number().describe("X coordinate"),
|
// ------------------------------------------------------
|
||||||
y: z.number().describe("Y coordinate"),
|
server.tool(
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
"place_component",
|
||||||
}).describe("Position coordinates and unit"),
|
{
|
||||||
reference: z.string().optional().describe("Optional desired reference (e.g., 'R5')"),
|
componentId: z
|
||||||
value: z.string().optional().describe("Optional component value (e.g., '10k')"),
|
.string()
|
||||||
footprint: z.string().optional().describe("Optional specific footprint name"),
|
.describe("Identifier for the component to place (e.g., 'R_0603_10k')"),
|
||||||
rotation: z.number().optional().describe("Optional rotation in degrees"),
|
position: z
|
||||||
layer: z.string().optional().describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')")
|
.object({
|
||||||
},
|
x: z.number().describe("X coordinate"),
|
||||||
async ({ componentId, position, reference, value, footprint, rotation, layer }) => {
|
y: z.number().describe("Y coordinate"),
|
||||||
logger.debug(`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`);
|
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||||
const result = await callKicadScript("place_component", {
|
})
|
||||||
componentId,
|
.describe("Position coordinates and unit"),
|
||||||
position,
|
reference: z
|
||||||
reference,
|
.string()
|
||||||
value,
|
.optional()
|
||||||
footprint,
|
.describe("Optional desired reference (e.g., 'R5')"),
|
||||||
rotation,
|
value: z
|
||||||
layer
|
.string()
|
||||||
});
|
.optional()
|
||||||
|
.describe("Optional component value (e.g., '10k')"),
|
||||||
return {
|
footprint: z
|
||||||
content: [{
|
.string()
|
||||||
type: "text",
|
.optional()
|
||||||
text: JSON.stringify(result)
|
.describe("Optional specific footprint name"),
|
||||||
}]
|
rotation: z.number().optional().describe("Optional rotation in degrees"),
|
||||||
};
|
layer: z
|
||||||
}
|
.string()
|
||||||
);
|
.optional()
|
||||||
|
.describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"),
|
||||||
// ------------------------------------------------------
|
},
|
||||||
// Move Component Tool
|
async ({
|
||||||
// ------------------------------------------------------
|
componentId,
|
||||||
server.tool(
|
position,
|
||||||
"move_component",
|
reference,
|
||||||
{
|
value,
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
footprint,
|
||||||
position: z.object({
|
rotation,
|
||||||
x: z.number().describe("X coordinate"),
|
layer,
|
||||||
y: z.number().describe("Y coordinate"),
|
}) => {
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
logger.debug(
|
||||||
}).describe("New position coordinates and unit"),
|
`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`,
|
||||||
rotation: z.number().optional().describe("Optional new rotation in degrees")
|
);
|
||||||
},
|
const result = await callKicadScript("place_component", {
|
||||||
async ({ reference, position, rotation }) => {
|
componentId,
|
||||||
logger.debug(`Moving component: ${reference} to ${position.x},${position.y} ${position.unit}`);
|
position,
|
||||||
const result = await callKicadScript("move_component", {
|
reference,
|
||||||
reference,
|
value,
|
||||||
position,
|
footprint,
|
||||||
rotation
|
rotation,
|
||||||
});
|
layer,
|
||||||
|
});
|
||||||
return {
|
|
||||||
content: [{
|
return {
|
||||||
type: "text",
|
content: [
|
||||||
text: JSON.stringify(result)
|
{
|
||||||
}]
|
type: "text",
|
||||||
};
|
text: JSON.stringify(result),
|
||||||
}
|
},
|
||||||
);
|
],
|
||||||
|
};
|
||||||
// ------------------------------------------------------
|
},
|
||||||
// Rotate Component Tool
|
);
|
||||||
// ------------------------------------------------------
|
|
||||||
server.tool(
|
// ------------------------------------------------------
|
||||||
"rotate_component",
|
// Move Component Tool
|
||||||
{
|
// ------------------------------------------------------
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
server.tool(
|
||||||
angle: z.number().describe("Rotation angle in degrees (absolute, not relative)")
|
"move_component",
|
||||||
},
|
{
|
||||||
async ({ reference, angle }) => {
|
reference: z
|
||||||
logger.debug(`Rotating component: ${reference} to ${angle} degrees`);
|
.string()
|
||||||
const result = await callKicadScript("rotate_component", {
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
reference,
|
position: z
|
||||||
angle
|
.object({
|
||||||
});
|
x: z.number().describe("X coordinate"),
|
||||||
|
y: z.number().describe("Y coordinate"),
|
||||||
return {
|
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||||
content: [{
|
})
|
||||||
type: "text",
|
.describe("New position coordinates and unit"),
|
||||||
text: JSON.stringify(result)
|
rotation: z
|
||||||
}]
|
.number()
|
||||||
};
|
.optional()
|
||||||
}
|
.describe("Optional new rotation in degrees"),
|
||||||
);
|
},
|
||||||
|
async ({ reference, position, rotation }) => {
|
||||||
// ------------------------------------------------------
|
logger.debug(
|
||||||
// Delete Component Tool
|
`Moving component: ${reference} to ${position.x},${position.y} ${position.unit}`,
|
||||||
// ------------------------------------------------------
|
);
|
||||||
server.tool(
|
const result = await callKicadScript("move_component", {
|
||||||
"delete_component",
|
reference,
|
||||||
{
|
position,
|
||||||
reference: z.string().describe("Reference designator of the component to delete (e.g., 'R5')")
|
rotation,
|
||||||
},
|
});
|
||||||
async ({ reference }) => {
|
|
||||||
logger.debug(`Deleting component: ${reference}`);
|
return {
|
||||||
const result = await callKicadScript("delete_component", { reference });
|
content: [
|
||||||
|
{
|
||||||
return {
|
type: "text",
|
||||||
content: [{
|
text: JSON.stringify(result),
|
||||||
type: "text",
|
},
|
||||||
text: JSON.stringify(result)
|
],
|
||||||
}]
|
};
|
||||||
};
|
},
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
// ------------------------------------------------------
|
||||||
// ------------------------------------------------------
|
// Rotate Component Tool
|
||||||
// Edit Component Properties Tool
|
// ------------------------------------------------------
|
||||||
// ------------------------------------------------------
|
server.tool(
|
||||||
server.tool(
|
"rotate_component",
|
||||||
"edit_component",
|
{
|
||||||
{
|
reference: z
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
.string()
|
||||||
newReference: z.string().optional().describe("Optional new reference designator"),
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
value: z.string().optional().describe("Optional new component value"),
|
angle: z
|
||||||
footprint: z.string().optional().describe("Optional new footprint")
|
.number()
|
||||||
},
|
.describe("Rotation angle in degrees (absolute, not relative)"),
|
||||||
async ({ reference, newReference, value, footprint }) => {
|
},
|
||||||
logger.debug(`Editing component: ${reference}`);
|
async ({ reference, angle }) => {
|
||||||
const result = await callKicadScript("edit_component", {
|
logger.debug(`Rotating component: ${reference} to ${angle} degrees`);
|
||||||
reference,
|
const result = await callKicadScript("rotate_component", {
|
||||||
newReference,
|
reference,
|
||||||
value,
|
angle,
|
||||||
footprint
|
});
|
||||||
});
|
|
||||||
|
return {
|
||||||
return {
|
content: [
|
||||||
content: [{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: JSON.stringify(result)
|
text: JSON.stringify(result),
|
||||||
}]
|
},
|
||||||
};
|
],
|
||||||
}
|
};
|
||||||
);
|
},
|
||||||
|
);
|
||||||
// ------------------------------------------------------
|
|
||||||
// Find Component Tool
|
// ------------------------------------------------------
|
||||||
// ------------------------------------------------------
|
// Delete Component Tool
|
||||||
server.tool(
|
// ------------------------------------------------------
|
||||||
"find_component",
|
server.tool(
|
||||||
{
|
"delete_component",
|
||||||
reference: z.string().optional().describe("Reference designator to search for"),
|
{
|
||||||
value: z.string().optional().describe("Component value to search for")
|
reference: z
|
||||||
},
|
.string()
|
||||||
async ({ reference, value }) => {
|
.describe(
|
||||||
logger.debug(`Finding component with ${reference ? `reference: ${reference}` : `value: ${value}`}`);
|
"Reference designator of the component to delete (e.g., 'R5')",
|
||||||
const result = await callKicadScript("find_component", { reference, value });
|
),
|
||||||
|
},
|
||||||
return {
|
async ({ reference }) => {
|
||||||
content: [{
|
logger.debug(`Deleting component: ${reference}`);
|
||||||
type: "text",
|
const result = await callKicadScript("delete_component", { reference });
|
||||||
text: JSON.stringify(result)
|
|
||||||
}]
|
return {
|
||||||
};
|
content: [
|
||||||
}
|
{
|
||||||
);
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
// ------------------------------------------------------
|
},
|
||||||
// Get Component Properties Tool
|
],
|
||||||
// ------------------------------------------------------
|
};
|
||||||
server.tool(
|
},
|
||||||
"get_component_properties",
|
);
|
||||||
{
|
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')")
|
// ------------------------------------------------------
|
||||||
},
|
// Edit Component Properties Tool
|
||||||
async ({ reference }) => {
|
// ------------------------------------------------------
|
||||||
logger.debug(`Getting properties for component: ${reference}`);
|
server.tool(
|
||||||
const result = await callKicadScript("get_component_properties", { reference });
|
"edit_component",
|
||||||
|
{
|
||||||
return {
|
reference: z
|
||||||
content: [{
|
.string()
|
||||||
type: "text",
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
text: JSON.stringify(result)
|
newReference: z
|
||||||
}]
|
.string()
|
||||||
};
|
.optional()
|
||||||
}
|
.describe("Optional new reference designator"),
|
||||||
);
|
value: z.string().optional().describe("Optional new component value"),
|
||||||
|
footprint: z.string().optional().describe("Optional new footprint"),
|
||||||
// ------------------------------------------------------
|
},
|
||||||
// Add Component Annotation Tool
|
async ({ reference, newReference, value, footprint }) => {
|
||||||
// ------------------------------------------------------
|
logger.debug(`Editing component: ${reference}`);
|
||||||
server.tool(
|
const result = await callKicadScript("edit_component", {
|
||||||
"add_component_annotation",
|
reference,
|
||||||
{
|
newReference,
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
value,
|
||||||
annotation: z.string().describe("Annotation or comment text to add"),
|
footprint,
|
||||||
visible: z.boolean().optional().describe("Whether the annotation should be visible on the PCB")
|
});
|
||||||
},
|
|
||||||
async ({ reference, annotation, visible }) => {
|
return {
|
||||||
logger.debug(`Adding annotation to component: ${reference}`);
|
content: [
|
||||||
const result = await callKicadScript("add_component_annotation", {
|
{
|
||||||
reference,
|
type: "text",
|
||||||
annotation,
|
text: JSON.stringify(result),
|
||||||
visible
|
},
|
||||||
});
|
],
|
||||||
|
};
|
||||||
return {
|
},
|
||||||
content: [{
|
);
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(result)
|
// ------------------------------------------------------
|
||||||
}]
|
// Find Component Tool
|
||||||
};
|
// ------------------------------------------------------
|
||||||
}
|
server.tool(
|
||||||
);
|
"find_component",
|
||||||
|
{
|
||||||
// ------------------------------------------------------
|
reference: z
|
||||||
// Group Components Tool
|
.string()
|
||||||
// ------------------------------------------------------
|
.optional()
|
||||||
server.tool(
|
.describe("Reference designator to search for"),
|
||||||
"group_components",
|
value: z.string().optional().describe("Component value to search for"),
|
||||||
{
|
},
|
||||||
references: z.array(z.string()).describe("Reference designators of components to group"),
|
async ({ reference, value }) => {
|
||||||
groupName: z.string().describe("Name for the component group")
|
logger.debug(
|
||||||
},
|
`Finding component with ${reference ? `reference: ${reference}` : `value: ${value}`}`,
|
||||||
async ({ references, groupName }) => {
|
);
|
||||||
logger.debug(`Grouping components: ${references.join(', ')} as ${groupName}`);
|
const result = await callKicadScript("find_component", {
|
||||||
const result = await callKicadScript("group_components", {
|
reference,
|
||||||
references,
|
value,
|
||||||
groupName
|
});
|
||||||
});
|
|
||||||
|
return {
|
||||||
return {
|
content: [
|
||||||
content: [{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: JSON.stringify(result)
|
text: JSON.stringify(result),
|
||||||
}]
|
},
|
||||||
};
|
],
|
||||||
}
|
};
|
||||||
);
|
},
|
||||||
|
);
|
||||||
// ------------------------------------------------------
|
|
||||||
// Replace Component Tool
|
// ------------------------------------------------------
|
||||||
// ------------------------------------------------------
|
// Get Component Properties Tool
|
||||||
server.tool(
|
// ------------------------------------------------------
|
||||||
"replace_component",
|
server.tool(
|
||||||
{
|
"get_component_properties",
|
||||||
reference: z.string().describe("Reference designator of the component to replace"),
|
{
|
||||||
newComponentId: z.string().describe("ID of the new component to use"),
|
reference: z
|
||||||
newFootprint: z.string().optional().describe("Optional new footprint"),
|
.string()
|
||||||
newValue: z.string().optional().describe("Optional new component value")
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
},
|
},
|
||||||
async ({ reference, newComponentId, newFootprint, newValue }) => {
|
async ({ reference }) => {
|
||||||
logger.debug(`Replacing component: ${reference} with ${newComponentId}`);
|
logger.debug(`Getting properties for component: ${reference}`);
|
||||||
const result = await callKicadScript("replace_component", {
|
const result = await callKicadScript("get_component_properties", {
|
||||||
reference,
|
reference,
|
||||||
newComponentId,
|
});
|
||||||
newFootprint,
|
|
||||||
newValue
|
return {
|
||||||
});
|
content: [
|
||||||
|
{
|
||||||
return {
|
type: "text",
|
||||||
content: [{
|
text: JSON.stringify(result),
|
||||||
type: "text",
|
},
|
||||||
text: JSON.stringify(result)
|
],
|
||||||
}]
|
};
|
||||||
};
|
},
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
// ------------------------------------------------------
|
||||||
logger.info('Component management tools registered');
|
// Add Component Annotation Tool
|
||||||
}
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"add_component_annotation",
|
||||||
|
{
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
|
annotation: z.string().describe("Annotation or comment text to add"),
|
||||||
|
visible: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Whether the annotation should be visible on the PCB"),
|
||||||
|
},
|
||||||
|
async ({ reference, annotation, visible }) => {
|
||||||
|
logger.debug(`Adding annotation to component: ${reference}`);
|
||||||
|
const result = await callKicadScript("add_component_annotation", {
|
||||||
|
reference,
|
||||||
|
annotation,
|
||||||
|
visible,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Group Components Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"group_components",
|
||||||
|
{
|
||||||
|
references: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe("Reference designators of components to group"),
|
||||||
|
groupName: z.string().describe("Name for the component group"),
|
||||||
|
},
|
||||||
|
async ({ references, groupName }) => {
|
||||||
|
logger.debug(
|
||||||
|
`Grouping components: ${references.join(", ")} as ${groupName}`,
|
||||||
|
);
|
||||||
|
const result = await callKicadScript("group_components", {
|
||||||
|
references,
|
||||||
|
groupName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Replace Component Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"replace_component",
|
||||||
|
{
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component to replace"),
|
||||||
|
newComponentId: z.string().describe("ID of the new component to use"),
|
||||||
|
newFootprint: z.string().optional().describe("Optional new footprint"),
|
||||||
|
newValue: z.string().optional().describe("Optional new component value"),
|
||||||
|
},
|
||||||
|
async ({ reference, newComponentId, newFootprint, newValue }) => {
|
||||||
|
logger.debug(`Replacing component: ${reference} with ${newComponentId}`);
|
||||||
|
const result = await callKicadScript("replace_component", {
|
||||||
|
reference,
|
||||||
|
newComponentId,
|
||||||
|
newFootprint,
|
||||||
|
newValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Get Component Pads Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"get_component_pads",
|
||||||
|
{
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'U1')"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for coordinates (default: mm)"),
|
||||||
|
},
|
||||||
|
async ({ reference, unit }) => {
|
||||||
|
logger.debug(`Getting pads for component: ${reference}`);
|
||||||
|
const result = await callKicadScript("get_component_pads", {
|
||||||
|
reference,
|
||||||
|
unit: unit || "mm",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Get Component List Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"get_component_list",
|
||||||
|
{
|
||||||
|
layer: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by layer (e.g., 'F.Cu', 'B.Cu')"),
|
||||||
|
boundingBox: z
|
||||||
|
.object({
|
||||||
|
x1: z.number(),
|
||||||
|
y1: z.number(),
|
||||||
|
x2: z.number(),
|
||||||
|
y2: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by bounding box region"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for coordinates (default: mm)"),
|
||||||
|
},
|
||||||
|
async ({ layer, boundingBox, unit }) => {
|
||||||
|
logger.debug("Getting component list");
|
||||||
|
const result = await callKicadScript("get_component_list", {
|
||||||
|
layer,
|
||||||
|
boundingBox,
|
||||||
|
unit: unit || "mm",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Get Pad Position Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"get_pad_position",
|
||||||
|
{
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Component reference designator (e.g., 'U1')"),
|
||||||
|
pad: z.string().describe("Pad number or name (e.g., '1', 'A1')"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for coordinates (default: mm)"),
|
||||||
|
},
|
||||||
|
async ({ reference, pad, unit }) => {
|
||||||
|
logger.debug(`Getting pad position for ${reference} pad ${pad}`);
|
||||||
|
const result = await callKicadScript("get_pad_position", {
|
||||||
|
reference,
|
||||||
|
pad,
|
||||||
|
unit: unit || "mm",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Place Component Array Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"place_component_array",
|
||||||
|
{
|
||||||
|
componentId: z.string().describe("Component identifier"),
|
||||||
|
startPosition: z
|
||||||
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]),
|
||||||
|
})
|
||||||
|
.describe("Starting position"),
|
||||||
|
rows: z.number().describe("Number of rows"),
|
||||||
|
columns: z.number().describe("Number of columns"),
|
||||||
|
rowSpacing: z.number().describe("Spacing between rows"),
|
||||||
|
columnSpacing: z.number().describe("Spacing between columns"),
|
||||||
|
startReference: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Starting reference (e.g., 'R1')"),
|
||||||
|
footprint: z.string().optional().describe("Footprint name"),
|
||||||
|
value: z.string().optional().describe("Component value"),
|
||||||
|
rotation: z.number().optional().describe("Rotation in degrees"),
|
||||||
|
},
|
||||||
|
async ({
|
||||||
|
componentId,
|
||||||
|
startPosition,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
rowSpacing,
|
||||||
|
columnSpacing,
|
||||||
|
startReference,
|
||||||
|
footprint,
|
||||||
|
value,
|
||||||
|
rotation,
|
||||||
|
}) => {
|
||||||
|
logger.debug(
|
||||||
|
`Placing component array: ${rows}x${columns} of ${componentId}`,
|
||||||
|
);
|
||||||
|
const result = await callKicadScript("place_component_array", {
|
||||||
|
componentId,
|
||||||
|
startPosition,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
rowSpacing,
|
||||||
|
columnSpacing,
|
||||||
|
startReference,
|
||||||
|
footprint,
|
||||||
|
value,
|
||||||
|
rotation,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Align Components Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"align_components",
|
||||||
|
{
|
||||||
|
references: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe("Array of component references to align"),
|
||||||
|
alignmentType: z
|
||||||
|
.enum(["horizontal", "vertical", "grid"])
|
||||||
|
.describe("Type of alignment"),
|
||||||
|
spacing: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Spacing between components in mm"),
|
||||||
|
referenceComponent: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Reference component for alignment"),
|
||||||
|
},
|
||||||
|
async ({ references, alignmentType, spacing, referenceComponent }) => {
|
||||||
|
logger.debug(`Aligning components: ${references.join(", ")}`);
|
||||||
|
const result = await callKicadScript("align_components", {
|
||||||
|
references,
|
||||||
|
alignmentType,
|
||||||
|
spacing,
|
||||||
|
referenceComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Duplicate Component Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"duplicate_component",
|
||||||
|
{
|
||||||
|
reference: z.string().describe("Reference of component to duplicate"),
|
||||||
|
offset: z
|
||||||
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.describe("Offset from original position"),
|
||||||
|
newReference: z.string().optional().describe("New reference designator"),
|
||||||
|
count: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Number of duplicates (default: 1)"),
|
||||||
|
},
|
||||||
|
async ({ reference, offset, newReference, count }) => {
|
||||||
|
logger.debug(`Duplicating component: ${reference}`);
|
||||||
|
const result = await callKicadScript("duplicate_component", {
|
||||||
|
reference,
|
||||||
|
offset,
|
||||||
|
newReference,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("Component management tools registered");
|
||||||
|
}
|
||||||
|
|||||||
123
src/tools/datasheet.ts
Normal file
123
src/tools/datasheet.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Datasheet tools for KiCAD MCP server
|
||||||
|
*
|
||||||
|
* Enriches KiCAD schematic symbols with LCSC datasheet URLs.
|
||||||
|
* URL schema: https://www.lcsc.com/datasheet/<LCSC#>.pdf (no API key required)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export function registerDatasheetTools(
|
||||||
|
server: McpServer,
|
||||||
|
callKicadScript: Function,
|
||||||
|
) {
|
||||||
|
// ── enrich_datasheets ──────────────────────────────────────────────────────
|
||||||
|
server.tool(
|
||||||
|
"enrich_datasheets",
|
||||||
|
`Fill in missing Datasheet URLs in a KiCAD schematic using LCSC part numbers.
|
||||||
|
|
||||||
|
For every placed symbol that has:
|
||||||
|
• (property "LCSC" "C123456") set
|
||||||
|
• (property "Datasheet" "~") or empty
|
||||||
|
|
||||||
|
Sets the Datasheet field to:
|
||||||
|
https://www.lcsc.com/datasheet/C123456.pdf
|
||||||
|
|
||||||
|
The URL is then visible in KiCAD's footprint browser, symbol properties dialog,
|
||||||
|
and any tool that reads the standard KiCAD Datasheet field.
|
||||||
|
No API key or internet lookup required – the URL is constructed directly.
|
||||||
|
|
||||||
|
Use dry_run=true to preview changes without writing.`,
|
||||||
|
{
|
||||||
|
schematic_path: z
|
||||||
|
.string()
|
||||||
|
.describe("Path to the .kicad_sch file to enrich"),
|
||||||
|
dry_run: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(false)
|
||||||
|
.describe(
|
||||||
|
"If true, show what would be changed without writing to disk (default: false)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
async (args: { schematic_path: string; dry_run?: boolean }) => {
|
||||||
|
const result = await callKicadScript("enrich_datasheets", args);
|
||||||
|
if (result.success) {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
if (args.dry_run) {
|
||||||
|
lines.push(`[DRY RUN] Schematic: ${result.schematic}\n`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Schematic: ${result.schematic}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`✓ Updated: ${result.updated}`);
|
||||||
|
lines.push(` Already set: ${result.already_set}`);
|
||||||
|
lines.push(` No LCSC number: ${result.no_lcsc}`);
|
||||||
|
lines.push(` No field: ${result.no_datasheet_field}`);
|
||||||
|
|
||||||
|
if (result.details && result.details.length > 0) {
|
||||||
|
lines.push("\nComponents updated:");
|
||||||
|
for (const d of result.details) {
|
||||||
|
lines.push(` ${d.reference.padEnd(6)} ${d.lcsc.padEnd(12)} → ${d.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.updated === 0 && !args.dry_run) {
|
||||||
|
lines.push(
|
||||||
|
"\nNo changes needed – all LCSC components already have a Datasheet URL.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Failed to enrich datasheets: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── get_datasheet_url ──────────────────────────────────────────────────────
|
||||||
|
server.tool(
|
||||||
|
"get_datasheet_url",
|
||||||
|
`Get the LCSC datasheet URL for a component by LCSC number.
|
||||||
|
|
||||||
|
Returns the direct PDF URL and product page URL.
|
||||||
|
No network request – URL is constructed from the LCSC number alone.
|
||||||
|
|
||||||
|
Example: get_datasheet_url("C179739")
|
||||||
|
→ https://www.lcsc.com/datasheet/C179739.pdf`,
|
||||||
|
{
|
||||||
|
lcsc: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'LCSC part number, with or without "C" prefix (e.g. "C179739" or "179739")',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
async (args: { lcsc: string }) => {
|
||||||
|
const result = await callKicadScript("get_datasheet_url", { lcsc: args.lcsc });
|
||||||
|
if (result.success) {
|
||||||
|
const lines = [
|
||||||
|
`LCSC: ${result.lcsc}`,
|
||||||
|
`Datasheet PDF: ${result.datasheet_url}`,
|
||||||
|
`Product page: ${result.product_url}`,
|
||||||
|
];
|
||||||
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Invalid LCSC number: ${args.lcsc}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Tools index for KiCAD MCP server
|
* Tools index for KiCAD MCP server
|
||||||
*
|
*
|
||||||
* Exports all tool registration functions
|
* Exports all tool registration functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { registerProjectTools } from './project.js';
|
export { registerProjectTools } from "./project.js";
|
||||||
export { registerBoardTools } from './board.js';
|
export { registerBoardTools } from "./board.js";
|
||||||
export { registerComponentTools } from './component.js';
|
export { registerComponentTools } from "./component.js";
|
||||||
export { registerRoutingTools } from './routing.js';
|
export { registerRoutingTools } from "./routing.js";
|
||||||
export { registerDesignRuleTools } from './design-rules.js';
|
export { registerDesignRuleTools } from "./design-rules.js";
|
||||||
export { registerExportTools } from './export.js';
|
export { registerExportTools } from "./export.js";
|
||||||
export { registerSchematicTools } from './schematic.js';
|
export { registerSchematicTools } from "./schematic.js";
|
||||||
export { registerLibraryTools } from './library.js';
|
export { registerLibraryTools } from "./library.js";
|
||||||
export { registerUITools } from './ui.js';
|
export { registerUITools } from "./ui.js";
|
||||||
|
export { registerDatasheetTools } from "./datasheet.js";
|
||||||
|
|||||||
@@ -1,159 +1,193 @@
|
|||||||
/**
|
/**
|
||||||
* Library tools for KiCAD MCP server
|
* Library tools for KiCAD MCP server
|
||||||
* Provides access to KiCAD footprint libraries and symbols
|
* Provides access to KiCAD footprint libraries and symbols
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerLibraryTools(server: McpServer, callKicadScript: Function) {
|
export function registerLibraryTools(
|
||||||
// List available footprint libraries
|
server: McpServer,
|
||||||
server.tool(
|
callKicadScript: Function,
|
||||||
"list_libraries",
|
) {
|
||||||
"List all available KiCAD footprint libraries",
|
// List available footprint libraries
|
||||||
{
|
server.tool(
|
||||||
search_paths: z.array(z.string()).optional()
|
"list_libraries",
|
||||||
.describe("Optional additional search paths for libraries")
|
"List all available KiCAD footprint libraries",
|
||||||
},
|
{
|
||||||
async (args: { search_paths?: string[] }) => {
|
search_paths: z
|
||||||
const result = await callKicadScript("list_libraries", args);
|
.array(z.string())
|
||||||
if (result.success && result.libraries) {
|
.optional()
|
||||||
return {
|
.describe("Optional additional search paths for libraries"),
|
||||||
content: [
|
},
|
||||||
{
|
async (args: { search_paths?: string[] }) => {
|
||||||
type: "text",
|
const result = await callKicadScript("list_libraries", args);
|
||||||
text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join('\n')}`
|
if (result.success && result.libraries) {
|
||||||
}
|
return {
|
||||||
]
|
content: [
|
||||||
};
|
{
|
||||||
}
|
type: "text",
|
||||||
return {
|
text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join("\n")}`,
|
||||||
content: [
|
},
|
||||||
{
|
],
|
||||||
type: "text",
|
};
|
||||||
text: `Failed to list libraries: ${result.message || 'Unknown error'}`
|
}
|
||||||
}
|
return {
|
||||||
]
|
content: [
|
||||||
};
|
{
|
||||||
}
|
type: "text",
|
||||||
);
|
text: `Failed to list libraries: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
// Search for footprints across all libraries
|
],
|
||||||
server.tool(
|
};
|
||||||
"search_footprints",
|
},
|
||||||
"Search for footprints matching a pattern across all libraries",
|
);
|
||||||
{
|
|
||||||
search_term: z.string()
|
// Search for footprints across all libraries
|
||||||
.describe("Search term or pattern to match footprint names"),
|
server.tool(
|
||||||
library: z.string().optional()
|
"search_footprints",
|
||||||
.describe("Optional specific library to search in"),
|
"Search for footprints matching a pattern across all libraries",
|
||||||
limit: z.number().optional().default(50)
|
{
|
||||||
.describe("Maximum number of results to return")
|
search_term: z
|
||||||
},
|
.string()
|
||||||
async (args: { search_term: string; library?: string; limit?: number }) => {
|
.describe("Search term or pattern to match footprint names"),
|
||||||
const result = await callKicadScript("search_footprints", args);
|
library: z
|
||||||
if (result.success && result.footprints) {
|
.string()
|
||||||
const footprintList = result.footprints.map((fp: any) =>
|
.optional()
|
||||||
`${fp.library}:${fp.name}${fp.description ? ' - ' + fp.description : ''}`
|
.describe("Optional specific library to search in"),
|
||||||
).join('\n');
|
limit: z
|
||||||
return {
|
.number()
|
||||||
content: [
|
.optional()
|
||||||
{
|
.default(50)
|
||||||
type: "text",
|
.describe("Maximum number of results to return"),
|
||||||
text: `Found ${result.footprints.length} matching footprints:\n${footprintList}`
|
},
|
||||||
}
|
async (args: { search_term: string; library?: string; limit?: number }) => {
|
||||||
]
|
const result = await callKicadScript("search_footprints", {
|
||||||
};
|
pattern: args.search_term,
|
||||||
}
|
library: args.library,
|
||||||
return {
|
limit: args.limit,
|
||||||
content: [
|
});
|
||||||
{
|
if (result.success && result.footprints) {
|
||||||
type: "text",
|
const footprintList = result.footprints
|
||||||
text: `Failed to search footprints: ${result.message || 'Unknown error'}`
|
.map(
|
||||||
}
|
(fp: any) =>
|
||||||
]
|
`${fp.full_name || fp.library + ":" + fp.footprint}${fp.description ? " - " + fp.description : ""}`,
|
||||||
};
|
)
|
||||||
}
|
.join("\n");
|
||||||
);
|
return {
|
||||||
|
content: [
|
||||||
// List footprints in a specific library
|
{
|
||||||
server.tool(
|
type: "text",
|
||||||
"list_library_footprints",
|
text: `Found ${result.footprints.length} matching footprints:\n${footprintList}`,
|
||||||
"List all footprints in a specific KiCAD library",
|
},
|
||||||
{
|
],
|
||||||
library_name: z.string()
|
};
|
||||||
.describe("Name of the library to list footprints from"),
|
}
|
||||||
filter: z.string().optional()
|
return {
|
||||||
.describe("Optional filter pattern for footprint names"),
|
content: [
|
||||||
limit: z.number().optional().default(100)
|
{
|
||||||
.describe("Maximum number of footprints to list")
|
type: "text",
|
||||||
},
|
text: `Failed to search footprints: ${result.message || "Unknown error"}`,
|
||||||
async (args: { library_name: string; filter?: string; limit?: number }) => {
|
},
|
||||||
const result = await callKicadScript("list_library_footprints", args);
|
],
|
||||||
if (result.success && result.footprints) {
|
};
|
||||||
const footprintList = result.footprints.map((fp: string) => ` - ${fp}`).join('\n');
|
},
|
||||||
return {
|
);
|
||||||
content: [
|
|
||||||
{
|
// List footprints in a specific library
|
||||||
type: "text",
|
server.tool(
|
||||||
text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}`
|
"list_library_footprints",
|
||||||
}
|
"List all footprints in a specific KiCAD library",
|
||||||
]
|
{
|
||||||
};
|
library_name: z
|
||||||
}
|
.string()
|
||||||
return {
|
.describe("Name of the library to list footprints from"),
|
||||||
content: [
|
filter: z
|
||||||
{
|
.string()
|
||||||
type: "text",
|
.optional()
|
||||||
text: `Failed to list footprints in library ${args.library_name}: ${result.message || 'Unknown error'}`
|
.describe("Optional filter pattern for footprint names"),
|
||||||
}
|
limit: z
|
||||||
]
|
.number()
|
||||||
};
|
.optional()
|
||||||
}
|
.default(100)
|
||||||
);
|
.describe("Maximum number of footprints to list"),
|
||||||
|
},
|
||||||
// Get detailed information about a specific footprint
|
async (args: { library_name: string; filter?: string; limit?: number }) => {
|
||||||
server.tool(
|
const result = await callKicadScript("list_library_footprints", args);
|
||||||
"get_footprint_info",
|
if (result.success && result.footprints) {
|
||||||
"Get detailed information about a specific footprint",
|
const footprintList = result.footprints
|
||||||
{
|
.map((fp: string) => ` - ${fp}`)
|
||||||
library_name: z.string()
|
.join("\n");
|
||||||
.describe("Name of the library containing the footprint"),
|
return {
|
||||||
footprint_name: z.string()
|
content: [
|
||||||
.describe("Name of the footprint to get information about")
|
{
|
||||||
},
|
type: "text",
|
||||||
async (args: { library_name: string; footprint_name: string }) => {
|
text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}`,
|
||||||
const result = await callKicadScript("get_footprint_info", args);
|
},
|
||||||
if (result.success && result.info) {
|
],
|
||||||
const info = result.info;
|
};
|
||||||
const details = [
|
}
|
||||||
`Footprint: ${info.name}`,
|
return {
|
||||||
`Library: ${info.library}`,
|
content: [
|
||||||
info.description ? `Description: ${info.description}` : '',
|
{
|
||||||
info.keywords ? `Keywords: ${info.keywords}` : '',
|
type: "text",
|
||||||
info.pads ? `Number of pads: ${info.pads}` : '',
|
text: `Failed to list footprints in library ${args.library_name}: ${result.message || "Unknown error"}`,
|
||||||
info.layers ? `Layers used: ${info.layers.join(', ')}` : '',
|
},
|
||||||
info.courtyard ? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm` : '',
|
],
|
||||||
info.attributes ? `Attributes: ${JSON.stringify(info.attributes)}` : ''
|
};
|
||||||
].filter(line => line).join('\n');
|
},
|
||||||
|
);
|
||||||
return {
|
|
||||||
content: [
|
// Get detailed information about a specific footprint
|
||||||
{
|
server.tool(
|
||||||
type: "text",
|
"get_footprint_info",
|
||||||
text: details
|
"Get detailed information about a specific footprint",
|
||||||
}
|
{
|
||||||
]
|
library_name: z
|
||||||
};
|
.string()
|
||||||
}
|
.describe("Name of the library containing the footprint"),
|
||||||
return {
|
footprint_name: z
|
||||||
content: [
|
.string()
|
||||||
{
|
.describe("Name of the footprint to get information about"),
|
||||||
type: "text",
|
},
|
||||||
text: `Failed to get footprint info: ${result.message || 'Unknown error'}`
|
async (args: { library_name: string; footprint_name: string }) => {
|
||||||
}
|
const result = await callKicadScript("get_footprint_info", args);
|
||||||
]
|
if (result.success && result.info) {
|
||||||
};
|
const info = result.info;
|
||||||
}
|
const details = [
|
||||||
);
|
`Footprint: ${info.name}`,
|
||||||
}
|
`Library: ${info.library}`,
|
||||||
|
info.description ? `Description: ${info.description}` : "",
|
||||||
|
info.keywords ? `Keywords: ${info.keywords}` : "",
|
||||||
|
info.pads ? `Number of pads: ${info.pads}` : "",
|
||||||
|
info.layers ? `Layers used: ${info.layers.join(", ")}` : "",
|
||||||
|
info.courtyard
|
||||||
|
? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm`
|
||||||
|
: "",
|
||||||
|
info.attributes
|
||||||
|
? `Attributes: ${JSON.stringify(info.attributes)}`
|
||||||
|
: "",
|
||||||
|
]
|
||||||
|
.filter((line) => line)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: details,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Failed to get footprint info: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,101 +1,324 @@
|
|||||||
/**
|
/**
|
||||||
* Routing tools for KiCAD MCP server
|
* Routing tools for KiCAD MCP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerRoutingTools(server: McpServer, callKicadScript: Function) {
|
export function registerRoutingTools(
|
||||||
// Add net tool
|
server: McpServer,
|
||||||
server.tool(
|
callKicadScript: Function,
|
||||||
"add_net",
|
) {
|
||||||
"Create a new net on the PCB",
|
// Add net tool
|
||||||
{
|
server.tool(
|
||||||
name: z.string().describe("Net name"),
|
"add_net",
|
||||||
netClass: z.string().optional().describe("Net class name"),
|
"Create a new net on the PCB",
|
||||||
},
|
{
|
||||||
async (args: { name: string; netClass?: string }) => {
|
name: z.string().describe("Net name"),
|
||||||
const result = await callKicadScript("add_net", args);
|
netClass: z.string().optional().describe("Net class name"),
|
||||||
return {
|
},
|
||||||
content: [{
|
async (args: { name: string; netClass?: string }) => {
|
||||||
type: "text",
|
const result = await callKicadScript("add_net", args);
|
||||||
text: JSON.stringify(result, null, 2)
|
return {
|
||||||
}]
|
content: [
|
||||||
};
|
{
|
||||||
}
|
type: "text",
|
||||||
);
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
// Route trace tool
|
],
|
||||||
server.tool(
|
};
|
||||||
"route_trace",
|
},
|
||||||
"Route a trace between two points",
|
);
|
||||||
{
|
|
||||||
start: z.object({
|
// Route trace tool
|
||||||
x: z.number(),
|
server.tool(
|
||||||
y: z.number(),
|
"route_trace",
|
||||||
unit: z.string().optional()
|
"Route a trace between two points",
|
||||||
}).describe("Start position"),
|
{
|
||||||
end: z.object({
|
start: z
|
||||||
x: z.number(),
|
.object({
|
||||||
y: z.number(),
|
x: z.number(),
|
||||||
unit: z.string().optional()
|
y: z.number(),
|
||||||
}).describe("End position"),
|
unit: z.string().optional(),
|
||||||
layer: z.string().describe("PCB layer"),
|
})
|
||||||
width: z.number().describe("Trace width in mm"),
|
.describe("Start position"),
|
||||||
net: z.string().describe("Net name"),
|
end: z
|
||||||
},
|
.object({
|
||||||
async (args: any) => {
|
x: z.number(),
|
||||||
const result = await callKicadScript("route_trace", args);
|
y: z.number(),
|
||||||
return {
|
unit: z.string().optional(),
|
||||||
content: [{
|
})
|
||||||
type: "text",
|
.describe("End position"),
|
||||||
text: JSON.stringify(result, null, 2)
|
layer: z.string().describe("PCB layer"),
|
||||||
}]
|
width: z.number().describe("Trace width in mm"),
|
||||||
};
|
net: z.string().describe("Net name"),
|
||||||
}
|
},
|
||||||
);
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("route_trace", args);
|
||||||
// Add via tool
|
return {
|
||||||
server.tool(
|
content: [
|
||||||
"add_via",
|
{
|
||||||
"Add a via to the PCB",
|
type: "text",
|
||||||
{
|
text: JSON.stringify(result, null, 2),
|
||||||
position: z.object({
|
},
|
||||||
x: z.number(),
|
],
|
||||||
y: z.number(),
|
};
|
||||||
unit: z.string().optional()
|
},
|
||||||
}).describe("Via position"),
|
);
|
||||||
net: z.string().describe("Net name"),
|
|
||||||
viaType: z.string().optional().describe("Via type (through, blind, buried)"),
|
// Add via tool
|
||||||
},
|
server.tool(
|
||||||
async (args: any) => {
|
"add_via",
|
||||||
const result = await callKicadScript("add_via", args);
|
"Add a via to the PCB",
|
||||||
return {
|
{
|
||||||
content: [{
|
position: z
|
||||||
type: "text",
|
.object({
|
||||||
text: JSON.stringify(result, null, 2)
|
x: z.number(),
|
||||||
}]
|
y: z.number(),
|
||||||
};
|
unit: z.string().optional(),
|
||||||
}
|
})
|
||||||
);
|
.describe("Via position"),
|
||||||
|
net: z.string().describe("Net name"),
|
||||||
// Add copper pour tool
|
viaType: z
|
||||||
server.tool(
|
.string()
|
||||||
"add_copper_pour",
|
.optional()
|
||||||
"Add a copper pour (ground/power plane) to the PCB",
|
.describe("Via type (through, blind, buried)"),
|
||||||
{
|
},
|
||||||
layer: z.string().describe("PCB layer"),
|
async (args: any) => {
|
||||||
net: z.string().describe("Net name"),
|
const result = await callKicadScript("add_via", args);
|
||||||
clearance: z.number().optional().describe("Clearance in mm"),
|
return {
|
||||||
},
|
content: [
|
||||||
async (args: any) => {
|
{
|
||||||
const result = await callKicadScript("add_copper_pour", args);
|
type: "text",
|
||||||
return {
|
text: JSON.stringify(result, null, 2),
|
||||||
content: [{
|
},
|
||||||
type: "text",
|
],
|
||||||
text: JSON.stringify(result, null, 2)
|
};
|
||||||
}]
|
},
|
||||||
};
|
);
|
||||||
}
|
|
||||||
);
|
// Add copper pour tool
|
||||||
}
|
server.tool(
|
||||||
|
"add_copper_pour",
|
||||||
|
"Add a copper pour (ground/power plane) to the PCB",
|
||||||
|
{
|
||||||
|
layer: z.string().describe("PCB layer"),
|
||||||
|
net: z.string().describe("Net name"),
|
||||||
|
clearance: z.number().optional().describe("Clearance in mm"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("add_copper_pour", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete trace tool
|
||||||
|
server.tool(
|
||||||
|
"delete_trace",
|
||||||
|
"Delete traces from the PCB. Can delete by UUID, position, or bulk-delete all traces on a net.",
|
||||||
|
{
|
||||||
|
traceUuid: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("UUID of a specific trace to delete"),
|
||||||
|
position: z
|
||||||
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Delete trace nearest to this position"),
|
||||||
|
net: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Delete all traces on this net (bulk delete)"),
|
||||||
|
layer: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by layer when using net-based deletion"),
|
||||||
|
includeVias: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Include vias in net-based deletion"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("delete_trace", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query traces tool
|
||||||
|
server.tool(
|
||||||
|
"query_traces",
|
||||||
|
"Query traces on the board with optional filters by net, layer, or bounding box.",
|
||||||
|
{
|
||||||
|
net: z.string().optional().describe("Filter by net name"),
|
||||||
|
layer: z.string().optional().describe("Filter by layer name"),
|
||||||
|
boundingBox: z
|
||||||
|
.object({
|
||||||
|
x1: z.number(),
|
||||||
|
y1: z.number(),
|
||||||
|
x2: z.number(),
|
||||||
|
y2: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by bounding box region"),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("query_traces", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get nets list tool
|
||||||
|
server.tool(
|
||||||
|
"get_nets_list",
|
||||||
|
"Get a list of all nets in the PCB with optional statistics.",
|
||||||
|
{
|
||||||
|
includeStats: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Include statistics (track count, total length, etc.)"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for length measurements"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("get_nets_list", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modify trace tool
|
||||||
|
server.tool(
|
||||||
|
"modify_trace",
|
||||||
|
"Modify an existing trace (change width, layer, or net).",
|
||||||
|
{
|
||||||
|
traceUuid: z.string().describe("UUID of the trace to modify"),
|
||||||
|
width: z.number().optional().describe("New trace width in mm"),
|
||||||
|
layer: z.string().optional().describe("New layer name"),
|
||||||
|
net: z.string().optional().describe("New net name"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("modify_trace", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create netclass tool
|
||||||
|
server.tool(
|
||||||
|
"create_netclass",
|
||||||
|
"Create a new net class with custom design rules.",
|
||||||
|
{
|
||||||
|
name: z.string().describe("Net class name"),
|
||||||
|
traceWidth: z.number().optional().describe("Default trace width in mm"),
|
||||||
|
clearance: z.number().optional().describe("Clearance in mm"),
|
||||||
|
viaDiameter: z.number().optional().describe("Via diameter in mm"),
|
||||||
|
viaDrill: z.number().optional().describe("Via drill size in mm"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("create_netclass", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Route differential pair tool
|
||||||
|
server.tool(
|
||||||
|
"route_differential_pair",
|
||||||
|
"Route a differential pair between two sets of points.",
|
||||||
|
{
|
||||||
|
positivePad: z
|
||||||
|
.object({
|
||||||
|
reference: z.string(),
|
||||||
|
pad: z.string(),
|
||||||
|
})
|
||||||
|
.describe("Positive pad (component and pad number)"),
|
||||||
|
negativePad: z
|
||||||
|
.object({
|
||||||
|
reference: z.string(),
|
||||||
|
pad: z.string(),
|
||||||
|
})
|
||||||
|
.describe("Negative pad (component and pad number)"),
|
||||||
|
layer: z.string().describe("PCB layer"),
|
||||||
|
width: z.number().describe("Trace width in mm"),
|
||||||
|
gap: z.number().describe("Gap between traces in mm"),
|
||||||
|
positiveNet: z.string().describe("Positive net name"),
|
||||||
|
negativeNet: z.string().describe("Negative net name"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("route_differential_pair", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refill zones tool
|
||||||
|
server.tool(
|
||||||
|
"refill_zones",
|
||||||
|
"Refill all copper zones on the board. WARNING: SWIG path has known segfault risk (see KNOWN_ISSUES.md). Prefer using IPC backend (KiCAD open) or triggering zone fill via KiCAD UI instead.",
|
||||||
|
{},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("refill_zones", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,268 +1,383 @@
|
|||||||
/**
|
/**
|
||||||
* Schematic tools for KiCAD MCP server
|
* Schematic tools for KiCAD MCP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerSchematicTools(server: McpServer, callKicadScript: Function) {
|
export function registerSchematicTools(
|
||||||
// Create schematic tool
|
server: McpServer,
|
||||||
server.tool(
|
callKicadScript: Function,
|
||||||
"create_schematic",
|
) {
|
||||||
"Create a new schematic",
|
// Create schematic tool
|
||||||
{
|
server.tool(
|
||||||
name: z.string().describe("Schematic name"),
|
"create_schematic",
|
||||||
path: z.string().optional().describe("Optional path"),
|
"Create a new schematic",
|
||||||
},
|
{
|
||||||
async (args: { name: string; path?: string }) => {
|
name: z.string().describe("Schematic name"),
|
||||||
const result = await callKicadScript("create_schematic", args);
|
path: z.string().optional().describe("Optional path"),
|
||||||
return {
|
},
|
||||||
content: [{
|
async (args: { name: string; path?: string }) => {
|
||||||
type: "text",
|
const result = await callKicadScript("create_schematic", args);
|
||||||
text: JSON.stringify(result, null, 2)
|
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"),
|
// Add component to schematic
|
||||||
symbol: z.string().describe("Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)"),
|
server.tool(
|
||||||
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
"add_schematic_component",
|
||||||
value: z.string().optional().describe("Component value"),
|
"Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')",
|
||||||
position: z.object({
|
{
|
||||||
x: z.number(),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
y: z.number()
|
symbol: z
|
||||||
}).optional().describe("Position on schematic"),
|
.string()
|
||||||
},
|
.describe(
|
||||||
async (args: { schematicPath: string; symbol: string; reference: string; value?: string; position?: { x: number; y: number } }) => {
|
"Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)",
|
||||||
// Transform to what Python backend expects
|
),
|
||||||
const [library, symbolName] = args.symbol.includes(':')
|
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
||||||
? args.symbol.split(':')
|
value: z.string().optional().describe("Component value"),
|
||||||
: ['Device', args.symbol];
|
position: z
|
||||||
|
.object({
|
||||||
const transformed = {
|
x: z.number(),
|
||||||
schematicPath: args.schematicPath,
|
y: z.number(),
|
||||||
component: {
|
})
|
||||||
library,
|
.optional()
|
||||||
type: symbolName,
|
.describe("Position on schematic"),
|
||||||
reference: args.reference,
|
},
|
||||||
value: args.value,
|
async (args: {
|
||||||
// Python expects flat x, y not nested position
|
schematicPath: string;
|
||||||
x: args.position?.x ?? 0,
|
symbol: string;
|
||||||
y: args.position?.y ?? 0
|
reference: string;
|
||||||
}
|
value?: string;
|
||||||
};
|
position?: { x: number; y: number };
|
||||||
|
}) => {
|
||||||
const result = await callKicadScript("add_schematic_component", transformed);
|
// Transform to what Python backend expects
|
||||||
if (result.success) {
|
const [library, symbolName] = args.symbol.includes(":")
|
||||||
return {
|
? args.symbol.split(":")
|
||||||
content: [{
|
: ["Device", args.symbol];
|
||||||
type: "text",
|
|
||||||
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`
|
const transformed = {
|
||||||
}]
|
schematicPath: args.schematicPath,
|
||||||
};
|
component: {
|
||||||
} else {
|
library,
|
||||||
return {
|
type: symbolName,
|
||||||
content: [{
|
reference: args.reference,
|
||||||
type: "text",
|
value: args.value,
|
||||||
text: `Failed to add component: ${result.message || JSON.stringify(result)}`
|
// Python expects flat x, y not nested position
|
||||||
}]
|
x: args.position?.x ?? 0,
|
||||||
};
|
y: args.position?.y ?? 0,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
const result = await callKicadScript(
|
||||||
// Connect components with wire
|
"add_schematic_component",
|
||||||
server.tool(
|
transformed,
|
||||||
"add_wire",
|
);
|
||||||
"Add a wire connection in the schematic",
|
if (result.success) {
|
||||||
{
|
return {
|
||||||
start: z.object({
|
content: [
|
||||||
x: z.number(),
|
{
|
||||||
y: z.number()
|
type: "text",
|
||||||
}).describe("Start position"),
|
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`,
|
||||||
end: z.object({
|
},
|
||||||
x: z.number(),
|
],
|
||||||
y: z.number()
|
};
|
||||||
}).describe("End position"),
|
} else {
|
||||||
},
|
return {
|
||||||
async (args: any) => {
|
content: [
|
||||||
const result = await callKicadScript("add_wire", args);
|
{
|
||||||
return {
|
type: "text",
|
||||||
content: [{
|
text: `Failed to add component: ${result.message || JSON.stringify(result)}`,
|
||||||
type: "text",
|
},
|
||||||
text: JSON.stringify(result, null, 2)
|
],
|
||||||
}]
|
};
|
||||||
};
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add pin-to-pin connection
|
// Delete component from schematic
|
||||||
server.tool(
|
server.tool(
|
||||||
"add_schematic_connection",
|
"delete_schematic_component",
|
||||||
"Connect two component pins with a wire",
|
`Remove a placed symbol from a KiCAD schematic (.kicad_sch).
|
||||||
{
|
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
This removes the symbol instance (the placed component) from the schematic.
|
||||||
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
It does NOT remove the symbol definition from lib_symbols.
|
||||||
sourcePin: z.string().describe("Source pin name/number (e.g., 1, 2, GND)"),
|
|
||||||
targetRef: z.string().describe("Target component reference (e.g., C1)"),
|
Note: This tool operates on schematic files (.kicad_sch).
|
||||||
targetPin: z.string().describe("Target pin name/number (e.g., 1, 2, VCC)")
|
To remove a footprint from a PCB, use delete_component instead.`,
|
||||||
},
|
{
|
||||||
async (args: { schematicPath: string; sourceRef: string; sourcePin: string; targetRef: string; targetPin: string }) => {
|
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||||
const result = await callKicadScript("add_schematic_connection", args);
|
reference: z
|
||||||
if (result.success) {
|
.string()
|
||||||
return {
|
.describe("Reference designator of the component to remove (e.g. R1, U3)"),
|
||||||
content: [{
|
},
|
||||||
type: "text",
|
async (args: { schematicPath: string; reference: string }) => {
|
||||||
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`
|
const result = await callKicadScript("delete_schematic_component", args);
|
||||||
}]
|
if (result.success) {
|
||||||
};
|
return {
|
||||||
} else {
|
content: [
|
||||||
return {
|
{
|
||||||
content: [{
|
type: "text",
|
||||||
type: "text",
|
text: `Successfully removed ${args.reference} from schematic`,
|
||||||
text: `Failed to add connection: ${result.message || 'Unknown error'}`
|
},
|
||||||
}]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
return {
|
||||||
);
|
content: [
|
||||||
|
{
|
||||||
// Add net label
|
type: "text",
|
||||||
server.tool(
|
text: `Failed to remove component: ${result.message || "Unknown error"}`,
|
||||||
"add_schematic_net_label",
|
},
|
||||||
"Add a net label to the schematic",
|
],
|
||||||
{
|
};
|
||||||
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).describe("Position [x, y] for the label")
|
|
||||||
},
|
// Connect components with wire
|
||||||
async (args: { schematicPath: string; netName: string; position: number[] }) => {
|
server.tool(
|
||||||
const result = await callKicadScript("add_schematic_net_label", args);
|
"add_wire",
|
||||||
if (result.success) {
|
"Add a wire connection in the schematic",
|
||||||
return {
|
{
|
||||||
content: [{
|
start: z
|
||||||
type: "text",
|
.object({
|
||||||
text: `Successfully added net label '${args.netName}' at position [${args.position}]`
|
x: z.number(),
|
||||||
}]
|
y: z.number(),
|
||||||
};
|
})
|
||||||
} else {
|
.describe("Start position"),
|
||||||
return {
|
end: z
|
||||||
content: [{
|
.object({
|
||||||
type: "text",
|
x: z.number(),
|
||||||
text: `Failed to add net label: ${result.message || 'Unknown error'}`
|
y: z.number(),
|
||||||
}]
|
})
|
||||||
};
|
.describe("End position"),
|
||||||
}
|
},
|
||||||
}
|
async (args: any) => {
|
||||||
);
|
const result = await callKicadScript("add_wire", args);
|
||||||
|
return {
|
||||||
// Connect pin to net
|
content: [
|
||||||
server.tool(
|
{
|
||||||
"connect_to_net",
|
type: "text",
|
||||||
"Connect a component pin to a named net",
|
text: JSON.stringify(result, null, 2),
|
||||||
{
|
},
|
||||||
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 }) => {
|
// Add pin-to-pin connection
|
||||||
const result = await callKicadScript("connect_to_net", args);
|
server.tool(
|
||||||
if (result.success) {
|
"add_schematic_connection",
|
||||||
return {
|
"Connect two component pins with a wire",
|
||||||
content: [{
|
{
|
||||||
type: "text",
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`
|
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
||||||
}]
|
sourcePin: z
|
||||||
};
|
.string()
|
||||||
} else {
|
.describe("Source pin name/number (e.g., 1, 2, GND)"),
|
||||||
return {
|
targetRef: z.string().describe("Target component reference (e.g., C1)"),
|
||||||
content: [{
|
targetPin: z
|
||||||
type: "text",
|
.string()
|
||||||
text: `Failed to connect to net: ${result.message || 'Unknown error'}`
|
.describe("Target pin name/number (e.g., 1, 2, VCC)"),
|
||||||
}]
|
},
|
||||||
};
|
async (args: {
|
||||||
}
|
schematicPath: string;
|
||||||
}
|
sourceRef: string;
|
||||||
);
|
sourcePin: string;
|
||||||
|
targetRef: string;
|
||||||
// Get net connections
|
targetPin: string;
|
||||||
server.tool(
|
}) => {
|
||||||
"get_net_connections",
|
const result = await callKicadScript("add_schematic_connection", args);
|
||||||
"Get all connections for a named net",
|
if (result.success) {
|
||||||
{
|
return {
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
content: [
|
||||||
netName: z.string().describe("Name of the net to query")
|
{
|
||||||
},
|
type: "text",
|
||||||
async (args: { schematicPath: string; netName: string }) => {
|
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`,
|
||||||
const result = await callKicadScript("get_net_connections", args);
|
},
|
||||||
if (result.success && result.connections) {
|
],
|
||||||
const connectionList = result.connections.map((conn: any) =>
|
};
|
||||||
` - ${conn.component}/${conn.pin}`
|
} else {
|
||||||
).join('\n');
|
return {
|
||||||
return {
|
content: [
|
||||||
content: [{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Net '${args.netName}' connections:\n${connectionList}`
|
text: `Failed to add connection: ${result.message || "Unknown error"}`,
|
||||||
}]
|
},
|
||||||
};
|
],
|
||||||
} else {
|
};
|
||||||
return {
|
}
|
||||||
content: [{
|
},
|
||||||
type: "text",
|
);
|
||||||
text: `Failed to get net connections: ${result.message || 'Unknown error'}`
|
|
||||||
}]
|
// Add net label
|
||||||
};
|
server.tool(
|
||||||
}
|
"add_schematic_net_label",
|
||||||
}
|
"Add a net label to the schematic",
|
||||||
);
|
{
|
||||||
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
// Generate netlist
|
netName: z
|
||||||
server.tool(
|
.string()
|
||||||
"generate_netlist",
|
.describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
|
||||||
"Generate a netlist from the schematic",
|
position: z
|
||||||
{
|
.array(z.number())
|
||||||
schematicPath: z.string().describe("Path to the schematic file")
|
.length(2)
|
||||||
},
|
.describe("Position [x, y] for the label"),
|
||||||
async (args: { schematicPath: string }) => {
|
},
|
||||||
const result = await callKicadScript("generate_netlist", args);
|
async (args: {
|
||||||
if (result.success && result.netlist) {
|
schematicPath: string;
|
||||||
const netlist = result.netlist;
|
netName: string;
|
||||||
const output = [
|
position: number[];
|
||||||
`=== Netlist for ${args.schematicPath} ===`,
|
}) => {
|
||||||
`\nComponents (${netlist.components.length}):`,
|
const result = await callKicadScript("add_schematic_net_label", args);
|
||||||
...netlist.components.map((comp: any) =>
|
if (result.success) {
|
||||||
` ${comp.reference}: ${comp.value} (${comp.footprint || 'No footprint'})`
|
return {
|
||||||
),
|
content: [
|
||||||
`\nNets (${netlist.nets.length}):`,
|
{
|
||||||
...netlist.nets.map((net: any) => {
|
type: "text",
|
||||||
const connections = net.connections.map((conn: any) =>
|
text: `Successfully added net label '${args.netName}' at position [${args.position}]`,
|
||||||
`${conn.component}/${conn.pin}`
|
},
|
||||||
).join(', ');
|
],
|
||||||
return ` ${net.name}: ${connections}`;
|
};
|
||||||
})
|
} else {
|
||||||
].join('\n');
|
return {
|
||||||
|
content: [
|
||||||
return {
|
{
|
||||||
content: [{
|
type: "text",
|
||||||
type: "text",
|
text: `Failed to add net label: ${result.message || "Unknown error"}`,
|
||||||
text: output
|
},
|
||||||
}]
|
],
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
return {
|
},
|
||||||
content: [{
|
);
|
||||||
type: "text",
|
|
||||||
text: `Failed to generate netlist: ${result.message || 'Unknown error'}`
|
// Connect pin to net
|
||||||
}]
|
server.tool(
|
||||||
};
|
"connect_to_net",
|
||||||
}
|
"Connect a component pin to a named net",
|
||||||
}
|
{
|
||||||
);
|
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: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} 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"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user