#
tokens: 20288/50000 1/94 files (page 6/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 6 of 6. Use http://codebase.md/mixelpixx/kicad-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG_2025-10-26.md
├── CHANGELOG_2025-11-01.md
├── CHANGELOG_2025-11-05.md
├── CHANGELOG_2025-11-30.md
├── config
│   ├── claude-desktop-config.json
│   ├── default-config.json
│   ├── linux-config.example.json
│   ├── macos-config.example.json
│   └── windows-config.example.json
├── CONTRIBUTING.md
├── docs
│   ├── BUILD_AND_TEST_SESSION.md
│   ├── CLIENT_CONFIGURATION.md
│   ├── IPC_API_MIGRATION_PLAN.md
│   ├── IPC_BACKEND_STATUS.md
│   ├── JLCPCB_INTEGRATION_PLAN.md
│   ├── KNOWN_ISSUES.md
│   ├── LIBRARY_INTEGRATION.md
│   ├── LINUX_COMPATIBILITY_AUDIT.md
│   ├── PLATFORM_GUIDE.md
│   ├── REALTIME_WORKFLOW.md
│   ├── ROADMAP.md
│   ├── STATUS_SUMMARY.md
│   ├── UI_AUTO_LAUNCH.md
│   ├── VISUAL_FEEDBACK.md
│   ├── WEEK1_SESSION1_SUMMARY.md
│   ├── WEEK1_SESSION2_SUMMARY.md
│   └── WINDOWS_TROUBLESHOOTING.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
│   │   ├── library.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
│   ├── resources
│   │   ├── __init__.py
│   │   └── resource_definitions.py
│   ├── schemas
│   │   ├── __init__.py
│   │   └── tool_schemas.py
│   ├── test_ipc_backend.py
│   └── utils
│       ├── __init__.py
│       ├── kicad_process.py
│       └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│   ├── auto_refresh_kicad.sh
│   └── install-linux.sh
├── setup-windows.ps1
├── 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
│   │   ├── library.ts
│   │   ├── project.ts
│   │   ├── routing.ts
│   │   ├── schematic.ts
│   │   └── ui.ts
│   └── utils
│       └── resource-helpers.ts
├── tests
│   ├── __init__.py
│   └── test_platform_helper.py
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/python/kicad_interface.py:
--------------------------------------------------------------------------------

```python
   1 | #!/usr/bin/env python3
   2 | """
   3 | KiCAD Python Interface Script for Model Context Protocol
   4 | 
   5 | This script handles communication between the MCP TypeScript server
   6 | and KiCAD's Python API (pcbnew). It receives commands via stdin as
   7 | JSON and returns responses via stdout also as JSON.
   8 | """
   9 | 
  10 | import sys
  11 | import json
  12 | import traceback
  13 | import logging
  14 | import os
  15 | from typing import Dict, Any, Optional
  16 | 
  17 | # Import tool schemas and resource definitions
  18 | from schemas.tool_schemas import TOOL_SCHEMAS
  19 | from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
  20 | 
  21 | # Configure logging
  22 | log_dir = os.path.join(os.path.expanduser('~'), '.kicad-mcp', 'logs')
  23 | os.makedirs(log_dir, exist_ok=True)
  24 | log_file = os.path.join(log_dir, 'kicad_interface.log')
  25 | 
  26 | logging.basicConfig(
  27 |     level=logging.DEBUG,
  28 |     format='%(asctime)s [%(levelname)s] %(message)s',
  29 |     handlers=[
  30 |         logging.FileHandler(log_file),
  31 |         logging.StreamHandler(sys.stderr)
  32 |     ]
  33 | )
  34 | logger = logging.getLogger('kicad_interface')
  35 | 
  36 | # Log Python environment details
  37 | logger.info(f"Python version: {sys.version}")
  38 | logger.info(f"Python executable: {sys.executable}")
  39 | logger.info(f"Platform: {sys.platform}")
  40 | logger.info(f"Working directory: {os.getcwd()}")
  41 | 
  42 | # Windows-specific diagnostics
  43 | if sys.platform == 'win32':
  44 |     logger.info("=== Windows Environment Diagnostics ===")
  45 |     logger.info(f"PYTHONPATH: {os.environ.get('PYTHONPATH', 'NOT SET')}")
  46 |     logger.info(f"PATH: {os.environ.get('PATH', 'NOT SET')[:200]}...")  # Truncate PATH
  47 | 
  48 |     # Check for common KiCAD installations
  49 |     common_kicad_paths = [
  50 |         r"C:\Program Files\KiCad",
  51 |         r"C:\Program Files (x86)\KiCad"
  52 |     ]
  53 | 
  54 |     found_kicad = False
  55 |     for base_path in common_kicad_paths:
  56 |         if os.path.exists(base_path):
  57 |             logger.info(f"Found KiCAD installation at: {base_path}")
  58 |             # List versions
  59 |             try:
  60 |                 versions = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]
  61 |                 logger.info(f"  Versions found: {', '.join(versions)}")
  62 |                 for version in versions:
  63 |                     python_path = os.path.join(base_path, version, 'lib', 'python3', 'dist-packages')
  64 |                     if os.path.exists(python_path):
  65 |                         logger.info(f"  ✓ Python path exists: {python_path}")
  66 |                         found_kicad = True
  67 |                     else:
  68 |                         logger.warning(f"  ✗ Python path missing: {python_path}")
  69 |             except Exception as e:
  70 |                 logger.warning(f"  Could not list versions: {e}")
  71 | 
  72 |     if not found_kicad:
  73 |         logger.warning("No KiCAD installations found in standard locations!")
  74 |         logger.warning("Please ensure KiCAD 9.0+ is installed from https://www.kicad.org/download/windows/")
  75 | 
  76 |     logger.info("========================================")
  77 | 
  78 | # Add utils directory to path for imports
  79 | utils_dir = os.path.join(os.path.dirname(__file__))
  80 | if utils_dir not in sys.path:
  81 |     sys.path.insert(0, utils_dir)
  82 | 
  83 | # Import platform helper and add KiCAD paths
  84 | from utils.platform_helper import PlatformHelper
  85 | from utils.kicad_process import check_and_launch_kicad, KiCADProcessManager
  86 | 
  87 | logger.info(f"Detecting KiCAD Python paths for {PlatformHelper.get_platform_name()}...")
  88 | paths_added = PlatformHelper.add_kicad_to_python_path()
  89 | 
  90 | if paths_added:
  91 |     logger.info("Successfully added KiCAD Python paths to sys.path")
  92 | else:
  93 |     logger.warning("No KiCAD Python paths found - attempting to import pcbnew from system path")
  94 | 
  95 | logger.info(f"Current Python path: {sys.path}")
  96 | 
  97 | # Check if auto-launch is enabled
  98 | AUTO_LAUNCH_KICAD = os.environ.get("KICAD_AUTO_LAUNCH", "false").lower() == "true"
  99 | if AUTO_LAUNCH_KICAD:
 100 |     logger.info("KiCAD auto-launch enabled")
 101 | 
 102 | # Check which backend to use
 103 | # KICAD_BACKEND can be: 'auto', 'ipc', or 'swig'
 104 | KICAD_BACKEND = os.environ.get("KICAD_BACKEND", "auto").lower()
 105 | logger.info(f"KiCAD backend preference: {KICAD_BACKEND}")
 106 | 
 107 | # Try to use IPC backend first if available and preferred
 108 | USE_IPC_BACKEND = False
 109 | ipc_backend = None
 110 | 
 111 | if KICAD_BACKEND in ('auto', 'ipc'):
 112 |     try:
 113 |         logger.info("Checking IPC backend availability...")
 114 |         from kicad_api.ipc_backend import IPCBackend
 115 | 
 116 |         # Try to connect to running KiCAD
 117 |         ipc_backend = IPCBackend()
 118 |         if ipc_backend.connect():
 119 |             USE_IPC_BACKEND = True
 120 |             logger.info(f"✓ Using IPC backend - real-time UI sync enabled!")
 121 |             logger.info(f"  KiCAD version: {ipc_backend.get_version()}")
 122 |         else:
 123 |             logger.info("IPC backend available but KiCAD not running with IPC enabled")
 124 |             ipc_backend = None
 125 |     except ImportError:
 126 |         logger.info("IPC backend not available (kicad-python not installed)")
 127 |     except Exception as e:
 128 |         logger.info(f"IPC backend connection failed: {e}")
 129 |         ipc_backend = None
 130 | 
 131 | # Fall back to SWIG backend if IPC not available
 132 | if not USE_IPC_BACKEND and KICAD_BACKEND != 'ipc':
 133 |     # Import KiCAD's Python API (SWIG)
 134 |     try:
 135 |         logger.info("Attempting to import pcbnew module (SWIG backend)...")
 136 |         import pcbnew  # type: ignore
 137 |         logger.info(f"Successfully imported pcbnew module from: {pcbnew.__file__}")
 138 |         logger.info(f"pcbnew version: {pcbnew.GetBuildVersion()}")
 139 |         logger.warning("Using SWIG backend - changes require manual reload in KiCAD UI")
 140 |     except ImportError as e:
 141 |         logger.error(f"Failed to import pcbnew module: {e}")
 142 |         logger.error(f"Current sys.path: {sys.path}")
 143 | 
 144 |         # Platform-specific help message
 145 |         help_message = ""
 146 |         if sys.platform == 'win32':
 147 |             help_message = """
 148 | Windows Troubleshooting:
 149 | 1. Verify KiCAD is installed: C:\\Program Files\\KiCad\\9.0
 150 | 2. Check PYTHONPATH environment variable points to:
 151 |    C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages
 152 | 3. Test with: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"
 153 | 4. Log file location: %USERPROFILE%\\.kicad-mcp\\logs\\kicad_interface.log
 154 | 5. Run setup-windows.ps1 for automatic configuration
 155 | """
 156 |         elif sys.platform == 'darwin':
 157 |             help_message = """
 158 | macOS Troubleshooting:
 159 | 1. Verify KiCAD is installed: /Applications/KiCad/KiCad.app
 160 | 2. Check PYTHONPATH points to KiCAD's Python packages
 161 | 3. Run: python3 -c "import pcbnew" to test
 162 | """
 163 |         else:  # Linux
 164 |             help_message = """
 165 | Linux Troubleshooting:
 166 | 1. Verify KiCAD is installed: apt list --installed | grep kicad
 167 | 2. Check: /usr/lib/kicad/lib/python3/dist-packages exists
 168 | 3. Test: python3 -c "import pcbnew"
 169 | """
 170 | 
 171 |         logger.error(help_message)
 172 | 
 173 |         error_response = {
 174 |             "success": False,
 175 |             "message": "Failed to import pcbnew module - KiCAD Python API not found",
 176 |             "errorDetails": f"Error: {str(e)}\n\n{help_message}\n\nPython sys.path:\n{chr(10).join(sys.path)}"
 177 |         }
 178 |         print(json.dumps(error_response))
 179 |         sys.exit(1)
 180 |     except Exception as e:
 181 |         logger.error(f"Unexpected error importing pcbnew: {e}")
 182 |         logger.error(traceback.format_exc())
 183 |         error_response = {
 184 |             "success": False,
 185 |             "message": "Error importing pcbnew module",
 186 |             "errorDetails": str(e)
 187 |         }
 188 |         print(json.dumps(error_response))
 189 |         sys.exit(1)
 190 | 
 191 | # If IPC-only mode requested but not available, exit with error
 192 | elif KICAD_BACKEND == 'ipc' and not USE_IPC_BACKEND:
 193 |     error_response = {
 194 |         "success": False,
 195 |         "message": "IPC backend requested but not available",
 196 |         "errorDetails": "KiCAD must be running with IPC API enabled. Enable at: Preferences > Plugins > Enable IPC API Server"
 197 |     }
 198 |     print(json.dumps(error_response))
 199 |     sys.exit(1)
 200 | 
 201 | # Import command handlers
 202 | try:
 203 |     logger.info("Importing command handlers...")
 204 |     from commands.project import ProjectCommands
 205 |     from commands.board import BoardCommands
 206 |     from commands.component import ComponentCommands
 207 |     from commands.routing import RoutingCommands
 208 |     from commands.design_rules import DesignRuleCommands
 209 |     from commands.export import ExportCommands
 210 |     from commands.schematic import SchematicManager
 211 |     from commands.component_schematic import ComponentManager
 212 |     from commands.connection_schematic import ConnectionManager
 213 |     from commands.library_schematic import LibraryManager as SchematicLibraryManager
 214 |     from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands
 215 |     logger.info("Successfully imported all command handlers")
 216 | except ImportError as e:
 217 |     logger.error(f"Failed to import command handlers: {e}")
 218 |     error_response = {
 219 |         "success": False,
 220 |         "message": "Failed to import command handlers",
 221 |         "errorDetails": str(e)
 222 |     }
 223 |     print(json.dumps(error_response))
 224 |     sys.exit(1)
 225 | 
 226 | class KiCADInterface:
 227 |     """Main interface class to handle KiCAD operations"""
 228 | 
 229 |     def __init__(self):
 230 |         """Initialize the interface and command handlers"""
 231 |         self.board = None
 232 |         self.project_filename = None
 233 |         self.use_ipc = USE_IPC_BACKEND
 234 |         self.ipc_backend = ipc_backend
 235 |         self.ipc_board_api = None
 236 | 
 237 |         if self.use_ipc:
 238 |             logger.info("Initializing with IPC backend (real-time UI sync enabled)")
 239 |             try:
 240 |                 self.ipc_board_api = self.ipc_backend.get_board()
 241 |                 logger.info("✓ Got IPC board API")
 242 |             except Exception as e:
 243 |                 logger.warning(f"Could not get IPC board API: {e}")
 244 |         else:
 245 |             logger.info("Initializing with SWIG backend")
 246 | 
 247 |         logger.info("Initializing command handlers...")
 248 | 
 249 |         # Initialize footprint library manager
 250 |         self.footprint_library = FootprintLibraryManager()
 251 | 
 252 |         # Initialize command handlers
 253 |         self.project_commands = ProjectCommands(self.board)
 254 |         self.board_commands = BoardCommands(self.board)
 255 |         self.component_commands = ComponentCommands(self.board, self.footprint_library)
 256 |         self.routing_commands = RoutingCommands(self.board)
 257 |         self.design_rule_commands = DesignRuleCommands(self.board)
 258 |         self.export_commands = ExportCommands(self.board)
 259 |         self.library_commands = LibraryCommands(self.footprint_library)
 260 | 
 261 |         # Schematic-related classes don't need board reference
 262 |         # as they operate directly on schematic files
 263 |         
 264 |         # Command routing dictionary
 265 |         self.command_routes = {
 266 |             # Project commands
 267 |             "create_project": self.project_commands.create_project,
 268 |             "open_project": self.project_commands.open_project,
 269 |             "save_project": self.project_commands.save_project,
 270 |             "get_project_info": self.project_commands.get_project_info,
 271 |             
 272 |             # Board commands
 273 |             "set_board_size": self.board_commands.set_board_size,
 274 |             "add_layer": self.board_commands.add_layer,
 275 |             "set_active_layer": self.board_commands.set_active_layer,
 276 |             "get_board_info": self.board_commands.get_board_info,
 277 |             "get_layer_list": self.board_commands.get_layer_list,
 278 |             "get_board_2d_view": self.board_commands.get_board_2d_view,
 279 |             "add_board_outline": self.board_commands.add_board_outline,
 280 |             "add_mounting_hole": self.board_commands.add_mounting_hole,
 281 |             "add_text": self.board_commands.add_text,
 282 |             "add_board_text": self.board_commands.add_text,  # Alias for TypeScript tool
 283 |             
 284 |             # Component commands
 285 |             "place_component": self.component_commands.place_component,
 286 |             "move_component": self.component_commands.move_component,
 287 |             "rotate_component": self.component_commands.rotate_component,
 288 |             "delete_component": self.component_commands.delete_component,
 289 |             "edit_component": self.component_commands.edit_component,
 290 |             "get_component_properties": self.component_commands.get_component_properties,
 291 |             "get_component_list": self.component_commands.get_component_list,
 292 |             "place_component_array": self.component_commands.place_component_array,
 293 |             "align_components": self.component_commands.align_components,
 294 |             "duplicate_component": self.component_commands.duplicate_component,
 295 |             
 296 |             # Routing commands
 297 |             "add_net": self.routing_commands.add_net,
 298 |             "route_trace": self.routing_commands.route_trace,
 299 |             "add_via": self.routing_commands.add_via,
 300 |             "delete_trace": self.routing_commands.delete_trace,
 301 |             "get_nets_list": self.routing_commands.get_nets_list,
 302 |             "create_netclass": self.routing_commands.create_netclass,
 303 |             "add_copper_pour": self.routing_commands.add_copper_pour,
 304 |             "route_differential_pair": self.routing_commands.route_differential_pair,
 305 |             "refill_zones": self._handle_refill_zones,
 306 | 
 307 |             # Design rule commands
 308 |             "set_design_rules": self.design_rule_commands.set_design_rules,
 309 |             "get_design_rules": self.design_rule_commands.get_design_rules,
 310 |             "run_drc": self.design_rule_commands.run_drc,
 311 |             "get_drc_violations": self.design_rule_commands.get_drc_violations,
 312 |             
 313 |             # Export commands
 314 |             "export_gerber": self.export_commands.export_gerber,
 315 |             "export_pdf": self.export_commands.export_pdf,
 316 |             "export_svg": self.export_commands.export_svg,
 317 |             "export_3d": self.export_commands.export_3d,
 318 |             "export_bom": self.export_commands.export_bom,
 319 | 
 320 |             # Library commands (footprint management)
 321 |             "list_libraries": self.library_commands.list_libraries,
 322 |             "search_footprints": self.library_commands.search_footprints,
 323 |             "list_library_footprints": self.library_commands.list_library_footprints,
 324 |             "get_footprint_info": self.library_commands.get_footprint_info,
 325 | 
 326 |             # Schematic commands
 327 |             "create_schematic": self._handle_create_schematic,
 328 |             "load_schematic": self._handle_load_schematic,
 329 |             "add_schematic_component": self._handle_add_schematic_component,
 330 |             "add_schematic_wire": self._handle_add_schematic_wire,
 331 |             "add_schematic_connection": self._handle_add_schematic_connection,
 332 |             "add_schematic_net_label": self._handle_add_schematic_net_label,
 333 |             "connect_to_net": self._handle_connect_to_net,
 334 |             "get_net_connections": self._handle_get_net_connections,
 335 |             "generate_netlist": self._handle_generate_netlist,
 336 |             "list_schematic_libraries": self._handle_list_schematic_libraries,
 337 |             "export_schematic_pdf": self._handle_export_schematic_pdf,
 338 | 
 339 |             # UI/Process management commands
 340 |             "check_kicad_ui": self._handle_check_kicad_ui,
 341 |             "launch_kicad_ui": self._handle_launch_kicad_ui,
 342 | 
 343 |             # IPC-specific commands (real-time operations)
 344 |             "get_backend_info": self._handle_get_backend_info,
 345 |             "ipc_add_track": self._handle_ipc_add_track,
 346 |             "ipc_add_via": self._handle_ipc_add_via,
 347 |             "ipc_add_text": self._handle_ipc_add_text,
 348 |             "ipc_list_components": self._handle_ipc_list_components,
 349 |             "ipc_get_tracks": self._handle_ipc_get_tracks,
 350 |             "ipc_get_vias": self._handle_ipc_get_vias,
 351 |             "ipc_save_board": self._handle_ipc_save_board
 352 |         }
 353 | 
 354 |         logger.info(f"KiCAD interface initialized (backend: {'IPC' if self.use_ipc else 'SWIG'})")
 355 | 
 356 |     # Commands that can be handled via IPC for real-time updates
 357 |     IPC_CAPABLE_COMMANDS = {
 358 |         # Routing commands
 359 |         "route_trace": "_ipc_route_trace",
 360 |         "add_via": "_ipc_add_via",
 361 |         "add_net": "_ipc_add_net",
 362 |         "delete_trace": "_ipc_delete_trace",
 363 |         "get_nets_list": "_ipc_get_nets_list",
 364 |         # Zone commands
 365 |         "add_copper_pour": "_ipc_add_copper_pour",
 366 |         "refill_zones": "_ipc_refill_zones",
 367 |         # Board commands
 368 |         "add_text": "_ipc_add_text",
 369 |         "add_board_text": "_ipc_add_text",
 370 |         "set_board_size": "_ipc_set_board_size",
 371 |         "get_board_info": "_ipc_get_board_info",
 372 |         "add_board_outline": "_ipc_add_board_outline",
 373 |         "add_mounting_hole": "_ipc_add_mounting_hole",
 374 |         "get_layer_list": "_ipc_get_layer_list",
 375 |         # Component commands
 376 |         "place_component": "_ipc_place_component",
 377 |         "move_component": "_ipc_move_component",
 378 |         "rotate_component": "_ipc_rotate_component",
 379 |         "delete_component": "_ipc_delete_component",
 380 |         "get_component_list": "_ipc_get_component_list",
 381 |         "get_component_properties": "_ipc_get_component_properties",
 382 |         # Save command
 383 |         "save_project": "_ipc_save_project",
 384 |     }
 385 | 
 386 |     def handle_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
 387 |         """Route command to appropriate handler, preferring IPC when available"""
 388 |         logger.info(f"Handling command: {command}")
 389 |         logger.debug(f"Command parameters: {params}")
 390 | 
 391 |         try:
 392 |             # Check if we can use IPC for this command (real-time UI sync)
 393 |             if self.use_ipc and self.ipc_board_api and command in self.IPC_CAPABLE_COMMANDS:
 394 |                 ipc_handler_name = self.IPC_CAPABLE_COMMANDS[command]
 395 |                 ipc_handler = getattr(self, ipc_handler_name, None)
 396 | 
 397 |                 if ipc_handler:
 398 |                     logger.info(f"Using IPC backend for {command} (real-time sync)")
 399 |                     result = ipc_handler(params)
 400 | 
 401 |                     # Add indicator that IPC was used
 402 |                     if isinstance(result, dict):
 403 |                         result["_backend"] = "ipc"
 404 |                         result["_realtime"] = True
 405 | 
 406 |                     logger.debug(f"IPC command result: {result}")
 407 |                     return result
 408 | 
 409 |             # Fall back to SWIG-based handler
 410 |             if self.use_ipc and command in self.IPC_CAPABLE_COMMANDS:
 411 |                 logger.warning(f"IPC handler not available for {command}, falling back to SWIG (deprecated)")
 412 | 
 413 |             # Get the handler for the command
 414 |             handler = self.command_routes.get(command)
 415 | 
 416 |             if handler:
 417 |                 # Execute the command
 418 |                 result = handler(params)
 419 |                 logger.debug(f"Command result: {result}")
 420 | 
 421 |                 # Add backend indicator
 422 |                 if isinstance(result, dict):
 423 |                     result["_backend"] = "swig"
 424 |                     result["_realtime"] = False
 425 | 
 426 |                 # Update board reference if command was successful
 427 |                 if result.get("success", False):
 428 |                     if command == "create_project" or command == "open_project":
 429 |                         logger.info("Updating board reference...")
 430 |                         # Get board from the project commands handler
 431 |                         self.board = self.project_commands.board
 432 |                         self._update_command_handlers()
 433 | 
 434 |                 return result
 435 |             else:
 436 |                 logger.error(f"Unknown command: {command}")
 437 |                 return {
 438 |                     "success": False,
 439 |                     "message": f"Unknown command: {command}",
 440 |                     "errorDetails": "The specified command is not supported"
 441 |                 }
 442 | 
 443 |         except Exception as e:
 444 |             # Get the full traceback
 445 |             traceback_str = traceback.format_exc()
 446 |             logger.error(f"Error handling command {command}: {str(e)}\n{traceback_str}")
 447 |             return {
 448 |                 "success": False,
 449 |                 "message": f"Error handling command: {command}",
 450 |                 "errorDetails": f"{str(e)}\n{traceback_str}"
 451 |             }
 452 | 
 453 |     def _update_command_handlers(self):
 454 |         """Update board reference in all command handlers"""
 455 |         logger.debug("Updating board reference in command handlers")
 456 |         self.project_commands.board = self.board
 457 |         self.board_commands.board = self.board
 458 |         self.component_commands.board = self.board
 459 |         self.routing_commands.board = self.board
 460 |         self.design_rule_commands.board = self.board
 461 |         self.export_commands.board = self.board
 462 |         
 463 |     # Schematic command handlers
 464 |     def _handle_create_schematic(self, params):
 465 |         """Create a new schematic"""
 466 |         logger.info("Creating schematic")
 467 |         try:
 468 |             # Support multiple parameter naming conventions for compatibility:
 469 |             # - TypeScript tools use: name, path
 470 |             # - Python schema uses: filename, title
 471 |             # - Legacy uses: projectName, path, metadata
 472 |             project_name = (
 473 |                 params.get("projectName") or
 474 |                 params.get("name") or
 475 |                 params.get("title")
 476 |             )
 477 | 
 478 |             # Handle filename parameter - it may contain full path
 479 |             filename = params.get("filename")
 480 |             if filename:
 481 |                 # If filename provided, extract name and path from it
 482 |                 if filename.endswith('.kicad_sch'):
 483 |                     filename = filename[:-10]  # Remove .kicad_sch extension
 484 |                 path = os.path.dirname(filename) or "."
 485 |                 project_name = project_name or os.path.basename(filename)
 486 |             else:
 487 |                 path = params.get("path", ".")
 488 |             metadata = params.get("metadata", {})
 489 | 
 490 |             if not project_name:
 491 |                 return {
 492 |                     "success": False,
 493 |                     "message": "Schematic name is required. Provide 'name', 'projectName', or 'filename' parameter."
 494 |                 }
 495 | 
 496 |             schematic = SchematicManager.create_schematic(project_name, metadata)
 497 |             file_path = f"{path}/{project_name}.kicad_sch"
 498 |             success = SchematicManager.save_schematic(schematic, file_path)
 499 | 
 500 |             return {"success": success, "file_path": file_path}
 501 |         except Exception as e:
 502 |             logger.error(f"Error creating schematic: {str(e)}")
 503 |             return {"success": False, "message": str(e)}
 504 |     
 505 |     def _handle_load_schematic(self, params):
 506 |         """Load an existing schematic"""
 507 |         logger.info("Loading schematic")
 508 |         try:
 509 |             filename = params.get("filename")
 510 |             
 511 |             if not filename:
 512 |                 return {"success": False, "message": "Filename is required"}
 513 |             
 514 |             schematic = SchematicManager.load_schematic(filename)
 515 |             success = schematic is not None
 516 |             
 517 |             if success:
 518 |                 metadata = SchematicManager.get_schematic_metadata(schematic)
 519 |                 return {"success": success, "metadata": metadata}
 520 |             else:
 521 |                 return {"success": False, "message": "Failed to load schematic"}
 522 |         except Exception as e:
 523 |             logger.error(f"Error loading schematic: {str(e)}")
 524 |             return {"success": False, "message": str(e)}
 525 |     
 526 |     def _handle_add_schematic_component(self, params):
 527 |         """Add a component to a schematic"""
 528 |         logger.info("Adding component to schematic")
 529 |         try:
 530 |             schematic_path = params.get("schematicPath")
 531 |             component = params.get("component", {})
 532 |             
 533 |             if not schematic_path:
 534 |                 return {"success": False, "message": "Schematic path is required"}
 535 |             if not component:
 536 |                 return {"success": False, "message": "Component definition is required"}
 537 |             
 538 |             schematic = SchematicManager.load_schematic(schematic_path)
 539 |             if not schematic:
 540 |                 return {"success": False, "message": "Failed to load schematic"}
 541 |             
 542 |             component_obj = ComponentManager.add_component(schematic, component)
 543 |             success = component_obj is not None
 544 |             
 545 |             if success:
 546 |                 SchematicManager.save_schematic(schematic, schematic_path)
 547 |                 return {"success": True}
 548 |             else:
 549 |                 return {"success": False, "message": "Failed to add component"}
 550 |         except Exception as e:
 551 |             logger.error(f"Error adding component to schematic: {str(e)}")
 552 |             return {"success": False, "message": str(e)}
 553 |     
 554 |     def _handle_add_schematic_wire(self, params):
 555 |         """Add a wire to a schematic"""
 556 |         logger.info("Adding wire to schematic")
 557 |         try:
 558 |             schematic_path = params.get("schematicPath")
 559 |             start_point = params.get("startPoint")
 560 |             end_point = params.get("endPoint")
 561 |             
 562 |             if not schematic_path:
 563 |                 return {"success": False, "message": "Schematic path is required"}
 564 |             if not start_point or not end_point:
 565 |                 return {"success": False, "message": "Start and end points are required"}
 566 |             
 567 |             schematic = SchematicManager.load_schematic(schematic_path)
 568 |             if not schematic:
 569 |                 return {"success": False, "message": "Failed to load schematic"}
 570 |             
 571 |             wire = ConnectionManager.add_wire(schematic, start_point, end_point)
 572 |             success = wire is not None
 573 |             
 574 |             if success:
 575 |                 SchematicManager.save_schematic(schematic, schematic_path)
 576 |                 return {"success": True}
 577 |             else:
 578 |                 return {"success": False, "message": "Failed to add wire"}
 579 |         except Exception as e:
 580 |             logger.error(f"Error adding wire to schematic: {str(e)}")
 581 |             return {"success": False, "message": str(e)}
 582 |     
 583 |     def _handle_list_schematic_libraries(self, params):
 584 |         """List available symbol libraries"""
 585 |         logger.info("Listing schematic libraries")
 586 |         try:
 587 |             search_paths = params.get("searchPaths")
 588 |             
 589 |             libraries = LibraryManager.list_available_libraries(search_paths)
 590 |             return {"success": True, "libraries": libraries}
 591 |         except Exception as e:
 592 |             logger.error(f"Error listing schematic libraries: {str(e)}")
 593 |             return {"success": False, "message": str(e)}
 594 |     
 595 |     def _handle_export_schematic_pdf(self, params):
 596 |         """Export schematic to PDF"""
 597 |         logger.info("Exporting schematic to PDF")
 598 |         try:
 599 |             schematic_path = params.get("schematicPath")
 600 |             output_path = params.get("outputPath")
 601 |             
 602 |             if not schematic_path:
 603 |                 return {"success": False, "message": "Schematic path is required"}
 604 |             if not output_path:
 605 |                 return {"success": False, "message": "Output path is required"}
 606 |             
 607 |             import subprocess
 608 |             result = subprocess.run(
 609 |                 ["kicad-cli", "sch", "export", "pdf", "--output", output_path, schematic_path],
 610 |                 capture_output=True, 
 611 |                 text=True
 612 |             )
 613 |             
 614 |             success = result.returncode == 0
 615 |             message = result.stderr if not success else ""
 616 |             
 617 |             return {"success": success, "message": message}
 618 |         except Exception as e:
 619 |             logger.error(f"Error exporting schematic to PDF: {str(e)}")
 620 |             return {"success": False, "message": str(e)}
 621 | 
 622 |     def _handle_add_schematic_connection(self, params):
 623 |         """Add a pin-to-pin connection in schematic"""
 624 |         logger.info("Adding pin-to-pin connection in schematic")
 625 |         try:
 626 |             schematic_path = params.get("schematicPath")
 627 |             source_ref = params.get("sourceRef")
 628 |             source_pin = params.get("sourcePin")
 629 |             target_ref = params.get("targetRef")
 630 |             target_pin = params.get("targetPin")
 631 | 
 632 |             if not all([schematic_path, source_ref, source_pin, target_ref, target_pin]):
 633 |                 return {"success": False, "message": "Missing required parameters"}
 634 | 
 635 |             schematic = SchematicManager.load_schematic(schematic_path)
 636 |             if not schematic:
 637 |                 return {"success": False, "message": "Failed to load schematic"}
 638 | 
 639 |             success = ConnectionManager.add_connection(schematic, source_ref, source_pin, target_ref, target_pin)
 640 | 
 641 |             if success:
 642 |                 SchematicManager.save_schematic(schematic, schematic_path)
 643 |                 return {"success": True}
 644 |             else:
 645 |                 return {"success": False, "message": "Failed to add connection"}
 646 |         except Exception as e:
 647 |             logger.error(f"Error adding schematic connection: {str(e)}")
 648 |             return {"success": False, "message": str(e)}
 649 | 
 650 |     def _handle_add_schematic_net_label(self, params):
 651 |         """Add a net label to schematic"""
 652 |         logger.info("Adding net label to schematic")
 653 |         try:
 654 |             schematic_path = params.get("schematicPath")
 655 |             net_name = params.get("netName")
 656 |             position = params.get("position")
 657 | 
 658 |             if not all([schematic_path, net_name, position]):
 659 |                 return {"success": False, "message": "Missing required parameters"}
 660 | 
 661 |             schematic = SchematicManager.load_schematic(schematic_path)
 662 |             if not schematic:
 663 |                 return {"success": False, "message": "Failed to load schematic"}
 664 | 
 665 |             label = ConnectionManager.add_net_label(schematic, net_name, position)
 666 | 
 667 |             if label:
 668 |                 SchematicManager.save_schematic(schematic, schematic_path)
 669 |                 return {"success": True}
 670 |             else:
 671 |                 return {"success": False, "message": "Failed to add net label"}
 672 |         except Exception as e:
 673 |             logger.error(f"Error adding net label: {str(e)}")
 674 |             return {"success": False, "message": str(e)}
 675 | 
 676 |     def _handle_connect_to_net(self, params):
 677 |         """Connect a component pin to a named net"""
 678 |         logger.info("Connecting component pin to net")
 679 |         try:
 680 |             schematic_path = params.get("schematicPath")
 681 |             component_ref = params.get("componentRef")
 682 |             pin_name = params.get("pinName")
 683 |             net_name = params.get("netName")
 684 | 
 685 |             if not all([schematic_path, component_ref, pin_name, net_name]):
 686 |                 return {"success": False, "message": "Missing required parameters"}
 687 | 
 688 |             schematic = SchematicManager.load_schematic(schematic_path)
 689 |             if not schematic:
 690 |                 return {"success": False, "message": "Failed to load schematic"}
 691 | 
 692 |             success = ConnectionManager.connect_to_net(schematic, component_ref, pin_name, net_name)
 693 | 
 694 |             if success:
 695 |                 SchematicManager.save_schematic(schematic, schematic_path)
 696 |                 return {"success": True}
 697 |             else:
 698 |                 return {"success": False, "message": "Failed to connect to net"}
 699 |         except Exception as e:
 700 |             logger.error(f"Error connecting to net: {str(e)}")
 701 |             return {"success": False, "message": str(e)}
 702 | 
 703 |     def _handle_get_net_connections(self, params):
 704 |         """Get all connections for a named net"""
 705 |         logger.info("Getting net connections")
 706 |         try:
 707 |             schematic_path = params.get("schematicPath")
 708 |             net_name = params.get("netName")
 709 | 
 710 |             if not all([schematic_path, net_name]):
 711 |                 return {"success": False, "message": "Missing required parameters"}
 712 | 
 713 |             schematic = SchematicManager.load_schematic(schematic_path)
 714 |             if not schematic:
 715 |                 return {"success": False, "message": "Failed to load schematic"}
 716 | 
 717 |             connections = ConnectionManager.get_net_connections(schematic, net_name)
 718 |             return {"success": True, "connections": connections}
 719 |         except Exception as e:
 720 |             logger.error(f"Error getting net connections: {str(e)}")
 721 |             return {"success": False, "message": str(e)}
 722 | 
 723 |     def _handle_generate_netlist(self, params):
 724 |         """Generate netlist from schematic"""
 725 |         logger.info("Generating netlist from schematic")
 726 |         try:
 727 |             schematic_path = params.get("schematicPath")
 728 | 
 729 |             if not schematic_path:
 730 |                 return {"success": False, "message": "Schematic path is required"}
 731 | 
 732 |             schematic = SchematicManager.load_schematic(schematic_path)
 733 |             if not schematic:
 734 |                 return {"success": False, "message": "Failed to load schematic"}
 735 | 
 736 |             netlist = ConnectionManager.generate_netlist(schematic)
 737 |             return {"success": True, "netlist": netlist}
 738 |         except Exception as e:
 739 |             logger.error(f"Error generating netlist: {str(e)}")
 740 |             return {"success": False, "message": str(e)}
 741 | 
 742 |     def _handle_check_kicad_ui(self, params):
 743 |         """Check if KiCAD UI is running"""
 744 |         logger.info("Checking if KiCAD UI is running")
 745 |         try:
 746 |             manager = KiCADProcessManager()
 747 |             is_running = manager.is_running()
 748 |             processes = manager.get_process_info() if is_running else []
 749 | 
 750 |             return {
 751 |                 "success": True,
 752 |                 "running": is_running,
 753 |                 "processes": processes,
 754 |                 "message": "KiCAD is running" if is_running else "KiCAD is not running"
 755 |             }
 756 |         except Exception as e:
 757 |             logger.error(f"Error checking KiCAD UI status: {str(e)}")
 758 |             return {"success": False, "message": str(e)}
 759 | 
 760 |     def _handle_launch_kicad_ui(self, params):
 761 |         """Launch KiCAD UI"""
 762 |         logger.info("Launching KiCAD UI")
 763 |         try:
 764 |             project_path = params.get("projectPath")
 765 |             auto_launch = params.get("autoLaunch", AUTO_LAUNCH_KICAD)
 766 | 
 767 |             # Convert project path to Path object if provided
 768 |             from pathlib import Path
 769 |             path_obj = Path(project_path) if project_path else None
 770 | 
 771 |             result = check_and_launch_kicad(path_obj, auto_launch)
 772 | 
 773 |             return {
 774 |                 "success": True,
 775 |                 **result
 776 |             }
 777 |         except Exception as e:
 778 |             logger.error(f"Error launching KiCAD UI: {str(e)}")
 779 |             return {"success": False, "message": str(e)}
 780 | 
 781 |     def _handle_refill_zones(self, params):
 782 |         """Refill all copper pour zones on the board"""
 783 |         logger.info("Refilling zones")
 784 |         try:
 785 |             if not self.board:
 786 |                 return {
 787 |                     "success": False,
 788 |                     "message": "No board is loaded",
 789 |                     "errorDetails": "Load or create a board first"
 790 |                 }
 791 | 
 792 |             # Use pcbnew's zone filler for SWIG backend
 793 |             filler = pcbnew.ZONE_FILLER(self.board)
 794 |             zones = self.board.Zones()
 795 |             filler.Fill(zones)
 796 | 
 797 |             return {
 798 |                 "success": True,
 799 |                 "message": "Zones refilled successfully",
 800 |                 "zoneCount": zones.size() if hasattr(zones, 'size') else len(list(zones))
 801 |             }
 802 |         except Exception as e:
 803 |             logger.error(f"Error refilling zones: {str(e)}")
 804 |             return {"success": False, "message": str(e)}
 805 | 
 806 |     # =========================================================================
 807 |     # IPC Backend handlers - these provide real-time UI synchronization
 808 |     # These methods are called automatically when IPC is available
 809 |     # =========================================================================
 810 | 
 811 |     def _ipc_route_trace(self, params):
 812 |         """IPC handler for route_trace - adds track with real-time UI update"""
 813 |         try:
 814 |             # Extract parameters matching the existing route_trace interface
 815 |             start = params.get("start", {})
 816 |             end = params.get("end", {})
 817 |             layer = params.get("layer", "F.Cu")
 818 |             width = params.get("width", 0.25)
 819 |             net = params.get("net")
 820 | 
 821 |             # Handle both dict format and direct x/y
 822 |             start_x = start.get("x", 0) if isinstance(start, dict) else params.get("startX", 0)
 823 |             start_y = start.get("y", 0) if isinstance(start, dict) else params.get("startY", 0)
 824 |             end_x = end.get("x", 0) if isinstance(end, dict) else params.get("endX", 0)
 825 |             end_y = end.get("y", 0) if isinstance(end, dict) else params.get("endY", 0)
 826 | 
 827 |             success = self.ipc_board_api.add_track(
 828 |                 start_x=start_x,
 829 |                 start_y=start_y,
 830 |                 end_x=end_x,
 831 |                 end_y=end_y,
 832 |                 width=width,
 833 |                 layer=layer,
 834 |                 net_name=net
 835 |             )
 836 | 
 837 |             return {
 838 |                 "success": success,
 839 |                 "message": "Added trace (visible in KiCAD UI)" if success else "Failed to add trace",
 840 |                 "trace": {
 841 |                     "start": {"x": start_x, "y": start_y, "unit": "mm"},
 842 |                     "end": {"x": end_x, "y": end_y, "unit": "mm"},
 843 |                     "layer": layer,
 844 |                     "width": width,
 845 |                     "net": net
 846 |                 }
 847 |             }
 848 |         except Exception as e:
 849 |             logger.error(f"IPC route_trace error: {e}")
 850 |             return {"success": False, "message": str(e)}
 851 | 
 852 |     def _ipc_add_via(self, params):
 853 |         """IPC handler for add_via - adds via with real-time UI update"""
 854 |         try:
 855 |             position = params.get("position", {})
 856 |             x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
 857 |             y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
 858 | 
 859 |             size = params.get("size", 0.8)
 860 |             drill = params.get("drill", 0.4)
 861 |             net = params.get("net")
 862 |             from_layer = params.get("from_layer", "F.Cu")
 863 |             to_layer = params.get("to_layer", "B.Cu")
 864 | 
 865 |             success = self.ipc_board_api.add_via(
 866 |                 x=x,
 867 |                 y=y,
 868 |                 diameter=size,
 869 |                 drill=drill,
 870 |                 net_name=net,
 871 |                 via_type="through"
 872 |             )
 873 | 
 874 |             return {
 875 |                 "success": success,
 876 |                 "message": "Added via (visible in KiCAD UI)" if success else "Failed to add via",
 877 |                 "via": {
 878 |                     "position": {"x": x, "y": y, "unit": "mm"},
 879 |                     "size": size,
 880 |                     "drill": drill,
 881 |                     "from_layer": from_layer,
 882 |                     "to_layer": to_layer,
 883 |                     "net": net
 884 |                 }
 885 |             }
 886 |         except Exception as e:
 887 |             logger.error(f"IPC add_via error: {e}")
 888 |             return {"success": False, "message": str(e)}
 889 | 
 890 |     def _ipc_add_net(self, params):
 891 |         """IPC handler for add_net"""
 892 |         # Note: Net creation via IPC is limited - nets are typically created
 893 |         # when components are placed. Return success for compatibility.
 894 |         name = params.get("name")
 895 |         logger.info(f"IPC add_net: {name} (nets auto-created with components)")
 896 |         return {
 897 |             "success": True,
 898 |             "message": f"Net '{name}' will be created when components are connected",
 899 |             "net": {"name": name}
 900 |         }
 901 | 
 902 |     def _ipc_add_copper_pour(self, params):
 903 |         """IPC handler for add_copper_pour - adds zone with real-time UI update"""
 904 |         try:
 905 |             layer = params.get("layer", "F.Cu")
 906 |             net = params.get("net")
 907 |             clearance = params.get("clearance", 0.5)
 908 |             min_width = params.get("minWidth", 0.25)
 909 |             points = params.get("points", [])
 910 |             priority = params.get("priority", 0)
 911 |             fill_type = params.get("fillType", "solid")
 912 |             name = params.get("name", "")
 913 | 
 914 |             if not points or len(points) < 3:
 915 |                 return {
 916 |                     "success": False,
 917 |                     "message": "At least 3 points are required for copper pour outline"
 918 |                 }
 919 | 
 920 |             # Convert points format if needed (handle both {x, y} and {x, y, unit})
 921 |             formatted_points = []
 922 |             for point in points:
 923 |                 formatted_points.append({
 924 |                     "x": point.get("x", 0),
 925 |                     "y": point.get("y", 0)
 926 |                 })
 927 | 
 928 |             success = self.ipc_board_api.add_zone(
 929 |                 points=formatted_points,
 930 |                 layer=layer,
 931 |                 net_name=net,
 932 |                 clearance=clearance,
 933 |                 min_thickness=min_width,
 934 |                 priority=priority,
 935 |                 fill_mode=fill_type,
 936 |                 name=name
 937 |             )
 938 | 
 939 |             return {
 940 |                 "success": success,
 941 |                 "message": "Added copper pour (visible in KiCAD UI)" if success else "Failed to add copper pour",
 942 |                 "pour": {
 943 |                     "layer": layer,
 944 |                     "net": net,
 945 |                     "clearance": clearance,
 946 |                     "minWidth": min_width,
 947 |                     "priority": priority,
 948 |                     "fillType": fill_type,
 949 |                     "pointCount": len(points)
 950 |                 }
 951 |             }
 952 |         except Exception as e:
 953 |             logger.error(f"IPC add_copper_pour error: {e}")
 954 |             return {"success": False, "message": str(e)}
 955 | 
 956 |     def _ipc_refill_zones(self, params):
 957 |         """IPC handler for refill_zones - refills all zones with real-time UI update"""
 958 |         try:
 959 |             success = self.ipc_board_api.refill_zones()
 960 | 
 961 |             return {
 962 |                 "success": success,
 963 |                 "message": "Zones refilled (visible in KiCAD UI)" if success else "Failed to refill zones"
 964 |             }
 965 |         except Exception as e:
 966 |             logger.error(f"IPC refill_zones error: {e}")
 967 |             return {"success": False, "message": str(e)}
 968 | 
 969 |     def _ipc_add_text(self, params):
 970 |         """IPC handler for add_text/add_board_text - adds text with real-time UI update"""
 971 |         try:
 972 |             text = params.get("text", "")
 973 |             position = params.get("position", {})
 974 |             x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
 975 |             y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
 976 |             layer = params.get("layer", "F.SilkS")
 977 |             size = params.get("size", 1.0)
 978 |             rotation = params.get("rotation", 0)
 979 | 
 980 |             success = self.ipc_board_api.add_text(
 981 |                 text=text,
 982 |                 x=x,
 983 |                 y=y,
 984 |                 layer=layer,
 985 |                 size=size,
 986 |                 rotation=rotation
 987 |             )
 988 | 
 989 |             return {
 990 |                 "success": success,
 991 |                 "message": f"Added text '{text}' (visible in KiCAD UI)" if success else "Failed to add text"
 992 |             }
 993 |         except Exception as e:
 994 |             logger.error(f"IPC add_text error: {e}")
 995 |             return {"success": False, "message": str(e)}
 996 | 
 997 |     def _ipc_set_board_size(self, params):
 998 |         """IPC handler for set_board_size"""
 999 |         try:
1000 |             width = params.get("width", 100)
1001 |             height = params.get("height", 100)
1002 |             unit = params.get("unit", "mm")
1003 | 
1004 |             success = self.ipc_board_api.set_size(width, height, unit)
1005 | 
1006 |             return {
1007 |                 "success": success,
1008 |                 "message": f"Board size set to {width}x{height} {unit} (visible in KiCAD UI)" if success else "Failed to set board size",
1009 |                 "boardSize": {"width": width, "height": height, "unit": unit}
1010 |             }
1011 |         except Exception as e:
1012 |             logger.error(f"IPC set_board_size error: {e}")
1013 |             return {"success": False, "message": str(e)}
1014 | 
1015 |     def _ipc_get_board_info(self, params):
1016 |         """IPC handler for get_board_info"""
1017 |         try:
1018 |             size = self.ipc_board_api.get_size()
1019 |             components = self.ipc_board_api.list_components()
1020 |             tracks = self.ipc_board_api.get_tracks()
1021 |             vias = self.ipc_board_api.get_vias()
1022 |             nets = self.ipc_board_api.get_nets()
1023 | 
1024 |             return {
1025 |                 "success": True,
1026 |                 "boardInfo": {
1027 |                     "size": size,
1028 |                     "componentCount": len(components),
1029 |                     "trackCount": len(tracks),
1030 |                     "viaCount": len(vias),
1031 |                     "netCount": len(nets),
1032 |                     "backend": "ipc",
1033 |                     "realtime": True
1034 |                 }
1035 |             }
1036 |         except Exception as e:
1037 |             logger.error(f"IPC get_board_info error: {e}")
1038 |             return {"success": False, "message": str(e)}
1039 | 
1040 |     def _ipc_place_component(self, params):
1041 |         """IPC handler for place_component - places component with real-time UI update"""
1042 |         try:
1043 |             reference = params.get("reference", params.get("componentId", ""))
1044 |             footprint = params.get("footprint", "")
1045 |             position = params.get("position", {})
1046 |             x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
1047 |             y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
1048 |             rotation = params.get("rotation", 0)
1049 |             layer = params.get("layer", "F.Cu")
1050 |             value = params.get("value", "")
1051 | 
1052 |             success = self.ipc_board_api.place_component(
1053 |                 reference=reference,
1054 |                 footprint=footprint,
1055 |                 x=x,
1056 |                 y=y,
1057 |                 rotation=rotation,
1058 |                 layer=layer,
1059 |                 value=value
1060 |             )
1061 | 
1062 |             return {
1063 |                 "success": success,
1064 |                 "message": f"Placed component {reference} (visible in KiCAD UI)" if success else "Failed to place component",
1065 |                 "component": {
1066 |                     "reference": reference,
1067 |                     "footprint": footprint,
1068 |                     "position": {"x": x, "y": y, "unit": "mm"},
1069 |                     "rotation": rotation,
1070 |                     "layer": layer
1071 |                 }
1072 |             }
1073 |         except Exception as e:
1074 |             logger.error(f"IPC place_component error: {e}")
1075 |             return {"success": False, "message": str(e)}
1076 | 
1077 |     def _ipc_move_component(self, params):
1078 |         """IPC handler for move_component - moves component with real-time UI update"""
1079 |         try:
1080 |             reference = params.get("reference", params.get("componentId", ""))
1081 |             position = params.get("position", {})
1082 |             x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
1083 |             y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
1084 |             rotation = params.get("rotation")
1085 | 
1086 |             success = self.ipc_board_api.move_component(
1087 |                 reference=reference,
1088 |                 x=x,
1089 |                 y=y,
1090 |                 rotation=rotation
1091 |             )
1092 | 
1093 |             return {
1094 |                 "success": success,
1095 |                 "message": f"Moved component {reference} (visible in KiCAD UI)" if success else "Failed to move component"
1096 |             }
1097 |         except Exception as e:
1098 |             logger.error(f"IPC move_component error: {e}")
1099 |             return {"success": False, "message": str(e)}
1100 | 
1101 |     def _ipc_delete_component(self, params):
1102 |         """IPC handler for delete_component - deletes component with real-time UI update"""
1103 |         try:
1104 |             reference = params.get("reference", params.get("componentId", ""))
1105 | 
1106 |             success = self.ipc_board_api.delete_component(reference=reference)
1107 | 
1108 |             return {
1109 |                 "success": success,
1110 |                 "message": f"Deleted component {reference} (visible in KiCAD UI)" if success else "Failed to delete component"
1111 |             }
1112 |         except Exception as e:
1113 |             logger.error(f"IPC delete_component error: {e}")
1114 |             return {"success": False, "message": str(e)}
1115 | 
1116 |     def _ipc_get_component_list(self, params):
1117 |         """IPC handler for get_component_list"""
1118 |         try:
1119 |             components = self.ipc_board_api.list_components()
1120 | 
1121 |             return {
1122 |                 "success": True,
1123 |                 "components": components,
1124 |                 "count": len(components)
1125 |             }
1126 |         except Exception as e:
1127 |             logger.error(f"IPC get_component_list error: {e}")
1128 |             return {"success": False, "message": str(e)}
1129 | 
1130 |     def _ipc_save_project(self, params):
1131 |         """IPC handler for save_project"""
1132 |         try:
1133 |             success = self.ipc_board_api.save()
1134 | 
1135 |             return {
1136 |                 "success": success,
1137 |                 "message": "Project saved" if success else "Failed to save project"
1138 |             }
1139 |         except Exception as e:
1140 |             logger.error(f"IPC save_project error: {e}")
1141 |             return {"success": False, "message": str(e)}
1142 | 
1143 |     def _ipc_delete_trace(self, params):
1144 |         """IPC handler for delete_trace - Note: IPC doesn't support direct trace deletion yet"""
1145 |         # IPC API doesn't have a direct delete track method
1146 |         # Fall back to SWIG for this operation
1147 |         logger.info("delete_trace: Falling back to SWIG (IPC doesn't support trace deletion)")
1148 |         return self.routing_commands.delete_trace(params)
1149 | 
1150 |     def _ipc_get_nets_list(self, params):
1151 |         """IPC handler for get_nets_list - gets nets with real-time data"""
1152 |         try:
1153 |             nets = self.ipc_board_api.get_nets()
1154 | 
1155 |             return {
1156 |                 "success": True,
1157 |                 "nets": nets,
1158 |                 "count": len(nets)
1159 |             }
1160 |         except Exception as e:
1161 |             logger.error(f"IPC get_nets_list error: {e}")
1162 |             return {"success": False, "message": str(e)}
1163 | 
1164 |     def _ipc_add_board_outline(self, params):
1165 |         """IPC handler for add_board_outline - adds board edge with real-time UI update"""
1166 |         try:
1167 |             from kipy.board_types import BoardSegment
1168 |             from kipy.geometry import Vector2
1169 |             from kipy.util.units import from_mm
1170 |             from kipy.proto.board.board_types_pb2 import BoardLayer
1171 | 
1172 |             board = self.ipc_board_api._get_board()
1173 | 
1174 |             points = params.get("points", [])
1175 |             width = params.get("width", 0.1)
1176 | 
1177 |             if len(points) < 2:
1178 |                 return {"success": False, "message": "At least 2 points required for board outline"}
1179 | 
1180 |             commit = board.begin_commit()
1181 |             lines_created = 0
1182 | 
1183 |             # Create line segments connecting the points
1184 |             for i in range(len(points)):
1185 |                 start = points[i]
1186 |                 end = points[(i + 1) % len(points)]  # Wrap around to close the outline
1187 | 
1188 |                 segment = BoardSegment()
1189 |                 segment.start = Vector2.from_xy(from_mm(start.get("x", 0)), from_mm(start.get("y", 0)))
1190 |                 segment.end = Vector2.from_xy(from_mm(end.get("x", 0)), from_mm(end.get("y", 0)))
1191 |                 segment.layer = BoardLayer.BL_Edge_Cuts
1192 |                 segment.attributes.stroke.width = from_mm(width)
1193 | 
1194 |                 board.create_items(segment)
1195 |                 lines_created += 1
1196 | 
1197 |             board.push_commit(commit, "Added board outline")
1198 | 
1199 |             return {
1200 |                 "success": True,
1201 |                 "message": f"Added board outline with {lines_created} segments (visible in KiCAD UI)",
1202 |                 "segments": lines_created
1203 |             }
1204 |         except Exception as e:
1205 |             logger.error(f"IPC add_board_outline error: {e}")
1206 |             return {"success": False, "message": str(e)}
1207 | 
1208 |     def _ipc_add_mounting_hole(self, params):
1209 |         """IPC handler for add_mounting_hole - adds mounting hole with real-time UI update"""
1210 |         try:
1211 |             from kipy.board_types import BoardCircle
1212 |             from kipy.geometry import Vector2
1213 |             from kipy.util.units import from_mm
1214 |             from kipy.proto.board.board_types_pb2 import BoardLayer
1215 | 
1216 |             board = self.ipc_board_api._get_board()
1217 | 
1218 |             x = params.get("x", 0)
1219 |             y = params.get("y", 0)
1220 |             diameter = params.get("diameter", 3.2)  # M3 hole default
1221 | 
1222 |             commit = board.begin_commit()
1223 | 
1224 |             # Create circle on Edge.Cuts layer for the hole
1225 |             circle = BoardCircle()
1226 |             circle.center = Vector2.from_xy(from_mm(x), from_mm(y))
1227 |             circle.radius = from_mm(diameter / 2)
1228 |             circle.layer = BoardLayer.BL_Edge_Cuts
1229 |             circle.attributes.stroke.width = from_mm(0.1)
1230 | 
1231 |             board.create_items(circle)
1232 |             board.push_commit(commit, f"Added mounting hole at ({x}, {y})")
1233 | 
1234 |             return {
1235 |                 "success": True,
1236 |                 "message": f"Added mounting hole at ({x}, {y}) mm (visible in KiCAD UI)",
1237 |                 "hole": {
1238 |                     "position": {"x": x, "y": y},
1239 |                     "diameter": diameter
1240 |                 }
1241 |             }
1242 |         except Exception as e:
1243 |             logger.error(f"IPC add_mounting_hole error: {e}")
1244 |             return {"success": False, "message": str(e)}
1245 | 
1246 |     def _ipc_get_layer_list(self, params):
1247 |         """IPC handler for get_layer_list - gets enabled layers"""
1248 |         try:
1249 |             layers = self.ipc_board_api.get_enabled_layers()
1250 | 
1251 |             return {
1252 |                 "success": True,
1253 |                 "layers": layers,
1254 |                 "count": len(layers)
1255 |             }
1256 |         except Exception as e:
1257 |             logger.error(f"IPC get_layer_list error: {e}")
1258 |             return {"success": False, "message": str(e)}
1259 | 
1260 |     def _ipc_rotate_component(self, params):
1261 |         """IPC handler for rotate_component - rotates component with real-time UI update"""
1262 |         try:
1263 |             reference = params.get("reference", params.get("componentId", ""))
1264 |             angle = params.get("angle", params.get("rotation", 90))
1265 | 
1266 |             # Get current component to find its position
1267 |             components = self.ipc_board_api.list_components()
1268 |             target = None
1269 |             for comp in components:
1270 |                 if comp.get("reference") == reference:
1271 |                     target = comp
1272 |                     break
1273 | 
1274 |             if not target:
1275 |                 return {"success": False, "message": f"Component {reference} not found"}
1276 | 
1277 |             # Calculate new rotation
1278 |             current_rotation = target.get("rotation", 0)
1279 |             new_rotation = (current_rotation + angle) % 360
1280 | 
1281 |             # Use move_component with new rotation (position stays the same)
1282 |             success = self.ipc_board_api.move_component(
1283 |                 reference=reference,
1284 |                 x=target.get("position", {}).get("x", 0),
1285 |                 y=target.get("position", {}).get("y", 0),
1286 |                 rotation=new_rotation
1287 |             )
1288 | 
1289 |             return {
1290 |                 "success": success,
1291 |                 "message": f"Rotated component {reference} by {angle}° (visible in KiCAD UI)" if success else "Failed to rotate component",
1292 |                 "newRotation": new_rotation
1293 |             }
1294 |         except Exception as e:
1295 |             logger.error(f"IPC rotate_component error: {e}")
1296 |             return {"success": False, "message": str(e)}
1297 | 
1298 |     def _ipc_get_component_properties(self, params):
1299 |         """IPC handler for get_component_properties - gets detailed component info"""
1300 |         try:
1301 |             reference = params.get("reference", params.get("componentId", ""))
1302 | 
1303 |             components = self.ipc_board_api.list_components()
1304 |             target = None
1305 |             for comp in components:
1306 |                 if comp.get("reference") == reference:
1307 |                     target = comp
1308 |                     break
1309 | 
1310 |             if not target:
1311 |                 return {"success": False, "message": f"Component {reference} not found"}
1312 | 
1313 |             return {
1314 |                 "success": True,
1315 |                 "component": target
1316 |             }
1317 |         except Exception as e:
1318 |             logger.error(f"IPC get_component_properties error: {e}")
1319 |             return {"success": False, "message": str(e)}
1320 | 
1321 |     # =========================================================================
1322 |     # Legacy IPC command handlers (explicit ipc_* commands)
1323 |     # =========================================================================
1324 | 
1325 |     def _handle_get_backend_info(self, params):
1326 |         """Get information about the current backend"""
1327 |         return {
1328 |             "success": True,
1329 |             "backend": "ipc" if self.use_ipc else "swig",
1330 |             "realtime_sync": self.use_ipc,
1331 |             "ipc_connected": self.ipc_backend.is_connected() if self.ipc_backend else False,
1332 |             "version": self.ipc_backend.get_version() if self.ipc_backend else "N/A",
1333 |             "message": "Using IPC backend with real-time UI sync" if self.use_ipc else "Using SWIG backend (requires manual reload)"
1334 |         }
1335 | 
1336 |     def _handle_ipc_add_track(self, params):
1337 |         """Add a track using IPC backend (real-time)"""
1338 |         if not self.use_ipc or not self.ipc_board_api:
1339 |             return {"success": False, "message": "IPC backend not available"}
1340 | 
1341 |         try:
1342 |             success = self.ipc_board_api.add_track(
1343 |                 start_x=params.get("startX", 0),
1344 |                 start_y=params.get("startY", 0),
1345 |                 end_x=params.get("endX", 0),
1346 |                 end_y=params.get("endY", 0),
1347 |                 width=params.get("width", 0.25),
1348 |                 layer=params.get("layer", "F.Cu"),
1349 |                 net_name=params.get("net")
1350 |             )
1351 |             return {
1352 |                 "success": success,
1353 |                 "message": "Track added (visible in KiCAD UI)" if success else "Failed to add track",
1354 |                 "realtime": True
1355 |             }
1356 |         except Exception as e:
1357 |             logger.error(f"Error adding track via IPC: {e}")
1358 |             return {"success": False, "message": str(e)}
1359 | 
1360 |     def _handle_ipc_add_via(self, params):
1361 |         """Add a via using IPC backend (real-time)"""
1362 |         if not self.use_ipc or not self.ipc_board_api:
1363 |             return {"success": False, "message": "IPC backend not available"}
1364 | 
1365 |         try:
1366 |             success = self.ipc_board_api.add_via(
1367 |                 x=params.get("x", 0),
1368 |                 y=params.get("y", 0),
1369 |                 diameter=params.get("diameter", 0.8),
1370 |                 drill=params.get("drill", 0.4),
1371 |                 net_name=params.get("net"),
1372 |                 via_type=params.get("type", "through")
1373 |             )
1374 |             return {
1375 |                 "success": success,
1376 |                 "message": "Via added (visible in KiCAD UI)" if success else "Failed to add via",
1377 |                 "realtime": True
1378 |             }
1379 |         except Exception as e:
1380 |             logger.error(f"Error adding via via IPC: {e}")
1381 |             return {"success": False, "message": str(e)}
1382 | 
1383 |     def _handle_ipc_add_text(self, params):
1384 |         """Add text using IPC backend (real-time)"""
1385 |         if not self.use_ipc or not self.ipc_board_api:
1386 |             return {"success": False, "message": "IPC backend not available"}
1387 | 
1388 |         try:
1389 |             success = self.ipc_board_api.add_text(
1390 |                 text=params.get("text", ""),
1391 |                 x=params.get("x", 0),
1392 |                 y=params.get("y", 0),
1393 |                 layer=params.get("layer", "F.SilkS"),
1394 |                 size=params.get("size", 1.0),
1395 |                 rotation=params.get("rotation", 0)
1396 |             )
1397 |             return {
1398 |                 "success": success,
1399 |                 "message": "Text added (visible in KiCAD UI)" if success else "Failed to add text",
1400 |                 "realtime": True
1401 |             }
1402 |         except Exception as e:
1403 |             logger.error(f"Error adding text via IPC: {e}")
1404 |             return {"success": False, "message": str(e)}
1405 | 
1406 |     def _handle_ipc_list_components(self, params):
1407 |         """List components using IPC backend"""
1408 |         if not self.use_ipc or not self.ipc_board_api:
1409 |             return {"success": False, "message": "IPC backend not available"}
1410 | 
1411 |         try:
1412 |             components = self.ipc_board_api.list_components()
1413 |             return {
1414 |                 "success": True,
1415 |                 "components": components,
1416 |                 "count": len(components)
1417 |             }
1418 |         except Exception as e:
1419 |             logger.error(f"Error listing components via IPC: {e}")
1420 |             return {"success": False, "message": str(e)}
1421 | 
1422 |     def _handle_ipc_get_tracks(self, params):
1423 |         """Get tracks using IPC backend"""
1424 |         if not self.use_ipc or not self.ipc_board_api:
1425 |             return {"success": False, "message": "IPC backend not available"}
1426 | 
1427 |         try:
1428 |             tracks = self.ipc_board_api.get_tracks()
1429 |             return {
1430 |                 "success": True,
1431 |                 "tracks": tracks,
1432 |                 "count": len(tracks)
1433 |             }
1434 |         except Exception as e:
1435 |             logger.error(f"Error getting tracks via IPC: {e}")
1436 |             return {"success": False, "message": str(e)}
1437 | 
1438 |     def _handle_ipc_get_vias(self, params):
1439 |         """Get vias using IPC backend"""
1440 |         if not self.use_ipc or not self.ipc_board_api:
1441 |             return {"success": False, "message": "IPC backend not available"}
1442 | 
1443 |         try:
1444 |             vias = self.ipc_board_api.get_vias()
1445 |             return {
1446 |                 "success": True,
1447 |                 "vias": vias,
1448 |                 "count": len(vias)
1449 |             }
1450 |         except Exception as e:
1451 |             logger.error(f"Error getting vias via IPC: {e}")
1452 |             return {"success": False, "message": str(e)}
1453 | 
1454 |     def _handle_ipc_save_board(self, params):
1455 |         """Save board using IPC backend"""
1456 |         if not self.use_ipc or not self.ipc_board_api:
1457 |             return {"success": False, "message": "IPC backend not available"}
1458 | 
1459 |         try:
1460 |             success = self.ipc_board_api.save()
1461 |             return {
1462 |                 "success": success,
1463 |                 "message": "Board saved" if success else "Failed to save board"
1464 |             }
1465 |         except Exception as e:
1466 |             logger.error(f"Error saving board via IPC: {e}")
1467 |             return {"success": False, "message": str(e)}
1468 | 
1469 | 
1470 | def main():
1471 |     """Main entry point"""
1472 |     logger.info("Starting KiCAD interface...")
1473 |     interface = KiCADInterface()
1474 | 
1475 |     try:
1476 |         logger.info("Processing commands from stdin...")
1477 |         # Process commands from stdin
1478 |         for line in sys.stdin:
1479 |             try:
1480 |                 # Parse command
1481 |                 logger.debug(f"Received input: {line.strip()}")
1482 |                 command_data = json.loads(line)
1483 | 
1484 |                 # Check if this is JSON-RPC 2.0 format
1485 |                 if 'jsonrpc' in command_data and command_data['jsonrpc'] == '2.0':
1486 |                     logger.info("Detected JSON-RPC 2.0 format message")
1487 |                     method = command_data.get('method')
1488 |                     params = command_data.get('params', {})
1489 |                     request_id = command_data.get('id')
1490 | 
1491 |                     # Handle MCP protocol methods
1492 |                     if method == 'initialize':
1493 |                         logger.info("Handling MCP initialize")
1494 |                         response = {
1495 |                             'jsonrpc': '2.0',
1496 |                             'id': request_id,
1497 |                             'result': {
1498 |                                 'protocolVersion': '2025-06-18',
1499 |                                 'capabilities': {
1500 |                                     'tools': {
1501 |                                         'listChanged': True
1502 |                                     },
1503 |                                     'resources': {
1504 |                                         'subscribe': False,
1505 |                                         'listChanged': True
1506 |                                     }
1507 |                                 },
1508 |                                 'serverInfo': {
1509 |                                     'name': 'kicad-mcp-server',
1510 |                                     'title': 'KiCAD PCB Design Assistant',
1511 |                                     'version': '2.1.0-alpha'
1512 |                                 },
1513 |                                 'instructions': 'AI-assisted PCB design with KiCAD. Use tools to create projects, design boards, place components, route traces, and export manufacturing files.'
1514 |                             }
1515 |                         }
1516 |                     elif method == 'tools/list':
1517 |                         logger.info("Handling MCP tools/list")
1518 |                         # Return list of available tools with proper schemas
1519 |                         tools = []
1520 |                         for cmd_name in interface.command_routes.keys():
1521 |                             # Get schema from TOOL_SCHEMAS if available
1522 |                             if cmd_name in TOOL_SCHEMAS:
1523 |                                 tool_def = TOOL_SCHEMAS[cmd_name].copy()
1524 |                                 tools.append(tool_def)
1525 |                             else:
1526 |                                 # Fallback for tools without schemas
1527 |                                 logger.warning(f"No schema defined for tool: {cmd_name}")
1528 |                                 tools.append({
1529 |                                     'name': cmd_name,
1530 |                                     'description': f'KiCAD command: {cmd_name}',
1531 |                                     'inputSchema': {
1532 |                                         'type': 'object',
1533 |                                         'properties': {}
1534 |                                     }
1535 |                                 })
1536 | 
1537 |                         logger.info(f"Returning {len(tools)} tools")
1538 |                         response = {
1539 |                             'jsonrpc': '2.0',
1540 |                             'id': request_id,
1541 |                             'result': {
1542 |                                 'tools': tools
1543 |                             }
1544 |                         }
1545 |                     elif method == 'tools/call':
1546 |                         logger.info("Handling MCP tools/call")
1547 |                         tool_name = params.get('name')
1548 |                         tool_params = params.get('arguments', {})
1549 | 
1550 |                         # Execute the command
1551 |                         result = interface.handle_command(tool_name, tool_params)
1552 | 
1553 |                         response = {
1554 |                             'jsonrpc': '2.0',
1555 |                             'id': request_id,
1556 |                             'result': {
1557 |                                 'content': [
1558 |                                     {
1559 |                                         'type': 'text',
1560 |                                         'text': json.dumps(result)
1561 |                                     }
1562 |                                 ]
1563 |                             }
1564 |                         }
1565 |                     elif method == 'resources/list':
1566 |                         logger.info("Handling MCP resources/list")
1567 |                         # Return list of available resources
1568 |                         response = {
1569 |                             'jsonrpc': '2.0',
1570 |                             'id': request_id,
1571 |                             'result': {
1572 |                                 'resources': RESOURCE_DEFINITIONS
1573 |                             }
1574 |                         }
1575 |                     elif method == 'resources/read':
1576 |                         logger.info("Handling MCP resources/read")
1577 |                         resource_uri = params.get('uri')
1578 | 
1579 |                         if not resource_uri:
1580 |                             response = {
1581 |                                 'jsonrpc': '2.0',
1582 |                                 'id': request_id,
1583 |                                 'error': {
1584 |                                     'code': -32602,
1585 |                                     'message': 'Missing required parameter: uri'
1586 |                                 }
1587 |                             }
1588 |                         else:
1589 |                             # Read the resource
1590 |                             resource_data = handle_resource_read(resource_uri, interface)
1591 | 
1592 |                             response = {
1593 |                                 'jsonrpc': '2.0',
1594 |                                 'id': request_id,
1595 |                                 'result': resource_data
1596 |                             }
1597 |                     else:
1598 |                         logger.error(f"Unknown JSON-RPC method: {method}")
1599 |                         response = {
1600 |                             'jsonrpc': '2.0',
1601 |                             'id': request_id,
1602 |                             'error': {
1603 |                                 'code': -32601,
1604 |                                 'message': f'Method not found: {method}'
1605 |                             }
1606 |                         }
1607 |                 else:
1608 |                     # Handle legacy custom format
1609 |                     logger.info("Detected custom format message")
1610 |                     command = command_data.get("command")
1611 |                     params = command_data.get("params", {})
1612 | 
1613 |                     if not command:
1614 |                         logger.error("Missing command field")
1615 |                         response = {
1616 |                             "success": False,
1617 |                             "message": "Missing command",
1618 |                             "errorDetails": "The command field is required"
1619 |                         }
1620 |                     else:
1621 |                         # Handle command
1622 |                         response = interface.handle_command(command, params)
1623 | 
1624 |                 # Send response
1625 |                 logger.debug(f"Sending response: {response}")
1626 |                 print(json.dumps(response))
1627 |                 sys.stdout.flush()
1628 | 
1629 |             except json.JSONDecodeError as e:
1630 |                 logger.error(f"Invalid JSON input: {str(e)}")
1631 |                 response = {
1632 |                     "success": False,
1633 |                     "message": "Invalid JSON input",
1634 |                     "errorDetails": str(e)
1635 |                 }
1636 |                 print(json.dumps(response))
1637 |                 sys.stdout.flush()
1638 | 
1639 |     except KeyboardInterrupt:
1640 |         logger.info("KiCAD interface stopped")
1641 |         sys.exit(0)
1642 | 
1643 |     except Exception as e:
1644 |         logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
1645 |         sys.exit(1)
1646 | 
1647 | if __name__ == "__main__":
1648 |     main()
1649 | 
```
Page 6/6FirstPrevNextLast