This is page 2 of 2. Use http://codebase.md/mixelpixx/kicad-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .github
│ └── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG_2025-10-26.md
├── config
│ ├── claude-desktop-config.json
│ ├── default-config.json
│ ├── linux-config.example.json
│ ├── macos-config.example.json
│ └── windows-config.example.json
├── CONTRIBUTING.md
├── LICENSE
├── package-json.json
├── package-lock.json
├── package.json
├── pytest.ini
├── python
│ ├── commands
│ │ ├── __init__.py
│ │ ├── board
│ │ │ ├── __init__.py
│ │ │ ├── layers.py
│ │ │ ├── outline.py
│ │ │ ├── size.py
│ │ │ └── view.py
│ │ ├── board.py
│ │ ├── component_schematic.py
│ │ ├── component.py
│ │ ├── connection_schematic.py
│ │ ├── design_rules.py
│ │ ├── export.py
│ │ ├── library_schematic.py
│ │ ├── project.py
│ │ ├── routing.py
│ │ └── schematic.py
│ ├── kicad_api
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── factory.py
│ │ ├── ipc_backend.py
│ │ └── swig_backend.py
│ ├── kicad_interface.py
│ ├── requirements.txt
│ └── utils
│ ├── __init__.py
│ ├── kicad_process.py
│ └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│ ├── auto_refresh_kicad.sh
│ └── install-linux.sh
├── src
│ ├── config.ts
│ ├── index.ts
│ ├── kicad-server.ts
│ ├── logger.ts
│ ├── prompts
│ │ ├── component.ts
│ │ ├── design.ts
│ │ ├── index.ts
│ │ └── routing.ts
│ ├── resources
│ │ ├── board.ts
│ │ ├── component.ts
│ │ ├── index.ts
│ │ ├── library.ts
│ │ └── project.ts
│ ├── server.ts
│ ├── tools
│ │ ├── board.ts
│ │ ├── component.ts
│ │ ├── component.txt
│ │ ├── design-rules.ts
│ │ ├── export.ts
│ │ ├── index.ts
│ │ ├── project.ts
│ │ ├── routing.ts
│ │ ├── schematic.ts
│ │ └── ui.ts
│ └── utils
│ └── resource-helpers.ts
├── tests
│ ├── __init__.py
│ └── test_platform_helper.py
├── tsconfig-json.json
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/python/commands/design_rules.py:
--------------------------------------------------------------------------------
```python
"""
Design rules command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
logger = logging.getLogger('kicad_interface')
class DesignRuleCommands:
"""Handles design rule checking and configuration"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
def set_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Set design rules for the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
design_settings = self.board.GetDesignSettings()
# Convert mm to nanometers for KiCAD internal units
scale = 1000000 # mm to nm
# Set clearance
if "clearance" in params:
design_settings.SetMinClearance(int(params["clearance"] * scale))
# Set track width
if "trackWidth" in params:
design_settings.SetCurrentTrackWidth(int(params["trackWidth"] * scale))
# Set via settings
if "viaDiameter" in params:
design_settings.SetCurrentViaSize(int(params["viaDiameter"] * scale))
if "viaDrill" in params:
design_settings.SetCurrentViaDrill(int(params["viaDrill"] * scale))
# Set micro via settings
if "microViaDiameter" in params:
design_settings.SetCurrentMicroViaSize(int(params["microViaDiameter"] * scale))
if "microViaDrill" in params:
design_settings.SetCurrentMicroViaDrill(int(params["microViaDrill"] * scale))
# Set minimum values
if "minTrackWidth" in params:
design_settings.m_TrackMinWidth = int(params["minTrackWidth"] * scale)
if "minViaDiameter" in params:
design_settings.m_ViasMinSize = int(params["minViaDiameter"] * scale)
if "minViaDrill" in params:
design_settings.m_ViasMinDrill = int(params["minViaDrill"] * scale)
if "minMicroViaDiameter" in params:
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
if "minMicroViaDrill" in params:
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
# Set hole diameter
if "minHoleDiameter" in params:
design_settings.m_MinHoleDiameter = int(params["minHoleDiameter"] * scale)
# Set courtyard settings
if "requireCourtyard" in params:
design_settings.m_RequireCourtyards = params["requireCourtyard"]
if "courtyardClearance" in params:
design_settings.m_CourtyardMinClearance = int(params["courtyardClearance"] * scale)
return {
"success": True,
"message": "Updated design rules",
"rules": {
"clearance": design_settings.GetMinClearance() / scale,
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
"microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale,
"microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale,
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
"minViaDiameter": design_settings.m_ViasMinSize / scale,
"minViaDrill": design_settings.m_ViasMinDrill / scale,
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
"minHoleDiameter": design_settings.m_MinHoleDiameter / scale,
"requireCourtyard": design_settings.m_RequireCourtyards,
"courtyardClearance": design_settings.m_CourtyardMinClearance / scale
}
}
except Exception as e:
logger.error(f"Error setting design rules: {str(e)}")
return {
"success": False,
"message": "Failed to set design rules",
"errorDetails": str(e)
}
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get current design rules"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
design_settings = self.board.GetDesignSettings()
scale = 1000000 # nm to mm
return {
"success": True,
"rules": {
"clearance": design_settings.GetMinClearance() / scale,
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
"microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale,
"microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale,
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
"minViaDiameter": design_settings.m_ViasMinSize / scale,
"minViaDrill": design_settings.m_ViasMinDrill / scale,
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
"minHoleDiameter": design_settings.m_MinHoleDiameter / scale,
"requireCourtyard": design_settings.m_RequireCourtyards,
"courtyardClearance": design_settings.m_CourtyardMinClearance / scale
}
}
except Exception as e:
logger.error(f"Error getting design rules: {str(e)}")
return {
"success": False,
"message": "Failed to get design rules",
"errorDetails": str(e)
}
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Run Design Rule Check"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
report_path = params.get("reportPath")
# Create DRC runner
drc = pcbnew.DRC(self.board)
# Run DRC
drc.Run()
# Get violations
violations = []
for marker in drc.GetMarkers():
violations.append({
"type": marker.GetErrorCode(),
"severity": "error",
"message": marker.GetDescription(),
"location": {
"x": marker.GetPos().x / 1000000,
"y": marker.GetPos().y / 1000000,
"unit": "mm"
}
})
# Save report if path provided
if report_path:
report_path = os.path.abspath(os.path.expanduser(report_path))
drc.WriteReport(report_path)
return {
"success": True,
"message": f"Found {len(violations)} DRC violations",
"violations": violations,
"reportPath": report_path if report_path else None
}
except Exception as e:
logger.error(f"Error running DRC: {str(e)}")
return {
"success": False,
"message": "Failed to run DRC",
"errorDetails": str(e)
}
def get_drc_violations(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get list of DRC violations"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
severity = params.get("severity", "all")
# Get DRC markers
violations = []
for marker in self.board.GetDRCMarkers():
violation = {
"type": marker.GetErrorCode(),
"severity": "error", # KiCAD DRC markers are always errors
"message": marker.GetDescription(),
"location": {
"x": marker.GetPos().x / 1000000,
"y": marker.GetPos().y / 1000000,
"unit": "mm"
}
}
# Filter by severity if specified
if severity == "all" or severity == violation["severity"]:
violations.append(violation)
return {
"success": True,
"violations": violations
}
except Exception as e:
logger.error(f"Error getting DRC violations: {str(e)}")
return {
"success": False,
"message": "Failed to get DRC violations",
"errorDetails": str(e)
}
```
--------------------------------------------------------------------------------
/src/prompts/routing.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Routing prompts for KiCAD MCP server
*
* These prompts guide the LLM in providing assistance with routing-related tasks
* in KiCAD PCB design.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { logger } from '../logger.js';
/**
* Register routing prompts with the MCP server
*
* @param server MCP server instance
*/
export function registerRoutingPrompts(server: McpServer): void {
logger.info('Registering routing prompts');
// ------------------------------------------------------
// Routing Strategy Prompt
// ------------------------------------------------------
server.prompt(
"routing_strategy",
{
board_info: z.string().describe("Information about the PCB board, including dimensions, layer stack-up, and components")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping to develop a routing strategy for a PCB design. Here's information about the board:
{{board_info}}
Consider the following aspects when developing your routing strategy:
1. Signal Integrity:
- Group related signals and keep them close
- Minimize trace length for high-speed signals
- Consider differential pair routing for appropriate signals
- Avoid right-angle bends in traces
2. Power Distribution:
- Use appropriate trace widths for power and ground
- Consider using power planes for better distribution
- Place decoupling capacitors close to ICs
3. EMI/EMC Considerations:
- Keep digital and analog sections separated
- Consider ground plane partitioning
- Minimize loop areas for sensitive signals
4. Manufacturing Constraints:
- Adhere to minimum trace width and spacing requirements
- Consider via size and placement restrictions
- Account for soldermask and silkscreen limitations
5. Layer Stack-up Utilization:
- Determine which signals go on which layers
- Plan for layer transitions (vias)
- Consider impedance control requirements
Provide a comprehensive routing strategy that addresses these aspects, with specific recommendations for this particular board design.`
}
}
]
})
);
// ------------------------------------------------------
// Differential Pair Routing Prompt
// ------------------------------------------------------
server.prompt(
"differential_pair_routing",
{
differential_pairs: z.string().describe("Information about the differential pairs to be routed, including signal names, source and destination components, and speed/frequency requirements")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping with routing differential pairs on a PCB. Here's information about the differential pairs:
{{differential_pairs}}
When routing differential pairs, follow these best practices:
1. Length Matching:
- Keep both traces in each pair the same length
- Maintain consistent spacing between the traces
- Use serpentine routing (meanders) for length matching when necessary
2. Impedance Control:
- Maintain consistent trace width and spacing to control impedance
- Consider the layer stack-up and dielectric properties
- Avoid changing layers if possible; when necessary, use symmetrical via pairs
3. Coupling and Crosstalk:
- Keep differential pairs tightly coupled to each other
- Maintain adequate spacing between different differential pairs
- Route away from single-ended signals that could cause interference
4. Reference Planes:
- Route over continuous reference planes
- Avoid splits in reference planes under differential pairs
- Consider the return path for the signals
5. Termination:
- Plan for proper termination at the ends of the pairs
- Consider the need for series or parallel termination resistors
- Place termination components close to the endpoints
Based on the provided information, suggest specific routing approaches for these differential pairs, including recommended trace width, spacing, and any special considerations for this particular design.`
}
}
]
})
);
// ------------------------------------------------------
// High-Speed Routing Prompt
// ------------------------------------------------------
server.prompt(
"high_speed_routing",
{
high_speed_signals: z.string().describe("Information about the high-speed signals to be routed, including signal names, source and destination components, and speed/frequency requirements")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping with routing high-speed signals on a PCB. Here's information about the high-speed signals:
{{high_speed_signals}}
When routing high-speed signals, consider these critical factors:
1. Impedance Control:
- Maintain consistent trace width to control impedance
- Use controlled impedance calculations based on layer stack-up
- Consider microstrip vs. stripline routing depending on signal requirements
2. Signal Integrity:
- Minimize trace length to reduce propagation delay
- Avoid sharp corners (use 45° angles or curves)
- Minimize vias to reduce discontinuities
- Consider using teardrops at pad connections
3. Crosstalk Mitigation:
- Maintain adequate spacing between high-speed traces
- Use ground traces or planes for isolation
- Cross traces at 90° when traces must cross on adjacent layers
4. Return Path Management:
- Ensure continuous return path under the signal
- Avoid reference plane splits under high-speed signals
- Use ground vias near signal vias for return path continuity
5. Termination and Loading:
- Plan for proper termination (series, parallel, AC, etc.)
- Consider transmission line effects
- Account for capacitive loading from components and vias
Based on the provided information, suggest specific routing approaches for these high-speed signals, including recommended trace width, layer assignment, and any special considerations for this particular design.`
}
}
]
})
);
// ------------------------------------------------------
// Power Distribution Prompt
// ------------------------------------------------------
server.prompt(
"power_distribution",
{
power_requirements: z.string().describe("Information about the power requirements, including voltage rails, current needs, and components requiring power")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping with designing the power distribution network for a PCB. Here's information about the power requirements:
{{power_requirements}}
Consider these key aspects of power distribution network design:
1. Power Planes vs. Traces:
- Determine when to use power planes versus wide traces
- Consider current requirements and voltage drop
- Plan the layer stack-up to accommodate power distribution
2. Decoupling Strategy:
- Place decoupling capacitors close to ICs
- Use appropriate capacitor values and types
- Consider high-frequency and bulk decoupling needs
- Plan for power entry filtering
3. Current Capacity:
- Calculate trace widths based on current requirements
- Consider thermal issues and heat dissipation
- Plan for current return paths
4. Voltage Regulation:
- Place regulators strategically
- Consider thermal management for regulators
- Plan feedback paths for regulators
5. EMI/EMC Considerations:
- Minimize loop areas
- Keep power and ground planes closely coupled
- Consider filtering for noise-sensitive circuits
Based on the provided information, suggest a comprehensive power distribution strategy, including specific recommendations for plane usage, trace widths, decoupling, and any special considerations for this particular design.`
}
}
]
})
);
// ------------------------------------------------------
// Via Usage Prompt
// ------------------------------------------------------
server.prompt(
"via_usage",
{
board_info: z.string().describe("Information about the PCB board, including layer count, thickness, and design requirements")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping with planning via usage in a PCB design. Here's information about the board:
{{board_info}}
Consider these important aspects of via usage:
1. Via Types:
- Through-hole vias (span all layers)
- Blind vias (connect outer layer to inner layer)
- Buried vias (connect inner layers only)
- Microvias (small diameter vias for HDI designs)
2. Manufacturing Constraints:
- Minimum via diameter and drill size
- Aspect ratio limitations (board thickness to hole diameter)
- Annular ring requirements
- Via-in-pad considerations and special processing
3. Signal Integrity Impact:
- Capacitive loading effects of vias
- Impedance discontinuities
- Stub effects in through-hole vias
- Strategies to minimize via impact on high-speed signals
4. Thermal Considerations:
- Using vias for thermal relief
- Via patterns for heat dissipation
- Thermal via sizing and spacing
5. Design Optimization:
- Via fanout strategies
- Sharing vias between signals vs. dedicated vias
- Via placement to minimize trace length
- Tenting and plugging options
Based on the provided information, recommend appropriate via strategies for this PCB design, including specific via types, sizes, and placement guidelines.`
}
}
]
})
);
logger.info('Routing prompts registered');
}
```
--------------------------------------------------------------------------------
/python/utils/kicad_process.py:
--------------------------------------------------------------------------------
```python
"""
KiCAD Process Management Utilities
Detects if KiCAD is running and provides auto-launch functionality.
"""
import os
import subprocess
import logging
import platform
import time
from pathlib import Path
from typing import Optional, List
logger = logging.getLogger(__name__)
class KiCADProcessManager:
"""Manages KiCAD process detection and launching"""
@staticmethod
def is_running() -> bool:
"""
Check if KiCAD is currently running
Returns:
True if KiCAD process found, False otherwise
"""
system = platform.system()
try:
if system == "Linux":
# Check for actual pcbnew/kicad binaries (not python scripts)
# Use exact process name matching to avoid matching our own kicad_interface.py
result = subprocess.run(
["pgrep", "-x", "pcbnew|kicad"],
capture_output=True,
text=True
)
if result.returncode == 0:
return True
# Also check with -f for full path matching, but exclude our script
result = subprocess.run(
["pgrep", "-f", "/pcbnew|/kicad"],
capture_output=True,
text=True
)
# Double-check it's not our own process
if result.returncode == 0:
pids = result.stdout.strip().split('\n')
for pid in pids:
try:
cmdline = subprocess.run(
["ps", "-p", pid, "-o", "command="],
capture_output=True,
text=True
)
if "kicad_interface.py" not in cmdline.stdout:
return True
except:
pass
return False
elif system == "Darwin": # macOS
result = subprocess.run(
["pgrep", "-f", "KiCad|pcbnew"],
capture_output=True,
text=True
)
return result.returncode == 0
elif system == "Windows":
result = subprocess.run(
["tasklist", "/FI", "IMAGENAME eq pcbnew.exe"],
capture_output=True,
text=True
)
return "pcbnew.exe" in result.stdout
else:
logger.warning(f"Process detection not implemented for {system}")
return False
except Exception as e:
logger.error(f"Error checking if KiCAD is running: {e}")
return False
@staticmethod
def get_executable_path() -> Optional[Path]:
"""
Get path to KiCAD executable
Returns:
Path to pcbnew/kicad executable, or None if not found
"""
system = platform.system()
# Try to find executable in PATH first
for cmd in ["pcbnew", "kicad"]:
result = subprocess.run(
["which", cmd] if system != "Windows" else ["where", cmd],
capture_output=True,
text=True
)
if result.returncode == 0:
path = result.stdout.strip().split("\n")[0]
logger.info(f"Found KiCAD executable: {path}")
return Path(path)
# Platform-specific default paths
if system == "Linux":
candidates = [
Path("/usr/bin/pcbnew"),
Path("/usr/local/bin/pcbnew"),
Path("/usr/bin/kicad"),
]
elif system == "Darwin": # macOS
candidates = [
Path("/Applications/KiCad/KiCad.app/Contents/MacOS/kicad"),
Path("/Applications/KiCad/pcbnew.app/Contents/MacOS/pcbnew"),
]
elif system == "Windows":
candidates = [
Path("C:/Program Files/KiCad/9.0/bin/pcbnew.exe"),
Path("C:/Program Files/KiCad/8.0/bin/pcbnew.exe"),
Path("C:/Program Files (x86)/KiCad/9.0/bin/pcbnew.exe"),
]
else:
candidates = []
for path in candidates:
if path.exists():
logger.info(f"Found KiCAD executable: {path}")
return path
logger.warning("Could not find KiCAD executable")
return None
@staticmethod
def launch(project_path: Optional[Path] = None, wait_for_start: bool = True) -> bool:
"""
Launch KiCAD PCB Editor
Args:
project_path: Optional path to .kicad_pcb file to open
wait_for_start: Wait for process to start before returning
Returns:
True if launch successful, False otherwise
"""
try:
# Check if already running
if KiCADProcessManager.is_running():
logger.info("KiCAD is already running")
return True
# Find executable
exe_path = KiCADProcessManager.get_executable_path()
if not exe_path:
logger.error("Cannot launch KiCAD: executable not found")
return False
# Build command
cmd = [str(exe_path)]
if project_path:
cmd.append(str(project_path))
logger.info(f"Launching KiCAD: {' '.join(cmd)}")
# Launch process in background
system = platform.system()
if system == "Windows":
# Windows: Use CREATE_NEW_PROCESS_GROUP to detach
subprocess.Popen(
cmd,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
# Unix: Use nohup or start in background
subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
# Wait for process to start
if wait_for_start:
logger.info("Waiting for KiCAD to start...")
for i in range(10): # Wait up to 5 seconds
time.sleep(0.5)
if KiCADProcessManager.is_running():
logger.info("✓ KiCAD started successfully")
return True
logger.warning("KiCAD process not detected after launch")
# Return True anyway, it might be starting
return True
return True
except Exception as e:
logger.error(f"Error launching KiCAD: {e}")
return False
@staticmethod
def get_process_info() -> List[dict]:
"""
Get information about running KiCAD processes
Returns:
List of process info dicts with pid, name, and command
"""
system = platform.system()
processes = []
try:
if system in ["Linux", "Darwin"]:
result = subprocess.run(
["ps", "aux"],
capture_output=True,
text=True
)
for line in result.stdout.split("\n"):
# Only match actual KiCAD binaries, not our MCP server processes
if ("pcbnew" in line.lower() or "kicad" in line.lower()) and "kicad_interface.py" not in line and "grep" not in line:
# More specific check: must have /pcbnew or /kicad in the path
if "/pcbnew" in line or "/kicad" in line or "KiCad.app" in line:
parts = line.split()
if len(parts) >= 11:
processes.append({
"pid": parts[1],
"name": parts[10],
"command": " ".join(parts[10:])
})
elif system == "Windows":
result = subprocess.run(
["tasklist", "/V", "/FO", "CSV"],
capture_output=True,
text=True
)
import csv
reader = csv.reader(result.stdout.split("\n"))
for row in reader:
if row and len(row) > 0:
if "pcbnew" in row[0].lower() or "kicad" in row[0].lower():
processes.append({
"pid": row[1] if len(row) > 1 else "unknown",
"name": row[0],
"command": row[0]
})
except Exception as e:
logger.error(f"Error getting process info: {e}")
return processes
def check_and_launch_kicad(project_path: Optional[Path] = None, auto_launch: bool = True) -> dict:
"""
Check if KiCAD is running and optionally launch it
Args:
project_path: Optional path to .kicad_pcb file to open
auto_launch: If True, launch KiCAD if not running
Returns:
Dict with status information
"""
manager = KiCADProcessManager()
is_running = manager.is_running()
if is_running:
processes = manager.get_process_info()
return {
"running": True,
"launched": False,
"processes": processes,
"message": "KiCAD is already running"
}
if not auto_launch:
return {
"running": False,
"launched": False,
"processes": [],
"message": "KiCAD is not running (auto-launch disabled)"
}
# Try to launch
logger.info("KiCAD not detected, attempting to launch...")
success = manager.launch(project_path)
return {
"running": success,
"launched": success,
"processes": manager.get_process_info() if success else [],
"message": "KiCAD launched successfully" if success else "Failed to launch KiCAD",
"project": str(project_path) if project_path else None
}
```
--------------------------------------------------------------------------------
/src/prompts/design.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Design prompts for KiCAD MCP server
*
* These prompts guide the LLM in providing assistance with general PCB design tasks
* in KiCAD.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { logger } from '../logger.js';
/**
* Register design prompts with the MCP server
*
* @param server MCP server instance
*/
export function registerDesignPrompts(server: McpServer): void {
logger.info('Registering design prompts');
// ------------------------------------------------------
// PCB Layout Review Prompt
// ------------------------------------------------------
server.prompt(
"pcb_layout_review",
{
pcb_design_info: z.string().describe("Information about the current PCB design, including board dimensions, layer stack-up, component placement, and routing details")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping to review a PCB layout for potential issues and improvements. Here's information about the current PCB design:
{{pcb_design_info}}
When reviewing the PCB layout, consider these key areas:
1. Component Placement:
- Logical grouping of related components
- Orientation for efficient routing
- Thermal considerations for heat-generating components
- Mechanical constraints (mounting holes, connectors at edges)
- Accessibility for testing and rework
2. Signal Integrity:
- Trace lengths for critical signals
- Differential pair routing quality
- Potential crosstalk issues
- Return path continuity
- Decoupling capacitor placement
3. Power Distribution:
- Adequate copper for power rails
- Power plane design and continuity
- Decoupling strategy effectiveness
- Voltage regulator thermal management
4. EMI/EMC Considerations:
- Ground plane integrity
- Potential antenna effects
- Shielding requirements
- Loop area minimization
- Edge radiation control
5. Manufacturing and Assembly:
- DFM (Design for Manufacturing) issues
- DFA (Design for Assembly) considerations
- Testability features
- Silkscreen clarity and usefulness
- Solder mask considerations
Based on the provided information, identify potential issues and suggest specific improvements to enhance the PCB design.`
}
}
]
})
);
// ------------------------------------------------------
// Layer Stack-up Planning Prompt
// ------------------------------------------------------
server.prompt(
"layer_stackup_planning",
{
design_requirements: z.string().describe("Information about the PCB design requirements, including signal types, speed/frequency, power requirements, and any special considerations")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping to plan an appropriate layer stack-up for a PCB design. Here's information about the design requirements:
{{design_requirements}}
When planning a PCB layer stack-up, consider these important factors:
1. Signal Integrity Requirements:
- Controlled impedance needs
- High-speed signal routing
- EMI/EMC considerations
- Crosstalk mitigation
2. Power Distribution Needs:
- Current requirements for power rails
- Power integrity considerations
- Decoupling effectiveness
- Thermal management
3. Manufacturing Constraints:
- Fabrication capabilities and limitations
- Cost considerations
- Available materials and their properties
- Standard vs. specialized processes
4. Layer Types and Arrangement:
- Signal layers
- Power and ground planes
- Mixed signal/plane layers
- Microstrip vs. stripline configurations
5. Material Selection:
- Dielectric constant (Er) requirements
- Loss tangent considerations for high-speed
- Thermal properties
- Mechanical stability
Based on the provided requirements, recommend an appropriate layer stack-up, including the number of layers, their arrangement, material specifications, and thickness parameters. Explain the rationale behind your recommendations.`
}
}
]
})
);
// ------------------------------------------------------
// Design Rule Development Prompt
// ------------------------------------------------------
server.prompt(
"design_rule_development",
{
project_requirements: z.string().describe("Information about the PCB project requirements, including technology, speed/frequency, manufacturing capabilities, and any special considerations")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping to develop appropriate design rules for a PCB project. Here's information about the project requirements:
{{project_requirements}}
When developing PCB design rules, consider these key areas:
1. Clearance Rules:
- Minimum spacing between copper features
- Different clearance requirements for different net classes
- High-voltage clearance requirements
- Polygon pour clearances
2. Width Rules:
- Minimum trace widths for signal nets
- Power trace width requirements based on current
- Differential pair width and spacing
- Net class-specific width rules
3. Via Rules:
- Minimum via size and drill diameter
- Via annular ring requirements
- Microvias and buried/blind via specifications
- Via-in-pad rules
4. Manufacturing Constraints:
- Minimum hole size
- Aspect ratio limitations
- Soldermask and silkscreen constraints
- Edge clearances
5. Special Requirements:
- Impedance control specifications
- High-speed routing constraints
- Thermal relief parameters
- Teardrop specifications
Based on the provided project requirements, recommend a comprehensive set of design rules that will ensure signal integrity, manufacturability, and reliability of the PCB. Provide specific values where appropriate and explain the rationale behind critical rules.`
}
}
]
})
);
// ------------------------------------------------------
// Component Selection Guidance Prompt
// ------------------------------------------------------
server.prompt(
"component_selection_guidance",
{
circuit_requirements: z.string().describe("Information about the circuit requirements, including functionality, performance needs, operating environment, and any special considerations")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping with component selection for a PCB design. Here's information about the circuit requirements:
{{circuit_requirements}}
When selecting components for a PCB design, consider these important factors:
1. Electrical Specifications:
- Voltage and current ratings
- Power handling capabilities
- Speed/frequency requirements
- Noise and precision considerations
- Operating temperature range
2. Package and Footprint:
- Space constraints on the PCB
- Thermal dissipation requirements
- Manual vs. automated assembly
- Inspection and rework considerations
- Available footprint libraries
3. Availability and Sourcing:
- Multiple source options
- Lead time considerations
- Lifecycle status (new, mature, end-of-life)
- Cost considerations
- Minimum order quantities
4. Reliability and Quality:
- Industrial vs. commercial vs. automotive grade
- Expected lifetime of the product
- Environmental conditions
- Compliance with relevant standards
5. Special Considerations:
- EMI/EMC performance
- Thermal characteristics
- Moisture sensitivity
- RoHS/REACH compliance
- Special handling requirements
Based on the provided circuit requirements, recommend appropriate component types, packages, and specific considerations for this design. Provide guidance on critical component selections and explain the rationale behind your recommendations.`
}
}
]
})
);
// ------------------------------------------------------
// PCB Design Optimization Prompt
// ------------------------------------------------------
server.prompt(
"pcb_design_optimization",
{
design_info: z.string().describe("Information about the current PCB design, including board dimensions, layer stack-up, component placement, and routing details"),
optimization_goals: z.string().describe("Specific goals for optimization, such as performance improvement, cost reduction, size reduction, or manufacturability enhancement")
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `You're helping to optimize a PCB design. Here's information about the current design and optimization goals:
{{design_info}}
{{optimization_goals}}
When optimizing a PCB design, consider these key areas based on the stated goals:
1. Performance Optimization:
- Critical signal path length reduction
- Impedance control improvement
- Decoupling strategy enhancement
- Thermal management improvement
- EMI/EMC reduction techniques
2. Manufacturability Optimization:
- DFM rule compliance
- Testability improvements
- Assembly process simplification
- Yield improvement opportunities
- Tolerance and variation management
3. Cost Optimization:
- Board size reduction opportunities
- Layer count optimization
- Component consolidation
- Alternative component options
- Panelization efficiency
4. Reliability Optimization:
- Stress point identification and mitigation
- Environmental robustness improvements
- Failure mode mitigation
- Margin analysis and improvement
- Redundancy considerations
5. Space/Size Optimization:
- Component placement density
- 3D space utilization
- Flex and rigid-flex opportunities
- Alternative packaging approaches
- Connector and interface optimization
Based on the provided information and optimization goals, suggest specific, actionable improvements to the PCB design. Prioritize your recommendations based on their potential impact and implementation feasibility.`
}
}
]
})
);
logger.info('Design prompts registered');
}
```
--------------------------------------------------------------------------------
/src/tools/board.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Board management tools for KiCAD MCP server
*
* These tools handle board setup, layer management, and board properties
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { logger } from '../logger.js';
// Command function type for KiCAD script calls
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
/**
* Register board management tools with the MCP server
*
* @param server MCP server instance
* @param callKicadScript Function to call KiCAD script commands
*/
export function registerBoardTools(server: McpServer, callKicadScript: CommandFunction): void {
logger.info('Registering board management tools');
// ------------------------------------------------------
// Set Board Size Tool
// ------------------------------------------------------
server.tool(
"set_board_size",
{
width: z.number().describe("Board width"),
height: z.number().describe("Board height"),
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
},
async ({ width, height, unit }) => {
logger.debug(`Setting board size to ${width}x${height} ${unit}`);
const result = await callKicadScript("set_board_size", {
width,
height,
unit
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Add Layer Tool
// ------------------------------------------------------
server.tool(
"add_layer",
{
name: z.string().describe("Layer name"),
type: z.enum([
"copper", "technical", "user", "signal"
]).describe("Layer type"),
position: z.enum([
"top", "bottom", "inner"
]).describe("Layer position"),
number: z.number().optional().describe("Layer number (for inner layers)")
},
async ({ name, type, position, number }) => {
logger.debug(`Adding ${type} layer: ${name}`);
const result = await callKicadScript("add_layer", {
name,
type,
position,
number
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Set Active Layer Tool
// ------------------------------------------------------
server.tool(
"set_active_layer",
{
layer: z.string().describe("Layer name to set as active")
},
async ({ layer }) => {
logger.debug(`Setting active layer to: ${layer}`);
const result = await callKicadScript("set_active_layer", { layer });
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Get Board Info Tool
// ------------------------------------------------------
server.tool(
"get_board_info",
{},
async () => {
logger.debug('Getting board information');
const result = await callKicadScript("get_board_info", {});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Get Layer List Tool
// ------------------------------------------------------
server.tool(
"get_layer_list",
{},
async () => {
logger.debug('Getting layer list');
const result = await callKicadScript("get_layer_list", {});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Add Board Outline Tool
// ------------------------------------------------------
server.tool(
"add_board_outline",
{
shape: z.enum(["rectangle", "circle", "polygon"]).describe("Shape of the outline"),
params: z.object({
// For rectangle
width: z.number().optional().describe("Width of rectangle"),
height: z.number().optional().describe("Height of rectangle"),
// For circle
radius: z.number().optional().describe("Radius of circle"),
// For polygon
points: z.array(
z.object({
x: z.number().describe("X coordinate"),
y: z.number().describe("Y coordinate")
})
).optional().describe("Points of polygon"),
// Common parameters
x: z.number().describe("X coordinate of center/origin"),
y: z.number().describe("Y coordinate of center/origin"),
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
}).describe("Parameters for the outline shape")
},
async ({ shape, params }) => {
logger.debug(`Adding ${shape} board outline`);
// Flatten params and rename x/y to centerX/centerY for Python compatibility
const { x, y, ...otherParams } = params;
const result = await callKicadScript("add_board_outline", {
shape,
centerX: x,
centerY: y,
...otherParams
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Add Mounting Hole Tool
// ------------------------------------------------------
server.tool(
"add_mounting_hole",
{
position: z.object({
x: z.number().describe("X coordinate"),
y: z.number().describe("Y coordinate"),
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
}).describe("Position of the mounting hole"),
diameter: z.number().describe("Diameter of the hole"),
padDiameter: z.number().optional().describe("Optional diameter of the pad around the hole")
},
async ({ position, diameter, padDiameter }) => {
logger.debug(`Adding mounting hole at (${position.x},${position.y}) ${position.unit}`);
const result = await callKicadScript("add_mounting_hole", {
position,
diameter,
padDiameter
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Add Text Tool
// ------------------------------------------------------
server.tool(
"add_board_text",
{
text: z.string().describe("Text content"),
position: z.object({
x: z.number().describe("X coordinate"),
y: z.number().describe("Y coordinate"),
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
}).describe("Position of the text"),
layer: z.string().describe("Layer to place the text on"),
size: z.number().describe("Text size"),
thickness: z.number().optional().describe("Line thickness"),
rotation: z.number().optional().describe("Rotation angle in degrees"),
style: z.enum(["normal", "italic", "bold"]).optional().describe("Text style")
},
async ({ text, position, layer, size, thickness, rotation, style }) => {
logger.debug(`Adding text "${text}" at (${position.x},${position.y}) ${position.unit}`);
const result = await callKicadScript("add_board_text", {
text,
position,
layer,
size,
thickness,
rotation,
style
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Add Zone Tool
// ------------------------------------------------------
server.tool(
"add_zone",
{
layer: z.string().describe("Layer for the zone"),
net: z.string().describe("Net name for the zone"),
points: z.array(
z.object({
x: z.number().describe("X coordinate"),
y: z.number().describe("Y coordinate")
})
).describe("Points defining the zone outline"),
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
clearance: z.number().optional().describe("Clearance value"),
minWidth: z.number().optional().describe("Minimum width"),
padConnection: z.enum(["thermal", "solid", "none"]).optional().describe("Pad connection type")
},
async ({ layer, net, points, unit, clearance, minWidth, padConnection }) => {
logger.debug(`Adding zone on layer ${layer} for net ${net}`);
const result = await callKicadScript("add_zone", {
layer,
net,
points,
unit,
clearance,
minWidth,
padConnection
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Get Board Extents Tool
// ------------------------------------------------------
server.tool(
"get_board_extents",
{
unit: z.enum(["mm", "inch"]).optional().describe("Unit of measurement for the result")
},
async ({ unit }) => {
logger.debug('Getting board extents');
const result = await callKicadScript("get_board_extents", { unit });
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
// ------------------------------------------------------
// Get Board 2D View Tool
// ------------------------------------------------------
server.tool(
"get_board_2d_view",
{
layers: z.array(z.string()).optional().describe("Optional array of layer names to include"),
width: z.number().optional().describe("Optional width of the image in pixels"),
height: z.number().optional().describe("Optional height of the image in pixels"),
format: z.enum(["png", "jpg", "svg"]).optional().describe("Image format")
},
async ({ layers, width, height, format }) => {
logger.debug('Getting 2D board view');
const result = await callKicadScript("get_board_2d_view", {
layers,
width,
height,
format
});
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
logger.info('Board management tools registered');
}
```
--------------------------------------------------------------------------------
/src/resources/board.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Board resources for KiCAD MCP server
*
* These resources provide information about the PCB board
* to the LLM, enabling better context-aware assistance.
*/
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { logger } from '../logger.js';
import { createJsonResponse, createBinaryResponse } from '../utils/resource-helpers.js';
// Command function type for KiCAD script calls
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
/**
* Register board resources with the MCP server
*
* @param server MCP server instance
* @param callKicadScript Function to call KiCAD script commands
*/
export function registerBoardResources(server: McpServer, callKicadScript: CommandFunction): void {
logger.info('Registering board resources');
// ------------------------------------------------------
// Board Information Resource
// ------------------------------------------------------
server.resource(
"board_info",
"kicad://board/info",
async (uri) => {
logger.debug('Retrieving board information');
const result = await callKicadScript("get_board_info", {});
if (!result.success) {
logger.error(`Failed to retrieve board information: ${result.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to retrieve board information",
details: result.errorDetails
}),
mimeType: "application/json"
}]
};
}
logger.debug('Successfully retrieved board information');
return {
contents: [{
uri: uri.href,
text: JSON.stringify(result),
mimeType: "application/json"
}]
};
}
);
// ------------------------------------------------------
// Layer List Resource
// ------------------------------------------------------
server.resource(
"layer_list",
"kicad://board/layers",
async (uri) => {
logger.debug('Retrieving layer list');
const result = await callKicadScript("get_layer_list", {});
if (!result.success) {
logger.error(`Failed to retrieve layer list: ${result.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to retrieve layer list",
details: result.errorDetails
}),
mimeType: "application/json"
}]
};
}
logger.debug(`Successfully retrieved ${result.layers?.length || 0} layers`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify(result),
mimeType: "application/json"
}]
};
}
);
// ------------------------------------------------------
// Board Extents Resource
// ------------------------------------------------------
server.resource(
"board_extents",
new ResourceTemplate("kicad://board/extents/{unit?}", {
list: async () => ({
resources: [
{ uri: "kicad://board/extents/mm", name: "Millimeters" },
{ uri: "kicad://board/extents/inch", name: "Inches" }
]
})
}),
async (uri, params) => {
const unit = params.unit || 'mm';
logger.debug(`Retrieving board extents in ${unit}`);
const result = await callKicadScript("get_board_extents", { unit });
if (!result.success) {
logger.error(`Failed to retrieve board extents: ${result.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to retrieve board extents",
details: result.errorDetails
}),
mimeType: "application/json"
}]
};
}
logger.debug('Successfully retrieved board extents');
return {
contents: [{
uri: uri.href,
text: JSON.stringify(result),
mimeType: "application/json"
}]
};
}
);
// ------------------------------------------------------
// Board 2D View Resource
// ------------------------------------------------------
server.resource(
"board_2d_view",
new ResourceTemplate("kicad://board/2d-view/{format?}", {
list: async () => ({
resources: [
{ uri: "kicad://board/2d-view/png", name: "PNG Format" },
{ uri: "kicad://board/2d-view/jpg", name: "JPEG Format" },
{ uri: "kicad://board/2d-view/svg", name: "SVG Format" }
]
})
}),
async (uri, params) => {
const format = (params.format || 'png') as 'png' | 'jpg' | 'svg';
const width = params.width ? parseInt(params.width as string) : undefined;
const height = params.height ? parseInt(params.height as string) : undefined;
// Handle layers parameter - could be string or array
const layers = typeof params.layers === 'string' ? params.layers.split(',') : params.layers;
logger.debug('Retrieving 2D board view');
const result = await callKicadScript("get_board_2d_view", {
layers,
width,
height,
format
});
if (!result.success) {
logger.error(`Failed to retrieve 2D board view: ${result.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to retrieve 2D board view",
details: result.errorDetails
}),
mimeType: "application/json"
}]
};
}
logger.debug('Successfully retrieved 2D board view');
if (format === 'svg') {
return {
contents: [{
uri: uri.href,
text: result.imageData,
mimeType: "image/svg+xml"
}]
};
} else {
return {
contents: [{
uri: uri.href,
blob: result.imageData,
mimeType: format === "jpg" ? "image/jpeg" : "image/png"
}]
};
}
}
);
// ------------------------------------------------------
// Board 3D View Resource
// ------------------------------------------------------
server.resource(
"board_3d_view",
new ResourceTemplate("kicad://board/3d-view/{angle?}", {
list: async () => ({
resources: [
{ uri: "kicad://board/3d-view/isometric", name: "Isometric View" },
{ uri: "kicad://board/3d-view/top", name: "Top View" },
{ uri: "kicad://board/3d-view/bottom", name: "Bottom View" }
]
})
}),
async (uri, params) => {
const angle = params.angle || 'isometric';
const width = params.width ? parseInt(params.width as string) : undefined;
const height = params.height ? parseInt(params.height as string) : undefined;
logger.debug(`Retrieving 3D board view from ${angle} angle`);
const result = await callKicadScript("get_board_3d_view", {
width,
height,
angle
});
if (!result.success) {
logger.error(`Failed to retrieve 3D board view: ${result.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to retrieve 3D board view",
details: result.errorDetails
}),
mimeType: "application/json"
}]
};
}
logger.debug('Successfully retrieved 3D board view');
return {
contents: [{
uri: uri.href,
blob: result.imageData,
mimeType: "image/png"
}]
};
}
);
// ------------------------------------------------------
// Board Statistics Resource
// ------------------------------------------------------
server.resource(
"board_statistics",
"kicad://board/statistics",
async (uri) => {
logger.debug('Generating board statistics');
// Get board info
const boardResult = await callKicadScript("get_board_info", {});
if (!boardResult.success) {
logger.error(`Failed to retrieve board information: ${boardResult.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to generate board statistics",
details: boardResult.errorDetails
}),
mimeType: "application/json"
}]
};
}
// Get component list
const componentsResult = await callKicadScript("get_component_list", {});
if (!componentsResult.success) {
logger.error(`Failed to retrieve component list: ${componentsResult.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to generate board statistics",
details: componentsResult.errorDetails
}),
mimeType: "application/json"
}]
};
}
// Get nets list
const netsResult = await callKicadScript("get_nets_list", {});
if (!netsResult.success) {
logger.error(`Failed to retrieve nets list: ${netsResult.errorDetails}`);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({
error: "Failed to generate board statistics",
details: netsResult.errorDetails
}),
mimeType: "application/json"
}]
};
}
// Combine all information into statistics
const statistics = {
board: {
size: boardResult.size,
layers: boardResult.layers?.length || 0,
title: boardResult.title
},
components: {
count: componentsResult.components?.length || 0,
types: countComponentTypes(componentsResult.components || [])
},
nets: {
count: netsResult.nets?.length || 0
}
};
logger.debug('Successfully generated board statistics');
return {
contents: [{
uri: uri.href,
text: JSON.stringify(statistics),
mimeType: "application/json"
}]
};
}
);
logger.info('Board resources registered');
}
/**
* Helper function to count component types
*/
function countComponentTypes(components: any[]): Record<string, number> {
const typeCounts: Record<string, number> = {};
for (const component of components) {
const type = component.value?.split(' ')[0] || 'Unknown';
typeCounts[type] = (typeCounts[type] || 0) + 1;
}
return typeCounts;
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
/**
* KiCAD MCP Server implementation
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import express from 'express';
import { spawn, ChildProcess } from 'child_process';
import { existsSync } from 'fs';
import { join, dirname } from 'path';
import { logger } from './logger.js';
// Import tool registration functions
import { registerProjectTools } from './tools/project.js';
import { registerBoardTools } from './tools/board.js';
import { registerComponentTools } from './tools/component.js';
import { registerRoutingTools } from './tools/routing.js';
import { registerDesignRuleTools } from './tools/design-rules.js';
import { registerExportTools } from './tools/export.js';
import { registerUITools } from './tools/ui.js';
// Import resource registration functions
import { registerProjectResources } from './resources/project.js';
import { registerBoardResources } from './resources/board.js';
import { registerComponentResources } from './resources/component.js';
import { registerLibraryResources } from './resources/library.js';
// Import prompt registration functions
import { registerComponentPrompts } from './prompts/component.js';
import { registerRoutingPrompts } from './prompts/routing.js';
import { registerDesignPrompts } from './prompts/design.js';
/**
* Find the Python executable to use
* Prioritizes virtual environment if available, falls back to system Python
*/
function findPythonExecutable(scriptPath: string): string {
const isWindows = process.platform === 'win32';
// Get the project root (parent of the python/ directory)
const projectRoot = dirname(dirname(scriptPath));
// Check for virtual environment
const venvPaths = [
join(projectRoot, 'venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'),
join(projectRoot, '.venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'),
];
for (const venvPath of venvPaths) {
if (existsSync(venvPath)) {
logger.info(`Found virtual environment Python at: ${venvPath}`);
return venvPath;
}
}
// Fall back to system Python or environment-specified Python
if (isWindows && process.env.KICAD_PYTHON) {
// Allow override via KICAD_PYTHON environment variable
return process.env.KICAD_PYTHON;
} else if (isWindows && process.env.PYTHONPATH?.includes('KiCad')) {
// Windows: Try KiCAD's bundled Python
const kicadPython = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
if (existsSync(kicadPython)) {
return kicadPython;
}
}
// Default to system Python
logger.info('Using system Python (no venv found)');
return isWindows ? 'python.exe' : 'python3';
}
/**
* KiCAD MCP Server class
*/
export class KiCADMcpServer {
private server: McpServer;
private pythonProcess: ChildProcess | null = null;
private kicadScriptPath: string;
private stdioTransport!: StdioServerTransport;
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
private processingRequest = false;
/**
* Constructor for the KiCAD MCP Server
* @param kicadScriptPath Path to the Python KiCAD interface script
* @param logLevel Log level for the server
*/
constructor(
kicadScriptPath: string,
logLevel: 'error' | 'warn' | 'info' | 'debug' = 'info'
) {
// Set up the logger
logger.setLogLevel(logLevel);
// Check if KiCAD script exists
this.kicadScriptPath = kicadScriptPath;
if (!existsSync(this.kicadScriptPath)) {
throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
}
// Initialize the MCP server
this.server = new McpServer({
name: 'kicad-mcp-server',
version: '1.0.0',
description: 'MCP server for KiCAD PCB design operations'
});
// Initialize STDIO transport
this.stdioTransport = new StdioServerTransport();
logger.info('Using STDIO transport for local communication');
// Register tools, resources, and prompts
this.registerAll();
}
/**
* Register all tools, resources, and prompts
*/
private registerAll(): void {
logger.info('Registering KiCAD tools, resources, and prompts...');
// Register all tools
registerProjectTools(this.server, this.callKicadScript.bind(this));
registerBoardTools(this.server, this.callKicadScript.bind(this));
registerComponentTools(this.server, this.callKicadScript.bind(this));
registerRoutingTools(this.server, this.callKicadScript.bind(this));
registerDesignRuleTools(this.server, this.callKicadScript.bind(this));
registerExportTools(this.server, this.callKicadScript.bind(this));
registerUITools(this.server, this.callKicadScript.bind(this));
// Register all resources
registerProjectResources(this.server, this.callKicadScript.bind(this));
registerBoardResources(this.server, this.callKicadScript.bind(this));
registerComponentResources(this.server, this.callKicadScript.bind(this));
registerLibraryResources(this.server, this.callKicadScript.bind(this));
// Register all prompts
registerComponentPrompts(this.server);
registerRoutingPrompts(this.server);
registerDesignPrompts(this.server);
logger.info('All KiCAD tools, resources, and prompts registered');
}
/**
* Start the MCP server and the Python KiCAD interface
*/
async start(): Promise<void> {
try {
logger.info('Starting KiCAD MCP server...');
// Start the Python process for KiCAD scripting
logger.info(`Starting Python process with script: ${this.kicadScriptPath}`);
const pythonExe = findPythonExecutable(this.kicadScriptPath);
logger.info(`Using Python executable: ${pythonExe}`);
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
PYTHONPATH: process.env.PYTHONPATH || 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
}
});
// Listen for process exit
this.pythonProcess.on('exit', (code, signal) => {
logger.warn(`Python process exited with code ${code} and signal ${signal}`);
this.pythonProcess = null;
});
// Listen for process errors
this.pythonProcess.on('error', (err) => {
logger.error(`Python process error: ${err.message}`);
});
// Set up error logging for stderr
if (this.pythonProcess.stderr) {
this.pythonProcess.stderr.on('data', (data: Buffer) => {
logger.error(`Python stderr: ${data.toString()}`);
});
}
// Connect server to STDIO transport
logger.info('Connecting MCP server to STDIO transport...');
try {
await this.server.connect(this.stdioTransport);
logger.info('Successfully connected to STDIO transport');
} catch (error) {
logger.error(`Failed to connect to STDIO transport: ${error}`);
throw error;
}
// Write a ready message to stderr (for debugging)
process.stderr.write('KiCAD MCP SERVER READY\n');
logger.info('KiCAD MCP server started and ready');
} catch (error) {
logger.error(`Failed to start KiCAD MCP server: ${error}`);
throw error;
}
}
/**
* Stop the MCP server and clean up resources
*/
async stop(): Promise<void> {
logger.info('Stopping KiCAD MCP server...');
// Kill the Python process if it's running
if (this.pythonProcess) {
this.pythonProcess.kill();
this.pythonProcess = null;
}
logger.info('KiCAD MCP server stopped');
}
/**
* Call the KiCAD scripting interface to execute commands
*
* @param command The command to execute
* @param params The parameters for the command
* @returns The result of the command execution
*/
private async callKicadScript(command: string, params: any): Promise<any> {
return new Promise((resolve, reject) => {
// Check if Python process is running
if (!this.pythonProcess) {
logger.error('Python process is not running');
reject(new Error("Python process for KiCAD scripting is not running"));
return;
}
// Add request to queue
this.requestQueue.push({
request: { command, params },
resolve,
reject
});
// Process the queue if not already processing
if (!this.processingRequest) {
this.processNextRequest();
}
});
}
/**
* Process the next request in the queue
*/
private processNextRequest(): void {
// If no more requests or already processing, return
if (this.requestQueue.length === 0 || this.processingRequest) {
return;
}
// Set processing flag
this.processingRequest = true;
// Get the next request
const { request, resolve, reject } = this.requestQueue.shift()!;
try {
logger.debug(`Processing KiCAD command: ${request.command}`);
// Format the command and parameters as JSON
const requestStr = JSON.stringify(request);
// Set up response handling
let responseData = '';
// Clear any previous listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
this.pythonProcess.stdout.removeAllListeners('end');
}
// Set up new listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.on('data', (data: Buffer) => {
const chunk = data.toString();
logger.debug(`Received data chunk: ${chunk.length} bytes`);
responseData += chunk;
// Check if we have a complete response
try {
// Try to parse the response as JSON
const result = JSON.parse(responseData);
// If we get here, we have a valid JSON response
logger.debug(`Completed KiCAD command: ${request.command} with result: ${result.success ? 'success' : 'failure'}`);
// Reset processing flag
this.processingRequest = false;
// Process next request if any
setTimeout(() => this.processNextRequest(), 0);
// Clear listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
this.pythonProcess.stdout.removeAllListeners('end');
}
// Resolve the promise with the result
resolve(result);
} catch (e) {
// Not a complete JSON yet, keep collecting data
}
});
}
// Set a timeout
const timeout = setTimeout(() => {
logger.error(`Command timeout: ${request.command}`);
// Clear listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
this.pythonProcess.stdout.removeAllListeners('end');
}
// Reset processing flag
this.processingRequest = false;
// Process next request
setTimeout(() => this.processNextRequest(), 0);
// Reject the promise
reject(new Error(`Command timeout: ${request.command}`));
}, 30000); // 30 seconds timeout
// Write the request to the Python process
logger.debug(`Sending request: ${requestStr}`);
this.pythonProcess?.stdin?.write(requestStr + '\n');
} catch (error) {
logger.error(`Error processing request: ${error}`);
// Reset processing flag
this.processingRequest = false;
// Process next request
setTimeout(() => this.processNextRequest(), 0);
// Reject the promise
reject(error);
}
}
}
```
--------------------------------------------------------------------------------
/CHANGELOG_2025-10-26.md:
--------------------------------------------------------------------------------
```markdown
# Changelog - October 26, 2025
## 🎉 Major Updates: Testing, Fixes, and UI Auto-Launch
**Summary:** Complete testing of KiCAD MCP server, critical bug fixes, and new UI auto-launch feature for seamless visual feedback.
---
## 🐛 Critical Fixes
### 1. Python Environment Detection (src/server.ts)
**Problem:** Server hardcoded to use system Python, couldn't access venv dependencies
**Fixed:**
- Added `findPythonExecutable()` function with platform detection
- Auto-detects virtual environment at `./venv/bin/python`
- Falls back to system Python if venv not found
- Cross-platform support (Linux, macOS, Windows)
**Files Changed:**
- `src/server.ts` (lines 32-70, 153)
**Impact:** ✅ `kicad-skip` and other venv packages now accessible
---
### 2. KiCAD Path Detection (python/utils/platform_helper.py)
**Problem:** Platform helper didn't check system dist-packages on Linux
**Fixed:**
- Added `/usr/lib/python3/dist-packages` to search paths
- Added `/usr/lib/python{version}/dist-packages` for version-specific installs
- Now finds pcbnew successfully on Ubuntu/Debian systems
**Files Changed:**
- `python/utils/platform_helper.py` (lines 82-89)
**Impact:** ✅ pcbnew module imports successfully from system installation
---
### 3. Board Reference Management (python/kicad_interface.py)
**Problem:** After opening project, board reference not properly updated
**Fixed:**
- Changed from `pcbnew.GetBoard()` (doesn't work) to `self.project_commands.board`
- Board reference now correctly propagates to all command handlers
**Files Changed:**
- `python/kicad_interface.py` (line 210)
**Impact:** ✅ All board operations work after opening project
---
### 4. Parameter Mapping Issues
#### open_project Parameter Mismatch (src/tools/project.ts)
**Problem:** TypeScript expected `path`, Python expected `filename`
**Fixed:**
- Changed tool schema to use `filename` parameter
- Updated type definition to match
**Files Changed:**
- `src/tools/project.ts` (line 33)
#### add_board_outline Parameter Structure (src/tools/board.ts)
**Problem:** Nested `params` object, Python expected flattened parameters
**Fixed:**
- Flatten params object in handler
- Rename `x`/`y` to `centerX`/`centerY` for Python compatibility
**Files Changed:**
- `src/tools/board.ts` (lines 168-185)
**Impact:** ✅ Tools now work correctly with proper parameter passing
---
## 🚀 New Features
### UI Auto-Launch System
**Description:** Automatic KiCAD UI detection and launching for seamless visual feedback
**New Files:**
- `python/utils/kicad_process.py` (286 lines)
- Cross-platform process detection (Linux, macOS, Windows)
- Automatic executable discovery
- Background process spawning
- Process info retrieval
- `src/tools/ui.ts` (45 lines)
- MCP tool definitions for UI management
- `check_kicad_ui` - Check if KiCAD is running
- `launch_kicad_ui` - Launch KiCAD with optional project
**Modified Files:**
- `python/kicad_interface.py` (added UI command handlers)
- `src/server.ts` (registered UI tools)
**New MCP Tools:**
1. **check_kicad_ui**
- Parameters: None
- Returns: running status, process list
2. **launch_kicad_ui**
- Parameters: `projectPath` (optional), `autoLaunch` (optional)
- Returns: launch status, process info
**Environment Variables:**
- `KICAD_AUTO_LAUNCH` - Enable automatic UI launching (default: false)
- `KICAD_EXECUTABLE` - Override KiCAD executable path (optional)
**Impact:** 🎉 Users can now see PCB changes in real-time with auto-reload workflow
---
## 📚 Documentation Updates
### New Documentation
1. **docs/UI_AUTO_LAUNCH.md** (500+ lines)
- Complete guide to UI auto-launch feature
- Usage examples and workflows
- Configuration options
- Troubleshooting guide
2. **docs/VISUAL_FEEDBACK.md** (400+ lines)
- Current SWIG workflow (manual reload)
- Future IPC workflow (real-time updates)
- Side-by-side design workflow
- Troubleshooting tips
3. **CHANGELOG_2025-10-26.md** (this file)
- Complete record of today's work
### Updated Documentation
1. **README.md**
- Added UI Auto-Launch feature section
- Updated "What Works Now" section
- Added UI management examples
- Marked component placement/routing as WIP
2. **config/linux-config.example.json**
- Added `KICAD_AUTO_LAUNCH` environment variable
- Added description field
- Note about auto-detected PYTHONPATH
3. **config/macos-config.example.json**
- Added `KICAD_AUTO_LAUNCH` environment variable
- Added description field
4. **config/windows-config.example.json**
- Added `KICAD_AUTO_LAUNCH` environment variable
- Added description field
---
## ✅ Testing Results
### Test Suite Executed
- Platform detection tests: **13/14 passed** (1 skipped - expected)
- MCP server startup: **✅ Success**
- Python module import: **✅ Success** (pcbnew v9.0.5)
- Command handlers: **✅ All imported**
### End-to-End Demo Created
**Project:** `/tmp/mcp_demo/New_Project.kicad_pcb`
**Operations Tested:**
1. ✅ create_project - Success
2. ✅ open_project - Success
3. ✅ add_board_outline - Success (68.6mm × 53.4mm Arduino shield)
4. ✅ add_mounting_hole - Success (4 holes at corners)
5. ✅ save_project - Success
6. ✅ get_project_info - Success
### Tool Success Rate
| Category | Tested | Passed | Rate |
|----------|--------|--------|------|
| Project Ops | 4 | 4 | 100% |
| Board Ops | 3 | 2 | 67% |
| UI Ops | 2 | 2 | 100% |
| **Overall** | **9** | **8** | **89%** |
### Known Issues
- ⚠️ `get_board_info` - KiCAD 9.0 API compatibility issue (`LT_USER` attribute)
- ⚠️ `place_component` - Library path integration needed
- ⚠️ Routing operations - Not yet tested
---
## 📊 Code Statistics
### Lines Added
- Python: ~400 lines
- TypeScript: ~100 lines
- Documentation: ~1,500 lines
- **Total: ~2,000 lines**
### Files Modified/Created
**New Files (7):**
- `python/utils/kicad_process.py`
- `src/tools/ui.ts`
- `docs/UI_AUTO_LAUNCH.md`
- `docs/VISUAL_FEEDBACK.md`
- `CHANGELOG_2025-10-26.md`
- `scripts/auto_refresh_kicad.sh`
**Modified Files (10):**
- `src/server.ts`
- `src/tools/project.ts`
- `src/tools/board.ts`
- `python/kicad_interface.py`
- `python/utils/platform_helper.py`
- `README.md`
- `config/linux-config.example.json`
- `config/macos-config.example.json`
- `config/windows-config.example.json`
---
## 🔧 Technical Improvements
### Architecture
- ✅ Proper separation of UI management concerns
- ✅ Cross-platform process management
- ✅ Automatic environment detection
- ✅ Robust error handling with fallbacks
### Developer Experience
- ✅ Virtual environment auto-detection
- ✅ No manual PYTHONPATH configuration needed (if venv exists)
- ✅ Clear error messages with helpful suggestions
- ✅ Comprehensive logging
### User Experience
- ✅ Automatic KiCAD launching
- ✅ Visual feedback workflow
- ✅ Natural language UI control
- ✅ Cross-platform compatibility
---
## 🎯 Week 1 Status Update
### Completed
- ✅ Cross-platform Python environment setup
- ✅ KiCAD path auto-detection
- ✅ Board creation and manipulation
- ✅ Project operations (create, open, save)
- ✅ **UI auto-launch and detection** (NEW!)
- ✅ **Visual feedback workflow** (NEW!)
- ✅ End-to-end testing
- ✅ Comprehensive documentation
### In Progress
- 🔄 Component library integration
- 🔄 Routing operations
- 🔄 IPC backend implementation (skeleton exists)
### Upcoming (Week 2-3)
- ⏳ IPC API migration (real-time UI updates)
- ⏳ JLCPCB parts integration
- ⏳ Digikey parts integration
- ⏳ Component placement with library support
---
## 🚀 User Impact
### Before Today
```
User: "Create a board"
→ Creates project file
→ User must manually open in KiCAD
→ User must manually reload after each change
```
### After Today
```
User: "Create a board"
→ Creates project file
→ Auto-launches KiCAD (optional)
→ KiCAD auto-detects changes and prompts reload
→ Seamless visual feedback!
```
---
## 📝 Migration Notes
### For Existing Users
1. **Rebuild required:** `npm run build`
2. **Restart MCP server** to load new features
3. **Optional:** Add `KICAD_AUTO_LAUNCH=true` to config for automatic launching
4. **Optional:** Install `inotify-tools` on Linux for file monitoring (future enhancement)
### Breaking Changes
None - all changes are backward compatible
### New Dependencies
- Python: None (all in stdlib)
- Node.js: None (existing SDK)
---
## 🐛 Bug Tracker
### Fixed Today
- [x] Python venv not detected
- [x] pcbnew import fails on Linux
- [x] Board reference not updating after open_project
- [x] Parameter mismatch in open_project
- [x] Parameter structure in add_board_outline
### Remaining Issues
- [ ] get_board_info KiCAD 9.0 API compatibility
- [ ] Component library path detection
- [ ] Routing operations implementation
---
## 🎓 Lessons Learned
1. **Process spawning:** Background processes need proper detachment (CREATE_NEW_PROCESS_GROUP on Windows, start_new_session on Unix)
2. **Parameter mapping:** TypeScript tool schemas must exactly match Python expectations - use transform functions when needed
3. **Board lifecycle:** KiCAD's pcbnew module doesn't provide a global GetBoard() - must maintain references explicitly
4. **Platform detection:** Each OS has different process management tools (pgrep, tasklist) - must handle gracefully
5. **Virtual environments:** Auto-detecting venv dramatically improves DX - no manual PYTHONPATH configuration needed
---
## 🙏 Acknowledgments
- **KiCAD Team** - For the excellent pcbnew Python API
- **Anthropic** - For the Model Context Protocol
- **kicad-python** - For IPC API library (future use)
- **kicad-skip** - For schematic generation support
---
## 📅 Timeline
- **Start Time:** ~2025-10-26 02:00 UTC
- **End Time:** ~2025-10-26 09:00 UTC
- **Duration:** ~7 hours
- **Commits:** Multiple (testing, fixes, features, docs)
---
## 🔮 Next Session
**Priority Tasks:**
1. Test UI auto-launch with user
2. Fix get_board_info KiCAD 9.0 API issue
3. Implement component library detection
4. Begin IPC backend migration
**Goals:**
- Component placement working end-to-end
- IPC backend operational for basic operations
- Real-time UI updates via IPC
---
**Session Status:** ✅ **COMPLETE - PRODUCTION READY**
---
## 🔧 Session 2: Bug Fixes & KiCAD 9.0 Compatibility (2025-10-26 PM)
### Issues Fixed
**1. KiCAD Process Detection Bug** ✅
- **Problem:** `check_kicad_ui` was detecting MCP server's own processes
- **Root Cause:** Process search matched `kicad_interface.py` in process names
- **Fix:** Added filters to exclude MCP server processes, only match actual KiCAD binaries
- **Files:** `python/utils/kicad_process.py:31-61, 196-213`
- **Result:** UI auto-launch now works correctly
**2. Missing Command Mapping** ✅
- **Problem:** `add_board_text` command not found
- **Root Cause:** TypeScript tool named `add_board_text`, Python expected `add_text`
- **Fix:** Added command alias in routing dictionary
- **Files:** `python/kicad_interface.py:150`
- **Result:** Text annotations now work
**3. KiCAD 9.0 API - set_board_size** ✅
- **Problem:** `BOX2I_SetSize` argument type mismatch
- **Root Cause:** KiCAD 9.0 changed SetSize to take two parameters instead of VECTOR2I
- **Fix:** Try new API first, fallback to old API for compatibility
- **Files:** `python/commands/board/size.py:44-57`
- **Result:** Board size setting now works on KiCAD 9.0
**4. KiCAD 9.0 API - add_text rotation** ✅
- **Problem:** `EDA_TEXT_SetTextAngle` expecting EDA_ANGLE, not integer
- **Root Cause:** KiCAD 9.0 uses EDA_ANGLE class instead of decidegrees
- **Fix:** Create EDA_ANGLE object, fallback to integer for older versions
- **Files:** `python/commands/board/outline.py:282-289`
- **Result:** Text annotations with rotation now work
### Testing Results
**Complete End-to-End Workflow:** ✅ **PASSING**
Created test board with:
- ✅ Project creation and opening
- ✅ Board size: 100mm x 80mm
- ✅ Rectangular board outline
- ✅ 4 mounting holes (3.2mm) at corners
- ✅ 2 text annotations on F.SilkS layer
- ✅ Project saved successfully
- ✅ KiCAD UI launched with project
### Code Statistics
**Lines Changed:** ~50 lines
**Files Modified:** 4
- `python/utils/kicad_process.py`
- `python/kicad_interface.py`
- `python/commands/board/size.py`
- `python/commands/board/outline.py`
**Documentation Updated:**
- `README.md` - Updated status, known issues, roadmap
- `CHANGELOG_2025-10-26.md` - This session log
### Current Status
**Working Features:** 11/14 core features (79%)
**Known Issues:** 4 (documented in README)
**KiCAD 9.0 Compatibility:** ✅ Major APIs fixed
### Next Steps
1. **Component Library Integration** (highest priority)
2. **Routing Operations Testing** (verify KiCAD 9.0 compatibility)
3. **IPC Backend Implementation** (real-time UI updates)
4. **Example Projects & Tutorials**
---
*Updated: 2025-10-26 PM*
*Version: 2.0.0-alpha.2*
*Session ID: Week 1 - Bug Fixes & Testing*
```
--------------------------------------------------------------------------------
/python/commands/board/outline.py:
--------------------------------------------------------------------------------
```python
"""
Board outline command implementations for KiCAD interface
"""
import pcbnew
import logging
import math
from typing import Dict, Any, Optional
logger = logging.getLogger('kicad_interface')
class BoardOutlineCommands:
"""Handles board outline operations"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a board outline to the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
shape = params.get("shape", "rectangle")
width = params.get("width")
height = params.get("height")
center_x = params.get("centerX", 0)
center_y = params.get("centerY", 0)
radius = params.get("radius")
corner_radius = params.get("cornerRadius", 0)
points = params.get("points", [])
unit = params.get("unit", "mm")
if shape not in ["rectangle", "circle", "polygon", "rounded_rectangle"]:
return {
"success": False,
"message": "Invalid shape",
"errorDetails": f"Shape '{shape}' not supported"
}
# Convert to internal units (nanometers)
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
# Create drawing for edge cuts
edge_layer = self.board.GetLayerID("Edge.Cuts")
if shape == "rectangle":
if width is None or height is None:
return {
"success": False,
"message": "Missing dimensions",
"errorDetails": "Both width and height are required for rectangle"
}
width_nm = int(width * scale)
height_nm = int(height * scale)
center_x_nm = int(center_x * scale)
center_y_nm = int(center_y * scale)
# Create rectangle
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
# Add lines for rectangle
self._add_edge_line(top_left, top_right, edge_layer)
self._add_edge_line(top_right, bottom_right, edge_layer)
self._add_edge_line(bottom_right, bottom_left, edge_layer)
self._add_edge_line(bottom_left, top_left, edge_layer)
elif shape == "rounded_rectangle":
if width is None or height is None:
return {
"success": False,
"message": "Missing dimensions",
"errorDetails": "Both width and height are required for rounded rectangle"
}
width_nm = int(width * scale)
height_nm = int(height * scale)
center_x_nm = int(center_x * scale)
center_y_nm = int(center_y * scale)
corner_radius_nm = int(corner_radius * scale)
# Create rounded rectangle
self._add_rounded_rect(
center_x_nm, center_y_nm,
width_nm, height_nm,
corner_radius_nm, edge_layer
)
elif shape == "circle":
if radius is None:
return {
"success": False,
"message": "Missing radius",
"errorDetails": "Radius is required for circle"
}
center_x_nm = int(center_x * scale)
center_y_nm = int(center_y * scale)
radius_nm = int(radius * scale)
# Create circle
circle = pcbnew.PCB_SHAPE(self.board)
circle.SetShape(pcbnew.SHAPE_T_CIRCLE)
circle.SetCenter(pcbnew.VECTOR2I(center_x_nm, center_y_nm))
circle.SetEnd(pcbnew.VECTOR2I(center_x_nm + radius_nm, center_y_nm))
circle.SetLayer(edge_layer)
circle.SetWidth(0) # Zero width for edge cuts
self.board.Add(circle)
elif shape == "polygon":
if not points or len(points) < 3:
return {
"success": False,
"message": "Missing points",
"errorDetails": "At least 3 points are required for polygon"
}
# Convert points to nm
polygon_points = []
for point in points:
x_nm = int(point["x"] * scale)
y_nm = int(point["y"] * scale)
polygon_points.append(pcbnew.VECTOR2I(x_nm, y_nm))
# Add lines for polygon
for i in range(len(polygon_points)):
self._add_edge_line(
polygon_points[i],
polygon_points[(i + 1) % len(polygon_points)],
edge_layer
)
return {
"success": True,
"message": f"Added board outline: {shape}",
"outline": {
"shape": shape,
"width": width,
"height": height,
"center": {"x": center_x, "y": center_y, "unit": unit},
"radius": radius,
"cornerRadius": corner_radius,
"points": points
}
}
except Exception as e:
logger.error(f"Error adding board outline: {str(e)}")
return {
"success": False,
"message": "Failed to add board outline",
"errorDetails": str(e)
}
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a mounting hole to the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
position = params.get("position")
diameter = params.get("diameter")
pad_diameter = params.get("padDiameter")
plated = params.get("plated", False)
if not position or not diameter:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "position and diameter are required"
}
# Convert to internal units (nanometers)
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
diameter_nm = int(diameter * scale)
pad_diameter_nm = int(pad_diameter * scale) if pad_diameter else diameter_nm + scale # 1mm larger by default
# Create footprint for mounting hole
module = pcbnew.FOOTPRINT(self.board)
module.SetReference(f"MH")
module.SetValue(f"MountingHole_{diameter}mm")
# Create the pad for the hole
pad = pcbnew.PAD(module)
pad.SetNumber(1)
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
module.Add(pad)
# Position the mounting hole
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
# Add to board
self.board.Add(module)
return {
"success": True,
"message": "Added mounting hole",
"mountingHole": {
"position": position,
"diameter": diameter,
"padDiameter": pad_diameter or diameter + 1,
"plated": plated
}
}
except Exception as e:
logger.error(f"Error adding mounting hole: {str(e)}")
return {
"success": False,
"message": "Failed to add mounting hole",
"errorDetails": str(e)
}
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add text annotation to the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
text = params.get("text")
position = params.get("position")
layer = params.get("layer", "F.SilkS")
size = params.get("size", 1.0)
thickness = params.get("thickness", 0.15)
rotation = params.get("rotation", 0)
mirror = params.get("mirror", False)
if not text or not position:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "text and position are required"
}
# Convert to internal units (nanometers)
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
size_nm = int(size * scale)
thickness_nm = int(thickness * scale)
# Get layer ID
layer_id = self.board.GetLayerID(layer)
if layer_id < 0:
return {
"success": False,
"message": "Invalid layer",
"errorDetails": f"Layer '{layer}' does not exist"
}
# Create text
pcb_text = pcbnew.PCB_TEXT(self.board)
pcb_text.SetText(text)
pcb_text.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
pcb_text.SetLayer(layer_id)
pcb_text.SetTextSize(pcbnew.VECTOR2I(size_nm, size_nm))
pcb_text.SetTextThickness(thickness_nm)
# Set rotation angle - KiCAD 9.0 uses EDA_ANGLE
try:
# Try KiCAD 9.0+ API (EDA_ANGLE)
angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
pcb_text.SetTextAngle(angle)
except (AttributeError, TypeError):
# Fall back to older API (decidegrees as integer)
pcb_text.SetTextAngle(int(rotation * 10))
pcb_text.SetMirrored(mirror)
# Add to board
self.board.Add(pcb_text)
return {
"success": True,
"message": "Added text annotation",
"text": {
"text": text,
"position": position,
"layer": layer,
"size": size,
"thickness": thickness,
"rotation": rotation,
"mirror": mirror
}
}
except Exception as e:
logger.error(f"Error adding text: {str(e)}")
return {
"success": False,
"message": "Failed to add text",
"errorDetails": str(e)
}
def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None:
"""Add a line to the edge cuts layer"""
line = pcbnew.PCB_SHAPE(self.board)
line.SetShape(pcbnew.SHAPE_T_SEGMENT)
line.SetStart(start)
line.SetEnd(end)
line.SetLayer(layer)
line.SetWidth(0) # Zero width for edge cuts
self.board.Add(line)
def _add_rounded_rect(self, center_x_nm: int, center_y_nm: int,
width_nm: int, height_nm: int,
radius_nm: int, layer: int) -> None:
"""Add a rounded rectangle to the edge cuts layer"""
if radius_nm <= 0:
# If no radius, create regular rectangle
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
self._add_edge_line(top_left, top_right, layer)
self._add_edge_line(top_right, bottom_right, layer)
self._add_edge_line(bottom_right, bottom_left, layer)
self._add_edge_line(bottom_left, top_left, layer)
return
# Calculate corner centers
half_width = width_nm // 2
half_height = height_nm // 2
# Ensure radius is not larger than half the smallest dimension
max_radius = min(half_width, half_height)
if radius_nm > max_radius:
radius_nm = max_radius
# Calculate corner centers
top_left_center = pcbnew.VECTOR2I(
center_x_nm - half_width + radius_nm,
center_y_nm - half_height + radius_nm
)
top_right_center = pcbnew.VECTOR2I(
center_x_nm + half_width - radius_nm,
center_y_nm - half_height + radius_nm
)
bottom_right_center = pcbnew.VECTOR2I(
center_x_nm + half_width - radius_nm,
center_y_nm + half_height - radius_nm
)
bottom_left_center = pcbnew.VECTOR2I(
center_x_nm - half_width + radius_nm,
center_y_nm + half_height - radius_nm
)
# Add arcs for corners
self._add_corner_arc(top_left_center, radius_nm, 180, 270, layer)
self._add_corner_arc(top_right_center, radius_nm, 270, 0, layer)
self._add_corner_arc(bottom_right_center, radius_nm, 0, 90, layer)
self._add_corner_arc(bottom_left_center, radius_nm, 90, 180, layer)
# Add lines for straight edges
# Top edge
self._add_edge_line(
pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm),
pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm),
layer
)
# Right edge
self._add_edge_line(
pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y),
pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y),
layer
)
# Bottom edge
self._add_edge_line(
pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm),
pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm),
layer
)
# Left edge
self._add_edge_line(
pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y),
pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y),
layer
)
def _add_corner_arc(self, center: pcbnew.VECTOR2I, radius: int,
start_angle: float, end_angle: float, layer: int) -> None:
"""Add an arc for a rounded corner"""
# Create arc for corner
arc = pcbnew.PCB_SHAPE(self.board)
arc.SetShape(pcbnew.SHAPE_T_ARC)
arc.SetCenter(center)
# Calculate start and end points
start_x = center.x + int(radius * math.cos(math.radians(start_angle)))
start_y = center.y + int(radius * math.sin(math.radians(start_angle)))
end_x = center.x + int(radius * math.cos(math.radians(end_angle)))
end_y = center.y + int(radius * math.sin(math.radians(end_angle)))
arc.SetStart(pcbnew.VECTOR2I(start_x, start_y))
arc.SetEnd(pcbnew.VECTOR2I(end_x, end_y))
arc.SetLayer(layer)
arc.SetWidth(0) # Zero width for edge cuts
self.board.Add(arc)
```
--------------------------------------------------------------------------------
/src/kicad-server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { spawn, ChildProcess } from 'child_process';
import { existsSync } from 'fs';
import path from 'path';
// Import all tool definitions for reference
// import { registerBoardTools } from './tools/board.js';
// import { registerComponentTools } from './tools/component.js';
// import { registerRoutingTools } from './tools/routing.js';
// import { registerDesignRuleTools } from './tools/design-rules.js';
// import { registerExportTools } from './tools/export.js';
// import { registerProjectTools } from './tools/project.js';
// import { registerSchematicTools } from './tools/schematic.js';
class KiCADServer {
private server: Server;
private pythonProcess: ChildProcess | null = null;
private kicadScriptPath: string;
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
private processingRequest = false;
constructor() {
// Set absolute path to the Python KiCAD interface script
// Using a hardcoded path to avoid cwd() issues when running from Cline
this.kicadScriptPath = 'c:/repo/KiCAD-MCP/python/kicad_interface.py';
// Check if script exists
if (!existsSync(this.kicadScriptPath)) {
throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
}
// Initialize the server
this.server = new Server(
{
name: 'kicad-mcp-server',
version: '1.0.0'
},
{
capabilities: {
tools: {
// Empty object here, tools will be registered dynamically
}
}
}
);
// Initialize handler with direct pass-through to Python KiCAD interface
// We don't register TypeScript tools since we'll handle everything in Python
// Register tool list handler
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// Project tools
{
name: 'create_project',
description: 'Create a new KiCAD project',
inputSchema: {
type: 'object',
properties: {
projectName: { type: 'string', description: 'Name of the new project' },
path: { type: 'string', description: 'Path where to create the project' },
template: { type: 'string', description: 'Optional template to use' }
},
required: ['projectName']
}
},
{
name: 'open_project',
description: 'Open an existing KiCAD project',
inputSchema: {
type: 'object',
properties: {
filename: { type: 'string', description: 'Path to the project file' }
},
required: ['filename']
}
},
{
name: 'save_project',
description: 'Save the current KiCAD project',
inputSchema: {
type: 'object',
properties: {
filename: { type: 'string', description: 'Optional path to save to' }
}
}
},
{
name: 'get_project_info',
description: 'Get information about the current project',
inputSchema: {
type: 'object',
properties: {}
}
},
// Board tools
{
name: 'set_board_size',
description: 'Set the size of the PCB board',
inputSchema: {
type: 'object',
properties: {
width: { type: 'number', description: 'Board width' },
height: { type: 'number', description: 'Board height' },
unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
},
required: ['width', 'height']
}
},
{
name: 'add_board_outline',
description: 'Add a board outline to the PCB',
inputSchema: {
type: 'object',
properties: {
shape: { type: 'string', description: 'Shape of outline (rectangle, circle, polygon, rounded_rectangle)' },
width: { type: 'number', description: 'Width for rectangle shapes' },
height: { type: 'number', description: 'Height for rectangle shapes' },
radius: { type: 'number', description: 'Radius for circle shapes' },
cornerRadius: { type: 'number', description: 'Corner radius for rounded rectangles' },
points: { type: 'array', description: 'Array of points for polygon shapes' },
centerX: { type: 'number', description: 'X coordinate of center' },
centerY: { type: 'number', description: 'Y coordinate of center' },
unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
}
}
},
// Component tools
{
name: 'place_component',
description: 'Place a component on the PCB',
inputSchema: {
type: 'object',
properties: {
componentId: { type: 'string', description: 'Component ID/footprint to place' },
position: { type: 'object', description: 'Position coordinates' },
reference: { type: 'string', description: 'Component reference designator' },
value: { type: 'string', description: 'Component value' },
rotation: { type: 'number', description: 'Rotation angle in degrees' },
layer: { type: 'string', description: 'Layer to place component on' }
},
required: ['componentId', 'position']
}
},
// Routing tools
{
name: 'add_net',
description: 'Add a new net to the PCB',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Net name' },
class: { type: 'string', description: 'Net class' }
},
required: ['name']
}
},
{
name: 'route_trace',
description: 'Route a trace between two points or pads',
inputSchema: {
type: 'object',
properties: {
start: { type: 'object', description: 'Start point or pad' },
end: { type: 'object', description: 'End point or pad' },
layer: { type: 'string', description: 'Layer to route on' },
width: { type: 'number', description: 'Track width' },
net: { type: 'string', description: 'Net name' }
},
required: ['start', 'end']
}
},
// Schematic tools
{
name: 'create_schematic',
description: 'Create a new KiCAD schematic',
inputSchema: {
type: 'object',
properties: {
projectName: { type: 'string', description: 'Name of the schematic project' },
path: { type: 'string', description: 'Path where to create the schematic file' },
metadata: { type: 'object', description: 'Optional metadata for the schematic' }
},
required: ['projectName']
}
},
{
name: 'load_schematic',
description: 'Load an existing KiCAD schematic',
inputSchema: {
type: 'object',
properties: {
filename: { type: 'string', description: 'Path to the schematic file to load' }
},
required: ['filename']
}
},
{
name: 'add_schematic_component',
description: 'Add a component to a KiCAD schematic',
inputSchema: {
type: 'object',
properties: {
schematicPath: { type: 'string', description: 'Path to the schematic file' },
component: {
type: 'object',
description: 'Component definition',
properties: {
type: { type: 'string', description: 'Component type (e.g., R, C, LED)' },
reference: { type: 'string', description: 'Reference designator (e.g., R1, C2)' },
value: { type: 'string', description: 'Component value (e.g., 10k, 0.1uF)' },
library: { type: 'string', description: 'Symbol library name' },
x: { type: 'number', description: 'X position in schematic' },
y: { type: 'number', description: 'Y position in schematic' },
rotation: { type: 'number', description: 'Rotation angle in degrees' },
properties: { type: 'object', description: 'Additional properties' }
},
required: ['type', 'reference']
}
},
required: ['schematicPath', 'component']
}
},
{
name: 'add_schematic_wire',
description: 'Add a wire connection to a KiCAD schematic',
inputSchema: {
type: 'object',
properties: {
schematicPath: { type: 'string', description: 'Path to the schematic file' },
startPoint: {
type: 'array',
description: 'Starting point coordinates [x, y]',
items: { type: 'number' },
minItems: 2,
maxItems: 2
},
endPoint: {
type: 'array',
description: 'Ending point coordinates [x, y]',
items: { type: 'number' },
minItems: 2,
maxItems: 2
}
},
required: ['schematicPath', 'startPoint', 'endPoint']
}
},
{
name: 'list_schematic_libraries',
description: 'List available KiCAD symbol libraries',
inputSchema: {
type: 'object',
properties: {
searchPaths: {
type: 'array',
description: 'Optional search paths for libraries',
items: { type: 'string' }
}
}
}
},
{
name: 'export_schematic_pdf',
description: 'Export a KiCAD schematic to PDF',
inputSchema: {
type: 'object',
properties: {
schematicPath: { type: 'string', description: 'Path to the schematic file' },
outputPath: { type: 'string', description: 'Path for the output PDF file' }
},
required: ['schematicPath', 'outputPath']
}
}
]
}));
// Register tool call handler
this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const toolName = request.params.name;
const args = request.params.arguments || {};
// Pass all commands directly to KiCAD Python interface
try {
return await this.callKicadScript(toolName, args);
} catch (error) {
console.error(`Error executing tool ${toolName}:`, error);
throw new Error(`Unknown tool: ${toolName}`);
}
});
}
async start() {
try {
console.error('Starting KiCAD MCP server...');
// Start the Python process for KiCAD scripting
console.error(`Starting Python process with script: ${this.kicadScriptPath}`);
const pythonExe = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
console.error(`Using Python executable: ${pythonExe}`);
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
PYTHONPATH: 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
}
});
// Listen for process exit
this.pythonProcess.on('exit', (code, signal) => {
console.error(`Python process exited with code ${code} and signal ${signal}`);
this.pythonProcess = null;
});
// Listen for process errors
this.pythonProcess.on('error', (err) => {
console.error(`Python process error: ${err.message}`);
});
// Set up error logging for stderr
if (this.pythonProcess.stderr) {
this.pythonProcess.stderr.on('data', (data: Buffer) => {
console.error(`Python stderr: ${data.toString()}`);
});
}
// Connect to transport
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('KiCAD MCP server running');
// Keep the process running
process.on('SIGINT', () => {
if (this.pythonProcess) {
this.pythonProcess.kill();
}
this.server.close().catch(console.error);
process.exit(0);
});
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Failed to start MCP server:', error.message);
} else {
console.error('Failed to start MCP server: Unknown error');
}
process.exit(1);
}
}
private async callKicadScript(command: string, params: any): Promise<any> {
return new Promise((resolve, reject) => {
// Check if Python process is running
if (!this.pythonProcess) {
console.error('Python process is not running');
reject(new Error("Python process for KiCAD scripting is not running"));
return;
}
// Add request to queue
this.requestQueue.push({
request: { command, params },
resolve,
reject
});
// Process the queue if not already processing
if (!this.processingRequest) {
this.processNextRequest();
}
});
}
private processNextRequest(): void {
// If no more requests or already processing, return
if (this.requestQueue.length === 0 || this.processingRequest) {
return;
}
// Set processing flag
this.processingRequest = true;
// Get the next request
const { request, resolve, reject } = this.requestQueue.shift()!;
try {
console.error(`Processing KiCAD command: ${request.command}`);
// Format the command and parameters as JSON
const requestStr = JSON.stringify(request);
// Set up response handling
let responseData = '';
// Clear any previous listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
}
// Set up new listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.on('data', (data: Buffer) => {
const chunk = data.toString();
console.error(`Received data chunk: ${chunk.length} bytes`);
responseData += chunk;
// Check if we have a complete response
try {
// Try to parse the response as JSON
const result = JSON.parse(responseData);
// If we get here, we have a valid JSON response
console.error(`Completed KiCAD command: ${request.command} with result: ${JSON.stringify(result)}`);
// Reset processing flag
this.processingRequest = false;
// Process next request if any
setTimeout(() => this.processNextRequest(), 0);
// Clear listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
}
// Resolve with the expected MCP tool response format
if (result.success) {
resolve({
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
});
} else {
resolve({
content: [
{
type: 'text',
text: result.errorDetails || result.message || 'Unknown error'
}
],
isError: true
});
}
} catch (e) {
// Not a complete JSON yet, keep collecting data
}
});
}
// Set a timeout
const timeout = setTimeout(() => {
console.error(`Command timeout: ${request.command}`);
// Clear listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
}
// Reset processing flag
this.processingRequest = false;
// Process next request
setTimeout(() => this.processNextRequest(), 0);
// Reject the promise
reject(new Error(`Command timeout: ${request.command}`));
}, 30000); // 30 seconds timeout
// Write the request to the Python process
console.error(`Sending request: ${requestStr}`);
this.pythonProcess?.stdin?.write(requestStr + '\n');
} catch (error) {
console.error(`Error processing request: ${error}`);
// Reset processing flag
this.processingRequest = false;
// Process next request
setTimeout(() => this.processNextRequest(), 0);
// Reject the promise
reject(error);
}
}
}
// Start the server
const server = new KiCADServer();
server.start().catch(console.error);
```
--------------------------------------------------------------------------------
/python/commands/export.py:
--------------------------------------------------------------------------------
```python
"""
Export command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
import base64
logger = logging.getLogger('kicad_interface')
class ExportCommands:
"""Handles export-related KiCAD operations"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
def export_gerber(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Export Gerber files"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
output_dir = params.get("outputDir")
layers = params.get("layers", [])
use_protel_extensions = params.get("useProtelExtensions", False)
generate_drill_files = params.get("generateDrillFiles", True)
generate_map_file = params.get("generateMapFile", False)
use_aux_origin = params.get("useAuxOrigin", False)
if not output_dir:
return {
"success": False,
"message": "Missing output directory",
"errorDetails": "outputDir parameter is required"
}
# Create output directory if it doesn't exist
output_dir = os.path.abspath(os.path.expanduser(output_dir))
os.makedirs(output_dir, exist_ok=True)
# Create plot controller
plotter = pcbnew.PLOT_CONTROLLER(self.board)
# Set up plot options
plot_opts = plotter.GetPlotOptions()
plot_opts.SetOutputDirectory(output_dir)
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_GERBER)
plot_opts.SetUseGerberProtelExtensions(use_protel_extensions)
plot_opts.SetUseAuxOrigin(use_aux_origin)
plot_opts.SetCreateGerberJobFile(generate_map_file)
plot_opts.SetSubtractMaskFromSilk(True)
# Plot specified layers or all copper layers
plotted_layers = []
if layers:
for layer_name in layers:
layer_id = self.board.GetLayerID(layer_name)
if layer_id >= 0:
plotter.PlotLayer(layer_id)
plotted_layers.append(layer_name)
else:
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
if self.board.IsLayerEnabled(layer_id):
layer_name = self.board.GetLayerName(layer_id)
plotter.PlotLayer(layer_id)
plotted_layers.append(layer_name)
# Generate drill files if requested
drill_files = []
if generate_drill_files:
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
drill_writer.SetFormat(True)
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
merge_npth = False # Keep plated/non-plated holes separate
drill_writer.SetOptions(merge_npth)
drill_writer.CreateDrillandMapFilesSet(output_dir, True, generate_map_file)
# Get list of generated drill files
for file in os.listdir(output_dir):
if file.endswith(".drl") or file.endswith(".cnc"):
drill_files.append(file)
return {
"success": True,
"message": "Exported Gerber files",
"files": {
"gerber": plotted_layers,
"drill": drill_files,
"map": ["job.gbrjob"] if generate_map_file else []
},
"outputDir": output_dir
}
except Exception as e:
logger.error(f"Error exporting Gerber files: {str(e)}")
return {
"success": False,
"message": "Failed to export Gerber files",
"errorDetails": str(e)
}
def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Export PDF files"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
output_path = params.get("outputPath")
layers = params.get("layers", [])
black_and_white = params.get("blackAndWhite", False)
frame_reference = params.get("frameReference", True)
page_size = params.get("pageSize", "A4")
if not output_path:
return {
"success": False,
"message": "Missing output path",
"errorDetails": "outputPath parameter is required"
}
# Create output directory if it doesn't exist
output_path = os.path.abspath(os.path.expanduser(output_path))
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Create plot controller
plotter = pcbnew.PLOT_CONTROLLER(self.board)
# Set up plot options
plot_opts = plotter.GetPlotOptions()
plot_opts.SetOutputDirectory(os.path.dirname(output_path))
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_PDF)
plot_opts.SetPlotFrameRef(frame_reference)
plot_opts.SetPlotValue(True)
plot_opts.SetPlotReference(True)
plot_opts.SetMonochrome(black_and_white)
# Set page size
page_sizes = {
"A4": (297, 210),
"A3": (420, 297),
"A2": (594, 420),
"A1": (841, 594),
"A0": (1189, 841),
"Letter": (279.4, 215.9),
"Legal": (355.6, 215.9),
"Tabloid": (431.8, 279.4)
}
if page_size in page_sizes:
height, width = page_sizes[page_size]
plot_opts.SetPageSettings((width, height))
# Plot specified layers or all enabled layers
plotted_layers = []
if layers:
for layer_name in layers:
layer_id = self.board.GetLayerID(layer_name)
if layer_id >= 0:
plotter.PlotLayer(layer_id)
plotted_layers.append(layer_name)
else:
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
if self.board.IsLayerEnabled(layer_id):
layer_name = self.board.GetLayerName(layer_id)
plotter.PlotLayer(layer_id)
plotted_layers.append(layer_name)
return {
"success": True,
"message": "Exported PDF file",
"file": {
"path": output_path,
"layers": plotted_layers,
"pageSize": page_size
}
}
except Exception as e:
logger.error(f"Error exporting PDF file: {str(e)}")
return {
"success": False,
"message": "Failed to export PDF file",
"errorDetails": str(e)
}
def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Export SVG files"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
output_path = params.get("outputPath")
layers = params.get("layers", [])
black_and_white = params.get("blackAndWhite", False)
include_components = params.get("includeComponents", True)
if not output_path:
return {
"success": False,
"message": "Missing output path",
"errorDetails": "outputPath parameter is required"
}
# Create output directory if it doesn't exist
output_path = os.path.abspath(os.path.expanduser(output_path))
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Create plot controller
plotter = pcbnew.PLOT_CONTROLLER(self.board)
# Set up plot options
plot_opts = plotter.GetPlotOptions()
plot_opts.SetOutputDirectory(os.path.dirname(output_path))
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_SVG)
plot_opts.SetPlotValue(include_components)
plot_opts.SetPlotReference(include_components)
plot_opts.SetMonochrome(black_and_white)
# Plot specified layers or all enabled layers
plotted_layers = []
if layers:
for layer_name in layers:
layer_id = self.board.GetLayerID(layer_name)
if layer_id >= 0:
plotter.PlotLayer(layer_id)
plotted_layers.append(layer_name)
else:
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
if self.board.IsLayerEnabled(layer_id):
layer_name = self.board.GetLayerName(layer_id)
plotter.PlotLayer(layer_id)
plotted_layers.append(layer_name)
return {
"success": True,
"message": "Exported SVG file",
"file": {
"path": output_path,
"layers": plotted_layers
}
}
except Exception as e:
logger.error(f"Error exporting SVG file: {str(e)}")
return {
"success": False,
"message": "Failed to export SVG file",
"errorDetails": str(e)
}
def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Export 3D model files"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
output_path = params.get("outputPath")
format = params.get("format", "STEP")
include_components = params.get("includeComponents", True)
include_copper = params.get("includeCopper", True)
include_solder_mask = params.get("includeSolderMask", True)
include_silkscreen = params.get("includeSilkscreen", True)
if not output_path:
return {
"success": False,
"message": "Missing output path",
"errorDetails": "outputPath parameter is required"
}
# Create output directory if it doesn't exist
output_path = os.path.abspath(os.path.expanduser(output_path))
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Get 3D viewer
viewer = self.board.Get3DViewer()
if not viewer:
return {
"success": False,
"message": "3D viewer not available",
"errorDetails": "Could not initialize 3D viewer"
}
# Set export options
viewer.SetCopperLayersOn(include_copper)
viewer.SetSolderMaskLayersOn(include_solder_mask)
viewer.SetSilkScreenLayersOn(include_silkscreen)
viewer.Set3DModelsOn(include_components)
# Export based on format
if format == "STEP":
viewer.ExportSTEPFile(output_path)
elif format == "VRML":
viewer.ExportVRMLFile(output_path)
else:
return {
"success": False,
"message": "Unsupported format",
"errorDetails": f"Format {format} is not supported"
}
return {
"success": True,
"message": f"Exported {format} file",
"file": {
"path": output_path,
"format": format
}
}
except Exception as e:
logger.error(f"Error exporting 3D model: {str(e)}")
return {
"success": False,
"message": "Failed to export 3D model",
"errorDetails": str(e)
}
def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Export Bill of Materials"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
output_path = params.get("outputPath")
format = params.get("format", "CSV")
group_by_value = params.get("groupByValue", True)
include_attributes = params.get("includeAttributes", [])
if not output_path:
return {
"success": False,
"message": "Missing output path",
"errorDetails": "outputPath parameter is required"
}
# Create output directory if it doesn't exist
output_path = os.path.abspath(os.path.expanduser(output_path))
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Get all components
components = []
for module in self.board.GetFootprints():
component = {
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFootprintName(),
"layer": self.board.GetLayerName(module.GetLayer())
}
# Add requested attributes
for attr in include_attributes:
if hasattr(module, f"Get{attr}"):
component[attr] = getattr(module, f"Get{attr}")()
components.append(component)
# Group by value if requested
if group_by_value:
grouped = {}
for comp in components:
key = f"{comp['value']}_{comp['footprint']}"
if key not in grouped:
grouped[key] = {
"value": comp["value"],
"footprint": comp["footprint"],
"quantity": 1,
"references": [comp["reference"]]
}
else:
grouped[key]["quantity"] += 1
grouped[key]["references"].append(comp["reference"])
components = list(grouped.values())
# Export based on format
if format == "CSV":
self._export_bom_csv(output_path, components)
elif format == "XML":
self._export_bom_xml(output_path, components)
elif format == "HTML":
self._export_bom_html(output_path, components)
elif format == "JSON":
self._export_bom_json(output_path, components)
else:
return {
"success": False,
"message": "Unsupported format",
"errorDetails": f"Format {format} is not supported"
}
return {
"success": True,
"message": f"Exported BOM to {format}",
"file": {
"path": output_path,
"format": format,
"componentCount": len(components)
}
}
except Exception as e:
logger.error(f"Error exporting BOM: {str(e)}")
return {
"success": False,
"message": "Failed to export BOM",
"errorDetails": str(e)
}
def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
"""Export BOM to CSV format"""
import csv
with open(path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=components[0].keys())
writer.writeheader()
writer.writerows(components)
def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
"""Export BOM to XML format"""
import xml.etree.ElementTree as ET
root = ET.Element("bom")
for comp in components:
comp_elem = ET.SubElement(root, "component")
for key, value in comp.items():
elem = ET.SubElement(comp_elem, key)
elem.text = str(value)
tree = ET.ElementTree(root)
tree.write(path, encoding='utf-8', xml_declaration=True)
def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
"""Export BOM to HTML format"""
html = ["<html><head><title>Bill of Materials</title></head><body>"]
html.append("<table border='1'><tr>")
# Headers
for key in components[0].keys():
html.append(f"<th>{key}</th>")
html.append("</tr>")
# Data
for comp in components:
html.append("<tr>")
for value in comp.values():
html.append(f"<td>{value}</td>")
html.append("</tr>")
html.append("</table></body></html>")
with open(path, 'w') as f:
f.write("\n".join(html))
def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
"""Export BOM to JSON format"""
import json
with open(path, 'w') as f:
json.dump({"components": components}, f, indent=2)
```
--------------------------------------------------------------------------------
/python/kicad_interface.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
"""
KiCAD Python Interface Script for Model Context Protocol
This script handles communication between the MCP TypeScript server
and KiCAD's Python API (pcbnew). It receives commands via stdin as
JSON and returns responses via stdout also as JSON.
"""
import sys
import json
import traceback
import logging
import os
from typing import Dict, Any, Optional
# Configure logging
log_dir = os.path.join(os.path.expanduser('~'), '.kicad-mcp', 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'kicad_interface.log')
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler(sys.stderr)
]
)
logger = logging.getLogger('kicad_interface')
# Log Python environment details
logger.info(f"Python version: {sys.version}")
logger.info(f"Python executable: {sys.executable}")
# Add utils directory to path for imports
utils_dir = os.path.join(os.path.dirname(__file__))
if utils_dir not in sys.path:
sys.path.insert(0, utils_dir)
# Import platform helper and add KiCAD paths
from utils.platform_helper import PlatformHelper
from utils.kicad_process import check_and_launch_kicad, KiCADProcessManager
logger.info(f"Detecting KiCAD Python paths for {PlatformHelper.get_platform_name()}...")
paths_added = PlatformHelper.add_kicad_to_python_path()
if paths_added:
logger.info("Successfully added KiCAD Python paths to sys.path")
else:
logger.warning("No KiCAD Python paths found - attempting to import pcbnew from system path")
logger.info(f"Current Python path: {sys.path}")
# Check if auto-launch is enabled
AUTO_LAUNCH_KICAD = os.environ.get("KICAD_AUTO_LAUNCH", "false").lower() == "true"
if AUTO_LAUNCH_KICAD:
logger.info("KiCAD auto-launch enabled")
# Import KiCAD's Python API
try:
logger.info("Attempting to import pcbnew module...")
import pcbnew # type: ignore
logger.info(f"Successfully imported pcbnew module from: {pcbnew.__file__}")
logger.info(f"pcbnew version: {pcbnew.GetBuildVersion()}")
except ImportError as e:
logger.error(f"Failed to import pcbnew module: {e}")
logger.error(f"Current sys.path: {sys.path}")
error_response = {
"success": False,
"message": "Failed to import pcbnew module",
"errorDetails": f"Error: {str(e)}\nPython path: {sys.path}"
}
print(json.dumps(error_response))
sys.exit(1)
except Exception as e:
logger.error(f"Unexpected error importing pcbnew: {e}")
logger.error(traceback.format_exc())
error_response = {
"success": False,
"message": "Error importing pcbnew module",
"errorDetails": str(e)
}
print(json.dumps(error_response))
sys.exit(1)
# Import command handlers
try:
logger.info("Importing command handlers...")
from commands.project import ProjectCommands
from commands.board import BoardCommands
from commands.component import ComponentCommands
from commands.routing import RoutingCommands
from commands.design_rules import DesignRuleCommands
from commands.export import ExportCommands
from commands.schematic import SchematicManager
from commands.component_schematic import ComponentManager
from commands.connection_schematic import ConnectionManager
from commands.library_schematic import LibraryManager
logger.info("Successfully imported all command handlers")
except ImportError as e:
logger.error(f"Failed to import command handlers: {e}")
error_response = {
"success": False,
"message": "Failed to import command handlers",
"errorDetails": str(e)
}
print(json.dumps(error_response))
sys.exit(1)
class KiCADInterface:
"""Main interface class to handle KiCAD operations"""
def __init__(self):
"""Initialize the interface and command handlers"""
self.board = None
self.project_filename = None
logger.info("Initializing command handlers...")
# Initialize command handlers
self.project_commands = ProjectCommands(self.board)
self.board_commands = BoardCommands(self.board)
self.component_commands = ComponentCommands(self.board)
self.routing_commands = RoutingCommands(self.board)
self.design_rule_commands = DesignRuleCommands(self.board)
self.export_commands = ExportCommands(self.board)
# Schematic-related classes don't need board reference
# as they operate directly on schematic files
# Command routing dictionary
self.command_routes = {
# Project commands
"create_project": self.project_commands.create_project,
"open_project": self.project_commands.open_project,
"save_project": self.project_commands.save_project,
"get_project_info": self.project_commands.get_project_info,
# Board commands
"set_board_size": self.board_commands.set_board_size,
"add_layer": self.board_commands.add_layer,
"set_active_layer": self.board_commands.set_active_layer,
"get_board_info": self.board_commands.get_board_info,
"get_layer_list": self.board_commands.get_layer_list,
"get_board_2d_view": self.board_commands.get_board_2d_view,
"add_board_outline": self.board_commands.add_board_outline,
"add_mounting_hole": self.board_commands.add_mounting_hole,
"add_text": self.board_commands.add_text,
"add_board_text": self.board_commands.add_text, # Alias for TypeScript tool
# Component commands
"place_component": self.component_commands.place_component,
"move_component": self.component_commands.move_component,
"rotate_component": self.component_commands.rotate_component,
"delete_component": self.component_commands.delete_component,
"edit_component": self.component_commands.edit_component,
"get_component_properties": self.component_commands.get_component_properties,
"get_component_list": self.component_commands.get_component_list,
"place_component_array": self.component_commands.place_component_array,
"align_components": self.component_commands.align_components,
"duplicate_component": self.component_commands.duplicate_component,
# Routing commands
"add_net": self.routing_commands.add_net,
"route_trace": self.routing_commands.route_trace,
"add_via": self.routing_commands.add_via,
"delete_trace": self.routing_commands.delete_trace,
"get_nets_list": self.routing_commands.get_nets_list,
"create_netclass": self.routing_commands.create_netclass,
"add_copper_pour": self.routing_commands.add_copper_pour,
"route_differential_pair": self.routing_commands.route_differential_pair,
# Design rule commands
"set_design_rules": self.design_rule_commands.set_design_rules,
"get_design_rules": self.design_rule_commands.get_design_rules,
"run_drc": self.design_rule_commands.run_drc,
"get_drc_violations": self.design_rule_commands.get_drc_violations,
# Export commands
"export_gerber": self.export_commands.export_gerber,
"export_pdf": self.export_commands.export_pdf,
"export_svg": self.export_commands.export_svg,
"export_3d": self.export_commands.export_3d,
"export_bom": self.export_commands.export_bom,
# Schematic commands
"create_schematic": self._handle_create_schematic,
"load_schematic": self._handle_load_schematic,
"add_schematic_component": self._handle_add_schematic_component,
"add_schematic_wire": self._handle_add_schematic_wire,
"list_schematic_libraries": self._handle_list_schematic_libraries,
"export_schematic_pdf": self._handle_export_schematic_pdf,
# UI/Process management commands
"check_kicad_ui": self._handle_check_kicad_ui,
"launch_kicad_ui": self._handle_launch_kicad_ui
}
logger.info("KiCAD interface initialized")
def handle_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""Route command to appropriate handler"""
logger.info(f"Handling command: {command}")
logger.debug(f"Command parameters: {params}")
try:
# Get the handler for the command
handler = self.command_routes.get(command)
if handler:
# Execute the command
result = handler(params)
logger.debug(f"Command result: {result}")
# Update board reference if command was successful
if result.get("success", False):
if command == "create_project" or command == "open_project":
logger.info("Updating board reference...")
# Get board from the project commands handler
self.board = self.project_commands.board
self._update_command_handlers()
return result
else:
logger.error(f"Unknown command: {command}")
return {
"success": False,
"message": f"Unknown command: {command}",
"errorDetails": "The specified command is not supported"
}
except Exception as e:
# Get the full traceback
traceback_str = traceback.format_exc()
logger.error(f"Error handling command {command}: {str(e)}\n{traceback_str}")
return {
"success": False,
"message": f"Error handling command: {command}",
"errorDetails": f"{str(e)}\n{traceback_str}"
}
def _update_command_handlers(self):
"""Update board reference in all command handlers"""
logger.debug("Updating board reference in command handlers")
self.project_commands.board = self.board
self.board_commands.board = self.board
self.component_commands.board = self.board
self.routing_commands.board = self.board
self.design_rule_commands.board = self.board
self.export_commands.board = self.board
# Schematic command handlers
def _handle_create_schematic(self, params):
"""Create a new schematic"""
logger.info("Creating schematic")
try:
project_name = params.get("projectName")
path = params.get("path", ".")
metadata = params.get("metadata", {})
if not project_name:
return {"success": False, "message": "Project name is required"}
schematic = SchematicManager.create_schematic(project_name, metadata)
file_path = f"{path}/{project_name}.kicad_sch"
success = SchematicManager.save_schematic(schematic, file_path)
return {"success": success, "file_path": file_path}
except Exception as e:
logger.error(f"Error creating schematic: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_load_schematic(self, params):
"""Load an existing schematic"""
logger.info("Loading schematic")
try:
filename = params.get("filename")
if not filename:
return {"success": False, "message": "Filename is required"}
schematic = SchematicManager.load_schematic(filename)
success = schematic is not None
if success:
metadata = SchematicManager.get_schematic_metadata(schematic)
return {"success": success, "metadata": metadata}
else:
return {"success": False, "message": "Failed to load schematic"}
except Exception as e:
logger.error(f"Error loading schematic: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_add_schematic_component(self, params):
"""Add a component to a schematic"""
logger.info("Adding component to schematic")
try:
schematic_path = params.get("schematicPath")
component = params.get("component", {})
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not component:
return {"success": False, "message": "Component definition is required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
component_obj = ComponentManager.add_component(schematic, component)
success = component_obj is not None
if success:
SchematicManager.save_schematic(schematic, schematic_path)
return {"success": True}
else:
return {"success": False, "message": "Failed to add component"}
except Exception as e:
logger.error(f"Error adding component to schematic: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_add_schematic_wire(self, params):
"""Add a wire to a schematic"""
logger.info("Adding wire to schematic")
try:
schematic_path = params.get("schematicPath")
start_point = params.get("startPoint")
end_point = params.get("endPoint")
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not start_point or not end_point:
return {"success": False, "message": "Start and end points are required"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
wire = ConnectionManager.add_wire(schematic, start_point, end_point)
success = wire is not None
if success:
SchematicManager.save_schematic(schematic, schematic_path)
return {"success": True}
else:
return {"success": False, "message": "Failed to add wire"}
except Exception as e:
logger.error(f"Error adding wire to schematic: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_list_schematic_libraries(self, params):
"""List available symbol libraries"""
logger.info("Listing schematic libraries")
try:
search_paths = params.get("searchPaths")
libraries = LibraryManager.list_available_libraries(search_paths)
return {"success": True, "libraries": libraries}
except Exception as e:
logger.error(f"Error listing schematic libraries: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_export_schematic_pdf(self, params):
"""Export schematic to PDF"""
logger.info("Exporting schematic to PDF")
try:
schematic_path = params.get("schematicPath")
output_path = params.get("outputPath")
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not output_path:
return {"success": False, "message": "Output path is required"}
import subprocess
result = subprocess.run(
["kicad-cli", "sch", "export", "pdf", "--output", output_path, schematic_path],
capture_output=True,
text=True
)
success = result.returncode == 0
message = result.stderr if not success else ""
return {"success": success, "message": message}
except Exception as e:
logger.error(f"Error exporting schematic to PDF: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_check_kicad_ui(self, params):
"""Check if KiCAD UI is running"""
logger.info("Checking if KiCAD UI is running")
try:
manager = KiCADProcessManager()
is_running = manager.is_running()
processes = manager.get_process_info() if is_running else []
return {
"success": True,
"running": is_running,
"processes": processes,
"message": "KiCAD is running" if is_running else "KiCAD is not running"
}
except Exception as e:
logger.error(f"Error checking KiCAD UI status: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_launch_kicad_ui(self, params):
"""Launch KiCAD UI"""
logger.info("Launching KiCAD UI")
try:
project_path = params.get("projectPath")
auto_launch = params.get("autoLaunch", AUTO_LAUNCH_KICAD)
# Convert project path to Path object if provided
from pathlib import Path
path_obj = Path(project_path) if project_path else None
result = check_and_launch_kicad(path_obj, auto_launch)
return {
"success": True,
**result
}
except Exception as e:
logger.error(f"Error launching KiCAD UI: {str(e)}")
return {"success": False, "message": str(e)}
def main():
"""Main entry point"""
logger.info("Starting KiCAD interface...")
interface = KiCADInterface()
try:
logger.info("Processing commands from stdin...")
# Process commands from stdin
for line in sys.stdin:
try:
# Parse command
logger.debug(f"Received input: {line.strip()}")
command_data = json.loads(line)
command = command_data.get("command")
params = command_data.get("params", {})
if not command:
logger.error("Missing command field")
response = {
"success": False,
"message": "Missing command",
"errorDetails": "The command field is required"
}
else:
# Handle command
response = interface.handle_command(command, params)
# Send response
logger.debug(f"Sending response: {response}")
print(json.dumps(response))
sys.stdout.flush()
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON input: {str(e)}")
response = {
"success": False,
"message": "Invalid JSON input",
"errorDetails": str(e)
}
print(json.dumps(response))
sys.stdout.flush()
except KeyboardInterrupt:
logger.info("KiCAD interface stopped")
sys.exit(0)
except Exception as e:
logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
sys.exit(1)
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/python/commands/routing.py:
--------------------------------------------------------------------------------
```python
"""
Routing-related command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
import math
from typing import Dict, Any, Optional, List, Tuple
logger = logging.getLogger('kicad_interface')
class RoutingCommands:
"""Handles routing-related KiCAD operations"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a new net to the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
name = params.get("name")
net_class = params.get("class")
if not name:
return {
"success": False,
"message": "Missing net name",
"errorDetails": "name parameter is required"
}
# Create new net
netinfo = self.board.GetNetInfo()
net = netinfo.FindNet(name)
if not net:
net = netinfo.AddNet(name)
# Set net class if provided
if net_class:
net_classes = self.board.GetNetClasses()
if net_classes.Find(net_class):
net.SetClass(net_classes.Find(net_class))
return {
"success": True,
"message": f"Added net: {name}",
"net": {
"name": name,
"class": net_class if net_class else "Default",
"netcode": net.GetNetCode()
}
}
except Exception as e:
logger.error(f"Error adding net: {str(e)}")
return {
"success": False,
"message": "Failed to add net",
"errorDetails": str(e)
}
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Route a trace between two points or pads"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
start = params.get("start")
end = params.get("end")
layer = params.get("layer", "F.Cu")
width = params.get("width")
net = params.get("net")
via = params.get("via", False)
if not start or not end:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "start and end points are required"
}
# Get layer ID
layer_id = self.board.GetLayerID(layer)
if layer_id < 0:
return {
"success": False,
"message": "Invalid layer",
"errorDetails": f"Layer '{layer}' does not exist"
}
# Get start point
start_point = self._get_point(start)
end_point = self._get_point(end)
# Create track segment
track = pcbnew.PCB_TRACK(self.board)
track.SetStart(start_point)
track.SetEnd(end_point)
track.SetLayer(layer_id)
# Set width (default to board's current track width)
if width:
track.SetWidth(int(width * 1000000)) # Convert mm to nm
else:
track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
# Set net if provided
if net:
netinfo = self.board.GetNetInfo()
net_obj = netinfo.FindNet(net)
if net_obj:
track.SetNet(net_obj)
# Add track to board
self.board.Add(track)
# Add via if requested and net is specified
if via and net:
via_point = end_point
self.add_via({
"position": {
"x": via_point.x / 1000000,
"y": via_point.y / 1000000,
"unit": "mm"
},
"net": net
})
return {
"success": True,
"message": "Added trace",
"trace": {
"start": {
"x": start_point.x / 1000000,
"y": start_point.y / 1000000,
"unit": "mm"
},
"end": {
"x": end_point.x / 1000000,
"y": end_point.y / 1000000,
"unit": "mm"
},
"layer": layer,
"width": track.GetWidth() / 1000000,
"net": net
}
}
except Exception as e:
logger.error(f"Error routing trace: {str(e)}")
return {
"success": False,
"message": "Failed to route trace",
"errorDetails": str(e)
}
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a via at the specified location"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
position = params.get("position")
size = params.get("size")
drill = params.get("drill")
net = params.get("net")
from_layer = params.get("from_layer", "F.Cu")
to_layer = params.get("to_layer", "B.Cu")
if not position:
return {
"success": False,
"message": "Missing position",
"errorDetails": "position parameter is required"
}
# Create via
via = pcbnew.PCB_VIA(self.board)
# Set position
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
# Set size and drill (default to board's current via settings)
design_settings = self.board.GetDesignSettings()
via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
# Set layers
from_id = self.board.GetLayerID(from_layer)
to_id = self.board.GetLayerID(to_layer)
if from_id < 0 or to_id < 0:
return {
"success": False,
"message": "Invalid layer",
"errorDetails": "Specified layers do not exist"
}
via.SetLayerPair(from_id, to_id)
# Set net if provided
if net:
netinfo = self.board.GetNetInfo()
net_obj = netinfo.FindNet(net)
if net_obj:
via.SetNet(net_obj)
# Add via to board
self.board.Add(via)
return {
"success": True,
"message": "Added via",
"via": {
"position": {
"x": position["x"],
"y": position["y"],
"unit": position["unit"]
},
"size": via.GetWidth() / 1000000,
"drill": via.GetDrill() / 1000000,
"from_layer": from_layer,
"to_layer": to_layer,
"net": net
}
}
except Exception as e:
logger.error(f"Error adding via: {str(e)}")
return {
"success": False,
"message": "Failed to add via",
"errorDetails": str(e)
}
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Delete a trace from the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
trace_uuid = params.get("traceUuid")
position = params.get("position")
if not trace_uuid and not position:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "Either traceUuid or position must be provided"
}
# Find track by UUID
if trace_uuid:
track = None
for item in self.board.Tracks():
if str(item.m_Uuid) == trace_uuid:
track = item
break
if not track:
return {
"success": False,
"message": "Track not found",
"errorDetails": f"Could not find track with UUID: {trace_uuid}"
}
self.board.Remove(track)
return {
"success": True,
"message": f"Deleted track: {trace_uuid}"
}
# Find track by position
if position:
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
point = pcbnew.VECTOR2I(x_nm, y_nm)
# Find closest track
closest_track = None
min_distance = float('inf')
for track in self.board.Tracks():
dist = self._point_to_track_distance(point, track)
if dist < min_distance:
min_distance = dist
closest_track = track
if closest_track and min_distance < 1000000: # Within 1mm
self.board.Remove(closest_track)
return {
"success": True,
"message": "Deleted track at specified position"
}
else:
return {
"success": False,
"message": "No track found",
"errorDetails": "No track found near specified position"
}
except Exception as e:
logger.error(f"Error deleting trace: {str(e)}")
return {
"success": False,
"message": "Failed to delete trace",
"errorDetails": str(e)
}
def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get a list of all nets in the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
nets = []
netinfo = self.board.GetNetInfo()
for net_code in range(netinfo.GetNetCount()):
net = netinfo.GetNetItem(net_code)
if net:
nets.append({
"name": net.GetNetname(),
"code": net.GetNetCode(),
"class": net.GetClassName()
})
return {
"success": True,
"nets": nets
}
except Exception as e:
logger.error(f"Error getting nets list: {str(e)}")
return {
"success": False,
"message": "Failed to get nets list",
"errorDetails": str(e)
}
def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Create a new net class with specified properties"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
name = params.get("name")
clearance = params.get("clearance")
track_width = params.get("trackWidth")
via_diameter = params.get("viaDiameter")
via_drill = params.get("viaDrill")
uvia_diameter = params.get("uviaDiameter")
uvia_drill = params.get("uviaDrill")
diff_pair_width = params.get("diffPairWidth")
diff_pair_gap = params.get("diffPairGap")
nets = params.get("nets", [])
if not name:
return {
"success": False,
"message": "Missing netclass name",
"errorDetails": "name parameter is required"
}
# Get net classes
net_classes = self.board.GetNetClasses()
# Create new net class if it doesn't exist
if not net_classes.Find(name):
netclass = pcbnew.NETCLASS(name)
net_classes.Add(netclass)
else:
netclass = net_classes.Find(name)
# Set properties
scale = 1000000 # mm to nm
if clearance is not None:
netclass.SetClearance(int(clearance * scale))
if track_width is not None:
netclass.SetTrackWidth(int(track_width * scale))
if via_diameter is not None:
netclass.SetViaDiameter(int(via_diameter * scale))
if via_drill is not None:
netclass.SetViaDrill(int(via_drill * scale))
if uvia_diameter is not None:
netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
if uvia_drill is not None:
netclass.SetMicroViaDrill(int(uvia_drill * scale))
if diff_pair_width is not None:
netclass.SetDiffPairWidth(int(diff_pair_width * scale))
if diff_pair_gap is not None:
netclass.SetDiffPairGap(int(diff_pair_gap * scale))
# Add nets to net class
netinfo = self.board.GetNetInfo()
for net_name in nets:
net = netinfo.FindNet(net_name)
if net:
net.SetClass(netclass)
return {
"success": True,
"message": f"Created net class: {name}",
"netClass": {
"name": name,
"clearance": netclass.GetClearance() / scale,
"trackWidth": netclass.GetTrackWidth() / scale,
"viaDiameter": netclass.GetViaDiameter() / scale,
"viaDrill": netclass.GetViaDrill() / scale,
"uviaDiameter": netclass.GetMicroViaDiameter() / scale,
"uviaDrill": netclass.GetMicroViaDrill() / scale,
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
"diffPairGap": netclass.GetDiffPairGap() / scale,
"nets": nets
}
}
except Exception as e:
logger.error(f"Error creating net class: {str(e)}")
return {
"success": False,
"message": "Failed to create net class",
"errorDetails": str(e)
}
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a copper pour (zone) to the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
layer = params.get("layer", "F.Cu")
net = params.get("net")
clearance = params.get("clearance")
min_width = params.get("minWidth", 0.2)
points = params.get("points", [])
priority = params.get("priority", 0)
fill_type = params.get("fillType", "solid") # solid or hatched
if not points or len(points) < 3:
return {
"success": False,
"message": "Missing points",
"errorDetails": "At least 3 points are required for copper pour outline"
}
# Get layer ID
layer_id = self.board.GetLayerID(layer)
if layer_id < 0:
return {
"success": False,
"message": "Invalid layer",
"errorDetails": f"Layer '{layer}' does not exist"
}
# Create zone
zone = pcbnew.ZONE(self.board)
zone.SetLayer(layer_id)
# Set net if provided
if net:
netinfo = self.board.GetNetInfo()
net_obj = netinfo.FindNet(net)
if net_obj:
zone.SetNet(net_obj)
# Set zone properties
scale = 1000000 # mm to nm
zone.SetPriority(priority)
if clearance is not None:
zone.SetLocalClearance(int(clearance * scale))
zone.SetMinThickness(int(min_width * scale))
# Set fill type
if fill_type == "hatched":
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
else:
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGON)
# Create outline
outline = zone.Outline()
# Add points to outline
for point in points:
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
x_nm = int(point["x"] * scale)
y_nm = int(point["y"] * scale)
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))
# Add zone to board
self.board.Add(zone)
# Fill zone
filler = pcbnew.ZONE_FILLER(self.board)
filler.Fill(self.board.Zones())
return {
"success": True,
"message": "Added copper pour",
"pour": {
"layer": layer,
"net": net,
"clearance": clearance,
"minWidth": min_width,
"priority": priority,
"fillType": fill_type,
"pointCount": len(points)
}
}
except Exception as e:
logger.error(f"Error adding copper pour: {str(e)}")
return {
"success": False,
"message": "Failed to add copper pour",
"errorDetails": str(e)
}
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Route a differential pair between two sets of points or pads"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
start_pos = params.get("startPos")
end_pos = params.get("endPos")
net_pos = params.get("netPos")
net_neg = params.get("netNeg")
layer = params.get("layer", "F.Cu")
width = params.get("width")
gap = params.get("gap")
if not start_pos or not end_pos or not net_pos or not net_neg:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "startPos, endPos, netPos, and netNeg are required"
}
# Get layer ID
layer_id = self.board.GetLayerID(layer)
if layer_id < 0:
return {
"success": False,
"message": "Invalid layer",
"errorDetails": f"Layer '{layer}' does not exist"
}
# Get nets
netinfo = self.board.GetNetInfo()
net_pos_obj = netinfo.FindNet(net_pos)
net_neg_obj = netinfo.FindNet(net_neg)
if not net_pos_obj or not net_neg_obj:
return {
"success": False,
"message": "Nets not found",
"errorDetails": "One or both nets specified for the differential pair do not exist"
}
# Get start and end points
start_point = self._get_point(start_pos)
end_point = self._get_point(end_pos)
# Calculate offset vectors for the two traces
# First, get the direction vector from start to end
dx = end_point.x - start_point.x
dy = end_point.y - start_point.y
length = math.sqrt(dx * dx + dy * dy)
if length <= 0:
return {
"success": False,
"message": "Invalid points",
"errorDetails": "Start and end points must be different"
}
# Normalize direction vector
dx /= length
dy /= length
# Get perpendicular vector
px = -dy
py = dx
# Set default gap if not provided
if gap is None:
gap = 0.2 # mm
# Convert to nm
gap_nm = int(gap * 1000000)
# Calculate offsets
offset_x = int(px * gap_nm / 2)
offset_y = int(py * gap_nm / 2)
# Create positive and negative trace points
pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
# Create positive trace
pos_track = pcbnew.PCB_TRACK(self.board)
pos_track.SetStart(pos_start)
pos_track.SetEnd(pos_end)
pos_track.SetLayer(layer_id)
pos_track.SetNet(net_pos_obj)
# Create negative trace
neg_track = pcbnew.PCB_TRACK(self.board)
neg_track.SetStart(neg_start)
neg_track.SetEnd(neg_end)
neg_track.SetLayer(layer_id)
neg_track.SetNet(net_neg_obj)
# Set width
if width:
trace_width_nm = int(width * 1000000)
pos_track.SetWidth(trace_width_nm)
neg_track.SetWidth(trace_width_nm)
else:
# Get default width from design rules or net class
trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
pos_track.SetWidth(trace_width)
neg_track.SetWidth(trace_width)
# Add tracks to board
self.board.Add(pos_track)
self.board.Add(neg_track)
return {
"success": True,
"message": "Added differential pair traces",
"diffPair": {
"posNet": net_pos,
"negNet": net_neg,
"layer": layer,
"width": pos_track.GetWidth() / 1000000,
"gap": gap,
"length": length / 1000000
}
}
except Exception as e:
logger.error(f"Error routing differential pair: {str(e)}")
return {
"success": False,
"message": "Failed to route differential pair",
"errorDetails": str(e)
}
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
"""Convert point specification to KiCAD point"""
if "x" in point_spec and "y" in point_spec:
scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
x_nm = int(point_spec["x"] * scale)
y_nm = int(point_spec["y"] * scale)
return pcbnew.VECTOR2I(x_nm, y_nm)
elif "pad" in point_spec and "componentRef" in point_spec:
module = self.board.FindFootprintByReference(point_spec["componentRef"])
if module:
pad = module.FindPadByName(point_spec["pad"])
if pad:
return pad.GetPosition()
raise ValueError("Invalid point specification")
def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
"""Calculate distance from point to track segment"""
start = track.GetStart()
end = track.GetEnd()
# Vector from start to end
v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
# Vector from start to point
w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
# Length of track squared
c1 = v.x * v.x + v.y * v.y
if c1 == 0:
return self._point_distance(point, start)
# Projection coefficient
c2 = float(w.x * v.x + w.y * v.y) / c1
if c2 < 0:
return self._point_distance(point, start)
elif c2 > 1:
return self._point_distance(point, end)
# Point on line
proj = pcbnew.VECTOR2I(
int(start.x + c2 * v.x),
int(start.y + c2 * v.y)
)
return self._point_distance(point, proj)
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
"""Calculate distance between two points"""
dx = p1.x - p2.x
dy = p1.y - p2.y
return (dx * dx + dy * dy) ** 0.5
```
--------------------------------------------------------------------------------
/python/commands/component.py:
--------------------------------------------------------------------------------
```python
"""
Component-related command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
import math
from typing import Dict, Any, Optional, List, Tuple
import base64
logger = logging.getLogger('kicad_interface')
class ComponentCommands:
"""Handles component-related KiCAD operations"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Place a component on the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
# Get parameters
component_id = params.get("componentId")
position = params.get("position")
reference = params.get("reference")
value = params.get("value")
footprint = params.get("footprint")
rotation = params.get("rotation", 0)
layer = params.get("layer", "F.Cu")
if not component_id or not position:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "componentId and position are required"
}
# Create new module (footprint)
module = pcbnew.FootprintLoad(self.board.GetLibraryPath(), component_id)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {component_id}"
}
# Set position
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
# Set reference if provided
if reference:
module.SetReference(reference)
# Set value if provided
if value:
module.SetValue(value)
# Set footprint if provided
if footprint:
module.SetFootprintName(footprint)
# Set rotation
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
# Set layer
layer_id = self.board.GetLayerID(layer)
if layer_id >= 0:
module.SetLayer(layer_id)
# Add to board
self.board.Add(module)
return {
"success": True,
"message": f"Placed component: {component_id}",
"component": {
"reference": module.GetReference(),
"value": module.GetValue(),
"position": {
"x": position["x"],
"y": position["y"],
"unit": position["unit"]
},
"rotation": rotation,
"layer": layer
}
}
except Exception as e:
logger.error(f"Error placing component: {str(e)}")
return {
"success": False,
"message": "Failed to place component",
"errorDetails": str(e)
}
def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Move an existing component to a new position"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
position = params.get("position")
rotation = params.get("rotation")
if not reference or not position:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "reference and position are required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Set new position
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
# Set new rotation if provided
if rotation is not None:
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
return {
"success": True,
"message": f"Moved component: {reference}",
"component": {
"reference": reference,
"position": {
"x": position["x"],
"y": position["y"],
"unit": position["unit"]
},
"rotation": rotation if rotation is not None else module.GetOrientation() / 10
}
}
except Exception as e:
logger.error(f"Error moving component: {str(e)}")
return {
"success": False,
"message": "Failed to move component",
"errorDetails": str(e)
}
def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Rotate an existing component"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
angle = params.get("angle")
if not reference or angle is None:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "reference and angle are required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Set rotation
module.SetOrientation(angle * 10) # KiCAD uses decidegrees
return {
"success": True,
"message": f"Rotated component: {reference}",
"component": {
"reference": reference,
"rotation": angle
}
}
except Exception as e:
logger.error(f"Error rotating component: {str(e)}")
return {
"success": False,
"message": "Failed to rotate component",
"errorDetails": str(e)
}
def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Delete a component from the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Remove from board
self.board.Remove(module)
return {
"success": True,
"message": f"Deleted component: {reference}"
}
except Exception as e:
logger.error(f"Error deleting component: {str(e)}")
return {
"success": False,
"message": "Failed to delete component",
"errorDetails": str(e)
}
def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Edit the properties of an existing component"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
new_reference = params.get("newReference")
value = params.get("value")
footprint = params.get("footprint")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Update properties
if new_reference:
module.SetReference(new_reference)
if value:
module.SetValue(value)
if footprint:
module.SetFootprintName(footprint)
return {
"success": True,
"message": f"Updated component: {reference}",
"component": {
"reference": new_reference or reference,
"value": value or module.GetValue(),
"footprint": footprint or module.GetFootprintName()
}
}
except Exception as e:
logger.error(f"Error editing component: {str(e)}")
return {
"success": False,
"message": "Failed to edit component",
"errorDetails": str(e)
}
def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get detailed properties of a component"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Get position in mm
pos = module.GetPosition()
x_mm = pos.x / 1000000
y_mm = pos.y / 1000000
return {
"success": True,
"component": {
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFootprintName(),
"position": {
"x": x_mm,
"y": y_mm,
"unit": "mm"
},
"rotation": module.GetOrientation() / 10,
"layer": self.board.GetLayerName(module.GetLayer()),
"attributes": {
"smd": module.GetAttributes() & pcbnew.FP_SMD,
"through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
"virtual": module.GetAttributes() & pcbnew.FP_VIRTUAL
}
}
}
except Exception as e:
logger.error(f"Error getting component properties: {str(e)}")
return {
"success": False,
"message": "Failed to get component properties",
"errorDetails": str(e)
}
def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get a list of all components on the board"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
components = []
for module in self.board.GetFootprints():
pos = module.GetPosition()
x_mm = pos.x / 1000000
y_mm = pos.y / 1000000
components.append({
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFootprintName(),
"position": {
"x": x_mm,
"y": y_mm,
"unit": "mm"
},
"rotation": module.GetOrientation() / 10,
"layer": self.board.GetLayerName(module.GetLayer())
})
return {
"success": True,
"components": components
}
except Exception as e:
logger.error(f"Error getting component list: {str(e)}")
return {
"success": False,
"message": "Failed to get component list",
"errorDetails": str(e)
}
def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Place an array of components in a grid or circular pattern"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
component_id = params.get("componentId")
pattern = params.get("pattern", "grid") # grid or circular
count = params.get("count")
reference_prefix = params.get("referencePrefix", "U")
value = params.get("value")
if not component_id or not count:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "componentId and count are required"
}
if pattern == "grid":
start_position = params.get("startPosition")
rows = params.get("rows")
columns = params.get("columns")
spacing_x = params.get("spacingX")
spacing_y = params.get("spacingY")
rotation = params.get("rotation", 0)
layer = params.get("layer", "F.Cu")
if not start_position or not rows or not columns or not spacing_x or not spacing_y:
return {
"success": False,
"message": "Missing grid parameters",
"errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
}
if rows * columns != count:
return {
"success": False,
"message": "Invalid grid parameters",
"errorDetails": "rows * columns must equal count"
}
placed_components = self._place_grid_array(
component_id,
start_position,
rows,
columns,
spacing_x,
spacing_y,
reference_prefix,
value,
rotation,
layer
)
elif pattern == "circular":
center = params.get("center")
radius = params.get("radius")
angle_start = params.get("angleStart", 0)
angle_step = params.get("angleStep")
rotation_offset = params.get("rotationOffset", 0)
layer = params.get("layer", "F.Cu")
if not center or not radius or not angle_step:
return {
"success": False,
"message": "Missing circular parameters",
"errorDetails": "For circular pattern, center, radius, and angleStep are required"
}
placed_components = self._place_circular_array(
component_id,
center,
radius,
count,
angle_start,
angle_step,
reference_prefix,
value,
rotation_offset,
layer
)
else:
return {
"success": False,
"message": "Invalid pattern",
"errorDetails": "Pattern must be 'grid' or 'circular'"
}
return {
"success": True,
"message": f"Placed {count} components in {pattern} pattern",
"components": placed_components
}
except Exception as e:
logger.error(f"Error placing component array: {str(e)}")
return {
"success": False,
"message": "Failed to place component array",
"errorDetails": str(e)
}
def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Align multiple components along a line or distribute them evenly"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
references = params.get("references", [])
alignment = params.get("alignment", "horizontal") # horizontal, vertical, or edge
distribution = params.get("distribution", "none") # none, equal, or spacing
spacing = params.get("spacing")
if not references or len(references) < 2:
return {
"success": False,
"message": "Missing references",
"errorDetails": "At least two component references are required"
}
# Find all referenced components
components = []
for ref in references:
module = self.board.FindFootprintByReference(ref)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {ref}"
}
components.append(module)
# Perform alignment based on selected option
if alignment == "horizontal":
self._align_components_horizontally(components, distribution, spacing)
elif alignment == "vertical":
self._align_components_vertically(components, distribution, spacing)
elif alignment == "edge":
edge = params.get("edge")
if not edge:
return {
"success": False,
"message": "Missing edge parameter",
"errorDetails": "Edge parameter is required for edge alignment"
}
self._align_components_to_edge(components, edge)
else:
return {
"success": False,
"message": "Invalid alignment option",
"errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
}
# Prepare result data
aligned_components = []
for module in components:
pos = module.GetPosition()
aligned_components.append({
"reference": module.GetReference(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": module.GetOrientation() / 10
})
return {
"success": True,
"message": f"Aligned {len(components)} components",
"alignment": alignment,
"distribution": distribution,
"components": aligned_components
}
except Exception as e:
logger.error(f"Error aligning components: {str(e)}")
return {
"success": False,
"message": "Failed to align components",
"errorDetails": str(e)
}
def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Duplicate an existing component"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
new_reference = params.get("newReference")
position = params.get("position")
rotation = params.get("rotation")
if not reference or not new_reference:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "reference and newReference are required"
}
# Find the source component
source = self.board.FindFootprintByReference(reference)
if not source:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Check if new reference already exists
if self.board.FindFootprintByReference(new_reference):
return {
"success": False,
"message": "Reference already exists",
"errorDetails": f"A component with reference {new_reference} already exists"
}
# Create new footprint with the same properties
new_module = pcbnew.FOOTPRINT(self.board)
new_module.SetFootprintName(source.GetFootprintName())
new_module.SetValue(source.GetValue())
new_module.SetReference(new_reference)
new_module.SetLayer(source.GetLayer())
# Copy pads and other items
for pad in source.Pads():
new_pad = pcbnew.PAD(new_module)
new_pad.Copy(pad)
new_module.Add(new_pad)
# Set position if provided, otherwise use offset from original
if position:
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
else:
# Offset by 5mm
source_pos = source.GetPosition()
new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
# Set rotation if provided, otherwise use same as original
if rotation is not None:
new_module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
else:
new_module.SetOrientation(source.GetOrientation())
# Add to board
self.board.Add(new_module)
# Get final position in mm
pos = new_module.GetPosition()
return {
"success": True,
"message": f"Duplicated component {reference} to {new_reference}",
"component": {
"reference": new_reference,
"value": new_module.GetValue(),
"footprint": new_module.GetFootprintName(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": new_module.GetOrientation() / 10,
"layer": self.board.GetLayerName(new_module.GetLayer())
}
}
except Exception as e:
logger.error(f"Error duplicating component: {str(e)}")
return {
"success": False,
"message": "Failed to duplicate component",
"errorDetails": str(e)
}
def _place_grid_array(self, component_id: str, start_position: Dict[str, Any],
rows: int, columns: int, spacing_x: float, spacing_y: float,
reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
"""Place components in a grid pattern and return the list of placed components"""
placed = []
# Convert spacing to nm
unit = start_position.get("unit", "mm")
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
spacing_x_nm = int(spacing_x * scale)
spacing_y_nm = int(spacing_y * scale)
# Get layer ID
layer_id = self.board.GetLayerID(layer)
for row in range(rows):
for col in range(columns):
# Calculate position
x = start_position["x"] + (col * spacing_x)
y = start_position["y"] + (row * spacing_y)
# Generate reference
index = row * columns + col + 1
component_reference = f"{reference_prefix}{index}"
# Place component
result = self.place_component({
"componentId": component_id,
"position": {"x": x, "y": y, "unit": unit},
"reference": component_reference,
"value": value,
"rotation": rotation,
"layer": layer
})
if result["success"]:
placed.append(result["component"])
return placed
def _place_circular_array(self, component_id: str, center: Dict[str, Any],
radius: float, count: int, angle_start: float,
angle_step: float, reference_prefix: str,
value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
"""Place components in a circular pattern and return the list of placed components"""
placed = []
# Get unit
unit = center.get("unit", "mm")
for i in range(count):
# Calculate angle for this component
angle = angle_start + (i * angle_step)
angle_rad = math.radians(angle)
# Calculate position
x = center["x"] + (radius * math.cos(angle_rad))
y = center["y"] + (radius * math.sin(angle_rad))
# Generate reference
component_reference = f"{reference_prefix}{i+1}"
# Calculate rotation (pointing outward from center)
component_rotation = angle + rotation_offset
# Place component
result = self.place_component({
"componentId": component_id,
"position": {"x": x, "y": y, "unit": unit},
"reference": component_reference,
"value": value,
"rotation": component_rotation,
"layer": layer
})
if result["success"]:
placed.append(result["component"])
return placed
def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT],
distribution: str, spacing: Optional[float]) -> None:
"""Align components horizontally and optionally distribute them"""
if not components:
return
# Find the average Y coordinate
y_sum = sum(module.GetPosition().y for module in components)
y_avg = y_sum // len(components)
# Sort components by X position
components.sort(key=lambda m: m.GetPosition().x)
# Set Y coordinate for all components
for module in components:
pos = module.GetPosition()
module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
# Handle distribution if requested
if distribution == "equal" and len(components) > 1:
# Get leftmost and rightmost X coordinates
x_min = components[0].GetPosition().x
x_max = components[-1].GetPosition().x
# Calculate equal spacing
total_space = x_max - x_min
spacing_nm = total_space // (len(components) - 1)
# Set X positions with equal spacing
for i in range(1, len(components) - 1):
pos = components[i].GetPosition()
new_x = x_min + (i * spacing_nm)
components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
elif distribution == "spacing" and spacing is not None:
# Convert spacing to nanometers
spacing_nm = int(spacing * 1000000) # assuming mm
# Set X positions with the specified spacing
x_current = components[0].GetPosition().x
for i in range(1, len(components)):
pos = components[i].GetPosition()
x_current += spacing_nm
components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT],
distribution: str, spacing: Optional[float]) -> None:
"""Align components vertically and optionally distribute them"""
if not components:
return
# Find the average X coordinate
x_sum = sum(module.GetPosition().x for module in components)
x_avg = x_sum // len(components)
# Sort components by Y position
components.sort(key=lambda m: m.GetPosition().y)
# Set X coordinate for all components
for module in components:
pos = module.GetPosition()
module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
# Handle distribution if requested
if distribution == "equal" and len(components) > 1:
# Get topmost and bottommost Y coordinates
y_min = components[0].GetPosition().y
y_max = components[-1].GetPosition().y
# Calculate equal spacing
total_space = y_max - y_min
spacing_nm = total_space // (len(components) - 1)
# Set Y positions with equal spacing
for i in range(1, len(components) - 1):
pos = components[i].GetPosition()
new_y = y_min + (i * spacing_nm)
components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
elif distribution == "spacing" and spacing is not None:
# Convert spacing to nanometers
spacing_nm = int(spacing * 1000000) # assuming mm
# Set Y positions with the specified spacing
y_current = components[0].GetPosition().y
for i in range(1, len(components)):
pos = components[i].GetPosition()
y_current += spacing_nm
components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
"""Align components to the specified edge of the board"""
if not components:
return
# Get board bounds
board_box = self.board.GetBoardEdgesBoundingBox()
left = board_box.GetLeft()
right = board_box.GetRight()
top = board_box.GetTop()
bottom = board_box.GetBottom()
# Align based on specified edge
if edge == "left":
for module in components:
pos = module.GetPosition()
module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y)) # 2mm offset from edge
elif edge == "right":
for module in components:
pos = module.GetPosition()
module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y)) # 2mm offset from edge
elif edge == "top":
for module in components:
pos = module.GetPosition()
module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000)) # 2mm offset from edge
elif edge == "bottom":
for module in components:
pos = module.GetPosition()
module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000)) # 2mm offset from edge
else:
logger.warning(f"Unknown edge alignment: {edge}")
```