This is page 1 of 4. 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
├── 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
│   ├── JLCPCB_INTEGRATION_PLAN.md
│   ├── KNOWN_ISSUES.md
│   ├── LIBRARY_INTEGRATION.md
│   ├── LINUX_COMPATIBILITY_AUDIT.md
│   ├── REALTIME_WORKFLOW.md
│   ├── ROADMAP.md
│   ├── STATUS_SUMMARY.md
│   ├── UI_AUTO_LAUNCH.md
│   ├── VISUAL_FEEDBACK.md
│   ├── WEEK1_SESSION1_SUMMARY.md
│   └── WEEK1_SESSION2_SUMMARY.md
├── LICENSE
├── package-json.json
├── package-lock.json
├── package.json
├── pytest.ini
├── python
│   ├── commands
│   │   ├── __init__.py
│   │   ├── board
│   │   │   ├── __init__.py
│   │   │   ├── layers.py
│   │   │   ├── outline.py
│   │   │   ├── size.py
│   │   │   └── view.py
│   │   ├── board.py
│   │   ├── component_schematic.py
│   │   ├── component.py
│   │   ├── connection_schematic.py
│   │   ├── design_rules.py
│   │   ├── export.py
│   │   ├── library_schematic.py
│   │   ├── project.py
│   │   ├── routing.py
│   │   └── schematic.py
│   ├── kicad_api
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── factory.py
│   │   ├── ipc_backend.py
│   │   └── swig_backend.py
│   ├── kicad_interface.py
│   ├── requirements.txt
│   └── utils
│       ├── __init__.py
│       ├── kicad_process.py
│       └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│   ├── auto_refresh_kicad.sh
│   └── install-linux.sh
├── src
│   ├── config.ts
│   ├── index.ts
│   ├── kicad-server.ts
│   ├── logger.ts
│   ├── prompts
│   │   ├── component.ts
│   │   ├── design.ts
│   │   ├── index.ts
│   │   └── routing.ts
│   ├── resources
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── index.ts
│   │   ├── library.ts
│   │   └── project.ts
│   ├── server.ts
│   ├── tools
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── component.txt
│   │   ├── design-rules.ts
│   │   ├── export.ts
│   │   ├── index.ts
│   │   ├── project.ts
│   │   ├── routing.ts
│   │   ├── schematic.ts
│   │   └── ui.ts
│   └── utils
│       └── resource-helpers.ts
├── tests
│   ├── __init__.py
│   └── test_platform_helper.py
├── tsconfig-json.json
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.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 | # IDEs
41 | .vscode/
42 | .idea/
43 | *.swp
44 | *.swo
45 | *~
46 | .DS_Store
47 | 
48 | # Logs
49 | logs/
50 | *.log
51 | ~/.kicad-mcp/
52 | 
53 | # Environment
54 | .env
55 | .env.local
56 | .env.*.local
57 | 
58 | # KiCAD
59 | *.kicad_pcb-bak
60 | *.kicad_sch-bak
61 | *.kicad_pro-bak
62 | *.kicad_prl
63 | *-backups/
64 | fp-info-cache
65 | 
66 | # Testing
67 | test_output/
68 | schematic_test_output/
69 | coverage.xml
70 | .coverage.*
71 | 
72 | # OS
73 | Thumbs.db
74 | Desktop.ini
75 | 
```
--------------------------------------------------------------------------------
/.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 | # Note: this is basically a BETA Build 
  2 | # I built this in Linux / Ubuntu using Claude Code.  I have ONLY tested in Ubuntu.
  3 | I am working on this again after dealing with some family issues.
  4 | I apologize for this looking abandoned, it is not.
  5 | 
  6 | 
  7 | 
  8 | 
  9 | # KiCAD MCP: AI-Assisted PCB Design
 10 | 
 11 | KiCAD MCP is a Model Context Protocol (MCP) implementation that enables Large Language Models (LLMs) like Claude to directly interact with KiCAD for printed circuit board design. It creates a standardized communication bridge between AI assistants and the KiCAD PCB design software, allowing for natural language control of advanced PCB design operations.
 12 | 
 13 | ## What is MCP?
 14 | 
 15 | The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard from Anthropic that allows AI assistants like Claude to securely connect to external tools and data sources. Think of it as a universal adapter that lets Claude interact with your local software - in this case, KiCAD.
 16 | 
 17 | **With this MCP server, you can:**
 18 | - Design PCBs by talking to Claude in natural language
 19 | - Automate complex KiCAD operations through AI assistance
 20 | - Get real-time feedback as Claude creates and modifies your boards
 21 | - Leverage AI to handle tedious PCB design tasks
 22 | 
 23 | ## NEW FEATURES
 24 | 
 25 | ### Schematic Generation
 26 | Now, in addition to PCB design, KiCAD MCP enables AI assistants to:
 27 | 
 28 | - Create and manage KiCAD schematics through natural language
 29 | - Add components like resistors, capacitors, and ICs to schematics
 30 | - Connect components with wires to create complete circuits
 31 | - Save and load schematic files in KiCAD format
 32 | - Export schematics to PDF
 33 | 
 34 | ### UI Auto-Launch
 35 | Seamless visual feedback for PCB design. The MCP server can now:
 36 | 
 37 | - Auto-detect if KiCAD UI is running
 38 | - Auto-launch KiCAD when needed
 39 | - Open projects directly in the UI
 40 | - Cross-platform support (Linux, macOS, Windows)
 41 | 
 42 | Just say "Create a board" and watch it appear in KiCAD. See [UI_AUTO_LAUNCH.md](docs/UI_AUTO_LAUNCH.md) for details.
 43 | 
 44 | ## Project Status
 45 | 
 46 | **This project is currently undergoing a major v2.0 rebuild**
 47 | 
 48 | **Current Status ():**
 49 | - Cross-platform support (Linux, Windows, macOS)
 50 | - CI/CD pipeline with automated testing
 51 | - Platform-agnostic path handling
 52 | - Migrating to KiCAD IPC API (from deprecated SWIG)
 53 | - Adding JLCPCB parts integration
 54 | - Adding Digikey parts integration
 55 | - Smart BOM management system
 56 | 
 57 | **What Works Now (Tested & Verified):**
 58 | - Project management (create, open, save)
 59 | - Board outline creation (rectangle, circle, polygon)
 60 | - Board size setting (KiCAD 9.0 compatible)
 61 | - Mounting holes with configurable diameters
 62 | - Board text annotations (KiCAD 9.0 compatible)
 63 | - Layer management (add, set active, list)
 64 | - UI auto-launch and detection
 65 | - Visual feedback workflow (manual reload)
 66 | - Cross-platform Python venv support
 67 | - Design rule checking
 68 | - Export (Gerber, PDF, SVG, 3D models)
 69 | - Schematic generation
 70 | 
 71 | **Known Issues:**
 72 | - Component placement needs library path integration
 73 | - Routing operations not yet tested with KiCAD 9.0
 74 | - `get_board_info` has KiCAD 9.0 API compatibility issue
 75 | - UI auto-reload requires manual confirmation (IPC will fix this)
 76 | 
 77 | **Next Priorities ():**
 78 | 1. Component Library Integration - Map JLCPCB/Digikey parts to KiCAD footprints
 79 | 2. Routing Operations - Test and fix trace routing, vias, copper pours
 80 | 3. IPC Backend - Enable real-time UI updates (no manual reload)
 81 | 4. Documentation - Add video tutorials and example projects
 82 | 
 83 | **Future (v2.0):**
 84 | - AI-assisted component selection with cost optimization
 85 | - Smart BOM management and supplier integration
 86 | - Design pattern library (Arduino shields, Raspberry Pi HATs, etc.)
 87 | - Guided workflows for beginners
 88 | - Auto-documentation generation
 89 | 
 90 | **Documentation:**
 91 | - [Status Summary](docs/STATUS_SUMMARY.md) - Current state at a glance
 92 | - [Roadmap](docs/ROADMAP.md) - Where we're going (12-week plan)
 93 | - [Known Issues](docs/KNOWN_ISSUES.md) - Problems and workarounds
 94 | - [Changelog](CHANGELOG_2025-10-26.md) - Recent updates and fixes
 95 | 
 96 | ## What It Does
 97 | 
 98 | KiCAD MCP transforms how engineers and designers work with KiCAD by enabling AI assistants to:
 99 | 
100 | - Create and manage KiCAD PCB projects through natural language requests
101 | - **Create schematics** with components and connections
102 | - Manipulate board geometry, outlines, layers, and properties
103 | - Place and organize components in various patterns (grid, circular, aligned)
104 | - Route traces, differential pairs, and create copper pours
105 | - Implement design rules and perform design rule checks
106 | - Generate exports in various formats (Gerber, PDF, SVG, 3D models)
107 | - Provide comprehensive context about the circuit board to the AI assistant
108 | 
109 | This enables a natural language-driven PCB design workflow where complex operations can be requested in plain English, while still maintaining full engineer oversight and control.
110 | 
111 | ## Core Architecture
112 | 
113 | - **TypeScript MCP Server**: Implements the Anthropic Model Context Protocol specification to communicate with Claude and other compatible AI assistants
114 | - **Python KiCAD Interface**: Handles actual KiCAD operations via pcbnew Python API and kicad-skip library with comprehensive error handling
115 | - **Modular Design**: Organizes functionality by domains (project, schematic, board, component, routing) for maintainability and extensibility
116 | 
117 | ## Prerequisites - READ THIS FIRST!
118 | 
119 | Before installing this MCP server, you **MUST** have:
120 | 
121 | ### 1. KiCAD 9.0 or Higher (REQUIRED!)
122 | 
123 | **This is the most critical requirement.** Without KiCAD properly installed with its Python module, this MCP server will not work.
124 | 
125 | - **Download:** [kicad.org/download](https://www.kicad.org/download/)
126 | - **Verify Python module:** After installing, run:
127 |   ```bash
128 |   python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
129 |   ```
130 |   If this fails, your KiCAD installation is incomplete.
131 | 
132 | ### 2. Python 3.10 or Higher
133 | 
134 | **Required Python packages:**
135 | ```
136 | kicad-skip>=0.1.0        # Schematic manipulation
137 | Pillow>=9.0.0            # Image processing for board rendering
138 | cairosvg>=2.7.0          # SVG rendering
139 | colorlog>=6.7.0          # Colored logging
140 | pydantic>=2.5.0          # Data validation
141 | requests>=2.31.0         # HTTP requests (for future API features)
142 | python-dotenv>=1.0.0     # Environment management
143 | ```
144 | 
145 | These will be installed automatically via `pip install -r requirements.txt`
146 | 
147 | ### 3. Node.js v18 or Higher
148 | 
149 | - **Download:** [nodejs.org](https://nodejs.org/)
150 | - **Verify:** Run `node --version` and `npm --version`
151 | 
152 | ### 4. An MCP-Compatible Client
153 | 
154 | Choose one:
155 | - **[Claude Desktop](https://claude.ai/download)** - Official Anthropic desktop app
156 | - **[Claude Code](https://docs.claude.com/claude-code)** - Official Anthropic CLI tool
157 | - **[Cline](https://github.com/cline/cline)** - Popular VSCode extension
158 | 
159 | ### 5. Operating System
160 | 
161 | - **Linux** (Ubuntu 22.04+, Fedora, Arch) - Primary platform, fully tested
162 | - **Windows 10/11** - Fully supported
163 | - **macOS** - Experimental (untested, please report issues!)
164 | 
165 | ## Installation
166 | 
167 | Choose your platform below for detailed installation instructions:
168 | 
169 | <details>
170 | <summary><b>Linux (Ubuntu/Debian)</b> - Click to expand</summary>
171 | 
172 | ### Step 1: Install KiCAD 9.0
173 | 
174 | ```bash
175 | # Add KiCAD 9.0 PPA (Ubuntu/Debian)
176 | sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
177 | sudo apt-get update
178 | 
179 | # Install KiCAD and libraries
180 | sudo apt-get install -y kicad kicad-libraries
181 | ```
182 | 
183 | ### Step 2: Install Node.js
184 | 
185 | ```bash
186 | # Install Node.js 20.x (recommended)
187 | curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
188 | sudo apt-get install -y nodejs
189 | 
190 | # Verify installation
191 | node --version  # Should be v20.x or higher
192 | npm --version
193 | ```
194 | 
195 | ### Step 3: Clone and Build
196 | 
197 | ```bash
198 | # Clone repository
199 | git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
200 | cd KiCAD-MCP-Server
201 | 
202 | # Install Node.js dependencies
203 | npm install
204 | 
205 | # Install Python dependencies
206 | pip3 install -r requirements.txt
207 | 
208 | # Build TypeScript
209 | npm run build
210 | ```
211 | 
212 | ### Step 4: Configure Cline
213 | 
214 | 1. Install VSCode and the Cline extension
215 | 2. Edit Cline MCP settings:
216 |    ```bash
217 |    code ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
218 |    ```
219 | 
220 | 3. Add this configuration (adjust paths for your system):
221 |    ```json
222 |    {
223 |      "mcpServers": {
224 |        "kicad": {
225 |          "command": "node",
226 |          "args": ["/home/YOUR_USERNAME/KiCAD-MCP-Server/dist/index.js"],
227 |          "env": {
228 |            "NODE_ENV": "production",
229 |            "PYTHONPATH": "/usr/lib/kicad/lib/python3/dist-packages",
230 |            "LOG_LEVEL": "info"
231 |          },
232 |          "description": "KiCAD PCB Design Assistant"
233 |        }
234 |      }
235 |    }
236 |    ```
237 | 
238 | 4. Restart VSCode
239 | 
240 | ### Step 5: Verify Installation
241 | 
242 | ```bash
243 | # Test platform detection
244 | python3 python/utils/platform_helper.py
245 | 
246 | # Run tests (optional)
247 | pytest tests/
248 | ```
249 | 
250 | **Troubleshooting:**
251 | - If KiCAD Python module not found, check: `python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"`
252 | - For PYTHONPATH issues, see: [docs/LINUX_COMPATIBILITY_AUDIT.md](docs/LINUX_COMPATIBILITY_AUDIT.md)
253 | 
254 | </details>
255 | 
256 | <details>
257 | <summary><b>Windows 10/11</b> - Click to expand</summary>
258 | 
259 | ### Step 1: Install KiCAD 9.0
260 | 
261 | 1. Download KiCAD 9.0 from [kicad.org/download/windows](https://www.kicad.org/download/windows/)
262 | 2. Run the installer with default options
263 | 3. Verify Python module is installed (included by default)
264 | 
265 | ### Step 2: Install Node.js
266 | 
267 | 1. Download Node.js 20.x from [nodejs.org](https://nodejs.org/)
268 | 2. Run installer with default options
269 | 3. Verify in PowerShell:
270 |    ```powershell
271 |    node --version
272 |    npm --version
273 |    ```
274 | 
275 | ### Step 3: Clone and Build
276 | 
277 | ```powershell
278 | # Clone repository
279 | git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
280 | cd KiCAD-MCP-Server
281 | 
282 | # Install dependencies
283 | npm install
284 | pip install -r requirements.txt
285 | 
286 | # Build
287 | npm run build
288 | ```
289 | 
290 | ### Step 4: Configure Cline
291 | 
292 | 1. Install VSCode and Cline extension
293 | 2. Edit Cline MCP settings at:
294 |    ```
295 |    %USERPROFILE%\AppData\Roaming\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
296 |    ```
297 | 
298 | 3. Add configuration:
299 |    ```json
300 |    {
301 |      "mcpServers": {
302 |        "kicad": {
303 |          "command": "C:\\Program Files\\nodejs\\node.exe",
304 |          "args": ["C:\\Users\\YOUR_USERNAME\\KiCAD-MCP-Server\\dist\\index.js"],
305 |          "env": {
306 |            "PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
307 |          }
308 |        }
309 |      }
310 |    }
311 |    ```
312 | 
313 | 4. Restart VSCode
314 | 
315 | </details>
316 | 
317 | <details>
318 | <summary><b>macOS</b> - Click to expand (Experimental)</summary>
319 | 
320 | ### Step 1: Install KiCAD 9.0
321 | 
322 | 1. Download KiCAD 9.0 from [kicad.org/download/macos](https://www.kicad.org/download/macos/)
323 | 2. Drag KiCAD.app to Applications folder
324 | 
325 | ### Step 2: Install Node.js
326 | 
327 | ```bash
328 | # Using Homebrew (install from brew.sh if needed)
329 | brew install node@20
330 | 
331 | # Verify
332 | node --version
333 | npm --version
334 | ```
335 | 
336 | ### Step 3: Clone and Build
337 | 
338 | ```bash
339 | git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
340 | cd KiCAD-MCP-Server
341 | npm install
342 | pip3 install -r requirements.txt
343 | npm run build
344 | ```
345 | 
346 | ### Step 4: Configure Cline
347 | 
348 | Edit `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`:
349 | 
350 | ```json
351 | {
352 |   "mcpServers": {
353 |     "kicad": {
354 |       "command": "node",
355 |       "args": ["/Users/YOUR_USERNAME/KiCAD-MCP-Server/dist/index.js"],
356 |       "env": {
357 |         "PYTHONPATH": "/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages"
358 |       }
359 |     }
360 |   }
361 | }
362 | ```
363 | 
364 | **Note:** macOS support is experimental. Please report issues on GitHub.
365 | 
366 | </details>
367 | 
368 | ## Quick Start
369 | 
370 | After installation, test with Cline:
371 | 
372 | 1. Open VSCode with Cline extension
373 | 2. Start a conversation with Claude
374 | 3. Try these commands:
375 | 
376 | ```
377 | Create a new KiCAD project named 'TestProject' in my home directory.
378 | ```
379 | 
380 | ```
381 | Set the board size to 100mm x 80mm and add a rectangular outline.
382 | ```
383 | 
384 | ```
385 | Show me the current board properties.
386 | ```
387 | 
388 | If Claude successfully executes these commands, your installation is working!
389 | 
390 | ### Configuration for Other Clients
391 | 
392 | The examples above show configuration for Cline (VSCode), but KiCAD MCP works with any MCP-compatible client:
393 | 
394 | - **Claude Desktop** - Desktop app from Anthropic
395 | - **Claude Code** - CLI tool from Anthropic
396 | - **Cline** - VSCode extension
397 | - **Any MCP client** - Using STDIO transport
398 | 
399 | For detailed configuration instructions for all clients, see:
400 | **[Client Configuration Guide](docs/CLIENT_CONFIGURATION.md)**
401 | 
402 | The guide includes:
403 | - Platform-specific configurations (Linux, macOS, Windows)
404 | - Client-specific setup (Claude Desktop, Cline, Claude Code)
405 | - Troubleshooting steps
406 | - How to find KiCAD Python paths
407 | - Advanced configuration options
408 | 
409 | ## Usage Examples
410 | 
411 | Here are some examples of what you can ask Claude to do with KiCAD MCP:
412 | 
413 | ### Project Management
414 | 
415 | ```
416 | Create a new KiCAD project named 'WiFiModule' in my Documents folder.
417 | ```
418 | 
419 | ```
420 | Open the existing KiCAD project at C:/Projects/Amplifier/Amplifier.kicad_pro
421 | ```
422 | 
423 | ### UI Management (NEW!)
424 | 
425 | ```
426 | Is KiCAD running?
427 | ```
428 | 
429 | ```
430 | Launch KiCAD with my project at /tmp/demo/project.kicad_pcb
431 | ```
432 | 
433 | ```
434 | Open KiCAD so I can see the board as we design it
435 | ```
436 | 
437 | ### Schematic Design
438 | 
439 | ```
440 | Create a new schematic named 'PowerSupply'.
441 | ```
442 | 
443 | ```
444 | Add a 10kΩ resistor and 0.1µF capacitor to the schematic.
445 | ```
446 | 
447 | ```
448 | Connect the resistor's pin 1 to the capacitor's pin 1.
449 | ```
450 | 
451 | ### Board Design
452 | 
453 | ```
454 | Set the board size to 100mm x 80mm.
455 | ```
456 | 
457 | ```
458 | Add a rounded rectangle board outline with 3mm corner radius.
459 | ```
460 | 
461 | ```
462 | Add mounting holes at each corner of the board, 5mm from the edges.
463 | ```
464 | 
465 | ### Component Placement
466 | 
467 | ```
468 | Place a 10uF capacitor at position x=50mm, y=30mm.
469 | ```
470 | 
471 | ```
472 | Create a grid of 8 LEDs, 4x2, starting at position x=20mm, y=10mm with 10mm spacing.
473 | ```
474 | 
475 | ```
476 | Align all resistors horizontally and distribute them evenly.
477 | ```
478 | 
479 | ### Routing
480 | 
481 | ```
482 | Create a new net named 'VCC' and assign it to the power net class.
483 | ```
484 | 
485 | ```
486 | Route a trace from component U1 pin 1 to component C3 pin 2 on layer F.Cu.
487 | ```
488 | 
489 | ```
490 | Add a copper pour for GND on the bottom layer.
491 | ```
492 | 
493 | ### Design Rules and Export
494 | 
495 | ```
496 | Set design rules with 0.2mm clearance and 0.25mm minimum track width.
497 | ```
498 | 
499 | ```
500 | Export Gerber files to the 'fabrication' directory.
501 | ```
502 | 
503 | ## Features by Category
504 | 
505 | ### Project Management
506 | - Create new KiCAD projects with customizable settings
507 | - Open existing KiCAD projects from file paths
508 | - Save projects with optional new locations
509 | - Retrieve project metadata and properties
510 | 
511 | ### Schematic Design
512 | - Create new schematics with customizable settings
513 | - Add components from symbol libraries (resistors, capacitors, ICs, etc.)
514 | - Connect components with wires to create circuits
515 | - Add labels, annotations, and documentation to schematics
516 | - Save and load schematics in KiCAD format
517 | - Export schematics to PDF for documentation
518 | 
519 | ### Board Design
520 | - Set precise board dimensions with support for metric and imperial units
521 | - Add custom board outlines (rectangle, rounded rectangle, circle, polygon)
522 | - Create and manage board layers with various configurations
523 | - Add mounting holes, text annotations, and other board features
524 | - Visualize the current board state
525 | 
526 | ### Components
527 | - Place components with specified footprints at precise locations
528 | - Create component arrays in grid or circular patterns
529 | - Move, rotate, and modify existing components
530 | - Align and distribute components evenly
531 | - Duplicate components with customizable properties
532 | - Get detailed component properties and listings
533 | 
534 | ### Routing
535 | - Create and manage nets with specific properties
536 | - Route traces between component pads or arbitrary points
537 | - Add vias, including blind and buried vias
538 | - Create differential pair routes for high-speed signals
539 | - Generate copper pours (ground planes, power planes)
540 | - Define net classes with specific design rules
541 | 
542 | ### Design Rules
543 | - Set global design rules for clearance, track width, etc.
544 | - Define specific rules for different net classes
545 | - Run Design Rule Check (DRC) to validate the design
546 | - View and manage DRC violations
547 | 
548 | ### Export
549 | - Generate industry-standard Gerber files for fabrication
550 | - Export PDF documentation of the PCB
551 | - Create SVG vector graphics of the board
552 | - Generate 3D models in STEP or VRML format
553 | - Produce bill of materials (BOM) in various formats
554 | 
555 | ## Implementation Details
556 | 
557 | The KiCAD MCP implementation uses a modular, maintainable architecture:
558 | 
559 | ### TypeScript MCP Server (Node.js)
560 | - **kicad-server.ts**: The main server that implements the MCP protocol
561 | - Uses STDIO transport for reliable communication with Cline
562 | - Manages the Python process for KiCAD operations
563 | - Handles command queuing, error recovery, and response formatting
564 | 
565 | ### Python Interface
566 | - **kicad_interface.py**: The main Python interface that:
567 |   - Parses commands received as JSON via stdin
568 |   - Routes commands to the appropriate specialized handlers
569 |   - Returns results as JSON via stdout
570 |   - Handles errors gracefully with detailed information
571 | 
572 | - **Modular Command Structure**:
573 |   - `commands/project.py`: Project creation, opening, saving
574 |   - `commands/schematic.py`: Schematic creation and management
575 |   - `commands/component_schematic.py`: Schematic component operations
576 |   - `commands/connection_schematic.py`: Wire and connection management
577 |   - `commands/library_schematic.py`: Symbol library integration
578 |   - `commands/board/`: Modular board manipulation functions
579 |     - `size.py`: Board size operations
580 |     - `layers.py`: Layer management
581 |     - `outline.py`: Board outline creation
582 |     - `view.py`: Visualization functions
583 |   - `commands/component.py`: PCB component placement and manipulation
584 |   - `commands/routing.py`: Trace routing and net management
585 |   - `commands/design_rules.py`: DRC and rule configuration
586 |   - `commands/export.py`: Output generation in various formats
587 | 
588 | This architecture ensures that each aspect of PCB design is handled by specialized modules while maintaining a clean, consistent interface layer.
589 | 
590 | ## Troubleshooting
591 | 
592 | ### Common Issues and Solutions
593 | 
594 | **Problem: KiCAD MCP isn't showing up in Claude's tools**
595 | - Make sure VSCode is completely restarted after updating the Cline MCP settings
596 | - Verify the paths in the config are correct for your system
597 | - Check that the `npm run build` completed successfully
598 | 
599 | **Problem: Node.js errors when launching the server**
600 | - Ensure you're using Node.js v18 or higher
601 | - Try running `npm install` again to ensure all dependencies are properly installed
602 | - Check the console output for specific error messages
603 | 
604 | **Problem: Python errors or KiCAD commands failing**
605 | - Verify that KiCAD 9.0 is properly installed
606 | - Check that the PYTHONPATH in the configuration points to the correct location
607 | - Try running a simple KiCAD Python script directly to ensure the pcbnew module is accessible
608 | 
609 | **Problem: Claude can't find or load your KiCAD project**
610 | - Use absolute paths when referring to project locations
611 | - Ensure the user running VSCode has access permissions to the directories
612 | 
613 | ### Getting Help
614 | 
615 | If you encounter issues not covered in this troubleshooting section:
616 | 1. Check the console output for error messages
617 | 2. Look for similar issues in the GitHub repository's Issues section
618 | 3. Open a new issue with detailed information about the problem
619 | 
620 | ## Contributing
621 | 
622 | Contributions to this project are welcome! Here's how you can help:
623 | 
624 | 1. **Report Bugs**: Open an issue describing what went wrong and how to reproduce it
625 | 2. **Suggest Features**: Have an idea? Share it via an issue
626 | 3. **Submit Pull Requests**: Fixed a bug or added a feature? Submit a PR!
627 | 4. **Improve Documentation**: Help clarify or expand the documentation
628 | 
629 | Please follow the existing code style and include tests for new features.
630 | 
631 | ## License
632 | 
633 | This project is licensed under the MIT License - see the LICENSE file for details.
634 | 
635 | 
636 | 
```
--------------------------------------------------------------------------------
/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/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 | 
```
--------------------------------------------------------------------------------
/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:
--------------------------------------------------------------------------------
```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 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/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.31.0
24 | 
25 | # Environment variable management
26 | python-dotenv>=1.0.0
27 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |   "name": "kicad-mcp",
 3 |   "version": "2.0.0-alpha.1",
 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.10.0",
37 |     "dotenv": "^16.0.3",
38 |     "express": "^5.1.0",
39 |     "zod": "^3.22.2"
40 |   },
41 |   "devDependencies": {
42 |     "@types/express": "^5.0.1",
43 |     "@types/node": "^20.5.6",
44 |     "nodemon": "^3.0.1",
45 |     "typescript": "^5.2.2"
46 |   }
47 | }
48 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/src/tools/schematic.ts:
--------------------------------------------------------------------------------
```typescript
 1 | /**
 2 |  * Schematic 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 registerSchematicTools(server: McpServer, callKicadScript: Function) {
 9 |   // Create schematic tool
10 |   server.tool(
11 |     "create_schematic",
12 |     "Create a new schematic",
13 |     {
14 |       name: z.string().describe("Schematic name"),
15 |       path: z.string().optional().describe("Optional path"),
16 |     },
17 |     async (args: { name: string; path?: string }) => {
18 |       const result = await callKicadScript("create_schematic", args);
19 |       return {
20 |         content: [{
21 |           type: "text",
22 |           text: JSON.stringify(result, null, 2)
23 |         }]
24 |       };
25 |     }
26 |   );
27 | 
28 |   // Add component to schematic
29 |   server.tool(
30 |     "add_schematic_component",
31 |     "Add a component to the schematic",
32 |     {
33 |       symbol: z.string().describe("Symbol library reference"),
34 |       reference: z.string().describe("Component reference (e.g., R1, U1)"),
35 |       value: z.string().optional().describe("Component value"),
36 |       position: z.object({
37 |         x: z.number(),
38 |         y: z.number()
39 |       }).optional().describe("Position on schematic"),
40 |     },
41 |     async (args: any) => {
42 |       const result = await callKicadScript("add_schematic_component", args);
43 |       return {
44 |         content: [{
45 |           type: "text",
46 |           text: JSON.stringify(result, null, 2)
47 |         }]
48 |       };
49 |     }
50 |   );
51 | 
52 |   // Connect components with wire
53 |   server.tool(
54 |     "add_wire",
55 |     "Add a wire connection in the schematic",
56 |     {
57 |       start: z.object({
58 |         x: z.number(),
59 |         y: z.number()
60 |       }).describe("Start position"),
61 |       end: z.object({
62 |         x: z.number(),
63 |         y: z.number()
64 |       }).describe("End position"),
65 |     },
66 |     async (args: any) => {
67 |       const result = await callKicadScript("add_wire", args);
68 |       return {
69 |         content: [{
70 |           type: "text",
71 |           text: JSON.stringify(result, null, 2)
72 |         }]
73 |       };
74 |     }
75 |   );
76 | }
77 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/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"""
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 |             # Convert to internal units (nanometers)
40 |             scale = 1000000 if unit == "mm" else 25400000  # mm or inch to nm
41 |             width_nm = int(width * scale)
42 |             height_nm = int(height * scale)
43 | 
44 |             # Set board size using KiCAD 9.0 API
45 |             # Note: In KiCAD 9.0, SetSize takes two separate parameters instead of VECTOR2I
46 |             board_box = self.board.GetBoardEdgesBoundingBox()
47 |             try:
48 |                 # Try KiCAD 9.0+ API (two parameters)
49 |                 board_box.SetSize(width_nm, height_nm)
50 |             except TypeError:
51 |                 # Fall back to older API (VECTOR2I)
52 |                 board_box.SetSize(pcbnew.VECTOR2I(width_nm, height_nm))
53 | 
54 |             # Note: SetBoardEdgesBoundingBox might not exist in all versions
55 |             # The board bounding box is typically derived from actual edge cuts
56 |             # For now, we'll just note the size was calculated
57 |             logger.info(f"Board size set to {width}x{height} {unit}")
58 | 
59 |             return {
60 |                 "success": True,
61 |                 "message": f"Set board size to {width}x{height} {unit}",
62 |                 "size": {
63 |                     "width": width,
64 |                     "height": height,
65 |                     "unit": unit
66 |                 }
67 |             }
68 | 
69 |         except Exception as e:
70 |             logger.error(f"Error setting board size: {str(e)}")
71 |             return {
72 |                 "success": False,
73 |                 "message": "Failed to set board size",
74 |                 "errorDetails": str(e)
75 |             }
76 | 
```
--------------------------------------------------------------------------------
/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
 91 |     switch (level) {
 92 |       case 'error':
 93 |         console.error(formattedMessage);
 94 |         break;
 95 |       case 'warn':
 96 |         console.warn(formattedMessage);
 97 |         break;
 98 |       case 'info':
 99 |       case 'debug':
100 |       default:
101 |         console.log(formattedMessage);
102 |         break;
103 |     }
104 |     
105 |     // Log to file
106 |     try {
107 |       // Ensure log directory exists
108 |       if (!existsSync(this.logDir)) {
109 |         mkdirSync(this.logDir, { recursive: true });
110 |       }
111 |       
112 |       const logFile = join(this.logDir, `kicad-mcp-${new Date().toISOString().split('T')[0]}.log`);
113 |       appendFileSync(logFile, formattedMessage + '\n');
114 |     } catch (error) {
115 |       console.error(`Failed to write to log file: ${error}`);
116 |     }
117 |   }
118 | }
119 | 
120 | // Create and export logger instance
121 | export const logger = new Logger();
122 | 
```
--------------------------------------------------------------------------------
/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 if this file is executed directly
111 | if (import.meta.url === `file://${process.argv[1]}`) {
112 |   main().catch((error) => {
113 |     logger.error(`Unhandled error in main: ${error}`);
114 |     process.exit(1);
115 |   });
116 | }
117 | 
118 | // For testing and programmatic usage
119 | export { KiCADMcpServer };
120 | 
```
--------------------------------------------------------------------------------
/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 | 
```
--------------------------------------------------------------------------------
/python/commands/schematic.py:
--------------------------------------------------------------------------------
```python
 1 | from skip import Schematic
 2 | import os
 3 | 
 4 | class SchematicManager:
 5 |     """Core schematic operations using kicad-skip"""
 6 | 
 7 |     @staticmethod
 8 |     def create_schematic(name, metadata=None):
 9 |         """Create a new empty schematic"""
10 |         # kicad-skip requires a filepath to create a schematic
11 |         # We'll create a blank schematic file by loading an existing file
12 |         # or we can create a template file first.
13 |         
14 |         # Create an empty template file first
15 |         temp_path = f"{name}_template.kicad_sch"
16 |         with open(temp_path, 'w') as f:
17 |             # Write minimal schematic file content
18 |             f.write("(kicad_sch (version 20230121) (generator \"KiCAD-MCP-Server\"))\n")
19 |         
20 |         # Now load it
21 |         sch = Schematic(temp_path)
22 |         sch.version = "20230121"  # Set appropriate version
23 |         sch.generator = "KiCAD-MCP-Server"
24 |         
25 |         # Clean up the template
26 |         os.remove(temp_path)
27 |         # Add metadata if provided
28 |         if metadata:
29 |             for key, value in metadata.items():
30 |                  # kicad-skip doesn't have a direct metadata property on Schematic,
31 |                  # but we can add properties to the root sheet if needed, or
32 |                  # include it in the file path/name convention.
33 |                  # For now, we'll just create the schematic.
34 |                  pass # Placeholder for potential metadata handling
35 | 
36 |         print(f"Created new schematic: {name}")
37 |         return sch
38 | 
39 |     @staticmethod
40 |     def load_schematic(file_path):
41 |         """Load an existing schematic"""
42 |         if not os.path.exists(file_path):
43 |             print(f"Error: Schematic file not found at {file_path}")
44 |             return None
45 |         try:
46 |             sch = Schematic(file_path)
47 |             print(f"Loaded schematic from: {file_path}")
48 |             return sch
49 |         except Exception as e:
50 |             print(f"Error loading schematic from {file_path}: {e}")
51 |             return None
52 | 
53 |     @staticmethod
54 |     def save_schematic(schematic, file_path):
55 |         """Save a schematic to file"""
56 |         try:
57 |             # kicad-skip uses write method, not save
58 |             schematic.write(file_path)
59 |             print(f"Saved schematic to: {file_path}")
60 |             return True
61 |         except Exception as e:
62 |             print(f"Error saving schematic to {file_path}: {e}")
63 |             return False
64 | 
65 |     @staticmethod
66 |     def get_schematic_metadata(schematic):
67 |         """Extract metadata from schematic"""
68 |         # kicad-skip doesn't expose a direct metadata object on Schematic.
69 |         # We can return basic info like version and generator.
70 |         metadata = {
71 |             "version": schematic.version,
72 |             "generator": schematic.generator,
73 |             # Add other relevant properties if needed
74 |         }
75 |         print("Extracted schematic metadata")
76 |         return metadata
77 | 
78 | if __name__ == '__main__':
79 |     # Example Usage (for testing)
80 |     # Create a new schematic
81 |     new_sch = SchematicManager.create_schematic("MyTestSchematic")
82 | 
83 |     # Save the schematic
84 |     test_file = "test_schematic.kicad_sch"
85 |     SchematicManager.save_schematic(new_sch, test_file)
86 | 
87 |     # Load the schematic
88 |     loaded_sch = SchematicManager.load_schematic(test_file)
89 |     if loaded_sch:
90 |         metadata = SchematicManager.get_schematic_metadata(loaded_sch)
91 |         print(f"Loaded schematic metadata: {metadata}")
92 | 
93 |     # Clean up test file
94 |     if os.path.exists(test_file):
95 |         os.remove(test_file)
96 |         print(f"Cleaned up {test_file}")
97 | 
```
--------------------------------------------------------------------------------
/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 |     # Add more abstract methods for routing, DRC, export, etc.
189 |     # These will be filled in during migration
190 | 
191 | 
192 | class BackendError(Exception):
193 |     """Base exception for backend errors"""
194 |     pass
195 | 
196 | 
197 | class ConnectionError(BackendError):
198 |     """Raised when connection to KiCAD fails"""
199 |     pass
200 | 
201 | 
202 | class APINotAvailableError(BackendError):
203 |     """Raised when required API is not available"""
204 |     pass
205 | 
```
--------------------------------------------------------------------------------
/docs/KNOWN_ISSUES.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Known Issues & Workarounds
  2 | 
  3 | **Last Updated:** 2025-10-26
  4 | **Version:** 2.0.0-alpha.2
  5 | 
  6 | This document tracks known issues and provides workarounds where available.
  7 | 
  8 | ---
  9 | 
 10 | ## 🐛 Current Issues
 11 | 
 12 | ### 1. Component Placement Fails - Library Path Not Found
 13 | 
 14 | **Status:** 🔴 **BLOCKING** - Cannot place components
 15 | 
 16 | **Symptoms:**
 17 | ```
 18 | Error: Could not find footprint library
 19 | ```
 20 | 
 21 | **Root Cause:** MCP server doesn't have access to KiCAD's footprint library paths
 22 | 
 23 | **Workaround:** None currently - feature not usable
 24 | 
 25 | **Fix Plan:** Week 2 priority
 26 | - Detect KiCAD library paths from environment
 27 | - Add configuration for custom library paths
 28 | - Integrate JLCPCB/Digikey part databases
 29 | 
 30 | **Tracking:** High Priority - Required for any real PCB design
 31 | 
 32 | ---
 33 | 
 34 | ### 2. Routing Operations Untested with KiCAD 9.0
 35 | 
 36 | **Status:** 🟡 **UNKNOWN** - May have API compatibility issues
 37 | 
 38 | **Affected Commands:**
 39 | - `route_trace`
 40 | - `add_via`
 41 | - `add_copper_pour`
 42 | - `route_differential_pair`
 43 | 
 44 | **Symptoms:** May fail with API type mismatch errors (like set_board_size did)
 45 | 
 46 | **Workaround:** None - needs testing and fixes
 47 | 
 48 | **Fix Plan:** Week 2 priority
 49 | - Test each routing command with KiCAD 9.0
 50 | - Fix API compatibility issues
 51 | - Add comprehensive routing examples
 52 | 
 53 | ---
 54 | 
 55 | ### 3. `get_board_info` KiCAD 9.0 API Issue
 56 | 
 57 | **Status:** 🟡 **KNOWN** - Non-critical
 58 | 
 59 | **Symptoms:**
 60 | ```
 61 | AttributeError: 'BOARD' object has no attribute 'LT_USER'
 62 | ```
 63 | 
 64 | **Root Cause:** KiCAD 9.0 changed layer enumeration constants
 65 | 
 66 | **Workaround:** Use `get_project_info` instead for basic project details
 67 | 
 68 | **Fix Plan:** Week 2
 69 | - Update to use KiCAD 9.0 layer constants
 70 | - Add backward compatibility for KiCAD 8.x
 71 | 
 72 | **Impact:** Low - informational command only
 73 | 
 74 | ---
 75 | 
 76 | ### 4. UI Auto-Reload Requires Manual Confirmation
 77 | 
 78 | **Status:** 🟢 **BY DESIGN** - Will be fixed by IPC
 79 | 
 80 | **Symptoms:**
 81 | - MCP makes changes
 82 | - KiCAD detects file change
 83 | - User must click "Reload" button to see changes
 84 | 
 85 | **Current Workflow:**
 86 | ```
 87 | 1. Claude makes change via MCP
 88 | 2. KiCAD shows: "File has been modified. Reload? [Yes] [No]"
 89 | 3. User clicks "Yes"
 90 | 4. Changes appear in UI
 91 | ```
 92 | 
 93 | **Why:** SWIG-based backend requires file I/O, can't push changes to running UI
 94 | 
 95 | **Fix Plan:** Weeks 2-3 - IPC Backend Migration
 96 | - Connect to KiCAD via IPC socket
 97 | - Make changes directly in running instance
 98 | - No file reload needed - instant visual feedback
 99 | 
100 | **Workaround:** This is the current expected behavior - just click reload!
101 | 
102 | ---
103 | 
104 | ## 🔧 Recently Fixed
105 | 
106 | ### ✅ KiCAD Process Detection (Fixed 2025-10-26)
107 | 
108 | **Was:** `check_kicad_ui` detected MCP server's own processes
109 | **Now:** Properly filters to only detect actual KiCAD binaries
110 | 
111 | ### ✅ set_board_size KiCAD 9.0 (Fixed 2025-10-26)
112 | 
113 | **Was:** Failed with `BOX2I_SetSize` type error
114 | **Now:** Works with KiCAD 9.0 API, backward compatible with 8.x
115 | 
116 | ### ✅ add_board_text KiCAD 9.0 (Fixed 2025-10-26)
117 | 
118 | **Was:** Failed with `EDA_ANGLE` type error
119 | **Now:** Works with KiCAD 9.0 API, backward compatible with 8.x
120 | 
121 | ### ✅ Missing add_board_text Command (Fixed 2025-10-26)
122 | 
123 | **Was:** Command not found error
124 | **Now:** Properly mapped to Python handler
125 | 
126 | ---
127 | 
128 | ## 📋 Reporting New Issues
129 | 
130 | If you encounter an issue not listed here:
131 | 
132 | 1. **Check MCP logs:** `~/.kicad-mcp/logs/kicad_interface.log`
133 | 2. **Check KiCAD version:** `pcbnew --version` (must be 9.0+)
134 | 3. **Try the operation in KiCAD directly** - is it a KiCAD issue?
135 | 4. **Open GitHub issue** with:
136 |    - Error message
137 |    - Log excerpt
138 |    - Steps to reproduce
139 |    - KiCAD version
140 |    - OS and version
141 | 
142 | ---
143 | 
144 | ## 🎯 Priority Matrix
145 | 
146 | | Issue | Priority | Impact | Effort | Status |
147 | |-------|----------|--------|--------|--------|
148 | | Component Library Integration | 🔴 Critical | High | Medium | Week 2 |
149 | | Routing KiCAD 9.0 Compatibility | 🟡 High | High | Low | Week 2 |
150 | | IPC Backend (Real-time UI) | 🟡 High | Medium | High | Week 2-3 |
151 | | get_board_info Fix | 🟢 Low | Low | Low | Week 2 |
152 | 
153 | ---
154 | 
155 | ## 💡 General Workarounds
156 | 
157 | ### Server Won't Start
158 | ```bash
159 | # Check Python can import pcbnew
160 | python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
161 | 
162 | # Check paths
163 | python3 python/utils/platform_helper.py
164 | ```
165 | 
166 | ### Commands Fail After Server Restart
167 | ```
168 | # Board reference is lost on restart
169 | # Always run open_project after server restart
170 | ```
171 | 
172 | ### KiCAD UI Doesn't Show Changes
173 | ```
174 | # File → Revert (or click reload prompt)
175 | # Or: Close and reopen file in KiCAD
176 | ```
177 | 
178 | ---
179 | 
180 | **Need Help?**
181 | - Check [docs/VISUAL_FEEDBACK.md](VISUAL_FEEDBACK.md) for workflow tips
182 | - Check [docs/UI_AUTO_LAUNCH.md](UI_AUTO_LAUNCH.md) for UI setup
183 | - Open an issue on GitHub
184 | 
```
--------------------------------------------------------------------------------
/python/commands/connection_schematic.py:
--------------------------------------------------------------------------------
```python
 1 | from skip import Schematic
 2 | # Wire and Net classes might not be directly importable in the current version
 3 | import os
 4 | 
 5 | class ConnectionManager:
 6 |     """Manage connections between components"""
 7 | 
 8 |     @staticmethod
 9 |     def add_wire(schematic: Schematic, start_point: list, end_point: list, properties: dict = None):
10 |         """Add a wire between two points"""
11 |         try:
12 |             wire = schematic.add_wire(start=start_point, end=end_point)
13 |             # kicad-skip wire properties are limited, but we can potentially
14 |             # add graphical properties if needed in the future.
15 |             print(f"Added wire from {start_point} to {end_point}.")
16 |             return wire
17 |         except Exception as e:
18 |             print(f"Error adding wire: {e}")
19 |             return None
20 | 
21 |     @staticmethod
22 |     def add_connection(schematic: Schematic, source_ref: str, source_pin: str, target_ref: str, target_pin: str):
23 |         """Add a connection between component pins"""
24 |         # kicad-skip handles connections implicitly through wires and labels.
25 |         # This method would typically involve adding wires and potentially net labels
26 |         # to connect the specified pins.
27 |         # A direct 'add_connection' between pins isn't a standard kicad-skip operation
28 |         # in the way it is in some other schematic tools.
29 |         # We will need to implement this logic by finding the component pins
30 |         # and adding wires/labels between their locations. This is more complex
31 |         # and might require pin location information which isn't directly
32 |         # exposed in a simple way by default in kicad-skip Symbol objects.
33 | 
34 |         # For now, this method will be a placeholder or require a more advanced
35 |         # implementation based on how kicad-skip handles net connections.
36 |         # A common approach is to add wires between graphical points and then
37 |         # add net labels to define the net name.
38 | 
39 |         print(f"Attempted to add connection between {source_ref}/{source_pin} and {target_ref}/{target_pin}. This requires advanced implementation.")
40 |         return False # Indicate not fully implemented yet
41 | 
42 |     @staticmethod
43 |     def remove_connection(schematic: Schematic, connection_id: str):
44 |         """Remove a connection"""
45 |         # Removing connections in kicad-skip typically means removing the wires
46 |         # or net labels that form the connection.
47 |         # This method would need to identify the relevant graphical elements
48 |         # based on a connection identifier (which we would need to define).
49 |         # This is also an advanced implementation task.
50 |         print(f"Attempted to remove connection with ID {connection_id}. This requires advanced implementation.")
51 |         return False # Indicate not fully implemented yet
52 | 
53 |     @staticmethod
54 |     def get_net_connections(schematic: Schematic, net_name: str):
55 |         """Get all connections in a named net"""
56 |         # kicad-skip represents nets implicitly through connected wires and net labels.
57 |         # To get connections for a net, we would need to iterate through wires
58 |         # and net labels to build a list of connected pins/points.
59 |         # This requires traversing the schematic's graphical elements and understanding
60 |         # how they form nets. This is an advanced implementation task.
61 |         print(f"Attempted to get connections for net '{net_name}'. This requires advanced implementation.")
62 |         return [] # Return empty list for now
63 | 
64 | if __name__ == '__main__':
65 |     # Example Usage (for testing)
66 |     from schematic import SchematicManager # Assuming schematic.py is in the same directory
67 | 
68 |     # Create a new schematic
69 |     test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
70 | 
71 |     # Add some wires
72 |     wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
73 |     wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
74 | 
75 |     # Note: add_connection, remove_connection, get_net_connections are placeholders
76 |     # and require more complex implementation based on kicad-skip's structure.
77 | 
78 |     # Example of how you might add a net label (requires finding a point on a wire)
79 |     # from skip import Label
80 |     # if wire1:
81 |     #     net_label_pos = wire1.start # Or calculate a point on the wire
82 |     #     net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
83 |     #     print(f"Added net label 'Net_01' at {net_label_pos}")
84 | 
85 |     # Save the schematic (optional)
86 |     # SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
87 | 
88 |     # Clean up (if saved)
89 |     # if os.path.exists("connection_test.kicad_sch"):
90 |     #     os.remove("connection_test.kicad_sch")
91 |     #     print("Cleaned up connection_test.kicad_sch")
92 | 
```
--------------------------------------------------------------------------------
/.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 | 
```
--------------------------------------------------------------------------------
/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
161 |     try:
162 |         import kicad
163 |         results['ipc'] = {
164 |             'available': True,
165 |             'version': getattr(kicad, '__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 | 
```
--------------------------------------------------------------------------------
/python/commands/board/view.py:
--------------------------------------------------------------------------------
```python
  1 | """
  2 | Board view command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import os
  6 | import pcbnew
  7 | import logging
  8 | from typing import Dict, Any, Optional, List, Tuple
  9 | from PIL import Image
 10 | import io
 11 | import base64
 12 | 
 13 | logger = logging.getLogger('kicad_interface')
 14 | 
 15 | class BoardViewCommands:
 16 |     """Handles board viewing operations"""
 17 | 
 18 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 19 |         """Initialize with optional board instance"""
 20 |         self.board = board
 21 | 
 22 |     def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
 23 |         """Get information about the current board"""
 24 |         try:
 25 |             if not self.board:
 26 |                 return {
 27 |                     "success": False,
 28 |                     "message": "No board is loaded",
 29 |                     "errorDetails": "Load or create a board first"
 30 |                 }
 31 | 
 32 |             # Get board dimensions
 33 |             board_box = self.board.GetBoardEdgesBoundingBox()
 34 |             width_nm = board_box.GetWidth()
 35 |             height_nm = board_box.GetHeight()
 36 | 
 37 |             # Convert to mm
 38 |             width_mm = width_nm / 1000000
 39 |             height_mm = height_nm / 1000000
 40 | 
 41 |             # Get layer information
 42 |             layers = []
 43 |             for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
 44 |                 if self.board.IsLayerEnabled(layer_id):
 45 |                     layers.append({
 46 |                         "name": self.board.GetLayerName(layer_id),
 47 |                         "type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
 48 |                         "id": layer_id
 49 |                     })
 50 | 
 51 |             return {
 52 |                 "success": True,
 53 |                 "board": {
 54 |                     "filename": self.board.GetFileName(),
 55 |                     "size": {
 56 |                         "width": width_mm,
 57 |                         "height": height_mm,
 58 |                         "unit": "mm"
 59 |                     },
 60 |                     "layers": layers,
 61 |                     "title": self.board.GetTitleBlock().GetTitle(),
 62 |                     "activeLayer": self.board.GetActiveLayer()
 63 |                 }
 64 |             }
 65 | 
 66 |         except Exception as e:
 67 |             logger.error(f"Error getting board info: {str(e)}")
 68 |             return {
 69 |                 "success": False,
 70 |                 "message": "Failed to get board information",
 71 |                 "errorDetails": str(e)
 72 |             }
 73 | 
 74 |     def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
 75 |         """Get a 2D image of the PCB"""
 76 |         try:
 77 |             if not self.board:
 78 |                 return {
 79 |                     "success": False,
 80 |                     "message": "No board is loaded",
 81 |                     "errorDetails": "Load or create a board first"
 82 |                 }
 83 | 
 84 |             # Get parameters
 85 |             width = params.get("width", 800)
 86 |             height = params.get("height", 600)
 87 |             format = params.get("format", "png")
 88 |             layers = params.get("layers", [])
 89 | 
 90 |             # Create plot controller
 91 |             plotter = pcbnew.PLOT_CONTROLLER(self.board)
 92 |             
 93 |             # Set up plot options
 94 |             plot_opts = plotter.GetPlotOptions()
 95 |             plot_opts.SetOutputDirectory(os.path.dirname(self.board.GetFileName()))
 96 |             plot_opts.SetScale(1)
 97 |             plot_opts.SetMirror(False)
 98 |             plot_opts.SetExcludeEdgeLayer(False)
 99 |             plot_opts.SetPlotFrameRef(False)
100 |             plot_opts.SetPlotValue(True)
101 |             plot_opts.SetPlotReference(True)
102 |             
103 |             # Plot to SVG first (for vector output)
104 |             temp_svg = os.path.join(os.path.dirname(self.board.GetFileName()), "temp_view.svg")
105 |             plotter.OpenPlotfile("temp_view", pcbnew.PLOT_FORMAT_SVG, "Temporary View")
106 |             
107 |             # Plot specified layers or all enabled layers
108 |             if layers:
109 |                 for layer_name in layers:
110 |                     layer_id = self.board.GetLayerID(layer_name)
111 |                     if layer_id >= 0 and self.board.IsLayerEnabled(layer_id):
112 |                         plotter.PlotLayer(layer_id)
113 |             else:
114 |                 for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
115 |                     if self.board.IsLayerEnabled(layer_id):
116 |                         plotter.PlotLayer(layer_id)
117 |             
118 |             plotter.ClosePlot()
119 | 
120 |             # Convert SVG to requested format
121 |             if format == "svg":
122 |                 with open(temp_svg, 'r') as f:
123 |                     svg_data = f.read()
124 |                 os.remove(temp_svg)
125 |                 return {
126 |                     "success": True,
127 |                     "imageData": svg_data,
128 |                     "format": "svg"
129 |                 }
130 |             else:
131 |                 # Use PIL to convert SVG to PNG/JPG
132 |                 from cairosvg import svg2png
133 |                 png_data = svg2png(url=temp_svg, output_width=width, output_height=height)
134 |                 os.remove(temp_svg)
135 |                 
136 |                 if format == "jpg":
137 |                     # Convert PNG to JPG
138 |                     img = Image.open(io.BytesIO(png_data))
139 |                     jpg_buffer = io.BytesIO()
140 |                     img.convert('RGB').save(jpg_buffer, format='JPEG')
141 |                     jpg_data = jpg_buffer.getvalue()
142 |                     return {
143 |                         "success": True,
144 |                         "imageData": base64.b64encode(jpg_data).decode('utf-8'),
145 |                         "format": "jpg"
146 |                     }
147 |                 else:
148 |                     return {
149 |                         "success": True,
150 |                         "imageData": base64.b64encode(png_data).decode('utf-8'),
151 |                         "format": "png"
152 |                     }
153 | 
154 |         except Exception as e:
155 |             logger.error(f"Error getting board 2D view: {str(e)}")
156 |             return {
157 |                 "success": False,
158 |                 "message": "Failed to get board 2D view",
159 |                 "errorDetails": str(e)
160 |             }
161 |     
162 |     def _get_layer_type_name(self, type_id: int) -> str:
163 |         """Convert KiCAD layer type constant to name"""
164 |         type_map = {
165 |             pcbnew.LT_SIGNAL: "signal",
166 |             pcbnew.LT_POWER: "power",
167 |             pcbnew.LT_MIXED: "mixed",
168 |             pcbnew.LT_JUMPER: "jumper",
169 |             pcbnew.LT_USER: "user"
170 |         }
171 |         return type_map.get(type_id, "unknown")
172 | 
```
--------------------------------------------------------------------------------
/python/kicad_api/ipc_backend.py:
--------------------------------------------------------------------------------
```python
  1 | """
  2 | IPC API Backend (KiCAD 9.0+)
  3 | 
  4 | Uses the official kicad-python library for inter-process communication
  5 | with a running KiCAD instance.
  6 | 
  7 | Note: Requires KiCAD to be running with IPC server enabled:
  8 |     Preferences > Plugins > Enable IPC API Server
  9 | """
 10 | import logging
 11 | from pathlib import Path
 12 | from typing import Optional, Dict, Any, List
 13 | 
 14 | from kicad_api.base import (
 15 |     KiCADBackend,
 16 |     BoardAPI,
 17 |     ConnectionError,
 18 |     APINotAvailableError
 19 | )
 20 | 
 21 | logger = logging.getLogger(__name__)
 22 | 
 23 | 
 24 | class IPCBackend(KiCADBackend):
 25 |     """
 26 |     KiCAD IPC API backend
 27 | 
 28 |     Communicates with KiCAD via Protocol Buffers over UNIX sockets.
 29 |     Requires KiCAD 9.0+ to be running with IPC enabled.
 30 |     """
 31 | 
 32 |     def __init__(self):
 33 |         self.kicad = None
 34 |         self._connected = False
 35 | 
 36 |     def connect(self) -> bool:
 37 |         """
 38 |         Connect to running KiCAD instance via IPC
 39 | 
 40 |         Returns:
 41 |             True if connection successful
 42 | 
 43 |         Raises:
 44 |             ConnectionError: If connection fails
 45 |         """
 46 |         try:
 47 |             # Import here to allow module to load even without kicad-python
 48 |             from kicad import KiCad
 49 | 
 50 |             logger.info("Connecting to KiCAD via IPC...")
 51 |             self.kicad = KiCad()
 52 | 
 53 |             # Verify connection with version check
 54 |             version = self.get_version()
 55 |             logger.info(f"✓ Connected to KiCAD {version} via IPC")
 56 |             self._connected = True
 57 |             return True
 58 | 
 59 |         except ImportError as e:
 60 |             logger.error("kicad-python library not found")
 61 |             raise APINotAvailableError(
 62 |                 "IPC backend requires kicad-python. "
 63 |                 "Install with: pip install kicad-python"
 64 |             ) from e
 65 |         except Exception as e:
 66 |             logger.error(f"Failed to connect via IPC: {e}")
 67 |             logger.info(
 68 |                 "Ensure KiCAD is running with IPC enabled: "
 69 |                 "Preferences > Plugins > Enable IPC API Server"
 70 |             )
 71 |             raise ConnectionError(f"IPC connection failed: {e}") from e
 72 | 
 73 |     def disconnect(self) -> None:
 74 |         """Disconnect from KiCAD"""
 75 |         if self.kicad:
 76 |             # kicad-python handles cleanup automatically
 77 |             self.kicad = None
 78 |             self._connected = False
 79 |             logger.info("Disconnected from KiCAD IPC")
 80 | 
 81 |     def is_connected(self) -> bool:
 82 |         """Check if connected"""
 83 |         return self._connected and self.kicad is not None
 84 | 
 85 |     def get_version(self) -> str:
 86 |         """Get KiCAD version"""
 87 |         if not self.kicad:
 88 |             raise ConnectionError("Not connected to KiCAD")
 89 | 
 90 |         try:
 91 |             # Use kicad-python's version checking
 92 |             version_info = self.kicad.check_version()
 93 |             return str(version_info)
 94 |         except Exception as e:
 95 |             logger.warning(f"Could not get version: {e}")
 96 |             return "unknown"
 97 | 
 98 |     # Project Operations
 99 |     def create_project(self, path: Path, name: str) -> Dict[str, Any]:
100 |         """
101 |         Create a new KiCAD project
102 | 
103 |         TODO: Implement with IPC API
104 |         """
105 |         if not self.is_connected():
106 |             raise ConnectionError("Not connected to KiCAD")
107 | 
108 |         logger.warning("create_project not yet implemented for IPC backend")
109 |         raise NotImplementedError(
110 |             "Project creation via IPC API is not yet implemented. "
111 |             "This will be added in Week 2-3 migration."
112 |         )
113 | 
114 |     def open_project(self, path: Path) -> Dict[str, Any]:
115 |         """Open existing project"""
116 |         if not self.is_connected():
117 |             raise ConnectionError("Not connected to KiCAD")
118 | 
119 |         logger.warning("open_project not yet implemented for IPC backend")
120 |         raise NotImplementedError("Coming in Week 2-3 migration")
121 | 
122 |     def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
123 |         """Save current project"""
124 |         if not self.is_connected():
125 |             raise ConnectionError("Not connected to KiCAD")
126 | 
127 |         logger.warning("save_project not yet implemented for IPC backend")
128 |         raise NotImplementedError("Coming in Week 2-3 migration")
129 | 
130 |     def close_project(self) -> None:
131 |         """Close current project"""
132 |         if not self.is_connected():
133 |             raise ConnectionError("Not connected to KiCAD")
134 | 
135 |         logger.warning("close_project not yet implemented for IPC backend")
136 |         raise NotImplementedError("Coming in Week 2-3 migration")
137 | 
138 |     # Board Operations
139 |     def get_board(self) -> BoardAPI:
140 |         """Get board API"""
141 |         if not self.is_connected():
142 |             raise ConnectionError("Not connected to KiCAD")
143 | 
144 |         return IPCBoardAPI(self.kicad)
145 | 
146 | 
147 | class IPCBoardAPI(BoardAPI):
148 |     """Board API implementation for IPC backend"""
149 | 
150 |     def __init__(self, kicad_instance):
151 |         self.kicad = kicad_instance
152 |         self._board = None
153 | 
154 |     def _get_board(self):
155 |         """Lazy-load board instance"""
156 |         if self._board is None:
157 |             self._board = self.kicad.get_board()
158 |         return self._board
159 | 
160 |     def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
161 |         """Set board size"""
162 |         logger.warning("set_size not yet implemented for IPC backend")
163 |         raise NotImplementedError("Coming in Week 2-3 migration")
164 | 
165 |     def get_size(self) -> Dict[str, float]:
166 |         """Get board size"""
167 |         logger.warning("get_size not yet implemented for IPC backend")
168 |         raise NotImplementedError("Coming in Week 2-3 migration")
169 | 
170 |     def add_layer(self, layer_name: str, layer_type: str) -> bool:
171 |         """Add layer"""
172 |         logger.warning("add_layer not yet implemented for IPC backend")
173 |         raise NotImplementedError("Coming in Week 2-3 migration")
174 | 
175 |     def list_components(self) -> List[Dict[str, Any]]:
176 |         """List components"""
177 |         logger.warning("list_components not yet implemented for IPC backend")
178 |         raise NotImplementedError("Coming in Week 2-3 migration")
179 | 
180 |     def place_component(
181 |         self,
182 |         reference: str,
183 |         footprint: str,
184 |         x: float,
185 |         y: float,
186 |         rotation: float = 0,
187 |         layer: str = "F.Cu"
188 |     ) -> bool:
189 |         """Place component"""
190 |         logger.warning("place_component not yet implemented for IPC backend")
191 |         raise NotImplementedError("Coming in Week 2-3 migration")
192 | 
193 | 
194 | # Note: Full implementation will be completed during Week 2-3 migration
195 | # This is a skeleton to establish the pattern
196 | 
```
--------------------------------------------------------------------------------
/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/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 |                         "isActive": layer_id == self.board.GetActiveLayer()
156 |                     })
157 | 
158 |             return {
159 |                 "success": True,
160 |                 "layers": layers
161 |             }
162 | 
163 |         except Exception as e:
164 |             logger.error(f"Error getting layer list: {str(e)}")
165 |             return {
166 |                 "success": False,
167 |                 "message": "Failed to get layer list",
168 |                 "errorDetails": str(e)
169 |             }
170 |     
171 |     def _get_layer_type(self, type_name: str) -> int:
172 |         """Convert layer type name to KiCAD layer type constant"""
173 |         type_map = {
174 |             "copper": pcbnew.LT_SIGNAL,
175 |             "technical": pcbnew.LT_SIGNAL,
176 |             "user": pcbnew.LT_USER,
177 |             "signal": pcbnew.LT_SIGNAL
178 |         }
179 |         return type_map.get(type_name.lower(), pcbnew.LT_SIGNAL)
180 | 
181 |     def _get_layer_type_name(self, type_id: int) -> str:
182 |         """Convert KiCAD layer type constant to name"""
183 |         type_map = {
184 |             pcbnew.LT_SIGNAL: "signal",
185 |             pcbnew.LT_POWER: "power",
186 |             pcbnew.LT_MIXED: "mixed",
187 |             pcbnew.LT_JUMPER: "jumper",
188 |             pcbnew.LT_USER: "user"
189 |         }
190 |         return type_map.get(type_id, "unknown")
191 | 
```
--------------------------------------------------------------------------------
/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/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/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 |             project_name = params.get("projectName", "New_Project")
 23 |             path = params.get("path", os.getcwd())
 24 |             template = params.get("template")
 25 | 
 26 |             # Generate the full project path
 27 |             project_path = os.path.join(path, project_name)
 28 |             if not project_path.endswith(".kicad_pro"):
 29 |                 project_path += ".kicad_pro"
 30 | 
 31 |             # Create project directory if it doesn't exist
 32 |             os.makedirs(os.path.dirname(project_path), exist_ok=True)
 33 | 
 34 |             # Create a new board
 35 |             board = pcbnew.BOARD()
 36 |             
 37 |             # Set project properties
 38 |             board.GetTitleBlock().SetTitle(project_name)
 39 |             
 40 |             # Set current date with proper parameter
 41 |             from datetime import datetime
 42 |             current_date = datetime.now().strftime("%Y-%m-%d")
 43 |             board.GetTitleBlock().SetDate(current_date)
 44 | 
 45 |             # If template is specified, try to load it
 46 |             if template:
 47 |                 template_path = os.path.expanduser(template)
 48 |                 if os.path.exists(template_path):
 49 |                     template_board = pcbnew.LoadBoard(template_path)
 50 |                     # Copy settings from template
 51 |                     board.SetDesignSettings(template_board.GetDesignSettings())
 52 |                     board.SetLayerStack(template_board.GetLayerStack())
 53 | 
 54 |             # Save the board
 55 |             board_path = project_path.replace(".kicad_pro", ".kicad_pcb")
 56 |             board.SetFileName(board_path)
 57 |             pcbnew.SaveBoard(board_path, board)
 58 | 
 59 |             # Create project file
 60 |             with open(project_path, 'w') as f:
 61 |                 f.write('{\n')
 62 |                 f.write('  "board": {\n')
 63 |                 f.write(f'    "filename": "{os.path.basename(board_path)}"\n')
 64 |                 f.write('  }\n')
 65 |                 f.write('}\n')
 66 | 
 67 |             self.board = board
 68 | 
 69 |             return {
 70 |                 "success": True,
 71 |                 "message": f"Created project: {project_name}",
 72 |                 "project": {
 73 |                     "name": project_name,
 74 |                     "path": project_path,
 75 |                     "boardPath": board_path
 76 |                 }
 77 |             }
 78 | 
 79 |         except Exception as e:
 80 |             logger.error(f"Error creating project: {str(e)}")
 81 |             return {
 82 |                 "success": False,
 83 |                 "message": "Failed to create project",
 84 |                 "errorDetails": str(e)
 85 |             }
 86 | 
 87 |     def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
 88 |         """Open an existing KiCAD project"""
 89 |         try:
 90 |             filename = params.get("filename")
 91 |             if not filename:
 92 |                 return {
 93 |                     "success": False,
 94 |                     "message": "No filename provided",
 95 |                     "errorDetails": "The filename parameter is required"
 96 |                 }
 97 | 
 98 |             # Expand user path and make absolute
 99 |             filename = os.path.abspath(os.path.expanduser(filename))
100 | 
101 |             # If it's a project file, get the board file
102 |             if filename.endswith(".kicad_pro"):
103 |                 board_path = filename.replace(".kicad_pro", ".kicad_pcb")
104 |             else:
105 |                 board_path = filename
106 | 
107 |             # Load the board
108 |             board = pcbnew.LoadBoard(board_path)
109 |             self.board = board
110 | 
111 |             return {
112 |                 "success": True,
113 |                 "message": f"Opened project: {os.path.basename(board_path)}",
114 |                 "project": {
115 |                     "name": os.path.splitext(os.path.basename(board_path))[0],
116 |                     "path": filename,
117 |                     "boardPath": board_path
118 |                 }
119 |             }
120 | 
121 |         except Exception as e:
122 |             logger.error(f"Error opening project: {str(e)}")
123 |             return {
124 |                 "success": False,
125 |                 "message": "Failed to open project",
126 |                 "errorDetails": str(e)
127 |             }
128 | 
129 |     def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
130 |         """Save the current KiCAD project"""
131 |         try:
132 |             if not self.board:
133 |                 return {
134 |                     "success": False,
135 |                     "message": "No board is loaded",
136 |                     "errorDetails": "Load or create a board first"
137 |                 }
138 | 
139 |             filename = params.get("filename")
140 |             if filename:
141 |                 # Save to new location
142 |                 filename = os.path.abspath(os.path.expanduser(filename))
143 |                 self.board.SetFileName(filename)
144 | 
145 |             # Save the board
146 |             pcbnew.SaveBoard(self.board.GetFileName(), self.board)
147 | 
148 |             return {
149 |                 "success": True,
150 |                 "message": f"Saved project to: {self.board.GetFileName()}",
151 |                 "project": {
152 |                     "name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
153 |                     "path": self.board.GetFileName()
154 |                 }
155 |             }
156 | 
157 |         except Exception as e:
158 |             logger.error(f"Error saving project: {str(e)}")
159 |             return {
160 |                 "success": False,
161 |                 "message": "Failed to save project",
162 |                 "errorDetails": str(e)
163 |             }
164 | 
165 |     def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
166 |         """Get information about the current project"""
167 |         try:
168 |             if not self.board:
169 |                 return {
170 |                     "success": False,
171 |                     "message": "No board is loaded",
172 |                     "errorDetails": "Load or create a board first"
173 |                 }
174 | 
175 |             title_block = self.board.GetTitleBlock()
176 |             filename = self.board.GetFileName()
177 |             
178 |             return {
179 |                 "success": True,
180 |                 "project": {
181 |                     "name": os.path.splitext(os.path.basename(filename))[0],
182 |                     "path": filename,
183 |                     "title": title_block.GetTitle(),
184 |                     "date": title_block.GetDate(),
185 |                     "revision": title_block.GetRevision(),
186 |                     "company": title_block.GetCompany(),
187 |                     "comment1": title_block.GetComment(0),
188 |                     "comment2": title_block.GetComment(1),
189 |                     "comment3": title_block.GetComment(2),
190 |                     "comment4": title_block.GetComment(3)
191 |                 }
192 |             }
193 | 
194 |         except Exception as e:
195 |             logger.error(f"Error getting project info: {str(e)}")
196 |             return {
197 |                 "success": False,
198 |                 "message": "Failed to get project information",
199 |                 "errorDetails": str(e)
200 |             }
201 | 
```
--------------------------------------------------------------------------------
/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 | 
```