feat: datasheet tools + fix missing delete_schematic_component
New tools - datasheet: - get_datasheet_url: construct LCSC datasheet PDF URL + product page URL without any API key (URL schema: https://www.lcsc.com/datasheet/<C#>.pdf) - enrich_datasheets: scan .kicad_sch, write LCSC datasheet URL into every symbol that has an LCSC property but an empty Datasheet field; supports dry_run=true for preview; text-based implementation (no skip writes) Implementation: python/commands/datasheet_manager.py New tool - schematic: - delete_schematic_component: remove a placed symbol from a .kicad_sch file by reference designator (e.g. R1, U3) Bug fix - delete_schematic_component (two separate root causes): 1. No MCP tool named delete_schematic_component was registered at all. Any delete-symbol request fell through to the PCB-only delete_component tool which searches pcbnew.BOARD and always returned 'Component not found' for schematic symbols. 2. component_schematic.py::remove_component() still used skip for writes. PR #40 rewrote DynamicSymbolLoader (add path) to avoid skip-induced schematic corruption, but the delete path was not touched by that PR. Fix: _handle_delete_schematic_component in kicad_interface.py uses direct text manipulation with parenthesis-depth tracking (same technique as PR #40), bypassing component_schematic.py entirely. Error message explicitly guides users: 'use delete_component for PCB footprints'. Files changed: - python/commands/datasheet_manager.py (new) - src/tools/datasheet.ts (new) - python/kicad_interface.py: 3 new handlers + dispatch entries - src/tools/schematic.ts: delete_schematic_component tool - src/server.ts: registerDatasheetTools import + call - src/tools/index.ts: export registerDatasheetTools - CHANGELOG.md: document all above
This commit is contained in:
39
CHANGELOG.md
39
CHANGELOG.md
@@ -36,6 +36,45 @@ All notable changes to the KiCAD MCP Server project are documented here.
|
|||||||
- `library.py`: Fix loop variable shadowing `Path` object (mypy)
|
- `library.py`: Fix loop variable shadowing `Path` object (mypy)
|
||||||
- `design_rules.py`: Add type annotation for `violation_counts` (mypy)
|
- `design_rules.py`: Add type annotation for `violation_counts` (mypy)
|
||||||
|
|
||||||
|
### Pending additions (not yet committed)
|
||||||
|
|
||||||
|
**Datasheet tools:**
|
||||||
|
- `get_datasheet_url` - Return LCSC datasheet PDF URL and product page URL for a given
|
||||||
|
LCSC number (e.g. `C179739` → `https://www.lcsc.com/datasheet/C179739.pdf`).
|
||||||
|
No API key required – URL is constructed directly from the LCSC number.
|
||||||
|
- `enrich_datasheets` - Scan a `.kicad_sch` file and write LCSC datasheet URLs into
|
||||||
|
every symbol that has an `LCSC` property but an empty `Datasheet` field. After
|
||||||
|
enrichment the URL appears natively in KiCAD's symbol properties, footprint browser
|
||||||
|
and any other tool that reads the standard KiCAD `Datasheet` field.
|
||||||
|
Supports `dry_run=true` for preview without writing.
|
||||||
|
Implementation: `python/commands/datasheet_manager.py` (text-based, no `skip` writes)
|
||||||
|
|
||||||
|
**Schematic tools:**
|
||||||
|
- `delete_schematic_component` - Remove a placed symbol from a `.kicad_sch` file by
|
||||||
|
reference designator (e.g. `R1`, `U3`).
|
||||||
|
|
||||||
|
### Bug Fixes (pending)
|
||||||
|
|
||||||
|
- `schematic.ts` / `kicad_interface.py`: Fix missing `delete_schematic_component` MCP tool.
|
||||||
|
|
||||||
|
**Root cause (two separate issues):**
|
||||||
|
1. No MCP tool named `delete_schematic_component` existed. Claude had no way to call
|
||||||
|
it, so any "delete schematic component" request fell through to the PCB-only
|
||||||
|
`delete_component` tool, which searches `pcbnew.BOARD` and always returned
|
||||||
|
"Component not found" for schematic symbols.
|
||||||
|
2. `component_schematic.py::remove_component()` still used `skip` for writes.
|
||||||
|
PR #40 rewrote `DynamicSymbolLoader` (add path) to avoid `skip`-induced schematic
|
||||||
|
corruption, but `remove_component` (delete path) was not touched by that PR.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
- Added `delete_schematic_component` to the TypeScript tool layer (`schematic.ts`)
|
||||||
|
with clear docstring distinguishing it from the PCB `delete_component`.
|
||||||
|
- Implemented `_handle_delete_schematic_component` in `kicad_interface.py` using
|
||||||
|
direct text manipulation (parenthesis-depth tracking, same approach as PR #40).
|
||||||
|
Does not call `component_schematic.py::remove_component()` at all.
|
||||||
|
- Error message explicitly guides the user when the wrong tool is used:
|
||||||
|
*"note: this tool removes schematic symbols, use delete_component for PCB footprints"*
|
||||||
|
|
||||||
### Pending fixes (not yet committed)
|
### Pending fixes (not yet committed)
|
||||||
|
|
||||||
- `connection_schematic.py` / `kicad_interface.py`: Fix `generate_netlist` missing
|
- `connection_schematic.py` / `kicad_interface.py`: Fix `generate_netlist` missing
|
||||||
|
|||||||
289
python/commands/datasheet_manager.py
Normal file
289
python/commands/datasheet_manager.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
"""
|
||||||
|
Datasheet Manager for KiCAD MCP Server
|
||||||
|
|
||||||
|
Enriches KiCAD schematic symbols with datasheet URLs derived from LCSC part numbers.
|
||||||
|
Uses direct text manipulation (like dynamic_symbol_loader.py) to avoid
|
||||||
|
skip-library-induced schematic corruption.
|
||||||
|
|
||||||
|
URL schema: https://www.lcsc.com/datasheet/{LCSC#}.pdf
|
||||||
|
No API key required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
LCSC_DATASHEET_URL = "https://www.lcsc.com/datasheet/{lcsc}.pdf"
|
||||||
|
LCSC_PRODUCT_URL = "https://www.lcsc.com/product-detail/{lcsc}.html"
|
||||||
|
|
||||||
|
# Values treated as "empty" datasheet
|
||||||
|
EMPTY_DATASHEET_VALUES = {"~", "", "~{DATASHEET}"}
|
||||||
|
|
||||||
|
|
||||||
|
class DatasheetManager:
|
||||||
|
"""
|
||||||
|
Enriches KiCAD schematics with LCSC datasheet URLs.
|
||||||
|
|
||||||
|
Reads .kicad_sch files, finds symbol instances that have an LCSC property
|
||||||
|
but an empty Datasheet property, and fills in the LCSC datasheet URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_lcsc(lcsc: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Normalize LCSC number to standard format 'C123456'.
|
||||||
|
|
||||||
|
Accepts: 'C123456', '123456', 'c123456'
|
||||||
|
Returns: 'C123456' or None if invalid
|
||||||
|
"""
|
||||||
|
lcsc = lcsc.strip()
|
||||||
|
if not lcsc:
|
||||||
|
return None
|
||||||
|
# Remove leading C/c
|
||||||
|
without_prefix = lcsc.lstrip("Cc")
|
||||||
|
if without_prefix.isdigit():
|
||||||
|
return f"C{without_prefix}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_lib_symbols_range(lines: List[str]):
|
||||||
|
"""
|
||||||
|
Find the line range of the (lib_symbols ...) section.
|
||||||
|
Returns (start, end) line indices or (None, None) if not found.
|
||||||
|
These lines must be excluded from symbol-instance processing.
|
||||||
|
"""
|
||||||
|
lib_sym_start = None
|
||||||
|
lib_sym_end = None
|
||||||
|
depth = 0
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if "(lib_symbols" in line and lib_sym_start is None:
|
||||||
|
lib_sym_start = i
|
||||||
|
depth = 0
|
||||||
|
for ch in line:
|
||||||
|
if ch == "(":
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
elif lib_sym_start is not None and lib_sym_end is None:
|
||||||
|
for ch in line:
|
||||||
|
if ch == "(":
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
lib_sym_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
return lib_sym_start, lib_sym_end
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_symbol_block(
|
||||||
|
lines: List[str], block_start: int, block_end: int
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Extract LCSC and Datasheet info from a placed symbol block.
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
- lcsc: normalized LCSC number or None
|
||||||
|
- datasheet_line: line index of Datasheet property or None
|
||||||
|
- datasheet_value: current Datasheet value or None
|
||||||
|
"""
|
||||||
|
lcsc_value = None
|
||||||
|
datasheet_line_idx = None
|
||||||
|
datasheet_current = None
|
||||||
|
|
||||||
|
for k in range(block_start, block_end + 1):
|
||||||
|
line = lines[k]
|
||||||
|
|
||||||
|
lcsc_match = re.search(r'\(property\s+"LCSC"\s+"([^"]*)"', line)
|
||||||
|
if lcsc_match:
|
||||||
|
lcsc_value = lcsc_match.group(1)
|
||||||
|
|
||||||
|
ds_match = re.search(r'\(property\s+"Datasheet"\s+"([^"]*)"', line)
|
||||||
|
if ds_match:
|
||||||
|
datasheet_line_idx = k
|
||||||
|
datasheet_current = ds_match.group(1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"lcsc": lcsc_value,
|
||||||
|
"datasheet_line": datasheet_line_idx,
|
||||||
|
"datasheet_value": datasheet_current,
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrich_schematic(
|
||||||
|
self, schematic_path: Path, dry_run: bool = False
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Scan a .kicad_sch file and fill in missing LCSC datasheet URLs.
|
||||||
|
|
||||||
|
For each placed symbol that has:
|
||||||
|
- (property "LCSC" "C123456") set
|
||||||
|
- (property "Datasheet" "~") or empty
|
||||||
|
|
||||||
|
Sets:
|
||||||
|
- (property "Datasheet" "https://www.lcsc.com/datasheet/C123456.pdf")
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schematic_path: Path to .kicad_sch file
|
||||||
|
dry_run: If True, return what would be changed without writing
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"updated": <count>,
|
||||||
|
"already_set": <count>,
|
||||||
|
"no_lcsc": <count>,
|
||||||
|
"no_datasheet_field": <count>,
|
||||||
|
"details": [{"reference": "...", "lcsc": "...", "url": "..."}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
schematic_path = Path(schematic_path)
|
||||||
|
if not schematic_path.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Schematic not found: {schematic_path}",
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
lines = content.split("\n")
|
||||||
|
new_lines = list(lines)
|
||||||
|
|
||||||
|
lib_sym_start, lib_sym_end = self._find_lib_symbols_range(lines)
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
already_set = 0
|
||||||
|
no_lcsc = 0
|
||||||
|
no_datasheet_field = 0
|
||||||
|
details = []
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(new_lines):
|
||||||
|
line = new_lines[i]
|
||||||
|
|
||||||
|
# Skip lib_symbols section
|
||||||
|
if lib_sym_start is not None and lib_sym_end is not None:
|
||||||
|
if lib_sym_start <= i <= lib_sym_end:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detect placed symbol: (symbol (lib_id "...")
|
||||||
|
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", line):
|
||||||
|
block_start = i
|
||||||
|
block_depth = 0
|
||||||
|
for ch in line:
|
||||||
|
if ch == "(":
|
||||||
|
block_depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
block_depth -= 1
|
||||||
|
|
||||||
|
j = i + 1
|
||||||
|
while j < len(new_lines) and block_depth > 0:
|
||||||
|
for ch in new_lines[j]:
|
||||||
|
if ch == "(":
|
||||||
|
block_depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
block_depth -= 1
|
||||||
|
if block_depth > 0:
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
block_end = j
|
||||||
|
info = self._process_symbol_block(new_lines, block_start, block_end)
|
||||||
|
|
||||||
|
raw_lcsc = info["lcsc"]
|
||||||
|
ds_line = info["datasheet_line"]
|
||||||
|
ds_value = info["datasheet_value"]
|
||||||
|
|
||||||
|
# Extract reference for reporting
|
||||||
|
ref_match = None
|
||||||
|
for k in range(block_start, block_end + 1):
|
||||||
|
m = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', new_lines[k])
|
||||||
|
if m:
|
||||||
|
ref_match = m.group(1)
|
||||||
|
break
|
||||||
|
reference = ref_match or "?"
|
||||||
|
|
||||||
|
if not raw_lcsc:
|
||||||
|
no_lcsc += 1
|
||||||
|
elif ds_line is None:
|
||||||
|
no_datasheet_field += 1
|
||||||
|
logger.warning(
|
||||||
|
f"Symbol {reference} has LCSC={raw_lcsc} but no Datasheet property"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lcsc_norm = self._normalize_lcsc(raw_lcsc)
|
||||||
|
if not lcsc_norm:
|
||||||
|
no_lcsc += 1
|
||||||
|
elif ds_value not in EMPTY_DATASHEET_VALUES:
|
||||||
|
already_set += 1
|
||||||
|
logger.debug(
|
||||||
|
f"Symbol {reference}: Datasheet already set to {ds_value!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
url = LCSC_DATASHEET_URL.format(lcsc=lcsc_norm)
|
||||||
|
if not dry_run:
|
||||||
|
new_lines[ds_line] = re.sub(
|
||||||
|
r'(property\s+"Datasheet"\s+)"[^"]*"',
|
||||||
|
f'\\1"{url}"',
|
||||||
|
new_lines[ds_line],
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
details.append(
|
||||||
|
{
|
||||||
|
"reference": reference,
|
||||||
|
"lcsc": lcsc_norm,
|
||||||
|
"url": url,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"{'[DRY RUN] ' if dry_run else ''}Set Datasheet for "
|
||||||
|
f"{reference} ({lcsc_norm}): {url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
i = block_end + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not dry_run and updated > 0:
|
||||||
|
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(new_lines))
|
||||||
|
logger.info(
|
||||||
|
f"Saved {schematic_path.name}: {updated} datasheet URLs written"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"updated": updated,
|
||||||
|
"already_set": already_set,
|
||||||
|
"no_lcsc": no_lcsc,
|
||||||
|
"no_datasheet_field": no_datasheet_field,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
"details": details,
|
||||||
|
"schematic": str(schematic_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_datasheet_url(self, lcsc: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Return the LCSC datasheet URL for a given LCSC number.
|
||||||
|
No network request – pure URL construction.
|
||||||
|
"""
|
||||||
|
norm = self._normalize_lcsc(lcsc)
|
||||||
|
if norm:
|
||||||
|
return LCSC_DATASHEET_URL.format(lcsc=norm)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_product_url(self, lcsc: str) -> Optional[str]:
|
||||||
|
"""Return the LCSC product page URL."""
|
||||||
|
norm = self._normalize_lcsc(lcsc)
|
||||||
|
if norm:
|
||||||
|
return LCSC_PRODUCT_URL.format(lcsc=norm)
|
||||||
|
return None
|
||||||
@@ -223,6 +223,7 @@ try:
|
|||||||
from commands.library_symbol import SymbolLibraryManager, SymbolLibraryCommands
|
from commands.library_symbol import SymbolLibraryManager, SymbolLibraryCommands
|
||||||
from commands.jlcpcb import JLCPCBClient, test_jlcpcb_connection
|
from commands.jlcpcb import JLCPCBClient, test_jlcpcb_connection
|
||||||
from commands.jlcpcb_parts import JLCPCBPartsManager
|
from commands.jlcpcb_parts import JLCPCBPartsManager
|
||||||
|
from commands.datasheet_manager import DatasheetManager
|
||||||
|
|
||||||
logger.info("Successfully imported all command handlers")
|
logger.info("Successfully imported all command handlers")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
@@ -357,10 +358,14 @@ class KiCADInterface:
|
|||||||
"get_jlcpcb_part": self._handle_get_jlcpcb_part,
|
"get_jlcpcb_part": self._handle_get_jlcpcb_part,
|
||||||
"get_jlcpcb_database_stats": self._handle_get_jlcpcb_database_stats,
|
"get_jlcpcb_database_stats": self._handle_get_jlcpcb_database_stats,
|
||||||
"suggest_jlcpcb_alternatives": self._handle_suggest_jlcpcb_alternatives,
|
"suggest_jlcpcb_alternatives": self._handle_suggest_jlcpcb_alternatives,
|
||||||
|
# Datasheet commands
|
||||||
|
"enrich_datasheets": self._handle_enrich_datasheets,
|
||||||
|
"get_datasheet_url": self._handle_get_datasheet_url,
|
||||||
# Schematic commands
|
# Schematic commands
|
||||||
"create_schematic": self._handle_create_schematic,
|
"create_schematic": self._handle_create_schematic,
|
||||||
"load_schematic": self._handle_load_schematic,
|
"load_schematic": self._handle_load_schematic,
|
||||||
"add_schematic_component": self._handle_add_schematic_component,
|
"add_schematic_component": self._handle_add_schematic_component,
|
||||||
|
"delete_schematic_component": self._handle_delete_schematic_component,
|
||||||
"add_schematic_wire": self._handle_add_schematic_wire,
|
"add_schematic_wire": self._handle_add_schematic_wire,
|
||||||
"add_schematic_connection": self._handle_add_schematic_connection,
|
"add_schematic_connection": self._handle_add_schematic_connection,
|
||||||
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
||||||
@@ -606,6 +611,97 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def _handle_delete_schematic_component(self, params):
|
||||||
|
"""Remove a placed symbol from a schematic using text-based manipulation (no skip writes)"""
|
||||||
|
logger.info("Deleting schematic component")
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
schematic_path = params.get("schematicPath")
|
||||||
|
reference = params.get("reference")
|
||||||
|
|
||||||
|
if not schematic_path:
|
||||||
|
return {"success": False, "message": "schematicPath is required"}
|
||||||
|
if not reference:
|
||||||
|
return {"success": False, "message": "reference is required"}
|
||||||
|
|
||||||
|
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 it
|
||||||
|
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 matching the reference
|
||||||
|
block_start = None
|
||||||
|
block_end = None
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
# Skip lib_symbols
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
i = b_end + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if block_start is None:
|
||||||
|
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]
|
||||||
|
|
||||||
|
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)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting schematic component: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_wire(self, params):
|
def _handle_add_schematic_wire(self, params):
|
||||||
"""Add a wire to a schematic using WireManager"""
|
"""Add a wire to a schematic using WireManager"""
|
||||||
logger.info("Adding wire to schematic")
|
logger.info("Adding wire to schematic")
|
||||||
@@ -1830,6 +1926,43 @@ class KiCADInterface:
|
|||||||
"message": f"Failed to suggest alternatives: {str(e)}",
|
"message": f"Failed to suggest alternatives: {str(e)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _handle_enrich_datasheets(self, params):
|
||||||
|
"""Enrich schematic Datasheet fields from LCSC numbers"""
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
schematic_path = params.get("schematic_path")
|
||||||
|
if not schematic_path:
|
||||||
|
return {"success": False, "message": "Missing schematic_path parameter"}
|
||||||
|
dry_run = params.get("dry_run", False)
|
||||||
|
manager = DatasheetManager()
|
||||||
|
return manager.enrich_schematic(Path(schematic_path), dry_run=dry_run)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error enriching datasheets: {e}", exc_info=True)
|
||||||
|
return {"success": False, "message": f"Failed to enrich datasheets: {str(e)}"}
|
||||||
|
|
||||||
|
def _handle_get_datasheet_url(self, params):
|
||||||
|
"""Return LCSC datasheet and product URLs for a part number"""
|
||||||
|
try:
|
||||||
|
lcsc = params.get("lcsc", "")
|
||||||
|
if not lcsc:
|
||||||
|
return {"success": False, "message": "Missing lcsc parameter"}
|
||||||
|
manager = DatasheetManager()
|
||||||
|
datasheet_url = manager.get_datasheet_url(lcsc)
|
||||||
|
product_url = manager.get_product_url(lcsc)
|
||||||
|
if not datasheet_url:
|
||||||
|
return {"success": False, "message": f"Invalid LCSC number: {lcsc}"}
|
||||||
|
norm = manager._normalize_lcsc(lcsc)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"lcsc": norm,
|
||||||
|
"datasheet_url": datasheet_url,
|
||||||
|
"product_url": product_url,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting datasheet URL: {e}", exc_info=True)
|
||||||
|
return {"success": False, "message": f"Failed to get datasheet URL: {str(e)}"}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point"""
|
"""Main entry point"""
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { registerSchematicTools } from "./tools/schematic.js";
|
|||||||
import { registerLibraryTools } from "./tools/library.js";
|
import { registerLibraryTools } from "./tools/library.js";
|
||||||
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
||||||
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
||||||
|
import { registerDatasheetTools } from "./tools/datasheet.js";
|
||||||
import { registerUITools } from "./tools/ui.js";
|
import { registerUITools } from "./tools/ui.js";
|
||||||
import { registerRouterTools } from "./tools/router.js";
|
import { registerRouterTools } from "./tools/router.js";
|
||||||
|
|
||||||
@@ -241,6 +242,7 @@ export class KiCADMcpServer {
|
|||||||
registerLibraryTools(this.server, this.callKicadScript.bind(this));
|
registerLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
||||||
|
registerDatasheetTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerUITools(this.server, this.callKicadScript.bind(this));
|
registerUITools(this.server, this.callKicadScript.bind(this));
|
||||||
|
|
||||||
// Register all resources
|
// Register all resources
|
||||||
|
|||||||
123
src/tools/datasheet.ts
Normal file
123
src/tools/datasheet.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Datasheet tools for KiCAD MCP server
|
||||||
|
*
|
||||||
|
* Enriches KiCAD schematic symbols with LCSC datasheet URLs.
|
||||||
|
* URL schema: https://www.lcsc.com/datasheet/<LCSC#>.pdf (no API key required)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export function registerDatasheetTools(
|
||||||
|
server: McpServer,
|
||||||
|
callKicadScript: Function,
|
||||||
|
) {
|
||||||
|
// ── enrich_datasheets ──────────────────────────────────────────────────────
|
||||||
|
server.tool(
|
||||||
|
"enrich_datasheets",
|
||||||
|
`Fill in missing Datasheet URLs in a KiCAD schematic using LCSC part numbers.
|
||||||
|
|
||||||
|
For every placed symbol that has:
|
||||||
|
• (property "LCSC" "C123456") set
|
||||||
|
• (property "Datasheet" "~") or empty
|
||||||
|
|
||||||
|
Sets the Datasheet field to:
|
||||||
|
https://www.lcsc.com/datasheet/C123456.pdf
|
||||||
|
|
||||||
|
The URL is then visible in KiCAD's footprint browser, symbol properties dialog,
|
||||||
|
and any tool that reads the standard KiCAD Datasheet field.
|
||||||
|
No API key or internet lookup required – the URL is constructed directly.
|
||||||
|
|
||||||
|
Use dry_run=true to preview changes without writing.`,
|
||||||
|
{
|
||||||
|
schematic_path: z
|
||||||
|
.string()
|
||||||
|
.describe("Path to the .kicad_sch file to enrich"),
|
||||||
|
dry_run: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(false)
|
||||||
|
.describe(
|
||||||
|
"If true, show what would be changed without writing to disk (default: false)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
async (args: { schematic_path: string; dry_run?: boolean }) => {
|
||||||
|
const result = await callKicadScript("enrich_datasheets", args);
|
||||||
|
if (result.success) {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
if (args.dry_run) {
|
||||||
|
lines.push(`[DRY RUN] Schematic: ${result.schematic}\n`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Schematic: ${result.schematic}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`✓ Updated: ${result.updated}`);
|
||||||
|
lines.push(` Already set: ${result.already_set}`);
|
||||||
|
lines.push(` No LCSC number: ${result.no_lcsc}`);
|
||||||
|
lines.push(` No field: ${result.no_datasheet_field}`);
|
||||||
|
|
||||||
|
if (result.details && result.details.length > 0) {
|
||||||
|
lines.push("\nComponents updated:");
|
||||||
|
for (const d of result.details) {
|
||||||
|
lines.push(` ${d.reference.padEnd(6)} ${d.lcsc.padEnd(12)} → ${d.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.updated === 0 && !args.dry_run) {
|
||||||
|
lines.push(
|
||||||
|
"\nNo changes needed – all LCSC components already have a Datasheet URL.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Failed to enrich datasheets: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── get_datasheet_url ──────────────────────────────────────────────────────
|
||||||
|
server.tool(
|
||||||
|
"get_datasheet_url",
|
||||||
|
`Get the LCSC datasheet URL for a component by LCSC number.
|
||||||
|
|
||||||
|
Returns the direct PDF URL and product page URL.
|
||||||
|
No network request – URL is constructed from the LCSC number alone.
|
||||||
|
|
||||||
|
Example: get_datasheet_url("C179739")
|
||||||
|
→ https://www.lcsc.com/datasheet/C179739.pdf`,
|
||||||
|
{
|
||||||
|
lcsc: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'LCSC part number, with or without "C" prefix (e.g. "C179739" or "179739")',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
async (args: { lcsc: string }) => {
|
||||||
|
const result = await callKicadScript("get_datasheet_url", { lcsc: args.lcsc });
|
||||||
|
if (result.success) {
|
||||||
|
const lines = [
|
||||||
|
`LCSC: ${result.lcsc}`,
|
||||||
|
`Datasheet PDF: ${result.datasheet_url}`,
|
||||||
|
`Product page: ${result.product_url}`,
|
||||||
|
];
|
||||||
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Invalid LCSC number: ${args.lcsc}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,12 +4,13 @@
|
|||||||
* Exports all tool registration functions
|
* Exports all tool registration functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { registerProjectTools } from './project.js';
|
export { registerProjectTools } from "./project.js";
|
||||||
export { registerBoardTools } from './board.js';
|
export { registerBoardTools } from "./board.js";
|
||||||
export { registerComponentTools } from './component.js';
|
export { registerComponentTools } from "./component.js";
|
||||||
export { registerRoutingTools } from './routing.js';
|
export { registerRoutingTools } from "./routing.js";
|
||||||
export { registerDesignRuleTools } from './design-rules.js';
|
export { registerDesignRuleTools } from "./design-rules.js";
|
||||||
export { registerExportTools } from './export.js';
|
export { registerExportTools } from "./export.js";
|
||||||
export { registerSchematicTools } from './schematic.js';
|
export { registerSchematicTools } from "./schematic.js";
|
||||||
export { registerLibraryTools } from './library.js';
|
export { registerLibraryTools } from "./library.js";
|
||||||
export { registerUITools } from './ui.js';
|
export { registerUITools } from "./ui.js";
|
||||||
|
export { registerDatasheetTools } from "./datasheet.js";
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
* Schematic tools for KiCAD MCP server
|
* Schematic tools for KiCAD MCP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerSchematicTools(server: McpServer, callKicadScript: Function) {
|
export function registerSchematicTools(
|
||||||
|
server: McpServer,
|
||||||
|
callKicadScript: Function,
|
||||||
|
) {
|
||||||
// Create schematic tool
|
// Create schematic tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"create_schematic",
|
"create_schematic",
|
||||||
@@ -17,12 +20,14 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
async (args: { name: string; path?: string }) => {
|
async (args: { name: string; path?: string }) => {
|
||||||
const result = await callKicadScript("create_schematic", args);
|
const result = await callKicadScript("create_schematic", args);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result, null, 2)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add component to schematic
|
// Add component to schematic
|
||||||
@@ -31,19 +36,32 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
"Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')",
|
"Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
symbol: z.string().describe("Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)"),
|
symbol: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)",
|
||||||
|
),
|
||||||
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
||||||
value: z.string().optional().describe("Component value"),
|
value: z.string().optional().describe("Component value"),
|
||||||
position: z.object({
|
position: z
|
||||||
x: z.number(),
|
.object({
|
||||||
y: z.number()
|
x: z.number(),
|
||||||
}).optional().describe("Position on schematic"),
|
y: z.number(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Position on schematic"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; symbol: string; reference: string; value?: string; position?: { x: number; y: number } }) => {
|
async (args: {
|
||||||
|
schematicPath: string;
|
||||||
|
symbol: string;
|
||||||
|
reference: string;
|
||||||
|
value?: string;
|
||||||
|
position?: { x: number; y: number };
|
||||||
|
}) => {
|
||||||
// Transform to what Python backend expects
|
// Transform to what Python backend expects
|
||||||
const [library, symbolName] = args.symbol.includes(':')
|
const [library, symbolName] = args.symbol.includes(":")
|
||||||
? args.symbol.split(':')
|
? args.symbol.split(":")
|
||||||
: ['Device', args.symbol];
|
: ["Device", args.symbol];
|
||||||
|
|
||||||
const transformed = {
|
const transformed = {
|
||||||
schematicPath: args.schematicPath,
|
schematicPath: args.schematicPath,
|
||||||
@@ -54,27 +72,73 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
value: args.value,
|
value: args.value,
|
||||||
// Python expects flat x, y not nested position
|
// Python expects flat x, y not nested position
|
||||||
x: args.position?.x ?? 0,
|
x: args.position?.x ?? 0,
|
||||||
y: args.position?.y ?? 0
|
y: args.position?.y ?? 0,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await callKicadScript("add_schematic_component", transformed);
|
const result = await callKicadScript(
|
||||||
|
"add_schematic_component",
|
||||||
|
transformed,
|
||||||
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`
|
type: "text",
|
||||||
}]
|
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Failed to add component: ${result.message || JSON.stringify(result)}`
|
type: "text",
|
||||||
}]
|
text: `Failed to add component: ${result.message || JSON.stringify(result)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete component from schematic
|
||||||
|
server.tool(
|
||||||
|
"delete_schematic_component",
|
||||||
|
`Remove a placed symbol from a KiCAD schematic (.kicad_sch).
|
||||||
|
|
||||||
|
This removes the symbol instance (the placed component) from the schematic.
|
||||||
|
It does NOT remove the symbol definition from lib_symbols.
|
||||||
|
|
||||||
|
Note: This tool operates on schematic files (.kicad_sch).
|
||||||
|
To remove a footprint from a PCB, use delete_component instead.`,
|
||||||
|
{
|
||||||
|
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component to remove (e.g. R1, U3)"),
|
||||||
|
},
|
||||||
|
async (args: { schematicPath: string; reference: string }) => {
|
||||||
|
const result = await callKicadScript("delete_schematic_component", args);
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully removed ${args.reference} from schematic`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Failed to remove component: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect components with wire
|
// Connect components with wire
|
||||||
@@ -82,24 +146,30 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
"add_wire",
|
"add_wire",
|
||||||
"Add a wire connection in the schematic",
|
"Add a wire connection in the schematic",
|
||||||
{
|
{
|
||||||
start: z.object({
|
start: z
|
||||||
x: z.number(),
|
.object({
|
||||||
y: z.number()
|
x: z.number(),
|
||||||
}).describe("Start position"),
|
y: z.number(),
|
||||||
end: z.object({
|
})
|
||||||
x: z.number(),
|
.describe("Start position"),
|
||||||
y: z.number()
|
end: z
|
||||||
}).describe("End position"),
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
})
|
||||||
|
.describe("End position"),
|
||||||
},
|
},
|
||||||
async (args: any) => {
|
async (args: any) => {
|
||||||
const result = await callKicadScript("add_wire", args);
|
const result = await callKicadScript("add_wire", args);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result, null, 2)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add pin-to-pin connection
|
// Add pin-to-pin connection
|
||||||
@@ -109,28 +179,42 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
||||||
sourcePin: z.string().describe("Source pin name/number (e.g., 1, 2, GND)"),
|
sourcePin: z
|
||||||
|
.string()
|
||||||
|
.describe("Source pin name/number (e.g., 1, 2, GND)"),
|
||||||
targetRef: z.string().describe("Target component reference (e.g., C1)"),
|
targetRef: z.string().describe("Target component reference (e.g., C1)"),
|
||||||
targetPin: z.string().describe("Target pin name/number (e.g., 1, 2, VCC)")
|
targetPin: z
|
||||||
|
.string()
|
||||||
|
.describe("Target pin name/number (e.g., 1, 2, VCC)"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; sourceRef: string; sourcePin: string; targetRef: string; targetPin: string }) => {
|
async (args: {
|
||||||
|
schematicPath: string;
|
||||||
|
sourceRef: string;
|
||||||
|
sourcePin: string;
|
||||||
|
targetRef: string;
|
||||||
|
targetPin: string;
|
||||||
|
}) => {
|
||||||
const result = await callKicadScript("add_schematic_connection", args);
|
const result = await callKicadScript("add_schematic_connection", args);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`
|
type: "text",
|
||||||
}]
|
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Failed to add connection: ${result.message || 'Unknown error'}`
|
type: "text",
|
||||||
}]
|
text: `Failed to add connection: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add net label
|
// Add net label
|
||||||
@@ -139,27 +223,40 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
"Add a net label to the schematic",
|
"Add a net label to the schematic",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
netName: z.string().describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
|
netName: z
|
||||||
position: z.array(z.number()).length(2).describe("Position [x, y] for the label")
|
.string()
|
||||||
|
.describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
|
||||||
|
position: z
|
||||||
|
.array(z.number())
|
||||||
|
.length(2)
|
||||||
|
.describe("Position [x, y] for the label"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; netName: string; position: number[] }) => {
|
async (args: {
|
||||||
|
schematicPath: string;
|
||||||
|
netName: string;
|
||||||
|
position: number[];
|
||||||
|
}) => {
|
||||||
const result = await callKicadScript("add_schematic_net_label", args);
|
const result = await callKicadScript("add_schematic_net_label", args);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Successfully added net label '${args.netName}' at position [${args.position}]`
|
type: "text",
|
||||||
}]
|
text: `Successfully added net label '${args.netName}' at position [${args.position}]`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Failed to add net label: ${result.message || 'Unknown error'}`
|
type: "text",
|
||||||
}]
|
text: `Failed to add net label: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect pin to net
|
// Connect pin to net
|
||||||
@@ -170,26 +267,35 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
componentRef: z.string().describe("Component reference (e.g., U1, R1)"),
|
componentRef: z.string().describe("Component reference (e.g., U1, R1)"),
|
||||||
pinName: z.string().describe("Pin name/number to connect"),
|
pinName: z.string().describe("Pin name/number to connect"),
|
||||||
netName: z.string().describe("Name of the net to connect to")
|
netName: z.string().describe("Name of the net to connect to"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; componentRef: string; pinName: string; netName: string }) => {
|
async (args: {
|
||||||
|
schematicPath: string;
|
||||||
|
componentRef: string;
|
||||||
|
pinName: string;
|
||||||
|
netName: string;
|
||||||
|
}) => {
|
||||||
const result = await callKicadScript("connect_to_net", args);
|
const result = await callKicadScript("connect_to_net", args);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`
|
type: "text",
|
||||||
}]
|
text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Failed to connect to net: ${result.message || 'Unknown error'}`
|
type: "text",
|
||||||
}]
|
text: `Failed to connect to net: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get net connections
|
// Get net connections
|
||||||
@@ -198,29 +304,33 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
"Get all connections for a named net",
|
"Get all connections for a named net",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
netName: z.string().describe("Name of the net to query")
|
netName: z.string().describe("Name of the net to query"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; netName: string }) => {
|
async (args: { schematicPath: string; netName: string }) => {
|
||||||
const result = await callKicadScript("get_net_connections", args);
|
const result = await callKicadScript("get_net_connections", args);
|
||||||
if (result.success && result.connections) {
|
if (result.success && result.connections) {
|
||||||
const connectionList = result.connections.map((conn: any) =>
|
const connectionList = result.connections
|
||||||
` - ${conn.component}/${conn.pin}`
|
.map((conn: any) => ` - ${conn.component}/${conn.pin}`)
|
||||||
).join('\n');
|
.join("\n");
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Net '${args.netName}' connections:\n${connectionList}`
|
type: "text",
|
||||||
}]
|
text: `Net '${args.netName}' connections:\n${connectionList}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Failed to get net connections: ${result.message || 'Unknown error'}`
|
type: "text",
|
||||||
}]
|
text: `Failed to get net connections: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate netlist
|
// Generate netlist
|
||||||
@@ -228,7 +338,7 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
"generate_netlist",
|
"generate_netlist",
|
||||||
"Generate a netlist from the schematic",
|
"Generate a netlist from the schematic",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file")
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string }) => {
|
async (args: { schematicPath: string }) => {
|
||||||
const result = await callKicadScript("generate_netlist", args);
|
const result = await callKicadScript("generate_netlist", args);
|
||||||
@@ -237,32 +347,37 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
const output = [
|
const output = [
|
||||||
`=== Netlist for ${args.schematicPath} ===`,
|
`=== Netlist for ${args.schematicPath} ===`,
|
||||||
`\nComponents (${netlist.components.length}):`,
|
`\nComponents (${netlist.components.length}):`,
|
||||||
...netlist.components.map((comp: any) =>
|
...netlist.components.map(
|
||||||
` ${comp.reference}: ${comp.value} (${comp.footprint || 'No footprint'})`
|
(comp: any) =>
|
||||||
|
` ${comp.reference}: ${comp.value} (${comp.footprint || "No footprint"})`,
|
||||||
),
|
),
|
||||||
`\nNets (${netlist.nets.length}):`,
|
`\nNets (${netlist.nets.length}):`,
|
||||||
...netlist.nets.map((net: any) => {
|
...netlist.nets.map((net: any) => {
|
||||||
const connections = net.connections.map((conn: any) =>
|
const connections = net.connections
|
||||||
`${conn.component}/${conn.pin}`
|
.map((conn: any) => `${conn.component}/${conn.pin}`)
|
||||||
).join(', ');
|
.join(", ");
|
||||||
return ` ${net.name}: ${connections}`;
|
return ` ${net.name}: ${connections}`;
|
||||||
})
|
}),
|
||||||
].join('\n');
|
].join("\n");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: output
|
type: "text",
|
||||||
}]
|
text: output,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Failed to generate netlist: ${result.message || 'Unknown error'}`
|
type: "text",
|
||||||
}]
|
text: `Failed to generate netlist: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user