Major improvements to schematic editing capabilities: ## Python Implementation (connection_schematic.py) - Implemented pin-to-pin connection logic using kicad-skip - Added get_pin_location() to find absolute pin positions - Implemented add_connection() for wire connections between component pins - Added add_net_label() for creating net labels - Added connect_to_net() to connect pins to named nets - Implemented get_net_connections() to query net connectivity - Added generate_netlist() for schematic netlist extraction ## MCP Handlers (kicad_interface.py) - Added 5 new command handlers: - add_schematic_connection - Pin-to-pin wiring - add_schematic_net_label - Net label placement - connect_to_net - Connect pin to named net - get_net_connections - Query net connectivity - generate_netlist - Export netlist data ## TypeScript Tools (schematic.ts) - Added 5 new MCP tools with proper schemas and validation - Enhanced user feedback with descriptive messages - Total schematic tools increased from 3 to 8 ## Features - Pin location calculation with symbol rotation support - Automatic wire stub creation for net labels - Comprehensive netlist generation with component and net info - Full logging for debugging connection issues This resolves the schematic editing limitations and enables users to: - Wire component pins together directly - Use net labels for cleaner schematics - Query schematic connectivity - Generate netlists for manufacturing 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
817 lines
36 KiB
Python
817 lines
36 KiB
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
|
|
|
|
# Import tool schemas and resource definitions
|
|
from schemas.tool_schemas import TOOL_SCHEMAS
|
|
from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
|
|
|
|
# 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}")
|
|
logger.info(f"Platform: {sys.platform}")
|
|
logger.info(f"Working directory: {os.getcwd()}")
|
|
|
|
# Windows-specific diagnostics
|
|
if sys.platform == 'win32':
|
|
logger.info("=== Windows Environment Diagnostics ===")
|
|
logger.info(f"PYTHONPATH: {os.environ.get('PYTHONPATH', 'NOT SET')}")
|
|
logger.info(f"PATH: {os.environ.get('PATH', 'NOT SET')[:200]}...") # Truncate PATH
|
|
|
|
# Check for common KiCAD installations
|
|
common_kicad_paths = [
|
|
r"C:\Program Files\KiCad",
|
|
r"C:\Program Files (x86)\KiCad"
|
|
]
|
|
|
|
found_kicad = False
|
|
for base_path in common_kicad_paths:
|
|
if os.path.exists(base_path):
|
|
logger.info(f"Found KiCAD installation at: {base_path}")
|
|
# List versions
|
|
try:
|
|
versions = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]
|
|
logger.info(f" Versions found: {', '.join(versions)}")
|
|
for version in versions:
|
|
python_path = os.path.join(base_path, version, 'lib', 'python3', 'dist-packages')
|
|
if os.path.exists(python_path):
|
|
logger.info(f" ✓ Python path exists: {python_path}")
|
|
found_kicad = True
|
|
else:
|
|
logger.warning(f" ✗ Python path missing: {python_path}")
|
|
except Exception as e:
|
|
logger.warning(f" Could not list versions: {e}")
|
|
|
|
if not found_kicad:
|
|
logger.warning("No KiCAD installations found in standard locations!")
|
|
logger.warning("Please ensure KiCAD 9.0+ is installed from https://www.kicad.org/download/windows/")
|
|
|
|
logger.info("========================================")
|
|
|
|
# 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}")
|
|
|
|
# Platform-specific help message
|
|
help_message = ""
|
|
if sys.platform == 'win32':
|
|
help_message = """
|
|
Windows Troubleshooting:
|
|
1. Verify KiCAD is installed: C:\\Program Files\\KiCad\\9.0
|
|
2. Check PYTHONPATH environment variable points to:
|
|
C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages
|
|
3. Test with: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"
|
|
4. Log file location: %USERPROFILE%\\.kicad-mcp\\logs\\kicad_interface.log
|
|
5. Run setup-windows.ps1 for automatic configuration
|
|
"""
|
|
elif sys.platform == 'darwin':
|
|
help_message = """
|
|
macOS Troubleshooting:
|
|
1. Verify KiCAD is installed: /Applications/KiCad/KiCad.app
|
|
2. Check PYTHONPATH points to KiCAD's Python packages
|
|
3. Run: python3 -c "import pcbnew" to test
|
|
"""
|
|
else: # Linux
|
|
help_message = """
|
|
Linux Troubleshooting:
|
|
1. Verify KiCAD is installed: apt list --installed | grep kicad
|
|
2. Check: /usr/lib/kicad/lib/python3/dist-packages exists
|
|
3. Test: python3 -c "import pcbnew"
|
|
"""
|
|
|
|
logger.error(help_message)
|
|
|
|
error_response = {
|
|
"success": False,
|
|
"message": "Failed to import pcbnew module - KiCAD Python API not found",
|
|
"errorDetails": f"Error: {str(e)}\n\n{help_message}\n\nPython sys.path:\n{chr(10).join(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 as SchematicLibraryManager
|
|
from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands
|
|
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 footprint library manager
|
|
self.footprint_library = FootprintLibraryManager()
|
|
|
|
# Initialize command handlers
|
|
self.project_commands = ProjectCommands(self.board)
|
|
self.board_commands = BoardCommands(self.board)
|
|
self.component_commands = ComponentCommands(self.board, self.footprint_library)
|
|
self.routing_commands = RoutingCommands(self.board)
|
|
self.design_rule_commands = DesignRuleCommands(self.board)
|
|
self.export_commands = ExportCommands(self.board)
|
|
self.library_commands = LibraryCommands(self.footprint_library)
|
|
|
|
# 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,
|
|
|
|
# Library commands (footprint management)
|
|
"list_libraries": self.library_commands.list_libraries,
|
|
"search_footprints": self.library_commands.search_footprints,
|
|
"list_library_footprints": self.library_commands.list_library_footprints,
|
|
"get_footprint_info": self.library_commands.get_footprint_info,
|
|
|
|
# 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,
|
|
"add_schematic_connection": self._handle_add_schematic_connection,
|
|
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
|
"connect_to_net": self._handle_connect_to_net,
|
|
"get_net_connections": self._handle_get_net_connections,
|
|
"generate_netlist": self._handle_generate_netlist,
|
|
"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_add_schematic_connection(self, params):
|
|
"""Add a pin-to-pin connection in schematic"""
|
|
logger.info("Adding pin-to-pin connection in schematic")
|
|
try:
|
|
schematic_path = params.get("schematicPath")
|
|
source_ref = params.get("sourceRef")
|
|
source_pin = params.get("sourcePin")
|
|
target_ref = params.get("targetRef")
|
|
target_pin = params.get("targetPin")
|
|
|
|
if not all([schematic_path, source_ref, source_pin, target_ref, target_pin]):
|
|
return {"success": False, "message": "Missing required parameters"}
|
|
|
|
schematic = SchematicManager.load_schematic(schematic_path)
|
|
if not schematic:
|
|
return {"success": False, "message": "Failed to load schematic"}
|
|
|
|
success = ConnectionManager.add_connection(schematic, source_ref, source_pin, target_ref, target_pin)
|
|
|
|
if success:
|
|
SchematicManager.save_schematic(schematic, schematic_path)
|
|
return {"success": True}
|
|
else:
|
|
return {"success": False, "message": "Failed to add connection"}
|
|
except Exception as e:
|
|
logger.error(f"Error adding schematic connection: {str(e)}")
|
|
return {"success": False, "message": str(e)}
|
|
|
|
def _handle_add_schematic_net_label(self, params):
|
|
"""Add a net label to schematic"""
|
|
logger.info("Adding net label to schematic")
|
|
try:
|
|
schematic_path = params.get("schematicPath")
|
|
net_name = params.get("netName")
|
|
position = params.get("position")
|
|
|
|
if not all([schematic_path, net_name, position]):
|
|
return {"success": False, "message": "Missing required parameters"}
|
|
|
|
schematic = SchematicManager.load_schematic(schematic_path)
|
|
if not schematic:
|
|
return {"success": False, "message": "Failed to load schematic"}
|
|
|
|
label = ConnectionManager.add_net_label(schematic, net_name, position)
|
|
|
|
if label:
|
|
SchematicManager.save_schematic(schematic, schematic_path)
|
|
return {"success": True}
|
|
else:
|
|
return {"success": False, "message": "Failed to add net label"}
|
|
except Exception as e:
|
|
logger.error(f"Error adding net label: {str(e)}")
|
|
return {"success": False, "message": str(e)}
|
|
|
|
def _handle_connect_to_net(self, params):
|
|
"""Connect a component pin to a named net"""
|
|
logger.info("Connecting component pin to net")
|
|
try:
|
|
schematic_path = params.get("schematicPath")
|
|
component_ref = params.get("componentRef")
|
|
pin_name = params.get("pinName")
|
|
net_name = params.get("netName")
|
|
|
|
if not all([schematic_path, component_ref, pin_name, net_name]):
|
|
return {"success": False, "message": "Missing required parameters"}
|
|
|
|
schematic = SchematicManager.load_schematic(schematic_path)
|
|
if not schematic:
|
|
return {"success": False, "message": "Failed to load schematic"}
|
|
|
|
success = ConnectionManager.connect_to_net(schematic, component_ref, pin_name, net_name)
|
|
|
|
if success:
|
|
SchematicManager.save_schematic(schematic, schematic_path)
|
|
return {"success": True}
|
|
else:
|
|
return {"success": False, "message": "Failed to connect to net"}
|
|
except Exception as e:
|
|
logger.error(f"Error connecting to net: {str(e)}")
|
|
return {"success": False, "message": str(e)}
|
|
|
|
def _handle_get_net_connections(self, params):
|
|
"""Get all connections for a named net"""
|
|
logger.info("Getting net connections")
|
|
try:
|
|
schematic_path = params.get("schematicPath")
|
|
net_name = params.get("netName")
|
|
|
|
if not all([schematic_path, net_name]):
|
|
return {"success": False, "message": "Missing required parameters"}
|
|
|
|
schematic = SchematicManager.load_schematic(schematic_path)
|
|
if not schematic:
|
|
return {"success": False, "message": "Failed to load schematic"}
|
|
|
|
connections = ConnectionManager.get_net_connections(schematic, net_name)
|
|
return {"success": True, "connections": connections}
|
|
except Exception as e:
|
|
logger.error(f"Error getting net connections: {str(e)}")
|
|
return {"success": False, "message": str(e)}
|
|
|
|
def _handle_generate_netlist(self, params):
|
|
"""Generate netlist from schematic"""
|
|
logger.info("Generating netlist from schematic")
|
|
try:
|
|
schematic_path = params.get("schematicPath")
|
|
|
|
if not schematic_path:
|
|
return {"success": False, "message": "Schematic path is required"}
|
|
|
|
schematic = SchematicManager.load_schematic(schematic_path)
|
|
if not schematic:
|
|
return {"success": False, "message": "Failed to load schematic"}
|
|
|
|
netlist = ConnectionManager.generate_netlist(schematic)
|
|
return {"success": True, "netlist": netlist}
|
|
except Exception as e:
|
|
logger.error(f"Error generating netlist: {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)
|
|
|
|
# Check if this is JSON-RPC 2.0 format
|
|
if 'jsonrpc' in command_data and command_data['jsonrpc'] == '2.0':
|
|
logger.info("Detected JSON-RPC 2.0 format message")
|
|
method = command_data.get('method')
|
|
params = command_data.get('params', {})
|
|
request_id = command_data.get('id')
|
|
|
|
# Handle MCP protocol methods
|
|
if method == 'initialize':
|
|
logger.info("Handling MCP initialize")
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'result': {
|
|
'protocolVersion': '2025-06-18',
|
|
'capabilities': {
|
|
'tools': {
|
|
'listChanged': True
|
|
},
|
|
'resources': {
|
|
'subscribe': False,
|
|
'listChanged': True
|
|
}
|
|
},
|
|
'serverInfo': {
|
|
'name': 'kicad-mcp-server',
|
|
'title': 'KiCAD PCB Design Assistant',
|
|
'version': '2.1.0-alpha'
|
|
},
|
|
'instructions': 'AI-assisted PCB design with KiCAD. Use tools to create projects, design boards, place components, route traces, and export manufacturing files.'
|
|
}
|
|
}
|
|
elif method == 'tools/list':
|
|
logger.info("Handling MCP tools/list")
|
|
# Return list of available tools with proper schemas
|
|
tools = []
|
|
for cmd_name in interface.command_routes.keys():
|
|
# Get schema from TOOL_SCHEMAS if available
|
|
if cmd_name in TOOL_SCHEMAS:
|
|
tool_def = TOOL_SCHEMAS[cmd_name].copy()
|
|
tools.append(tool_def)
|
|
else:
|
|
# Fallback for tools without schemas
|
|
logger.warning(f"No schema defined for tool: {cmd_name}")
|
|
tools.append({
|
|
'name': cmd_name,
|
|
'description': f'KiCAD command: {cmd_name}',
|
|
'inputSchema': {
|
|
'type': 'object',
|
|
'properties': {}
|
|
}
|
|
})
|
|
|
|
logger.info(f"Returning {len(tools)} tools")
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'result': {
|
|
'tools': tools
|
|
}
|
|
}
|
|
elif method == 'tools/call':
|
|
logger.info("Handling MCP tools/call")
|
|
tool_name = params.get('name')
|
|
tool_params = params.get('arguments', {})
|
|
|
|
# Execute the command
|
|
result = interface.handle_command(tool_name, tool_params)
|
|
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'result': {
|
|
'content': [
|
|
{
|
|
'type': 'text',
|
|
'text': json.dumps(result)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
elif method == 'resources/list':
|
|
logger.info("Handling MCP resources/list")
|
|
# Return list of available resources
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'result': {
|
|
'resources': RESOURCE_DEFINITIONS
|
|
}
|
|
}
|
|
elif method == 'resources/read':
|
|
logger.info("Handling MCP resources/read")
|
|
resource_uri = params.get('uri')
|
|
|
|
if not resource_uri:
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'error': {
|
|
'code': -32602,
|
|
'message': 'Missing required parameter: uri'
|
|
}
|
|
}
|
|
else:
|
|
# Read the resource
|
|
resource_data = handle_resource_read(resource_uri, interface)
|
|
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'result': resource_data
|
|
}
|
|
else:
|
|
logger.error(f"Unknown JSON-RPC method: {method}")
|
|
response = {
|
|
'jsonrpc': '2.0',
|
|
'id': request_id,
|
|
'error': {
|
|
'code': -32601,
|
|
'message': f'Method not found: {method}'
|
|
}
|
|
}
|
|
else:
|
|
# Handle legacy custom format
|
|
logger.info("Detected custom format message")
|
|
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()
|