This is page 1 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
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Node.js
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | dist/
7 | .npm
8 | .eslintcache
9 |
10 | # Python
11 | __pycache__/
12 | *.py[cod]
13 | *$py.class
14 | *.so
15 | .Python
16 | build/
17 | develop-eggs/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 | .pytest_cache/
31 | .coverage
32 | htmlcov/
33 | .tox/
34 | .hypothesis/
35 | *.cover
36 | .mypy_cache/
37 | .dmypy.json
38 | dmypy.json
39 |
40 | # Virtual Environments
41 | venv/
42 | env/
43 | ENV/
44 |
45 | # IDEs
46 | .vscode/
47 | .idea/
48 | *.swp
49 | *.swo
50 | *~
51 | .DS_Store
52 |
53 | # Logs
54 | logs/
55 | *.log
56 | ~/.kicad-mcp/
57 |
58 | # Environment
59 | .env
60 | .env.local
61 | .env.*.local
62 |
63 | # KiCAD
64 | *.kicad_pcb-bak
65 | *.kicad_sch-bak
66 | *.kicad_pro-bak
67 | *.kicad_prl
68 | *-backups/
69 | fp-info-cache
70 |
71 | # Testing
72 | test_output/
73 | schematic_test_output/
74 | coverage.xml
75 | .coverage.*
76 |
77 | # OS
78 | Thumbs.db
79 | Desktop.ini
80 |
```
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Pre-commit hooks configuration
2 | # See https://pre-commit.com for more information
3 |
4 | repos:
5 | # Python code formatting
6 | - repo: https://github.com/psf/black
7 | rev: 23.7.0
8 | hooks:
9 | - id: black
10 | language_version: python3
11 | files: ^python/
12 |
13 | # Python import sorting
14 | - repo: https://github.com/pycqa/isort
15 | rev: 5.12.0
16 | hooks:
17 | - id: isort
18 | files: ^python/
19 | args: ["--profile", "black"]
20 |
21 | # Python type checking
22 | - repo: https://github.com/pre-commit/mirrors-mypy
23 | rev: v1.5.0
24 | hooks:
25 | - id: mypy
26 | files: ^python/
27 | args: [--ignore-missing-imports]
28 |
29 | # Python linting
30 | - repo: https://github.com/pycqa/flake8
31 | rev: 6.1.0
32 | hooks:
33 | - id: flake8
34 | files: ^python/
35 | args: [--max-line-length=100, --extend-ignore=E203]
36 |
37 | # TypeScript/JavaScript formatting
38 | - repo: https://github.com/pre-commit/mirrors-prettier
39 | rev: v3.0.3
40 | hooks:
41 | - id: prettier
42 | types_or: [javascript, typescript, json, yaml, markdown]
43 | files: \.(ts|js|json|ya?ml|md)$
44 |
45 | # General file checks
46 | - repo: https://github.com/pre-commit/pre-commit-hooks
47 | rev: v4.4.0
48 | hooks:
49 | - id: trailing-whitespace
50 | - id: end-of-file-fixer
51 | - id: check-yaml
52 | - id: check-json
53 | - id: check-added-large-files
54 | args: [--maxkb=500]
55 | - id: check-merge-conflict
56 | - id: detect-private-key
57 |
58 | # Python security checks
59 | - repo: https://github.com/PyCQA/bandit
60 | rev: 1.7.5
61 | hooks:
62 | - id: bandit
63 | args: [-c, pyproject.toml]
64 | files: ^python/
65 |
66 | # Markdown linting
67 | - repo: https://github.com/igorshubovych/markdownlint-cli
68 | rev: v0.37.0
69 | hooks:
70 | - id: markdownlint
71 | args: [--fix]
72 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # KiCAD MCP Server
2 |
3 | A Model Context Protocol (MCP) server that enables AI assistants like Claude to interact with KiCAD for PCB design automation. Built on the MCP 2025-06-18 specification, this server provides comprehensive tool schemas and real-time project state access for intelligent PCB design workflows.
4 |
5 | ## Overview
6 |
7 | The [Model Context Protocol](https://modelcontextprotocol.io/) is an open standard from Anthropic that allows AI assistants to securely connect to external tools and data sources. This implementation provides a standardized bridge between AI assistants and KiCAD, enabling natural language control of PCB design operations.
8 |
9 | **Key Capabilities:**
10 | - 52 fully-documented tools with JSON Schema validation
11 | - 8 dynamic resources exposing project state
12 | - Full MCP 2025-06-18 protocol compliance
13 | - Cross-platform support (Linux, Windows, macOS)
14 | - Real-time KiCAD UI integration via IPC API (experimental)
15 | - Comprehensive error handling and logging
16 |
17 | ## What's New in v2.1.0
18 |
19 | ### IPC Backend (Experimental)
20 | We are currently implementing and testing the KiCAD 9.0 IPC API for real-time UI synchronization:
21 | - Changes made via MCP tools appear immediately in the KiCAD UI
22 | - No manual reload required when IPC is active
23 | - Hybrid backend: uses IPC when available, falls back to SWIG API
24 | - 20+ commands now support IPC including routing, component placement, and zone operations
25 |
26 | Note: IPC features are under active development and testing. Enable IPC in KiCAD via Preferences > Plugins > Enable IPC API Server.
27 |
28 | ### Comprehensive Tool Schemas
29 | Every tool now includes complete JSON Schema definitions with:
30 | - Detailed parameter descriptions and constraints
31 | - Input validation with type checking
32 | - Required vs. optional parameter specifications
33 | - Enumerated values for categorical inputs
34 | - Clear documentation of what each tool does
35 |
36 | ### Resources Capability
37 | Access project state without executing tools:
38 | - `kicad://project/current/info` - Project metadata
39 | - `kicad://project/current/board` - Board properties
40 | - `kicad://project/current/components` - Component list (JSON)
41 | - `kicad://project/current/nets` - Electrical nets
42 | - `kicad://project/current/layers` - Layer stack configuration
43 | - `kicad://project/current/design-rules` - Current DRC settings
44 | - `kicad://project/current/drc-report` - Design rule violations
45 | - `kicad://board/preview.png` - Board visualization (PNG)
46 |
47 | ### Protocol Compliance
48 | - Updated to MCP SDK 1.21.0 (latest)
49 | - Full JSON-RPC 2.0 support
50 | - Proper capability negotiation
51 | - Standards-compliant error codes
52 |
53 | ## Available Tools
54 |
55 | The server provides 52 tools organized into functional categories:
56 |
57 | ### Project Management (4 tools)
58 | - `create_project` - Initialize new KiCAD projects
59 | - `open_project` - Load existing project files
60 | - `save_project` - Save current project state
61 | - `get_project_info` - Retrieve project metadata
62 |
63 | ### Board Operations (9 tools)
64 | - `set_board_size` - Configure PCB dimensions
65 | - `add_board_outline` - Create board edge (rectangle, circle, polygon)
66 | - `add_layer` - Add custom layers to stack
67 | - `set_active_layer` - Switch working layer
68 | - `get_layer_list` - List all board layers
69 | - `get_board_info` - Retrieve board properties
70 | - `get_board_2d_view` - Generate board preview image
71 | - `add_mounting_hole` - Place mounting holes
72 | - `add_board_text` - Add text annotations
73 |
74 | ### Component Placement (10 tools)
75 | - `place_component` - Place single component with footprint
76 | - `move_component` - Reposition existing component
77 | - `rotate_component` - Rotate component by angle
78 | - `delete_component` - Remove component from board
79 | - `edit_component` - Modify component properties
80 | - `get_component_properties` - Query component details
81 | - `get_component_list` - List all placed components
82 | - `place_component_array` - Create component grids/patterns
83 | - `align_components` - Align multiple components
84 | - `duplicate_component` - Copy existing component
85 |
86 | ### Routing & Nets (8 tools)
87 | - `add_net` - Create electrical net
88 | - `route_trace` - Route copper traces
89 | - `add_via` - Place vias for layer transitions
90 | - `delete_trace` - Remove traces
91 | - `get_nets_list` - List all nets
92 | - `create_netclass` - Define net class with rules
93 | - `add_copper_pour` - Create copper zones/pours
94 | - `route_differential_pair` - Route differential signals
95 |
96 | ### Library Management (4 tools)
97 | - `list_libraries` - List available footprint libraries
98 | - `search_footprints` - Search for footprints
99 | - `list_library_footprints` - List footprints in library
100 | - `get_footprint_info` - Get footprint details
101 |
102 | ### Design Rules (4 tools)
103 | - `set_design_rules` - Configure DRC parameters
104 | - `get_design_rules` - Retrieve current rules
105 | - `run_drc` - Execute design rule check
106 | - `get_drc_violations` - Get DRC error report
107 |
108 | ### Export (5 tools)
109 | - `export_gerber` - Generate Gerber fabrication files
110 | - `export_pdf` - Export PDF documentation
111 | - `export_svg` - Create SVG vector graphics
112 | - `export_3d` - Generate 3D models (STEP/VRML)
113 | - `export_bom` - Produce bill of materials
114 |
115 | ### Schematic Design (6 tools)
116 | - `create_schematic` - Initialize new schematic
117 | - `load_schematic` - Open existing schematic
118 | - `add_schematic_component` - Place symbols
119 | - `add_schematic_wire` - Connect component pins
120 | - `list_schematic_libraries` - List symbol libraries
121 | - `export_schematic_pdf` - Export schematic PDF
122 |
123 | ### UI Management (2 tools)
124 | - `check_kicad_ui` - Check if KiCAD is running
125 | - `launch_kicad_ui` - Launch KiCAD application
126 |
127 | ## Prerequisites
128 |
129 | ### Required Software
130 |
131 | **KiCAD 9.0 or Higher**
132 | - Download from [kicad.org/download](https://www.kicad.org/download/)
133 | - Must include Python module (pcbnew)
134 | - Verify installation:
135 | ```bash
136 | python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
137 | ```
138 |
139 | **Node.js 18 or Higher**
140 | - Download from [nodejs.org](https://nodejs.org/)
141 | - Verify: `node --version` and `npm --version`
142 |
143 | **Python 3.10 or Higher**
144 | - Usually included with KiCAD
145 | - Required packages (auto-installed):
146 | - kicad-python (kipy) >= 0.5.0 (IPC API support, optional but recommended)
147 | - kicad-skip >= 0.1.0 (schematic support)
148 | - Pillow >= 9.0.0 (image processing)
149 | - cairosvg >= 2.7.0 (SVG rendering)
150 | - colorlog >= 6.7.0 (logging)
151 | - pydantic >= 2.5.0 (validation)
152 | - requests >= 2.32.5 (HTTP client)
153 | - python-dotenv >= 1.0.0 (environment)
154 |
155 | **MCP Client**
156 | Choose one:
157 | - [Claude Desktop](https://claude.ai/download) - Official Anthropic desktop app
158 | - [Claude Code](https://docs.claude.com/claude-code) - Official CLI tool
159 | - [Cline](https://github.com/cline/cline) - VSCode extension
160 |
161 | ### Supported Platforms
162 | - **Linux** (Ubuntu 22.04+, Fedora, Arch) - Primary platform, fully tested
163 | - **Windows 10/11** - Fully supported with automated setup
164 | - **macOS** - Experimental support
165 |
166 | ## Installation
167 |
168 | ### Linux (Ubuntu/Debian)
169 |
170 | ```bash
171 | # Install KiCAD 9.0
172 | sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
173 | sudo apt-get update
174 | sudo apt-get install -y kicad kicad-libraries
175 |
176 | # Install Node.js
177 | curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
178 | sudo apt-get install -y nodejs
179 |
180 | # Clone and build
181 | git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
182 | cd KiCAD-MCP-Server
183 | npm install
184 | pip3 install -r requirements.txt
185 | npm run build
186 |
187 | # Verify
188 | python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
189 | ```
190 |
191 | ### Windows 10/11
192 |
193 | **Automated Setup (Recommended):**
194 | ```powershell
195 | git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
196 | cd KiCAD-MCP-Server
197 | .\setup-windows.ps1
198 | ```
199 |
200 | The script will:
201 | - Detect KiCAD installation
202 | - Verify prerequisites
203 | - Install dependencies
204 | - Build project
205 | - Generate configuration
206 | - Run diagnostics
207 |
208 | **Manual Setup:**
209 | See [Windows Installation Guide](docs/WINDOWS_SETUP.md) for detailed instructions.
210 |
211 | ### macOS
212 |
213 | ```bash
214 | # Install KiCAD 9.0 from kicad.org/download/macos
215 |
216 | # Install Node.js
217 | brew install node@20
218 |
219 | # Clone and build
220 | git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
221 | cd KiCAD-MCP-Server
222 | npm install
223 | pip3 install -r requirements.txt
224 | npm run build
225 | ```
226 |
227 | ## Configuration
228 |
229 | ### Claude Desktop
230 |
231 | Edit configuration file:
232 | - **Linux/macOS:** `~/.config/Claude/claude_desktop_config.json`
233 | - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
234 |
235 | **Configuration:**
236 | ```json
237 | {
238 | "mcpServers": {
239 | "kicad": {
240 | "command": "node",
241 | "args": ["/path/to/KiCAD-MCP-Server/dist/index.js"],
242 | "env": {
243 | "PYTHONPATH": "/path/to/kicad/python",
244 | "LOG_LEVEL": "info"
245 | }
246 | }
247 | }
248 | }
249 | ```
250 |
251 | **Platform-specific PYTHONPATH:**
252 | - **Linux:** `/usr/lib/kicad/lib/python3/dist-packages`
253 | - **Windows:** `C:\Program Files\KiCad\9.0\lib\python3\dist-packages`
254 | - **macOS:** `/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages`
255 |
256 | ### Cline (VSCode)
257 |
258 | Edit: `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
259 |
260 | Use the same configuration format as Claude Desktop above.
261 |
262 | ### Claude Code
263 |
264 | Claude Code automatically detects MCP servers in the current directory. No additional configuration needed.
265 |
266 | ## Usage Examples
267 |
268 | ### Basic PCB Design Workflow
269 |
270 | ```text
271 | Create a new KiCAD project named 'LEDBoard' in my Documents folder.
272 | Set the board size to 50mm x 50mm and add a rectangular outline.
273 | Place a mounting hole at each corner, 3mm from the edges, with 3mm diameter.
274 | Add text 'LED Controller v1.0' on the front silkscreen at position x=25mm, y=45mm.
275 | ```
276 |
277 | ### Component Placement
278 |
279 | ```text
280 | Place an LED at x=10mm, y=10mm using footprint LED_SMD:LED_0805_2012Metric.
281 | Create a grid of 4 resistors (R1-R4) starting at x=20mm, y=20mm with 5mm spacing.
282 | Align all resistors horizontally and distribute them evenly.
283 | ```
284 |
285 | ### Routing
286 |
287 | ```text
288 | Create a net named 'LED1' and route a 0.3mm trace from R1 pad 2 to LED1 anode.
289 | Add a copper pour for GND on the bottom layer covering the entire board.
290 | Create a differential pair for USB_P and USB_N with 0.2mm width and 0.15mm gap.
291 | ```
292 |
293 | ### Design Verification
294 |
295 | ```text
296 | Set design rules with 0.15mm clearance and 0.2mm minimum track width.
297 | Run a design rule check and show me any violations.
298 | Export Gerber files to the 'fabrication' folder.
299 | ```
300 |
301 | ### Using Resources
302 |
303 | Resources provide read-only access to project state:
304 |
305 | ```text
306 | Show me the current component list.
307 | What are the current design rules?
308 | Display the board preview.
309 | List all electrical nets.
310 | ```
311 |
312 | ## Architecture
313 |
314 | ### MCP Protocol Layer
315 | - **JSON-RPC 2.0 Transport:** Bi-directional communication via STDIO
316 | - **Protocol Version:** MCP 2025-06-18
317 | - **Capabilities:** Tools (52), Resources (8)
318 | - **Error Handling:** Standard JSON-RPC error codes
319 |
320 | ### TypeScript Server (`src/`)
321 | - Implements MCP protocol specification
322 | - Manages Python subprocess lifecycle
323 | - Handles message routing and validation
324 | - Provides logging and error recovery
325 |
326 | ### Python Interface (`python/`)
327 | - **kicad_interface.py:** Main entry point, MCP message handler, command routing
328 | - **kicad_api/:** Backend implementations
329 | - `base.py` - Abstract base classes for backends
330 | - `ipc_backend.py` - KiCAD 9.0 IPC API backend (real-time UI sync)
331 | - `swig_backend.py` - pcbnew SWIG API backend (file-based operations)
332 | - `factory.py` - Backend auto-detection and instantiation
333 | - **schemas/tool_schemas.py:** JSON Schema definitions for all tools
334 | - **resources/resource_definitions.py:** Resource handlers and URIs
335 | - **commands/:** Modular command implementations
336 | - `project.py` - Project operations
337 | - `board.py` - Board manipulation
338 | - `component.py` - Component placement
339 | - `routing.py` - Trace routing and nets
340 | - `design_rules.py` - DRC operations
341 | - `export.py` - File generation
342 | - `schematic.py` - Schematic design
343 | - `library.py` - Footprint libraries
344 |
345 | ### KiCAD Integration
346 | - **pcbnew API (SWIG):** Direct Python bindings to KiCAD for file operations
347 | - **IPC API (kipy):** Real-time communication with running KiCAD instance (experimental)
348 | - **Hybrid Backend:** Automatically uses IPC when available, falls back to SWIG
349 | - **kicad-skip:** Schematic file manipulation
350 | - **Platform Detection:** Cross-platform path handling
351 | - **UI Management:** Automatic KiCAD UI launch/detection
352 |
353 | ## Development
354 |
355 | ### Building from Source
356 |
357 | ```bash
358 | # Install dependencies
359 | npm install
360 | pip3 install -r requirements.txt
361 |
362 | # Build TypeScript
363 | npm run build
364 |
365 | # Watch mode for development
366 | npm run dev
367 | ```
368 |
369 | ### Running Tests
370 |
371 | ```bash
372 | # TypeScript tests
373 | npm run test:ts
374 |
375 | # Python tests
376 | npm run test:py
377 |
378 | # All tests with coverage
379 | npm run test:coverage
380 | ```
381 |
382 | ### Linting and Formatting
383 |
384 | ```bash
385 | # Lint TypeScript and Python
386 | npm run lint
387 |
388 | # Format code
389 | npm run format
390 | ```
391 |
392 | ## Troubleshooting
393 |
394 | ### Server Not Appearing in Client
395 |
396 | **Symptoms:** MCP server doesn't show up in Claude Desktop or Cline
397 |
398 | **Solutions:**
399 | 1. Verify build completed: `ls dist/index.js`
400 | 2. Check configuration paths are absolute
401 | 3. Restart MCP client completely
402 | 4. Check client logs for error messages
403 |
404 | ### Python Module Import Errors
405 |
406 | **Symptoms:** `ModuleNotFoundError: No module named 'pcbnew'`
407 |
408 | **Solutions:**
409 | 1. Verify KiCAD installation: `python3 -c "import pcbnew"`
410 | 2. Check PYTHONPATH in configuration matches your KiCAD installation
411 | 3. Ensure KiCAD was installed with Python support
412 |
413 | ### Tool Execution Failures
414 |
415 | **Symptoms:** Tools fail with unclear errors
416 |
417 | **Solutions:**
418 | 1. Check server logs: `~/.kicad-mcp/logs/kicad_interface.log`
419 | 2. Verify a project is loaded before running board operations
420 | 3. Ensure file paths are absolute, not relative
421 | 4. Check tool parameter types match schema requirements
422 |
423 | ### Windows-Specific Issues
424 |
425 | **Symptoms:** Server fails to start on Windows
426 |
427 | **Solutions:**
428 | 1. Run automated diagnostics: `.\setup-windows.ps1`
429 | 2. Verify Python path uses double backslashes: `C:\\Program Files\\KiCad\\9.0`
430 | 3. Check Windows Event Viewer for Node.js errors
431 | 4. See [Windows Troubleshooting Guide](docs/WINDOWS_TROUBLESHOOTING.md)
432 |
433 | ### Getting Help
434 |
435 | 1. Check the [GitHub Issues](https://github.com/mixelpixx/KiCAD-MCP-Server/issues)
436 | 2. Review server logs: `~/.kicad-mcp/logs/kicad_interface.log`
437 | 3. Open a new issue with:
438 | - Operating system and version
439 | - KiCAD version (`python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"`)
440 | - Node.js version (`node --version`)
441 | - Full error message and stack trace
442 | - Relevant log excerpts
443 |
444 | ## Project Status
445 |
446 | **Current Version:** 2.1.0-alpha
447 |
448 | **Working Features:**
449 | - Project creation and management
450 | - Board outline and sizing
451 | - Layer management
452 | - Component placement with footprint library loading
453 | - Mounting holes and text annotations
454 | - Design rule checking
455 | - Export to Gerber, PDF, SVG, 3D
456 | - Schematic creation and editing
457 | - UI auto-launch
458 | - Full MCP protocol compliance
459 |
460 | **Under Active Development (IPC Backend):**
461 | - Real-time UI synchronization via KiCAD 9.0 IPC API
462 | - IPC-enabled commands: route_trace, add_via, place_component, move_component, delete_component, add_copper_pour, refill_zones, add_board_outline, add_mounting_hole, and more
463 | - Hybrid footprint loading (SWIG for library access, IPC for placement)
464 | - Zone/copper pour support via IPC
465 |
466 | Note: IPC features are experimental and under testing. Some commands may not work as expected in all scenarios.
467 |
468 | **Planned:**
469 | - JLCPCB parts integration
470 | - Digikey API integration
471 | - Advanced routing algorithms
472 | - Smart BOM management
473 | - AI-assisted component selection
474 | - Design pattern library (Arduino shields, RPi HATs)
475 |
476 | See [ROADMAP.md](docs/ROADMAP.md) for detailed development timeline.
477 |
478 | ## Contributing
479 |
480 | Contributions are welcome! Please follow these guidelines:
481 |
482 | 1. **Report Bugs:** Open an issue with reproduction steps
483 | 2. **Suggest Features:** Describe use case and expected behavior
484 | 3. **Submit Pull Requests:**
485 | - Fork the repository
486 | - Create a feature branch
487 | - Follow existing code style
488 | - Add tests for new functionality
489 | - Update documentation
490 | - Submit PR with clear description
491 |
492 | See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
493 |
494 | ## License
495 |
496 | This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
497 |
498 | ## Acknowledgments
499 |
500 | - Built on the [Model Context Protocol](https://modelcontextprotocol.io/) by Anthropic
501 | - Powered by [KiCAD](https://www.kicad.org/) open-source PCB design software
502 | - Uses [kicad-skip](https://github.com/kicad-skip) for schematic manipulation
503 |
504 | ## Citation
505 |
506 | If you use this project in your research or publication, please cite:
507 |
508 | ```bibtex
509 | @software{kicad_mcp_server,
510 | title = {KiCAD MCP Server: AI-Assisted PCB Design},
511 | author = {mixelpixx},
512 | year = {2025},
513 | url = {https://github.com/mixelpixx/KiCAD-MCP-Server},
514 | version = {2.1.0-alpha}
515 | }
516 | ```
517 |
518 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to KiCAD MCP Server
2 |
3 | Thank you for your interest in contributing to the KiCAD MCP Server! This guide will help you get started with development.
4 |
5 | ## Table of Contents
6 |
7 | - [Development Environment Setup](#development-environment-setup)
8 | - [Project Structure](#project-structure)
9 | - [Development Workflow](#development-workflow)
10 | - [Testing](#testing)
11 | - [Code Style](#code-style)
12 | - [Pull Request Process](#pull-request-process)
13 | - [Roadmap & Planning](#roadmap--planning)
14 |
15 | ---
16 |
17 | ## Development Environment Setup
18 |
19 | ### Prerequisites
20 |
21 | - **KiCAD 9.0 or higher** - [Download here](https://www.kicad.org/download/)
22 | - **Node.js v18+** - [Download here](https://nodejs.org/)
23 | - **Python 3.10+** - Should come with KiCAD, or install separately
24 | - **Git** - For version control
25 |
26 | ### Platform-Specific Setup
27 |
28 | #### Linux (Ubuntu/Debian)
29 |
30 | ```bash
31 | # Install KiCAD 9.0 from official PPA
32 | sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
33 | sudo apt-get update
34 | sudo apt-get install -y kicad kicad-libraries
35 |
36 | # Install Node.js (if not already installed)
37 | curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
38 | sudo apt-get install -y nodejs
39 |
40 | # Clone the repository
41 | git clone https://github.com/yourusername/kicad-mcp-server.git
42 | cd kicad-mcp-server
43 |
44 | # Install Node.js dependencies
45 | npm install
46 |
47 | # Install Python dependencies
48 | pip3 install -r requirements-dev.txt
49 |
50 | # Build TypeScript
51 | npm run build
52 |
53 | # Run tests
54 | npm test
55 | pytest
56 | ```
57 |
58 | #### Windows
59 |
60 | ```powershell
61 | # Install KiCAD 9.0 from https://www.kicad.org/download/windows/
62 |
63 | # Install Node.js from https://nodejs.org/
64 |
65 | # Clone the repository
66 | git clone https://github.com/yourusername/kicad-mcp-server.git
67 | cd kicad-mcp-server
68 |
69 | # Install Node.js dependencies
70 | npm install
71 |
72 | # Install Python dependencies
73 | pip install -r requirements-dev.txt
74 |
75 | # Build TypeScript
76 | npm run build
77 |
78 | # Run tests
79 | npm test
80 | pytest
81 | ```
82 |
83 | #### macOS
84 |
85 | ```bash
86 | # Install KiCAD 9.0 from https://www.kicad.org/download/macos/
87 |
88 | # Install Node.js via Homebrew
89 | brew install node
90 |
91 | # Clone the repository
92 | git clone https://github.com/yourusername/kicad-mcp-server.git
93 | cd kicad-mcp-server
94 |
95 | # Install Node.js dependencies
96 | npm install
97 |
98 | # Install Python dependencies
99 | pip3 install -r requirements-dev.txt
100 |
101 | # Build TypeScript
102 | npm run build
103 |
104 | # Run tests
105 | npm test
106 | pytest
107 | ```
108 |
109 | ---
110 |
111 | ## Project Structure
112 |
113 | ```
114 | kicad-mcp-server/
115 | ├── .github/
116 | │ └── workflows/ # CI/CD pipelines
117 | ├── config/ # Configuration examples
118 | │ ├── linux-config.example.json
119 | │ ├── windows-config.example.json
120 | │ └── macos-config.example.json
121 | ├── docs/ # Documentation
122 | ├── python/ # Python interface layer
123 | │ ├── commands/ # KiCAD command handlers
124 | │ ├── integrations/ # External API integrations (JLCPCB, Digikey)
125 | │ ├── utils/ # Utility modules
126 | │ └── kicad_interface.py # Main Python entry point
127 | ├── src/ # TypeScript MCP server
128 | │ ├── tools/ # MCP tool implementations
129 | │ ├── resources/ # MCP resource implementations
130 | │ ├── prompts/ # MCP prompt implementations
131 | │ └── server.ts # Main server
132 | ├── tests/ # Test suite
133 | │ ├── unit/
134 | │ ├── integration/
135 | │ └── fixtures/
136 | ├── dist/ # Compiled JavaScript (generated)
137 | ├── node_modules/ # Node dependencies (generated)
138 | ├── package.json # Node.js configuration
139 | ├── tsconfig.json # TypeScript configuration
140 | ├── pytest.ini # Pytest configuration
141 | ├── requirements.txt # Python production dependencies
142 | └── requirements-dev.txt # Python dev dependencies
143 | ```
144 |
145 | ---
146 |
147 | ## Development Workflow
148 |
149 | ### 1. Create a Feature Branch
150 |
151 | ```bash
152 | git checkout -b feature/your-feature-name
153 | ```
154 |
155 | ### 2. Make Changes
156 |
157 | - Edit TypeScript files in `src/`
158 | - Edit Python files in `python/`
159 | - Add tests for new features
160 |
161 | ### 3. Build & Test
162 |
163 | ```bash
164 | # Build TypeScript
165 | npm run build
166 |
167 | # Run TypeScript linter
168 | npm run lint
169 |
170 | # Run Python formatter
171 | black python/
172 |
173 | # Run Python type checker
174 | mypy python/
175 |
176 | # Run all tests
177 | npm test
178 | pytest
179 |
180 | # Run specific test file
181 | pytest tests/test_platform_helper.py -v
182 |
183 | # Run with coverage
184 | pytest --cov=python --cov-report=html
185 | ```
186 |
187 | ### 4. Commit Changes
188 |
189 | ```bash
190 | git add .
191 | git commit -m "feat: Add your feature description"
192 | ```
193 |
194 | **Commit Message Convention:**
195 | - `feat:` - New feature
196 | - `fix:` - Bug fix
197 | - `docs:` - Documentation changes
198 | - `test:` - Adding/updating tests
199 | - `refactor:` - Code refactoring
200 | - `chore:` - Maintenance tasks
201 |
202 | ### 5. Push and Create Pull Request
203 |
204 | ```bash
205 | git push origin feature/your-feature-name
206 | ```
207 |
208 | Then create a Pull Request on GitHub.
209 |
210 | ---
211 |
212 | ## Testing
213 |
214 | ### Running Tests
215 |
216 | ```bash
217 | # All tests
218 | pytest
219 |
220 | # Unit tests only
221 | pytest -m unit
222 |
223 | # Integration tests (requires KiCAD installed)
224 | pytest -m integration
225 |
226 | # Platform-specific tests
227 | pytest -m linux # Linux tests only
228 | pytest -m windows # Windows tests only
229 |
230 | # With coverage report
231 | pytest --cov=python --cov-report=term-missing
232 |
233 | # Verbose output
234 | pytest -v
235 |
236 | # Stop on first failure
237 | pytest -x
238 | ```
239 |
240 | ### Writing Tests
241 |
242 | Tests should be placed in `tests/` directory:
243 |
244 | ```python
245 | # tests/test_my_feature.py
246 | import pytest
247 |
248 | @pytest.mark.unit
249 | def test_my_feature():
250 | """Test description"""
251 | # Arrange
252 | expected = "result"
253 |
254 | # Act
255 | result = my_function()
256 |
257 | # Assert
258 | assert result == expected
259 |
260 | @pytest.mark.integration
261 | @pytest.mark.linux
262 | def test_linux_integration():
263 | """Integration test for Linux"""
264 | # This test will only run on Linux in CI
265 | pass
266 | ```
267 |
268 | ---
269 |
270 | ## Code Style
271 |
272 | ### Python
273 |
274 | We use **Black** for code formatting and **MyPy** for type checking.
275 |
276 | ```bash
277 | # Format all Python files
278 | black python/
279 |
280 | # Check types
281 | mypy python/
282 |
283 | # Run linter
284 | pylint python/
285 | ```
286 |
287 | **Python Style Guidelines:**
288 | - Use type hints for all function signatures
289 | - Use pathlib.Path for file paths (not os.path)
290 | - Use descriptive variable names
291 | - Add docstrings to all public functions/classes
292 | - Follow PEP 8
293 |
294 | **Example:**
295 | ```python
296 | from pathlib import Path
297 | from typing import List, Optional
298 |
299 | def find_kicad_libraries(search_path: Path) -> List[Path]:
300 | """
301 | Find all KiCAD symbol libraries in the given path.
302 |
303 | Args:
304 | search_path: Directory to search for .kicad_sym files
305 |
306 | Returns:
307 | List of paths to found library files
308 |
309 | Raises:
310 | ValueError: If search_path doesn't exist
311 | """
312 | if not search_path.exists():
313 | raise ValueError(f"Search path does not exist: {search_path}")
314 |
315 | return list(search_path.glob("**/*.kicad_sym"))
316 | ```
317 |
318 | ### TypeScript
319 |
320 | We use **ESLint** and **Prettier** for TypeScript.
321 |
322 | ```bash
323 | # Format TypeScript files
324 | npx prettier --write "src/**/*.ts"
325 |
326 | # Run linter
327 | npx eslint src/
328 | ```
329 |
330 | **TypeScript Style Guidelines:**
331 | - Use interfaces for data structures
332 | - Use async/await for asynchronous code
333 | - Use descriptive variable names
334 | - Add JSDoc comments to exported functions
335 |
336 | ---
337 |
338 | ## Pull Request Process
339 |
340 | 1. **Update Documentation** - If you change functionality, update docs
341 | 2. **Add Tests** - All new features should have tests
342 | 3. **Run CI Locally** - Ensure all tests pass before pushing
343 | 4. **Create PR** - Use a clear, descriptive title
344 | 5. **Request Review** - Tag relevant maintainers
345 | 6. **Address Feedback** - Make requested changes
346 | 7. **Merge** - Maintainer will merge when approved
347 |
348 | ### PR Checklist
349 |
350 | - [ ] Code follows style guidelines
351 | - [ ] All tests pass locally
352 | - [ ] New tests added for new features
353 | - [ ] Documentation updated
354 | - [ ] Commit messages follow convention
355 | - [ ] No merge conflicts
356 | - [ ] CI/CD pipeline passes
357 |
358 | ---
359 |
360 | ## Roadmap & Planning
361 |
362 | We track work using GitHub Projects and Issues:
363 |
364 | - **GitHub Projects** - High-level roadmap and sprints
365 | - **GitHub Issues** - Specific bugs and features
366 | - **GitHub Discussions** - Design discussions and proposals
367 |
368 | ### Current Priorities (Week 1-4)
369 |
370 | 1. ✅ Linux compatibility fixes
371 | 2. ✅ Platform-agnostic path handling
372 | 3. ✅ CI/CD pipeline setup
373 | 4. 🔄 Migrate to KiCAD IPC API
374 | 5. ⏳ Add JLCPCB integration
375 | 6. ⏳ Add Digikey integration
376 |
377 | See [docs/REBUILD_PLAN.md](docs/REBUILD_PLAN.md) for the complete 12-week roadmap.
378 |
379 | ---
380 |
381 | ## Getting Help
382 |
383 | - **GitHub Discussions** - Ask questions, propose ideas
384 | - **GitHub Issues** - Report bugs, request features
385 | - **Discord** - Real-time chat (link TBD)
386 |
387 | ---
388 |
389 | ## License
390 |
391 | By contributing, you agree that your contributions will be licensed under the MIT License.
392 |
393 | ---
394 |
395 | ## Thank You! 🎉
396 |
397 | Your contributions make this project better for everyone. We appreciate your time and effort!
398 |
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Tests for KiCAD MCP Server"""
2 |
```
--------------------------------------------------------------------------------
/python/utils/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Utility modules for KiCAD MCP Server"""
2 |
```
--------------------------------------------------------------------------------
/python/schemas/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tool schema definitions for KiCAD MCP Server
3 | """
4 |
5 | from .tool_schemas import TOOL_SCHEMAS
6 |
7 | __all__ = ['TOOL_SCHEMAS']
8 |
```
--------------------------------------------------------------------------------
/python/requirements.txt:
--------------------------------------------------------------------------------
```
1 | # KiCAD MCP Python Interface Requirements
2 |
3 | # Image processing
4 | Pillow>=9.0.0
5 | cairosvg>=2.7.0
6 |
7 | # Type hints
8 | typing-extensions>=4.0.0
9 |
10 | # Logging
11 | colorlog>=6.7.0
12 |
13 | kicad-skip
14 |
```
--------------------------------------------------------------------------------
/config/default-config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "kicad-mcp-server",
3 | "version": "1.0.0",
4 | "description": "MCP server for KiCAD PCB design operations",
5 | "pythonPath": "",
6 | "kicadPath": "",
7 | "logLevel": "info",
8 | "logDir": ""
9 | }
10 |
```
--------------------------------------------------------------------------------
/python/resources/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Resource definitions for KiCAD MCP Server
3 | """
4 |
5 | from .resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
6 |
7 | __all__ = ['RESOURCE_DEFINITIONS', 'handle_resource_read']
8 |
```
--------------------------------------------------------------------------------
/src/prompts/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Prompts index for KiCAD MCP server
3 | *
4 | * Exports all prompt registration functions
5 | */
6 |
7 | export { registerComponentPrompts } from './component.js';
8 | export { registerRoutingPrompts } from './routing.js';
9 | export { registerDesignPrompts } from './design.js';
10 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "outDir": "dist",
9 | "declaration": true,
10 | "sourceMap": true
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": ["node_modules", "dist"]
14 | }
15 |
```
--------------------------------------------------------------------------------
/config/claude-desktop-config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "kicad_helper": {
4 | "command": "node",
5 | "args": ["dist/index.js"],
6 | "cwd": "c:/repo/KiCAD-MCP",
7 | "env": {
8 | "NODE_ENV": "production",
9 | "PYTHONPATH": "C:/Program Files/KiCad/9.0/lib/python3/dist-packages"
10 | },
11 | "description": "KiCAD PCB Design Assistant"
12 | }
13 | }
14 | }
15 |
```
--------------------------------------------------------------------------------
/python/commands/board.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Board-related command implementations for KiCAD interface
3 |
4 | This file is maintained for backward compatibility.
5 | It imports and re-exports the BoardCommands class from the board package.
6 | """
7 |
8 | from commands.board import BoardCommands
9 |
10 | # Re-export the BoardCommands class for backward compatibility
11 | __all__ = ['BoardCommands']
12 |
```
--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Resources index for KiCAD MCP server
3 | *
4 | * Exports all resource registration functions
5 | */
6 |
7 | export { registerProjectResources } from './project.js';
8 | export { registerBoardResources } from './board.js';
9 | export { registerComponentResources } from './component.js';
10 | export { registerLibraryResources } from './library.js';
11 |
```
--------------------------------------------------------------------------------
/python/commands/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | KiCAD command implementations package
3 | """
4 |
5 | from .project import ProjectCommands
6 | from .board import BoardCommands
7 | from .component import ComponentCommands
8 | from .routing import RoutingCommands
9 | from .design_rules import DesignRuleCommands
10 | from .export import ExportCommands
11 |
12 | __all__ = [
13 | 'ProjectCommands',
14 | 'BoardCommands',
15 | 'ComponentCommands',
16 | 'RoutingCommands',
17 | 'DesignRuleCommands',
18 | 'ExportCommands'
19 | ]
20 |
```
--------------------------------------------------------------------------------
/config/windows-config.example.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "kicad": {
4 | "command": "node",
5 | "args": ["C:\\Users\\YOUR_USERNAME\\MCP\\KiCAD-MCP-Server\\dist\\index.js"],
6 | "env": {
7 | "NODE_ENV": "production",
8 | "PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\bin\\Lib\\site-packages",
9 | "LOG_LEVEL": "info",
10 | "KICAD_AUTO_LAUNCH": "false"
11 | },
12 | "description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
13 | }
14 | }
15 | }
16 |
```
--------------------------------------------------------------------------------
/config/linux-config.example.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "kicad": {
4 | "command": "node",
5 | "args": ["/home/YOUR_USERNAME/MCP/KiCAD-MCP-Server/dist/index.js"],
6 | "env": {
7 | "NODE_ENV": "production",
8 | "PYTHONPATH": "/usr/share/kicad/scripting/plugins:/usr/lib/kicad/lib/python3/dist-packages",
9 | "LOG_LEVEL": "info",
10 | "KICAD_AUTO_LAUNCH": "false"
11 | },
12 | "description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
13 | }
14 | }
15 | }
16 |
```
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
```
1 | # KiCAD MCP Server - Development Dependencies
2 | # Testing, linting, and development tools
3 |
4 | # Include production dependencies
5 | -r requirements.txt
6 |
7 | # Testing framework
8 | pytest>=7.4.0
9 | pytest-cov>=4.1.0
10 | pytest-asyncio>=0.21.0
11 | pytest-mock>=3.11.0
12 |
13 | # Code quality
14 | black>=23.7.0
15 | mypy>=1.5.0
16 | pylint>=2.17.0
17 | flake8>=6.1.0
18 | isort>=5.12.0
19 |
20 | # Type stubs
21 | types-requests>=2.31.0
22 | types-Pillow>=10.0.0
23 |
24 | # Pre-commit hooks
25 | pre-commit>=3.3.0
26 |
27 | # Development utilities
28 | ipython>=8.14.0
29 | ipdb>=0.13.13
30 |
```
--------------------------------------------------------------------------------
/config/macos-config.example.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "kicad": {
4 | "command": "node",
5 | "args": ["/Users/YOUR_USERNAME/MCP/KiCAD-MCP-Server/dist/index.js"],
6 | "env": {
7 | "NODE_ENV": "production",
8 | "PYTHONPATH": "/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current/lib/python3.11/site-packages",
9 | "LOG_LEVEL": "info",
10 | "KICAD_AUTO_LAUNCH": "false"
11 | },
12 | "description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
13 | }
14 | }
15 | }
16 |
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | # KiCAD MCP Server - Python Dependencies
2 | # Production dependencies only
3 |
4 | # KiCAD Python API (IPC - for future migration)
5 | # kicad-python>=0.5.0 # Uncomment when migrating to IPC API
6 |
7 | # Schematic manipulation
8 | kicad-skip>=0.1.0
9 |
10 | # Image processing for board rendering
11 | Pillow>=9.0.0
12 |
13 | # SVG rendering
14 | cairosvg>=2.7.0
15 |
16 | # Colored logging
17 | colorlog>=6.7.0
18 |
19 | # Data validation (for future features)
20 | pydantic>=2.5.0
21 |
22 | # HTTP requests (for JLCPCB/Digikey APIs - future)
23 | requests>=2.32.5
24 |
25 | # Environment variable management
26 | python-dotenv>=1.0.0
27 |
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tools index for KiCAD MCP server
3 | *
4 | * Exports all tool registration functions
5 | */
6 |
7 | export { registerProjectTools } from './project.js';
8 | export { registerBoardTools } from './board.js';
9 | export { registerComponentTools } from './component.js';
10 | export { registerRoutingTools } from './routing.js';
11 | export { registerDesignRuleTools } from './design-rules.js';
12 | export { registerExportTools } from './export.js';
13 | export { registerSchematicTools } from './schematic.js';
14 | export { registerLibraryTools } from './library.js';
15 | export { registerUITools } from './ui.js';
16 |
```
--------------------------------------------------------------------------------
/scripts/auto_refresh_kicad.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # Auto-refresh KiCAD when .kicad_pcb files change
3 | # Usage: ./auto_refresh_kicad.sh /path/to/project.kicad_pcb
4 |
5 | if [ -z "$1" ]; then
6 | echo "Usage: $0 <path-to-kicad-pcb-file>"
7 | exit 1
8 | fi
9 |
10 | PCB_FILE="$1"
11 |
12 | if [ ! -f "$PCB_FILE" ]; then
13 | echo "Error: File not found: $PCB_FILE"
14 | exit 1
15 | fi
16 |
17 | echo "Monitoring: $PCB_FILE"
18 | echo "When changes are saved, KiCAD will detect them and prompt to reload."
19 | echo "Press Ctrl+C to stop monitoring."
20 |
21 | # Watch for file changes
22 | inotifywait -m -e modify "$PCB_FILE" |
23 | while read path action file; do
24 | echo "[$(date '+%H:%M:%S')] File changed - KiCAD should prompt to reload"
25 | # KiCAD automatically detects file changes in most versions
26 | done
27 |
```
--------------------------------------------------------------------------------
/package-json.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "kicad-mcp",
3 | "version": "1.0.0",
4 | "description": "Model Context Protocol server for KiCAD PCB design",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "build": "tsc",
9 | "start": "node dist/index.js",
10 | "dev": "tsc -w & nodemon dist/index.js",
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "keywords": [
14 | "kicad",
15 | "mcp",
16 | "model-context-protocol",
17 | "pcb-design",
18 | "ai",
19 | "claude"
20 | ],
21 | "author": "",
22 | "license": "MIT",
23 | "dependencies": {
24 | "@modelcontextprotocol/sdk": "^1.10.0",
25 | "dotenv": "^16.0.3",
26 | "zod": "^3.22.2"
27 | },
28 | "devDependencies": {
29 | "@types/node": "^20.5.6",
30 | "nodemon": "^3.0.1",
31 | "typescript": "^5.2.2"
32 | }
33 | }
34 |
```
--------------------------------------------------------------------------------
/python/kicad_api/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | KiCAD API Abstraction Layer
3 |
4 | This module provides a unified interface to KiCAD's Python APIs,
5 | supporting both the legacy SWIG bindings and the new IPC API.
6 |
7 | Usage:
8 | from kicad_api import create_backend
9 |
10 | # Auto-detect best available backend
11 | backend = create_backend()
12 |
13 | # Or specify explicitly
14 | backend = create_backend('ipc') # Use IPC API
15 | backend = create_backend('swig') # Use legacy SWIG
16 |
17 | # Connect and use
18 | if backend.connect():
19 | board = backend.get_board()
20 | board.set_size(100, 80)
21 | """
22 |
23 | from kicad_api.factory import create_backend
24 | from kicad_api.base import KiCADBackend
25 |
26 | __all__ = ['create_backend', 'KiCADBackend']
27 | __version__ = '2.0.0-alpha.1'
28 |
```
--------------------------------------------------------------------------------
/src/tools/component.txt:
--------------------------------------------------------------------------------
```
1 | /**
2 | * Component management tools for KiCAD MCP server
3 | */
4 |
5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6 | import { z } from 'zod';
7 | import { logger } from '../logger.js';
8 |
9 | // Command function type for KiCAD script calls
10 | type CommandFunction = (command: string, params: any) => Promise<any>;
11 |
12 | /**
13 | * Register component management tools with the MCP server
14 | *
15 | * @param server MCP server instance
16 | * @param callKicadScript Function to call KiCAD script commands
17 | */
18 | export function registerComponentTools(server: McpServer, callKicadScript: CommandFunction): void {
19 | logger.info('Registering component management tools');
20 |
21 | // ------------------------------------------------------
22 | // Place Component Tool
23 | // ------------------------------------------------------
24 | server.registerTool({
25 | name: "place_component",
26 | description: "Places a component on the PCB at the specified location",
```
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
```
1 | [pytest]
2 | # Pytest configuration for KiCAD MCP Server
3 |
4 | # Test discovery patterns
5 | python_files = test_*.py *_test.py
6 | python_classes = Test*
7 | python_functions = test_*
8 |
9 | # Test paths
10 | testpaths = tests python/tests
11 |
12 | # Minimum Python version
13 | minversion = 6.0
14 |
15 | # Additional options
16 | addopts =
17 | -ra
18 | --strict-markers
19 | --strict-config
20 | --showlocals
21 | --tb=short
22 | --cov=python
23 | --cov-report=term-missing
24 | --cov-report=html
25 | --cov-report=xml
26 | --cov-branch
27 |
28 | # Markers for organizing tests
29 | markers =
30 | unit: Unit tests (fast, no external dependencies)
31 | integration: Integration tests (requires KiCAD)
32 | slow: Slow-running tests
33 | linux: Linux-specific tests
34 | windows: Windows-specific tests
35 | macos: macOS-specific tests
36 |
37 | # Ignore patterns
38 | norecursedirs = .git .tox dist build *.egg node_modules
39 |
40 | # Coverage settings
41 | [coverage:run]
42 | source = python
43 | omit =
44 | */tests/*
45 | */test_*.py
46 | */__pycache__/*
47 | */site-packages/*
48 |
49 | [coverage:report]
50 | precision = 2
51 | show_missing = True
52 | skip_covered = False
53 |
```
--------------------------------------------------------------------------------
/src/tools/ui.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * UI/Process management tools for KiCAD MCP server
3 | */
4 |
5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6 | import { z } from 'zod';
7 | import { logger } from '../logger.js';
8 |
9 | export function registerUITools(server: McpServer, callKicadScript: Function) {
10 | // Check if KiCAD UI is running
11 | server.tool(
12 | "check_kicad_ui",
13 | "Check if KiCAD UI is currently running",
14 | {},
15 | async () => {
16 | logger.info('Checking KiCAD UI status');
17 | const result = await callKicadScript("check_kicad_ui", {});
18 | return {
19 | content: [{
20 | type: "text",
21 | text: JSON.stringify(result, null, 2)
22 | }]
23 | };
24 | }
25 | );
26 |
27 | // Launch KiCAD UI
28 | server.tool(
29 | "launch_kicad_ui",
30 | "Launch KiCAD UI, optionally with a project file",
31 | {
32 | projectPath: z.string().optional().describe("Optional path to .kicad_pcb file to open"),
33 | autoLaunch: z.boolean().optional().describe("Whether to launch KiCAD if not running (default: true)")
34 | },
35 | async (args: { projectPath?: string; autoLaunch?: boolean }) => {
36 | logger.info(`Launching KiCAD UI${args.projectPath ? ' with project: ' + args.projectPath : ''}`);
37 | const result = await callKicadScript("launch_kicad_ui", args);
38 | return {
39 | content: [{
40 | type: "text",
41 | text: JSON.stringify(result, null, 2)
42 | }]
43 | };
44 | }
45 | );
46 |
47 | logger.info('UI management tools registered');
48 | }
49 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "kicad-mcp",
3 | "version": "2.1.0-alpha",
4 | "description": "AI-assisted PCB design with KiCAD via Model Context Protocol",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "build": "tsc",
9 | "build:watch": "tsc --watch",
10 | "start": "node dist/index.js",
11 | "dev": "npm run build:watch & nodemon dist/index.js",
12 | "clean": "rm -rf dist",
13 | "rebuild": "npm run clean && npm run build",
14 | "test": "npm run test:ts && npm run test:py",
15 | "test:ts": "echo 'TypeScript tests not yet configured'",
16 | "test:py": "pytest tests/ -v",
17 | "test:coverage": "pytest tests/ --cov=python --cov-report=html --cov-report=term",
18 | "lint": "npm run lint:ts && npm run lint:py",
19 | "lint:ts": "eslint src/ || echo 'ESLint not configured'",
20 | "lint:py": "cd python && black . && mypy . && flake8 .",
21 | "format": "prettier --write 'src/**/*.ts' && black python/",
22 | "prepare": "npm run build",
23 | "pretest": "npm run build"
24 | },
25 | "keywords": [
26 | "kicad",
27 | "mcp",
28 | "model-context-protocol",
29 | "pcb-design",
30 | "ai",
31 | "claude"
32 | ],
33 | "author": "",
34 | "license": "MIT",
35 | "dependencies": {
36 | "@modelcontextprotocol/sdk": "^1.21.0",
37 | "dotenv": "^17.0.0",
38 | "express": "^5.1.0",
39 | "zod": "^3.25.0"
40 | },
41 | "devDependencies": {
42 | "@cfworker/json-schema": "^4.1.1",
43 | "@types/express": "^5.0.5",
44 | "@types/glob": "^8.1.0",
45 | "@types/node": "^20.19.0",
46 | "nodemon": "^3.0.1",
47 | "typescript": "^5.9.3"
48 | }
49 | }
50 |
```
--------------------------------------------------------------------------------
/src/utils/resource-helpers.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Resource helper utilities for MCP resources
3 | */
4 |
5 | /**
6 | * Create a JSON response for MCP resources
7 | *
8 | * @param data Data to serialize as JSON
9 | * @param uri Optional URI for the resource
10 | * @returns MCP resource response object
11 | */
12 | export function createJsonResponse(data: any, uri?: string) {
13 | return {
14 | contents: [{
15 | uri: uri || "data:application/json",
16 | mimeType: "application/json",
17 | text: JSON.stringify(data, null, 2)
18 | }]
19 | };
20 | }
21 |
22 | /**
23 | * Create a binary response for MCP resources
24 | *
25 | * @param data Binary data (Buffer or base64 string)
26 | * @param mimeType MIME type of the binary data
27 | * @param uri Optional URI for the resource
28 | * @returns MCP resource response object
29 | */
30 | export function createBinaryResponse(data: Buffer | string, mimeType: string, uri?: string) {
31 | const blob = typeof data === 'string' ? data : data.toString('base64');
32 |
33 | return {
34 | contents: [{
35 | uri: uri || `data:${mimeType}`,
36 | mimeType: mimeType,
37 | blob: blob
38 | }]
39 | };
40 | }
41 |
42 | /**
43 | * Create an error response for MCP resources
44 | *
45 | * @param error Error message
46 | * @param details Optional error details
47 | * @param uri Optional URI for the resource
48 | * @returns MCP resource error response
49 | */
50 | export function createErrorResponse(error: string, details?: string, uri?: string) {
51 | return {
52 | contents: [{
53 | uri: uri || "data:application/json",
54 | mimeType: "application/json",
55 | text: JSON.stringify({
56 | error,
57 | details
58 | }, null, 2)
59 | }]
60 | };
61 | }
62 |
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Configuration handling for KiCAD MCP server
3 | */
4 |
5 | import { readFile } from 'fs/promises';
6 | import { existsSync } from 'fs';
7 | import { join, dirname } from 'path';
8 | import { fileURLToPath } from 'url';
9 | import { z } from 'zod';
10 | import { logger } from './logger.js';
11 |
12 | // Get the current directory
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = dirname(__filename);
15 |
16 | // Default config location
17 | const DEFAULT_CONFIG_PATH = join(dirname(__dirname), 'config', 'default-config.json');
18 |
19 | /**
20 | * Server configuration schema
21 | */
22 | const ConfigSchema = z.object({
23 | name: z.string().default('kicad-mcp-server'),
24 | version: z.string().default('1.0.0'),
25 | description: z.string().default('MCP server for KiCAD PCB design operations'),
26 | pythonPath: z.string().optional(),
27 | kicadPath: z.string().optional(),
28 | logLevel: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
29 | logDir: z.string().optional()
30 | });
31 |
32 | /**
33 | * Server configuration type
34 | */
35 | export type Config = z.infer<typeof ConfigSchema>;
36 |
37 | /**
38 | * Load configuration from file
39 | *
40 | * @param configPath Path to the configuration file (optional)
41 | * @returns Loaded and validated configuration
42 | */
43 | export async function loadConfig(configPath?: string): Promise<Config> {
44 | try {
45 | // Determine which config file to load
46 | const filePath = configPath || DEFAULT_CONFIG_PATH;
47 |
48 | // Check if file exists
49 | if (!existsSync(filePath)) {
50 | logger.warn(`Configuration file not found: ${filePath}, using defaults`);
51 | return ConfigSchema.parse({});
52 | }
53 |
54 | // Read and parse configuration
55 | const configData = await readFile(filePath, 'utf-8');
56 | const config = JSON.parse(configData);
57 |
58 | // Validate configuration
59 | return ConfigSchema.parse(config);
60 | } catch (error) {
61 | logger.error(`Error loading configuration: ${error}`);
62 |
63 | // Return default configuration
64 | return ConfigSchema.parse({});
65 | }
66 | }
67 |
```
--------------------------------------------------------------------------------
/src/tools/project.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Project management tools for KiCAD MCP server
3 | */
4 |
5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6 | import { z } from 'zod';
7 |
8 | export function registerProjectTools(server: McpServer, callKicadScript: Function) {
9 | // Create project tool
10 | server.tool(
11 | "create_project",
12 | "Create a new KiCAD project",
13 | {
14 | path: z.string().describe("Project directory path"),
15 | name: z.string().describe("Project name"),
16 | },
17 | async (args: { path: string; name: string }) => {
18 | const result = await callKicadScript("create_project", args);
19 | return {
20 | content: [{
21 | type: "text",
22 | text: JSON.stringify(result, null, 2)
23 | }]
24 | };
25 | }
26 | );
27 |
28 | // Open project tool
29 | server.tool(
30 | "open_project",
31 | "Open an existing KiCAD project",
32 | {
33 | filename: z.string().describe("Path to .kicad_pro or .kicad_pcb file"),
34 | },
35 | async (args: { filename: string }) => {
36 | const result = await callKicadScript("open_project", args);
37 | return {
38 | content: [{
39 | type: "text",
40 | text: JSON.stringify(result, null, 2)
41 | }]
42 | };
43 | }
44 | );
45 |
46 | // Save project tool
47 | server.tool(
48 | "save_project",
49 | "Save the current KiCAD project",
50 | {
51 | path: z.string().optional().describe("Optional new path to save to"),
52 | },
53 | async (args: { path?: string }) => {
54 | const result = await callKicadScript("save_project", args);
55 | return {
56 | content: [{
57 | type: "text",
58 | text: JSON.stringify(result, null, 2)
59 | }]
60 | };
61 | }
62 | );
63 |
64 | // Get project info tool
65 | server.tool(
66 | "get_project_info",
67 | "Get information about the current KiCAD project",
68 | {},
69 | async () => {
70 | const result = await callKicadScript("get_project_info", {});
71 | return {
72 | content: [{
73 | type: "text",
74 | text: JSON.stringify(result, null, 2)
75 | }]
76 | };
77 | }
78 | );
79 | }
80 |
```
--------------------------------------------------------------------------------
/python/commands/board/size.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Board size command implementations for KiCAD interface
3 | """
4 |
5 | import pcbnew
6 | import logging
7 | from typing import Dict, Any, Optional
8 |
9 | logger = logging.getLogger('kicad_interface')
10 |
11 | class BoardSizeCommands:
12 | """Handles board size operations"""
13 |
14 | def __init__(self, board: Optional[pcbnew.BOARD] = None):
15 | """Initialize with optional board instance"""
16 | self.board = board
17 |
18 | def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
19 | """Set the size of the PCB board by creating edge cuts outline"""
20 | try:
21 | if not self.board:
22 | return {
23 | "success": False,
24 | "message": "No board is loaded",
25 | "errorDetails": "Load or create a board first"
26 | }
27 |
28 | width = params.get("width")
29 | height = params.get("height")
30 | unit = params.get("unit", "mm")
31 |
32 | if width is None or height is None:
33 | return {
34 | "success": False,
35 | "message": "Missing dimensions",
36 | "errorDetails": "Both width and height are required"
37 | }
38 |
39 | # Create board outline using BoardOutlineCommands
40 | # This properly creates edge cuts on Edge.Cuts layer
41 | from commands.board.outline import BoardOutlineCommands
42 | outline_commands = BoardOutlineCommands(self.board)
43 |
44 | # Create rectangular outline centered at origin
45 | result = outline_commands.add_board_outline({
46 | "shape": "rectangle",
47 | "centerX": width / 2, # Center X
48 | "centerY": height / 2, # Center Y
49 | "width": width,
50 | "height": height,
51 | "unit": unit
52 | })
53 |
54 | if result.get("success"):
55 | return {
56 | "success": True,
57 | "message": f"Created board outline: {width}x{height} {unit}",
58 | "size": {
59 | "width": width,
60 | "height": height,
61 | "unit": unit
62 | }
63 | }
64 | else:
65 | return result
66 |
67 | except Exception as e:
68 | logger.error(f"Error setting board size: {str(e)}")
69 | return {
70 | "success": False,
71 | "message": "Failed to set board size",
72 | "errorDetails": str(e)
73 | }
74 |
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Logger for KiCAD MCP server
3 | */
4 |
5 | import { existsSync, mkdirSync, appendFileSync } from 'fs';
6 | import { join } from 'path';
7 | import * as os from 'os';
8 |
9 | // Log levels
10 | type LogLevel = 'error' | 'warn' | 'info' | 'debug';
11 |
12 | // Default log directory
13 | const DEFAULT_LOG_DIR = join(os.homedir(), '.kicad-mcp', 'logs');
14 |
15 | /**
16 | * Logger class for KiCAD MCP server
17 | */
18 | class Logger {
19 | private logLevel: LogLevel = 'info';
20 | private logDir: string = DEFAULT_LOG_DIR;
21 |
22 | /**
23 | * Set the log level
24 | * @param level Log level to set
25 | */
26 | setLogLevel(level: LogLevel): void {
27 | this.logLevel = level;
28 | }
29 |
30 | /**
31 | * Set the log directory
32 | * @param dir Directory to store log files
33 | */
34 | setLogDir(dir: string): void {
35 | this.logDir = dir;
36 |
37 | // Ensure log directory exists
38 | if (!existsSync(this.logDir)) {
39 | mkdirSync(this.logDir, { recursive: true });
40 | }
41 | }
42 |
43 | /**
44 | * Log an error message
45 | * @param message Message to log
46 | */
47 | error(message: string): void {
48 | this.log('error', message);
49 | }
50 |
51 | /**
52 | * Log a warning message
53 | * @param message Message to log
54 | */
55 | warn(message: string): void {
56 | if (['error', 'warn', 'info', 'debug'].includes(this.logLevel)) {
57 | this.log('warn', message);
58 | }
59 | }
60 |
61 | /**
62 | * Log an info message
63 | * @param message Message to log
64 | */
65 | info(message: string): void {
66 | if (['info', 'debug'].includes(this.logLevel)) {
67 | this.log('info', message);
68 | }
69 | }
70 |
71 | /**
72 | * Log a debug message
73 | * @param message Message to log
74 | */
75 | debug(message: string): void {
76 | if (this.logLevel === 'debug') {
77 | this.log('debug', message);
78 | }
79 | }
80 |
81 | /**
82 | * Log a message with the specified level
83 | * @param level Log level
84 | * @param message Message to log
85 | */
86 | private log(level: LogLevel, message: string): void {
87 | const timestamp = new Date().toISOString();
88 | const formattedMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
89 |
90 | // Log to console.error (stderr) only - stdout is reserved for MCP protocol
91 | // All log levels go to stderr to avoid corrupting STDIO MCP transport
92 | console.error(formattedMessage);
93 |
94 | // Log to file
95 | try {
96 | // Ensure log directory exists
97 | if (!existsSync(this.logDir)) {
98 | mkdirSync(this.logDir, { recursive: true });
99 | }
100 |
101 | const logFile = join(this.logDir, `kicad-mcp-${new Date().toISOString().split('T')[0]}.log`);
102 | appendFileSync(logFile, formattedMessage + '\n');
103 | } catch (error) {
104 | console.error(`Failed to write to log file: ${error}`);
105 | }
106 | }
107 | }
108 |
109 | // Create and export logger instance
110 | export const logger = new Logger();
111 |
```
--------------------------------------------------------------------------------
/src/tools/routing.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Routing tools for KiCAD MCP server
3 | */
4 |
5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6 | import { z } from 'zod';
7 |
8 | export function registerRoutingTools(server: McpServer, callKicadScript: Function) {
9 | // Add net tool
10 | server.tool(
11 | "add_net",
12 | "Create a new net on the PCB",
13 | {
14 | name: z.string().describe("Net name"),
15 | netClass: z.string().optional().describe("Net class name"),
16 | },
17 | async (args: { name: string; netClass?: string }) => {
18 | const result = await callKicadScript("add_net", args);
19 | return {
20 | content: [{
21 | type: "text",
22 | text: JSON.stringify(result, null, 2)
23 | }]
24 | };
25 | }
26 | );
27 |
28 | // Route trace tool
29 | server.tool(
30 | "route_trace",
31 | "Route a trace between two points",
32 | {
33 | start: z.object({
34 | x: z.number(),
35 | y: z.number(),
36 | unit: z.string().optional()
37 | }).describe("Start position"),
38 | end: z.object({
39 | x: z.number(),
40 | y: z.number(),
41 | unit: z.string().optional()
42 | }).describe("End position"),
43 | layer: z.string().describe("PCB layer"),
44 | width: z.number().describe("Trace width in mm"),
45 | net: z.string().describe("Net name"),
46 | },
47 | async (args: any) => {
48 | const result = await callKicadScript("route_trace", args);
49 | return {
50 | content: [{
51 | type: "text",
52 | text: JSON.stringify(result, null, 2)
53 | }]
54 | };
55 | }
56 | );
57 |
58 | // Add via tool
59 | server.tool(
60 | "add_via",
61 | "Add a via to the PCB",
62 | {
63 | position: z.object({
64 | x: z.number(),
65 | y: z.number(),
66 | unit: z.string().optional()
67 | }).describe("Via position"),
68 | net: z.string().describe("Net name"),
69 | viaType: z.string().optional().describe("Via type (through, blind, buried)"),
70 | },
71 | async (args: any) => {
72 | const result = await callKicadScript("add_via", args);
73 | return {
74 | content: [{
75 | type: "text",
76 | text: JSON.stringify(result, null, 2)
77 | }]
78 | };
79 | }
80 | );
81 |
82 | // Add copper pour tool
83 | server.tool(
84 | "add_copper_pour",
85 | "Add a copper pour (ground/power plane) to the PCB",
86 | {
87 | layer: z.string().describe("PCB layer"),
88 | net: z.string().describe("Net name"),
89 | clearance: z.number().optional().describe("Clearance in mm"),
90 | },
91 | async (args: any) => {
92 | const result = await callKicadScript("add_copper_pour", args);
93 | return {
94 | content: [{
95 | type: "text",
96 | text: JSON.stringify(result, null, 2)
97 | }]
98 | };
99 | }
100 | );
101 | }
102 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * KiCAD Model Context Protocol Server
3 | * Main entry point
4 | */
5 |
6 | import { join, dirname } from 'path';
7 | import { fileURLToPath } from 'url';
8 | import { KiCADMcpServer } from './server.js';
9 | import { loadConfig } from './config.js';
10 | import { logger } from './logger.js';
11 |
12 | // Get the current directory
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = dirname(__filename);
15 |
16 | /**
17 | * Main function to start the KiCAD MCP server
18 | */
19 | async function main() {
20 | try {
21 | // Parse command line arguments
22 | const args = process.argv.slice(2);
23 | const options = parseCommandLineArgs(args);
24 |
25 | // Load configuration
26 | const config = await loadConfig(options.configPath);
27 |
28 | // Path to the Python script that interfaces with KiCAD
29 | const kicadScriptPath = join(dirname(__dirname), 'python', 'kicad_interface.py');
30 |
31 | // Create the server
32 | const server = new KiCADMcpServer(
33 | kicadScriptPath,
34 | config.logLevel
35 | );
36 |
37 | // Start the server
38 | await server.start();
39 |
40 | // Setup graceful shutdown
41 | setupGracefulShutdown(server);
42 |
43 | logger.info('KiCAD MCP server started with STDIO transport');
44 |
45 | } catch (error) {
46 | logger.error(`Failed to start KiCAD MCP server: ${error}`);
47 | process.exit(1);
48 | }
49 | }
50 |
51 | /**
52 | * Parse command line arguments
53 | */
54 | function parseCommandLineArgs(args: string[]) {
55 | let configPath = undefined;
56 |
57 | for (let i = 0; i < args.length; i++) {
58 | if (args[i] === '--config' && i + 1 < args.length) {
59 | configPath = args[i + 1];
60 | i++;
61 | }
62 | }
63 |
64 | return { configPath };
65 | }
66 |
67 | /**
68 | * Setup graceful shutdown handlers
69 | */
70 | function setupGracefulShutdown(server: KiCADMcpServer) {
71 | // Handle termination signals
72 | process.on('SIGINT', async () => {
73 | logger.info('Received SIGINT signal. Shutting down...');
74 | await shutdownServer(server);
75 | });
76 |
77 | process.on('SIGTERM', async () => {
78 | logger.info('Received SIGTERM signal. Shutting down...');
79 | await shutdownServer(server);
80 | });
81 |
82 | // Handle uncaught exceptions
83 | process.on('uncaughtException', async (error) => {
84 | logger.error(`Uncaught exception: ${error}`);
85 | await shutdownServer(server);
86 | });
87 |
88 | // Handle unhandled promise rejections
89 | process.on('unhandledRejection', async (reason) => {
90 | logger.error(`Unhandled promise rejection: ${reason}`);
91 | await shutdownServer(server);
92 | });
93 | }
94 |
95 | /**
96 | * Shut down the server and exit
97 | */
98 | async function shutdownServer(server: KiCADMcpServer) {
99 | try {
100 | logger.info('Shutting down KiCAD MCP server...');
101 | await server.stop();
102 | logger.info('Server shutdown complete. Exiting...');
103 | process.exit(0);
104 | } catch (error) {
105 | logger.error(`Error during shutdown: ${error}`);
106 | process.exit(1);
107 | }
108 | }
109 |
110 | // Run the main function - always run when imported as module entry point
111 | // The import.meta.url check was failing on Windows due to path separators
112 | main().catch((error) => {
113 | console.error(`Unhandled error in main: ${error}`);
114 | process.exit(1);
115 | });
116 |
117 | // For testing and programmatic usage
118 | export { KiCADMcpServer };
119 |
```
--------------------------------------------------------------------------------
/CHANGELOG_2025-11-30.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog - 2025-11-30
2 |
3 | ## IPC Backend Implementation - Real-time UI Synchronization
4 |
5 | This release implements the **KiCAD IPC API backend**, enabling real-time UI synchronization between the MCP server and KiCAD. Changes made through MCP tools now appear **instantly** in the KiCAD UI without requiring manual reload.
6 |
7 | ### Major Features
8 |
9 | #### Real-time UI Sync via IPC API
10 | - **Instant updates**: Tracks, vias, components, and text appear immediately in KiCAD
11 | - **No reload required**: Eliminates the manual File > Reload workflow
12 | - **Transaction support**: Operations can be grouped for single undo/redo steps
13 | - **Auto-detection**: Server automatically uses IPC when KiCAD is running with IPC enabled
14 |
15 | #### Automatic Backend Selection
16 | - IPC backend is now the **default** when available
17 | - Transparent fallback to SWIG when IPC unavailable
18 | - Environment variable `KICAD_BACKEND` for explicit control:
19 | - `auto` (default): Try IPC first, fall back to SWIG
20 | - `ipc`: Force IPC only
21 | - `swig`: Force SWIG only (deprecated)
22 |
23 | #### Commands with IPC Support
24 | The following commands now automatically use IPC for real-time updates:
25 |
26 | | Command | Description |
27 | |---------|-------------|
28 | | `route_trace` | Add traces with instant UI update |
29 | | `add_via` | Add vias with instant UI update |
30 | | `add_text` / `add_board_text` | Add text with instant UI update |
31 | | `set_board_size` | Set board size with instant outline update |
32 | | `get_board_info` | Read live board data |
33 | | `place_component` | Place components with instant UI update |
34 | | `move_component` | Move components with instant UI update |
35 | | `delete_component` | Delete components with instant UI update |
36 | | `get_component_list` | Read live component list |
37 | | `save_project` | Save via IPC |
38 |
39 | ### New Files
40 |
41 | - `python/kicad_api/ipc_backend.py` - Complete IPC backend implementation (~870 lines)
42 | - `python/test_ipc_backend.py` - Test script for IPC functionality
43 | - `docs/IPC_BACKEND_STATUS.md` - Implementation status documentation
44 |
45 | ### Modified Files
46 |
47 | - `python/kicad_interface.py` - Added IPC integration and automatic command routing
48 | - `python/kicad_api/base.py` - Added routing and transaction methods to base class
49 | - `python/kicad_api/factory.py` - Fixed kipy module detection
50 | - `docs/ROADMAP.md` - Updated Week 3 status to complete
51 |
52 | ### Dependencies
53 |
54 | - Added `kicad-python>=0.5.0` - Official KiCAD IPC API Python library
55 |
56 | ### Requirements
57 |
58 | To use real-time mode:
59 | 1. KiCAD 9.0+ must be running
60 | 2. Enable IPC API: `Preferences > Plugins > Enable IPC API Server`
61 | 3. Have a board open in PCB editor
62 |
63 | ### Deprecation Notice
64 |
65 | The **SWIG backend is now deprecated**:
66 | - Will continue to work as fallback
67 | - No new features will be added to SWIG path
68 | - Will be removed when KiCAD 10.0 drops SWIG support
69 |
70 | ### Testing
71 |
72 | Run the IPC test script:
73 | ```bash
74 | ./venv/bin/python python/test_ipc_backend.py
75 | ```
76 |
77 | Or test individual commands:
78 | ```bash
79 | echo '{"command": "get_backend_info", "params": {}}' | \
80 | PYTHONPATH=python ./venv/bin/python python/kicad_interface.py
81 | ```
82 |
83 | ### Breaking Changes
84 |
85 | None. All existing commands continue to work. IPC is used transparently when available.
86 |
87 | ---
88 |
89 | **Version:** 2.1.0-alpha
90 | **Date:** 2025-11-30
91 |
```
--------------------------------------------------------------------------------
/python/commands/board/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Board-related command implementations for KiCAD interface
3 | """
4 |
5 | import pcbnew
6 | import logging
7 | from typing import Dict, Any, Optional
8 |
9 | # Import specialized modules
10 | from .size import BoardSizeCommands
11 | from .layers import BoardLayerCommands
12 | from .outline import BoardOutlineCommands
13 | from .view import BoardViewCommands
14 |
15 | logger = logging.getLogger('kicad_interface')
16 |
17 | class BoardCommands:
18 | """Handles board-related KiCAD operations"""
19 |
20 | def __init__(self, board: Optional[pcbnew.BOARD] = None):
21 | """Initialize with optional board instance"""
22 | self.board = board
23 |
24 | # Initialize specialized command classes
25 | self.size_commands = BoardSizeCommands(board)
26 | self.layer_commands = BoardLayerCommands(board)
27 | self.outline_commands = BoardOutlineCommands(board)
28 | self.view_commands = BoardViewCommands(board)
29 |
30 | # Delegate board size commands
31 | def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
32 | """Set the size of the PCB board"""
33 | self.size_commands.board = self.board
34 | return self.size_commands.set_board_size(params)
35 |
36 | # Delegate layer commands
37 | def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
38 | """Add a new layer to the PCB"""
39 | self.layer_commands.board = self.board
40 | return self.layer_commands.add_layer(params)
41 |
42 | def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
43 | """Set the active layer for PCB operations"""
44 | self.layer_commands.board = self.board
45 | return self.layer_commands.set_active_layer(params)
46 |
47 | def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
48 | """Get a list of all layers in the PCB"""
49 | self.layer_commands.board = self.board
50 | return self.layer_commands.get_layer_list(params)
51 |
52 | # Delegate board outline commands
53 | def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
54 | """Add a board outline to the PCB"""
55 | self.outline_commands.board = self.board
56 | return self.outline_commands.add_board_outline(params)
57 |
58 | def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
59 | """Add a mounting hole to the PCB"""
60 | self.outline_commands.board = self.board
61 | return self.outline_commands.add_mounting_hole(params)
62 |
63 | def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
64 | """Add text annotation to the PCB"""
65 | self.outline_commands.board = self.board
66 | return self.outline_commands.add_text(params)
67 |
68 | # Delegate view commands
69 | def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
70 | """Get information about the current board"""
71 | self.view_commands.board = self.board
72 | return self.view_commands.get_board_info(params)
73 |
74 | def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
75 | """Get a 2D image of the PCB"""
76 | self.view_commands.board = self.board
77 | return self.view_commands.get_board_2d_view(params)
78 |
79 | def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]:
80 | """Get the bounding box extents of the board"""
81 | self.view_commands.board = self.board
82 | return self.view_commands.get_board_extents(params)
83 |
```
--------------------------------------------------------------------------------
/python/commands/schematic.py:
--------------------------------------------------------------------------------
```python
1 | from skip import Schematic
2 | import os
3 | import logging
4 |
5 | logger = logging.getLogger('kicad_interface')
6 |
7 | class SchematicManager:
8 | """Core schematic operations using kicad-skip"""
9 |
10 | @staticmethod
11 | def create_schematic(name, metadata=None):
12 | """Create a new empty schematic"""
13 | # kicad-skip requires a filepath to create a schematic
14 | # We'll create a blank schematic file by loading an existing file
15 | # or we can create a template file first.
16 |
17 | # Create an empty template file first
18 | temp_path = f"{name}_template.kicad_sch"
19 | with open(temp_path, 'w') as f:
20 | # Write minimal schematic file content
21 | f.write("(kicad_sch (version 20230121) (generator \"KiCAD-MCP-Server\"))\n")
22 |
23 | # Now load it
24 | sch = Schematic(temp_path)
25 | sch.version = "20230121" # Set appropriate version
26 | sch.generator = "KiCAD-MCP-Server"
27 |
28 | # Clean up the template
29 | os.remove(temp_path)
30 | # Add metadata if provided
31 | if metadata:
32 | for key, value in metadata.items():
33 | # kicad-skip doesn't have a direct metadata property on Schematic,
34 | # but we can add properties to the root sheet if needed, or
35 | # include it in the file path/name convention.
36 | # For now, we'll just create the schematic.
37 | pass # Placeholder for potential metadata handling
38 |
39 | logger.info(f"Created new schematic: {name}")
40 | return sch
41 |
42 | @staticmethod
43 | def load_schematic(file_path):
44 | """Load an existing schematic"""
45 | if not os.path.exists(file_path):
46 | logger.error(f"Schematic file not found at {file_path}")
47 | return None
48 | try:
49 | sch = Schematic(file_path)
50 | logger.info(f"Loaded schematic from: {file_path}")
51 | return sch
52 | except Exception as e:
53 | logger.error(f"Error loading schematic from {file_path}: {e}")
54 | return None
55 |
56 | @staticmethod
57 | def save_schematic(schematic, file_path):
58 | """Save a schematic to file"""
59 | try:
60 | # kicad-skip uses write method, not save
61 | schematic.write(file_path)
62 | logger.info(f"Saved schematic to: {file_path}")
63 | return True
64 | except Exception as e:
65 | logger.error(f"Error saving schematic to {file_path}: {e}")
66 | return False
67 |
68 | @staticmethod
69 | def get_schematic_metadata(schematic):
70 | """Extract metadata from schematic"""
71 | # kicad-skip doesn't expose a direct metadata object on Schematic.
72 | # We can return basic info like version and generator.
73 | metadata = {
74 | "version": schematic.version,
75 | "generator": schematic.generator,
76 | # Add other relevant properties if needed
77 | }
78 | logger.debug("Extracted schematic metadata")
79 | return metadata
80 |
81 | if __name__ == '__main__':
82 | # Example Usage (for testing)
83 | # Create a new schematic
84 | new_sch = SchematicManager.create_schematic("MyTestSchematic")
85 |
86 | # Save the schematic
87 | test_file = "test_schematic.kicad_sch"
88 | SchematicManager.save_schematic(new_sch, test_file)
89 |
90 | # Load the schematic
91 | loaded_sch = SchematicManager.load_schematic(test_file)
92 | if loaded_sch:
93 | metadata = SchematicManager.get_schematic_metadata(loaded_sch)
94 | print(f"Loaded schematic metadata: {metadata}")
95 |
96 | # Clean up test file
97 | if os.path.exists(test_file):
98 | os.remove(test_file)
99 | print(f"Cleaned up {test_file}")
100 |
```
--------------------------------------------------------------------------------
/src/tools/library.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Library tools for KiCAD MCP server
3 | * Provides access to KiCAD footprint libraries and symbols
4 | */
5 |
6 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7 | import { z } from 'zod';
8 |
9 | export function registerLibraryTools(server: McpServer, callKicadScript: Function) {
10 | // List available footprint libraries
11 | server.tool(
12 | "list_libraries",
13 | "List all available KiCAD footprint libraries",
14 | {
15 | search_paths: z.array(z.string()).optional()
16 | .describe("Optional additional search paths for libraries")
17 | },
18 | async (args: { search_paths?: string[] }) => {
19 | const result = await callKicadScript("list_libraries", args);
20 | if (result.success && result.libraries) {
21 | return {
22 | content: [
23 | {
24 | type: "text",
25 | text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join('\n')}`
26 | }
27 | ]
28 | };
29 | }
30 | return {
31 | content: [
32 | {
33 | type: "text",
34 | text: `Failed to list libraries: ${result.message || 'Unknown error'}`
35 | }
36 | ]
37 | };
38 | }
39 | );
40 |
41 | // Search for footprints across all libraries
42 | server.tool(
43 | "search_footprints",
44 | "Search for footprints matching a pattern across all libraries",
45 | {
46 | search_term: z.string()
47 | .describe("Search term or pattern to match footprint names"),
48 | library: z.string().optional()
49 | .describe("Optional specific library to search in"),
50 | limit: z.number().optional().default(50)
51 | .describe("Maximum number of results to return")
52 | },
53 | async (args: { search_term: string; library?: string; limit?: number }) => {
54 | const result = await callKicadScript("search_footprints", args);
55 | if (result.success && result.footprints) {
56 | const footprintList = result.footprints.map((fp: any) =>
57 | `${fp.library}:${fp.name}${fp.description ? ' - ' + fp.description : ''}`
58 | ).join('\n');
59 | return {
60 | content: [
61 | {
62 | type: "text",
63 | text: `Found ${result.footprints.length} matching footprints:\n${footprintList}`
64 | }
65 | ]
66 | };
67 | }
68 | return {
69 | content: [
70 | {
71 | type: "text",
72 | text: `Failed to search footprints: ${result.message || 'Unknown error'}`
73 | }
74 | ]
75 | };
76 | }
77 | );
78 |
79 | // List footprints in a specific library
80 | server.tool(
81 | "list_library_footprints",
82 | "List all footprints in a specific KiCAD library",
83 | {
84 | library_name: z.string()
85 | .describe("Name of the library to list footprints from"),
86 | filter: z.string().optional()
87 | .describe("Optional filter pattern for footprint names"),
88 | limit: z.number().optional().default(100)
89 | .describe("Maximum number of footprints to list")
90 | },
91 | async (args: { library_name: string; filter?: string; limit?: number }) => {
92 | const result = await callKicadScript("list_library_footprints", args);
93 | if (result.success && result.footprints) {
94 | const footprintList = result.footprints.map((fp: string) => ` - ${fp}`).join('\n');
95 | return {
96 | content: [
97 | {
98 | type: "text",
99 | text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}`
100 | }
101 | ]
102 | };
103 | }
104 | return {
105 | content: [
106 | {
107 | type: "text",
108 | text: `Failed to list footprints in library ${args.library_name}: ${result.message || 'Unknown error'}`
109 | }
110 | ]
111 | };
112 | }
113 | );
114 |
115 | // Get detailed information about a specific footprint
116 | server.tool(
117 | "get_footprint_info",
118 | "Get detailed information about a specific footprint",
119 | {
120 | library_name: z.string()
121 | .describe("Name of the library containing the footprint"),
122 | footprint_name: z.string()
123 | .describe("Name of the footprint to get information about")
124 | },
125 | async (args: { library_name: string; footprint_name: string }) => {
126 | const result = await callKicadScript("get_footprint_info", args);
127 | if (result.success && result.info) {
128 | const info = result.info;
129 | const details = [
130 | `Footprint: ${info.name}`,
131 | `Library: ${info.library}`,
132 | info.description ? `Description: ${info.description}` : '',
133 | info.keywords ? `Keywords: ${info.keywords}` : '',
134 | info.pads ? `Number of pads: ${info.pads}` : '',
135 | info.layers ? `Layers used: ${info.layers.join(', ')}` : '',
136 | info.courtyard ? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm` : '',
137 | info.attributes ? `Attributes: ${JSON.stringify(info.attributes)}` : ''
138 | ].filter(line => line).join('\n');
139 |
140 | return {
141 | content: [
142 | {
143 | type: "text",
144 | text: details
145 | }
146 | ]
147 | };
148 | }
149 | return {
150 | content: [
151 | {
152 | type: "text",
153 | text: `Failed to get footprint info: ${result.message || 'Unknown error'}`
154 | }
155 | ]
156 | };
157 | }
158 | );
159 | }
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI/CD Pipeline
2 |
3 | on:
4 | push:
5 | branches: [ main, develop ]
6 | pull_request:
7 | branches: [ main, develop ]
8 |
9 | jobs:
10 | # TypeScript/Node.js tests
11 | typescript-tests:
12 | name: TypeScript Build & Test
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | matrix:
16 | os: [ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest]
17 | node-version: [18.x, 20.x, 22.x]
18 |
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v4
22 |
23 | - name: Setup Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'npm'
28 |
29 | - name: Install dependencies
30 | run: npm ci
31 |
32 | - name: Run TypeScript compiler
33 | run: npm run build
34 |
35 | - name: Run linter
36 | run: npm run lint || echo "Linter not configured yet"
37 |
38 | - name: Run tests
39 | run: npm test || echo "Tests not configured yet"
40 |
41 | # Python tests
42 | python-tests:
43 | name: Python Tests
44 | runs-on: ${{ matrix.os }}
45 | strategy:
46 | matrix:
47 | os: [ubuntu-24.04, ubuntu-22.04]
48 | python-version: ['3.10', '3.11', '3.12']
49 |
50 | steps:
51 | - name: Checkout code
52 | uses: actions/checkout@v4
53 |
54 | - name: Setup Python ${{ matrix.python-version }}
55 | uses: actions/setup-python@v5
56 | with:
57 | python-version: ${{ matrix.python-version }}
58 | cache: 'pip'
59 |
60 | - name: Install Python dependencies
61 | run: |
62 | python -m pip install --upgrade pip
63 | pip install pytest pytest-cov black mypy pylint
64 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
65 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
66 |
67 | - name: Run Black formatter check
68 | run: black --check python/ || echo "Black not configured yet"
69 |
70 | - name: Run MyPy type checker
71 | run: mypy python/ || echo "MyPy not configured yet"
72 |
73 | - name: Run Pylint
74 | run: pylint python/ || echo "Pylint not configured yet"
75 |
76 | - name: Run pytest
77 | run: pytest python/ --cov=python --cov-report=xml || echo "Tests not configured yet"
78 |
79 | - name: Upload coverage to Codecov
80 | uses: codecov/codecov-action@v4
81 | with:
82 | file: ./coverage.xml
83 | flags: python
84 | name: python-${{ matrix.python-version }}
85 | if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04'
86 |
87 | # Integration tests (requires KiCAD)
88 | integration-tests:
89 | name: Integration Tests (Linux + KiCAD)
90 | runs-on: ubuntu-24.04
91 |
92 | steps:
93 | - name: Checkout code
94 | uses: actions/checkout@v4
95 |
96 | - name: Setup Node.js
97 | uses: actions/setup-node@v4
98 | with:
99 | node-version: '20.x'
100 | cache: 'npm'
101 |
102 | - name: Setup Python
103 | uses: actions/setup-python@v5
104 | with:
105 | python-version: '3.12'
106 | cache: 'pip'
107 |
108 | - name: Add KiCAD PPA and Install KiCAD 9.0
109 | run: |
110 | sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
111 | sudo apt-get update
112 | sudo apt-get install -y kicad kicad-libraries
113 |
114 | - name: Verify KiCAD installation
115 | run: |
116 | kicad-cli version || echo "kicad-cli not found"
117 | python3 -c "import pcbnew; print(f'pcbnew version: {pcbnew.GetBuildVersion()}')" || echo "pcbnew module not found"
118 |
119 | - name: Install dependencies
120 | run: |
121 | npm ci
122 | pip install -r requirements.txt
123 |
124 | - name: Build TypeScript
125 | run: npm run build
126 |
127 | - name: Run integration tests
128 | run: |
129 | echo "Integration tests not yet configured"
130 | # pytest tests/integration/
131 |
132 | # Docker build test
133 | docker-build:
134 | name: Docker Build Test
135 | runs-on: ubuntu-latest
136 |
137 | steps:
138 | - name: Checkout code
139 | uses: actions/checkout@v4
140 |
141 | - name: Set up Docker Buildx
142 | uses: docker/setup-buildx-action@v3
143 |
144 | - name: Build Docker image
145 | run: |
146 | echo "Docker build not yet configured"
147 | # docker build -t kicad-mcp-server:test .
148 |
149 | # Code quality checks
150 | code-quality:
151 | name: Code Quality
152 | runs-on: ubuntu-latest
153 |
154 | steps:
155 | - name: Checkout code
156 | uses: actions/checkout@v4
157 |
158 | - name: Setup Node.js
159 | uses: actions/setup-node@v4
160 | with:
161 | node-version: '20.x'
162 |
163 | - name: Install dependencies
164 | run: npm ci
165 |
166 | - name: Run ESLint
167 | run: npx eslint src/ || echo "ESLint not configured yet"
168 |
169 | - name: Run Prettier check
170 | run: npx prettier --check "src/**/*.ts" || echo "Prettier not configured yet"
171 |
172 | - name: Check for security vulnerabilities
173 | run: npm audit --audit-level=moderate || echo "No critical vulnerabilities"
174 |
175 | # Documentation check
176 | docs-check:
177 | name: Documentation Check
178 | runs-on: ubuntu-latest
179 |
180 | steps:
181 | - name: Checkout code
182 | uses: actions/checkout@v4
183 |
184 | - name: Check README exists
185 | run: test -f README.md
186 |
187 | - name: Check for broken links in docs
188 | run: |
189 | sudo apt-get install -y linkchecker || true
190 | # linkchecker docs/ || echo "Link checker not configured"
191 |
192 | - name: Validate JSON files
193 | run: |
194 | find . -name "*.json" -not -path "./node_modules/*" -not -path "./dist/*" | xargs -I {} sh -c 'python3 -m json.tool {} > /dev/null && echo "✓ {}" || echo "✗ {}"'
195 |
```
--------------------------------------------------------------------------------
/docs/KNOWN_ISSUES.md:
--------------------------------------------------------------------------------
```markdown
1 | # Known Issues & Workarounds
2 |
3 | **Last Updated:** 2025-12-02
4 | **Version:** 2.1.0-alpha
5 |
6 | This document tracks known issues and provides workarounds where available.
7 |
8 | ---
9 |
10 | ## Current Issues
11 |
12 | ### 1. `get_board_info` KiCAD 9.0 API Issue
13 |
14 | **Status:** KNOWN - Non-critical
15 |
16 | **Symptoms:**
17 | ```
18 | AttributeError: 'BOARD' object has no attribute 'LT_USER'
19 | ```
20 |
21 | **Root Cause:** KiCAD 9.0 changed layer enumeration constants
22 |
23 | **Workaround:** Use `get_project_info` instead for basic project details
24 |
25 | **Impact:** Low - informational command only
26 |
27 | ---
28 |
29 | ### 2. Zone Filling via SWIG Causes Segfault
30 |
31 | **Status:** KNOWN - Workaround available
32 |
33 | **Symptoms:**
34 | - Copper pours created but not filled automatically when using SWIG backend
35 | - Calling `ZONE_FILLER` via SWIG causes segfault
36 |
37 | **Workaround Options:**
38 | 1. Use IPC backend (zones fill correctly via IPC)
39 | 2. Open the board in KiCAD UI - zones fill automatically when opened
40 |
41 | **Impact:** Medium - affects copper pour visualization until opened in KiCAD
42 |
43 | ---
44 |
45 | ### 3. UI Manual Reload Required (SWIG Backend)
46 |
47 | **Status:** BY DESIGN - Fixed by IPC
48 |
49 | **Symptoms:**
50 | - MCP makes changes via SWIG backend
51 | - KiCAD doesn't show changes until file is reloaded
52 |
53 | **Current Workflow:**
54 | ```
55 | 1. MCP makes change via SWIG
56 | 2. KiCAD shows: "File has been modified. Reload? [Yes] [No]"
57 | 3. User clicks "Yes"
58 | 4. Changes appear in UI
59 | ```
60 |
61 | **Why:** SWIG-based backend requires file I/O, can't push changes to running UI
62 |
63 | **Fix:** Use IPC backend for real-time updates (requires KiCAD to be running with IPC enabled)
64 |
65 | **Workaround:** Click reload prompt or use File > Revert
66 |
67 | ---
68 |
69 | ### 4. IPC Backend Experimental
70 |
71 | **Status:** UNDER DEVELOPMENT
72 |
73 | **Description:**
74 | The IPC backend is currently being implemented and tested. Some commands may not work as expected in all scenarios.
75 |
76 | **Known IPC Limitations:**
77 | - KiCAD must be running with IPC enabled
78 | - Some commands fall back to SWIG (e.g., delete_trace)
79 | - Footprint loading uses hybrid approach (SWIG for library, IPC for placement)
80 | - Error handling may not be comprehensive in all cases
81 |
82 | **Workaround:** If IPC fails, the server automatically falls back to SWIG backend
83 |
84 | ---
85 |
86 | ### 5. Schematic Support Limited
87 |
88 | **Status:** KNOWN - Partial support
89 |
90 | **Description:**
91 | Schematic operations use the kicad-skip library which has some limitations with KiCAD 9.0 file format changes.
92 |
93 | **Affected Commands:**
94 | - `create_schematic`
95 | - `add_schematic_component`
96 | - `add_schematic_wire`
97 |
98 | **Workaround:** Manual schematic creation may be more reliable for complex designs
99 |
100 | ---
101 |
102 | ## Recently Fixed
103 |
104 | ### Component Library Integration (Fixed 2025-11-01)
105 |
106 | **Was:** Could not find footprint libraries
107 | **Now:** Auto-discovers 153 KiCAD footprint libraries, search and list working
108 |
109 | ### Routing Operations KiCAD 9.0 (Fixed 2025-11-01)
110 |
111 | **Was:** Multiple API compatibility issues with KiCAD 9.0
112 | **Now:** All routing commands tested and working:
113 | - `netinfo.FindNet()` -> `netinfo.NetsByName()[name]`
114 | - `zone.SetPriority()` -> `zone.SetAssignedPriority()`
115 | - `ZONE_FILL_MODE_POLYGON` -> `ZONE_FILL_MODE_POLYGONS`
116 |
117 | ### KiCAD Process Detection (Fixed 2025-10-26)
118 |
119 | **Was:** `check_kicad_ui` detected MCP server's own processes
120 | **Now:** Properly filters to only detect actual KiCAD binaries
121 |
122 | ### set_board_size KiCAD 9.0 (Fixed 2025-10-26)
123 |
124 | **Was:** Failed with `BOX2I_SetSize` type error
125 | **Now:** Works with KiCAD 9.0 API
126 |
127 | ### add_board_text KiCAD 9.0 (Fixed 2025-10-26)
128 |
129 | **Was:** Failed with `EDA_ANGLE` type error
130 | **Now:** Works with KiCAD 9.0 API
131 |
132 | ### Schematic Parameter Mismatch (Fixed 2025-12-02)
133 |
134 | **Was:** `create_schematic` failed due to parameter name differences between TypeScript and Python
135 | **Now:** Accepts multiple parameter naming conventions (`name`, `projectName`, `title`, `filename`)
136 |
137 | ---
138 |
139 | ## Reporting New Issues
140 |
141 | If you encounter an issue not listed here:
142 |
143 | 1. **Check MCP logs:** `~/.kicad-mcp/logs/kicad_interface.log`
144 | 2. **Check KiCAD version:** `python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"` (must be 9.0+)
145 | 3. **Try the operation in KiCAD directly** - is it a KiCAD issue?
146 | 4. **Open GitHub issue** with:
147 | - Error message
148 | - Log excerpt
149 | - Steps to reproduce
150 | - KiCAD version
151 | - OS and version
152 |
153 | ---
154 |
155 | ## Priority Matrix
156 |
157 | | Issue | Priority | Impact | Status |
158 | |-------|----------|--------|--------|
159 | | IPC Backend Testing | High | Medium | In Progress |
160 | | get_board_info Fix | Low | Low | Known |
161 | | Zone Filling (SWIG) | Medium | Medium | Workaround Available |
162 | | Schematic Support | Medium | Medium | Partial |
163 |
164 | ---
165 |
166 | ## General Workarounds
167 |
168 | ### Server Won't Start
169 | ```bash
170 | # Check Python can import pcbnew
171 | python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
172 |
173 | # Check paths
174 | python3 python/utils/platform_helper.py
175 | ```
176 |
177 | ### Commands Fail After Server Restart
178 | ```
179 | # Board reference is lost on restart
180 | # Always run open_project after server restart
181 | ```
182 |
183 | ### KiCAD UI Doesn't Show Changes (SWIG Mode)
184 | ```
185 | # File > Revert (or click reload prompt)
186 | # Or: Close and reopen file in KiCAD
187 | # Or: Use IPC backend for automatic updates
188 | ```
189 |
190 | ### IPC Not Connecting
191 | ```
192 | # Ensure KiCAD is running
193 | # Enable IPC: Preferences > Plugins > Enable IPC API Server
194 | # Have a board open in PCB editor
195 | # Check socket exists: ls /tmp/kicad/api.sock
196 | ```
197 |
198 | ---
199 |
200 | **Need Help?**
201 | - Check [IPC_BACKEND_STATUS.md](IPC_BACKEND_STATUS.md) for IPC details
202 | - Check [REALTIME_WORKFLOW.md](REALTIME_WORKFLOW.md) for workflow tips
203 | - Check logs: `~/.kicad-mcp/logs/kicad_interface.log`
204 | - Open an issue on GitHub
205 |
```
--------------------------------------------------------------------------------
/CHANGELOG_2025-11-05.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog - November 5, 2025
2 |
3 | ## Windows Support Package
4 |
5 | **Focus:** Comprehensive Windows support improvements and platform documentation
6 |
7 | **Status:** Complete
8 |
9 | ---
10 |
11 | ## New Features
12 |
13 | ### Windows Automated Setup
14 | - **setup-windows.ps1** - PowerShell script for one-command setup
15 | - Auto-detects KiCAD installation and version
16 | - Validates all prerequisites (Node.js, Python, pcbnew)
17 | - Installs dependencies automatically
18 | - Builds TypeScript project
19 | - Generates MCP configuration
20 | - Runs comprehensive diagnostic tests
21 | - Provides colored output with clear success/failure indicators
22 | - Generates detailed error reports with solutions
23 |
24 | ### Enhanced Error Diagnostics
25 | - **Python Interface** (kicad_interface.py)
26 | - Windows-specific environment diagnostics on startup
27 | - Auto-detects KiCAD installations in standard Windows locations
28 | - Lists found KiCAD versions and Python paths
29 | - Platform-specific error messages with actionable troubleshooting steps
30 | - Detailed logging of PYTHONPATH and system PATH
31 |
32 | - **Server Startup Validation** (src/server.ts)
33 | - New `validatePrerequisites()` method
34 | - Tests pcbnew import before starting Python process
35 | - Validates Python executable exists
36 | - Checks project build status
37 | - Catches configuration errors early
38 | - Writes errors to both log file and stderr (visible in Claude Desktop)
39 | - Platform-specific troubleshooting hints in error messages
40 |
41 | ### Documentation
42 |
43 | - **WINDOWS_TROUBLESHOOTING.md** - Comprehensive Windows guide
44 | - 8 common issues with step-by-step solutions
45 | - Configuration examples for Claude Desktop and Cline
46 | - Manual testing procedures
47 | - Advanced diagnostics section
48 | - Success checklist
49 | - Known limitations
50 |
51 | - **PLATFORM_GUIDE.md** - Linux vs Windows comparison
52 | - Detailed comparison table
53 | - Installation differences explained
54 | - Path handling conventions
55 | - Python environment differences
56 | - Testing and debugging workflows
57 | - Platform-specific best practices
58 | - Migration guidance
59 |
60 | - **README.md** - Updated Windows section
61 | - Automated setup prominently featured
62 | - Honest status: "Supported (community tested)"
63 | - Links to troubleshooting resources
64 | - Both automated and manual setup paths
65 | - Clear verification steps
66 |
67 | ### Documentation Cleanup
68 | - Removed all emojis from documentation (per project guidelines)
69 | - Updated STATUS_SUMMARY.md Windows status from "UNTESTED" to "SUPPORTED"
70 | - Consistent formatting across all documentation files
71 |
72 | ---
73 |
74 | ## Bug Fixes
75 |
76 | ### Startup Reliability
77 | - Server no longer fails silently on Windows
78 | - Prerequisite validation catches common configuration errors before they cause crashes
79 | - Clear error messages guide users to solutions
80 |
81 | ### Path Handling
82 | - Improved path handling for Windows (backslash and forward slash support)
83 | - Better documentation of path escaping in JSON configuration files
84 |
85 | ---
86 |
87 | ## Improvements
88 |
89 | ### GitHub Issue Support
90 | - Responded to Issue #5 with initial troubleshooting steps
91 | - Posted comprehensive update announcing all Windows improvements
92 | - Provided clear next steps for affected users
93 |
94 | ### Testing
95 | - TypeScript build verified with new validation code
96 | - All changes compile without errors or warnings
97 |
98 | ---
99 |
100 | ## Files Changed
101 |
102 | ### New Files
103 | - `setup-windows.ps1` - Automated Windows setup script (500+ lines)
104 | - `docs/WINDOWS_TROUBLESHOOTING.md` - Windows troubleshooting guide
105 | - `docs/PLATFORM_GUIDE.md` - Linux vs Windows comparison
106 | - `CHANGELOG_2025-11-05.md` - This changelog
107 |
108 | ### Modified Files
109 | - `README.md` - Updated Windows installation section
110 | - `docs/STATUS_SUMMARY.md` - Updated Windows status and removed emojis
111 | - `docs/ROADMAP.md` - Removed emojis
112 | - `python/kicad_interface.py` - Added Windows diagnostics
113 | - `src/server.ts` - Added startup validation
114 |
115 | ---
116 |
117 | ## Breaking Changes
118 |
119 | None. All changes are backward compatible.
120 |
121 | ---
122 |
123 | ## Known Issues
124 |
125 | ### Not Fixed
126 | - JLCPCB integration still in planning phase (not implemented)
127 | - macOS remains untested
128 | - `get_board_info` layer constants issue (low priority)
129 | - Zone filling disabled due to SWIG API segfault
130 |
131 | ---
132 |
133 | ## Migration Notes
134 |
135 | ### Upgrading from Previous Version
136 |
137 | **For Windows users:**
138 | 1. Pull latest changes
139 | 2. Run `setup-windows.ps1`
140 | 3. Update your MCP client configuration if prompted
141 | 4. Restart your MCP client
142 |
143 | **For Linux users:**
144 | 1. Pull latest changes
145 | 2. Run `npm install` and `npm run build`
146 | 3. No configuration changes needed
147 |
148 | ---
149 |
150 | ## Testing Performed
151 |
152 | - PowerShell script tested on Windows 10 (simulated)
153 | - TypeScript compilation verified
154 | - Documentation reviewed for consistency
155 | - Path handling verified in configuration examples
156 | - Startup validation logic tested
157 |
158 | ---
159 |
160 | ## Next Steps
161 |
162 | ### Week 2 Completion
163 | - Consider JLCPCB integration implementation
164 | - Create example projects (LED blinker)
165 | - Windows community testing and feedback
166 |
167 | ### Week 3 Planning
168 | - IPC Backend implementation for real-time UI updates
169 | - Fix remaining minor issues
170 | - macOS testing and support
171 |
172 | ---
173 |
174 | ## Contributors
175 |
176 | - mixelpixx (Chris) - Windows support implementation
177 | - spplecxer - Issue #5 report (Windows crash)
178 |
179 | ---
180 |
181 | ## References
182 |
183 | - Issue #5: https://github.com/mixelpixx/KiCAD-MCP-Server/issues/5
184 | - Windows Installation Guide: [README.md](README.md#windows-1011)
185 | - Troubleshooting: [docs/WINDOWS_TROUBLESHOOTING.md](docs/WINDOWS_TROUBLESHOOTING.md)
186 | - Platform Comparison: [docs/PLATFORM_GUIDE.md](docs/PLATFORM_GUIDE.md)
187 |
188 | ---
189 |
190 | **Summary:** This release significantly improves Windows support with automated setup, comprehensive diagnostics, and detailed documentation. Windows users now have a smooth onboarding experience comparable to Linux users.
191 |
```
--------------------------------------------------------------------------------
/python/kicad_api/factory.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Backend factory for creating appropriate KiCAD API backend
3 |
4 | Auto-detects available backends and provides fallback mechanism.
5 | """
6 | import os
7 | import logging
8 | from typing import Optional
9 | from pathlib import Path
10 |
11 | from kicad_api.base import KiCADBackend, APINotAvailableError
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
17 | """
18 | Create appropriate KiCAD backend
19 |
20 | Args:
21 | backend_type: Backend to use:
22 | - 'ipc': Use IPC API (recommended)
23 | - 'swig': Use legacy SWIG bindings
24 | - None or 'auto': Auto-detect (try IPC first, fall back to SWIG)
25 |
26 | Returns:
27 | KiCADBackend instance
28 |
29 | Raises:
30 | APINotAvailableError: If no backend is available
31 |
32 | Environment Variables:
33 | KICAD_BACKEND: Override backend selection ('ipc', 'swig', or 'auto')
34 | """
35 | # Check environment variable override
36 | if backend_type is None:
37 | backend_type = os.environ.get('KICAD_BACKEND', 'auto').lower()
38 |
39 | logger.info(f"Requested backend: {backend_type}")
40 |
41 | # Try specific backend if requested
42 | if backend_type == 'ipc':
43 | return _create_ipc_backend()
44 | elif backend_type == 'swig':
45 | return _create_swig_backend()
46 | elif backend_type == 'auto':
47 | return _auto_detect_backend()
48 | else:
49 | raise ValueError(f"Unknown backend type: {backend_type}")
50 |
51 |
52 | def _create_ipc_backend() -> KiCADBackend:
53 | """
54 | Create IPC backend
55 |
56 | Returns:
57 | IPCBackend instance
58 |
59 | Raises:
60 | APINotAvailableError: If kicad-python not available
61 | """
62 | try:
63 | from kicad_api.ipc_backend import IPCBackend
64 | logger.info("Creating IPC backend")
65 | return IPCBackend()
66 | except ImportError as e:
67 | logger.error(f"IPC backend not available: {e}")
68 | raise APINotAvailableError(
69 | "IPC backend requires 'kicad-python' package. "
70 | "Install with: pip install kicad-python"
71 | ) from e
72 |
73 |
74 | def _create_swig_backend() -> KiCADBackend:
75 | """
76 | Create SWIG backend
77 |
78 | Returns:
79 | SWIGBackend instance
80 |
81 | Raises:
82 | APINotAvailableError: If pcbnew not available
83 | """
84 | try:
85 | from kicad_api.swig_backend import SWIGBackend
86 | logger.info("Creating SWIG backend")
87 | logger.warning(
88 | "SWIG backend is DEPRECATED and will be removed in KiCAD 10.0. "
89 | "Please migrate to IPC backend."
90 | )
91 | return SWIGBackend()
92 | except ImportError as e:
93 | logger.error(f"SWIG backend not available: {e}")
94 | raise APINotAvailableError(
95 | "SWIG backend requires 'pcbnew' module. "
96 | "Ensure KiCAD Python module is in PYTHONPATH."
97 | ) from e
98 |
99 |
100 | def _auto_detect_backend() -> KiCADBackend:
101 | """
102 | Auto-detect best available backend
103 |
104 | Priority:
105 | 1. IPC API (if kicad-python available and KiCAD running)
106 | 2. SWIG API (if pcbnew available)
107 |
108 | Returns:
109 | Best available KiCADBackend
110 |
111 | Raises:
112 | APINotAvailableError: If no backend available
113 | """
114 | logger.info("Auto-detecting available KiCAD backend...")
115 |
116 | # Try IPC first (preferred)
117 | try:
118 | backend = _create_ipc_backend()
119 | # Test connection
120 | if backend.connect():
121 | logger.info("✓ IPC backend available and connected")
122 | return backend
123 | else:
124 | logger.warning("IPC backend available but connection failed")
125 | except (ImportError, APINotAvailableError) as e:
126 | logger.debug(f"IPC backend not available: {e}")
127 |
128 | # Fall back to SWIG
129 | try:
130 | backend = _create_swig_backend()
131 | logger.warning(
132 | "Using deprecated SWIG backend. "
133 | "For best results, use IPC API with KiCAD running."
134 | )
135 | return backend
136 | except (ImportError, APINotAvailableError) as e:
137 | logger.error(f"SWIG backend not available: {e}")
138 |
139 | # No backend available
140 | raise APINotAvailableError(
141 | "No KiCAD backend available. Please install either:\n"
142 | " - kicad-python (recommended): pip install kicad-python\n"
143 | " - Ensure KiCAD Python module (pcbnew) is in PYTHONPATH"
144 | )
145 |
146 |
147 | def get_available_backends() -> dict:
148 | """
149 | Check which backends are available
150 |
151 | Returns:
152 | Dictionary with backend availability:
153 | {
154 | 'ipc': {'available': bool, 'version': str or None},
155 | 'swig': {'available': bool, 'version': str or None}
156 | }
157 | """
158 | results = {}
159 |
160 | # Check IPC (kicad-python uses 'kipy' module name)
161 | try:
162 | import kipy
163 | results['ipc'] = {
164 | 'available': True,
165 | 'version': getattr(kipy, '__version__', 'unknown')
166 | }
167 | except ImportError:
168 | results['ipc'] = {'available': False, 'version': None}
169 |
170 | # Check SWIG
171 | try:
172 | import pcbnew
173 | results['swig'] = {
174 | 'available': True,
175 | 'version': pcbnew.GetBuildVersion()
176 | }
177 | except ImportError:
178 | results['swig'] = {'available': False, 'version': None}
179 |
180 | return results
181 |
182 |
183 | if __name__ == "__main__":
184 | # Quick diagnostic
185 | import json
186 | print("KiCAD Backend Availability:")
187 | print(json.dumps(get_available_backends(), indent=2))
188 |
189 | print("\nAttempting to create backend...")
190 | try:
191 | backend = create_backend()
192 | print(f"✓ Created backend: {type(backend).__name__}")
193 | if backend.connect():
194 | print(f"✓ Connected to KiCAD: {backend.get_version()}")
195 | else:
196 | print("✗ Failed to connect to KiCAD")
197 | except Exception as e:
198 | print(f"✗ Error: {e}")
199 |
```
--------------------------------------------------------------------------------
/tests/test_platform_helper.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for platform_helper utility
3 |
4 | These are unit tests that work on all platforms.
5 | """
6 | import pytest
7 | import platform
8 | from pathlib import Path
9 | import sys
10 | import os
11 |
12 | # Add parent directory to path to import utils
13 | sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
14 |
15 | from utils.platform_helper import PlatformHelper, detect_platform
16 |
17 |
18 | class TestPlatformDetection:
19 | """Test platform detection functions"""
20 |
21 | def test_exactly_one_platform_detected(self):
22 | """Ensure exactly one platform is detected"""
23 | platforms = [
24 | PlatformHelper.is_windows(),
25 | PlatformHelper.is_linux(),
26 | PlatformHelper.is_macos(),
27 | ]
28 | assert sum(platforms) == 1, "Exactly one platform should be detected"
29 |
30 | def test_platform_name_is_valid(self):
31 | """Test platform name is human-readable"""
32 | name = PlatformHelper.get_platform_name()
33 | assert name in ["Windows", "Linux", "macOS"], f"Unknown platform: {name}"
34 |
35 | def test_platform_name_matches_detection(self):
36 | """Ensure platform name matches detection functions"""
37 | name = PlatformHelper.get_platform_name()
38 | if name == "Windows":
39 | assert PlatformHelper.is_windows()
40 | elif name == "Linux":
41 | assert PlatformHelper.is_linux()
42 | elif name == "macOS":
43 | assert PlatformHelper.is_macos()
44 |
45 |
46 | class TestPathGeneration:
47 | """Test path generation functions"""
48 |
49 | def test_config_dir_exists_after_ensure(self):
50 | """Test that config directory is created"""
51 | PlatformHelper.ensure_directories()
52 | config_dir = PlatformHelper.get_config_dir()
53 | assert config_dir.exists(), f"Config dir should exist: {config_dir}"
54 | assert config_dir.is_dir(), f"Config dir should be a directory: {config_dir}"
55 |
56 | def test_log_dir_exists_after_ensure(self):
57 | """Test that log directory is created"""
58 | PlatformHelper.ensure_directories()
59 | log_dir = PlatformHelper.get_log_dir()
60 | assert log_dir.exists(), f"Log dir should exist: {log_dir}"
61 | assert log_dir.is_dir(), f"Log dir should be a directory: {log_dir}"
62 |
63 | def test_cache_dir_exists_after_ensure(self):
64 | """Test that cache directory is created"""
65 | PlatformHelper.ensure_directories()
66 | cache_dir = PlatformHelper.get_cache_dir()
67 | assert cache_dir.exists(), f"Cache dir should exist: {cache_dir}"
68 | assert cache_dir.is_dir(), f"Cache dir should be a directory: {cache_dir}"
69 |
70 | def test_config_dir_is_platform_appropriate(self):
71 | """Test that config directory follows platform conventions"""
72 | config_dir = PlatformHelper.get_config_dir()
73 |
74 | if PlatformHelper.is_linux():
75 | # Should be ~/.config/kicad-mcp or $XDG_CONFIG_HOME/kicad-mcp
76 | if "XDG_CONFIG_HOME" in os.environ:
77 | expected = Path(os.environ["XDG_CONFIG_HOME"]) / "kicad-mcp"
78 | else:
79 | expected = Path.home() / ".config" / "kicad-mcp"
80 | assert config_dir == expected
81 |
82 | elif PlatformHelper.is_windows():
83 | # Should be %USERPROFILE%\.kicad-mcp
84 | expected = Path.home() / ".kicad-mcp"
85 | assert config_dir == expected
86 |
87 | elif PlatformHelper.is_macos():
88 | # Should be ~/Library/Application Support/kicad-mcp
89 | expected = Path.home() / "Library" / "Application Support" / "kicad-mcp"
90 | assert config_dir == expected
91 |
92 | def test_python_executable_is_valid(self):
93 | """Test that Python executable path is valid"""
94 | exe = PlatformHelper.get_python_executable()
95 | assert exe.exists(), f"Python executable should exist: {exe}"
96 | assert str(exe) == sys.executable
97 |
98 | def test_kicad_library_search_paths_returns_list(self):
99 | """Test that library search paths returns a list"""
100 | paths = PlatformHelper.get_kicad_library_search_paths()
101 | assert isinstance(paths, list)
102 | assert len(paths) > 0
103 | # All paths should be strings (glob patterns)
104 | assert all(isinstance(p, str) for p in paths)
105 |
106 |
107 | class TestDetectPlatform:
108 | """Test the detect_platform convenience function"""
109 |
110 | def test_detect_platform_returns_dict(self):
111 | """Test that detect_platform returns a dictionary"""
112 | info = detect_platform()
113 | assert isinstance(info, dict)
114 |
115 | def test_detect_platform_has_required_keys(self):
116 | """Test that detect_platform includes all required keys"""
117 | info = detect_platform()
118 | required_keys = [
119 | "system",
120 | "platform",
121 | "is_windows",
122 | "is_linux",
123 | "is_macos",
124 | "python_version",
125 | "python_executable",
126 | "config_dir",
127 | "log_dir",
128 | "cache_dir",
129 | "kicad_python_paths",
130 | ]
131 | for key in required_keys:
132 | assert key in info, f"Missing key: {key}"
133 |
134 | def test_detect_platform_python_version_format(self):
135 | """Test that Python version is in correct format"""
136 | info = detect_platform()
137 | version = info["python_version"]
138 | # Should be like "3.12.3"
139 | parts = version.split(".")
140 | assert len(parts) == 3
141 | assert all(p.isdigit() for p in parts)
142 |
143 |
144 | @pytest.mark.integration
145 | class TestKiCADPathDetection:
146 | """Tests that require KiCAD to be installed"""
147 |
148 | def test_kicad_python_paths_exist(self):
149 | """Test that at least one KiCAD Python path exists (if KiCAD is installed)"""
150 | paths = PlatformHelper.get_kicad_python_paths()
151 | # This test only makes sense if KiCAD is installed
152 | # In CI, KiCAD should be installed
153 | if paths:
154 | assert all(p.exists() for p in paths), "All returned paths should exist"
155 |
156 | def test_can_import_pcbnew_after_adding_paths(self):
157 | """Test that pcbnew can be imported after adding KiCAD paths"""
158 | PlatformHelper.add_kicad_to_python_path()
159 | try:
160 | import pcbnew
161 | # If we get here, pcbnew is available
162 | assert pcbnew is not None
163 | version = pcbnew.GetBuildVersion()
164 | assert version is not None
165 | print(f"Found KiCAD version: {version}")
166 | except ImportError:
167 | pytest.skip("KiCAD pcbnew module not available (KiCAD not installed)")
168 |
169 |
170 | if __name__ == "__main__":
171 | # Run tests with pytest
172 | pytest.main([__file__, "-v"])
173 |
```
--------------------------------------------------------------------------------
/python/commands/library_schematic.py:
--------------------------------------------------------------------------------
```python
1 | from skip import Schematic
2 | # Symbol class might not be directly importable in the current version
3 | import os
4 | import glob
5 |
6 | class LibraryManager:
7 | """Manage symbol libraries"""
8 |
9 | @staticmethod
10 | def list_available_libraries(search_paths=None):
11 | """List all available symbol libraries"""
12 | if search_paths is None:
13 | # Default library paths based on common KiCAD installations
14 | # This would need to be configured for the specific environment
15 | search_paths = [
16 | "C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym", # Windows path pattern
17 | "/usr/share/kicad/symbols/*.kicad_sym", # Linux path pattern
18 | "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", # macOS path pattern
19 | os.path.expanduser("~/Documents/KiCad/*/symbols/*.kicad_sym") # User libraries pattern
20 | ]
21 |
22 | libraries = []
23 | for path_pattern in search_paths:
24 | try:
25 | # Use glob to find all matching files
26 | matching_libs = glob.glob(path_pattern, recursive=True)
27 | libraries.extend(matching_libs)
28 | except Exception as e:
29 | print(f"Error searching for libraries at {path_pattern}: {e}")
30 |
31 | # Extract library names from paths
32 | library_names = [os.path.splitext(os.path.basename(lib))[0] for lib in libraries]
33 | print(f"Found {len(library_names)} libraries: {', '.join(library_names[:10])}{'...' if len(library_names) > 10 else ''}")
34 |
35 | # Return both full paths and library names
36 | return {"paths": libraries, "names": library_names}
37 |
38 | @staticmethod
39 | def list_library_symbols(library_path):
40 | """List all symbols in a library"""
41 | try:
42 | # kicad-skip doesn't provide a direct way to simply list symbols in a library
43 | # without loading each one. We might need to implement this using KiCAD's Python API
44 | # directly, or by using a different approach.
45 | # For now, this is a placeholder implementation.
46 |
47 | # A potential approach would be to load the library file using KiCAD's Python API
48 | # or by parsing the library file format.
49 | # KiCAD symbol libraries are .kicad_sym files which are S-expression format
50 | print(f"Attempted to list symbols in library {library_path}. This requires advanced implementation.")
51 | return []
52 | except Exception as e:
53 | print(f"Error listing symbols in library {library_path}: {e}")
54 | return []
55 |
56 | @staticmethod
57 | def get_symbol_details(library_path, symbol_name):
58 | """Get detailed information about a symbol"""
59 | try:
60 | # Similar to list_library_symbols, this might require a more direct approach
61 | # using KiCAD's Python API or by parsing the symbol library.
62 | print(f"Attempted to get details for symbol {symbol_name} in library {library_path}. This requires advanced implementation.")
63 | return {}
64 | except Exception as e:
65 | print(f"Error getting symbol details for {symbol_name} in {library_path}: {e}")
66 | return {}
67 |
68 | @staticmethod
69 | def search_symbols(query, search_paths=None):
70 | """Search for symbols matching criteria"""
71 | try:
72 | # This would typically involve:
73 | # 1. Getting a list of all libraries using list_available_libraries
74 | # 2. For each library, getting a list of all symbols
75 | # 3. Filtering symbols based on the query
76 |
77 | # For now, this is a placeholder implementation
78 | libraries = LibraryManager.list_available_libraries(search_paths)
79 |
80 | results = []
81 | print(f"Searched for symbols matching '{query}'. This requires advanced implementation.")
82 | return results
83 | except Exception as e:
84 | print(f"Error searching for symbols matching '{query}': {e}")
85 | return []
86 |
87 | @staticmethod
88 | def get_default_symbol_for_component_type(component_type, search_paths=None):
89 | """Get a recommended default symbol for a given component type"""
90 | # This method provides a simplified way to get a symbol for common component types
91 | # It's useful when the user doesn't specify a particular library/symbol
92 |
93 | # Define common mappings from component type to library/symbol
94 | common_mappings = {
95 | "resistor": {"library": "Device", "symbol": "R"},
96 | "capacitor": {"library": "Device", "symbol": "C"},
97 | "inductor": {"library": "Device", "symbol": "L"},
98 | "diode": {"library": "Device", "symbol": "D"},
99 | "led": {"library": "Device", "symbol": "LED"},
100 | "transistor_npn": {"library": "Device", "symbol": "Q_NPN_BCE"},
101 | "transistor_pnp": {"library": "Device", "symbol": "Q_PNP_BCE"},
102 | "opamp": {"library": "Amplifier_Operational", "symbol": "OpAmp_Dual_Generic"},
103 | "microcontroller": {"library": "MCU_Module", "symbol": "Arduino_UNO_R3"},
104 | # Add more common components as needed
105 | }
106 |
107 | # Normalize input to lowercase
108 | component_type_lower = component_type.lower()
109 |
110 | # Try direct match first
111 | if component_type_lower in common_mappings:
112 | return common_mappings[component_type_lower]
113 |
114 | # Try partial matches
115 | for key, value in common_mappings.items():
116 | if component_type_lower in key or key in component_type_lower:
117 | return value
118 |
119 | # Default fallback
120 | return {"library": "Device", "symbol": "R"}
121 |
122 | if __name__ == '__main__':
123 | # Example Usage (for testing)
124 | # List available libraries
125 | libraries = LibraryManager.list_available_libraries()
126 | if libraries["paths"]:
127 | first_lib = libraries["paths"][0]
128 | lib_name = libraries["names"][0]
129 | print(f"Testing with first library: {lib_name} ({first_lib})")
130 |
131 | # List symbols in the first library
132 | symbols = LibraryManager.list_library_symbols(first_lib)
133 | # This will report that it requires advanced implementation
134 |
135 | # Get default symbol for a component type
136 | resistor_sym = LibraryManager.get_default_symbol_for_component_type("resistor")
137 | print(f"Default symbol for resistor: {resistor_sym['library']}/{resistor_sym['symbol']}")
138 |
139 | # Try a partial match
140 | cap_sym = LibraryManager.get_default_symbol_for_component_type("cap")
141 | print(f"Default symbol for 'cap': {cap_sym['library']}/{cap_sym['symbol']}")
142 |
```
--------------------------------------------------------------------------------
/python/commands/board/layers.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Board layer command implementations for KiCAD interface
3 | """
4 |
5 | import pcbnew
6 | import logging
7 | from typing import Dict, Any, Optional
8 |
9 | logger = logging.getLogger('kicad_interface')
10 |
11 | class BoardLayerCommands:
12 | """Handles board layer operations"""
13 |
14 | def __init__(self, board: Optional[pcbnew.BOARD] = None):
15 | """Initialize with optional board instance"""
16 | self.board = board
17 |
18 | def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
19 | """Add a new layer to the PCB"""
20 | try:
21 | if not self.board:
22 | return {
23 | "success": False,
24 | "message": "No board is loaded",
25 | "errorDetails": "Load or create a board first"
26 | }
27 |
28 | name = params.get("name")
29 | layer_type = params.get("type")
30 | position = params.get("position")
31 | number = params.get("number")
32 |
33 | if not name or not layer_type or not position:
34 | return {
35 | "success": False,
36 | "message": "Missing parameters",
37 | "errorDetails": "name, type, and position are required"
38 | }
39 |
40 | # Get layer stack
41 | layer_stack = self.board.GetLayerStack()
42 |
43 | # Determine layer ID based on position and number
44 | layer_id = None
45 | if position == "inner":
46 | if number is None:
47 | return {
48 | "success": False,
49 | "message": "Missing layer number",
50 | "errorDetails": "number is required for inner layers"
51 | }
52 | layer_id = pcbnew.In1_Cu + (number - 1)
53 | elif position == "top":
54 | layer_id = pcbnew.F_Cu
55 | elif position == "bottom":
56 | layer_id = pcbnew.B_Cu
57 |
58 | if layer_id is None:
59 | return {
60 | "success": False,
61 | "message": "Invalid layer position",
62 | "errorDetails": "position must be 'top', 'bottom', or 'inner'"
63 | }
64 |
65 | # Set layer properties
66 | layer_stack.SetLayerName(layer_id, name)
67 | layer_stack.SetLayerType(layer_id, self._get_layer_type(layer_type))
68 |
69 | # Enable the layer
70 | self.board.SetLayerEnabled(layer_id, True)
71 |
72 | return {
73 | "success": True,
74 | "message": f"Added layer: {name}",
75 | "layer": {
76 | "name": name,
77 | "type": layer_type,
78 | "position": position,
79 | "number": number
80 | }
81 | }
82 |
83 | except Exception as e:
84 | logger.error(f"Error adding layer: {str(e)}")
85 | return {
86 | "success": False,
87 | "message": "Failed to add layer",
88 | "errorDetails": str(e)
89 | }
90 |
91 | def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
92 | """Set the active layer for PCB operations"""
93 | try:
94 | if not self.board:
95 | return {
96 | "success": False,
97 | "message": "No board is loaded",
98 | "errorDetails": "Load or create a board first"
99 | }
100 |
101 | layer = params.get("layer")
102 | if not layer:
103 | return {
104 | "success": False,
105 | "message": "No layer specified",
106 | "errorDetails": "layer parameter is required"
107 | }
108 |
109 | # Find layer ID by name
110 | layer_id = self.board.GetLayerID(layer)
111 | if layer_id < 0:
112 | return {
113 | "success": False,
114 | "message": "Layer not found",
115 | "errorDetails": f"Layer '{layer}' does not exist"
116 | }
117 |
118 | # Set active layer
119 | self.board.SetActiveLayer(layer_id)
120 |
121 | return {
122 | "success": True,
123 | "message": f"Set active layer to: {layer}",
124 | "layer": {
125 | "name": layer,
126 | "id": layer_id
127 | }
128 | }
129 |
130 | except Exception as e:
131 | logger.error(f"Error setting active layer: {str(e)}")
132 | return {
133 | "success": False,
134 | "message": "Failed to set active layer",
135 | "errorDetails": str(e)
136 | }
137 |
138 | def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
139 | """Get a list of all layers in the PCB"""
140 | try:
141 | if not self.board:
142 | return {
143 | "success": False,
144 | "message": "No board is loaded",
145 | "errorDetails": "Load or create a board first"
146 | }
147 |
148 | layers = []
149 | for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
150 | if self.board.IsLayerEnabled(layer_id):
151 | layers.append({
152 | "name": self.board.GetLayerName(layer_id),
153 | "type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
154 | "id": layer_id
155 | # Note: isActive removed - GetActiveLayer() doesn't exist in KiCAD 9.0
156 | # Active layer is a UI concept not applicable to headless scripting
157 | })
158 |
159 | return {
160 | "success": True,
161 | "layers": layers
162 | }
163 |
164 | except Exception as e:
165 | logger.error(f"Error getting layer list: {str(e)}")
166 | return {
167 | "success": False,
168 | "message": "Failed to get layer list",
169 | "errorDetails": str(e)
170 | }
171 |
172 | def _get_layer_type(self, type_name: str) -> int:
173 | """Convert layer type name to KiCAD layer type constant"""
174 | type_map = {
175 | "copper": pcbnew.LT_SIGNAL,
176 | "technical": pcbnew.LT_SIGNAL,
177 | "user": pcbnew.LT_SIGNAL, # LT_USER removed in KiCAD 9.0, use LT_SIGNAL instead
178 | "signal": pcbnew.LT_SIGNAL
179 | }
180 | return type_map.get(type_name.lower(), pcbnew.LT_SIGNAL)
181 |
182 | def _get_layer_type_name(self, type_id: int) -> str:
183 | """Convert KiCAD layer type constant to name"""
184 | type_map = {
185 | pcbnew.LT_SIGNAL: "signal",
186 | pcbnew.LT_POWER: "power",
187 | pcbnew.LT_MIXED: "mixed",
188 | pcbnew.LT_JUMPER: "jumper"
189 | }
190 | # Note: LT_USER was removed in KiCAD 9.0
191 | return type_map.get(type_id, "unknown")
192 |
```
--------------------------------------------------------------------------------
/python/kicad_api/swig_backend.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | SWIG Backend (Legacy - DEPRECATED)
3 |
4 | Uses the legacy SWIG-based pcbnew Python bindings.
5 | This backend wraps the existing implementation for backward compatibility.
6 |
7 | WARNING: SWIG bindings are deprecated as of KiCAD 9.0
8 | and will be removed in KiCAD 10.0.
9 | Please migrate to IPC backend.
10 | """
11 | import logging
12 | from pathlib import Path
13 | from typing import Optional, Dict, Any, List
14 |
15 | from kicad_api.base import (
16 | KiCADBackend,
17 | BoardAPI,
18 | ConnectionError,
19 | APINotAvailableError
20 | )
21 |
22 | logger = logging.getLogger(__name__)
23 |
24 |
25 | class SWIGBackend(KiCADBackend):
26 | """
27 | Legacy SWIG-based backend
28 |
29 | Wraps existing commands/project.py, commands/component.py, etc.
30 | for compatibility during migration period.
31 | """
32 |
33 | def __init__(self):
34 | self._connected = False
35 | self._pcbnew = None
36 | logger.warning(
37 | "⚠️ Using DEPRECATED SWIG backend. "
38 | "This will be removed in KiCAD 10.0. "
39 | "Please migrate to IPC API."
40 | )
41 |
42 | def connect(self) -> bool:
43 | """
44 | 'Connect' to SWIG API (just validates pcbnew import)
45 |
46 | Returns:
47 | True if pcbnew module available
48 | """
49 | try:
50 | import pcbnew
51 | self._pcbnew = pcbnew
52 | version = pcbnew.GetBuildVersion()
53 | logger.info(f"✓ Connected to pcbnew (SWIG): {version}")
54 | self._connected = True
55 | return True
56 | except ImportError as e:
57 | logger.error("pcbnew module not found")
58 | raise APINotAvailableError(
59 | "SWIG backend requires pcbnew module. "
60 | "Ensure KiCAD Python module is in PYTHONPATH."
61 | ) from e
62 |
63 | def disconnect(self) -> None:
64 | """Disconnect from SWIG API (no-op)"""
65 | self._connected = False
66 | self._pcbnew = None
67 | logger.info("Disconnected from SWIG backend")
68 |
69 | def is_connected(self) -> bool:
70 | """Check if connected"""
71 | return self._connected
72 |
73 | def get_version(self) -> str:
74 | """Get KiCAD version"""
75 | if not self.is_connected():
76 | raise ConnectionError("Not connected")
77 |
78 | return self._pcbnew.GetBuildVersion()
79 |
80 | # Project Operations
81 | def create_project(self, path: Path, name: str) -> Dict[str, Any]:
82 | """Create project using existing SWIG implementation"""
83 | if not self.is_connected():
84 | raise ConnectionError("Not connected")
85 |
86 | # Import existing implementation
87 | from commands.project import ProjectCommands
88 |
89 | try:
90 | result = ProjectCommands.create_project(str(path), name)
91 | return result
92 | except Exception as e:
93 | logger.error(f"Failed to create project: {e}")
94 | raise
95 |
96 | def open_project(self, path: Path) -> Dict[str, Any]:
97 | """Open project using existing SWIG implementation"""
98 | if not self.is_connected():
99 | raise ConnectionError("Not connected")
100 |
101 | from commands.project import ProjectCommands
102 |
103 | try:
104 | result = ProjectCommands.open_project(str(path))
105 | return result
106 | except Exception as e:
107 | logger.error(f"Failed to open project: {e}")
108 | raise
109 |
110 | def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
111 | """Save project using existing SWIG implementation"""
112 | if not self.is_connected():
113 | raise ConnectionError("Not connected")
114 |
115 | from commands.project import ProjectCommands
116 |
117 | try:
118 | path_str = str(path) if path else None
119 | result = ProjectCommands.save_project(path_str)
120 | return result
121 | except Exception as e:
122 | logger.error(f"Failed to save project: {e}")
123 | raise
124 |
125 | def close_project(self) -> None:
126 | """Close project (SWIG doesn't have explicit close)"""
127 | logger.info("Closing project (SWIG backend)")
128 | # SWIG backend doesn't maintain project state,
129 | # so this is essentially a no-op
130 |
131 | # Board Operations
132 | def get_board(self) -> BoardAPI:
133 | """Get board API"""
134 | if not self.is_connected():
135 | raise ConnectionError("Not connected")
136 |
137 | return SWIGBoardAPI(self._pcbnew)
138 |
139 |
140 | class SWIGBoardAPI(BoardAPI):
141 | """Board API implementation wrapping SWIG/pcbnew"""
142 |
143 | def __init__(self, pcbnew_module):
144 | self.pcbnew = pcbnew_module
145 | self._board = None
146 |
147 | def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
148 | """Set board size using existing implementation"""
149 | from commands.board import BoardCommands
150 |
151 | try:
152 | result = BoardCommands.set_board_size(width, height, unit)
153 | return result.get("success", False)
154 | except Exception as e:
155 | logger.error(f"Failed to set board size: {e}")
156 | return False
157 |
158 | def get_size(self) -> Dict[str, float]:
159 | """Get board size"""
160 | # TODO: Implement using existing SWIG code
161 | raise NotImplementedError("get_size not yet wrapped")
162 |
163 | def add_layer(self, layer_name: str, layer_type: str) -> bool:
164 | """Add layer using existing implementation"""
165 | from commands.board import BoardCommands
166 |
167 | try:
168 | result = BoardCommands.add_layer(layer_name, layer_type)
169 | return result.get("success", False)
170 | except Exception as e:
171 | logger.error(f"Failed to add layer: {e}")
172 | return False
173 |
174 | def list_components(self) -> List[Dict[str, Any]]:
175 | """List components using existing implementation"""
176 | from commands.component import ComponentCommands
177 |
178 | try:
179 | result = ComponentCommands.get_component_list()
180 | if result.get("success"):
181 | return result.get("components", [])
182 | return []
183 | except Exception as e:
184 | logger.error(f"Failed to list components: {e}")
185 | return []
186 |
187 | def place_component(
188 | self,
189 | reference: str,
190 | footprint: str,
191 | x: float,
192 | y: float,
193 | rotation: float = 0,
194 | layer: str = "F.Cu"
195 | ) -> bool:
196 | """Place component using existing implementation"""
197 | from commands.component import ComponentCommands
198 |
199 | try:
200 | result = ComponentCommands.place_component(
201 | component_id=footprint,
202 | position={"x": x, "y": y, "unit": "mm"},
203 | reference=reference,
204 | rotation=rotation,
205 | layer=layer
206 | )
207 | return result.get("success", False)
208 | except Exception as e:
209 | logger.error(f"Failed to place component: {e}")
210 | return False
211 |
212 |
213 | # This backend serves as a wrapper during the migration period.
214 | # Once IPC backend is fully implemented, this can be deprecated.
215 |
```
--------------------------------------------------------------------------------
/python/kicad_api/base.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Abstract base class for KiCAD API backends
3 |
4 | Defines the interface that all KiCAD backends must implement.
5 | """
6 | from abc import ABC, abstractmethod
7 | from pathlib import Path
8 | from typing import Optional, Dict, Any, List
9 | import logging
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class KiCADBackend(ABC):
15 | """Abstract base class for KiCAD API backends"""
16 |
17 | @abstractmethod
18 | def connect(self) -> bool:
19 | """
20 | Connect to KiCAD
21 |
22 | Returns:
23 | True if connection successful, False otherwise
24 | """
25 | pass
26 |
27 | @abstractmethod
28 | def disconnect(self) -> None:
29 | """Disconnect from KiCAD and clean up resources"""
30 | pass
31 |
32 | @abstractmethod
33 | def is_connected(self) -> bool:
34 | """
35 | Check if currently connected to KiCAD
36 |
37 | Returns:
38 | True if connected, False otherwise
39 | """
40 | pass
41 |
42 | @abstractmethod
43 | def get_version(self) -> str:
44 | """
45 | Get KiCAD version
46 |
47 | Returns:
48 | Version string (e.g., "9.0.0")
49 | """
50 | pass
51 |
52 | # Project Operations
53 | @abstractmethod
54 | def create_project(self, path: Path, name: str) -> Dict[str, Any]:
55 | """
56 | Create a new KiCAD project
57 |
58 | Args:
59 | path: Directory path for the project
60 | name: Project name
61 |
62 | Returns:
63 | Dictionary with project info
64 | """
65 | pass
66 |
67 | @abstractmethod
68 | def open_project(self, path: Path) -> Dict[str, Any]:
69 | """
70 | Open an existing KiCAD project
71 |
72 | Args:
73 | path: Path to .kicad_pro file
74 |
75 | Returns:
76 | Dictionary with project info
77 | """
78 | pass
79 |
80 | @abstractmethod
81 | def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
82 | """
83 | Save the current project
84 |
85 | Args:
86 | path: Optional new path to save to
87 |
88 | Returns:
89 | Dictionary with save status
90 | """
91 | pass
92 |
93 | @abstractmethod
94 | def close_project(self) -> None:
95 | """Close the current project"""
96 | pass
97 |
98 | # Board Operations
99 | @abstractmethod
100 | def get_board(self) -> 'BoardAPI':
101 | """
102 | Get board API for current project
103 |
104 | Returns:
105 | BoardAPI instance
106 | """
107 | pass
108 |
109 |
110 | class BoardAPI(ABC):
111 | """Abstract interface for board operations"""
112 |
113 | @abstractmethod
114 | def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
115 | """
116 | Set board size
117 |
118 | Args:
119 | width: Board width
120 | height: Board height
121 | unit: Unit of measurement ("mm" or "in")
122 |
123 | Returns:
124 | True if successful
125 | """
126 | pass
127 |
128 | @abstractmethod
129 | def get_size(self) -> Dict[str, float]:
130 | """
131 | Get current board size
132 |
133 | Returns:
134 | Dictionary with width, height, unit
135 | """
136 | pass
137 |
138 | @abstractmethod
139 | def add_layer(self, layer_name: str, layer_type: str) -> bool:
140 | """
141 | Add a layer to the board
142 |
143 | Args:
144 | layer_name: Name of the layer
145 | layer_type: Type ("copper", "technical", "user")
146 |
147 | Returns:
148 | True if successful
149 | """
150 | pass
151 |
152 | @abstractmethod
153 | def list_components(self) -> List[Dict[str, Any]]:
154 | """
155 | List all components on the board
156 |
157 | Returns:
158 | List of component dictionaries
159 | """
160 | pass
161 |
162 | @abstractmethod
163 | def place_component(
164 | self,
165 | reference: str,
166 | footprint: str,
167 | x: float,
168 | y: float,
169 | rotation: float = 0,
170 | layer: str = "F.Cu"
171 | ) -> bool:
172 | """
173 | Place a component on the board
174 |
175 | Args:
176 | reference: Component reference (e.g., "R1")
177 | footprint: Footprint library path
178 | x: X position (mm)
179 | y: Y position (mm)
180 | rotation: Rotation angle (degrees)
181 | layer: Layer name
182 |
183 | Returns:
184 | True if successful
185 | """
186 | pass
187 |
188 | # Routing Operations
189 | def add_track(
190 | self,
191 | start_x: float,
192 | start_y: float,
193 | end_x: float,
194 | end_y: float,
195 | width: float = 0.25,
196 | layer: str = "F.Cu",
197 | net_name: Optional[str] = None
198 | ) -> bool:
199 | """
200 | Add a track (trace) to the board
201 |
202 | Args:
203 | start_x: Start X position (mm)
204 | start_y: Start Y position (mm)
205 | end_x: End X position (mm)
206 | end_y: End Y position (mm)
207 | width: Track width (mm)
208 | layer: Layer name
209 | net_name: Optional net name
210 |
211 | Returns:
212 | True if successful
213 | """
214 | raise NotImplementedError()
215 |
216 | def add_via(
217 | self,
218 | x: float,
219 | y: float,
220 | diameter: float = 0.8,
221 | drill: float = 0.4,
222 | net_name: Optional[str] = None,
223 | via_type: str = "through"
224 | ) -> bool:
225 | """
226 | Add a via to the board
227 |
228 | Args:
229 | x: X position (mm)
230 | y: Y position (mm)
231 | diameter: Via diameter (mm)
232 | drill: Drill diameter (mm)
233 | net_name: Optional net name
234 | via_type: Via type ("through", "blind", "micro")
235 |
236 | Returns:
237 | True if successful
238 | """
239 | raise NotImplementedError()
240 |
241 | # Transaction support for undo/redo
242 | def begin_transaction(self, description: str = "MCP Operation") -> None:
243 | """Begin a transaction for grouping operations."""
244 | pass # Optional - not all backends support this
245 |
246 | def commit_transaction(self, description: str = "MCP Operation") -> None:
247 | """Commit the current transaction."""
248 | pass # Optional
249 |
250 | def rollback_transaction(self) -> None:
251 | """Roll back the current transaction."""
252 | pass # Optional
253 |
254 | def save(self) -> bool:
255 | """Save the board."""
256 | raise NotImplementedError()
257 |
258 | # Query operations
259 | def get_tracks(self) -> List[Dict[str, Any]]:
260 | """Get all tracks on the board."""
261 | raise NotImplementedError()
262 |
263 | def get_vias(self) -> List[Dict[str, Any]]:
264 | """Get all vias on the board."""
265 | raise NotImplementedError()
266 |
267 | def get_nets(self) -> List[Dict[str, Any]]:
268 | """Get all nets on the board."""
269 | raise NotImplementedError()
270 |
271 | def get_selection(self) -> List[Dict[str, Any]]:
272 | """Get currently selected items."""
273 | raise NotImplementedError()
274 |
275 |
276 | class BackendError(Exception):
277 | """Base exception for backend errors"""
278 | pass
279 |
280 |
281 | class ConnectionError(BackendError):
282 | """Raised when connection to KiCAD fails"""
283 | pass
284 |
285 |
286 | class APINotAvailableError(BackendError):
287 | """Raised when required API is not available"""
288 | pass
289 |
```
--------------------------------------------------------------------------------
/docs/IPC_BACKEND_STATUS.md:
--------------------------------------------------------------------------------
```markdown
1 | # KiCAD IPC Backend Implementation Status
2 |
3 | **Status:** Under Active Development and Testing
4 | **Date:** 2025-12-02
5 | **KiCAD Version:** 9.0.6
6 | **kicad-python Version:** 0.5.0
7 |
8 | ---
9 |
10 | ## Overview
11 |
12 | The IPC backend provides real-time UI synchronization with KiCAD 9.0+ via the official IPC API. When KiCAD is running with IPC enabled, commands can update the KiCAD UI immediately without requiring manual reload.
13 |
14 | This feature is experimental and under active testing. The server uses a hybrid approach: IPC when available, automatic fallback to SWIG when IPC is not connected.
15 |
16 | ## Key Differences
17 |
18 | | Feature | SWIG | IPC |
19 | |---------|------|-----|
20 | | UI Updates | Manual reload required | Immediate (when working) |
21 | | Undo/Redo | Not supported | Transaction support |
22 | | API Stability | Deprecated in KiCAD 9 | Official, versioned |
23 | | Connection | File-based | Live socket connection |
24 | | KiCAD Required | No (file operations) | Yes (must be running) |
25 |
26 | ## Implemented IPC Commands
27 |
28 | The following MCP commands have IPC handlers:
29 |
30 | | Command | IPC Handler | Status |
31 | |---------|-------------|--------|
32 | | `route_trace` | `_ipc_route_trace` | Implemented |
33 | | `add_via` | `_ipc_add_via` | Implemented |
34 | | `add_net` | `_ipc_add_net` | Implemented |
35 | | `delete_trace` | `_ipc_delete_trace` | Falls back to SWIG |
36 | | `get_nets_list` | `_ipc_get_nets_list` | Implemented |
37 | | `add_copper_pour` | `_ipc_add_copper_pour` | Implemented |
38 | | `refill_zones` | `_ipc_refill_zones` | Implemented |
39 | | `add_text` | `_ipc_add_text` | Implemented |
40 | | `add_board_text` | `_ipc_add_text` | Implemented |
41 | | `set_board_size` | `_ipc_set_board_size` | Implemented |
42 | | `get_board_info` | `_ipc_get_board_info` | Implemented |
43 | | `add_board_outline` | `_ipc_add_board_outline` | Implemented |
44 | | `add_mounting_hole` | `_ipc_add_mounting_hole` | Implemented |
45 | | `get_layer_list` | `_ipc_get_layer_list` | Implemented |
46 | | `place_component` | `_ipc_place_component` | Implemented (hybrid) |
47 | | `move_component` | `_ipc_move_component` | Implemented |
48 | | `rotate_component` | `_ipc_rotate_component` | Implemented |
49 | | `delete_component` | `_ipc_delete_component` | Implemented |
50 | | `get_component_list` | `_ipc_get_component_list` | Implemented |
51 | | `get_component_properties` | `_ipc_get_component_properties` | Implemented |
52 | | `save_project` | `_ipc_save_project` | Implemented |
53 |
54 | ### Implemented Backend Features
55 |
56 | **Core Connection:**
57 | - Connect to running KiCAD instance
58 | - Auto-detect socket path (`/tmp/kicad/api.sock`)
59 | - Version checking and validation
60 | - Auto-fallback to SWIG when IPC unavailable
61 | - Change notification callbacks
62 |
63 | **Board Operations:**
64 | - Get board reference
65 | - Get/Set board size
66 | - List enabled layers
67 | - Save board
68 | - Add board outline segments
69 | - Add mounting holes
70 |
71 | **Component Operations:**
72 | - List all components
73 | - Place component (hybrid: SWIG for library loading, IPC for placement)
74 | - Move component
75 | - Rotate component
76 | - Delete component
77 | - Get component properties
78 |
79 | **Routing Operations:**
80 | - Add track
81 | - Add via
82 | - Get all tracks
83 | - Get all vias
84 | - Get all nets
85 |
86 | **Zone Operations:**
87 | - Add copper pour zones
88 | - Get zones list
89 | - Refill zones
90 |
91 | **UI Integration:**
92 | - Add text to board
93 | - Get current selection
94 | - Clear selection
95 |
96 | **Transaction Support:**
97 | - Begin transaction
98 | - Commit transaction (with description for undo)
99 | - Rollback transaction
100 |
101 | ## Usage
102 |
103 | ### Prerequisites
104 |
105 | 1. **KiCAD 9.0+** must be running
106 | 2. **IPC API must be enabled**: `Preferences > Plugins > Enable IPC API Server`
107 | 3. A board must be open in the PCB editor
108 |
109 | ### Installation
110 |
111 | ```bash
112 | pip install kicad-python
113 | ```
114 |
115 | ### Testing
116 |
117 | Run the test script to verify IPC functionality:
118 |
119 | ```bash
120 | # Make sure KiCAD is running with IPC enabled and a board open
121 | ./venv/bin/python python/test_ipc_backend.py
122 | ```
123 |
124 | ## Architecture
125 |
126 | ```
127 | +-------------------------------------------------------------+
128 | | MCP Server (TypeScript/Node.js) |
129 | +---------------------------+---------------------------------+
130 | | JSON commands
131 | +---------------------------v---------------------------------+
132 | | Python Interface Layer |
133 | | +--------------------------------------------------------+ |
134 | | | kicad_interface.py | |
135 | | | - Routes commands to IPC or SWIG handlers | |
136 | | | - IPC_CAPABLE_COMMANDS dict defines routing | |
137 | | +--------------------------------------------------------+ |
138 | | +--------------------------------------------------------+ |
139 | | | kicad_api/ipc_backend.py | |
140 | | | - IPCBackend (connection management) | |
141 | | | - IPCBoardAPI (board operations) | |
142 | | +--------------------------------------------------------+ |
143 | +---------------------------+---------------------------------+
144 | | kicad-python (kipy) library
145 | +---------------------------v---------------------------------+
146 | | Protocol Buffers over UNIX Sockets |
147 | +---------------------------+---------------------------------+
148 | |
149 | +---------------------------v---------------------------------+
150 | | KiCAD 9.0+ (IPC Server) |
151 | +-------------------------------------------------------------+
152 | ```
153 |
154 | ## Known Limitations
155 |
156 | 1. **KiCAD must be running**: Unlike SWIG, IPC requires KiCAD to be open
157 | 2. **Project creation**: Not supported via IPC, uses file system
158 | 3. **Footprint library access**: Uses hybrid approach (SWIG loads from library, IPC places)
159 | 4. **Delete trace**: Falls back to SWIG (IPC API doesn't support direct deletion)
160 | 5. **Some operations may not work as expected**: This is experimental code
161 |
162 | ## Troubleshooting
163 |
164 | ### "Connection failed"
165 | - Ensure KiCAD is running
166 | - Enable IPC API: `Preferences > Plugins > Enable IPC API Server`
167 | - Check if a board is open
168 |
169 | ### "kicad-python not found"
170 | ```bash
171 | pip install kicad-python
172 | ```
173 |
174 | ### "Version mismatch"
175 | - Update kicad-python: `pip install --upgrade kicad-python`
176 | - Ensure KiCAD 9.0+ is installed
177 |
178 | ### "No board open"
179 | - Open a board in KiCAD's PCB editor before connecting
180 |
181 | ## File Structure
182 |
183 | ```
184 | python/kicad_api/
185 | ├── __init__.py # Package exports
186 | ├── base.py # Abstract base classes
187 | ├── factory.py # Backend auto-detection
188 | ├── ipc_backend.py # IPC implementation
189 | └── swig_backend.py # Legacy SWIG wrapper
190 |
191 | python/
192 | └── test_ipc_backend.py # IPC test script
193 | ```
194 |
195 | ## Future Work
196 |
197 | 1. More comprehensive testing of all IPC commands
198 | 2. Footprint library integration via IPC (when kipy supports it)
199 | 3. Schematic IPC support (when available in kicad-python)
200 | 4. Event subscriptions to react to changes made in KiCAD UI
201 | 5. Multi-board support
202 |
203 | ## Related Documentation
204 |
205 | - [ROADMAP.md](./ROADMAP.md) - Project roadmap
206 | - [IPC_API_MIGRATION_PLAN.md](./IPC_API_MIGRATION_PLAN.md) - Migration details
207 | - [REALTIME_WORKFLOW.md](./REALTIME_WORKFLOW.md) - Collaboration workflows
208 | - [kicad-python docs](https://docs.kicad.org/kicad-python-main/) - Official API docs
209 |
210 | ---
211 |
212 | **Last Updated:** 2025-12-02
213 |
```
--------------------------------------------------------------------------------
/python/commands/component_schematic.py:
--------------------------------------------------------------------------------
```python
1 | from skip import Schematic
2 | # Symbol class might not be directly importable in the current version
3 | import os
4 |
5 | class ComponentManager:
6 | """Manage components in a schematic"""
7 |
8 | @staticmethod
9 | def add_component(schematic: Schematic, component_def: dict):
10 | """Add a component to the schematic"""
11 | try:
12 | # Create a new symbol
13 | symbol = schematic.add_symbol(
14 | lib=component_def.get('library', 'Device'),
15 | name=component_def.get('type', 'R'), # Default to Resistor symbol 'R'
16 | reference=component_def.get('reference', 'R?'),
17 | at=[component_def.get('x', 0), component_def.get('y', 0)],
18 | unit=component_def.get('unit', 1),
19 | rotation=component_def.get('rotation', 0)
20 | )
21 |
22 | # Set properties
23 | if 'value' in component_def:
24 | symbol.property.Value.value = component_def['value']
25 | if 'footprint' in component_def:
26 | symbol.property.Footprint.value = component_def['footprint']
27 | if 'datasheet' in component_def:
28 | symbol.property.Datasheet.value = component_def['datasheet']
29 |
30 | # Add additional properties
31 | for key, value in component_def.get('properties', {}).items():
32 | # Avoid overwriting standard properties unless explicitly intended
33 | if key not in ['Reference', 'Value', 'Footprint', 'Datasheet']:
34 | symbol.property.append(key, value)
35 |
36 | print(f"Added component {symbol.reference} ({symbol.name}) to schematic.")
37 | return symbol
38 | except Exception as e:
39 | print(f"Error adding component: {e}")
40 | return None
41 |
42 | @staticmethod
43 | def remove_component(schematic: Schematic, component_ref: str):
44 | """Remove a component from the schematic by reference designator"""
45 | try:
46 | # kicad-skip doesn't have a direct remove_symbol method by reference.
47 | # We need to find the symbol and then remove it from the symbols list.
48 | symbol_to_remove = None
49 | for symbol in schematic.symbol:
50 | if symbol.reference == component_ref:
51 | symbol_to_remove = symbol
52 | break
53 |
54 | if symbol_to_remove:
55 | schematic.symbol.remove(symbol_to_remove)
56 | print(f"Removed component {component_ref} from schematic.")
57 | return True
58 | else:
59 | print(f"Component with reference {component_ref} not found.")
60 | return False
61 | except Exception as e:
62 | print(f"Error removing component {component_ref}: {e}")
63 | return False
64 |
65 |
66 | @staticmethod
67 | def update_component(schematic: Schematic, component_ref: str, new_properties: dict):
68 | """Update component properties by reference designator"""
69 | try:
70 | symbol_to_update = None
71 | for symbol in schematic.symbol:
72 | if symbol.reference == component_ref:
73 | symbol_to_update = symbol
74 | break
75 |
76 | if symbol_to_update:
77 | for key, value in new_properties.items():
78 | if key in symbol_to_update.property:
79 | symbol_to_update.property[key].value = value
80 | else:
81 | # Add as a new property if it doesn't exist
82 | symbol_to_update.property.append(key, value)
83 | print(f"Updated properties for component {component_ref}.")
84 | return True
85 | else:
86 | print(f"Component with reference {component_ref} not found.")
87 | return False
88 | except Exception as e:
89 | print(f"Error updating component {component_ref}: {e}")
90 | return False
91 |
92 | @staticmethod
93 | def get_component(schematic: Schematic, component_ref: str):
94 | """Get a component by reference designator"""
95 | for symbol in schematic.symbol:
96 | if symbol.reference == component_ref:
97 | print(f"Found component with reference {component_ref}.")
98 | return symbol
99 | print(f"Component with reference {component_ref} not found.")
100 | return None
101 |
102 | @staticmethod
103 | def search_components(schematic: Schematic, query: str):
104 | """Search for components matching criteria (basic implementation)"""
105 | # This is a basic search, could be expanded to use regex or more complex logic
106 | matching_components = []
107 | query_lower = query.lower()
108 | for symbol in schematic.symbol:
109 | if query_lower in symbol.reference.lower() or \
110 | query_lower in symbol.name.lower() or \
111 | (hasattr(symbol.property, 'Value') and query_lower in symbol.property.Value.value.lower()):
112 | matching_components.append(symbol)
113 | print(f"Found {len(matching_components)} components matching query '{query}'.")
114 | return matching_components
115 |
116 | @staticmethod
117 | def get_all_components(schematic: Schematic):
118 | """Get all components in schematic"""
119 | print(f"Retrieving all {len(schematic.symbol)} components.")
120 | return list(schematic.symbol)
121 |
122 | if __name__ == '__main__':
123 | # Example Usage (for testing)
124 | from schematic import SchematicManager # Assuming schematic.py is in the same directory
125 |
126 | # Create a new schematic
127 | test_sch = SchematicManager.create_schematic("ComponentTestSchematic")
128 |
129 | # Add components
130 | comp1_def = {"type": "R", "reference": "R1", "value": "10k", "x": 100, "y": 100}
131 | comp2_def = {"type": "C", "reference": "C1", "value": "0.1uF", "x": 200, "y": 100, "library": "Device"}
132 | comp3_def = {"type": "LED", "reference": "D1", "x": 300, "y": 100, "library": "Device", "properties": {"Color": "Red"}}
133 |
134 | comp1 = ComponentManager.add_component(test_sch, comp1_def)
135 | comp2 = ComponentManager.add_component(test_sch, comp2_def)
136 | comp3 = ComponentManager.add_component(test_sch, comp3_def)
137 |
138 | # Get a component
139 | retrieved_comp = ComponentManager.get_component(test_sch, "C1")
140 | if retrieved_comp:
141 | print(f"Retrieved component: {retrieved_comp.reference} ({retrieved_comp.value})")
142 |
143 | # Update a component
144 | ComponentManager.update_component(test_sch, "R1", {"value": "20k", "Tolerance": "5%"})
145 |
146 | # Search components
147 | matching_comps = ComponentManager.search_components(test_sch, "100") # Search by position
148 | print(f"Search results for '100': {[c.reference for c in matching_comps]}")
149 |
150 | # Get all components
151 | all_comps = ComponentManager.get_all_components(test_sch)
152 | print(f"All components: {[c.reference for c in all_comps]}")
153 |
154 | # Remove a component
155 | ComponentManager.remove_component(test_sch, "D1")
156 | all_comps_after_remove = ComponentManager.get_all_components(test_sch)
157 | print(f"Components after removing D1: {[c.reference for c in all_comps_after_remove]}")
158 |
159 | # Save the schematic (optional)
160 | # SchematicManager.save_schematic(test_sch, "component_test.kicad_sch")
161 |
162 | # Clean up (if saved)
163 | # if os.path.exists("component_test.kicad_sch"):
164 | # os.remove("component_test.kicad_sch")
165 | # print("Cleaned up component_test.kicad_sch")
166 |
```
--------------------------------------------------------------------------------
/python/commands/project.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Project-related command implementations for KiCAD interface
3 | """
4 |
5 | import os
6 | import pcbnew # type: ignore
7 | import logging
8 | from typing import Dict, Any, Optional
9 |
10 | logger = logging.getLogger('kicad_interface')
11 |
12 | class ProjectCommands:
13 | """Handles project-related KiCAD operations"""
14 |
15 | def __init__(self, board: Optional[pcbnew.BOARD] = None):
16 | """Initialize with optional board instance"""
17 | self.board = board
18 |
19 | def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
20 | """Create a new KiCAD project"""
21 | try:
22 | # Accept both 'name' (from MCP tool) and 'projectName' (legacy)
23 | project_name = params.get("name") or params.get("projectName", "New_Project")
24 | path = params.get("path", os.getcwd())
25 | template = params.get("template")
26 |
27 | # Generate the full project path
28 | project_path = os.path.join(path, project_name)
29 | if not project_path.endswith(".kicad_pro"):
30 | project_path += ".kicad_pro"
31 |
32 | # Create project directory if it doesn't exist
33 | os.makedirs(os.path.dirname(project_path), exist_ok=True)
34 |
35 | # Create a new board
36 | board = pcbnew.BOARD()
37 |
38 | # Set project properties
39 | board.GetTitleBlock().SetTitle(project_name)
40 |
41 | # Set current date with proper parameter
42 | from datetime import datetime
43 | current_date = datetime.now().strftime("%Y-%m-%d")
44 | board.GetTitleBlock().SetDate(current_date)
45 |
46 | # If template is specified, try to load it
47 | if template:
48 | template_path = os.path.expanduser(template)
49 | if os.path.exists(template_path):
50 | template_board = pcbnew.LoadBoard(template_path)
51 | # Copy settings from template
52 | board.SetDesignSettings(template_board.GetDesignSettings())
53 | board.SetLayerStack(template_board.GetLayerStack())
54 |
55 | # Save the board
56 | board_path = project_path.replace(".kicad_pro", ".kicad_pcb")
57 | board.SetFileName(board_path)
58 | pcbnew.SaveBoard(board_path, board)
59 |
60 | # Create project file
61 | with open(project_path, 'w') as f:
62 | f.write('{\n')
63 | f.write(' "board": {\n')
64 | f.write(f' "filename": "{os.path.basename(board_path)}"\n')
65 | f.write(' }\n')
66 | f.write('}\n')
67 |
68 | self.board = board
69 |
70 | return {
71 | "success": True,
72 | "message": f"Created project: {project_name}",
73 | "project": {
74 | "name": project_name,
75 | "path": project_path,
76 | "boardPath": board_path
77 | }
78 | }
79 |
80 | except Exception as e:
81 | logger.error(f"Error creating project: {str(e)}")
82 | return {
83 | "success": False,
84 | "message": "Failed to create project",
85 | "errorDetails": str(e)
86 | }
87 |
88 | def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
89 | """Open an existing KiCAD project"""
90 | try:
91 | filename = params.get("filename")
92 | if not filename:
93 | return {
94 | "success": False,
95 | "message": "No filename provided",
96 | "errorDetails": "The filename parameter is required"
97 | }
98 |
99 | # Expand user path and make absolute
100 | filename = os.path.abspath(os.path.expanduser(filename))
101 |
102 | # If it's a project file, get the board file
103 | if filename.endswith(".kicad_pro"):
104 | board_path = filename.replace(".kicad_pro", ".kicad_pcb")
105 | else:
106 | board_path = filename
107 |
108 | # Load the board
109 | board = pcbnew.LoadBoard(board_path)
110 | self.board = board
111 |
112 | return {
113 | "success": True,
114 | "message": f"Opened project: {os.path.basename(board_path)}",
115 | "project": {
116 | "name": os.path.splitext(os.path.basename(board_path))[0],
117 | "path": filename,
118 | "boardPath": board_path
119 | }
120 | }
121 |
122 | except Exception as e:
123 | logger.error(f"Error opening project: {str(e)}")
124 | return {
125 | "success": False,
126 | "message": "Failed to open project",
127 | "errorDetails": str(e)
128 | }
129 |
130 | def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
131 | """Save the current KiCAD project"""
132 | try:
133 | if not self.board:
134 | return {
135 | "success": False,
136 | "message": "No board is loaded",
137 | "errorDetails": "Load or create a board first"
138 | }
139 |
140 | filename = params.get("filename")
141 | if filename:
142 | # Save to new location
143 | filename = os.path.abspath(os.path.expanduser(filename))
144 | self.board.SetFileName(filename)
145 |
146 | # Save the board
147 | pcbnew.SaveBoard(self.board.GetFileName(), self.board)
148 |
149 | return {
150 | "success": True,
151 | "message": f"Saved project to: {self.board.GetFileName()}",
152 | "project": {
153 | "name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
154 | "path": self.board.GetFileName()
155 | }
156 | }
157 |
158 | except Exception as e:
159 | logger.error(f"Error saving project: {str(e)}")
160 | return {
161 | "success": False,
162 | "message": "Failed to save project",
163 | "errorDetails": str(e)
164 | }
165 |
166 | def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
167 | """Get information about the current project"""
168 | try:
169 | if not self.board:
170 | return {
171 | "success": False,
172 | "message": "No board is loaded",
173 | "errorDetails": "Load or create a board first"
174 | }
175 |
176 | title_block = self.board.GetTitleBlock()
177 | filename = self.board.GetFileName()
178 |
179 | return {
180 | "success": True,
181 | "project": {
182 | "name": os.path.splitext(os.path.basename(filename))[0],
183 | "path": filename,
184 | "title": title_block.GetTitle(),
185 | "date": title_block.GetDate(),
186 | "revision": title_block.GetRevision(),
187 | "company": title_block.GetCompany(),
188 | "comment1": title_block.GetComment(0),
189 | "comment2": title_block.GetComment(1),
190 | "comment3": title_block.GetComment(2),
191 | "comment4": title_block.GetComment(3)
192 | }
193 | }
194 |
195 | except Exception as e:
196 | logger.error(f"Error getting project info: {str(e)}")
197 | return {
198 | "success": False,
199 | "message": "Failed to get project information",
200 | "errorDetails": str(e)
201 | }
202 |
```