We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/KiCAD-MCP-Server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
#!/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,
"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)
# 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()