diff --git a/.gitignore b/.gitignore
index 21b91f1..c13e2ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,6 +89,7 @@ Desktop.ini
# Generated local config files (contain machine-specific paths)
windows-mcp-config.json
+.mcp.json
# Personal notes / local contributions (not for upstream)
myContribution/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d00ab29..d251130 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+> **⚠️ Research Project — No Stability Guarantees**
+> This is an unstable, experimental research project. APIs, tool names, parameters,
+> and file formats may change at any time without notice. Back-compatibility is
+> explicitly **not** preserved between versions.
+
All notable changes to the KiCAD MCP Server project are documented here.
## [Unreleased]
@@ -71,6 +76,14 @@ All notable changes to the KiCAD MCP Server project are documented here.
no-op removal, special-character escaping, UUID preservation, and the two
new convenience tools.
+### Removed
+
+- `add_schematic_junction` MCP tool has been removed. Junctions are now
+ inserted and removed automatically via `WireManager.sync_junctions` whenever
+ wires are added, deleted, or moved.
+- Junction placement is pin-aware: `sync_junctions` consults component pin
+ positions so that T-junctions at component pins are correctly recognised.
+
---
## [2.2.3] - 2026-03-11
diff --git a/python/commands/wire_manager.py b/python/commands/wire_manager.py
index f52662d..20c3043 100644
--- a/python/commands/wire_manager.py
+++ b/python/commands/wire_manager.py
@@ -32,6 +32,12 @@ _SYM_WIDTH = Symbol("width")
_SYM_TYPE = Symbol("type")
_SYM_UUID = Symbol("uuid")
_SYM_SHEET_INSTANCES = Symbol("sheet_instances")
+_SYM_JUNCTION = Symbol("junction")
+_SYM_LIB_SYMBOLS = Symbol("lib_symbols")
+_SYM_LIB_ID = Symbol("lib_id")
+_SYM_MIRROR = Symbol("mirror")
+_SYM_PIN = Symbol("pin")
+_IU_PER_MM = 10000
def _find_insertion_point(content: str) -> int:
@@ -176,6 +182,8 @@ class WireManager:
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected wire from {start_point} to {end_point}")
+ WireManager.sync_junctions(sch_data)
+
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
@@ -252,6 +260,8 @@ class WireManager:
f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline"
)
+ WireManager.sync_junctions(sch_data)
+
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
@@ -450,74 +460,266 @@ class WireManager:
return splits
@staticmethod
- def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
+ def _collect_wire_endpoints(sch_data: list) -> List[Tuple[float, float]]:
+ """Return all (x, y) endpoints for every wire in sch_data."""
+ endpoints: List[Tuple[float, float]] = []
+ for item in sch_data:
+ parsed = WireManager._parse_wire(item)
+ if parsed is not None:
+ (x1, y1), (x2, y2), _, _ = parsed
+ endpoints.append((x1, y1))
+ endpoints.append((x2, y2))
+ return endpoints
+
+ @staticmethod
+ def _get_existing_junctions(sch_data: list) -> dict:
+ """Return {(iu_x, iu_y): index_in_sch_data} for every junction element."""
+ result: dict = {}
+ for i, item in enumerate(sch_data):
+ if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_JUNCTION):
+ continue
+ at_entry = next(
+ (p for p in item[1:] if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_AT),
+ None,
+ )
+ if at_entry is None:
+ continue
+ x, y = float(at_entry[1]), float(at_entry[2])
+ result[(round(x * _IU_PER_MM), round(y * _IU_PER_MM))] = i
+ return result
+
+ @staticmethod
+ def _make_junction_sexp(x: float, y: float, diameter: float = 0) -> list:
+ return [
+ _SYM_JUNCTION,
+ [_SYM_AT, x, y],
+ [Symbol("diameter"), diameter],
+ [Symbol("color"), 0, 0, 0, 0],
+ [_SYM_UUID, str(uuid.uuid4())],
+ ]
+
+ # Regex to parse sub-unit names like "LM324_2_1" → (base="LM324", unit=2, style=1)
+ # The sub-unit suffix is __