This is page 1 of 3. 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
├── 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 |
```
--------------------------------------------------------------------------------
/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 |
```
--------------------------------------------------------------------------------
/src/prompts/component.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Component prompts for KiCAD MCP server
3 | *
4 | * These prompts guide the LLM in providing assistance with component-related tasks
5 | * in KiCAD PCB design.
6 | */
7 |
8 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9 | import { z } from 'zod';
10 | import { logger } from '../logger.js';
11 |
12 | /**
13 | * Register component prompts with the MCP server
14 | *
15 | * @param server MCP server instance
16 | */
17 | export function registerComponentPrompts(server: McpServer): void {
18 | logger.info('Registering component prompts');
19 |
20 | // ------------------------------------------------------
21 | // Component Selection Prompt
22 | // ------------------------------------------------------
23 | server.prompt(
24 | "component_selection",
25 | {
26 | requirements: z.string().describe("Description of the circuit requirements and constraints")
27 | },
28 | () => ({
29 | messages: [
30 | {
31 | role: "user",
32 | content: {
33 | type: "text",
34 | text: `You're helping to select components for a circuit design. Given the following requirements:
35 |
36 | {{requirements}}
37 |
38 | Suggest appropriate components with their values, ratings, and footprints. Consider factors like:
39 | - Power and voltage ratings
40 | - Current handling capabilities
41 | - Tolerance requirements
42 | - Physical size constraints and package types
43 | - Availability and cost considerations
44 | - Thermal characteristics
45 | - Performance specifications
46 |
47 | For each component type, recommend specific values and provide a brief explanation of your recommendation. If appropriate, suggest alternatives with different trade-offs.`
48 | }
49 | }
50 | ]
51 | })
52 | );
53 |
54 | // ------------------------------------------------------
55 | // Component Placement Strategy Prompt
56 | // ------------------------------------------------------
57 | server.prompt(
58 | "component_placement_strategy",
59 | {
60 | components: z.string().describe("List of components to be placed on the PCB")
61 | },
62 | () => ({
63 | messages: [
64 | {
65 | role: "user",
66 | content: {
67 | type: "text",
68 | text: `You're helping with component placement for a PCB layout. Here are the components to place:
69 |
70 | {{components}}
71 |
72 | Provide a strategy for optimal placement considering:
73 |
74 | 1. Signal Integrity:
75 | - Group related components to minimize signal path length
76 | - Keep sensitive signals away from noisy components
77 | - Consider appropriate placement for bypass/decoupling capacitors
78 |
79 | 2. Thermal Management:
80 | - Distribute heat-generating components
81 | - Ensure adequate spacing for cooling
82 | - Placement near heat sinks or vias for thermal dissipation
83 |
84 | 3. EMI/EMC Concerns:
85 | - Separate digital and analog sections
86 | - Consider ground plane partitioning
87 | - Shield sensitive components
88 |
89 | 4. Manufacturing and Assembly:
90 | - Component orientation for automated assembly
91 | - Adequate spacing for rework
92 | - Consider component height distribution
93 |
94 | Group components functionally and suggest a logical arrangement. If possible, provide a rough sketch or description of component zones.`
95 | }
96 | }
97 | ]
98 | })
99 | );
100 |
101 | // ------------------------------------------------------
102 | // Component Replacement Analysis Prompt
103 | // ------------------------------------------------------
104 | server.prompt(
105 | "component_replacement_analysis",
106 | {
107 | component_info: z.string().describe("Information about the component that needs to be replaced")
108 | },
109 | () => ({
110 | messages: [
111 | {
112 | role: "user",
113 | content: {
114 | type: "text",
115 | text: `You're helping to find a replacement for a component that is unavailable or needs to be updated. Here's the original component information:
116 |
117 | {{component_info}}
118 |
119 | Consider these factors when suggesting replacements:
120 |
121 | 1. Electrical Compatibility:
122 | - Match or exceed key electrical specifications
123 | - Ensure voltage/current/power ratings are compatible
124 | - Consider parametric equivalents
125 |
126 | 2. Physical Compatibility:
127 | - Footprint compatibility or adaptation requirements
128 | - Package differences and mounting considerations
129 | - Size and clearance requirements
130 |
131 | 3. Performance Impact:
132 | - How the replacement might affect circuit performance
133 | - Potential need for circuit adjustments
134 |
135 | 4. Availability and Cost:
136 | - Current market availability
137 | - Cost comparison with original part
138 | - Lead time considerations
139 |
140 | Suggest suitable replacement options and explain the advantages and disadvantages of each. Include any circuit modifications that might be necessary.`
141 | }
142 | }
143 | ]
144 | })
145 | );
146 |
147 | // ------------------------------------------------------
148 | // Component Troubleshooting Prompt
149 | // ------------------------------------------------------
150 | server.prompt(
151 | "component_troubleshooting",
152 | {
153 | issue_description: z.string().describe("Description of the component or circuit issue being troubleshooted")
154 | },
155 | () => ({
156 | messages: [
157 | {
158 | role: "user",
159 | content: {
160 | type: "text",
161 | text: `You're helping to troubleshoot an issue with a component or circuit section in a PCB design. Here's the issue description:
162 |
163 | {{issue_description}}
164 |
165 | Use the following systematic approach to diagnose the problem:
166 |
167 | 1. Component Verification:
168 | - Check component values, footprints, and orientation
169 | - Verify correct part numbers and specifications
170 | - Examine for potential manufacturing defects
171 |
172 | 2. Circuit Analysis:
173 | - Review the schematic for design errors
174 | - Check for proper connections and signal paths
175 | - Verify power and ground connections
176 |
177 | 3. Layout Review:
178 | - Examine component placement and orientation
179 | - Check for adequate clearances
180 | - Review trace routing and potential interference
181 |
182 | 4. Environmental Factors:
183 | - Consider temperature, humidity, and other environmental impacts
184 | - Check for potential EMI/RFI issues
185 | - Review mechanical stress or vibration effects
186 |
187 | Based on the available information, suggest likely causes of the issue and recommend specific steps to diagnose and resolve the problem.`
188 | }
189 | }
190 | ]
191 | })
192 | );
193 |
194 | // ------------------------------------------------------
195 | // Component Value Calculation Prompt
196 | // ------------------------------------------------------
197 | server.prompt(
198 | "component_value_calculation",
199 | {
200 | circuit_requirements: z.string().describe("Description of the circuit function and performance requirements")
201 | },
202 | () => ({
203 | messages: [
204 | {
205 | role: "user",
206 | content: {
207 | type: "text",
208 | text: `You're helping to calculate appropriate component values for a specific circuit function. Here's the circuit description and requirements:
209 |
210 | {{circuit_requirements}}
211 |
212 | Follow these steps to determine the optimal component values:
213 |
214 | 1. Identify the relevant circuit equations and design formulas
215 | 2. Consider the design constraints and performance requirements
216 | 3. Calculate initial component values based on ideal behavior
217 | 4. Adjust for real-world factors:
218 | - Component tolerances
219 | - Temperature coefficients
220 | - Parasitic effects
221 | - Available standard values
222 |
223 | Present your calculations step-by-step, showing your work and explaining your reasoning. Recommend specific component values, explaining why they're appropriate for this application. If there are multiple valid approaches, discuss the trade-offs between them.`
224 | }
225 | }
226 | ]
227 | })
228 | );
229 |
230 | logger.info('Component prompts registered');
231 | }
232 |
```