fix: footprint param ignored, delete duplicates, add edit_schematic_component

Bug 1 - add_schematic_component: footprint parameter silently ignored
  The footprint value from MCP params was never passed through to
  DynamicSymbolLoader.add_component() / create_component_instance().
  Every placed symbol had an empty Footprint field regardless of input.
  Fix: added footprint: str='' to both functions, passed through all
  call sites, added footprint to schematic.ts tool schema.

Bug 2 - delete_schematic_component: only deleted first duplicate
  When a reference appeared multiple times (e.g. after a failed
  add attempt), only the first instance was removed due to break
  after first match. Fix: collect all matching blocks first, then
  delete back-to-front to preserve indices. Response now includes
  deleted_count.

New tool - edit_schematic_component
  Update footprint, value or reference of a placed symbol in-place.
  More efficient than delete+re-add: preserves position and UUID.
  Accepts: schematicPath, reference, footprint?, value?, newReference?

All 3 fixes verified by live tests on a real JLCPCB/KiCAD 9 project:
  - R_TEST1: footprint Resistor_SMD:R_0603_1608Metric written correctly
  - J1 duplicate: deleted_count=2 with single call
  - J2 edit: PinSocket footprint assigned in-place, no delete+add needed
  - PCB update (F8) confirmed: only components with footprint imported
This commit is contained in:
Tom
2026-02-28 15:21:07 +01:00
parent ce78fb4111
commit b1eb570183
4 changed files with 299 additions and 104 deletions
+30 -32
View File
@@ -4,44 +4,42 @@ All notable changes to the KiCAD MCP Server project are documented here.
## [2.2.1-alpha] - 2026-02-28
### New MCP Tools
- `edit_schematic_component` Update properties of a placed symbol in-place (footprint,
value, reference rename). More efficient than delete + re-add: preserves position and UUID.
### Bug Fixes
- `add_schematic_component`: `footprint` parameter was accepted but silently ignored the
value was never passed through to `DynamicSymbolLoader.add_component()` /
`create_component_instance()`. All newly placed symbols always had an empty Footprint
field. Fix: added `footprint: str = ""` to both functions and threaded it through every
call site including the TypeScript tool schema.
- `delete_schematic_component`: only deleted the first matching instance when duplicate
references existed (e.g. after an aborted add attempt). Root cause: loop used `break`
after the first match. Fix: collect all matching blocks first, then delete them all back-
to-front (to preserve line indices). Response now includes `deleted_count`.
- `templates/*.kicad_sch`, `project.py`, `schematic.py`: Update KiCAD schematic format
version from `20230121` (KiCAD 7) to `20250114` (KiCAD 9).
version from `20230121` (KiCAD 7) to `20250114` (KiCAD 9). The MCP server targets
KiCAD 9 exclusively (`pcbnew.pyd` compiled for KiCAD 9.0, Python 3.11.5) generating
files in an outdated format caused a spurious "This file was created with an older
KiCAD version" warning on every newly created schematic.
**Root cause:** The MCP server targets KiCAD 9 exclusively `pcbnew.pyd` is compiled
for KiCAD 9.0 / Python 3.11.5, and the server explicitly selects the KiCAD 9 bundled
Python on Windows. Generating new schematics with a 2-year-old format tag caused a
spurious "This file was created with an older KiCAD version, it will be updated on
save" dialog on every newly created schematic, which confused users into thinking
something was broken.
- `template_with_symbols_expanded.kicad_sch`: Remove 13 corrupt `_TEMPLATE_*` placed-symbol
blocks with `(lib_id -100)` an integer caused by old sexpdata serializer (same bug
PR #40 fixed for the add path). KiCAD crashed with a null-pointer when selecting these
symbols. They appeared as grey `_TEMPLATE_R?`, `_TEMPLATE_U_REG?` etc. labels far
outside the sheet boundary (~5000mm off-sheet).
**Fix:** Updated header in all 4 template files and both inline schematic generators
(`project.py`, `schematic.py`) to emit `(version 20250114)`.
**Discovered via:** live testing on a real JLCPCB/KiCAD 9 project.
**Affected users:** schematics created from this template before this fix contain the
same corrupt blocks remove all `(symbol (lib_id -100) ...)` blocks whose Reference
starts with `_TEMPLATE_`.
- `template_with_symbols_expanded.kicad_sch`: Remove 13 corrupt `_TEMPLATE_*`
placed-symbol blocks containing `(lib_id -100)`.
**Root cause:** The expanded template was generated by the old sexpdata serializer
(the same corruption PR #40 fixed for the DynamicSymbolLoader add-path). The serializer
converted the string `"Device:R"` to the integer `-100`, producing `(lib_id -100)`
instead of `(lib_id "Device:R")`. KiCAD cannot resolve an integer as a library
reference and crashes with a null-pointer when the user attempts to select or box-select
these symbols. They appeared as grey `_TEMPLATE_R?`, `_TEMPLATE_C?`, `_TEMPLATE_U_REG?`
etc. labels ~5000 mm outside the sheet boundary invisible during normal work but
triggering a crash when accidentally selected.
**Discovered via:** Live testing on a real JLCPCB/KiCAD 9 project where Claude Desktop
(via MCP) created a schematic from the expanded template.
**Fix:** Removed all 13 `_TEMPLATE_*` placed-symbol blocks using parenthesis-depth
tracking (same text-manipulation approach as PR #40). The `lib_symbols` section
(symbol definitions) is unchanged only the corrupt placed instances were removed.
**Affected users:** Any schematic created from `template_with_symbols_expanded.kicad_sch`
before this fix contains the same corrupt blocks. To clean an existing schematic,
remove all `(symbol (lib_id -100) ...)` blocks whose `Reference` property starts
with `_TEMPLATE_`.
---
---
+110 -57
View File
@@ -14,7 +14,7 @@ import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
logger = logging.getLogger('kicad_interface')
logger = logging.getLogger("kicad_interface")
class DynamicSymbolLoader:
@@ -43,7 +43,7 @@ class DynamicSymbolLoader:
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
]
for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']:
for env_var in ["KICAD9_SYMBOL_DIR", "KICAD8_SYMBOL_DIR", "KICAD_SYMBOL_DIR"]:
if env_var in os.environ:
possible_paths.insert(0, Path(os.environ[env_var]))
@@ -63,13 +63,14 @@ class DynamicSymbolLoader:
Extract a complete symbol block from a library or schematic file by matching
parentheses depth. Returns the raw text of the symbol definition.
"""
lines = text.split('\n')
lines = text.split("\n")
start = None
for i, line in enumerate(lines):
stripped = line.strip()
# Match exact symbol name (not sub-symbols like Name_0_1)
if stripped.startswith(f'(symbol "{symbol_name}"') and \
not re.match(r'.*_\d+_\d+"', stripped):
if stripped.startswith(f'(symbol "{symbol_name}"') and not re.match(
r'.*_\d+_\d+"', stripped
):
start = i
break
@@ -80,9 +81,9 @@ class DynamicSymbolLoader:
end = None
for i in range(start, len(lines)):
for ch in lines[i]:
if ch == '(':
if ch == "(":
depth += 1
elif ch == ')':
elif ch == ")":
depth -= 1
if depth == 0:
end = i
@@ -93,9 +94,11 @@ class DynamicSymbolLoader:
if end is None:
return None
return '\n'.join(lines[start:end + 1])
return "\n".join(lines[start : end + 1])
def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
def extract_symbol_from_library(
self, library_name: str, symbol_name: str
) -> Optional[str]:
"""
Extract a symbol definition from a KiCad .kicad_sym library file.
Returns the raw text block, ready to be injected into a schematic.
@@ -112,12 +115,14 @@ class DynamicSymbolLoader:
if not lib_path:
return None
with open(lib_path, 'r', encoding='utf-8') as f:
with open(lib_path, "r", encoding="utf-8") as f:
lib_content = f.read()
block = self._extract_symbol_block(lib_content, symbol_name)
if block is None:
logger.warning(f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym")
logger.warning(
f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym"
)
return None
# Check if this symbol uses (extends "ParentName")
@@ -125,14 +130,16 @@ class DynamicSymbolLoader:
parent_block = None
if extends_match:
parent_name = extends_match.group(1)
logger.info(f"Symbol {symbol_name} extends {parent_name}, extracting parent too")
logger.info(
f"Symbol {symbol_name} extends {parent_name}, extracting parent too"
)
parent_block = self._extract_symbol_block(lib_content, parent_name)
if parent_block:
# Prefix parent top-level name with library
parent_block = parent_block.replace(
f'(symbol "{parent_name}"',
f'(symbol "{library_name}:{parent_name}"',
1 # Only first occurrence (top-level)
1, # Only first occurrence (top-level)
)
# Prefix top-level symbol name with library
@@ -140,13 +147,13 @@ class DynamicSymbolLoader:
block = block.replace(
f'(symbol "{symbol_name}"',
f'(symbol "{full_name}"',
1 # Only first occurrence (top-level)
1, # Only first occurrence (top-level)
)
# Sub-symbols like "Name_0_1" keep their short names (already correct from library)
# Combine parent + child if extends is used
if parent_block:
result = parent_block + '\n' + block
result = parent_block + "\n" + block
else:
result = block
@@ -154,14 +161,16 @@ class DynamicSymbolLoader:
logger.info(f"Extracted symbol {full_name} ({len(result)} chars)")
return result
def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool:
def inject_symbol_into_schematic(
self, schematic_path: Path, library_name: str, symbol_name: str
) -> bool:
"""
Inject a symbol definition into a schematic's lib_symbols section.
Uses text manipulation to preserve file formatting.
"""
full_name = f"{library_name}:{symbol_name}"
with open(schematic_path, 'r', encoding='utf-8') as f:
with open(schematic_path, "r", encoding="utf-8") as f:
content = f.read()
# Check if symbol already exists
@@ -172,36 +181,38 @@ class DynamicSymbolLoader:
# Extract symbol from library
symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
if not symbol_block:
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
raise ValueError(
f"Symbol '{symbol_name}' not found in library '{library_name}'"
)
# Indent the block to match lib_symbols indentation (4 spaces for top-level)
indented_lines = []
for line in symbol_block.split('\n'):
for line in symbol_block.split("\n"):
# Add 4-space indent for the content inside lib_symbols
indented_lines.append(' ' + line if line.strip() else line)
indented_block = '\n'.join(indented_lines)
indented_lines.append(" " + line if line.strip() else line)
indented_block = "\n".join(indented_lines)
# Find the end of lib_symbols section to insert before closing )
lines = content.split('\n')
lines = content.split("\n")
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:
if "(lib_symbols" in line and lib_sym_start is None:
lib_sym_start = i
depth = 0
for ch in line:
if ch == '(':
if ch == "(":
depth += 1
elif ch == ')':
elif ch == ")":
depth -= 1
continue
if lib_sym_start is not None and lib_sym_end is None:
for ch in line:
if ch == '(':
if ch == "(":
depth += 1
elif ch == ')':
elif ch == ")":
depth -= 1
if depth == 0:
lib_sym_end = i
@@ -215,15 +226,29 @@ class DynamicSymbolLoader:
# Insert the symbol block just before the closing ) of lib_symbols
lines.insert(lib_sym_end, indented_block)
with open(schematic_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
with open(schematic_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
logger.info(f"Injected symbol {full_name} into {schematic_path.name}")
# Handle both Path objects and strings
sch_name = (
schematic_path.name
if hasattr(schematic_path, "name")
else str(schematic_path)
)
logger.info(f"Injected symbol {full_name} into {sch_name}")
return True
def create_component_instance(self, schematic_path: Path, library_name: str,
symbol_name: str, reference: str,
value: str = "", x: float = 0, y: float = 0) -> bool:
def create_component_instance(
self,
schematic_path: Path,
library_name: str,
symbol_name: str,
reference: str,
value: str = "",
footprint: str = "",
x: float = 0,
y: float = 0,
) -> bool:
"""
Add a component instance to the schematic.
This creates the (symbol ...) block with lib_id reference.
@@ -231,7 +256,7 @@ class DynamicSymbolLoader:
full_lib_id = f"{library_name}:{symbol_name}"
new_uuid = str(uuid.uuid4())
instance_block = f''' (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
instance_block = f""" (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
(in_bom yes) (on_board yes) (dnp no)
(uuid "{new_uuid}")
(property "Reference" "{reference}" (at {x} {y - 2.54} 0)
@@ -240,30 +265,30 @@ class DynamicSymbolLoader:
(property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
(effects (font (size 1.27 1.27)))
)
(property "Footprint" "" (at {x} {y} 0)
(property "Footprint" "{footprint}" (at {x} {y} 0)
(effects (font (size 1.27 1.27)) (hide yes))
)
(property "Datasheet" "~" (at {x} {y} 0)
(effects (font (size 1.27 1.27)) (hide yes))
)
)'''
)"""
with open(schematic_path, 'r', encoding='utf-8') as f:
with open(schematic_path, "r", encoding="utf-8") as f:
content = f.read()
# Insert before (sheet_instances or at end before final )
lines = content.split('\n')
lines = content.split("\n")
insert_pos = None
for i, line in enumerate(lines):
if '(sheet_instances' in line:
if "(sheet_instances" in line:
insert_pos = i
break
if insert_pos is None:
# Insert before the last closing parenthesis
for i in range(len(lines) - 1, -1, -1):
if lines[i].strip() == ')':
if lines[i].strip() == ")":
insert_pos = i
break
@@ -272,13 +297,17 @@ class DynamicSymbolLoader:
lines.insert(insert_pos, instance_block)
with open(schematic_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
with open(schematic_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
logger.info(
f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})"
)
return True
def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str:
def load_symbol_dynamically(
self, schematic_path: Path, library_name: str, symbol_name: str
) -> str:
"""
Complete workflow: inject symbol definition and create a template instance.
Returns a template reference name.
@@ -289,21 +318,34 @@ class DynamicSymbolLoader:
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
# Step 2: Create an offscreen template instance
lib_clean = library_name.replace('-', '_').replace('.', '_')
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
lib_clean = library_name.replace("-", "_").replace(".", "_")
sym_clean = symbol_name.replace("-", "_").replace(".", "_")
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
self.create_component_instance(
schematic_path, library_name, symbol_name,
reference=template_ref, value=symbol_name,
x=-200, y=-200
schematic_path,
library_name,
symbol_name,
reference=template_ref,
value=symbol_name,
x=-200,
y=-200,
)
logger.info(f"Symbol loaded. Template reference: {template_ref}")
return template_ref
def add_component(self, schematic_path: Path, library_name: str, symbol_name: str,
reference: str, value: str = "", x: float = 0, y: float = 0) -> bool:
def add_component(
self,
schematic_path: Path,
library_name: str,
symbol_name: str,
reference: str,
value: str = "",
footprint: str = "",
x: float = 0,
y: float = 0,
) -> bool:
"""
High-level: ensure symbol definition exists in schematic, then add an instance.
This is the main entry point for adding components.
@@ -313,12 +355,18 @@ class DynamicSymbolLoader:
# Add the component instance
return self.create_component_instance(
schematic_path, library_name, symbol_name,
reference=reference, value=value, x=x, y=y
schematic_path,
library_name,
symbol_name,
reference=reference,
value=value,
footprint=footprint,
x=x,
y=y,
)
if __name__ == '__main__':
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
loader = DynamicSymbolLoader()
@@ -329,7 +377,12 @@ if __name__ == '__main__':
print(f" Found {len(lib_dirs)} directories")
print("\n2. Extracting symbols...")
for lib, sym in [('Device', 'R'), ('Device', 'C'), ('Device', 'LED'), ('Device', 'Q_NMOS')]:
for lib, sym in [
("Device", "R"),
("Device", "C"),
("Device", "LED"),
("Device", "Q_NMOS"),
]:
block = loader.extract_symbol_from_library(lib, sym)
if block:
print(f" OK: {lib}:{sym} ({len(block)} chars)")
@@ -337,8 +390,8 @@ if __name__ == '__main__':
print(f" FAIL: {lib}:{sym}")
print("\n3. Testing extends resolution...")
block = loader.extract_symbol_from_library('Regulator_Switching', 'LM2596S-5')
if block and 'LM2596S-12' in block:
block = loader.extract_symbol_from_library("Regulator_Switching", "LM2596S-5")
if block and "LM2596S-12" in block:
print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
else:
print(f" FAIL: extends not resolved")
+107 -15
View File
@@ -366,6 +366,7 @@ class KiCADInterface:
"load_schematic": self._handle_load_schematic,
"add_schematic_component": self._handle_add_schematic_component,
"delete_schematic_component": self._handle_delete_schematic_component,
"edit_schematic_component": self._handle_edit_schematic_component,
"add_schematic_wire": self._handle_add_schematic_wire,
"add_schematic_connection": self._handle_add_schematic_connection,
"add_schematic_net_label": self._handle_add_schematic_net_label,
@@ -585,6 +586,7 @@ class KiCADInterface:
library = component.get("library", "Device")
reference = component.get("reference", "X?")
value = component.get("value", comp_type)
footprint = component.get("footprint", "")
x = component.get("x", 0)
y = component.get("y", 0)
@@ -595,6 +597,7 @@ class KiCADInterface:
comp_type,
reference=reference,
value=value,
footprint=footprint,
x=x,
y=y,
)
@@ -646,9 +649,8 @@ class KiCADInterface:
lib_sym_end = i
break
# Find the placed symbol block matching the reference
block_start = None
block_end = None
# Find ALL placed symbol blocks matching the reference (handles duplicates)
blocks_to_delete = []
i = 0
while i < len(lines):
# Skip lib_symbols
@@ -666,35 +668,33 @@ class KiCADInterface:
j += 1
b_end = j - 1
# Check if this block contains the target reference
block_text = "\n".join(lines[b_start:b_end + 1])
# Match: (property "Reference" "R1" ...
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
block_start = b_start
block_end = b_end
break
blocks_to_delete.append((b_start, b_end))
i = b_end + 1
continue
i += 1
if block_start is None:
if not blocks_to_delete:
return {
"success": False,
"message": f"Component '{reference}' not found in schematic (note: this tool removes schematic symbols, use delete_component for PCB footprints)",
}
# Remove the block (and any trailing blank line)
del lines[block_start:block_end + 1]
if block_start < len(lines) and lines[block_start].strip() == "":
del lines[block_start]
# Delete from back to front to preserve line indices
for b_start, b_end in sorted(blocks_to_delete, reverse=True):
del lines[b_start:b_end + 1]
if b_start < len(lines) and lines[b_start].strip() == "":
del lines[b_start]
with open(sch_file, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
logger.info(f"Deleted schematic component {reference} from {sch_file.name}")
return {"success": True, "reference": reference, "schematic": str(sch_file)}
deleted_count = len(blocks_to_delete)
logger.info(f"Deleted {deleted_count} instance(s) of {reference} from {sch_file.name}")
return {"success": True, "reference": reference, "deleted_count": deleted_count, "schematic": str(sch_file)}
except Exception as e:
logger.error(f"Error deleting schematic component: {e}")
@@ -702,6 +702,98 @@ class KiCADInterface:
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_edit_schematic_component(self, params):
"""Update properties of a placed symbol in a schematic (footprint, value, reference).
Uses text-based in-place editing preserves position, UUID and all other fields."""
logger.info("Editing schematic component")
try:
from pathlib import Path
import re
schematic_path = params.get("schematicPath")
reference = params.get("reference")
new_footprint = params.get("footprint")
new_value = params.get("value")
new_reference = params.get("newReference")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
if not reference:
return {"success": False, "message": "reference is required"}
if not any([new_footprint is not None, new_value is not None, new_reference is not None]):
return {"success": False, "message": "At least one of footprint, value, or newReference must be provided"}
sch_file = Path(schematic_path)
if not sch_file.exists():
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
with open(sch_file, "r", encoding="utf-8") as f:
lines = f.read().split("\n")
# Find lib_symbols range to skip
lib_sym_start, lib_sym_end = None, 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 = sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
elif lib_sym_start is not None and lib_sym_end is None:
depth += sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
if depth == 0:
lib_sym_end = i
break
# Find the placed symbol block
block_start = block_end = None
i = 0
while i < len(lines):
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
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", lines[i]):
b_start = i
b_depth = sum(1 for c in lines[i] if c == "(") - sum(1 for c in lines[i] if c == ")")
j = i + 1
while j < len(lines) and b_depth > 0:
b_depth += sum(1 for c in lines[j] if c == "(") - sum(1 for c in lines[j] if c == ")")
j += 1
b_end = j - 1
block_text = "\n".join(lines[b_start:b_end + 1])
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
block_start, block_end = b_start, b_end
break
i = b_end + 1
continue
i += 1
if block_start is None:
return {"success": False, "message": f"Component '{reference}' not found in schematic"}
# Apply in-place property updates within the block
for k in range(block_start, block_end + 1):
line = lines[k]
if new_footprint is not None and re.match(r'\s*\(property\s+"Footprint"\s+"', line):
line = re.sub(r'(\(property\s+"Footprint"\s+)"[^"]*"', rf'\1"{new_footprint}"', line)
if new_value is not None and re.match(r'\s*\(property\s+"Value"\s+"', line):
line = re.sub(r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', line)
if new_reference is not None and re.match(r'\s*\(property\s+"Reference"\s+"', line):
line = re.sub(r'(\(property\s+"Reference"\s+)"[^"]*"', rf'\1"{new_reference}"', line)
lines[k] = line
with open(sch_file, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
changes = {k: v for k, v in {"footprint": new_footprint, "value": new_value, "reference": new_reference}.items() if v is not None}
logger.info(f"Edited schematic component {reference}: {changes}")
return {"success": True, "reference": reference, "updated": changes}
except Exception as e:
logger.error(f"Error editing schematic component: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_add_schematic_wire(self, params):
"""Add a wire to a schematic using WireManager"""
logger.info("Adding wire to schematic")
+52
View File
@@ -43,6 +43,7 @@ export function registerSchematicTools(
),
reference: z.string().describe("Component reference (e.g., R1, U1)"),
value: z.string().optional().describe("Component value"),
footprint: z.string().optional().describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
position: z
.object({
x: z.number(),
@@ -56,6 +57,7 @@ export function registerSchematicTools(
symbol: string;
reference: string;
value?: string;
footprint?: string;
position?: { x: number; y: number };
}) => {
// Transform to what Python backend expects
@@ -70,6 +72,7 @@ export function registerSchematicTools(
type: symbolName,
reference: args.reference,
value: args.value,
footprint: args.footprint ?? "",
// Python expects flat x, y not nested position
x: args.position?.x ?? 0,
y: args.position?.y ?? 0,
@@ -141,6 +144,55 @@ To remove a footprint from a PCB, use delete_component instead.`,
},
);
// Edit component properties in schematic (footprint, value, reference)
server.tool(
"edit_schematic_component",
`Update properties of a placed symbol in a KiCAD schematic (.kicad_sch) in-place.
Use this tool to assign or update a footprint, change the value, or rename the reference
of an already-placed component. This is more efficient than delete + re-add because it
preserves the component's position and UUID.
Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_component.`,
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Current reference designator of the component (e.g. R1, U3)"),
footprint: z.string().optional().describe("New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)"),
value: z.string().optional().describe("New value string (e.g. 10k, 100nF)"),
newReference: z.string().optional().describe("Rename the reference designator (e.g. R1 → R10)"),
},
async (args: {
schematicPath: string;
reference: string;
footprint?: string;
value?: string;
newReference?: string;
}) => {
const result = await callKicadScript("edit_schematic_component", args);
if (result.success) {
const changes = Object.entries(result.updated ?? {})
.map(([k, v]) => `${k}=${v}`)
.join(", ");
return {
content: [
{
type: "text" as const,
text: `Successfully updated ${args.reference}: ${changes}`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: `Failed to edit component: ${result.message || "Unknown error"}`,
},
],
};
},
);
// Connect components with wire
server.tool(
"add_wire",