feat: add footprint + symbol creator tools
Footprint tools: - create_footprint: generates .kicad_mod via f-string (SMD/THT, courtyard, silkscreen, fab) - edit_footprint_pad: updates size/position/drill/shape in-place - list_footprint_libraries: scans .pretty directories - register_footprint_library: adds entry to fp-lib-table Symbol tools: - create_symbol: generates .kicad_sym with pins, rectangles, polylines - delete_symbol: removes symbol from library - list_symbols_in_library: lists all symbols in a .kicad_sym file - register_symbol_library: adds entry to sym-lib-table Also: - Fix footprint format version: 20250114 (schematic) -> 20241229 (footprint) - Two MCP prompts: create_footprint_guide + footprint_ipc_checklist Live tested: TMC2209 footprint (19 pads), R_0603_Test, edit_footprint_pad, TMC2209_Custom symbol with pins all confirmed in KiCAD 9
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
"""
|
||||
Footprint Creator for KiCAD MCP Server
|
||||
|
||||
Creates and edits .kicad_mod footprint files using raw text/S-Expression generation.
|
||||
Supports THT and SMD pads, courtyard, silkscreen, and fab layer graphics.
|
||||
|
||||
KiCAD 9 .kicad_mod format reference:
|
||||
https://dev-docs.kicad.org/en/file-formats/sexpr-footprint/
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
KICAD9_FORMAT_VERSION = "20250114" # .kicad_sch schematic files
|
||||
KICAD9_FOOTPRINT_VERSION = "20241229" # .kicad_mod footprint files
|
||||
|
||||
|
||||
def _fmt(v: float) -> str:
|
||||
"""Format a float without unnecessary trailing zeros."""
|
||||
return f"{v:g}"
|
||||
|
||||
|
||||
class FootprintCreator:
|
||||
"""
|
||||
Creates and edits KiCAD .kicad_mod footprint files via text generation.
|
||||
No sexpdata – pure f-string assembly to guarantee format correctness.
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Public API #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def create_footprint(
|
||||
self,
|
||||
library_path: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
tags: str = "",
|
||||
pads: Optional[List[Dict[str, Any]]] = None,
|
||||
courtyard: Optional[Dict[str, Any]] = None,
|
||||
silkscreen: Optional[Dict[str, Any]] = None,
|
||||
fab_layer: Optional[Dict[str, Any]] = None,
|
||||
ref_position: Optional[Dict[str, float]] = None,
|
||||
value_position: Optional[Dict[str, float]] = None,
|
||||
overwrite: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new .kicad_mod footprint file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
library_path : str
|
||||
Path to the .pretty directory (created if missing).
|
||||
name : str
|
||||
Footprint name, e.g. "R_0603_Custom".
|
||||
description : str
|
||||
Human-readable description.
|
||||
tags : str
|
||||
Space-separated tag string.
|
||||
pads : list of dicts
|
||||
Each pad dict supports:
|
||||
number (str) – pad number / net name, e.g. "1"
|
||||
type (str) – "smd" | "thru_hole" | "np_thru_hole"
|
||||
shape (str) – "rect" | "circle" | "oval" | "roundrect"
|
||||
at (dict) – {"x": float, "y": float, "angle": float (opt)}
|
||||
size (dict) – {"w": float, "h": float}
|
||||
drill (float or dict) – scalar for round drill, dict for oval:
|
||||
{"w": float, "h": float}
|
||||
layers (list) – override default layer list
|
||||
roundrect_ratio (float) – 0.0..0.5 for roundrect shape
|
||||
courtyard : dict or None
|
||||
{"x1": float, "y1": float, "x2": float, "y2": float, "width": float}
|
||||
silkscreen : dict or None
|
||||
{"x1": float, "y1": float, "x2": float, "y2": float, "width": float}
|
||||
fab_layer : dict or None
|
||||
{"x1": float, "y1": float, "x2": float, "y2": float, "width": float}
|
||||
ref_position : dict or None – {"x": float, "y": float}
|
||||
value_position : dict or None – {"x": float, "y": float}
|
||||
overwrite : bool
|
||||
If False (default), raise if file already exists.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with "success", "path", "pad_count"
|
||||
"""
|
||||
lib = Path(library_path)
|
||||
if not lib.suffix == ".pretty":
|
||||
lib = lib.with_suffix(".pretty")
|
||||
lib.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mod_path = lib / f"{name}.kicad_mod"
|
||||
if mod_path.exists() and not overwrite:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Footprint already exists: {mod_path}. Use overwrite=true to replace.",
|
||||
"path": str(mod_path),
|
||||
}
|
||||
|
||||
pads = pads or []
|
||||
lines: List[str] = []
|
||||
|
||||
# ---- header ----
|
||||
lines.append(f'(footprint "{name}"')
|
||||
lines.append(f' (version {KICAD9_FOOTPRINT_VERSION})')
|
||||
lines.append(f' (generator "kicad-mcp")')
|
||||
lines.append(f' (generator_version "9.0")')
|
||||
lines.append(f' (layer "F.Cu")')
|
||||
if description:
|
||||
lines.append(f' (descr "{_esc(description)}")')
|
||||
if tags:
|
||||
lines.append(f' (tags "{_esc(tags)}")')
|
||||
lines.append("")
|
||||
|
||||
# ---- reference / value text ----
|
||||
ref_x = ref_position.get("x", 0.0) if ref_position else 0.0
|
||||
ref_y = ref_position.get("y", -1.27) if ref_position else -1.27
|
||||
val_x = value_position.get("x", 0.0) if value_position else 0.0
|
||||
val_y = value_position.get("y", 1.27) if value_position else 1.27
|
||||
|
||||
lines.append(
|
||||
f' (property "Reference" "REF**" (at {_fmt(ref_x)} {_fmt(ref_y)} 0)'
|
||||
)
|
||||
lines.append(f' (layer "F.SilkS")')
|
||||
lines.append(f' (uuid "{_new_uuid()}")')
|
||||
lines.append(f' (effects (font (size 1 1) (thickness 0.15)))')
|
||||
lines.append(f' )')
|
||||
lines.append(
|
||||
f' (property "Value" "{_esc(name)}" (at {_fmt(val_x)} {_fmt(val_y)} 0)'
|
||||
)
|
||||
lines.append(f' (layer "F.Fab")')
|
||||
lines.append(f' (uuid "{_new_uuid()}")')
|
||||
lines.append(f' (effects (font (size 1 1) (thickness 0.15)))')
|
||||
lines.append(f' )')
|
||||
lines.append(f' (property "Datasheet" "" (at 0 0 0)')
|
||||
lines.append(f' (layer "F.Fab")')
|
||||
lines.append(f' (uuid "{_new_uuid()}")')
|
||||
lines.append(f' (effects (font (size 1 1) (thickness 0.15)))')
|
||||
lines.append(f' )')
|
||||
lines.append("")
|
||||
|
||||
# ---- courtyard ----
|
||||
if courtyard:
|
||||
lines.extend(_rect_lines(courtyard, "F.CrtYd", default_width=0.05))
|
||||
|
||||
# ---- silkscreen ----
|
||||
if silkscreen:
|
||||
lines.extend(_rect_lines(silkscreen, "F.SilkS", default_width=0.12))
|
||||
|
||||
# ---- fab layer ----
|
||||
if fab_layer:
|
||||
lines.extend(_rect_lines(fab_layer, "F.Fab", default_width=0.1))
|
||||
|
||||
# ---- pads ----
|
||||
for pad in pads:
|
||||
lines.extend(_pad_lines(pad))
|
||||
lines.append("")
|
||||
|
||||
lines.append(")")
|
||||
|
||||
content = "\n".join(lines) + "\n"
|
||||
mod_path.write_text(content, encoding="utf-8")
|
||||
logger.info(f"Created footprint: {mod_path} ({len(pads)} pads)")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"path": str(mod_path),
|
||||
"name": name,
|
||||
"pad_count": len(pads),
|
||||
}
|
||||
|
||||
def edit_footprint_pad(
|
||||
self,
|
||||
footprint_path: str,
|
||||
pad_number: str,
|
||||
size: Optional[Dict[str, float]] = None,
|
||||
at: Optional[Dict[str, float]] = None,
|
||||
drill: Optional[Any] = None,
|
||||
shape: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Edit an existing pad in a .kicad_mod file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
footprint_path : str
|
||||
Full path to the .kicad_mod file.
|
||||
pad_number : str
|
||||
Pad number to update (e.g. "1", "2").
|
||||
size : dict or None – {"w": float, "h": float}
|
||||
at : dict or None – {"x": float, "y": float, "angle": float (opt)}
|
||||
drill : float or dict or None
|
||||
shape : str or None – "rect" | "circle" | "oval" | "roundrect"
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with "success", "updated", "pad_number"
|
||||
"""
|
||||
path = Path(footprint_path)
|
||||
if not path.exists():
|
||||
return {"success": False, "error": f"File not found: {footprint_path}"}
|
||||
|
||||
content = path.read_text(encoding="utf-8")
|
||||
updated: List[str] = []
|
||||
|
||||
# Find the pad block for pad_number and apply modifications
|
||||
# Strategy: locate "(pad "<pad_number>"" line and patch individual fields
|
||||
# We use a simple line-by-line state machine that tracks brace depth
|
||||
# to stay inside the correct pad block.
|
||||
|
||||
def patch_pad_block(block: str) -> str:
|
||||
nonlocal updated
|
||||
changes = []
|
||||
if size:
|
||||
new_size = f'(size {_fmt(size["w"])} {_fmt(size["h"])})'
|
||||
block, n = re.subn(r'\(size\s+[\d.]+\s+[\d.]+\)', new_size, block)
|
||||
if n:
|
||||
changes.append(f"size→{new_size}")
|
||||
if at:
|
||||
angle = at.get("angle", 0)
|
||||
new_at = f'(at {_fmt(at["x"])} {_fmt(at["y"])} {_fmt(angle)})'
|
||||
block, n = re.subn(r'\(at\s+[-\d.]+\s+[-\d.]+(?:\s+[-\d.]+)?\)', new_at, block)
|
||||
if n:
|
||||
changes.append(f"at→{new_at}")
|
||||
if drill is not None:
|
||||
if isinstance(drill, (int, float)):
|
||||
new_drill = f'(drill {_fmt(drill)})'
|
||||
else:
|
||||
new_drill = f'(drill oval {_fmt(drill["w"])} {_fmt(drill["h"])})'
|
||||
block, n = re.subn(r'\(drill(?:\s+oval)?\s+[-\d.]+(?:\s+[-\d.]+)?\)', new_drill, block)
|
||||
if n:
|
||||
changes.append(f"drill→{new_drill}")
|
||||
else:
|
||||
# Insert drill before closing paren of pad block
|
||||
block = block.rstrip().rstrip(')') + f'\n {new_drill}\n )'
|
||||
changes.append(f"drill (inserted)→{new_drill}")
|
||||
if shape:
|
||||
block, n = re.subn(
|
||||
r'(pad\s+"[^"]*"\s+\w+\s+)\w+',
|
||||
lambda m: m.group(1) + shape,
|
||||
block,
|
||||
count=1
|
||||
)
|
||||
if n:
|
||||
changes.append(f"shape→{shape}")
|
||||
updated.extend(changes)
|
||||
return block
|
||||
|
||||
# Parse blocks
|
||||
result_lines = []
|
||||
in_target_pad = False
|
||||
pad_depth = 0
|
||||
pad_block_lines: List[str] = []
|
||||
|
||||
for line in content.split("\n"):
|
||||
stripped = line.strip()
|
||||
if not in_target_pad:
|
||||
# Detect start of target pad
|
||||
if re.match(rf'\(pad\s+"{re.escape(pad_number)}"\s+', stripped):
|
||||
in_target_pad = True
|
||||
pad_depth = stripped.count("(") - stripped.count(")")
|
||||
pad_block_lines = [line]
|
||||
else:
|
||||
result_lines.append(line)
|
||||
else:
|
||||
pad_block_lines.append(line)
|
||||
pad_depth += stripped.count("(") - stripped.count(")")
|
||||
if pad_depth <= 0:
|
||||
# End of pad block – patch and flush
|
||||
block = "\n".join(pad_block_lines)
|
||||
block = patch_pad_block(block)
|
||||
result_lines.extend(block.split("\n"))
|
||||
in_target_pad = False
|
||||
pad_block_lines = []
|
||||
|
||||
if not updated:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Pad \"{pad_number}\" not found or no changes made in {footprint_path}",
|
||||
}
|
||||
|
||||
path.write_text("\n".join(result_lines), encoding="utf-8")
|
||||
logger.info(f"Edited pad {pad_number} in {path.name}: {updated}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"footprint_path": str(path),
|
||||
"pad_number": pad_number,
|
||||
"updated": updated,
|
||||
}
|
||||
|
||||
def list_footprint_libraries(self, search_paths: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
"""List all .pretty libraries and their footprints."""
|
||||
default_paths = [
|
||||
r"C:\Program Files\KiCad\9.0\share\kicad\footprints",
|
||||
r"C:\Program Files\KiCad\8.0\share\kicad\footprints",
|
||||
"/usr/share/kicad/footprints",
|
||||
"/usr/local/share/kicad/footprints",
|
||||
os.path.expanduser("~/Documents/KiCad/9.0/footprints"),
|
||||
]
|
||||
paths = search_paths or default_paths
|
||||
libraries = {}
|
||||
for base in paths:
|
||||
bp = Path(base)
|
||||
if not bp.exists():
|
||||
continue
|
||||
for pretty in sorted(bp.glob("*.pretty")):
|
||||
name = pretty.stem
|
||||
mods = sorted(p.stem for p in pretty.glob("*.kicad_mod"))
|
||||
libraries[name] = {"path": str(pretty), "count": len(mods), "footprints": mods[:20]}
|
||||
return {"success": True, "library_count": len(libraries), "libraries": libraries}
|
||||
|
||||
def register_footprint_library(
|
||||
self,
|
||||
library_path: str,
|
||||
library_name: Optional[str] = None,
|
||||
description: str = "",
|
||||
scope: str = "project",
|
||||
project_path: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Register a .pretty library in KiCAD's fp-lib-table so KiCAD can find it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
library_path : str
|
||||
Full path to the .pretty directory.
|
||||
library_name : str or None
|
||||
Nickname for the library (default: directory stem).
|
||||
description : str
|
||||
Optional description string.
|
||||
scope : str
|
||||
"project" (writes fp-lib-table next to .kicad_pro) or
|
||||
"global" (writes to ~/.config/kicad/9.0/fp-lib-table).
|
||||
project_path : str or None
|
||||
Path to the .kicad_pro file or its directory (needed for scope="project").
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with "success", "table_path", "library_name", "already_registered"
|
||||
"""
|
||||
pretty = Path(library_path)
|
||||
if not pretty.suffix == ".pretty":
|
||||
pretty = pretty.with_suffix(".pretty")
|
||||
|
||||
name = library_name or pretty.stem
|
||||
uri = str(pretty).replace("\\", "/") # KiCAD prefers forward slashes
|
||||
|
||||
# Resolve fp-lib-table path
|
||||
if scope == "project":
|
||||
if project_path:
|
||||
proj = Path(project_path)
|
||||
table_dir = proj if proj.is_dir() else proj.parent
|
||||
else:
|
||||
# Default: same directory as the .pretty library
|
||||
table_dir = pretty.parent
|
||||
table_path = table_dir / "fp-lib-table"
|
||||
else: # global
|
||||
cfg_dirs = [
|
||||
Path(os.environ.get("APPDATA", "")) / "kicad" / "9.0",
|
||||
Path.home() / ".config" / "kicad" / "9.0",
|
||||
Path.home() / ".local" / "share" / "kicad" / "9.0",
|
||||
]
|
||||
table_path = None
|
||||
for d in cfg_dirs:
|
||||
candidate = d / "fp-lib-table"
|
||||
if candidate.exists():
|
||||
table_path = candidate
|
||||
break
|
||||
if table_path is None:
|
||||
# Create in first writable config dir
|
||||
for d in cfg_dirs:
|
||||
try:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
table_path = d / "fp-lib-table"
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if table_path is None:
|
||||
return {"success": False, "error": "Could not find or create global fp-lib-table"}
|
||||
|
||||
# Read existing table or start fresh
|
||||
if table_path.exists():
|
||||
content = table_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
content = "(fp_lib_table\n (version 7)\n)\n"
|
||||
|
||||
# Check if already registered (by name OR by uri)
|
||||
if f'(name "{name}")' in content or uri in content:
|
||||
return {
|
||||
"success": True,
|
||||
"already_registered": True,
|
||||
"table_path": str(table_path),
|
||||
"library_name": name,
|
||||
}
|
||||
|
||||
# Insert new lib entry before closing paren
|
||||
new_entry = (
|
||||
f' (lib (name "{name}")'
|
||||
f'(type "KiCad")'
|
||||
f'(uri "{uri}")'
|
||||
f'(options "")'
|
||||
f'(descr "{_esc(description)}"))'
|
||||
)
|
||||
# Insert before the last closing paren
|
||||
content = content.rstrip()
|
||||
if content.endswith(")"):
|
||||
content = content[:-1].rstrip() + "\n" + new_entry + "\n)\n"
|
||||
else:
|
||||
content += "\n" + new_entry + "\n)\n"
|
||||
|
||||
table_path.write_text(content, encoding="utf-8")
|
||||
logger.info(f"Registered library '{name}' in {table_path}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"already_registered": False,
|
||||
"table_path": str(table_path),
|
||||
"library_name": name,
|
||||
"uri": uri,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Internal helpers #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _esc(s: str) -> str:
|
||||
"""Escape double-quotes inside S-Expression string values."""
|
||||
return s.replace('"', '\\"')
|
||||
|
||||
|
||||
def _new_uuid() -> str:
|
||||
import uuid
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
_DEFAULT_SMD_LAYERS = ["F.Cu", "F.Paste", "F.Mask"]
|
||||
_DEFAULT_THT_LAYERS = ["*.Cu", "*.Mask"]
|
||||
|
||||
|
||||
def _pad_lines(pad: Dict[str, Any]) -> List[str]:
|
||||
number = str(pad.get("number", "1"))
|
||||
ptype = pad.get("type", "smd").lower() # smd | thru_hole | np_thru_hole
|
||||
shape = pad.get("shape", "rect").lower() # rect | circle | oval | roundrect
|
||||
at = pad.get("at", {"x": 0.0, "y": 0.0})
|
||||
size = pad.get("size", {"w": 1.0, "h": 1.0})
|
||||
drill = pad.get("drill", None)
|
||||
layers = pad.get("layers", None)
|
||||
rr_ratio = pad.get("roundrect_ratio", 0.25)
|
||||
|
||||
ax = _fmt(at.get("x", 0.0))
|
||||
ay = _fmt(at.get("y", 0.0))
|
||||
aangle = at.get("angle", None)
|
||||
at_str = f"(at {ax} {ay})" if aangle is None else f"(at {ax} {ay} {_fmt(aangle)})"
|
||||
|
||||
sw = _fmt(size.get("w", 1.0))
|
||||
sh = _fmt(size.get("h", 1.0))
|
||||
|
||||
if layers is None:
|
||||
layers = _DEFAULT_THT_LAYERS if ptype in ("thru_hole", "np_thru_hole") else _DEFAULT_SMD_LAYERS
|
||||
layers_str = " ".join(f'"{l}"' for l in layers)
|
||||
|
||||
lines = [f' (pad "{number}" {ptype} {shape}']
|
||||
lines.append(f" {at_str}")
|
||||
lines.append(f" (size {sw} {sh})")
|
||||
|
||||
if drill is not None:
|
||||
if isinstance(drill, (int, float)):
|
||||
lines.append(f" (drill {_fmt(drill)})")
|
||||
elif isinstance(drill, dict):
|
||||
dw = _fmt(drill.get("w", 1.0))
|
||||
dh = _fmt(drill.get("h", 1.0))
|
||||
lines.append(f" (drill oval {dw} {dh})")
|
||||
|
||||
lines.append(f" (layers {layers_str})")
|
||||
|
||||
if shape == "roundrect":
|
||||
lines.append(f" (roundrect_rratio {_fmt(rr_ratio)})")
|
||||
|
||||
lines.append(f' (uuid "{_new_uuid()}")')
|
||||
lines.append(f" )")
|
||||
return lines
|
||||
|
||||
|
||||
def _rect_lines(rect: Dict[str, Any], layer: str, default_width: float = 0.05) -> List[str]:
|
||||
x1 = _fmt(rect.get("x1", -1.0))
|
||||
y1 = _fmt(rect.get("y1", -1.0))
|
||||
x2 = _fmt(rect.get("x2", 1.0))
|
||||
y2 = _fmt(rect.get("y2", 1.0))
|
||||
w = _fmt(rect.get("width", default_width))
|
||||
return [
|
||||
f' (fp_rect',
|
||||
f' (start {x1} {y1})',
|
||||
f' (end {x2} {y2})',
|
||||
f' (stroke (width {w}) (type default))',
|
||||
f' (fill none)',
|
||||
f' (layer "{layer}")',
|
||||
f' (uuid "{_new_uuid()}")',
|
||||
f' )',
|
||||
"",
|
||||
]
|
||||
@@ -0,0 +1,465 @@
|
||||
"""
|
||||
Symbol Creator for KiCAD MCP Server
|
||||
|
||||
Creates and edits .kicad_sym symbol library files using raw S-Expression text generation.
|
||||
No sexpdata – pure f-string assembly to guarantee format correctness.
|
||||
|
||||
KiCAD 9 .kicad_sym format:
|
||||
- Library file starts with (kicad_symbol_lib (version 20241209) ...)
|
||||
- Each symbol has a parent block with properties + two sub-symbols:
|
||||
SymbolName_0_1 → body graphics (rectangle, polyline, circle, arc)
|
||||
SymbolName_1_1 → pins
|
||||
- All coordinates in mm, 2.54mm grid typical for schematic symbols
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
KICAD9_SYMBOL_LIB_VERSION = "20241209"
|
||||
|
||||
# Pin electrical types
|
||||
PIN_TYPES = {
|
||||
"input", "output", "bidirectional", "tri_state", "passive",
|
||||
"free", "unspecified", "power_in", "power_out",
|
||||
"open_collector", "open_emitter", "no_connect",
|
||||
}
|
||||
|
||||
# Pin graphic shapes
|
||||
PIN_SHAPES = {
|
||||
"line", "inverted", "clock", "inverted_clock", "input_low",
|
||||
"clock_low", "output_low", "falling_edge_clock", "non_logic",
|
||||
}
|
||||
|
||||
|
||||
def _fmt(v: float) -> str:
|
||||
return f"{v:g}"
|
||||
|
||||
|
||||
def _esc(s: str) -> str:
|
||||
return s.replace('"', '\\"')
|
||||
|
||||
|
||||
class SymbolCreator:
|
||||
"""Creates and edits KiCAD .kicad_sym symbol library files."""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# create_symbol #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def create_symbol(
|
||||
self,
|
||||
library_path: str,
|
||||
name: str,
|
||||
reference_prefix: str = "U",
|
||||
description: str = "",
|
||||
keywords: str = "",
|
||||
datasheet: str = "~",
|
||||
footprint: str = "",
|
||||
in_bom: bool = True,
|
||||
on_board: bool = True,
|
||||
pins: Optional[List[Dict[str, Any]]] = None,
|
||||
rectangles: Optional[List[Dict[str, Any]]] = None,
|
||||
polylines: Optional[List[Dict[str, Any]]] = None,
|
||||
overwrite: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Add a new symbol to a .kicad_sym library (creates the file if missing).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
library_path : str
|
||||
Path to the .kicad_sym file (created if missing).
|
||||
name : str
|
||||
Symbol name, e.g. "TMC2209", "MyOpAmp".
|
||||
reference_prefix : str
|
||||
Schematic reference prefix, e.g. "U", "R", "J".
|
||||
description : str
|
||||
Human-readable description.
|
||||
keywords : str
|
||||
Space-separated keyword string for search.
|
||||
datasheet : str
|
||||
Datasheet URL or "~".
|
||||
footprint : str
|
||||
Default footprint, e.g. "Package_SO:SOIC-8".
|
||||
in_bom : bool
|
||||
Include in BOM (default True).
|
||||
on_board : bool
|
||||
Include in netlist for PCB (default True).
|
||||
pins : list of dicts
|
||||
Each pin dict:
|
||||
name (str) – pin name, e.g. "VCC", "GND", "~" for unnamed
|
||||
number (str) – pin number, e.g. "1", "A1"
|
||||
type (str) – electrical type: input|output|bidirectional|
|
||||
passive|power_in|power_out|tri_state|
|
||||
open_collector|open_emitter|free|unspecified
|
||||
at (dict) – {"x": float, "y": float, "angle": float}
|
||||
angle: 0=right, 90=up, 180=left, 270=down
|
||||
length (float) – pin length in mm (default 2.54)
|
||||
shape (str) – graphic shape: line|inverted|clock|...
|
||||
(default "line")
|
||||
rectangles : list of dicts or None
|
||||
Body rectangles: {"x1","y1","x2","y2", "width"(opt), "fill"(opt)}
|
||||
fill: "none"|"outline"|"background" (default "background")
|
||||
polylines : list of dicts or None
|
||||
{"points": [{"x":float,"y":float},...], "width"(opt), "fill"(opt)}
|
||||
overwrite : bool
|
||||
Replace existing symbol with same name (default False).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with "success", "library_path", "symbol_name", "pin_count"
|
||||
"""
|
||||
lib_path = Path(library_path)
|
||||
if lib_path.suffix.lower() != ".kicad_sym":
|
||||
lib_path = lib_path.with_suffix(".kicad_sym")
|
||||
|
||||
lib_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load or create library
|
||||
if lib_path.exists():
|
||||
lib_content = lib_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
lib_content = (
|
||||
f'(kicad_symbol_lib\n'
|
||||
f' (version {KICAD9_SYMBOL_LIB_VERSION})\n'
|
||||
f' (generator "kicad-mcp")\n'
|
||||
f' (generator_version "9.0")\n'
|
||||
f')\n'
|
||||
)
|
||||
|
||||
# Check for duplicate
|
||||
if f'(symbol "{name}"' in lib_content:
|
||||
if not overwrite:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f'Symbol "{name}" already exists in {lib_path}. Use overwrite=true.',
|
||||
"library_path": str(lib_path),
|
||||
}
|
||||
lib_content = self._remove_symbol(lib_content, name)
|
||||
|
||||
pins = pins or []
|
||||
rectangles = rectangles or []
|
||||
polylines = polylines or []
|
||||
|
||||
symbol_block = self._build_symbol_block(
|
||||
name=name,
|
||||
reference_prefix=reference_prefix,
|
||||
description=description,
|
||||
keywords=keywords,
|
||||
datasheet=datasheet,
|
||||
footprint=footprint,
|
||||
in_bom=in_bom,
|
||||
on_board=on_board,
|
||||
pins=pins,
|
||||
rectangles=rectangles,
|
||||
polylines=polylines,
|
||||
)
|
||||
|
||||
# Insert before closing paren of library
|
||||
lib_content = lib_content.rstrip()
|
||||
if lib_content.endswith(")"):
|
||||
lib_content = lib_content[:-1].rstrip() + "\n" + symbol_block + "\n)\n"
|
||||
else:
|
||||
lib_content += "\n" + symbol_block + "\n)\n"
|
||||
|
||||
lib_path.write_text(lib_content, encoding="utf-8")
|
||||
logger.info(f"Created symbol '{name}' in {lib_path} ({len(pins)} pins)")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"library_path": str(lib_path),
|
||||
"symbol_name": name,
|
||||
"pin_count": len(pins),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# delete_symbol #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def delete_symbol(self, library_path: str, name: str) -> Dict[str, Any]:
|
||||
"""Remove a symbol from a .kicad_sym library."""
|
||||
lib_path = Path(library_path)
|
||||
if not lib_path.exists():
|
||||
return {"success": False, "error": f"Library not found: {library_path}"}
|
||||
|
||||
content = lib_path.read_text(encoding="utf-8")
|
||||
if f'(symbol "{name}"' not in content:
|
||||
return {"success": False, "error": f'Symbol "{name}" not found in {library_path}'}
|
||||
|
||||
new_content = self._remove_symbol(content, name)
|
||||
lib_path.write_text(new_content, encoding="utf-8")
|
||||
return {"success": True, "library_path": str(lib_path), "deleted": name}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# list_symbols (in a single library file) #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def list_symbols(self, library_path: str) -> Dict[str, Any]:
|
||||
"""List all symbols in a .kicad_sym file."""
|
||||
lib_path = Path(library_path)
|
||||
if not lib_path.exists():
|
||||
return {"success": False, "error": f"Library not found: {library_path}"}
|
||||
|
||||
content = lib_path.read_text(encoding="utf-8")
|
||||
# Only top-level symbols (not sub-symbols like _0_1 or _1_1)
|
||||
names = re.findall(r'^\s*\(symbol "([^"_][^"]*)"', content, re.MULTILINE)
|
||||
# Filter out sub-symbols (contain _N_N suffix)
|
||||
symbols = [n for n in names if not re.search(r'_\d+_\d+$', n)]
|
||||
return {
|
||||
"success": True,
|
||||
"library_path": str(lib_path),
|
||||
"symbol_count": len(symbols),
|
||||
"symbols": symbols,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# register_symbol_library #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def register_symbol_library(
|
||||
self,
|
||||
library_path: str,
|
||||
library_name: Optional[str] = None,
|
||||
description: str = "",
|
||||
scope: str = "project",
|
||||
project_path: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Register a .kicad_sym library in KiCAD's sym-lib-table.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
library_path : str – path to the .kicad_sym file
|
||||
library_name : str – nickname (default: file stem)
|
||||
scope : str – "project" or "global"
|
||||
project_path : str – .kicad_pro or directory (for scope=project)
|
||||
"""
|
||||
sym_path = Path(library_path)
|
||||
name = library_name or sym_path.stem
|
||||
uri = str(sym_path).replace("\\", "/")
|
||||
|
||||
if scope == "project":
|
||||
if project_path:
|
||||
proj = Path(project_path)
|
||||
table_dir = proj if proj.is_dir() else proj.parent
|
||||
else:
|
||||
table_dir = sym_path.parent
|
||||
table_path = table_dir / "sym-lib-table"
|
||||
else:
|
||||
cfg_dirs = [
|
||||
Path(os.environ.get("APPDATA", "")) / "kicad" / "9.0",
|
||||
Path.home() / ".config" / "kicad" / "9.0",
|
||||
]
|
||||
table_path = None
|
||||
for d in cfg_dirs:
|
||||
candidate = d / "sym-lib-table"
|
||||
if candidate.exists():
|
||||
table_path = candidate
|
||||
break
|
||||
if table_path is None:
|
||||
for d in cfg_dirs:
|
||||
try:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
table_path = d / "sym-lib-table"
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if table_path is None:
|
||||
return {"success": False, "error": "Could not find/create global sym-lib-table"}
|
||||
|
||||
if table_path.exists():
|
||||
content = table_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
content = "(sym_lib_table\n (version 7)\n)\n"
|
||||
|
||||
if f'(name "{name}")' in content or uri in content:
|
||||
return {
|
||||
"success": True,
|
||||
"already_registered": True,
|
||||
"table_path": str(table_path),
|
||||
"library_name": name,
|
||||
}
|
||||
|
||||
new_entry = (
|
||||
f' (lib (name "{name}")'
|
||||
f'(type "KiCad")'
|
||||
f'(uri "{uri}")'
|
||||
f'(options "")'
|
||||
f'(descr "{_esc(description)}"))'
|
||||
)
|
||||
content = content.rstrip()
|
||||
if content.endswith(")"):
|
||||
content = content[:-1].rstrip() + "\n" + new_entry + "\n)\n"
|
||||
else:
|
||||
content += "\n" + new_entry + "\n)\n"
|
||||
|
||||
table_path.write_text(content, encoding="utf-8")
|
||||
logger.info(f"Registered symbol library '{name}' in {table_path}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"already_registered": False,
|
||||
"table_path": str(table_path),
|
||||
"library_name": name,
|
||||
"uri": uri,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Internal helpers #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _build_symbol_block(
|
||||
self,
|
||||
name: str,
|
||||
reference_prefix: str,
|
||||
description: str,
|
||||
keywords: str,
|
||||
datasheet: str,
|
||||
footprint: str,
|
||||
in_bom: bool,
|
||||
on_board: bool,
|
||||
pins: List[Dict[str, Any]],
|
||||
rectangles: List[Dict[str, Any]],
|
||||
polylines: List[Dict[str, Any]],
|
||||
) -> str:
|
||||
lines: List[str] = []
|
||||
bom_str = "yes" if in_bom else "no"
|
||||
board_str = "yes" if on_board else "no"
|
||||
|
||||
lines.append(f' (symbol "{name}"')
|
||||
lines.append(f' (exclude_from_sim no)')
|
||||
lines.append(f' (in_bom {bom_str})')
|
||||
lines.append(f' (on_board {board_str})')
|
||||
|
||||
# Properties
|
||||
lines.extend(_property_block("Reference", reference_prefix, 2.54, 0, visible=True))
|
||||
lines.extend(_property_block("Value", name, 0, -2.54, visible=True))
|
||||
lines.extend(_property_block("Footprint", footprint, 0, -5.08, visible=False))
|
||||
lines.extend(_property_block("Datasheet", datasheet or "~", 0, -7.62, visible=False))
|
||||
lines.extend(_property_block("Description", description, 0, -10.16, visible=False))
|
||||
if keywords:
|
||||
lines.extend(_property_block("ki_keywords", keywords, 0, 0, visible=False))
|
||||
|
||||
# Sub-symbol _0_1: body graphics
|
||||
lines.append(f' (symbol "{name}_0_1"')
|
||||
for rect in rectangles:
|
||||
lines.extend(_rect_sym_lines(rect))
|
||||
for pl in polylines:
|
||||
lines.extend(_polyline_lines(pl))
|
||||
lines.append(f' )')
|
||||
|
||||
# Sub-symbol _1_1: pins
|
||||
lines.append(f' (symbol "{name}_1_1"')
|
||||
for pin in pins:
|
||||
lines.extend(_pin_lines(pin))
|
||||
lines.append(f' )')
|
||||
|
||||
lines.append(f' )')
|
||||
return "\n".join(lines)
|
||||
|
||||
def _remove_symbol(self, content: str, name: str) -> str:
|
||||
"""Remove a complete symbol block from library content."""
|
||||
lines = content.split("\n")
|
||||
result = []
|
||||
skip = False
|
||||
depth = 0
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not skip:
|
||||
if re.match(rf'^\s*\(symbol "{re.escape(name)}"', line) and \
|
||||
not re.search(r'_\d+_\d+"', line):
|
||||
skip = True
|
||||
depth = stripped.count("(") - stripped.count(")")
|
||||
continue
|
||||
result.append(line)
|
||||
else:
|
||||
depth += stripped.count("(") - stripped.count(")")
|
||||
if depth <= 0:
|
||||
skip = False
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# S-Expression helper functions #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _property_block(
|
||||
key: str, value: str, x: float, y: float, visible: bool = True
|
||||
) -> List[str]:
|
||||
hide = "" if visible else "\n (hide yes)"
|
||||
return [
|
||||
f' (property "{_esc(key)}" "{_esc(value)}"',
|
||||
f' (at {_fmt(x)} {_fmt(y)} 0)',
|
||||
f' (effects',
|
||||
f' (font (size 1.27 1.27))',
|
||||
f' ){hide}',
|
||||
f' )',
|
||||
]
|
||||
|
||||
|
||||
def _rect_sym_lines(rect: Dict[str, Any]) -> List[str]:
|
||||
x1 = _fmt(rect.get("x1", -2.54))
|
||||
y1 = _fmt(rect.get("y1", -2.54))
|
||||
x2 = _fmt(rect.get("x2", 2.54))
|
||||
y2 = _fmt(rect.get("y2", 2.54))
|
||||
w = _fmt(rect.get("width", 0.254))
|
||||
fill = rect.get("fill", "background")
|
||||
return [
|
||||
f' (rectangle',
|
||||
f' (start {x1} {y1})',
|
||||
f' (end {x2} {y2})',
|
||||
f' (stroke (width {w}) (type default))',
|
||||
f' (fill (type {fill}))',
|
||||
f' )',
|
||||
]
|
||||
|
||||
|
||||
def _polyline_lines(pl: Dict[str, Any]) -> List[str]:
|
||||
pts = pl.get("points", [])
|
||||
w = _fmt(pl.get("width", 0.254))
|
||||
fill = pl.get("fill", "none")
|
||||
lines = [
|
||||
f' (polyline',
|
||||
f' (pts',
|
||||
]
|
||||
for pt in pts:
|
||||
lines.append(f' (xy {_fmt(pt["x"])} {_fmt(pt["y"])})')
|
||||
lines += [
|
||||
f' )',
|
||||
f' (stroke (width {w}) (type default))',
|
||||
f' (fill (type {fill}))',
|
||||
f' )',
|
||||
]
|
||||
return lines
|
||||
|
||||
|
||||
def _pin_lines(pin: Dict[str, Any]) -> List[str]:
|
||||
ptype = pin.get("type", "passive").lower()
|
||||
shape = pin.get("shape", "line").lower()
|
||||
at = pin.get("at", {"x": 0, "y": 0, "angle": 0})
|
||||
x = _fmt(at.get("x", 0))
|
||||
y = _fmt(at.get("y", 0))
|
||||
angle = _fmt(at.get("angle", 0))
|
||||
length = _fmt(pin.get("length", 2.54))
|
||||
pin_name = pin.get("name", "~")
|
||||
pin_number = str(pin.get("number", "1"))
|
||||
|
||||
return [
|
||||
f' (pin {ptype} {shape}',
|
||||
f' (at {x} {y} {angle})',
|
||||
f' (length {length})',
|
||||
f' (name "{_esc(pin_name)}"',
|
||||
f' (effects (font (size 1.27 1.27)))',
|
||||
f' )',
|
||||
f' (number "{_esc(pin_number)}"',
|
||||
f' (effects (font (size 1.27 1.27)))',
|
||||
f' )',
|
||||
f' )',
|
||||
]
|
||||
@@ -224,6 +224,8 @@ try:
|
||||
from commands.jlcpcb import JLCPCBClient, test_jlcpcb_connection
|
||||
from commands.jlcpcb_parts import JLCPCBPartsManager
|
||||
from commands.datasheet_manager import DatasheetManager
|
||||
from commands.footprint import FootprintCreator
|
||||
from commands.symbol_creator import SymbolCreator
|
||||
|
||||
logger.info("Successfully imported all command handlers")
|
||||
except ImportError as e:
|
||||
@@ -387,6 +389,16 @@ class KiCADInterface:
|
||||
"ipc_get_tracks": self._handle_ipc_get_tracks,
|
||||
"ipc_get_vias": self._handle_ipc_get_vias,
|
||||
"ipc_save_board": self._handle_ipc_save_board,
|
||||
# Footprint commands
|
||||
"create_footprint": self._handle_create_footprint,
|
||||
"edit_footprint_pad": self._handle_edit_footprint_pad,
|
||||
"list_footprint_libraries": self._handle_list_footprint_libraries,
|
||||
"register_footprint_library": self._handle_register_footprint_library,
|
||||
# Symbol creator commands
|
||||
"create_symbol": self._handle_create_symbol,
|
||||
"delete_symbol": self._handle_delete_symbol,
|
||||
"list_symbols_in_library": self._handle_list_symbols_in_library,
|
||||
"register_symbol_library": self._handle_register_symbol_library,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -854,6 +866,146 @@ class KiCADInterface:
|
||||
logger.error(f"Error listing schematic libraries: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Footprint handlers #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _handle_create_footprint(self, params):
|
||||
"""Create a new .kicad_mod footprint file in a .pretty library."""
|
||||
logger.info(f"create_footprint: {params.get('name')} in {params.get('libraryPath')}")
|
||||
try:
|
||||
creator = FootprintCreator()
|
||||
return creator.create_footprint(
|
||||
library_path=params.get("libraryPath", ""),
|
||||
name=params.get("name", ""),
|
||||
description=params.get("description", ""),
|
||||
tags=params.get("tags", ""),
|
||||
pads=params.get("pads", []),
|
||||
courtyard=params.get("courtyard"),
|
||||
silkscreen=params.get("silkscreen"),
|
||||
fab_layer=params.get("fabLayer"),
|
||||
ref_position=params.get("refPosition"),
|
||||
value_position=params.get("valuePosition"),
|
||||
overwrite=params.get("overwrite", False),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"create_footprint error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_edit_footprint_pad(self, params):
|
||||
"""Edit an existing pad in a .kicad_mod file."""
|
||||
logger.info(f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}")
|
||||
try:
|
||||
creator = FootprintCreator()
|
||||
return creator.edit_footprint_pad(
|
||||
footprint_path=params.get("footprintPath", ""),
|
||||
pad_number=str(params.get("padNumber", "1")),
|
||||
size=params.get("size"),
|
||||
at=params.get("at"),
|
||||
drill=params.get("drill"),
|
||||
shape=params.get("shape"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"edit_footprint_pad error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_list_footprint_libraries(self, params):
|
||||
"""List .pretty footprint libraries and their contents."""
|
||||
logger.info("list_footprint_libraries")
|
||||
try:
|
||||
creator = FootprintCreator()
|
||||
return creator.list_footprint_libraries(
|
||||
search_paths=params.get("searchPaths")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"list_footprint_libraries error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_register_footprint_library(self, params):
|
||||
"""Register a .pretty library in KiCAD's fp-lib-table."""
|
||||
logger.info(f"register_footprint_library: {params.get('libraryPath')}")
|
||||
try:
|
||||
creator = FootprintCreator()
|
||||
return creator.register_footprint_library(
|
||||
library_path=params.get("libraryPath", ""),
|
||||
library_name=params.get("libraryName"),
|
||||
description=params.get("description", ""),
|
||||
scope=params.get("scope", "project"),
|
||||
project_path=params.get("projectPath"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"register_footprint_library error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Symbol creator handlers #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _handle_create_symbol(self, params):
|
||||
"""Create a new symbol in a .kicad_sym library."""
|
||||
logger.info(f"create_symbol: {params.get('name')} in {params.get('libraryPath')}")
|
||||
try:
|
||||
creator = SymbolCreator()
|
||||
return creator.create_symbol(
|
||||
library_path=params.get("libraryPath", ""),
|
||||
name=params.get("name", ""),
|
||||
reference_prefix=params.get("referencePrefix", "U"),
|
||||
description=params.get("description", ""),
|
||||
keywords=params.get("keywords", ""),
|
||||
datasheet=params.get("datasheet", "~"),
|
||||
footprint=params.get("footprint", ""),
|
||||
in_bom=params.get("inBom", True),
|
||||
on_board=params.get("onBoard", True),
|
||||
pins=params.get("pins", []),
|
||||
rectangles=params.get("rectangles", []),
|
||||
polylines=params.get("polylines", []),
|
||||
overwrite=params.get("overwrite", False),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"create_symbol error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_delete_symbol(self, params):
|
||||
"""Delete a symbol from a .kicad_sym library."""
|
||||
logger.info(f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}")
|
||||
try:
|
||||
creator = SymbolCreator()
|
||||
return creator.delete_symbol(
|
||||
library_path=params.get("libraryPath", ""),
|
||||
name=params.get("name", ""),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"delete_symbol error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_list_symbols_in_library(self, params):
|
||||
"""List all symbols in a .kicad_sym file."""
|
||||
logger.info(f"list_symbols_in_library: {params.get('libraryPath')}")
|
||||
try:
|
||||
creator = SymbolCreator()
|
||||
return creator.list_symbols(
|
||||
library_path=params.get("libraryPath", ""),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"list_symbols_in_library error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_register_symbol_library(self, params):
|
||||
"""Register a .kicad_sym library in KiCAD's sym-lib-table."""
|
||||
logger.info(f"register_symbol_library: {params.get('libraryPath')}")
|
||||
try:
|
||||
creator = SymbolCreator()
|
||||
return creator.register_symbol_library(
|
||||
library_path=params.get("libraryPath", ""),
|
||||
library_name=params.get("libraryName"),
|
||||
description=params.get("description", ""),
|
||||
scope=params.get("scope", "project"),
|
||||
project_path=params.get("projectPath"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"register_symbol_library error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_export_schematic_pdf(self, params):
|
||||
"""Export schematic to PDF"""
|
||||
logger.info("Exporting schematic to PDF")
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Footprint prompts for KiCAD MCP server
|
||||
*
|
||||
* Guides Claude in creating and editing KiCAD footprints (.kicad_mod)
|
||||
* using the create_footprint, edit_footprint_pad, and list_footprint_libraries tools.
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
export function registerFootprintPrompts(server: McpServer): void {
|
||||
logger.info("Registering footprint prompts");
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Create Footprint Prompt
|
||||
// ------------------------------------------------------
|
||||
server.prompt(
|
||||
"create_footprint_guide",
|
||||
{
|
||||
component: z
|
||||
.string()
|
||||
.describe(
|
||||
"Component description, e.g. 'SOT-23 NPN transistor' or '2-pin JST XH 2.5mm connector'",
|
||||
),
|
||||
libraryPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Target .pretty library path (optional)"),
|
||||
},
|
||||
() => ({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `You are a KiCAD footprint expert. Create a correct KiCAD 9 footprint using the create_footprint tool.
|
||||
|
||||
## Component to footprint
|
||||
{{component}}
|
||||
|
||||
## Library path
|
||||
{{libraryPath}}
|
||||
|
||||
## Rules for correct footprints
|
||||
|
||||
### Coordinate system
|
||||
- Origin (0,0) is the footprint anchor, typically the centre of the pad pattern.
|
||||
- X increases to the right, Y increases downward (same as KiCAD screen).
|
||||
- All values in millimetres.
|
||||
|
||||
### SMD pads
|
||||
- type: "smd"
|
||||
- Default layers: ["F.Cu", "F.Paste", "F.Mask"]
|
||||
- No drill needed.
|
||||
- Common shapes: "rect" for square/rectangular, "roundrect" for ICs.
|
||||
|
||||
### THT pads
|
||||
- type: "thru_hole"
|
||||
- Default layers: ["*.Cu", "*.Mask"]
|
||||
- drill required (round = scalar, oval = {w, h}).
|
||||
- Pad 1 is typically square (rect), remaining pads are circle.
|
||||
|
||||
### Courtyard (F.CrtYd)
|
||||
- Add 0.25 mm clearance around the outermost extent of pads.
|
||||
- Line width: 0.05 mm.
|
||||
|
||||
### Silkscreen (F.SilkS)
|
||||
- Shows the component body outline, typically slightly inside the courtyard.
|
||||
- Line width: 0.12 mm.
|
||||
- Must not overlap pads.
|
||||
|
||||
### Fab layer (F.Fab)
|
||||
- Shows the realistic component outline with pin-1 marker.
|
||||
- Line width: 0.10 mm.
|
||||
|
||||
### Reference text
|
||||
- Place "REF**" above the courtyard (negative Y = above).
|
||||
- Value text below the courtyard (positive Y = below).
|
||||
|
||||
## Workflow
|
||||
1. Calculate pad positions from datasheet pitch and land pattern.
|
||||
2. Call create_footprint with pads[], courtyard, silkscreen, fabLayer.
|
||||
3. Verify with edit_footprint_pad if any correction is needed.
|
||||
|
||||
## Common packages quick reference
|
||||
| Package | Pitch | Pad size (SMD) | Notes |
|
||||
|-----------|--------|------------------|------------------------------|
|
||||
| 0402 | 1.0 mm | 0.6 × 0.7 mm | Very small, min 0.5 mm drill |
|
||||
| 0603 | 1.6 mm | 1.0 × 1.0 mm | Standard small passive |
|
||||
| 0805 | 2.0 mm | 1.4 × 1.2 mm | Easy to hand-solder |
|
||||
| SOT-23 | 0.95 mm| 1.0 × 1.3 mm | 3-pin, 2 on one side |
|
||||
| SOT-23-5 | 0.95 mm| 0.6 × 1.0 mm | 5-pin |
|
||||
| SOIC-8 | 1.27 mm| 1.6 × 0.6 mm | 4 pins each side |
|
||||
| DIP-8 | 2.54 mm| dia 1.6, drill 0.8| THT, 100 mil grid |
|
||||
|
||||
Now create the footprint for: {{component}}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Footprint IPC Checklist Prompt
|
||||
// ------------------------------------------------------
|
||||
server.prompt(
|
||||
"footprint_ipc_checklist",
|
||||
{
|
||||
footprintPath: z
|
||||
.string()
|
||||
.describe("Path to the .kicad_mod file to review"),
|
||||
},
|
||||
() => ({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `Review the footprint at {{footprintPath}} against IPC-7351 land pattern guidelines.
|
||||
|
||||
Check:
|
||||
1. **Pad size** – is the copper area sufficient for soldering (not undersized)?
|
||||
2. **Courtyard** – at least 0.25 mm clearance around all pads?
|
||||
3. **Silkscreen** – does it overlap pads? (it should NOT)
|
||||
4. **Pad 1 marker** – is pin 1 identifiable (square pad or triangle on silkscreen)?
|
||||
5. **Drill size** – for THT: drill ≥ lead diameter + 0.3 mm?
|
||||
6. **Layer assignments** – SMD pads: F.Cu/F.Paste/F.Mask; THT: *.Cu/*.Mask?
|
||||
7. **Anchor** – is the origin centred on the pad pattern?
|
||||
|
||||
Use edit_footprint_pad to fix any issues found.`,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
+10
-9
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Prompts index for KiCAD MCP server
|
||||
*
|
||||
* Exports all prompt registration functions
|
||||
*/
|
||||
|
||||
export { registerComponentPrompts } from './component.js';
|
||||
export { registerRoutingPrompts } from './routing.js';
|
||||
export { registerDesignPrompts } from './design.js';
|
||||
/**
|
||||
* Prompts index for KiCAD MCP server
|
||||
*
|
||||
* Exports all prompt registration functions
|
||||
*/
|
||||
|
||||
export { registerComponentPrompts } from "./component.js";
|
||||
export { registerRoutingPrompts } from "./routing.js";
|
||||
export { registerDesignPrompts } from "./design.js";
|
||||
export { registerFootprintPrompts } from "./footprint.js";
|
||||
|
||||
@@ -22,6 +22,8 @@ import { registerLibraryTools } from "./tools/library.js";
|
||||
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
||||
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
||||
import { registerDatasheetTools } from "./tools/datasheet.js";
|
||||
import { registerFootprintTools } from "./tools/footprint.js";
|
||||
import { registerSymbolCreatorTools } from "./tools/symbol-creator.js";
|
||||
import { registerUITools } from "./tools/ui.js";
|
||||
import { registerRouterTools } from "./tools/router.js";
|
||||
|
||||
@@ -35,6 +37,7 @@ import { registerLibraryResources } from "./resources/library.js";
|
||||
import { registerComponentPrompts } from "./prompts/component.js";
|
||||
import { registerRoutingPrompts } from "./prompts/routing.js";
|
||||
import { registerDesignPrompts } from "./prompts/design.js";
|
||||
import { registerFootprintPrompts } from "./prompts/footprint.js";
|
||||
|
||||
/**
|
||||
* Find the Python executable to use
|
||||
@@ -243,6 +246,8 @@ export class KiCADMcpServer {
|
||||
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
||||
registerDatasheetTools(this.server, this.callKicadScript.bind(this));
|
||||
registerFootprintTools(this.server, this.callKicadScript.bind(this));
|
||||
registerSymbolCreatorTools(this.server, this.callKicadScript.bind(this));
|
||||
registerUITools(this.server, this.callKicadScript.bind(this));
|
||||
|
||||
// Register all resources
|
||||
@@ -255,6 +260,7 @@ export class KiCADMcpServer {
|
||||
registerComponentPrompts(this.server);
|
||||
registerRoutingPrompts(this.server);
|
||||
registerDesignPrompts(this.server);
|
||||
registerFootprintPrompts(this.server);
|
||||
|
||||
logger.info("All KiCAD tools, resources, and prompts registered");
|
||||
logger.info(
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Footprint tools for KiCAD MCP server
|
||||
*
|
||||
* create_footprint – generate a complete .kicad_mod file in a .pretty library
|
||||
* edit_footprint_pad – update size / position / drill / shape of one pad
|
||||
* list_footprint_libraries – list available .pretty libraries
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// ---- shared sub-schemas ------------------------------------------------- //
|
||||
|
||||
const PadPosition = z.object({
|
||||
x: z.number().describe("X position in mm"),
|
||||
y: z.number().describe("Y position in mm"),
|
||||
angle: z.number().optional().describe("Rotation angle in degrees (default 0)"),
|
||||
});
|
||||
|
||||
const PadSize = z.object({
|
||||
w: z.number().describe("Width in mm"),
|
||||
h: z.number().describe("Height in mm"),
|
||||
});
|
||||
|
||||
const PadSchema = z.object({
|
||||
number: z.string().describe("Pad number / name, e.g. '1', '2', 'A1'"),
|
||||
type: z
|
||||
.enum(["smd", "thru_hole", "np_thru_hole"])
|
||||
.describe("Pad type: smd | thru_hole | np_thru_hole"),
|
||||
shape: z
|
||||
.enum(["rect", "circle", "oval", "roundrect"])
|
||||
.optional()
|
||||
.describe("Pad shape (default: rect for SMD, circle for THT)"),
|
||||
at: PadPosition.describe("Pad centre position"),
|
||||
size: PadSize.describe("Pad size in mm"),
|
||||
drill: z
|
||||
.union([
|
||||
z.number().describe("Round drill diameter in mm"),
|
||||
z.object({ w: z.number(), h: z.number() }).describe("Oval drill w×h in mm"),
|
||||
])
|
||||
.optional()
|
||||
.describe("Drill size (required for thru_hole pads)"),
|
||||
layers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Override default layer list, e.g. ['F.Cu','F.Paste','F.Mask']"),
|
||||
roundrect_ratio: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(0.5)
|
||||
.optional()
|
||||
.describe("Corner radius ratio for roundrect shape (0.0–0.5, default 0.25)"),
|
||||
});
|
||||
|
||||
const RectSchema = z.object({
|
||||
x1: z.number().describe("Left X in mm"),
|
||||
y1: z.number().describe("Top Y in mm"),
|
||||
x2: z.number().describe("Right X in mm"),
|
||||
y2: z.number().describe("Bottom Y in mm"),
|
||||
width: z.number().optional().describe("Line width in mm"),
|
||||
});
|
||||
|
||||
// ---- tool registration --------------------------------------------------- //
|
||||
|
||||
export function registerFootprintTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
// ── create_footprint ──────────────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"create_footprint",
|
||||
"Create a new KiCAD footprint (.kicad_mod) inside a .pretty library directory. " +
|
||||
"Supports SMD and THT pads, courtyard, silkscreen, and fab-layer rectangles.",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe(
|
||||
"Path to the .pretty library directory (created if missing). " +
|
||||
"E.g. C:/MyProject/MyLib.pretty",
|
||||
),
|
||||
name: z.string().describe("Footprint name, e.g. 'R_0603_Custom'"),
|
||||
description: z.string().optional().describe("Human-readable description"),
|
||||
tags: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Space-separated tag string, e.g. 'resistor SMD 0603'"),
|
||||
pads: z
|
||||
.array(PadSchema)
|
||||
.optional()
|
||||
.describe("List of pads to add (can be empty for outlines-only footprints)"),
|
||||
courtyard: RectSchema.optional().describe(
|
||||
"Courtyard rectangle on F.CrtYd (recommended: 0.25 mm clearance around pads)",
|
||||
),
|
||||
silkscreen: RectSchema.optional().describe(
|
||||
"Silkscreen rectangle on F.SilkS",
|
||||
),
|
||||
fabLayer: RectSchema.optional().describe(
|
||||
"Fab-layer rectangle on F.Fab (shows component body)",
|
||||
),
|
||||
refPosition: z
|
||||
.object({ x: z.number(), y: z.number() })
|
||||
.optional()
|
||||
.describe("Position of the REF** text (default: 0, -1.27)"),
|
||||
valuePosition: z
|
||||
.object({ x: z.number(), y: z.number() })
|
||||
.optional()
|
||||
.describe("Position of the Value text (default: 0, 1.27)"),
|
||||
overwrite: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace existing footprint file (default: false)"),
|
||||
},
|
||||
async (args: {
|
||||
libraryPath: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
tags?: string;
|
||||
pads?: z.infer<typeof PadSchema>[];
|
||||
courtyard?: z.infer<typeof RectSchema>;
|
||||
silkscreen?: z.infer<typeof RectSchema>;
|
||||
fabLayer?: z.infer<typeof RectSchema>;
|
||||
refPosition?: { x: number; y: number };
|
||||
valuePosition?: { x: number; y: number };
|
||||
overwrite?: boolean;
|
||||
}) => {
|
||||
const result = await callKicadScript("create_footprint", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── edit_footprint_pad ────────────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"edit_footprint_pad",
|
||||
"Edit an existing pad inside a .kicad_mod footprint file. " +
|
||||
"Updates size, position, drill, or shape without recreating the whole footprint.",
|
||||
{
|
||||
footprintPath: z
|
||||
.string()
|
||||
.describe("Full path to the .kicad_mod file, e.g. C:/MyLib.pretty/R_Custom.kicad_mod"),
|
||||
padNumber: z
|
||||
.union([z.string(), z.number()])
|
||||
.describe("Pad number to edit, e.g. '1' or 2"),
|
||||
size: PadSize.optional().describe("New pad size in mm"),
|
||||
at: PadPosition.optional().describe("New pad position in mm"),
|
||||
drill: z
|
||||
.union([
|
||||
z.number().describe("Round drill diameter in mm"),
|
||||
z.object({ w: z.number(), h: z.number() }).describe("Oval drill"),
|
||||
])
|
||||
.optional()
|
||||
.describe("New drill size (for THT pads)"),
|
||||
shape: z
|
||||
.enum(["rect", "circle", "oval", "roundrect"])
|
||||
.optional()
|
||||
.describe("New pad shape"),
|
||||
},
|
||||
async (args: {
|
||||
footprintPath: string;
|
||||
padNumber: string | number;
|
||||
size?: { w: number; h: number };
|
||||
at?: { x: number; y: number; angle?: number };
|
||||
drill?: number | { w: number; h: number };
|
||||
shape?: string;
|
||||
}) => {
|
||||
const result = await callKicadScript("edit_footprint_pad", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── register_footprint_library ───────────────────────────────────────── //
|
||||
server.tool(
|
||||
"register_footprint_library",
|
||||
"Register a .pretty footprint library in KiCAD's fp-lib-table so KiCAD can find the footprints. " +
|
||||
"Run this after create_footprint when KiCAD shows 'library not found in footprint library table'.",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe("Full path to the .pretty directory to register"),
|
||||
libraryName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Nickname for the library in KiCAD (default: directory name without .pretty)"),
|
||||
description: z.string().optional().describe("Optional description"),
|
||||
scope: z
|
||||
.enum(["project", "global"])
|
||||
.optional()
|
||||
.describe(
|
||||
"project = writes fp-lib-table next to the .kicad_pro file (default); " +
|
||||
"global = writes to the user's global KiCAD config",
|
||||
),
|
||||
projectPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Path to the .kicad_pro file or its directory (required for scope=project " +
|
||||
"when the library is not in the project folder)",
|
||||
),
|
||||
},
|
||||
async (args: {
|
||||
libraryPath: string;
|
||||
libraryName?: string;
|
||||
description?: string;
|
||||
scope?: "project" | "global";
|
||||
projectPath?: string;
|
||||
}) => {
|
||||
const result = await callKicadScript("register_footprint_library", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── list_footprint_libraries ─────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"list_footprint_libraries",
|
||||
"List available .pretty footprint libraries and their contents (first 20 footprints per library). " +
|
||||
"Searches KiCAD standard install paths by default.",
|
||||
{
|
||||
searchPaths: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Override default search paths. Each entry should be a directory that contains .pretty subdirs.",
|
||||
),
|
||||
},
|
||||
async (args: { searchPaths?: string[] }) => {
|
||||
const result = await callKicadScript("list_footprint_libraries", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -14,3 +14,5 @@ export { registerSchematicTools } from "./schematic.js";
|
||||
export { registerLibraryTools } from "./library.js";
|
||||
export { registerUITools } from "./ui.js";
|
||||
export { registerDatasheetTools } from "./datasheet.js";
|
||||
export { registerFootprintTools } from "./footprint.js";
|
||||
export { registerSymbolCreatorTools } from "./symbol-creator.js";
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Symbol creator tools for KiCAD MCP server
|
||||
*
|
||||
* create_symbol – add a new symbol to a .kicad_sym library
|
||||
* delete_symbol – remove a symbol from a library
|
||||
* list_symbols_in_library – list all symbols in a .kicad_sym file
|
||||
* register_symbol_library – add library to sym-lib-table
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
const PinSchema = z.object({
|
||||
name: z.string().describe("Pin name, e.g. 'VCC', 'GND', 'IN+', '~' for unnamed"),
|
||||
number: z.union([z.string(), z.number()]).describe("Pin number, e.g. '1', '2', 'A1'"),
|
||||
type: z
|
||||
.enum([
|
||||
"input", "output", "bidirectional", "tri_state", "passive",
|
||||
"free", "unspecified", "power_in", "power_out",
|
||||
"open_collector", "open_emitter", "no_connect",
|
||||
])
|
||||
.describe("Electrical pin type"),
|
||||
at: z.object({
|
||||
x: z.number().describe("X position in mm"),
|
||||
y: z.number().describe("Y position in mm"),
|
||||
angle: z.number().describe(
|
||||
"Direction the pin wire extends FROM the symbol body: 0=right, 90=up, 180=left, 270=down"
|
||||
),
|
||||
}).describe("Pin endpoint position (where the wire connects)"),
|
||||
length: z.number().optional().describe("Pin length in mm (default 2.54)"),
|
||||
shape: z
|
||||
.enum(["line", "inverted", "clock", "inverted_clock", "input_low",
|
||||
"clock_low", "output_low", "falling_edge_clock", "non_logic"])
|
||||
.optional()
|
||||
.describe("Pin graphic shape (default: line)"),
|
||||
});
|
||||
|
||||
const RectSchema = z.object({
|
||||
x1: z.number(), y1: z.number(),
|
||||
x2: z.number(), y2: z.number(),
|
||||
width: z.number().optional().describe("Stroke width in mm (default 0.254)"),
|
||||
fill: z.enum(["none", "outline", "background"]).optional()
|
||||
.describe("Fill type (default: background)"),
|
||||
});
|
||||
|
||||
const PolylineSchema = z.object({
|
||||
points: z.array(z.object({ x: z.number(), y: z.number() }))
|
||||
.describe("List of XY points in mm"),
|
||||
width: z.number().optional().describe("Stroke width in mm (default 0.254)"),
|
||||
fill: z.enum(["none", "outline", "background"]).optional(),
|
||||
});
|
||||
|
||||
export function registerSymbolCreatorTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
// ── create_symbol ────────────────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"create_symbol",
|
||||
"Create a new schematic symbol in a .kicad_sym library file (created if missing). " +
|
||||
"After creation, use register_symbol_library so KiCAD finds it. " +
|
||||
"Pin positions are where the wire connects; the symbol body is drawn between them.\n\n" +
|
||||
"Coordinate tips:\n" +
|
||||
"- Body rectangle typically spans ±2.54 to ±5.08 mm\n" +
|
||||
"- Pins on left side: at.x = body_left - length, angle=0 (wire goes right)\n" +
|
||||
"- Pins on right side: at.x = body_right + length, angle=180 (wire goes left)\n" +
|
||||
"- Pins on top: at.y = body_top + length, angle=270 (wire goes down)\n" +
|
||||
"- Pins on bottom: at.y = body_bottom - length, angle=90 (wire goes up)\n" +
|
||||
"- Standard pin length: 2.54 mm, standard grid: 2.54 mm",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe("Path to the .kicad_sym file (created if missing)"),
|
||||
name: z.string().describe("Symbol name, e.g. 'TMC2209', 'MyOpAmp'"),
|
||||
referencePrefix: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Schematic reference prefix: 'U' (IC), 'R' (resistor), 'J' (connector), etc. Default: 'U'"),
|
||||
description: z.string().optional().describe("Human-readable description"),
|
||||
keywords: z.string().optional().describe("Space-separated search keywords"),
|
||||
datasheet: z.string().optional().describe("Datasheet URL or '~'"),
|
||||
footprint: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Default footprint, e.g. 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm'"),
|
||||
inBom: z.boolean().optional().describe("Include in BOM (default true)"),
|
||||
onBoard: z.boolean().optional().describe("Include in netlist for PCB (default true)"),
|
||||
pins: z
|
||||
.array(PinSchema)
|
||||
.optional()
|
||||
.describe("List of pins (can be empty for graphical-only symbols)"),
|
||||
rectangles: z
|
||||
.array(RectSchema)
|
||||
.optional()
|
||||
.describe("Body rectangle(s). Typically one rectangle defining the IC body."),
|
||||
polylines: z
|
||||
.array(PolylineSchema)
|
||||
.optional()
|
||||
.describe("Polyline graphics for custom body shapes (op-amp triangles, etc.)"),
|
||||
overwrite: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace existing symbol with same name (default false)"),
|
||||
},
|
||||
async (args: {
|
||||
libraryPath: string;
|
||||
name: string;
|
||||
referencePrefix?: string;
|
||||
description?: string;
|
||||
keywords?: string;
|
||||
datasheet?: string;
|
||||
footprint?: string;
|
||||
inBom?: boolean;
|
||||
onBoard?: boolean;
|
||||
pins?: z.infer<typeof PinSchema>[];
|
||||
rectangles?: z.infer<typeof RectSchema>[];
|
||||
polylines?: z.infer<typeof PolylineSchema>[];
|
||||
overwrite?: boolean;
|
||||
}) => {
|
||||
const result = await callKicadScript("create_symbol", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── delete_symbol ────────────────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"delete_symbol",
|
||||
"Remove a symbol from a .kicad_sym library file.",
|
||||
{
|
||||
libraryPath: z.string().describe("Path to the .kicad_sym file"),
|
||||
name: z.string().describe("Symbol name to delete"),
|
||||
},
|
||||
async (args: { libraryPath: string; name: string }) => {
|
||||
const result = await callKicadScript("delete_symbol", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── list_symbols_in_library ──────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"list_symbols_in_library",
|
||||
"List all symbol names in a .kicad_sym library file.",
|
||||
{
|
||||
libraryPath: z.string().describe("Path to the .kicad_sym file"),
|
||||
},
|
||||
async (args: { libraryPath: string }) => {
|
||||
const result = await callKicadScript("list_symbols_in_library", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ── register_symbol_library ──────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"register_symbol_library",
|
||||
"Register a .kicad_sym library in KiCAD's sym-lib-table so symbols can be used in schematics. " +
|
||||
"Run this after create_symbol when KiCAD shows 'library not found'.",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe("Full path to the .kicad_sym file"),
|
||||
libraryName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Nickname (default: file name without extension)"),
|
||||
description: z.string().optional(),
|
||||
scope: z
|
||||
.enum(["project", "global"])
|
||||
.optional()
|
||||
.describe("project = writes sym-lib-table next to .kicad_pro; global = user config"),
|
||||
projectPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Path to .kicad_pro or its directory (for scope=project)"),
|
||||
},
|
||||
async (args: {
|
||||
libraryPath: string;
|
||||
libraryName?: string;
|
||||
description?: string;
|
||||
scope?: "project" | "global";
|
||||
projectPath?: string;
|
||||
}) => {
|
||||
const result = await callKicadScript("register_symbol_library", args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user