merge: upstream/main (57 commits) — preserve PR #102 net label fix

Merged upstream/main into our fork. Conflict in connection_schematic.py
resolved by taking upstream's file and re-applying our fix:
- all_match_points = connected_wire_points | label positions
- Allows nets where labels are placed directly at pin endpoints (no wire)

Upstream changes include: security fixes (8 vulns), new schematic tools
(get_net_at_point, find_orphaned_wires, snap_to_grid, get_wire_connections),
generate_netlist rewrite via kicad-cli, wire preservation on component move,
schematic analysis tools, KiCad 10 support.
This commit is contained in:
ffindog
2026-04-15 22:27:41 +10:00
159 changed files with 39178 additions and 23947 deletions

24
.flake8 Normal file
View File

@@ -0,0 +1,24 @@
[flake8]
max-line-length = 100
extend-ignore =
E203,
W503,
E501,
F401,
F541,
F841,
F811,
F821,
F824,
E722,
E402,
E741,
E231
exclude =
.git,
__pycache__,
dist,
build,
.eggs,
.venv,
node_modules

View File

@@ -1,194 +1,194 @@
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
# TypeScript/Node.js tests
typescript-tests:
name: TypeScript Build & Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest]
node-version: [18.x, 20.x, 22.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run TypeScript compiler
run: npm run build
- name: Run linter
run: npm run lint || echo "Linter not configured yet"
- name: Run tests
run: npm test || echo "Tests not configured yet"
# Python tests
python-tests:
name: Python Tests
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-22.04]
python-version: ['3.10', '3.11', '3.12']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov black mypy pylint
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- name: Run Black formatter check
run: black --check python/ || echo "Black not configured yet"
- name: Run MyPy type checker
run: mypy python/ || echo "MyPy not configured yet"
- name: Run Pylint
run: pylint python/ || echo "Pylint not configured yet"
- name: Run pytest
run: pytest python/ --cov=python --cov-report=xml || echo "Tests not configured yet"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: python
name: python-${{ matrix.python-version }}
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04'
# Integration tests (requires KiCAD)
integration-tests:
name: Integration Tests (Linux + KiCAD)
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Add KiCAD PPA and Install KiCAD 9.0
run: |
sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
sudo apt-get update
sudo apt-get install -y kicad kicad-libraries
- name: Verify KiCAD installation
run: |
kicad-cli version || echo "kicad-cli not found"
python3 -c "import pcbnew; print(f'pcbnew version: {pcbnew.GetBuildVersion()}')" || echo "pcbnew module not found"
- name: Install dependencies
run: |
npm ci
pip install -r requirements.txt
- name: Build TypeScript
run: npm run build
- name: Run integration tests
run: |
echo "Integration tests not yet configured"
# pytest tests/integration/
# Docker build test
docker-build:
name: Docker Build Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
run: |
echo "Docker build not yet configured"
# docker build -t kicad-mcp-server:test .
# Code quality checks
code-quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npx eslint src/ || echo "ESLint not configured yet"
- name: Run Prettier check
run: npx prettier --check "src/**/*.ts" || echo "Prettier not configured yet"
- name: Check for security vulnerabilities
run: npm audit --audit-level=moderate || echo "No critical vulnerabilities"
# Documentation check
docs-check:
name: Documentation Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check README exists
run: test -f README.md
- name: Check for broken links in docs
run: |
sudo apt-get install -y linkchecker || true
# linkchecker docs/ || echo "Link checker not configured"
- name: Validate JSON files
run: |
find . -name "*.json" -not -path "./node_modules/*" -not -path "./dist/*" | xargs -I {} sh -c 'python3 -m json.tool {} > /dev/null && echo "✓ {}" || echo "✗ {}"'
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# TypeScript/Node.js tests
typescript-tests:
name: TypeScript Build & Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest]
node-version: [18.x, 20.x, 22.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run TypeScript compiler
run: npm run build
- name: Run linter
run: npm run lint || echo "Linter not configured yet"
- name: Run tests
run: npm test || echo "Tests not configured yet"
# Python tests
python-tests:
name: Python Tests
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-22.04]
python-version: ["3.10", "3.11", "3.12"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov black mypy pylint
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- name: Run Black formatter check
run: black --check python/ || echo "Black not configured yet"
- name: Run MyPy type checker
run: mypy python/ || echo "MyPy not configured yet"
- name: Run Pylint
run: pylint python/ || echo "Pylint not configured yet"
- name: Run pytest
run: pytest python/ --cov=python --cov-report=xml || echo "Tests not configured yet"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: python
name: python-${{ matrix.python-version }}
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04'
# Integration tests (requires KiCAD)
integration-tests:
name: Integration Tests (Linux + KiCAD)
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "npm"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Add KiCAD PPA and Install KiCAD 9.0
run: |
sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
sudo apt-get update
sudo apt-get install -y kicad kicad-libraries
- name: Verify KiCAD installation
run: |
kicad-cli version || echo "kicad-cli not found"
python3 -c "import pcbnew; print(f'pcbnew version: {pcbnew.GetBuildVersion()}')" || echo "pcbnew module not found"
- name: Install dependencies
run: |
npm ci
pip install -r requirements.txt
- name: Build TypeScript
run: npm run build
- name: Run integration tests
run: |
echo "Integration tests not yet configured"
# pytest tests/integration/
# Docker build test
docker-build:
name: Docker Build Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
run: |
echo "Docker build not yet configured"
# docker build -t kicad-mcp-server:test .
# Code quality checks
code-quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npx eslint src/ || echo "ESLint not configured yet"
- name: Run Prettier check
run: npx prettier --check "src/**/*.ts" || echo "Prettier not configured yet"
- name: Check for security vulnerabilities
run: npm audit --audit-level=moderate || echo "No critical vulnerabilities"
# Documentation check
docs-check:
name: Documentation Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check README exists
run: test -f README.md
- name: Check for broken links in docs
run: |
sudo apt-get install -y linkchecker || true
# linkchecker docs/ || echo "Link checker not configured"
- name: Validate JSON files
run: |
find . -name "*.json" -not -path "./node_modules/*" -not -path "./dist/*" | xargs -I {} sh -c 'python3 -m json.tool {} > /dev/null && echo "✓ {}" || echo "✗ {}"'

3
.gitignore vendored
View File

@@ -86,5 +86,8 @@ data/
Thumbs.db
Desktop.ini
# Generated local config files (contain machine-specific paths)
windows-mcp-config.json
# Personal notes / local contributions (not for upstream)
myContribution/

View File

@@ -1,71 +1,60 @@
# Pre-commit hooks configuration
# See https://pre-commit.com for more information
repos:
# Python code formatting
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
language_version: python3
files: ^python/
# Python import sorting
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
files: ^python/
args: ["--profile", "black"]
# Python type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.0
hooks:
- id: mypy
files: ^python/
args: [--ignore-missing-imports]
# Python linting
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
files: ^python/
args: [--max-line-length=100, --extend-ignore=E203]
# TypeScript/JavaScript formatting
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
- id: prettier
types_or: [javascript, typescript, json, yaml, markdown]
files: \.(ts|js|json|ya?ml|md)$
# General file checks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
- id: detect-private-key
# Python security checks
- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: [-c, pyproject.toml]
files: ^python/
# Markdown linting
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.37.0
hooks:
- id: markdownlint
args: [--fix]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ["--maxkb=500"]
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 26.3.1
hooks:
- id: black
language_version: python3
args: [--config=pyproject.toml]
- repo: https://github.com/pycqa/isort
rev: 8.0.1
hooks:
- id: isort
args: [--settings-path=pyproject.toml]
- repo: local
hooks:
- id: prettier
name: prettier
entry: npx prettier --write --ignore-unknown
language: node
types_or: [javascript, ts, json, yaml, markdown]
exclude: ^(dist/|package-lock\.json|\.venv/|python/)
- repo: https://github.com/pycqa/flake8
rev: "7.3.0"
hooks:
- id: flake8
files: ^python/
args: [--config=.flake8]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.19.1"
hooks:
- id: mypy
files: ^python/
exclude: ^python/commands/board\.py$
args: [--config-file=pyproject.toml]
additional_dependencies:
- types-requests
- pytest
- repo: local
hooks:
- id: eslint
name: eslint
entry: npx eslint --fix
language: node
types: [ts]
files: ^src/

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
coverage.xml
htmlcov/
data/
*.kicad_*

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2
}

File diff suppressed because it is too large Load Diff

60
CLAUDE.md Normal file
View File

@@ -0,0 +1,60 @@
# KiCAD MCP Server
## Related Source
- KiCad source code is located at `../kicad-source/` (absolute: `/home/eugene/Projects/kicad-source/`)
## Testing
### When to Write Tests
Write tests for every non-trivial change to Python handler or business logic code:
- **New MCP tools** — add tests for schema validation, handler dispatch, parameter validation, and the core logic path (happy path + key error cases).
- **Changes to existing tools** — add tests covering the changed behaviour; update any tests that no longer reflect reality.
- **Bug fixes** — add a regression test that would have caught the bug before adding the fix.
- **Refactors that delete or rename public methods** — add a test asserting the old name no longer exists (see `TestConnectionManagerOrphanedMethodsRemoved` for the pattern).
You do **not** need tests for TypeScript/TS-layer glue code that only forwards calls to Python (the TS test runner is not yet configured).
### Test Levels
| Level | Use for | Marker |
| ----------- | -------------------------------------------------------------------------------------------------- | -------------------------- |
| Unit | Schema shape, parameter validation, pure logic, mock-heavy handler dispatch | `@pytest.mark.unit` |
| Integration | Real file I/O against a `.kicad_sch` / `.kicad_pcb` copy; WireManager, JunctionManager round-trips | `@pytest.mark.integration` |
Keep unit tests free of file I/O. Keep integration tests free of business-logic assertions that belong in unit tests.
### Where to Put Tests
```
tests/
test_<feature_name>.py # all Python tests go here
```
Group related test classes inside a single file (e.g. `TestSchemas`, `TestHandlerDispatch`, `TestHandleAddSchematicWireRouting` all in `test_wire_junction_changes.py`). Name classes `Test<Area>` and methods `test_<what_is_verified>`.
Use `python/templates/empty.kicad_sch` as the base fixture for integration tests — copy it to a `tempfile` directory, run the handler, then parse the result with `sexpdata`.
### Running Tests
Always use the `.venv` virtualenv for Python commands:
```bash
npm run test:py # pytest tests/ -v
.venv/bin/pytest tests/ -v # all Python tests
.venv/bin/pytest -m unit # unit tests only
.venv/bin/pytest -m integration # integration tests only
.venv/bin/pytest --cov=python # with coverage report
.venv/bin/mypy python/ # type checking
```
## Git Workflow
- **Never open a pull request automatically.** Commit and push when asked, but always wait for explicit instructions before running `gh pr create` or any equivalent command.
## Python Code Style
- **Never use `assert` in production code** — raise a specific exception (`ValueError`, `RuntimeError`, etc.) instead. `assert` is stripped in optimised builds and gives poor error messages.
- **Do not introduce logic-breaking workarounds to satisfy the type checker** (e.g. `x or ""` when `""` is not a valid substitute for `None`). Fix the types or narrow with a proper guard (`if x is None: raise ...`).

View File

@@ -1,397 +1,437 @@
# Contributing to KiCAD MCP Server
Thank you for your interest in contributing to the KiCAD MCP Server! This guide will help you get started with development.
## Table of Contents
- [Development Environment Setup](#development-environment-setup)
- [Project Structure](#project-structure)
- [Development Workflow](#development-workflow)
- [Testing](#testing)
- [Code Style](#code-style)
- [Pull Request Process](#pull-request-process)
- [Roadmap & Planning](#roadmap--planning)
---
## Development Environment Setup
### Prerequisites
- **KiCAD 9.0 or higher** - [Download here](https://www.kicad.org/download/)
- **Node.js v18+** - [Download here](https://nodejs.org/)
- **Python 3.10+** - Should come with KiCAD, or install separately
- **Git** - For version control
### Platform-Specific Setup
#### Linux (Ubuntu/Debian)
```bash
# Install KiCAD 9.0 from official PPA
sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
sudo apt-get update
sudo apt-get install -y kicad kicad-libraries
# Install Node.js (if not already installed)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Clone the repository
git clone https://github.com/yourusername/kicad-mcp-server.git
cd kicad-mcp-server
# Install Node.js dependencies
npm install
# Install Python dependencies
pip3 install -r requirements-dev.txt
# Build TypeScript
npm run build
# Run tests
npm test
pytest
```
#### Windows
```powershell
# Install KiCAD 9.0 from https://www.kicad.org/download/windows/
# Install Node.js from https://nodejs.org/
# Clone the repository
git clone https://github.com/yourusername/kicad-mcp-server.git
cd kicad-mcp-server
# Install Node.js dependencies
npm install
# Install Python dependencies
pip install -r requirements-dev.txt
# Build TypeScript
npm run build
# Run tests
npm test
pytest
```
#### macOS
```bash
# Install KiCAD 9.0 from https://www.kicad.org/download/macos/
# Install Node.js via Homebrew
brew install node
# Clone the repository
git clone https://github.com/yourusername/kicad-mcp-server.git
cd kicad-mcp-server
# Install Node.js dependencies
npm install
# Install Python dependencies
pip3 install -r requirements-dev.txt
# Build TypeScript
npm run build
# Run tests
npm test
pytest
```
---
## Project Structure
```
kicad-mcp-server/
├── .github/
│ └── workflows/ # CI/CD pipelines
├── config/ # Configuration examples
│ ├── linux-config.example.json
│ ├── windows-config.example.json
│ └── macos-config.example.json
├── docs/ # Documentation
├── python/ # Python interface layer
│ ├── commands/ # KiCAD command handlers
│ ├── integrations/ # External API integrations (JLCPCB, Digikey)
│ ├── utils/ # Utility modules
│ └── kicad_interface.py # Main Python entry point
├── src/ # TypeScript MCP server
│ ├── tools/ # MCP tool implementations
│ ├── resources/ # MCP resource implementations
│ ├── prompts/ # MCP prompt implementations
│ └── server.ts # Main server
├── tests/ # Test suite
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── dist/ # Compiled JavaScript (generated)
├── node_modules/ # Node dependencies (generated)
├── package.json # Node.js configuration
├── tsconfig.json # TypeScript configuration
├── pytest.ini # Pytest configuration
├── requirements.txt # Python production dependencies
── requirements-dev.txt # Python dev dependencies
```
---
## Development Workflow
### 1. Create a Feature Branch
```bash
git checkout -b feature/your-feature-name
```
### 2. Make Changes
- Edit TypeScript files in `src/`
- Edit Python files in `python/`
- Add tests for new features
### 3. Build & Test
```bash
# Build TypeScript
npm run build
# Run TypeScript linter
npm run lint
# Run Python formatter
black python/
# Run Python type checker
mypy python/
# Run all tests
npm test
pytest
# Run specific test file
pytest tests/test_platform_helper.py -v
# Run with coverage
pytest --cov=python --cov-report=html
```
### 4. Commit Changes
```bash
git add .
git commit -m "feat: Add your feature description"
```
**Commit Message Convention:**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `test:` - Adding/updating tests
- `refactor:` - Code refactoring
- `chore:` - Maintenance tasks
### 5. Push and Create Pull Request
```bash
git push origin feature/your-feature-name
```
Then create a Pull Request on GitHub.
---
## Testing
### Running Tests
```bash
# All tests
pytest
# Unit tests only
pytest -m unit
# Integration tests (requires KiCAD installed)
pytest -m integration
# Platform-specific tests
pytest -m linux # Linux tests only
pytest -m windows # Windows tests only
# With coverage report
pytest --cov=python --cov-report=term-missing
# Verbose output
pytest -v
# Stop on first failure
pytest -x
```
### Writing Tests
Tests should be placed in `tests/` directory:
```python
# tests/test_my_feature.py
import pytest
@pytest.mark.unit
def test_my_feature():
"""Test description"""
# Arrange
expected = "result"
# Act
result = my_function()
# Assert
assert result == expected
@pytest.mark.integration
@pytest.mark.linux
def test_linux_integration():
"""Integration test for Linux"""
# This test will only run on Linux in CI
pass
```
---
## Code Style
### Python
We use **Black** for code formatting and **MyPy** for type checking.
```bash
# Format all Python files
black python/
# Check types
mypy python/
# Run linter
pylint python/
```
**Python Style Guidelines:**
- Use type hints for all function signatures
- Use pathlib.Path for file paths (not os.path)
- Use descriptive variable names
- Add docstrings to all public functions/classes
- Follow PEP 8
**Example:**
```python
from pathlib import Path
from typing import List, Optional
def find_kicad_libraries(search_path: Path) -> List[Path]:
"""
Find all KiCAD symbol libraries in the given path.
Args:
search_path: Directory to search for .kicad_sym files
Returns:
List of paths to found library files
Raises:
ValueError: If search_path doesn't exist
"""
if not search_path.exists():
raise ValueError(f"Search path does not exist: {search_path}")
return list(search_path.glob("**/*.kicad_sym"))
```
### TypeScript
We use **ESLint** and **Prettier** for TypeScript.
```bash
# Format TypeScript files
npx prettier --write "src/**/*.ts"
# Run linter
npx eslint src/
```
**TypeScript Style Guidelines:**
- Use interfaces for data structures
- Use async/await for asynchronous code
- Use descriptive variable names
- Add JSDoc comments to exported functions
---
## Pull Request Process
1. **Update Documentation** - If you change functionality, update docs
2. **Add Tests** - All new features should have tests
3. **Run CI Locally** - Ensure all tests pass before pushing
4. **Create PR** - Use a clear, descriptive title
5. **Request Review** - Tag relevant maintainers
6. **Address Feedback** - Make requested changes
7. **Merge** - Maintainer will merge when approved
### PR Checklist
- [ ] Code follows style guidelines
- [ ] All tests pass locally
- [ ] New tests added for new features
- [ ] Documentation updated
- [ ] Commit messages follow convention
- [ ] No merge conflicts
- [ ] CI/CD pipeline passes
---
## Roadmap & Planning
We track work using GitHub Projects and Issues:
- **GitHub Projects** - High-level roadmap and sprints
- **GitHub Issues** - Specific bugs and features
- **GitHub Discussions** - Design discussions and proposals
### Current Priorities (Week 1-4)
1. ✅ Linux compatibility fixes
2. ✅ Platform-agnostic path handling
3. ✅ CI/CD pipeline setup
4. 🔄 Migrate to KiCAD IPC API
5. Add JLCPCB integration
6. ⏳ Add Digikey integration
See [docs/REBUILD_PLAN.md](docs/REBUILD_PLAN.md) for the complete 12-week roadmap.
---
## Getting Help
- **GitHub Discussions** - Ask questions, propose ideas
- **GitHub Issues** - Report bugs, request features
- **Discord** - Real-time chat (link TBD)
---
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
---
## Thank You! 🎉
Your contributions make this project better for everyone. We appreciate your time and effort!
# Contributing to KiCAD MCP Server
Thank you for your interest in contributing to the KiCAD MCP Server! This guide will help you get started with development.
## Table of Contents
- [Development Environment Setup](#development-environment-setup)
- [Project Structure](#project-structure)
- [Architecture Overview](#architecture-overview)
- [Development Workflow](#development-workflow)
- [Testing](#testing)
- [Code Style](#code-style)
- [Pull Request Process](#pull-request-process)
- [Roadmap & Planning](#roadmap--planning)
---
## Development Environment Setup
### Prerequisites
- **KiCAD 9.0 or higher** - [Download here](https://www.kicad.org/download/)
- **Node.js v18+** - [Download here](https://nodejs.org/)
- **Python 3.10+** - Should come with KiCAD, or install separately
- **Git** - For version control
### Platform-Specific Setup
#### Linux (Ubuntu/Debian)
```bash
# Install KiCAD 9.0 from official PPA
sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
sudo apt-get update
sudo apt-get install -y kicad kicad-libraries
# Install Node.js (if not already installed)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Clone the repository
git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
cd kicad-mcp-server
# Install Node.js dependencies
npm install
# Install Python dependencies
pip3 install -r requirements-dev.txt
# Build TypeScript
npm run build
# Run tests
npm test
pytest
```
#### Windows
```powershell
# Install KiCAD 9.0 from https://www.kicad.org/download/windows/
# Install Node.js from https://nodejs.org/
# Clone the repository
git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
cd kicad-mcp-server
# Install Node.js dependencies
npm install
# Install Python dependencies
pip install -r requirements-dev.txt
# Build TypeScript
npm run build
# Run tests
npm test
pytest
```
#### macOS
```bash
# Install KiCAD 9.0 from https://www.kicad.org/download/macos/
# Install Node.js via Homebrew
brew install node
# Clone the repository
git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
cd kicad-mcp-server
# Install Node.js dependencies
npm install
# Install Python dependencies
pip3 install -r requirements-dev.txt
# Build TypeScript
npm run build
# Run tests
npm test
pytest
```
### Pre-commit Hooks
This project uses [pre-commit](https://pre-commit.com/) to run linters and formatters automatically before each commit. Pre-commit hooks prevent noisy formatting diffs caused by different IDE configurations across contributors, catch common mistakes and type errors before they reach code review, and ensure every commit in the repository meets the same quality baseline — so reviewers can focus on logic and design rather than style issues.
**All contributors must install pre-commit hooks after cloning the repo:**
```bash
# Install pre-commit (if not already installed)
pip install pre-commit
# Install the git hooks
pre-commit install
# (Optional) Run against all files to verify setup
pre-commit run --all-files
```
> **Important:** Do not use `git commit --no-verify` to bypass pre-commit hooks. The hooks enforce code quality checks (Black, isort, Prettier, flake8, mypy, ESLint) that must pass before code is merged. If a hook fails, fix the underlying issue rather than skipping the check.
---
## Project Structure
```
kicad-mcp-server/
├── .github/
│ └── workflows/ # CI/CD pipelines
├── config/ # Configuration examples
│ ├── linux-config.example.json
│ ├── windows-config.example.json
│ └── macos-config.example.json
├── docs/ # Documentation
── python/ # Python interface layer
│ ├── commands/ # KiCAD command handlers
│ ├── integrations/ # External API integrations (JLCPCB, Digikey)
│ ├── utils/ # Utility modules
│ └── kicad_interface.py # Main Python entry point
├── src/ # TypeScript MCP server
│ ├── tools/ # MCP tool implementations
│ ├── resources/ # MCP resource implementations
│ ├── prompts/ # MCP prompt implementations
│ └── server.ts # Main server
├── tests/ # Test suite
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── dist/ # Compiled JavaScript (generated)
├── node_modules/ # Node dependencies (generated)
├── package.json # Node.js configuration
├── tsconfig.json # TypeScript configuration
├── pytest.ini # Pytest configuration
├── requirements.txt # Python production dependencies
└── requirements-dev.txt # Python dev dependencies
```
---
## Architecture Overview
The KiCAD MCP Server is organized into several key components:
- **TypeScript MCP Server** (`src/`) - Handles MCP protocol communication and tool routing
- **Python KiCAD Interface** (`python/`) - Interfaces with KiCAD's Python API (pcbnew)
- **Tool Router** - Organizes 122+ tools into 8 discoverable categories
- **Resource System** - Provides dynamic project/board state information
- **Prompt System** - Offers context-aware design prompts
**Current Tool Count:** 122+ tools across 8 categories (direct + routed)
For detailed architecture information, see `docs/ROUTER_ARCHITECTURE.md`.
---
## Development Workflow
### 1. Create a Feature Branch
```bash
git checkout -b feature/your-feature-name
```
### 2. Make Changes
- Edit TypeScript files in `src/`
- Edit Python files in `python/`
- Add tests for new features
### 3. Build & Test
```bash
# Build TypeScript
npm run build
# Run TypeScript linter
npm run lint
# Run Python formatter
black python/
# Run Python type checker
mypy python/
# Run all tests
npm test
pytest
# Run specific test file
pytest tests/test_platform_helper.py -v
# Run with coverage
pytest --cov=python --cov-report=html
```
### 4. Commit Changes
```bash
git add .
git commit -m "feat: Add your feature description"
```
**Commit Message Convention:**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `test:` - Adding/updating tests
- `refactor:` - Code refactoring
- `chore:` - Maintenance tasks
### 5. Push and Create Pull Request
```bash
git push origin feature/your-feature-name
```
Then create a Pull Request on GitHub.
---
## Testing
### Running Tests
```bash
# All tests
pytest
# Unit tests only
pytest -m unit
# Integration tests (requires KiCAD installed)
pytest -m integration
# Platform-specific tests
pytest -m linux # Linux tests only
pytest -m windows # Windows tests only
# With coverage report
pytest --cov=python --cov-report=term-missing
# Verbose output
pytest -v
# Stop on first failure
pytest -x
```
### Writing Tests
Tests should be placed in `tests/` directory:
```python
# tests/test_my_feature.py
import pytest
@pytest.mark.unit
def test_my_feature():
"""Test description"""
# Arrange
expected = "result"
# Act
result = my_function()
# Assert
assert result == expected
@pytest.mark.integration
@pytest.mark.linux
def test_linux_integration():
"""Integration test for Linux"""
# This test will only run on Linux in CI
pass
```
---
## Code Style
### Python
We use **Black** for code formatting and **MyPy** for type checking.
```bash
# Format all Python files
black python/
# Check types
mypy python/
# Run linter
pylint python/
```
**Python Style Guidelines:**
- Use type hints for all function signatures
- Use pathlib.Path for file paths (not os.path)
- Use descriptive variable names
- Add docstrings to all public functions/classes
- Follow PEP 8
**Example:**
```python
from pathlib import Path
from typing import List, Optional
def find_kicad_libraries(search_path: Path) -> List[Path]:
"""
Find all KiCAD symbol libraries in the given path.
Args:
search_path: Directory to search for .kicad_sym files
Returns:
List of paths to found library files
Raises:
ValueError: If search_path doesn't exist
"""
if not search_path.exists():
raise ValueError(f"Search path does not exist: {search_path}")
return list(search_path.glob("**/*.kicad_sym"))
```
### TypeScript
We use **ESLint** and **Prettier** for TypeScript.
```bash
# Format TypeScript files
npx prettier --write "src/**/*.ts"
# Run linter
npx eslint src/
```
**TypeScript Style Guidelines:**
- Use interfaces for data structures
- Use async/await for asynchronous code
- Use descriptive variable names
- Add JSDoc comments to exported functions
---
## Pull Request Process
1. **Update Documentation** - If you change functionality, update docs
2. **Add Tests** - All new features should have tests
3. **Run CI Locally** - Ensure all tests pass before pushing
4. **Create PR** - Use a clear, descriptive title
5. **Request Review** - Tag relevant maintainers
6. **Address Feedback** - Make requested changes
7. **Merge** - Maintainer will merge when approved
### PR Checklist
- [ ] Code follows style guidelines
- [ ] All tests pass locally
- [ ] New tests added for new features
- [ ] Documentation updated
- [ ] Commit messages follow convention
- [ ] No merge conflicts
- [ ] CI/CD pipeline passes
---
## Roadmap & Planning
We track work using GitHub Projects and Issues:
- **GitHub Projects** - High-level roadmap and sprints
- **GitHub Issues** - Specific bugs and features
- **GitHub Discussions** - Design discussions and proposals
### Current Priorities (Week 1-4)
1. ✅ Linux compatibility fixes
2. ✅ Platform-agnostic path handling
3. ✅ CI/CD pipeline setup
4. 🔄 Migrate to KiCAD IPC API
5. ⏳ Add JLCPCB integration
6. ⏳ Add Digikey integration
See [docs/REBUILD_PLAN.md](docs/REBUILD_PLAN.md) for the complete 12-week roadmap.
---
## Getting Help
- **GitHub Discussions** - Ask questions, propose ideas
- **GitHub Issues** - Report bugs, request features
- **Discord** - Real-time chat (link TBD)
---
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
---
## Thank You! 🎉
Your contributions make this project better for everyone. We appreciate your time and effort!

2127
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/home/YOUR_USERNAME/MCP/KiCAD-MCP-Server/dist/index.js"],
"env": {
"NODE_ENV": "production",
"PYTHONPATH": "/usr/share/kicad/scripting/plugins:/usr/lib/kicad/lib/python3/dist-packages",
"LOG_LEVEL": "info",
"KICAD_AUTO_LAUNCH": "false"
},
"description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
}
}
}
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/home/YOUR_USERNAME/MCP/KiCAD-MCP-Server/dist/index.js"],
"env": {
"NODE_ENV": "production",
"PYTHONPATH": "/usr/share/kicad/scripting/plugins:/usr/lib/kicad/lib/python3/dist-packages",
"LOG_LEVEL": "info",
"KICAD_AUTO_LAUNCH": "false"
},
"description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
}
}
}

View File

@@ -1,15 +1,15 @@
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/Users/YOUR_USERNAME/MCP/KiCAD-MCP-Server/dist/index.js"],
"env": {
"NODE_ENV": "production",
"PYTHONPATH": "/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current/lib/python3.11/site-packages",
"LOG_LEVEL": "info",
"KICAD_AUTO_LAUNCH": "false"
},
"description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
}
}
}
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/Users/YOUR_USERNAME/MCP/KiCAD-MCP-Server/dist/index.js"],
"env": {
"NODE_ENV": "production",
"PYTHONPATH": "/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current/lib/python3.11/site-packages",
"LOG_LEVEL": "info",
"KICAD_AUTO_LAUNCH": "false"
},
"description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
}
}
}

View File

@@ -1,16 +1,16 @@
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["C:\\Users\\YOUR_USERNAME\\MCP\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"NODE_ENV": "production",
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\bin\\Lib\\site-packages",
"LOG_LEVEL": "info",
"KICAD_AUTO_LAUNCH": "false",
"KICAD_MCP_DEV": "0"
},
"description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
}
}
}
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["C:\\Users\\YOUR_USERNAME\\MCP\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"NODE_ENV": "production",
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\bin\\Lib\\site-packages",
"LOG_LEVEL": "info",
"KICAD_AUTO_LAUNCH": "false",
"KICAD_MCP_DEV": "0"
},
"description": "KiCAD PCB Design Assistant - Note: PYTHONPATH auto-detected if venv exists"
}
}
}

324
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,324 @@
# KiCAD MCP Server Architecture
This document describes the system architecture for contributors who want to understand, modify, or extend the server.
---
## System Overview
```
AI Assistant (Claude, etc.)
|
| MCP Protocol (JSON-RPC 2.0 over STDIO)
v
TypeScript MCP Server (src/)
|
| Spawn Python subprocess, pass JSON commands
v
Python KiCAD Interface (python/)
|
| pcbnew SWIG API or KiCAD IPC API
v
KiCAD 9.0+
```
The server has two layers:
1. **TypeScript layer** -- implements the MCP protocol, registers tools with schemas, validates input, manages the Python subprocess
2. **Python layer** -- interfaces with KiCAD's pcbnew API (SWIG bindings) or IPC API for actual PCB/schematic operations
---
## Directory Structure
```
KiCAD-MCP-Server/
src/ # TypeScript MCP server
server.ts # Main server, tool registration, Python subprocess
logger.ts # Logging configuration
tools/ # Tool definitions (one file per category)
registry.ts # Tool category definitions and lookup
router.ts # Router tools (list/search/execute)
project.ts # Project management tools
board.ts # Board operations tools
component.ts # Component tools
routing.ts # Routing tools
design-rules.ts # DRC tools
export.ts # Export tools
schematic.ts # Schematic tools
library.ts # Footprint library tools
library-symbol.ts # Symbol library tools
footprint.ts # Footprint creator tools
symbol-creator.ts # Symbol creator tools
datasheet.ts # Datasheet tools
jlcpcb-api.ts # JLCPCB integration tools
freerouting.ts # Autorouter tools
ui.ts # UI management tools
resources/ # MCP resource definitions
prompts/ # MCP prompt templates
utils/ # Utility functions
python/ # Python KiCAD interface
kicad_interface.py # Main entry point, command router
commands/ # Command implementations
project.py # Project operations
board.py # Board manipulation
component.py # PCB component operations
component_schematic.py # Schematic component operations
connection_schematic.py # Schematic wiring and connections
schematic.py # Schematic file management
routing.py # Trace routing
design_rules.py # DRC operations
export.py # File export
library.py # Footprint library access
library_symbol.py # Symbol library access
footprint.py # Custom footprint creation
symbol_creator.py # Custom symbol creation
datasheet_manager.py # Datasheet enrichment
jlcpcb.py # JLCPCB API client
jlcsearch.py # JLCSearch public API client
jlcpcb_parts.py # JLCPCB parts database
freerouting.py # Freerouting autorouter
svg_import.py # SVG to PCB polygon conversion
dynamic_symbol_loader.py # Dynamic symbol injection
wire_manager.py # S-expression wire creation
pin_locator.py # Pin position discovery
layers.py # Layer utilities
outline.py # Board outline utilities
size.py # Size/dimension utilities
view.py # Board rendering utilities
kicad_api/ # Backend abstraction
base.py # Abstract base class
factory.py # Backend auto-detection
swig_backend.py # pcbnew SWIG API backend
ipc_backend.py # KiCAD 9.0 IPC API backend
schemas/ # JSON Schema definitions
tool_schemas.py # Tool parameter schemas
resources/ # Resource handlers
templates/ # Schematic/project templates
tests/ # Python test suite
utils/ # Platform detection, helpers
docs/ # Documentation
config/ # Configuration examples
```
---
## TypeScript Layer
### Server Startup (`src/server.ts`)
1. Creates an MCP server instance
2. Registers all tools from each tool file (registerProjectTools, registerBoardTools, etc.)
3. Registers resources and prompts
4. Starts the STDIO transport for MCP communication
5. On first tool call, spawns the Python subprocess
### Tool Registration
Each tool file exports a `register*Tools(server, callKicadScript)` function that:
- Defines tool name, description, and Zod schema for parameters
- Registers a handler that calls `callKicadScript(command, args)`
Example from `src/tools/project.ts`:
```typescript
server.tool(
"create_project",
"Create a new KiCAD project",
{ name: z.string(), path: z.string() },
async (args) => {
const result = await callKicadScript("create_project", args);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
},
);
```
### Tool Router (`src/tools/router.ts` and `src/tools/registry.ts`)
The router pattern reduces AI context usage:
- `registry.ts` defines tool categories and which tools are "direct" (always visible) vs "routed" (discoverable)
- `router.ts` provides 4 meta-tools: `list_tool_categories`, `get_category_tools`, `search_tools`, `execute_tool`
- Routed tools are not registered as individual MCP tools -- they are invoked through `execute_tool`
### Python Subprocess Communication
`callKicadScript(command, args)` in `server.ts`:
1. Spawns `python3 python/kicad_interface.py` (if not already running)
2. Sends a JSON message: `{"command": "...", "params": {...}}`
3. Reads the JSON response
4. Returns the result to the MCP tool handler
---
## Python Layer
### Main Entry Point (`python/kicad_interface.py`)
- Reads JSON commands from stdin
- Routes commands to the appropriate handler
- Manages the pcbnew board object lifecycle
- Handles backend selection (SWIG vs IPC)
- Auto-saves after board-modifying operations
### Command Routing
Commands are routed by name to handler methods. The mapping is defined in `kicad_interface.py`. Each handler:
1. Receives a params dict
2. Calls the appropriate command class method
3. Returns a result dict with `success`, `message`, and any additional data
### Backend System (`python/kicad_api/`)
Two backends for interacting with KiCAD:
**SWIG Backend** (default):
- Direct Python bindings to KiCAD's C++ API via SWIG
- Operates on files -- loads .kicad_pcb, modifies in memory, saves back
- Works without KiCAD running
- Requires manual UI reload to see changes
**IPC Backend** (experimental):
- Communicates with running KiCAD via IPC API socket
- Changes appear in the UI immediately
- Requires KiCAD 9.0+ running with IPC enabled
- Falls back to SWIG when unavailable
`factory.py` auto-detects which backend to use.
### Schematic System
Schematic manipulation uses a different stack than PCB operations:
- **kicad-skip** library for reading/modifying schematic files
- **S-expression parsing** for direct file manipulation (wires, symbols)
- **DynamicSymbolLoader** for injecting any KiCad symbol into a schematic
- **WireManager** for creating wires via S-expression injection
- **PinLocator** for discovering pin positions with rotation support
---
## Adding a New Tool
### Step 1: Define the TypeScript Schema
Create or edit a file in `src/tools/`. Register the tool with `server.tool()`:
```typescript
server.tool(
"my_new_tool",
"Description of what the tool does",
{
param1: z.string().describe("Description of param1"),
param2: z.number().optional().describe("Optional param2"),
},
async (args) => {
const result = await callKicadScript("my_new_tool", args);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
},
);
```
### Step 2: Add to Registry (if routed)
If the tool should be discoverable via the router (not always visible), add it to a category in `src/tools/registry.ts`:
```typescript
{
name: "category_name",
tools: ["existing_tool", "my_new_tool"]
}
```
If the tool should always be visible, add it to `directToolNames` instead.
### Step 3: Import in server.ts
Import and call the registration function in `src/server.ts`:
```typescript
import { registerMyTools } from "./tools/my-tools.js";
registerMyTools(server, callKicadScript);
```
### Step 4: Implement the Python Handler
Add a handler in `python/kicad_interface.py` or create a new command module in `python/commands/`:
```python
def handle_my_new_tool(self, params):
# Implementation using pcbnew API
return {"success": True, "message": "Done", "data": result}
```
Route the command in the main handler:
```python
elif command == "my_new_tool":
return self.handle_my_new_tool(params)
```
### Step 5: Build and Test
```bash
npm run build # Compile TypeScript
npm run test:py # Run Python tests
```
---
## Testing
### Python Tests
Located in `python/tests/`. Run with:
```bash
pytest python/tests/ -v
```
Key test files:
- `test_schematic_tools.py` -- schematic tool tests
- `test_freerouting.py` -- autorouter tests
- `test_delete_schematic_component.py` -- component deletion tests
- `test_schematic_component_fields.py` -- field inspection tests
- `test_platform_helper.py` -- platform detection tests
### Manual Testing
1. Build the server: `npm run build`
2. Configure in Claude Desktop or Claude Code
3. Test tools interactively through your MCP client
---
## Key Design Decisions
- **TypeScript + Python split**: TypeScript handles MCP protocol (well-supported SDK), Python handles KiCAD (only available API)
- **Router pattern**: Reduces AI context from ~80K tokens (122 tools) to manageable size
- **Auto-save**: Every board-modifying SWIG operation auto-saves to prevent data loss
- **Dynamic symbol loading**: Works around kicad-skip's inability to create symbols from scratch
- **S-expression wire injection**: Works around kicad-skip's inability to create wires
---
## Source Files Reference
| File | Purpose |
| ------------------------------------------ | ----------------------------------- |
| `src/server.ts` | MCP server, subprocess management |
| `src/tools/registry.ts` | Tool categories and organization |
| `src/tools/router.ts` | Router meta-tools |
| `python/kicad_interface.py` | Python entry point, command routing |
| `python/kicad_api/factory.py` | Backend selection |
| `python/commands/dynamic_symbol_loader.py` | Symbol injection system |
| `python/commands/wire_manager.py` | Wire creation engine |
| `python/commands/pin_locator.py` | Pin position discovery |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,108 @@
# Datasheet Management Tools
**Added in:** v2.2.0-alpha
Two tools for managing component datasheets using LCSC part numbers. Datasheet URLs are constructed directly from LCSC numbers -- no API key or network requests required.
---
## Tools Reference
### `enrich_datasheets`
Scans a KiCAD schematic and fills in missing Datasheet URLs for components that have an LCSC part number set.
**How it works:**
For every placed symbol that has:
- An LCSC property set (e.g., `(property "LCSC" "C123456")`)
- An empty or missing Datasheet field
The tool sets the Datasheet field to: `https://www.lcsc.com/datasheet/C123456.pdf`
The URL is then visible in KiCAD's footprint browser, symbol properties dialog, and any tool that reads the standard Datasheet field.
**Parameters:**
| Parameter | Type | Required | Default | Description |
| ---------------- | ------- | -------- | ------- | --------------------------------------- |
| `schematic_path` | string | Yes | -- | Path to the .kicad_sch file to enrich |
| `dry_run` | boolean | No | false | Preview changes without writing to disk |
**Returns:**
- Number of components updated
- Number already set (skipped)
- Number without LCSC number
- Details of each updated component (reference, LCSC number, URL)
**Example:**
```
Enrich datasheets for all components in ~/Projects/MyBoard/MyBoard.kicad_sch
```
Use `dry_run=true` to preview what would change:
```
Preview datasheet enrichment for ~/Projects/MyBoard/MyBoard.kicad_sch with dry run enabled.
```
---
### `get_datasheet_url`
Get the LCSC datasheet URL for a single component by LCSC number.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | -------------------------------------------------------------------------- |
| `lcsc` | string | Yes | LCSC part number, with or without "C" prefix (e.g., "C179739" or "179739") |
**Returns:**
- Datasheet PDF URL
- Product page URL
**Example:**
```
Get the datasheet URL for LCSC part C179739.
```
---
## Workflow
### Adding Datasheets to a Design
1. **Add components with LCSC numbers** -- When placing components from JLCPCB libraries or manually setting the LCSC property, each component gets an LCSC part number
2. **Run enrich_datasheets** -- Scans all components and fills in any missing Datasheet URLs
3. **Verify in KiCAD** -- Open the schematic in KiCAD and check that Datasheet fields are populated. Double-clicking a component shows the URL in its properties
### Integration with JLCPCB Workflow
These tools complement the JLCPCB integration:
1. Use `search_jlcpcb_parts` to find components
2. Place components with LCSC numbers from the search results
3. Run `enrich_datasheets` to auto-populate datasheet URLs
4. Use `export_bom` to generate a BOM with datasheet links
---
## Notes
- The datasheet URL format (`https://www.lcsc.com/datasheet/<LCSC#>.pdf`) works for the vast majority of LCSC parts
- No network request is made -- the URL is constructed from the part number alone
- Components without an LCSC property are skipped silently
- Components that already have a Datasheet URL set are not overwritten
---
## Source Files
- TypeScript tool definitions: `src/tools/datasheet.ts`
- Python implementation: `python/commands/datasheet_manager.py`

View File

@@ -0,0 +1,415 @@
# Creating Custom Footprints and Symbols
Added in: v2.2.1-alpha (PRs #48, #49, contributor: @Kletternaut)
When existing KiCAD libraries don't have the component you need, these 8 tools let you create custom footprints and symbols programmatically. This enables automated part creation for custom PCB designs, specialized components, or rapid prototyping workflows where manual library editing would be time-consuming.
## Part 1: Footprint Creator
Footprints define the physical copper pads, silkscreen markings, and courtyard boundaries for PCB components. The footprint creator tools generate `.kicad_mod` files inside `.pretty` library directories.
### create_footprint
Create a new KiCAD footprint (.kicad_mod) inside a .pretty library directory. Supports SMD and THT pads, courtyard, silkscreen, and fab-layer rectangles.
| Parameter | Type | Required | Description |
| --------------- | ------- | -------- | ------------------------------------------------------------------------------------------ |
| `libraryPath` | string | Yes | Path to the .pretty library directory (created if missing). E.g. C:/MyProject/MyLib.pretty |
| `name` | string | Yes | Footprint name, e.g. 'R_0603_Custom' |
| `description` | string | No | Human-readable description |
| `tags` | string | No | Space-separated tag string, e.g. 'resistor SMD 0603' |
| `pads` | array | No | List of pad objects (see Pad Schema below). Can be empty for outlines-only footprints |
| `courtyard` | object | No | Courtyard rectangle on F.CrtYd (recommended: 0.25 mm clearance around pads) |
| `silkscreen` | object | No | Silkscreen rectangle on F.SilkS |
| `fabLayer` | object | No | Fab-layer rectangle on F.Fab (shows component body) |
| `refPosition` | object | No | Position of the REF\*\* text, e.g. {x: 0, y: -1.27} (default: 0, -1.27) |
| `valuePosition` | object | No | Position of the Value text, e.g. {x: 0, y: 1.27} (default: 0, 1.27) |
| `overwrite` | boolean | No | Replace existing footprint file (default: false) |
#### Pad Schema
Each pad object in the `pads` array supports:
| Parameter | Type | Required | Description |
| ----------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------- |
| `number` | string | Yes | Pad number / name, e.g. '1', '2', 'A1' |
| `type` | enum | Yes | Pad type: `smd`, `thru_hole`, or `np_thru_hole` |
| `shape` | enum | No | Pad shape: `rect`, `circle`, `oval`, or `roundrect` (default: rect for SMD, circle for THT) |
| `at` | object | Yes | Pad centre position: {x: number, y: number, angle?: number} in mm |
| `size` | object | Yes | Pad size: {w: number, h: number} in mm |
| `drill` | number or object | No | Round drill diameter (mm) or oval drill {w: number, h: number} (required for thru_hole pads) |
| `layers` | array | No | Override default layer list, e.g. ['F.Cu','F.Paste','F.Mask'] |
| `roundrect_ratio` | number | No | Corner radius ratio for roundrect shape (0.0-0.5, default 0.25) |
#### Rectangle Schema (courtyard, silkscreen, fabLayer)
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ---------------- |
| `x1` | number | Yes | Left X in mm |
| `y1` | number | Yes | Top Y in mm |
| `x2` | number | Yes | Right X in mm |
| `y2` | number | Yes | Bottom Y in mm |
| `width` | number | No | Line width in mm |
#### Pad Types
- **SMD (smd)**: Surface-mount pads for components that sit on top of the PCB. Default layers: F.Cu, F.Paste, F.Mask
- **THT (thru_hole)**: Through-hole pads for components with leads that pass through the PCB. Requires `drill` parameter. Default layers: \*.Cu, F.Mask, B.Mask
- **NPTH (np_thru_hole)**: Non-plated through-holes for mechanical mounting. Requires `drill` parameter. Default layers: \*.Mask
### edit_footprint_pad
Edit an existing pad inside a .kicad_mod footprint file. Updates size, position, drill, or shape without recreating the whole footprint.
| Parameter | Type | Required | Description |
| --------------- | ---------------- | -------- | ---------------------------------------------------------------------------- |
| `footprintPath` | string | Yes | Full path to the .kicad_mod file, e.g. C:/MyLib.pretty/R_Custom.kicad_mod |
| `padNumber` | string or number | Yes | Pad number to edit, e.g. '1' or 2 |
| `size` | object | No | New pad size: {w: number, h: number} in mm |
| `at` | object | No | New pad position: {x: number, y: number, angle?: number} in mm |
| `drill` | number or object | No | New drill size: number (round) or {w: number, h: number} (oval) for THT pads |
| `shape` | enum | No | New pad shape: `rect`, `circle`, `oval`, or `roundrect` |
**When to use:** Use this tool when you need to adjust an existing footprint's pad dimensions or positions without recreating the entire footprint. Useful for fine-tuning after initial creation or adapting existing footprints.
### register_footprint_library
Register a .pretty footprint library in KiCAD's fp-lib-table so KiCAD can find the footprints. Run this after create_footprint when KiCAD shows 'library not found in footprint library table'.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `libraryPath` | string | Yes | Full path to the .pretty directory to register |
| `libraryName` | string | No | Nickname for the library in KiCAD (default: directory name without .pretty) |
| `description` | string | No | Optional description |
| `scope` | enum | No | `project` = writes fp-lib-table next to the .kicad_pro file (default); `global` = writes to the user's global KiCAD config |
| `projectPath` | string | No | Path to the .kicad_pro file or its directory (required for scope=project when the library is not in the project folder) |
**How fp-lib-table works:** KiCAD maintains a table mapping library nicknames to filesystem paths. Project-scope tables (fp-lib-table in the project directory) take precedence over global tables. This allows project-specific libraries without polluting the global configuration.
### list_footprint_libraries
List available .pretty footprint libraries and their contents (first 20 footprints per library). Searches KiCAD standard install paths by default.
| Parameter | Type | Required | Description |
| ------------- | ----- | -------- | --------------------------------------------------------------------------------------------- |
| `searchPaths` | array | No | Override default search paths. Each entry should be a directory that contains .pretty subdirs |
### Example: Creating a Custom SOT-23 Footprint
This example creates a simple 3-pad SMD footprint for a SOT-23 transistor package:
```javascript
// Step 1: Create the footprint
{
"libraryPath": "/home/user/myproject/CustomParts.pretty",
"name": "SOT-23_Custom",
"description": "SOT-23 3-pin SMD package, custom pitch",
"tags": "SOT-23 transistor SMD",
"pads": [
{
"number": "1",
"type": "smd",
"shape": "rect",
"at": {"x": -0.95, "y": 1.0},
"size": {"w": 0.6, "h": 0.7}
},
{
"number": "2",
"type": "smd",
"shape": "rect",
"at": {"x": 0.95, "y": 1.0},
"size": {"w": 0.6, "h": 0.7}
},
{
"number": "3",
"type": "smd",
"shape": "rect",
"at": {"x": 0, "y": -1.0},
"size": {"w": 0.6, "h": 0.7}
}
],
"courtyard": {
"x1": -1.5,
"y1": -1.5,
"x2": 1.5,
"y2": 1.5,
"width": 0.05
},
"silkscreen": {
"x1": -1.3,
"y1": -0.3,
"x2": 1.3,
"y2": 0.3,
"width": 0.12
},
"fabLayer": {
"x1": -1.25,
"y1": -0.25,
"x2": 1.25,
"y2": 0.25,
"width": 0.1
}
}
// Step 2: Register the library so KiCAD can find it
{
"libraryPath": "/home/user/myproject/CustomParts.pretty",
"scope": "project",
"projectPath": "/home/user/myproject/myproject.kicad_pro"
}
```
The footprint will be saved as `/home/user/myproject/CustomParts.pretty/SOT-23_Custom.kicad_mod` and will be available in KiCAD's footprint browser under the library name "CustomParts".
## Part 2: Symbol Creator
Symbols define the schematic representation of electronic components, including pins, graphical body shapes, and electrical properties. The symbol creator tools generate and manage `.kicad_sym` library files.
### create_symbol
Create a new schematic symbol in a .kicad_sym library file (created if missing). After creation, use register_symbol_library so KiCAD finds it.
Pin positions are where the wire connects; the symbol body is drawn between them.
**Coordinate tips:**
- Body rectangle typically spans ±2.54 to ±5.08 mm
- Pins on left side: at.x = body_left - length, angle=0 (wire goes right)
- Pins on right side: at.x = body_right + length, angle=180 (wire goes left)
- Pins on top: at.y = body_top + length, angle=270 (wire goes down)
- Pins on bottom: at.y = body_bottom - length, angle=90 (wire goes up)
- Standard pin length: 2.54 mm, standard grid: 2.54 mm
| Parameter | Type | Required | Description |
| ----------------- | ------- | -------- | ---------------------------------------------------------------------------------------- |
| `libraryPath` | string | Yes | Path to the .kicad_sym file (created if missing) |
| `name` | string | Yes | Symbol name, e.g. 'TMC2209', 'MyOpAmp' |
| `referencePrefix` | string | No | Schematic reference prefix: 'U' (IC), 'R' (resistor), 'J' (connector), etc. Default: 'U' |
| `description` | string | No | Human-readable description |
| `keywords` | string | No | Space-separated search keywords |
| `datasheet` | string | No | Datasheet URL or '~' |
| `footprint` | string | No | Default footprint, e.g. 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm' |
| `inBom` | boolean | No | Include in BOM (default true) |
| `onBoard` | boolean | No | Include in netlist for PCB (default true) |
| `pins` | array | No | List of pin objects (see Pin Schema below). Can be empty for graphical-only symbols |
| `rectangles` | array | No | Body rectangle(s). Typically one rectangle defining the IC body |
| `polylines` | array | No | Polyline graphics for custom body shapes (op-amp triangles, etc.) |
| `overwrite` | boolean | No | Replace existing symbol with same name (default false) |
#### Pin Schema
Each pin object in the `pins` array supports:
| Parameter | Type | Required | Description |
| --------- | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `name` | string | Yes | Pin name, e.g. 'VCC', 'GND', 'IN+', '~' for unnamed |
| `number` | string or number | Yes | Pin number, e.g. '1', '2', 'A1' |
| `type` | enum | Yes | Electrical pin type (see Pin Types below) |
| `at` | object | Yes | Pin endpoint position: {x: number, y: number, angle: number} where angle is the direction the pin wire extends FROM the symbol body |
| `length` | number | No | Pin length in mm (default 2.54) |
| `shape` | enum | No | Pin graphic shape (default: line) |
**Pin angle conventions:**
- 0 = right (wire extends to the right from the symbol body)
- 90 = up (wire extends upward)
- 180 = left (wire extends to the left)
- 270 = down (wire extends downward)
#### Pin Types (Electrical)
| Type | Description |
| ---------------- | ----------------------------------------- |
| `input` | Input pin |
| `output` | Output pin |
| `bidirectional` | Bidirectional I/O |
| `tri_state` | Tri-state output |
| `passive` | Passive component (resistors, capacitors) |
| `free` | Free pin (no electrical rule checking) |
| `unspecified` | Unspecified type |
| `power_in` | Power input (VCC, VDD) |
| `power_out` | Power output (regulators) |
| `open_collector` | Open collector output |
| `open_emitter` | Open emitter output |
| `no_connect` | Not connected |
#### Pin Shapes (Graphical)
| Shape | Description |
| -------------------- | -------------------------- |
| `line` | Standard pin (default) |
| `inverted` | Pin with inversion bubble |
| `clock` | Clock input (triangle) |
| `inverted_clock` | Inverted clock with bubble |
| `input_low` | Active-low input |
| `clock_low` | Active-low clock |
| `output_low` | Active-low output |
| `falling_edge_clock` | Falling edge triggered |
| `non_logic` | Non-logic pin |
#### Rectangle Schema
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------------------------------- |
| `x1` | number | Yes | Left X in mm |
| `y1` | number | Yes | Top Y in mm |
| `x2` | number | Yes | Right X in mm |
| `y2` | number | Yes | Bottom Y in mm |
| `width` | number | No | Stroke width in mm (default 0.254) |
| `fill` | enum | No | Fill type: `none`, `outline`, or `background` (default: background) |
#### Polyline Schema
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------------------ |
| `points` | array | Yes | List of XY points: [{x: number, y: number}, ...] in mm |
| `width` | number | No | Stroke width in mm (default 0.254) |
| `fill` | enum | No | Fill type: `none`, `outline`, or `background` |
### delete_symbol
Remove a symbol from a .kicad_sym library file.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------- |
| `libraryPath` | string | Yes | Path to the .kicad_sym file |
| `name` | string | Yes | Symbol name to delete |
### list_symbols_in_library
List all symbol names in a .kicad_sym library file.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------- |
| `libraryPath` | string | Yes | Path to the .kicad_sym file |
### register_symbol_library
Register a .kicad_sym library in KiCAD's sym-lib-table so symbols can be used in schematics. Run this after create_symbol when KiCAD shows 'library not found'.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
| `libraryPath` | string | Yes | Full path to the .kicad_sym file |
| `libraryName` | string | No | Nickname (default: file name without extension) |
| `description` | string | No | Optional description |
| `scope` | enum | No | `project` = writes sym-lib-table next to .kicad_pro (default); `global` = user config |
| `projectPath` | string | No | Path to .kicad_pro or its directory (for scope=project) |
### Example: Creating a Simple IC Symbol
This example creates a 4-pin IC symbol (VCC, GND, IN, OUT):
```javascript
// Step 1: Create the symbol
{
"libraryPath": "/home/user/myproject/CustomSymbols.kicad_sym",
"name": "MyRegulator",
"referencePrefix": "U",
"description": "Simple voltage regulator",
"keywords": "regulator power",
"datasheet": "~",
"footprint": "Package_TO_SOT_SMD:SOT-23",
"pins": [
{
"name": "VIN",
"number": "1",
"type": "power_in",
"at": {"x": -7.62, "y": 2.54, "angle": 0},
"length": 2.54
},
{
"name": "GND",
"number": "2",
"type": "power_in",
"at": {"x": 0, "y": -7.62, "angle": 90},
"length": 2.54
},
{
"name": "VOUT",
"number": "3",
"type": "power_out",
"at": {"x": 7.62, "y": 2.54, "angle": 180},
"length": 2.54
}
],
"rectangles": [
{
"x1": -5.08,
"y1": -5.08,
"x2": 5.08,
"y2": 5.08,
"width": 0.254,
"fill": "background"
}
]
}
// Step 2: Register the library
{
"libraryPath": "/home/user/myproject/CustomSymbols.kicad_sym",
"scope": "project",
"projectPath": "/home/user/myproject/myproject.kicad_pro"
}
```
**Pin positioning explained:**
- VIN pin at (-7.62, 2.54, angle=0): Wire extends to the right, so the symbol body should be to the right. Body left edge is at -5.08, and pin length is 2.54, so -7.62 = -5.08 - 2.54
- GND pin at (0, -7.62, angle=90): Wire extends upward, body bottom is at -5.08, so -7.62 = -5.08 - 2.54
- VOUT pin at (7.62, 2.54, angle=180): Wire extends to the left, body right is at 5.08, so 7.62 = 5.08 + 2.54
## Coordinate Systems
### Footprint Coordinates
- Origin (0, 0) is typically at the component center or pin 1
- Positive X extends right, positive Y extends down (PCB view from top)
- All dimensions in millimeters
- Courtyard should extend 0.25mm beyond pads for IPC-7351 compliance
- Silkscreen should not overlap pads (typically 0.1-0.2mm clearance)
### Symbol Coordinates
- Origin (0, 0) is typically at the symbol center
- Positive X extends right, positive Y extends up (schematic convention)
- All dimensions in millimeters
- Standard grid is 2.54mm (100 mil) for pin spacing
- Pin positions define where wires connect, not where the pin graphic starts
- Body graphics are drawn independently of pin positions
### Key Difference
Footprints use a "Y-down" coordinate system (like screen coordinates), while symbols use a "Y-up" coordinate system (like mathematical graphs). This is a KiCAD convention that matches industry standards for PCB layout vs schematic capture.
## Integration with Design Workflow
### Typical Workflow
1. **Create the symbol** using `create_symbol` with pin definitions and body graphics
2. **Register the symbol library** using `register_symbol_library` so it appears in the schematic editor
3. **Create the footprint** using `create_footprint` with pad definitions and courtyard
4. **Register the footprint library** using `register_footprint_library` so it appears in the PCB editor
5. **Link symbol to footprint** by setting the `footprint` parameter in `create_symbol`, or assign it later in the schematic editor
### Library Organization
- **Project-scope libraries**: Store in the project directory, register with `scope: "project"`. Best for project-specific custom parts.
- **Global libraries**: Store in a central location, register with `scope: "global"`. Best for reusable parts across multiple projects.
- **Naming conventions**: Use descriptive names. For footprints: `PackageType_Variant`, e.g. `SOIC-8_Custom`. For symbols: `PartNumber` or `FunctionDescription`.
### Validation
After creating custom parts:
- Open KiCAD schematic editor and verify the symbol appears in the "Add Symbol" dialog
- Check pin numbers, names, and electrical types in symbol properties
- Open KiCAD PCB editor and verify the footprint appears in the footprint browser
- Use the 3D viewer to check pad positions and courtyard clearances
- Run Design Rules Check (DRC) to ensure courtyard and clearance compliance
## Source Files
- TypeScript tool definitions: `/home/chris/MCP/KiCAD-MCP-Server/src/tools/footprint.ts`
- TypeScript symbol definitions: `/home/chris/MCP/KiCAD-MCP-Server/src/tools/symbol-creator.ts`
- Python footprint implementation: `/home/chris/MCP/KiCAD-MCP-Server/python/commands/footprint.py`
- Python symbol implementation: `/home/chris/MCP/KiCAD-MCP-Server/python/commands/symbol_creator.py`

234
docs/FREEROUTING_GUIDE.md Normal file
View File

@@ -0,0 +1,234 @@
# Freerouting Integration Guide
**Added in:** v2.2.3 (PR #68, contributor: @jflaflamme)
Freerouting is an open-source autorouter that can automatically route PCB traces. This integration lets you run Freerouting directly from MCP tools without leaving your AI-assisted design workflow.
---
## How It Works
The autorouter uses the Specctra DSN/SES interchange format:
1. Export the current PCB to Specctra DSN format
2. Run Freerouting CLI on the DSN file
3. Import the routed SES result back into the PCB
4. Save the board
The `autoroute` tool performs all four steps in a single call.
---
## Prerequisites
### Freerouting JAR
Download the Freerouting executable JAR:
```bash
mkdir -p ~/.kicad-mcp
curl -L -o ~/.kicad-mcp/freerouting.jar \
https://github.com/freerouting/freerouting/releases/download/v2.0.1/freerouting-2.0.1-executable.jar
```
The default location is `~/.kicad-mcp/freerouting.jar`. You can override this with:
- The `freeroutingJar` parameter on any tool call
- The `FREEROUTING_JAR` environment variable
### Java Runtime (Option A -- Direct Execution)
Freerouting 2.x requires Java 21 or higher.
```bash
# Ubuntu/Debian
sudo apt install openjdk-21-jre
# Verify
java -version
```
### Docker or Podman (Option B -- No Java Install Needed)
If you do not have Java 21+ installed, the integration automatically falls back to Docker or Podman using the `eclipse-temurin:21-jre` image.
```bash
# Pull the image (one-time)
docker pull eclipse-temurin:21-jre
# Or with Podman
podman pull eclipse-temurin:21-jre
```
### Automatic Runtime Detection
The autorouter checks for runtimes in this order:
1. Local Java 21+ (direct execution, fastest)
2. Docker (container execution)
3. Podman (container execution)
If none are available, an error is returned with installation instructions.
---
## Tools Reference
### `check_freerouting`
Verify that prerequisites are installed before running the autorouter.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `freeroutingJar` | string | No | Path to freerouting.jar to check |
**Returns:** Java availability, version, Docker status, JAR location
**Example:**
```
Check if Freerouting is ready on my system.
```
### `autoroute`
Run the full autorouting workflow (export DSN, route, import SES).
**Parameters:**
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `boardPath` | string | No | Current board | Path to .kicad_pcb file |
| `freeroutingJar` | string | No | ~/.kicad-mcp/freerouting.jar | Path to freerouting.jar |
| `maxPasses` | number | No | 20 | Maximum routing passes |
| `timeout` | number | No | 300 | Timeout in seconds |
**Example:**
```
Autoroute the current board using Freerouting with a 5-minute timeout.
```
### `export_dsn`
Export the PCB to Specctra DSN format for manual routing workflows.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `boardPath` | string | No | Path to .kicad_pcb file (default: current board) |
| `outputPath` | string | No | Output DSN file path (default: same directory as board) |
### `import_ses`
Import a routed Specctra SES file back into the PCB.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `sesPath` | string | Yes | Path to the .ses file to import |
| `boardPath` | string | No | Path to .kicad_pcb file (default: current board) |
---
## Workflows
### Automated (Recommended)
A single tool call handles everything:
```
1. Open the project
2. Check Freerouting dependencies
3. Run autoroute with max 10 passes
4. Run DRC to verify the result
5. Export Gerbers
```
### Manual DSN/SES Workflow
For advanced users or external autorouters:
```
1. Export the board to Specctra DSN format
2. (Run Freerouting GUI or another autorouter externally)
3. Import the routed SES file
```
This is useful when you want to:
- Use the Freerouting GUI for interactive routing
- Use a different autorouter that supports DSN/SES
- Route the board on a different machine
---
## Configuration
### Environment Variable
Set `FREEROUTING_JAR` in your MCP client configuration to avoid specifying the path on every call:
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/path/to/KiCAD-MCP-Server/dist/index.js"],
"env": {
"FREEROUTING_JAR": "/path/to/freerouting.jar"
}
}
}
}
```
---
## Troubleshooting
### "Neither Java 21+ nor Docker found"
Install either Java 21+ or Docker/Podman. See the Prerequisites section above.
### "Java found but version < 21"
Freerouting 2.x requires Java 21+. Either:
- Upgrade your Java installation
- Install Docker as a fallback
### Timeout Errors
For complex boards, increase the timeout:
```
Autoroute with timeout 600 and max passes 30.
```
### Routing Quality
If the autorouter does not route all connections:
- Increase `maxPasses` (default: 20)
- Check that your design rules allow the autorouter enough clearance
- Run DRC after autorouting to identify any violations
- Consider routing critical traces manually first, then autorouting the rest
### Docker Permission Errors
If Docker reports permission errors:
```bash
# Add your user to the docker group
sudo usermod -aG docker $USER
# Log out and back in for the change to take effect
```
---
## Source Files
- TypeScript tool definitions: `src/tools/freerouting.ts`
- Python implementation: `python/commands/freerouting.py`
- Tests: `python/tests/test_freerouting.py`

89
docs/INDEX.md Normal file
View File

@@ -0,0 +1,89 @@
# Documentation Index
KiCAD MCP Server -- AI-assisted PCB design via Model Context Protocol
**Version:** 2.2.3 | **Tools:** 122 | **Last Updated:** 2026-03-21
---
## Getting Started
| Document | Description |
| ----------------------------------------------- | -------------------------------------------------------------- |
| [README](../README.md) | Project overview, installation, configuration, quick start |
| [Client Configuration](CLIENT_CONFIGURATION.md) | MCP client setup (Claude Desktop, Cline, Claude Code) |
| [Platform Guide](PLATFORM_GUIDE.md) | Linux vs Windows vs macOS differences |
| [PCB Design Workflow](PCB_DESIGN_WORKFLOW.md) | End-to-end design guide from project creation to manufacturing |
---
## Tool References
| Document | Description |
| ----------------------------------------------------------------------- | ----------------------------------------------------------- |
| [Tool Inventory](TOOL_INVENTORY.md) | Complete list of all 122 tools with access types |
| [Schematic Tools Reference](SCHEMATIC_TOOLS_REFERENCE.md) | 27 schematic tools -- components, wiring, analysis, export |
| [Routing Tools Reference](ROUTING_TOOLS_REFERENCE.md) | 13 routing tools -- traces, vias, differential pairs, zones |
| [Footprint and Symbol Creator Guide](FOOTPRINT_SYMBOL_CREATOR_GUIDE.md) | 8 tools for creating custom footprints and symbols |
| [Freerouting Guide](FREEROUTING_GUIDE.md) | 4 autorouter tools -- setup, usage, Docker support |
| [SVG Import Guide](SVG_IMPORT_GUIDE.md) | Import SVG logos onto PCB layers |
| [Datasheet Tools Guide](DATASHEET_TOOLS_GUIDE.md) | Datasheet enrichment via LCSC |
---
## Integration Guides
| Document | Description |
| --------------------------------------------- | -------------------------------------------------- |
| [JLCPCB Integration](JLCPCB_INTEGRATION.md) | JLCPCB parts catalog, pricing, component selection |
| [JLCPCB Usage Guide](JLCPCB_USAGE_GUIDE.md) | Detailed JLCPCB setup and usage |
| [Library Integration](LIBRARY_INTEGRATION.md) | Footprint and symbol library setup |
| [IPC Backend Status](IPC_BACKEND_STATUS.md) | Real-time KiCAD UI synchronization (experimental) |
---
## Workflows
| Document | Description |
| --------------------------------------------- | ----------------------------------------- |
| [Realtime Workflow](REALTIME_WORKFLOW.md) | Working with IPC backend for live updates |
| [Visual Feedback](VISUAL_FEEDBACK.md) | UI visual feedback guide |
| [UI Auto Launch](UI_AUTO_LAUNCH.md) | Automatic KiCAD UI launch feature |
| [Router Guide](mcp-router-guide.md) | Tool router pattern usage |
| [Router Architecture](ROUTER_ARCHITECTURE.md) | Router pattern design |
| [Router Quick Start](ROUTER_QUICK_START.md) | Quick start for the router pattern |
---
## Troubleshooting
| Document | Description |
| --------------------------------------------------------- | ------------------------------ |
| [Known Issues](KNOWN_ISSUES.md) | Current issues and workarounds |
| [Windows Troubleshooting](WINDOWS_TROUBLESHOOTING.md) | Windows-specific problems |
| [Linux Compatibility Audit](LINUX_COMPATIBILITY_AUDIT.md) | Linux platform details |
---
## Project Information
| Document | Description |
| ----------------------------------- | ----------------------------------------- |
| [Status Summary](STATUS_SUMMARY.md) | Current project status and feature matrix |
| [Roadmap](ROADMAP.md) | Development roadmap and planned features |
| [Changelog](../CHANGELOG.md) | Detailed release notes for all versions |
---
## For Contributors
| Document | Description |
| ---------------------------------- | ---------------------------------------- |
| [Contributing](../CONTRIBUTING.md) | How to contribute to the project |
| [Architecture](ARCHITECTURE.md) | System architecture and adding new tools |
---
## Archive
Historical planning documents are preserved in [docs/archive/](archive/README.md).

View File

@@ -1,212 +1,223 @@
# KiCAD IPC Backend Implementation Status
**Status:** Under Active Development and Testing
**Date:** 2025-12-02
**KiCAD Version:** 9.0.6
**kicad-python Version:** 0.5.0
---
## Overview
The IPC backend provides real-time UI synchronization with KiCAD 9.0+ via the official IPC API. When KiCAD is running with IPC enabled, commands can update the KiCAD UI immediately without requiring manual reload.
This feature is experimental and under active testing. The server uses a hybrid approach: IPC when available, automatic fallback to SWIG when IPC is not connected.
## Key Differences
| Feature | SWIG | IPC |
|---------|------|-----|
| UI Updates | Manual reload required | Immediate (when working) |
| Undo/Redo | Not supported | Transaction support |
| API Stability | Deprecated in KiCAD 9 | Official, versioned |
| Connection | File-based | Live socket connection |
| KiCAD Required | No (file operations) | Yes (must be running) |
## Implemented IPC Commands
The following MCP commands have IPC handlers:
| Command | IPC Handler | Status |
|---------|-------------|--------|
| `route_trace` | `_ipc_route_trace` | Implemented |
| `add_via` | `_ipc_add_via` | Implemented |
| `add_net` | `_ipc_add_net` | Implemented |
| `delete_trace` | `_ipc_delete_trace` | Falls back to SWIG |
| `get_nets_list` | `_ipc_get_nets_list` | Implemented |
| `add_copper_pour` | `_ipc_add_copper_pour` | Implemented |
| `refill_zones` | `_ipc_refill_zones` | Implemented |
| `add_text` | `_ipc_add_text` | Implemented |
| `add_board_text` | `_ipc_add_text` | Implemented |
| `set_board_size` | `_ipc_set_board_size` | Implemented |
| `get_board_info` | `_ipc_get_board_info` | Implemented |
| `add_board_outline` | `_ipc_add_board_outline` | Implemented |
| `add_mounting_hole` | `_ipc_add_mounting_hole` | Implemented |
| `get_layer_list` | `_ipc_get_layer_list` | Implemented |
| `place_component` | `_ipc_place_component` | Implemented (hybrid) |
| `move_component` | `_ipc_move_component` | Implemented |
| `rotate_component` | `_ipc_rotate_component` | Implemented |
| `delete_component` | `_ipc_delete_component` | Implemented |
| `get_component_list` | `_ipc_get_component_list` | Implemented |
| `get_component_properties` | `_ipc_get_component_properties` | Implemented |
| `save_project` | `_ipc_save_project` | Implemented |
### Implemented Backend Features
**Core Connection:**
- Connect to running KiCAD instance
- Auto-detect socket path (`/tmp/kicad/api.sock`)
- Version checking and validation
- Auto-fallback to SWIG when IPC unavailable
- Change notification callbacks
**Board Operations:**
- Get board reference
- Get/Set board size
- List enabled layers
- Save board
- Add board outline segments
- Add mounting holes
**Component Operations:**
- List all components
- Place component (hybrid: SWIG for library loading, IPC for placement)
- Move component
- Rotate component
- Delete component
- Get component properties
**Routing Operations:**
- Add track
- Add via
- Get all tracks
- Get all vias
- Get all nets
**Zone Operations:**
- Add copper pour zones
- Get zones list
- Refill zones
**UI Integration:**
- Add text to board
- Get current selection
- Clear selection
**Transaction Support:**
- Begin transaction
- Commit transaction (with description for undo)
- Rollback transaction
## Usage
### Prerequisites
1. **KiCAD 9.0+** must be running
2. **IPC API must be enabled**: `Preferences > Plugins > Enable IPC API Server`
3. A board must be open in the PCB editor
### Installation
```bash
pip install kicad-python
```
### Testing
Run the test script to verify IPC functionality:
```bash
# Make sure KiCAD is running with IPC enabled and a board open
./venv/bin/python python/test_ipc_backend.py
```
## Architecture
```
+-------------------------------------------------------------+
| MCP Server (TypeScript/Node.js) |
+---------------------------+---------------------------------+
| JSON commands
+---------------------------v---------------------------------+
| Python Interface Layer |
| +--------------------------------------------------------+ |
| | kicad_interface.py | |
| | - Routes commands to IPC or SWIG handlers | |
| | - IPC_CAPABLE_COMMANDS dict defines routing | |
| +--------------------------------------------------------+ |
| +--------------------------------------------------------+ |
| | kicad_api/ipc_backend.py | |
| | - IPCBackend (connection management) | |
| | - IPCBoardAPI (board operations) | |
| +--------------------------------------------------------+ |
+---------------------------+---------------------------------+
| kicad-python (kipy) library
+---------------------------v---------------------------------+
| Protocol Buffers over UNIX Sockets |
+---------------------------+---------------------------------+
|
+---------------------------v---------------------------------+
| KiCAD 9.0+ (IPC Server) |
+-------------------------------------------------------------+
```
## Known Limitations
1. **KiCAD must be running**: Unlike SWIG, IPC requires KiCAD to be open
2. **Project creation**: Not supported via IPC, uses file system
3. **Footprint library access**: Uses hybrid approach (SWIG loads from library, IPC places)
4. **Delete trace**: Falls back to SWIG (IPC API doesn't support direct deletion)
5. **Some operations may not work as expected**: This is experimental code
## Troubleshooting
### "Connection failed"
- Ensure KiCAD is running
- Enable IPC API: `Preferences > Plugins > Enable IPC API Server`
- Check if a board is open
### "kicad-python not found"
```bash
pip install kicad-python
```
### "Version mismatch"
- Update kicad-python: `pip install --upgrade kicad-python`
- Ensure KiCAD 9.0+ is installed
### "No board open"
- Open a board in KiCAD's PCB editor before connecting
## File Structure
```
python/kicad_api/
├── __init__.py # Package exports
├── base.py # Abstract base classes
├── factory.py # Backend auto-detection
├── ipc_backend.py # IPC implementation
└── swig_backend.py # Legacy SWIG wrapper
python/
└── test_ipc_backend.py # IPC test script
```
## Future Work
1. More comprehensive testing of all IPC commands
2. Footprint library integration via IPC (when kipy supports it)
3. Schematic IPC support (when available in kicad-python)
4. Event subscriptions to react to changes made in KiCAD UI
5. Multi-board support
## Related Documentation
- [ROADMAP.md](./ROADMAP.md) - Project roadmap
- [IPC_API_MIGRATION_PLAN.md](./IPC_API_MIGRATION_PLAN.md) - Migration details
- [REALTIME_WORKFLOW.md](./REALTIME_WORKFLOW.md) - Collaboration workflows
- [kicad-python docs](https://docs.kicad.org/kicad-python-main/) - Official API docs
---
**Last Updated:** 2025-12-02
# KiCAD IPC Backend Implementation Status
**Status:** Under Active Development and Testing
**Date:** 2026-03-21
**KiCAD Version:** 9.0+
**kicad-python Version:** 0.5.0+
---
## Overview
The IPC backend provides real-time UI synchronization with KiCAD 9.0+ via the official IPC API. When KiCAD is running with IPC enabled, commands can update the KiCAD UI immediately without requiring manual reload.
This feature is experimental and under active testing. The server uses a hybrid approach: IPC when available, automatic fallback to SWIG when IPC is not connected.
## Key Differences
| Feature | SWIG | IPC |
| -------------- | ---------------------- | ------------------------ |
| UI Updates | Manual reload required | Immediate (when working) |
| Undo/Redo | Not supported | Transaction support |
| API Stability | Deprecated in KiCAD 9 | Official, versioned |
| Connection | File-based | Live socket connection |
| KiCAD Required | No (file operations) | Yes (must be running) |
## Implemented IPC Commands
The following MCP commands have IPC handlers:
| Command | IPC Handler | Status |
| -------------------------- | ------------------------------- | -------------------- |
| `route_trace` | `_ipc_route_trace` | Implemented |
| `add_via` | `_ipc_add_via` | Implemented |
| `add_net` | `_ipc_add_net` | Implemented |
| `delete_trace` | `_ipc_delete_trace` | Falls back to SWIG |
| `get_nets_list` | `_ipc_get_nets_list` | Implemented |
| `add_copper_pour` | `_ipc_add_copper_pour` | Implemented |
| `refill_zones` | `_ipc_refill_zones` | Implemented |
| `add_text` | `_ipc_add_text` | Implemented |
| `add_board_text` | `_ipc_add_text` | Implemented |
| `set_board_size` | `_ipc_set_board_size` | Implemented |
| `get_board_info` | `_ipc_get_board_info` | Implemented |
| `add_board_outline` | `_ipc_add_board_outline` | Implemented |
| `add_mounting_hole` | `_ipc_add_mounting_hole` | Implemented |
| `get_layer_list` | `_ipc_get_layer_list` | Implemented |
| `place_component` | `_ipc_place_component` | Implemented (hybrid) |
| `move_component` | `_ipc_move_component` | Implemented |
| `rotate_component` | `_ipc_rotate_component` | Implemented |
| `delete_component` | `_ipc_delete_component` | Implemented |
| `get_component_list` | `_ipc_get_component_list` | Implemented |
| `get_component_properties` | `_ipc_get_component_properties` | Implemented |
| `save_project` | `_ipc_save_project` | Implemented |
### Implemented Backend Features
**Core Connection:**
- Connect to running KiCAD instance
- Auto-detect socket path (`/tmp/kicad/api.sock`)
- Version checking and validation
- Auto-fallback to SWIG when IPC unavailable
- Change notification callbacks
**Board Operations:**
- Get board reference
- Get/Set board size
- List enabled layers
- Save board
- Add board outline segments
- Add mounting holes
**Component Operations:**
- List all components
- Place component (hybrid: SWIG for library loading, IPC for placement)
- Move component
- Rotate component
- Delete component
- Get component properties
**Routing Operations:**
- Add track
- Add via
- Get all tracks
- Get all vias
- Get all nets
**Zone Operations:**
- Add copper pour zones
- Get zones list
- Refill zones
**UI Integration:**
- Add text to board
- Get current selection
- Clear selection
**Transaction Support:**
- Begin transaction
- Commit transaction (with description for undo)
- Rollback transaction
## Usage
### Prerequisites
1. **KiCAD 9.0+** must be running
2. **IPC API must be enabled**: `Preferences > Plugins > Enable IPC API Server`
3. A board must be open in the PCB editor
### Installation
```bash
pip install kicad-python
```
### Testing
Run the test script to verify IPC functionality:
```bash
# Make sure KiCAD is running with IPC enabled and a board open
./venv/bin/python python/test_ipc_backend.py
```
## Architecture
```
+-------------------------------------------------------------+
| MCP Server (TypeScript/Node.js) |
+---------------------------+---------------------------------+
| JSON commands
+---------------------------v---------------------------------+
| Python Interface Layer |
| +--------------------------------------------------------+ |
| | kicad_interface.py | |
| | - Routes commands to IPC or SWIG handlers | |
| | - IPC_CAPABLE_COMMANDS dict defines routing | |
| +--------------------------------------------------------+ |
| +--------------------------------------------------------+ |
| | kicad_api/ipc_backend.py | |
| | - IPCBackend (connection management) | |
| | - IPCBoardAPI (board operations) | |
| +--------------------------------------------------------+ |
+---------------------------+---------------------------------+
| kicad-python (kipy) library
+---------------------------v---------------------------------+
| Protocol Buffers over UNIX Sockets |
+---------------------------+---------------------------------+
|
+---------------------------v---------------------------------+
| KiCAD 9.0+ (IPC Server) |
+-------------------------------------------------------------+
```
## Known Limitations
1. **KiCAD must be running**: Unlike SWIG, IPC requires KiCAD to be open
2. **Project creation**: Not supported via IPC, uses file system
3. **Footprint library access**: Uses hybrid approach (SWIG loads from library, IPC places)
4. **Delete trace**: Falls back to SWIG (IPC API doesn't support direct deletion)
5. **Some operations may not work as expected**: This is experimental code
## Troubleshooting
### "Connection failed"
- Ensure KiCAD is running
- Enable IPC API: `Preferences > Plugins > Enable IPC API Server`
- Check if a board is open
### "kicad-python not found"
```bash
pip install kicad-python
```
### "Version mismatch"
- Update kicad-python: `pip install --upgrade kicad-python`
- Ensure KiCAD 9.0+ is installed
### "No board open"
- Open a board in KiCAD's PCB editor before connecting
## File Structure
```
python/kicad_api/
├── __init__.py # Package exports
├── base.py # Abstract base classes
├── factory.py # Backend auto-detection
├── ipc_backend.py # IPC implementation
└── swig_backend.py # Legacy SWIG wrapper
python/
└── test_ipc_backend.py # IPC test script
```
## Future Work
1. More comprehensive testing of all IPC commands
2. Footprint library integration via IPC (when kipy supports it)
3. Schematic IPC support (when available in kicad-python)
4. Event subscriptions to react to changes made in KiCAD UI
5. Multi-board support
## Related Documentation
- [ROADMAP.md](./ROADMAP.md) - Project roadmap
- [IPC_API_MIGRATION_PLAN.md](./IPC_API_MIGRATION_PLAN.md) - Migration details
- [REALTIME_WORKFLOW.md](./REALTIME_WORKFLOW.md) - Collaboration workflows
- [kicad-python docs](https://docs.kicad.org/kicad-python-main/) - Official API docs
---
**Last Updated:** 2026-03-21

View File

@@ -1,344 +1,374 @@
# JLCPCB Parts Integration - Complete Guide
## Overview
The KiCAD MCP Server integrates with JLCPCB's parts library to provide intelligent component selection, cost optimization, and automated part sourcing for PCB assembly.
**Current Implementation**: Uses the **JLCSearch public API** (by tscircuit) for free, unauthenticated access to JLCPCB's ~100k parts catalog.
## Features
**Parametric Search** - Find components by specifications (resistance, capacitance, package, etc.)
**Price Comparison** - Compare Basic vs Extended library pricing
**Alternative Suggestions** - Find cheaper or higher-stock alternatives
**Footprint Mapping** - Automatic JLCPCB package to KiCad footprint mapping
**Stock Availability** - Real-time stock levels from JLCPCB
**No Authentication Required** - Public API, no API keys needed
## Quick Start
### 1. Search for Components
```python
from commands.jlcsearch import JLCSearchClient
client = JLCSearchClient()
# Search for resistors
resistors = client.search_resistors(
resistance=10000, # 10kΩ
package="0603",
limit=20
)
# Search for capacitors
capacitors = client.search_capacitors(
capacitance=1e-7, # 100nF
package="0603",
limit=20
)
# General component search
components = client.search_components(
"components",
package="0603",
limit=100
)
```
### 2. Get Part Details
```python
# Get specific part by LCSC number
part = client.get_part_by_lcsc(25804) # C25804
print(f"Part: {part['mfr']}")
print(f"Stock: {part['stock']}")
print(f"Price: ${part['price1']}")
print(f"Basic Library: {part['is_basic']}")
```
### 3. Database Integration
```python
from commands.jlcpcb_parts import JLCPCBPartsManager
# Initialize database
db = JLCPCBPartsManager() # Uses data/jlcpcb_parts.db
# Download and import parts (one-time setup)
client = JLCSearchClient()
parts = client.download_all_components()
db.import_jlcsearch_parts(parts)
# Search imported database
results = db.search_parts(
query="resistor",
package="0603",
library_type="Basic",
in_stock=True,
limit=20
)
```
### 4. Footprint Mapping
```python
# Map JLCPCB package to KiCad footprints
footprints = db.map_package_to_footprint("0603")
# Returns:
# [
# "Resistor_SMD:R_0603_1608Metric",
# "Capacitor_SMD:C_0603_1608Metric",
# "LED_SMD:LED_0603_1608Metric"
# ]
```
## API Reference
### JLCSearchClient
#### `search_resistors(resistance, package, limit)`
Search for resistors by value and package.
**Parameters:**
- `resistance` (int, optional): Resistance in ohms
- `package` (str, optional): Package size ("0402", "0603", "0805", etc.)
- `limit` (int): Maximum results (default: 100)
**Returns:** List of resistor dicts with fields:
- `lcsc`: LCSC number (integer)
- `mfr`: Manufacturer part number
- `package`: Package size
- `is_basic`: True if Basic library part (no assembly fee)
- `resistance`: Resistance in ohms
- `tolerance_fraction`: Tolerance (0.01 = 1%)
- `power_watts`: Power rating in mW
- `stock`: Available stock
- `price1`: Unit price in USD
#### `search_capacitors(capacitance, package, limit)`
Search for capacitors by value and package.
**Parameters:**
- `capacitance` (float, optional): Capacitance in farads (e.g., 1e-7 for 100nF)
- `package` (str, optional): Package size
- `limit` (int): Maximum results
**Returns:** List of capacitor dicts
#### `search_components(category, limit, offset, **filters)`
General component search.
**Parameters:**
- `category` (str): "resistors", "capacitors", "components", etc.
- `limit` (int): Maximum results
- `offset` (int): Pagination offset
- `**filters`: Additional filters (package="0603", lcsc=25804, etc.)
**Returns:** List of component dicts
#### `download_all_components(callback, batch_size)`
Download entire JLCPCB parts catalog.
**Parameters:**
- `callback` (callable, optional): Progress callback(parts_count, status_msg)
- `batch_size` (int): Parts per batch (default: 1000)
**Returns:** List of all parts (~100k components)
**Note:** This may take 5-10 minutes to complete.
### JLCPCBPartsManager
#### `import_jlcsearch_parts(parts, progress_callback)`
Import parts from JLCSearch into local SQLite database.
**Parameters:**
- `parts` (list): List of part dicts from JLCSearchClient
- `progress_callback` (callable, optional): Progress updates
#### `search_parts(query, category, package, library_type, manufacturer, in_stock, limit)`
Search local database with filters.
**Parameters:**
- `query` (str, optional): Free-text search
- `category` (str, optional): Category filter
- `package` (str, optional): Package filter
- `library_type` (str, optional): "Basic", "Extended", or "Preferred"
- `manufacturer` (str, optional): Manufacturer filter
- `in_stock` (bool): Only in-stock parts (default: True)
- `limit` (int): Maximum results
**Returns:** List of matching parts
#### `get_part_info(lcsc_number)`
Get detailed part information.
**Parameters:**
- `lcsc_number` (str): LCSC part number (e.g., "C25804")
**Returns:** Part dict or None
#### `get_database_stats()`
Get database statistics.
**Returns:** Dict with:
- `total_parts`: Total parts count
- `basic_parts`: Basic library count
- `extended_parts`: Extended library count
- `in_stock`: Parts with stock > 0
- `db_path`: Database file path
#### `map_package_to_footprint(package)`
Map JLCPCB package to KiCad footprints.
**Parameters:**
- `package` (str): JLCPCB package name
**Returns:** List of KiCad footprint library references
## Data Format
### JLCSearch Part Object
```json
{
"lcsc": 25804,
"mfr": "0603WAF1002T5E",
"package": "0603",
"is_basic": true,
"is_preferred": false,
"resistance": 10000,
"tolerance_fraction": 0.01,
"power_watts": 100,
"stock": 37165617,
"price1": 0.000842857
}
```
### Database Schema
```sql
CREATE TABLE components (
lcsc TEXT PRIMARY KEY, -- "C25804"
category TEXT, -- "Resistors"
subcategory TEXT, -- "Chip Resistor"
mfr_part TEXT, -- "0603WAF1002T5E"
package TEXT, -- "0603"
solder_joints INTEGER,
manufacturer TEXT,
library_type TEXT, -- "Basic" or "Extended"
description TEXT, -- "10kΩ ±1% 100mW"
datasheet TEXT,
stock INTEGER,
price_json TEXT, -- JSON array of price breaks
last_updated INTEGER -- Unix timestamp
);
```
## Package to Footprint Mappings
| JLCPCB Package | KiCad Footprints |
|----------------|------------------|
| 0402 | Resistor_SMD:R_0402_1005Metric<br>Capacitor_SMD:C_0402_1005Metric<br>LED_SMD:LED_0402_1005Metric |
| 0603 | Resistor_SMD:R_0603_1608Metric<br>Capacitor_SMD:C_0603_1608Metric<br>LED_SMD:LED_0603_1608Metric |
| 0805 | Resistor_SMD:R_0805_2012Metric<br>Capacitor_SMD:C_0805_2012Metric |
| 1206 | Resistor_SMD:R_1206_3216Metric<br>Capacitor_SMD:C_1206_3216Metric |
| SOT-23 | Package_TO_SOT_SMD:SOT-23<br>Package_TO_SOT_SMD:SOT-23-3 |
| SOT-23-5 | Package_TO_SOT_SMD:SOT-23-5 |
| SOT-23-6 | Package_TO_SOT_SMD:SOT-23-6 |
| SOT-223 | Package_TO_SOT_SMD:SOT-223 |
| SOIC-8 | Package_SO:SOIC-8_3.9x4.9mm_P1.27mm |
| QFN-20 | Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm |
## Best Practices
### 1. Always Use Basic Library Parts First
Basic library parts have **no assembly fee** ($0/part), while Extended parts cost **$3/part**.
```python
# Filter for Basic parts only
basic_parts = [p for p in results if p['is_basic']]
```
### 2. Check Stock Availability
Ensure sufficient stock before committing to a design.
```python
# Only use parts with >1000 stock
high_stock = [p for p in results if p['stock'] > 1000]
```
### 3. Compare Prices
Even within Basic library, prices vary significantly.
```python
# Find cheapest option
cheapest = min(results, key=lambda x: x.get('price1', 999))
```
### 4. Use Standardized Packages
Stick to common packages (0402, 0603, 0805) for better availability and pricing.
### 5. Cache Database Locally
Download the full parts database once and search locally for faster results.
```python
# Initial download (one-time, ~5-10 minutes)
if not os.path.exists("data/jlcpcb_parts.db"):
parts = client.download_all_components()
db.import_jlcsearch_parts(parts)
# Subsequent searches use local database (instant)
results = db.search_parts(...)
```
## Troubleshooting
### API Rate Limiting
JLCSearch is a community service. If you hit rate limits:
- Add delays between requests (`time.sleep(0.1)`)
- Use the local database instead of repeated API calls
- Download the full database once and work offline
### Missing Data
JLCSearch may not have all fields that official JLCPCB API provides:
- No datasheets (use manufacturer website)
- Limited category information
- No solder joint count
### Stock Discrepancies
Stock levels are updated periodically but may lag real-time JLCPCB data by a few hours.
## Official JLCPCB API (Alternative)
The project also includes an implementation of the official JLCPCB API with HMAC-SHA256 authentication. However, this requires:
1. API approval from JLCPCB (not all applications are approved)
2. APP_ID, ACCESS_KEY, and SECRET_KEY credentials
3. Previous order history with JLCPCB
To use the official API instead of JLCSearch:
```python
from commands.jlcpcb import JLCPCBClient
# Set credentials in .env file:
# JLCPCB_APP_ID=<your_app_id>
# JLCPCB_API_KEY=<your_access_key>
# JLCPCB_API_SECRET=<your_secret_key>
client = JLCPCBClient(app_id, access_key, secret_key)
data = client.fetch_parts_page()
```
**Note:** Most users should use JLCSearch public API instead, as it's freely available and requires no authentication.
## Credits
- **JLCSearch API**: https://jlcsearch.tscircuit.com/ (by [@tscircuit](https://github.com/tscircuit/jlcsearch))
- **JLCParts Database**: https://github.com/yaqwsx/jlcparts (by [@yaqwsx](https://github.com/yaqwsx))
- **JLCPCB**: https://jlcpcb.com/ (official parts library provider)
## License
This integration uses publicly available JLCPCB parts data via the JLCSearch community service. Users must comply with JLCPCB's terms of service when using this data for production PCB orders.
# JLCPCB Parts Integration - Complete Guide
## Overview
The KiCAD MCP Server integrates with JLCPCB's parts library to provide intelligent component selection, cost optimization, and automated part sourcing for PCB assembly.
**Current Implementation**: Uses the **JLCSearch public API** (by tscircuit) for free, unauthenticated access to JLCPCB's ~100k parts catalog.
## Features
**Parametric Search** - Find components by specifications (resistance, capacitance, package, etc.)
**Price Comparison** - Compare Basic vs Extended library pricing
**Alternative Suggestions** - Find cheaper or higher-stock alternatives
**Footprint Mapping** - Automatic JLCPCB package to KiCad footprint mapping
**Stock Availability** - Real-time stock levels from JLCPCB
**No Authentication Required** - Public API, no API keys needed
## Quick Start
### 1. Search for Components
```python
from commands.jlcsearch import JLCSearchClient
client = JLCSearchClient()
# Search for resistors
resistors = client.search_resistors(
resistance=10000, # 10kΩ
package="0603",
limit=20
)
# Search for capacitors
capacitors = client.search_capacitors(
capacitance=1e-7, # 100nF
package="0603",
limit=20
)
# General component search
components = client.search_components(
"components",
package="0603",
limit=100
)
```
### 2. Get Part Details
```python
# Get specific part by LCSC number
part = client.get_part_by_lcsc(25804) # C25804
print(f"Part: {part['mfr']}")
print(f"Stock: {part['stock']}")
print(f"Price: ${part['price1']}")
print(f"Basic Library: {part['is_basic']}")
```
### 3. Database Integration
```python
from commands.jlcpcb_parts import JLCPCBPartsManager
# Initialize database
db = JLCPCBPartsManager() # Uses data/jlcpcb_parts.db
# Download and import parts (one-time setup)
client = JLCSearchClient()
parts = client.download_all_components()
db.import_jlcsearch_parts(parts)
# Search imported database
results = db.search_parts(
query="resistor",
package="0603",
library_type="Basic",
in_stock=True,
limit=20
)
```
### 4. Footprint Mapping
```python
# Map JLCPCB package to KiCad footprints
footprints = db.map_package_to_footprint("0603")
# Returns:
# [
# "Resistor_SMD:R_0603_1608Metric",
# "Capacitor_SMD:C_0603_1608Metric",
# "LED_SMD:LED_0603_1608Metric"
# ]
```
## API Reference
### JLCSearchClient
#### `search_resistors(resistance, package, limit)`
Search for resistors by value and package.
**Parameters:**
- `resistance` (int, optional): Resistance in ohms
- `package` (str, optional): Package size ("0402", "0603", "0805", etc.)
- `limit` (int): Maximum results (default: 100)
**Returns:** List of resistor dicts with fields:
- `lcsc`: LCSC number (integer)
- `mfr`: Manufacturer part number
- `package`: Package size
- `is_basic`: True if Basic library part (no assembly fee)
- `resistance`: Resistance in ohms
- `tolerance_fraction`: Tolerance (0.01 = 1%)
- `power_watts`: Power rating in mW
- `stock`: Available stock
- `price1`: Unit price in USD
#### `search_capacitors(capacitance, package, limit)`
Search for capacitors by value and package.
**Parameters:**
- `capacitance` (float, optional): Capacitance in farads (e.g., 1e-7 for 100nF)
- `package` (str, optional): Package size
- `limit` (int): Maximum results
**Returns:** List of capacitor dicts
#### `search_components(category, limit, offset, **filters)`
General component search.
**Parameters:**
- `category` (str): "resistors", "capacitors", "components", etc.
- `limit` (int): Maximum results
- `offset` (int): Pagination offset
- `**filters`: Additional filters (package="0603", lcsc=25804, etc.)
**Returns:** List of component dicts
#### `download_all_components(callback, batch_size)`
Download entire JLCPCB parts catalog.
**Parameters:**
- `callback` (callable, optional): Progress callback(parts_count, status_msg)
- `batch_size` (int): Parts per batch (default: 1000)
**Returns:** List of all parts (~100k components)
**Note:** This may take 5-10 minutes to complete.
### JLCPCBPartsManager
#### `import_jlcsearch_parts(parts, progress_callback)`
Import parts from JLCSearch into local SQLite database.
**Parameters:**
- `parts` (list): List of part dicts from JLCSearchClient
- `progress_callback` (callable, optional): Progress updates
#### `search_parts(query, category, package, library_type, manufacturer, in_stock, limit)`
Search local database with filters.
**Parameters:**
- `query` (str, optional): Free-text search
- `category` (str, optional): Category filter
- `package` (str, optional): Package filter
- `library_type` (str, optional): "Basic", "Extended", or "Preferred"
- `manufacturer` (str, optional): Manufacturer filter
- `in_stock` (bool): Only in-stock parts (default: True)
- `limit` (int): Maximum results
**Returns:** List of matching parts
#### `get_part_info(lcsc_number)`
Get detailed part information.
**Parameters:**
- `lcsc_number` (str): LCSC part number (e.g., "C25804")
**Returns:** Part dict or None
#### `get_database_stats()`
Get database statistics.
**Returns:** Dict with:
- `total_parts`: Total parts count
- `basic_parts`: Basic library count
- `extended_parts`: Extended library count
- `in_stock`: Parts with stock > 0
- `db_path`: Database file path
#### `map_package_to_footprint(package)`
Map JLCPCB package to KiCad footprints.
**Parameters:**
- `package` (str): JLCPCB package name
**Returns:** List of KiCad footprint library references
## Data Format
### JLCSearch Part Object
```json
{
"lcsc": 25804,
"mfr": "0603WAF1002T5E",
"package": "0603",
"is_basic": true,
"is_preferred": false,
"resistance": 10000,
"tolerance_fraction": 0.01,
"power_watts": 100,
"stock": 37165617,
"price1": 0.000842857
}
```
### Database Schema
```sql
CREATE TABLE components (
lcsc TEXT PRIMARY KEY, -- "C25804"
category TEXT, -- "Resistors"
subcategory TEXT, -- "Chip Resistor"
mfr_part TEXT, -- "0603WAF1002T5E"
package TEXT, -- "0603"
solder_joints INTEGER,
manufacturer TEXT,
library_type TEXT, -- "Basic" or "Extended"
description TEXT, -- "10kΩ ±1% 100mW"
datasheet TEXT,
stock INTEGER,
price_json TEXT, -- JSON array of price breaks
last_updated INTEGER -- Unix timestamp
);
```
## Package to Footprint Mappings
| JLCPCB Package | KiCad Footprints |
| -------------- | ------------------------------------------------------------------------------------------------ |
| 0402 | Resistor_SMD:R_0402_1005Metric<br>Capacitor_SMD:C_0402_1005Metric<br>LED_SMD:LED_0402_1005Metric |
| 0603 | Resistor_SMD:R_0603_1608Metric<br>Capacitor_SMD:C_0603_1608Metric<br>LED_SMD:LED_0603_1608Metric |
| 0805 | Resistor_SMD:R_0805_2012Metric<br>Capacitor_SMD:C_0805_2012Metric |
| 1206 | Resistor_SMD:R_1206_3216Metric<br>Capacitor_SMD:C_1206_3216Metric |
| SOT-23 | Package_TO_SOT_SMD:SOT-23<br>Package_TO_SOT_SMD:SOT-23-3 |
| SOT-23-5 | Package_TO_SOT_SMD:SOT-23-5 |
| SOT-23-6 | Package_TO_SOT_SMD:SOT-23-6 |
| SOT-223 | Package_TO_SOT_SMD:SOT-223 |
| SOIC-8 | Package_SO:SOIC-8_3.9x4.9mm_P1.27mm |
| QFN-20 | Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm |
## Best Practices
### 1. Always Use Basic Library Parts First
Basic library parts have **no assembly fee** ($0/part), while Extended parts cost **$3/part**.
```python
# Filter for Basic parts only
basic_parts = [p for p in results if p['is_basic']]
```
### 2. Check Stock Availability
Ensure sufficient stock before committing to a design.
```python
# Only use parts with >1000 stock
high_stock = [p for p in results if p['stock'] > 1000]
```
### 3. Compare Prices
Even within Basic library, prices vary significantly.
```python
# Find cheapest option
cheapest = min(results, key=lambda x: x.get('price1', 999))
```
### 4. Use Standardized Packages
Stick to common packages (0402, 0603, 0805) for better availability and pricing.
### 5. Cache Database Locally
Download the full parts database once and search locally for faster results.
```python
# Initial download (one-time, ~5-10 minutes)
if not os.path.exists("data/jlcpcb_parts.db"):
parts = client.download_all_components()
db.import_jlcsearch_parts(parts)
# Subsequent searches use local database (instant)
results = db.search_parts(...)
```
## Troubleshooting
### API Rate Limiting
JLCSearch is a community service. If you hit rate limits:
- Add delays between requests (`time.sleep(0.1)`)
- Use the local database instead of repeated API calls
- Download the full database once and work offline
### Missing Data
JLCSearch may not have all fields that official JLCPCB API provides:
- No datasheets (use manufacturer website)
- Limited category information
- No solder joint count
### Stock Discrepancies
Stock levels are updated periodically but may lag real-time JLCPCB data by a few hours.
## Official JLCPCB API (Alternative)
The project also includes an implementation of the official JLCPCB API with HMAC-SHA256 authentication. However, this requires:
1. API approval from JLCPCB (not all applications are approved)
2. APP_ID, ACCESS_KEY, and SECRET_KEY credentials
3. Previous order history with JLCPCB
To use the official API instead of JLCSearch:
```python
from commands.jlcpcb import JLCPCBClient
# Set credentials in .env file:
# JLCPCB_APP_ID=<your_app_id>
# JLCPCB_API_KEY=<your_access_key>
# JLCPCB_API_SECRET=<your_secret_key>
client = JLCPCBClient(app_id, access_key, secret_key)
data = client.fetch_parts_page()
```
**Note:** Most users should use JLCSearch public API instead, as it's freely available and requires no authentication.
## Credits
- **JLCSearch API**: https://jlcsearch.tscircuit.com/ (by [@tscircuit](https://github.com/tscircuit/jlcsearch))
- **JLCParts Database**: https://github.com/yaqwsx/jlcparts (by [@yaqwsx](https://github.com/yaqwsx))
- **JLCPCB**: https://jlcpcb.com/ (official parts library provider)
## License
This integration uses publicly available JLCPCB parts data via the JLCSearch community service. Users must comply with JLCPCB's terms of service when using this data for production PCB orders.

File diff suppressed because it is too large Load Diff

View File

@@ -1,203 +1,191 @@
# Known Issues & Workarounds
**Last Updated:** 2025-12-02
**Version:** 2.1.0-alpha
This document tracks known issues and provides workarounds where available.
---
## Current Issues
### 1. `get_board_info` KiCAD 9.0 API Issue
**Status:** KNOWN - Non-critical
**Symptoms:**
```
AttributeError: 'BOARD' object has no attribute 'LT_USER'
```
**Root Cause:** KiCAD 9.0 changed layer enumeration constants
**Workaround:** Use `get_project_info` instead for basic project details
**Impact:** Low - informational command only
---
### 2. Zone Filling via SWIG Causes Segfault
**Status:** KNOWN - Workaround available
**Symptoms:**
- Copper pours created but not filled automatically when using SWIG backend
- Calling `ZONE_FILLER` via SWIG causes segfault
**Workaround Options:**
1. Use IPC backend (zones fill correctly via IPC)
2. Open the board in KiCAD UI - zones fill automatically when opened
**Impact:** Medium - affects copper pour visualization until opened in KiCAD
---
### 3. UI Manual Reload Required (SWIG Backend)
**Status:** BY DESIGN - Fixed by IPC
**Symptoms:**
- MCP makes changes via SWIG backend
- KiCAD doesn't show changes until file is reloaded
**Current Workflow:**
```
1. MCP makes change via SWIG
2. KiCAD shows: "File has been modified. Reload? [Yes] [No]"
3. User clicks "Yes"
4. Changes appear in UI
```
**Why:** SWIG-based backend requires file I/O, can't push changes to running UI
**Fix:** Use IPC backend for real-time updates (requires KiCAD to be running with IPC enabled)
**Workaround:** Click reload prompt or use File > Revert
---
### 4. IPC Backend Experimental
**Status:** UNDER DEVELOPMENT
**Description:**
The IPC backend is currently being implemented and tested. Some commands may not work as expected in all scenarios.
**Known IPC Limitations:**
- KiCAD must be running with IPC enabled
- Some commands fall back to SWIG (e.g., delete_trace)
- Footprint loading uses hybrid approach (SWIG for library, IPC for placement)
- Error handling may not be comprehensive in all cases
**Workaround:** If IPC fails, the server automatically falls back to SWIG backend
---
---
## Recently Fixed
### Schematic Component Corruption (Fixed 2026-02-26)
**Was:** `add_schematic_component` corrupted .kicad_sch files due to sexpdata formatting issues
**Now:** Rewritten to use text manipulation, preserves KiCAD file formatting perfectly
**Impact:** Schematic workflow fully functional with all component types
**Fixed in:** PR #40, commit a69d288
### DRC Violations API KiCAD 9.0 (Fixed 2026-02-26)
**Was:** `get_drc_violations` failed with `AttributeError: 'BOARD' object has no attribute 'GetDRCMarkers'`
**Now:** Reimplemented to use `run_drc()` internally which calls kicad-cli
**Impact:** Maintains backward compatibility while using stable kicad-cli interface
### Component Library Integration (Fixed 2025-11-01)
**Was:** Could not find footprint libraries
**Now:** Auto-discovers 153 KiCAD footprint libraries, search and list working
### Routing Operations KiCAD 9.0 (Fixed 2025-11-01)
**Was:** Multiple API compatibility issues with KiCAD 9.0
**Now:** All routing commands tested and working:
- `netinfo.FindNet()` -> `netinfo.NetsByName()[name]`
- `zone.SetPriority()` -> `zone.SetAssignedPriority()`
- `ZONE_FILL_MODE_POLYGON` -> `ZONE_FILL_MODE_POLYGONS`
### KiCAD Process Detection (Fixed 2025-10-26)
**Was:** `check_kicad_ui` detected MCP server's own processes
**Now:** Properly filters to only detect actual KiCAD binaries
### set_board_size KiCAD 9.0 (Fixed 2025-10-26)
**Was:** Failed with `BOX2I_SetSize` type error
**Now:** Works with KiCAD 9.0 API
### add_board_text KiCAD 9.0 (Fixed 2025-10-26)
**Was:** Failed with `EDA_ANGLE` type error
**Now:** Works with KiCAD 9.0 API
### Schematic Parameter Mismatch (Fixed 2025-12-02)
**Was:** `create_schematic` failed due to parameter name differences between TypeScript and Python
**Now:** Accepts multiple parameter naming conventions (`name`, `projectName`, `title`, `filename`)
---
## Reporting New Issues
If you encounter an issue not listed here:
1. **Check MCP logs:** `~/.kicad-mcp/logs/kicad_interface.log`
2. **Check KiCAD version:** `python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"` (must be 9.0+)
3. **Try the operation in KiCAD directly** - is it a KiCAD issue?
4. **Open GitHub issue** with:
- Error message
- Log excerpt
- Steps to reproduce
- KiCAD version
- OS and version
---
## Priority Matrix
| Issue | Priority | Impact | Status |
|-------|----------|--------|--------|
| IPC Backend Testing | High | Medium | In Progress |
| get_board_info Fix | Low | Low | Known |
| Zone Filling (SWIG) | Medium | Medium | Workaround Available |
| Schematic Support | Medium | Medium | Partial |
---
## General Workarounds
### Server Won't Start
```bash
# Check Python can import pcbnew
python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
# Check paths
python3 python/utils/platform_helper.py
```
### Commands Fail After Server Restart
```
# Board reference is lost on restart
# Always run open_project after server restart
```
### KiCAD UI Doesn't Show Changes (SWIG Mode)
```
# File > Revert (or click reload prompt)
# Or: Close and reopen file in KiCAD
# Or: Use IPC backend for automatic updates
```
### IPC Not Connecting
```
# Ensure KiCAD is running
# Enable IPC: Preferences > Plugins > Enable IPC API Server
# Have a board open in PCB editor
# Check socket exists: ls /tmp/kicad/api.sock
```
---
**Need Help?**
- Check [IPC_BACKEND_STATUS.md](IPC_BACKEND_STATUS.md) for IPC details
- Check [REALTIME_WORKFLOW.md](REALTIME_WORKFLOW.md) for workflow tips
- Check logs: `~/.kicad-mcp/logs/kicad_interface.log`
- Open an issue on GitHub
# Known Issues & Workarounds
**Last Updated:** 2026-03-21
**Version:** 2.2.3
This document tracks known issues and provides workarounds where available.
---
## Current Issues
### 1. `get_board_info` KiCAD 9.0 API Issue
**Status:** KNOWN - Non-critical
**Symptoms:**
```
AttributeError: 'BOARD' object has no attribute 'LT_USER'
```
**Root Cause:** KiCAD 9.0 changed layer enumeration constants
**Workaround:** Use `get_project_info` instead for basic project details
**Impact:** Low - informational command only
---
### 2. Zone Filling via SWIG Causes Segfault
**Status:** KNOWN - Workaround available
**Symptoms:**
- Copper pours created but not filled automatically when using SWIG backend
- Calling `ZONE_FILLER` via SWIG causes segfault
**Workaround Options:**
1. Use IPC backend (zones fill correctly via IPC)
2. Open the board in KiCAD UI -- zones fill automatically when opened
3. Use `refill_zones` tool (may still segfault in some configurations)
**Impact:** Medium - affects copper pour visualization until opened in KiCAD
---
### 3. UI Manual Reload Required (SWIG Backend)
**Status:** BY DESIGN
**Symptoms:**
- MCP makes changes via SWIG backend
- KiCAD does not show changes until file is reloaded
**Why:** SWIG-based backend modifies files directly and cannot push changes to a running UI
**Fix:** Use IPC backend for real-time updates (requires KiCAD running with IPC enabled)
**Workaround:** Click the reload prompt in KiCAD or use File > Revert
---
### 4. IPC Backend Limitations
**Status:** EXPERIMENTAL
**Known Limitations:**
- KiCAD must be running with IPC enabled (Preferences > Plugins > Enable IPC API Server)
- Some commands fall back to SWIG (e.g., delete_trace)
- Footprint loading uses hybrid approach (SWIG for library, IPC for placement)
**Workaround:** The server automatically falls back to SWIG backend when IPC is unavailable
---
### 5. package.json Version Mismatch
**Status:** KNOWN - Non-critical
**Symptoms:** package.json shows version 2.1.0-alpha while CHANGELOG documents version 2.2.3
**Impact:** Cosmetic only. CHANGELOG.md is the authoritative version reference.
---
## Recently Fixed (v2.2.0 - v2.2.3)
### B.Cu Footprint Routing (Fixed v2.2.3)
- `route_pad_to_pad` now correctly detects B.Cu footprints and inserts vias
- KiCAD 9 SWIG `pad.GetLayerName()` always returned F.Cu for flipped footprints -- fixed using `footprint.GetLayer()`
### B.Cu Placement Hang (Fixed v2.2.3)
- Placing footprints on B.Cu no longer causes ~30s freeze
- Fix: call `board.Add()` before `Flip()`
### Board Outline Rounded Corners (Fixed v2.2.3)
- `add_board_outline` now correctly applies cornerRadius for rounded_rectangle shape
### Project-Local Library Resolution (Fixed v2.2.2)
- `add_schematic_component` and `place_component` now search project-local sym-lib-table and fp-lib-table
- Previously only global KiCAD library paths were searched
### Template File Corruption (Fixed v2.2.2)
- Removed invalid `;;` comment lines from template schematics
- Restored KiCAD 9 format version (20250114) in templates
### copy_routing_pattern Empty Results (Fixed v2.2.2)
- Added geometric fallback when pads have no net assignments
### Schematic Component Corruption (Fixed v2.2.1)
- `add_schematic_component` no longer corrupts .kicad_sch files
- Rewritten to use text manipulation instead of sexpdata formatting
### SWIG/UUID Comparison Bugs (Fixed v2.2.0)
- Fixed SwigPyObject UUID comparison
- Fixed SWIG iterator invalidation after board.Remove()
- Added board.SetModified() to prevent dangling pointer crashes
---
## Reporting New Issues
If you encounter an issue not listed here:
1. **Check MCP logs:** `~/.kicad-mcp/logs/kicad_interface.log`
2. **Enable developer mode:** Set `KICAD_MCP_DEV=1` to capture session logs
3. **Check KiCAD version:** `python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"` (must be 9.0+)
4. **Try the operation in KiCAD directly** -- is it a KiCAD issue?
5. **Open a GitHub issue** with:
- Error message and log excerpt
- Steps to reproduce
- KiCAD version and OS
- MCP session log (from `logs/` folder if dev mode is enabled)
---
## General Workarounds
### Server Will Not Start
```bash
# Check Python can import pcbnew
python3 -c "import pcbnew; print(pcbnew.GetBuildVersion())"
# Check paths
python3 python/utils/platform_helper.py
```
### Commands Fail After Server Restart
```
# Board reference is lost on restart
# Always run open_project after server restart
```
### KiCAD UI Does Not Show Changes (SWIG Mode)
```
# File > Revert (or click reload prompt)
# Or: Close and reopen file in KiCAD
# Or: Use IPC backend for automatic updates
```
### IPC Not Connecting
```
# Ensure KiCAD is running
# Enable IPC: Preferences > Plugins > Enable IPC API Server
# Have a board open in PCB editor
# Check socket exists: ls /tmp/kicad/api.sock
```
---
**Need Help?**
- Check [IPC_BACKEND_STATUS.md](IPC_BACKEND_STATUS.md) for IPC details
- Check logs: `~/.kicad-mcp/logs/kicad_interface.log`
- Open an issue on GitHub

View File

@@ -1,352 +1,390 @@
# KiCAD Footprint Library Integration
**Status:** ✅ COMPLETE (Week 2 - Component Library Integration)
**Date:** 2025-11-01
**Version:** 2.1.0-alpha
## Overview
The KiCAD MCP Server now includes full footprint library integration, enabling:
- ✅ Automatic discovery of all installed KiCAD footprint libraries
-Search and browse footprints across all libraries
-Component placement using library footprints
- ✅ Support for both `Library:Footprint` and `Footprint` formats
## How It Works
### Library Discovery
The `LibraryManager` class automatically discovers footprint libraries by:
1. **Parsing fp-lib-table files:**
- Global: `~/.config/kicad/9.0/fp-lib-table`
- Project-specific: `project-dir/fp-lib-table`
2. **Resolving environment variables:**
- `${KICAD9_FOOTPRINT_DIR}``/usr/share/kicad/footprints`
- `${K IPRJMOD}` → project directory
- Supports custom paths
3. **Indexing footprints:**
- Scans `.kicad_mod` files in each library
- Caches results for performance
- Provides fast search capabilities
### Supported Formats
**Library:Footprint format (recommended):**
```json
{
"componentId": "Resistor_SMD:R_0603_1608Metric"
}
```
**Footprint-only format (searches all libraries):**
```json
{
"componentId": "R_0603_1608Metric"
}
```
## New MCP Tools
### 1. `list_libraries`
List all available footprint libraries.
**Parameters:** None
**Returns:**
```json
{
"success": true,
"libraries": ["Resistor_SMD", "Capacitor_SMD", "LED_SMD", ...],
"count": 153
}
```
### 2. `search_footprints`
Search for footprints matching a pattern.
**Parameters:**
```json
{
"pattern": "*0603*", // Supports wildcards
"limit": 20 // Optional, default: 20
}
```
**Returns:**
```json
{
"success": true,
"footprints": [
{
"library": "Resistor_SMD",
"footprint": "R_0603_1608Metric",
"full_name": "Resistor_SMD:R_0603_1608Metric"
},
...
]
}
```
### 3. `list_library_footprints`
List all footprints in a specific library.
**Parameters:**
```json
{
"library": "Resistor_SMD"
}
```
**Returns:**
```json
{
"success": true,
"library": "Resistor_SMD",
"footprints": ["R_0402_1005Metric", "R_0603_1608Metric", ...],
"count": 120
}
```
### 4. `get_footprint_info`
Get detailed information about a specific footprint.
**Parameters:**
```json
{
"footprint": "Resistor_SMD:R_0603_1608Metric"
}
```
**Returns:**
```json
{
"success": true,
"footprint_info": {
"library": "Resistor_SMD",
"footprint": "R_0603_1608Metric",
"full_name": "Resistor_SMD:R_0603_1608Metric",
"library_path": "/usr/share/kicad/footprints/Resistor_SMD.pretty"
}
}
```
## Updated Component Placement
The `place_component` tool now uses the library system:
```json
{
"componentId": "Resistor_SMD:R_0603_1608Metric", // Library:Footprint format
"position": {"x": 50, "y": 40, "unit": "mm"},
"reference": "R1",
"value": "10k",
"rotation": 0,
"layer": "F.Cu"
}
```
**Features:**
- ✅ Automatic footprint discovery across all libraries
- ✅ Helpful error messages with suggestions
- ✅ Supports KiCAD 9.0 API (EDA_ANGLE, GetFPIDAsString)
## Example Usage (Claude Code)
**Search for a resistor footprint:**
```
User: "Find me a 0603 resistor footprint"
Claude: [uses search_footprints tool with pattern "*R_0603*"]
Found: Resistor_SMD:R_0603_1608Metric
```
**Place a component:**
```
User: "Place a 10k 0603 resistor at 50,40mm"
Claude: [uses place_component with "Resistor_SMD:R_0603_1608Metric"]
✅ Placed R1: 10k at (50, 40) mm
```
**List available capacitors:**
```
User: "What capacitor footprints are available?"
Claude: [uses list_library_footprints with "Capacitor_SMD"]
Found 103 capacitor footprints including:
- C_0402_1005Metric
- C_0603_1608Metric
- C_0805_2012Metric
...
```
## Configuration
### Custom Library Paths
The system automatically detects KiCAD installations, but you can add custom libraries:
1. **Via KiCAD Preferences:**
- Open KiCAD → Preferences → Manage Footprint Libraries
- Add your custom library paths
- The MCP server will automatically discover them
2. **Via Project fp-lib-table:**
- Create `fp-lib-table` in your project directory
- Follow the KiCAD S-expression format
### Supported Platforms
-**Linux:** `/usr/share/kicad/footprints`, `~/.config/kicad/9.0/`
-**Windows:** `C:/Program Files/KiCAD/*/share/kicad/footprints`
-**macOS:** `/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints`
## KiCAD 9.0 API Compatibility
The library integration includes full KiCAD 9.0 API support:
### Fixed API Changes:
1.`SetOrientation()` → now uses `EDA_ANGLE(degrees, DEGREES_T)`
2.`GetOrientation()` → returns `EDA_ANGLE`, call `.AsDegrees()`
3.`GetFootprintName()` → now `GetFPIDAsString()`
### Example Fixes:
**Old (KiCAD 8.0):**
```python
module.SetOrientation(90 * 10) # Decidegrees
rotation = module.GetOrientation() / 10
```
**New (KiCAD 9.0):**
```python
angle = pcbnew.EDA_ANGLE(90, pcbnew.DEGREES_T)
module.SetOrientation(angle)
rotation = module.GetOrientation().AsDegrees()
```
## Implementation Details
### LibraryManager Class
**Location:** `python/commands/library.py`
**Key Methods:**
- `_load_libraries()` - Parse fp-lib-table files
- `_parse_fp_lib_table()` - S-expression parser
- `_resolve_uri()` - Handle environment variables
- `find_footprint()` - Locate footprint in libraries
- `search_footprints()` - Pattern-based search
- `list_footprints()` - List library contents
**Performance:**
- Libraries loaded once at startup
- Footprint lists cached on first access
- Fast search using Python regex
- Minimal memory footprint
### Integration Points
1. **KiCADInterface (`kicad_interface.py`):**
- Creates `FootprintLibraryManager` on init
- Passes to `ComponentCommands`
- Routes library commands
2. **ComponentCommands (`component.py`):**
- Uses `LibraryManager.find_footprint()`
- Provides suggestions on errors
- Supports both lookup formats
3. **MCP Tools (`src/tools/index.ts`):**
- Exposes 4 new library tools
- Fully typed TypeScript interfaces
- Documented parameters
## Testing
**Test Coverage:**
- ✅ Library path discovery (Linux/Windows/macOS)
- ✅ fp-lib-table parsing
- ✅ Environment variable resolution
- ✅ Footprint search and lookup
- ✅ Component placement integration
- ✅ Error handling and suggestions
**Verified With:**
- KiCAD 9.0.5 on Ubuntu 24.04
- 153 standard libraries (8,000+ footprints)
- pcbnew Python API
## Known Limitations
1. **Library Updates:** Changes to fp-lib-table require server restart
2. **Custom Libraries:** Must be added via KiCAD preferences first
3. **Network Libraries:** GitHub-based libraries not yet supported
4. **Search Performance:** Linear search across all libraries (fast for <200 libs)
## Future Enhancements
- [ ] Watch fp-lib-table for changes (auto-reload)
- [ ] Support for GitHub library URLs
- [ ] Fuzzy search for typo tolerance
- [ ] Library metadata (descriptions, categories)
- [ ] Footprint previews (SVG/PNG generation)
- [ ] Most-used footprints caching
## Troubleshooting
### "No footprint libraries found"
**Cause:** fp-lib-table not found or empty
**Solution:**
1. Verify KiCAD is installed
2. Open KiCAD and ensure libraries are configured
3. Check `~/.config/kicad/9.0/fp-lib-table` exists
### "Footprint not found"
**Cause:** Footprint doesn't exist or library not loaded
**Solution:**
1. Use `search_footprints` to find similar footprints
2. Check library name is correct
3. Verify library is in fp-lib-table
### "Failed to load footprint"
**Cause:** Corrupt .kicad_mod file or permissions issue
**Solution:**
1. Check file permissions on library directories
2. Reinstall KiCAD libraries if corrupt
3. Check logs for detailed error
## Related Documentation
- [ROADMAP.md](./ROADMAP.md) - Week 2 planning
- [STATUS_SUMMARY.md](./STATUS_SUMMARY.md) - Current implementation status
- [API.md](./API.md) - Full MCP API reference
- [KiCAD Documentation](https://docs.kicad.org/9.0/en/pcbnew/pcbnew.html) - Official KiCAD docs
## Changelog
**2025-11-01 - v2.1.0-alpha**
- Implemented LibraryManager class
- Added 4 new MCP library tools
- Updated component placement to use libraries
- Fixed all KiCAD 9.0 API compatibility issues
- Tested end-to-end with real components
- Created comprehensive documentation
---
**Status: PRODUCTION READY** 🎉
The library integration is complete and fully functional. Component placement now works seamlessly with KiCAD's footprint libraries, enabling AI-driven PCB design with real, validated components.
# KiCAD Library Integration
**Status:** ✅ COMPLETE
**Date:** 2026-03-21
**Version:** 2.2.3+
## Overview
The KiCAD MCP Server includes full library integration for both footprints and symbols, enabling:
-Automatic discovery of all installed KiCAD footprint libraries
-Automatic discovery of KiCAD symbol libraries (including project-local)
- ✅ Search and browse footprints/symbols across all libraries
- ✅ Component placement using library footprints
- ✅ Symbol creation and editing with project-local library support (v2.2.2+)
- ✅ Support for both `Library:Footprint` and `Footprint` formats
## How It Works
### Library Discovery
The library system automatically discovers both footprint and symbol libraries:
**Footprint Libraries** - `LibraryManager` class:
1. **Parsing fp-lib-table files:**
- Global: `~/.config/kicad/9.0/fp-lib-table`
- Project-specific: `project-dir/fp-lib-table`
**Symbol Libraries** - `DynamicSymbolLoader` class (v2.2.2+):
1. **Parsing sym-lib-table files:**
- Global: `~/.config/kicad/9.0/sym-lib-table`
- Project-local: `project-dir/sym-lib-table` (added v2.2.2)
2. **Resolving environment variables:**
- `${KICAD9_FOOTPRINT_DIR}``/usr/share/kicad/footprints`
- `${K IPRJMOD}` → project directory
- Supports custom paths
3. **Indexing footprints:**
- Scans `.kicad_mod` files in each library
- Caches results for performance
- Provides fast search capabilities
### Supported Formats
**Library:Footprint format (recommended):**
```json
{
"componentId": "Resistor_SMD:R_0603_1608Metric"
}
```
**Footprint-only format (searches all libraries):**
```json
{
"componentId": "R_0603_1608Metric"
}
```
## New MCP Tools
### 1. `list_libraries`
List all available footprint libraries.
**Parameters:** None
**Returns:**
```json
{
"success": true,
"libraries": ["Resistor_SMD", "Capacitor_SMD", "LED_SMD", ...],
"count": 153
}
```
### 2. `search_footprints`
Search for footprints matching a pattern.
**Parameters:**
```json
{
"pattern": "*0603*", // Supports wildcards
"limit": 20 // Optional, default: 20
}
```
**Returns:**
```json
{
"success": true,
"footprints": [
{
"library": "Resistor_SMD",
"footprint": "R_0603_1608Metric",
"full_name": "Resistor_SMD:R_0603_1608Metric"
},
...
]
}
```
### 3. `list_library_footprints`
List all footprints in a specific library.
**Parameters:**
```json
{
"library": "Resistor_SMD"
}
```
**Returns:**
```json
{
"success": true,
"library": "Resistor_SMD",
"footprints": ["R_0402_1005Metric", "R_0603_1608Metric", ...],
"count": 120
}
```
### 4. `get_footprint_info`
Get detailed information about a specific footprint.
**Parameters:**
```json
{
"footprint": "Resistor_SMD:R_0603_1608Metric"
}
```
**Returns:**
```json
{
"success": true,
"footprint_info": {
"library": "Resistor_SMD",
"footprint": "R_0603_1608Metric",
"full_name": "Resistor_SMD:R_0603_1608Metric",
"library_path": "/usr/share/kicad/footprints/Resistor_SMD.pretty"
}
}
```
## Updated Component Placement
The `place_component` tool now uses the library system:
```json
{
"componentId": "Resistor_SMD:R_0603_1608Metric", // Library:Footprint format
"position": { "x": 50, "y": 40, "unit": "mm" },
"reference": "R1",
"value": "10k",
"rotation": 0,
"layer": "F.Cu"
}
```
**Features:**
- ✅ Automatic footprint discovery across all libraries
- ✅ Helpful error messages with suggestions
- ✅ Supports KiCAD 9.0 API (EDA_ANGLE, GetFPIDAsString)
## Example Usage (Claude Code)
**Search for a resistor footprint:**
```
User: "Find me a 0603 resistor footprint"
Claude: [uses search_footprints tool with pattern "*R_0603*"]
Found: Resistor_SMD:R_0603_1608Metric
```
**Place a component:**
```
User: "Place a 10k 0603 resistor at 50,40mm"
Claude: [uses place_component with "Resistor_SMD:R_0603_1608Metric"]
✅ Placed R1: 10k at (50, 40) mm
```
**List available capacitors:**
```
User: "What capacitor footprints are available?"
Claude: [uses list_library_footprints with "Capacitor_SMD"]
Found 103 capacitor footprints including:
- C_0402_1005Metric
- C_0603_1608Metric
- C_0805_2012Metric
...
```
## Configuration
### Custom Library Paths
The system automatically detects KiCAD installations, but you can add custom libraries:
1. **Via KiCAD Preferences:**
- Open KiCAD → Preferences → Manage Footprint Libraries
- Add your custom library paths
- The MCP server will automatically discover them
2. **Via Project fp-lib-table:**
- Create `fp-lib-table` in your project directory
- Follow the KiCAD S-expression format
### Supported Platforms
-**Linux:** `/usr/share/kicad/footprints`, `~/.config/kicad/9.0/`
-**Windows:** `C:/Program Files/KiCAD/*/share/kicad/footprints`
-**macOS:** `/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints`
## KiCAD 9.0 API Compatibility
The library integration includes full KiCAD 9.0 API support:
### Fixed API Changes:
1.`SetOrientation()` → now uses `EDA_ANGLE(degrees, DEGREES_T)`
2.`GetOrientation()` → returns `EDA_ANGLE`, call `.AsDegrees()`
3.`GetFootprintName()` → now `GetFPIDAsString()`
### Example Fixes:
**Old (KiCAD 8.0):**
```python
module.SetOrientation(90 * 10) # Decidegrees
rotation = module.GetOrientation() / 10
```
**New (KiCAD 9.0):**
```python
angle = pcbnew.EDA_ANGLE(90, pcbnew.DEGREES_T)
module.SetOrientation(angle)
rotation = module.GetOrientation().AsDegrees()
```
## Implementation Details
### LibraryManager Class
**Location:** `python/commands/library.py`
**Key Methods:**
- `_load_libraries()` - Parse fp-lib-table files
- `_parse_fp_lib_table()` - S-expression parser
- `_resolve_uri()` - Handle environment variables
- `find_footprint()` - Locate footprint in libraries
- `search_footprints()` - Pattern-based search
- `list_footprints()` - List library contents
**Performance:**
- Libraries loaded once at startup
- Footprint lists cached on first access
- Fast search using Python regex
- Minimal memory footprint
### Integration Points
1. **KiCADInterface (`kicad_interface.py`):**
- Creates `FootprintLibraryManager` on init
- Passes to `ComponentCommands`
- Routes library commands
2. **ComponentCommands (`component.py`):**
- Uses `LibraryManager.find_footprint()`
- Provides suggestions on errors
- Supports both lookup formats
3. **MCP Tools (`src/tools/index.ts`):**
- Exposes 4 new library tools
- Fully typed TypeScript interfaces
- Documented parameters
## Testing
**Test Coverage:**
- ✅ Library path discovery (Linux/Windows/macOS)
- ✅ fp-lib-table parsing
- ✅ Environment variable resolution
- ✅ Footprint search and lookup
- ✅ Component placement integration
- ✅ Error handling and suggestions
**Verified With:**
- KiCAD 9.0.5 on Ubuntu 24.04
- 153 standard libraries (8,000+ footprints)
- pcbnew Python API
## Known Limitations
1. **Library Updates:** Changes to fp-lib-table require server restart
2. **Custom Libraries:** Must be added via KiCAD preferences first
3. **Network Libraries:** GitHub-based libraries not yet supported
4. **Search Performance:** Linear search across all libraries (fast for <200 libs)
## Future Enhancements
- [ ] Watch fp-lib-table for changes (auto-reload)
- [ ] Support for GitHub library URLs
- [ ] Fuzzy search for typo tolerance
- [ ] Library metadata (descriptions, categories)
- [ ] Footprint previews (SVG/PNG generation)
- [ ] Most-used footprints caching
## Troubleshooting
### "No footprint libraries found"
**Cause:** fp-lib-table not found or empty
**Solution:**
1. Verify KiCAD is installed
2. Open KiCAD and ensure libraries are configured
3. Check `~/.config/kicad/9.0/fp-lib-table` exists
### "Footprint not found"
**Cause:** Footprint doesn't exist or library not loaded
**Solution:**
1. Use `search_footprints` to find similar footprints
2. Check library name is correct
3. Verify library is in fp-lib-table
### "Failed to load footprint"
**Cause:** Corrupt .kicad_mod file or permissions issue
**Solution:**
1. Check file permissions on library directories
2. Reinstall KiCAD libraries if corrupt
3. Check logs for detailed error
## Related Documentation
- [ROADMAP.md](./ROADMAP.md) - Week 2 planning
- [STATUS_SUMMARY.md](./STATUS_SUMMARY.md) - Current implementation status
- [API.md](./API.md) - Full MCP API reference
- [KiCAD Documentation](https://docs.kicad.org/9.0/en/pcbnew/pcbnew.html) - Official KiCAD docs
## Changelog
**2026-03-21 - v2.2.3+**
- Project-local symbol library support (v2.2.2)
- Project-local footprint library support (v2.2.2)
- Implemented LibraryManager class
- Added 4 new MCP library tools
- Updated component placement to use libraries
- Fixed all KiCAD 9.0 API compatibility issues
- Tested end-to-end with real components
- Created comprehensive documentation
---
**Status: PRODUCTION READY** 🎉
The library integration is complete and fully functional. Component placement now works seamlessly with KiCAD's footprint libraries, enabling AI-driven PCB design with real, validated components.

View File

@@ -1,313 +1,336 @@
# Linux Compatibility Audit Report
**Date:** 2025-10-25
**Target Platform:** Ubuntu 24.04 LTS (primary), Fedora, Arch (secondary)
**Current Status:** Windows-optimized, partial Linux support
---
## Executive Summary
The KiCAD MCP Server was originally developed for Windows and has several compatibility issues preventing smooth operation on Linux. This audit identifies all platform-specific issues and provides remediation priorities.
**Overall Status:** 🟡 **PARTIAL COMPATIBILITY**
- ✅ TypeScript server: Good cross-platform support
- 🟡 Python interface: Mixed (some hardcoded paths)
- ❌ Configuration: Windows-specific examples
- ❌ Documentation: Windows-only instructions
---
## Critical Issues (P0 - Must Fix)
### 1. Hardcoded Windows Paths in Config Examples
**File:** `config/claude-desktop-config.json`
```json
"cwd": "c:/repo/KiCAD-MCP",
"PYTHONPATH": "C:/Program Files/KiCad/9.0/lib/python3/dist-packages"
```
**Impact:** Config file won't work on Linux without manual editing
**Fix:** Create platform-specific config templates
**Priority:** P0
---
### 2. Library Search Paths (Mixed Approach)
**File:** `python/commands/library_schematic.py:16`
```python
search_paths = [
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym", # Windows
"/usr/share/kicad/symbols/*.kicad_sym", # Linux
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", # macOS
]
```
**Impact:** Works but inefficient (checks all platforms)
**Fix:** Auto-detect platform and use appropriate paths
**Priority:** P0
---
### 3. Python Path Detection
**File:** `python/kicad_interface.py:38-45`
```python
kicad_paths = [
os.path.join(os.path.dirname(sys.executable), 'Lib', 'site-packages'),
os.path.dirname(sys.executable)
]
```
**Impact:** Paths use Windows convention ('Lib' is 'lib' on Linux)
**Fix:** Platform-specific path detection
**Priority:** P0
---
## High Priority Issues (P1)
### 4. Documentation is Windows-Only
**Files:** `README.md`, installation instructions
**Issues:**
- Installation paths reference `C:\Program Files`
- VSCode settings path is Windows format
- No Linux-specific troubleshooting
**Fix:** Add Linux installation section
**Priority:** P1
---
### 5. Missing Python Dependencies Documentation
**File:** None (no requirements.txt)
**Impact:** Users don't know what Python packages to install
**Fix:** Create `requirements.txt` and `requirements-dev.txt`
**Priority:** P1
---
### 6. Path Handling Uses os.path Instead of pathlib
**Files:** All Python files (11 files)
**Impact:** Code is less readable and more error-prone
**Fix:** Migrate to `pathlib.Path` throughout
**Priority:** P1
---
## Medium Priority Issues (P2)
### 7. No Linux-Specific Testing
**Impact:** Can't verify Linux compatibility
**Fix:** Add GitHub Actions with Ubuntu runner
**Priority:** P2
---
### 8. Log File Paths May Differ
**File:** `src/logger.ts:13`
```typescript
const DEFAULT_LOG_DIR = join(os.homedir(), '.kicad-mcp', 'logs');
```
**Impact:** `.kicad-mcp` is okay for Linux, but best practice is `~/.config/kicad-mcp`
**Fix:** Use XDG Base Directory spec on Linux
**Priority:** P2
---
### 9. No Bash/Shell Scripts for Linux
**Impact:** Manual setup is harder on Linux
**Fix:** Create `install.sh` and `run.sh` scripts
**Priority:** P2
---
## Low Priority Issues (P3)
### 10. TypeScript Build Uses Windows Conventions
**File:** `package.json`
**Impact:** Works but could be more Linux-friendly
**Fix:** Add platform-specific build scripts
**Priority:** P3
---
## Positive Findings ✅
### What's Already Good:
1. **TypeScript Path Handling** - Uses `path.join()` and `os.homedir()` correctly
2. **Node.js Dependencies** - All cross-platform
3. **JSON Communication** - Platform-agnostic
4. **Python Base** - Python 3 works identically on all platforms
---
## Recommended Fixes - Priority Order
### **Week 1 - Critical Fixes (P0)**
1. **Create Platform-Specific Config Templates**
```bash
config/
├── linux-config.example.json
├── windows-config.example.json
└── macos-config.example.json
```
2. **Fix Python Path Detection**
```python
# Detect platform and set appropriate paths
import platform
import sys
from pathlib import Path
if platform.system() == "Windows":
kicad_paths = [Path(sys.executable).parent / "Lib" / "site-packages"]
else: # Linux/Mac
kicad_paths = [Path(sys.executable).parent / "lib" / "python3.X" / "site-packages"]
```
3. **Update Library Search Path Logic**
```python
def get_kicad_library_paths():
"""Auto-detect KiCAD library paths based on platform"""
system = platform.system()
if system == "Windows":
return ["C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym"]
elif system == "Linux":
return ["/usr/share/kicad/symbols/*.kicad_sym"]
elif system == "Darwin": # macOS
return ["/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym"]
```
### **Week 1 - High Priority (P1)**
4. **Create requirements.txt**
```txt
# requirements.txt
kicad-skip>=0.1.0
Pillow>=9.0.0
cairosvg>=2.7.0
colorlog>=6.7.0
```
5. **Add Linux Installation Documentation**
- Ubuntu/Debian instructions
- Fedora/RHEL instructions
- Arch Linux instructions
6. **Migrate to pathlib**
- Convert all `os.path` calls to `Path`
- More Pythonic and readable
---
## Testing Checklist
### Ubuntu 24.04 LTS Testing
- [ ] Install KiCAD 9.0 from official PPA
- [ ] Install Node.js 18+ from NodeSource
- [ ] Clone repository
- [ ] Run `npm install`
- [ ] Run `npm run build`
- [ ] Configure MCP settings (Cline)
- [ ] Test: Create project
- [ ] Test: Place components
- [ ] Test: Export Gerbers
### Fedora Testing
- [ ] Install KiCAD from Fedora repos
- [ ] Test same workflow
### Arch Testing
- [ ] Install KiCAD from AUR
- [ ] Test same workflow
---
## Platform Detection Helper
Create `python/utils/platform_helper.py`:
```python
"""Platform detection and path utilities"""
import platform
import sys
from pathlib import Path
from typing import List
class PlatformHelper:
@staticmethod
def is_windows() -> bool:
return platform.system() == "Windows"
@staticmethod
def is_linux() -> bool:
return platform.system() == "Linux"
@staticmethod
def is_macos() -> bool:
return platform.system() == "Darwin"
@staticmethod
def get_kicad_python_path() -> Path:
"""Get KiCAD Python dist-packages path"""
if PlatformHelper.is_windows():
return Path("C:/Program Files/KiCad/9.0/lib/python3/dist-packages")
elif PlatformHelper.is_linux():
# Common Linux paths
candidates = [
Path("/usr/lib/kicad/lib/python3/dist-packages"),
Path("/usr/share/kicad/scripting/plugins"),
]
for path in candidates:
if path.exists():
return path
elif PlatformHelper.is_macos():
return Path("/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.X/lib/python3.X/site-packages")
raise RuntimeError(f"Could not find KiCAD Python path for {platform.system()}")
@staticmethod
def get_config_dir() -> Path:
"""Get appropriate config directory"""
if PlatformHelper.is_windows():
return Path.home() / ".kicad-mcp"
elif PlatformHelper.is_linux():
# Use XDG Base Directory specification
xdg_config = os.environ.get("XDG_CONFIG_HOME")
if xdg_config:
return Path(xdg_config) / "kicad-mcp"
return Path.home() / ".config" / "kicad-mcp"
elif PlatformHelper.is_macos():
return Path.home() / "Library" / "Application Support" / "kicad-mcp"
```
---
## Success Criteria
✅ Server starts on Ubuntu 24.04 LTS without errors
✅ Can create and manipulate KiCAD projects
✅ CI/CD pipeline tests on Linux
✅ Documentation includes Linux setup
✅ All tests pass on Linux
---
## Next Steps
1. Implement P0 fixes (this week)
2. Set up GitHub Actions CI/CD
3. Test on Ubuntu 24.04 LTS
4. Document Linux-specific issues
5. Create installation scripts
---
**Audited by:** Claude Code
**Review Status:** ✅ Complete
# Linux Compatibility Audit Report
**Date:** 2025-10-25
**Target Platform:** Ubuntu 24.04 LTS (primary), Fedora, Arch (secondary)
**Current Status:** Windows-optimized, partial Linux support
---
## Executive Summary
The KiCAD MCP Server was originally developed for Windows and has several compatibility issues preventing smooth operation on Linux. This audit identifies all platform-specific issues and provides remediation priorities.
**Overall Status:** 🟡 **PARTIAL COMPATIBILITY**
- ✅ TypeScript server: Good cross-platform support
- 🟡 Python interface: Mixed (some hardcoded paths)
- ❌ Configuration: Windows-specific examples
- ❌ Documentation: Windows-only instructions
---
## Critical Issues (P0 - Must Fix)
### 1. Hardcoded Windows Paths in Config Examples
**File:** `config/claude-desktop-config.json`
```json
"cwd": "c:/repo/KiCAD-MCP",
"PYTHONPATH": "C:/Program Files/KiCad/9.0/lib/python3/dist-packages"
```
**Impact:** Config file won't work on Linux without manual editing
**Fix:** Create platform-specific config templates
**Priority:** P0
---
### 2. Library Search Paths (Mixed Approach)
**File:** `python/commands/library_schematic.py:16`
```python
search_paths = [
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym", # Windows
"/usr/share/kicad/symbols/*.kicad_sym", # Linux
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", # macOS
]
```
**Impact:** Works but inefficient (checks all platforms)
**Fix:** Auto-detect platform and use appropriate paths
**Priority:** P0
---
### 3. Python Path Detection
**File:** `python/kicad_interface.py:38-45`
```python
kicad_paths = [
os.path.join(os.path.dirname(sys.executable), 'Lib', 'site-packages'),
os.path.dirname(sys.executable)
]
```
**Impact:** Paths use Windows convention ('Lib' is 'lib' on Linux)
**Fix:** Platform-specific path detection
**Priority:** P0
---
## High Priority Issues (P1)
### 4. Documentation is Windows-Only
**Files:** `README.md`, installation instructions
**Issues:**
- Installation paths reference `C:\Program Files`
- VSCode settings path is Windows format
- No Linux-specific troubleshooting
**Fix:** Add Linux installation section
**Priority:** P1
---
### 5. Missing Python Dependencies Documentation
**File:** None (no requirements.txt)
**Impact:** Users don't know what Python packages to install
**Fix:** Create `requirements.txt` and `requirements-dev.txt`
**Priority:** P1
---
### 6. Path Handling Uses os.path Instead of pathlib
**Files:** All Python files (11 files)
**Impact:** Code is less readable and more error-prone
**Fix:** Migrate to `pathlib.Path` throughout
**Priority:** P1
---
## Medium Priority Issues (P2)
### 7. No Linux-Specific Testing
**Impact:** Can't verify Linux compatibility
**Fix:** Add GitHub Actions with Ubuntu runner
**Priority:** P2
---
### 8. Log File Paths May Differ
**File:** `src/logger.ts:13`
```typescript
const DEFAULT_LOG_DIR = join(os.homedir(), ".kicad-mcp", "logs");
```
**Impact:** `.kicad-mcp` is okay for Linux, but best practice is `~/.config/kicad-mcp`
**Fix:** Use XDG Base Directory spec on Linux
**Priority:** P2
---
### 9. No Bash/Shell Scripts for Linux
**Impact:** Manual setup is harder on Linux
**Fix:** Create `install.sh` and `run.sh` scripts
**Priority:** P2
---
## Low Priority Issues (P3)
### 10. TypeScript Build Uses Windows Conventions
**File:** `package.json`
**Impact:** Works but could be more Linux-friendly
**Fix:** Add platform-specific build scripts
**Priority:** P3
---
## Positive Findings ✅
### What's Already Good:
1. **TypeScript Path Handling** - Uses `path.join()` and `os.homedir()` correctly
2. **Node.js Dependencies** - All cross-platform
3. **JSON Communication** - Platform-agnostic
4. **Python Base** - Python 3 works identically on all platforms
---
## Recommended Fixes - Priority Order
### **Week 1 - Critical Fixes (P0)**
1. **Create Platform-Specific Config Templates**
```bash
config/
├── linux-config.example.json
├── windows-config.example.json
└── macos-config.example.json
```
2. **Fix Python Path Detection**
```python
# Detect platform and set appropriate paths
import platform
import sys
from pathlib import Path
if platform.system() == "Windows":
kicad_paths = [Path(sys.executable).parent / "Lib" / "site-packages"]
else: # Linux/Mac
kicad_paths = [Path(sys.executable).parent / "lib" / "python3.X" / "site-packages"]
```
3. **Update Library Search Path Logic**
```python
def get_kicad_library_paths():
"""Auto-detect KiCAD library paths based on platform"""
system = platform.system()
if system == "Windows":
return ["C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym"]
elif system == "Linux":
return ["/usr/share/kicad/symbols/*.kicad_sym"]
elif system == "Darwin": # macOS
return ["/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym"]
```
### **Week 1 - High Priority (P1)**
4. **Create requirements.txt**
```txt
# requirements.txt
kicad-skip>=0.1.0
Pillow>=9.0.0
cairosvg>=2.7.0
colorlog>=6.7.0
```
5. **Add Linux Installation Documentation**
- Ubuntu/Debian instructions
- Fedora/RHEL instructions
- Arch Linux instructions
6. **Migrate to pathlib**
- Convert all `os.path` calls to `Path`
- More Pythonic and readable
---
## Testing Checklist
### Ubuntu 24.04 LTS Testing
- [ ] Install KiCAD 9.0 from official PPA
- [ ] Install Node.js 18+ from NodeSource
- [ ] Clone repository
- [ ] Run `npm install`
- [ ] Run `npm run build`
- [ ] Configure MCP settings (Cline)
- [ ] Test: Create project
- [ ] Test: Place components
- [ ] Test: Export Gerbers
### Fedora Testing
- [ ] Install KiCAD from Fedora repos
- [ ] Test same workflow
### Arch Testing
- [ ] Install KiCAD from AUR
- [ ] Test same workflow
---
## Platform Detection Helper
Create `python/utils/platform_helper.py`:
```python
"""Platform detection and path utilities"""
import platform
import sys
from pathlib import Path
from typing import List
class PlatformHelper:
@staticmethod
def is_windows() -> bool:
return platform.system() == "Windows"
@staticmethod
def is_linux() -> bool:
return platform.system() == "Linux"
@staticmethod
def is_macos() -> bool:
return platform.system() == "Darwin"
@staticmethod
def get_kicad_python_path() -> Path:
"""Get KiCAD Python dist-packages path"""
if PlatformHelper.is_windows():
return Path("C:/Program Files/KiCad/9.0/lib/python3/dist-packages")
elif PlatformHelper.is_linux():
# Common Linux paths
candidates = [
Path("/usr/lib/kicad/lib/python3/dist-packages"),
Path("/usr/share/kicad/scripting/plugins"),
]
for path in candidates:
if path.exists():
return path
elif PlatformHelper.is_macos():
return Path("/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.X/lib/python3.X/site-packages")
raise RuntimeError(f"Could not find KiCAD Python path for {platform.system()}")
@staticmethod
def get_config_dir() -> Path:
"""Get appropriate config directory"""
if PlatformHelper.is_windows():
return Path.home() / ".kicad-mcp"
elif PlatformHelper.is_linux():
# Use XDG Base Directory specification
xdg_config = os.environ.get("XDG_CONFIG_HOME")
if xdg_config:
return Path(xdg_config) / "kicad-mcp"
return Path.home() / ".config" / "kicad-mcp"
elif PlatformHelper.is_macos():
return Path.home() / "Library" / "Application Support" / "kicad-mcp"
```
---
## Success Criteria
✅ Server starts on Ubuntu 24.04 LTS without errors
✅ Can create and manipulate KiCAD projects
✅ CI/CD pipeline tests on Linux
✅ Documentation includes Linux setup
✅ All tests pass on Linux
---
## Next Steps
1. Implement P0 fixes (this week)
2. Set up GitHub Actions CI/CD
3. Test on Ubuntu 24.04 LTS
4. Document Linux-specific issues
5. Create installation scripts
---
**Audited by:** Claude Code
**Review Status:** ✅ Complete

311
docs/PCB_DESIGN_WORKFLOW.md Normal file
View File

@@ -0,0 +1,311 @@
# End-to-End PCB Design Workflow
This guide walks through the complete PCB design process using the KiCAD MCP Server, from project creation to manufacturing-ready output.
---
## Overview
A typical PCB design follows this flow:
```
Project Setup -> Schematic Design -> PCB Layout -> Verification -> Manufacturing Output
```
Each stage maps to specific MCP tools. You can ask your AI assistant to perform any of these steps using natural language.
---
## Stage 1: Project Setup
### Create a New Project
```
Create a new KiCAD project named "LEDBoard" in ~/Projects/
```
This uses `create_project` to generate:
- `.kicad_pro` -- project file
- `.kicad_pcb` -- PCB layout file
- `.kicad_sch` -- schematic file (with template symbols pre-loaded)
### Set Up the Board
```
Set the board size to 50mm x 50mm.
Add a rectangular board outline.
Add mounting holes at each corner, 3mm from the edges, 3mm diameter.
```
**Tools used:** `set_board_size`, `add_board_outline`, `add_mounting_hole`
---
## Stage 2: Schematic Design
### Place Components
```
Add an LED from the Device library to the schematic at position 100, 50.
Add a 1K resistor at position 100, 70.
Add a connector from the Connector_Generic library with 2 pins at position 60, 60.
```
**Tool:** `add_schematic_component`
The dynamic symbol loader provides access to all ~10,000 KiCad standard symbols. Specify any library and symbol name.
### Wire Components
```
Connect R1 pin 2 to LED1 pin 1.
Add a net label "VCC" at position 60, 50.
Connect J1 pin 1 to the VCC net.
Connect LED1 pin 2 to GND.
```
**Tools:** `add_schematic_connection`, `add_schematic_net_label`, `connect_to_net`
### FFC/Ribbon Cable Passthrough (Special Workflow)
For passthrough adapter boards (e.g., Raspberry Pi CSI adapters):
```
Connect all pins from J1 to J2 as a passthrough with net prefix "CSI_".
```
**Tool:** `connect_passthrough` -- automatically wires matching pins between two connectors
### Annotate and Validate
```
Annotate the schematic to assign reference designators.
Run an electrical rule check.
```
**Tools:** `annotate_schematic`, `run_erc`
### Preview the Schematic
```
Show me the schematic as an image.
Export the schematic to PDF.
```
**Tools:** `get_schematic_view`, `export_schematic_pdf`
---
## Stage 3: PCB Layout
### Synchronize Schematic to PCB
```
Sync the schematic to the board.
```
**Tool:** `sync_schematic_to_board` -- imports all component footprints and net assignments from the schematic into the PCB (equivalent to pressing F8 in KiCAD)
### Place Components
```
Move R1 to position x=15, y=25.
Move LED1 to position x=25, y=25.
Align all resistors horizontally.
```
**Tools:** `move_component`, `align_components`
### Route Traces
**Preferred approach -- pad-to-pad routing:**
```
Route R1 pad 2 to LED1 pad 1 with 0.3mm trace width.
```
**Tool:** `route_pad_to_pad` -- auto-detects pad positions, nets, and inserts vias when pads are on different layers
**Manual approach:**
```
Route a trace from x=15, y=25 to x=25, y=25 on the front copper layer.
```
**Tool:** `route_trace`
### Advanced Routing
**Differential pairs:**
```
Route a differential pair for USB_P and USB_N with 0.2mm width and 0.15mm gap.
```
**Copper zones:**
```
Add a GND copper pour on the bottom layer covering the entire board.
```
**Tools:** `route_differential_pair`, `add_copper_pour`
### Autorouting
For boards with many connections:
```
Check if Freerouting is available.
Autoroute the board using Freerouting.
```
**Tools:** `check_freerouting`, `autoroute`
See [Freerouting Guide](FREEROUTING_GUIDE.md) for setup details.
---
## Stage 4: Verification
### Design Rule Check
```
Set design rules with 0.15mm clearance and 0.2mm minimum track width.
Run the design rule check.
Show me all DRC violations.
```
**Tools:** `set_design_rules`, `run_drc`, `get_drc_violations`
### Visual Inspection
```
Show me a 2D view of the board.
```
**Tool:** `get_board_2d_view`
### Save a Checkpoint
```
Save a snapshot named "post-routing" with label "All traces routed, DRC clean".
```
**Tool:** `snapshot_project`
---
## Stage 5: Manufacturing Output
### Gerber Files
```
Export Gerber files to the fabrication folder.
```
**Tool:** `export_gerber`
### Bill of Materials
```
Export BOM as CSV.
```
**Tool:** `export_bom` (supports CSV, XML, HTML, JSON)
### Pick and Place
```
Export the component position file.
```
**Tool:** `export_position_file`
### 3D Preview
```
Export a 3D STEP model of the board.
```
**Tool:** `export_3d` (supports STEP, STL, VRML, OBJ)
### Documentation
```
Export a PDF of the board layout.
Export an SVG of the board.
```
**Tools:** `export_pdf`, `export_svg`
---
## Optional: JLCPCB Component Selection
Before placing components, you can search JLCPCB's catalog for optimal parts:
```
Search JLCPCB for 10K resistors in 0603 package, Basic parts only.
Show me the cheapest option with good stock.
Suggest alternatives to part C25804.
```
After selecting parts, enrich datasheets:
```
Enrich datasheets for all components in the schematic.
```
**Tools:** `search_jlcpcb_parts`, `get_jlcpcb_part`, `suggest_jlcpcb_alternatives`, `enrich_datasheets`
See [JLCPCB Integration](JLCPCB_INTEGRATION.md) for details.
---
## Optional: Custom Components
When existing libraries do not have the part you need:
```
Create a custom footprint for a 4-pin SOT-23 package.
Create a custom symbol for the XYZ IC with 8 pins.
Register the custom library so it can be used in the project.
```
**Tools:** `create_footprint`, `create_symbol`, `register_footprint_library`, `register_symbol_library`
See [Footprint and Symbol Creator Guide](FOOTPRINT_SYMBOL_CREATOR_GUIDE.md) for details.
---
## Optional: Add a Logo
```
Import our company logo from ~/logos/logo.svg onto the front silkscreen at position x=25 y=45 with width 10mm.
```
**Tool:** `import_svg_logo`
See [SVG Import Guide](SVG_IMPORT_GUIDE.md) for requirements and tips.
---
## Tips
- **Save frequently** -- use `save_project` after major changes
- **Use snapshots** -- `snapshot_project` creates named checkpoints you can return to
- **Validate early** -- run ERC after schematic changes and DRC after routing
- **Start with schematic** -- always design the schematic first, then sync to PCB
- **Use route_pad_to_pad** -- it is faster and more reliable than manual XY coordinate routing
- **Check the KiCAD UI** -- use `launch_kicad_ui` to open the design for visual verification
---
## Related Documentation
- [Tool Inventory](TOOL_INVENTORY.md) -- complete list of all 122 tools
- [Schematic Tools Reference](SCHEMATIC_TOOLS_REFERENCE.md) -- detailed schematic tool docs
- [Routing Tools Reference](ROUTING_TOOLS_REFERENCE.md) -- detailed routing tool docs
- [Freerouting Guide](FREEROUTING_GUIDE.md) -- autorouter setup and usage
- [JLCPCB Integration](JLCPCB_INTEGRATION.md) -- parts selection and cost optimization

File diff suppressed because it is too large Load Diff

View File

@@ -1,416 +1,441 @@
# Real-Time Collaboration Workflow
**Status:** ✅ TESTED AND WORKING
**Date:** 2025-11-01
**Version:** 2.1.0-alpha
## Overview
The KiCAD MCP Server enables **real-time paired circuit board design** between Claude Code (via MCP) and a human designer using the KiCAD UI. Both workflows have been tested and confirmed working:
-**MCP→UI**: AI places components, human sees them in KiCAD
-**UI→MCP**: Human edits board, AI reads changes back
## How It Works
### Architecture
The MCP server uses KiCAD's Python API (`pcbnew` module) to read and write `.kicad_pcb` files. The KiCAD UI and MCP both operate on the same file, enabling collaboration through the file system.
```
┌─────────────────┐ ┌──────────────────┐
│ Claude Code │ │ Human Designer │
│ (via MCP) │ │ (KiCAD UI) │
└────────┬────────┘ └────────┬─────────┘
│ │
│ pcbnew Python API │ KiCAD UI
│ │
▼ ▼
┌─────────────────────────────────────┐
│ project.kicad_pcb (file system) │
└─────────────────────────────────────┘
```
### MCP→UI Workflow (AI to Human)
**Use case:** Claude places components via MCP, human sees them in KiCAD UI
1. **Claude places components** via MCP tools:
```python
# MCP internally uses:
board = pcbnew.LoadBoard('project.kicad_pcb')
module = pcbnew.FootprintLoad(library_path, 'R_0603_1608Metric')
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
board.Add(module)
pcbnew.SaveBoard('project.kicad_pcb', board)
```
2. **Human opens/reloads in KiCAD UI:**
- **Option A (first time):** Open the project in KiCAD
- **Option B (already open):** File → Revert or close and reopen the PCB editor
- Components appear instantly ✅
**Example:**
```
User: "Place a 10k resistor at position 30, 30mm"
Claude: [uses place_component MCP tool]
✅ Placed R1: 10k at (30.0, 30.0) mm
User: [opens KiCAD UI]
[sees R1 at the specified position]
```
### UI→MCP Workflow (Human to AI)
**Use case:** Human edits board in KiCAD UI, Claude reads changes via MCP
1. **Human makes changes in KiCAD UI:**
- Move components
- Add new components
- Route traces
- Edit properties
2. **Human saves the file:**
- Ctrl+S or File → Save
- KiCAD writes changes to `.kicad_pcb` file
3. **Claude reads changes** via MCP tools:
```python
# MCP internally uses:
board = pcbnew.LoadBoard('project.kicad_pcb')
footprints = board.GetFootprints()
# Reads all current component positions, values, etc.
```
4. **Claude can see the updates:**
- New component positions
- Added/removed components
- Updated values and references
- New traces and nets
**Example:**
```
User: "I moved R1 to a new position, can you see it?"
Claude: [uses get_board_info MCP tool]
Yes! I can see R1 is now at (59.175, 49.0) mm
(previously it was at 30.0, 30.0 mm)
```
## Tested Workflows
### Test 1: MCP→UI (Verified ✅)
**Setup:**
- Created new board via MCP (100x80mm)
- Placed R1 (10k resistor) at (30, 30) mm
- Placed D1 (RED LED) at (50, 30) mm
**Result:**
- Opened KiCAD PCB editor
- Both components visible at correct positions ✅
- All properties (reference, value, rotation) correct ✅
### Test 2: UI→MCP (Verified ✅)
**Setup:**
- User moved R1 from (30, 30) mm to (59.175, 49.0) mm in UI
- User saved file (Ctrl+S)
**Result:**
- MCP read board via `get_board_info`
- New position detected correctly ✅
- D1 position unchanged (as expected) ✅
## Current Capabilities
### What Works
1. **Bidirectional sync** (via file save/reload)
2. **Component placement** (MCP→UI)
3. **Component reading** (UI→MCP)
4. **Position/rotation updates** (both directions)
5. **Value/reference changes** (both directions)
6. **Trace routing** (both directions)
7. **Net information** (both directions)
8. **Board properties** (size, layers, design rules)
### MCP Tools for Collaboration
**Reading board state:**
- `get_board_info` - Get all components and their positions
- `get_project_info` - Get project metadata
- `list_components` - List all components (if implemented)
**Modifying board:**
- `place_component` - Add new components
- `add_trace` - Add copper traces
- `add_via` - Add vias
- `add_copper_pour` - Add copper zones
- `add_mounting_hole` - Add mounting holes
- `add_board_text` - Add text to board
## Limitations
### Current Limitations
1. **Manual Save Required**
- UI changes require manual save (Ctrl+S)
- No automatic file watching (yet)
- Workaround: Always save before asking Claude
2. **Manual Reload Required**
- MCP changes require reload in UI
- Options: File → Revert, or close/reopen
- Future: Could implement auto-reload trigger
3. **No Live Sync**
- Changes not visible until save/reload
- Not truly "real-time" (more like "near-time")
- File-based sync has ~1-5 second latency
4. **No Conflict Detection**
- If both edit simultaneously, last save wins
- No merge conflict resolution
- Best practice: Take turns editing
5. **No Change Notifications**
- MCP doesn't know when UI saves
- UI doesn't know when MCP saves
- Future: Could add file system watchers
### Known Issues
1. **Zone Filling:** Copper pours created by MCP won't be filled (requires UI to fill)
2. **Undo History:** UI undo history lost after MCP changes
3. **DRC Errors:** MCP doesn't run design rule checks automatically
## Best Practices
### For AI-Human Collaboration
1. **Establish Turn-Taking:**
```
User: "I'm going to add some components, one sec"
[User edits in UI]
User: "Done, saved the file"
Claude: [reads changes] "I see you added C1 and C2..."
```
2. **Always Save/Reload:**
- Human: Save after every change (Ctrl+S)
- Human: Reload after Claude makes changes
- Claude: Always read fresh before making decisions
3. **Communicate Changes:**
```
Claude: "I'm placing R1-R4 now..."
[MCP places components]
Claude: "Done! Reload the board to see them"
User: [File → Revert]
```
4. **Use Descriptive References:**
- Good: R1, R2, C1, C2 (sequential)
- Bad: R_temp, R_test (unclear)
### Workflow Patterns
**Pattern 1: AI Does Layout, Human Reviews**
```
1. Claude places all components via MCP
2. Claude routes critical traces via MCP
3. Human opens in KiCAD UI
4. Human fine-tunes positions
5. Human completes routing
6. Saves → Claude reads final result
```
**Pattern 2: Human Sketches, AI Refines**
```
1. Human places major components in UI
2. Saves → Claude reads layout
3. Claude suggests improvements
4. Claude places remaining components via MCP
5. Human reloads and reviews
6. Iterate until satisfied
```
**Pattern 3: Pair Programming Style**
```
User: "Place a 10k pull-up resistor on pin 3"
Claude: [places R1 at calculated position]
"Done! Check position (45, 20) mm"
User: [reloads] "Looks good, now add the LED"
Claude: [places D1]
[Continue back-and-forth]
```
## Future Enhancements
### Planned Improvements
1. **File System Watchers** (Week 4+)
- Auto-detect when UI saves file
- Auto-reload UI when MCP saves (via IPC)
- Near-instant sync (<100ms)
2. **IPC Backend** (Week 3)
- Direct communication with KiCAD process
- Live sync without file save/reload
- True real-time collaboration
3. **Change Notifications**
- MCP sends notification when it modifies board
- UI shows toast: "Claude added 4 components"
- Automatic reload option
4. **Conflict Detection**
- Detect when both edited same component
- Show diff/merge UI
- Allow choosing which changes to keep
5. **Collaborative Cursor**
- Show Claude's "cursor" in UI
- Highlight component being placed
- Visual feedback for AI actions
### Long-Term Vision
**Fully Real-Time Collaboration:**
- Both AI and human see changes instantly
- No manual save/reload required
- Conflict detection and resolution
- Visual indicators for who's editing what
- Chat/comment system for design discussion
**Example Future Workflow:**
```
[Claude and human both have board open]
Claude: [starts placing R1]
[R1 appears in UI with "Claude is placing..." indicator]
User: [sees R1 appear in real-time]
[moves D1 to better position]
[Claude sees D1 move instantly]
Claude: "Good position for D1! I'll route them now"
[traces appear as Claude creates them]
```
## Technical Details
### File Format
KiCAD uses S-expression format (`.kicad_pcb`):
```lisp
(kicad_pcb (version 20240108) (generator "pcbnew")
(footprint "Resistor_SMD:R_0603_1608Metric"
(layer "F.Cu")
(at 30.0 30.0 0)
(property "Reference" "R1")
(property "Value" "10k")
...
)
)
```
### Sync Mechanism
**Current (File-based):**
1. MCP: `pcbnew.SaveBoard(path, board)` → writes file
2. UI: File → Revert → reads file
3. Latency: ~1-5 seconds (manual)
**Future (IPC-based):**
1. MCP: `kicad.AddFootprint(...)` → sends IPC command
2. KiCAD: Receives command → updates internal state
3. UI: Automatically refreshes display
4. Latency: ~50-100ms (automatic)
### Python API Used
```python
import pcbnew
# Load board
board = pcbnew.LoadBoard('project.kicad_pcb')
# Read components
for fp in board.GetFootprints():
ref = fp.Reference().GetText()
pos = fp.GetPosition()
x_mm = pos.x / 1_000_000.0
y_mm = pos.y / 1_000_000.0
# Modify board
module = pcbnew.FootprintLoad(lib_path, 'R_0603')
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
board.Add(module)
# Save changes
pcbnew.SaveBoard('project.kicad_pcb', board)
```
## Troubleshooting
### "I don't see MCP changes in KiCAD UI"
**Cause:** UI hasn't reloaded the file
**Solution:**
1. File → Revert (or Ctrl+R if configured)
2. Or close PCB editor and reopen
3. Or restart KiCAD
### "MCP doesn't see my UI changes"
**Cause:** File not saved
**Solution:**
1. Save file: Ctrl+S or File → Save
2. Verify save: Check file modification time
3. Ask Claude to read board again
### "Changes disappeared after reload"
**Cause:** File overwritten by other party
**Solution:**
1. Always save before asking MCP to make changes
2. Don't edit while MCP is working
3. Take turns to avoid conflicts
### "Components appear in wrong positions"
**Cause:** Unit conversion error or coordinate system mismatch
**Solution:**
1. Check KiCAD units (View → Switch Units)
2. MCP uses millimeters internally
3. Report issue if positions consistently wrong
## Conclusion
**The real-time collaboration workflow is WORKING and TESTED! ✅**
The KiCAD MCP Server successfully enables paired circuit board design between AI (Claude) and human designers. While it requires manual save/reload steps, both MCP→UI and UI→MCP workflows function correctly.
**Current State:** "Near-real-time" collaboration (1-5 second latency)
**Future State:** True real-time with IPC backend (<100ms latency)
**Mission Accomplished:** Real-time paired circuit board design is operational and ready for use! 🎉
---
## Related Documentation
- [LIBRARY_INTEGRATION.md](./LIBRARY_INTEGRATION.md) - Component library system
- [STATUS_SUMMARY.md](./STATUS_SUMMARY.md) - Current implementation status
- [ROADMAP.md](./ROADMAP.md) - Future development plans
- [API.md](./API.md) - Full MCP API reference
## Changelog
**2025-11-01 - v2.1.0-alpha**
- Tested MCPUI workflow (placing components via MCP, viewing in UI)
- Tested UIMCP workflow (editing in UI, reading via MCP)
- Documented best practices and limitations
- Confirmed real-time collaboration mission is met
# Real-Time Collaboration Workflow
**Status:** ✅ TESTED AND WORKING
**Date:** 2025-11-01
**Version:** 2.1.0-alpha
## Overview
The KiCAD MCP Server enables **real-time paired circuit board design** between Claude Code (via MCP) and a human designer using the KiCAD UI. Both workflows have been tested and confirmed working:
-**MCP→UI**: AI places components, human sees them in KiCAD
-**UI→MCP**: Human edits board, AI reads changes back
## How It Works
### Architecture
The MCP server uses KiCAD's Python API (`pcbnew` module) to read and write `.kicad_pcb` files. The KiCAD UI and MCP both operate on the same file, enabling collaboration through the file system.
```
┌─────────────────┐ ┌──────────────────┐
│ Claude Code │ │ Human Designer │
│ (via MCP) │ │ (KiCAD UI) │
└────────┬────────┘ └────────┬─────────┘
│ │
│ pcbnew Python API │ KiCAD UI
│ │
▼ ▼
┌─────────────────────────────────────┐
│ project.kicad_pcb (file system) │
└─────────────────────────────────────┘
```
### MCP→UI Workflow (AI to Human)
**Use case:** Claude places components via MCP, human sees them in KiCAD UI
1. **Claude places components** via MCP tools:
```python
# MCP internally uses:
board = pcbnew.LoadBoard('project.kicad_pcb')
module = pcbnew.FootprintLoad(library_path, 'R_0603_1608Metric')
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
board.Add(module)
pcbnew.SaveBoard('project.kicad_pcb', board)
```
2. **Human opens/reloads in KiCAD UI:**
- **Option A (first time):** Open the project in KiCAD
- **Option B (already open):** File → Revert or close and reopen the PCB editor
- Components appear instantly ✅
**Example:**
```
User: "Place a 10k resistor at position 30, 30mm"
Claude: [uses place_component MCP tool]
✅ Placed R1: 10k at (30.0, 30.0) mm
User: [opens KiCAD UI]
[sees R1 at the specified position]
```
### UI→MCP Workflow (Human to AI)
**Use case:** Human edits board in KiCAD UI, Claude reads changes via MCP
1. **Human makes changes in KiCAD UI:**
- Move components
- Add new components
- Route traces
- Edit properties
2. **Human saves the file:**
- Ctrl+S or File → Save
- KiCAD writes changes to `.kicad_pcb` file
3. **Claude reads changes** via MCP tools:
```python
# MCP internally uses:
board = pcbnew.LoadBoard('project.kicad_pcb')
footprints = board.GetFootprints()
# Reads all current component positions, values, etc.
```
4. **Claude can see the updates:**
- New component positions
- Added/removed components
- Updated values and references
- New traces and nets
**Example:**
```
User: "I moved R1 to a new position, can you see it?"
Claude: [uses get_board_info MCP tool]
Yes! I can see R1 is now at (59.175, 49.0) mm
(previously it was at 30.0, 30.0 mm)
```
## Tested Workflows
### Test 1: MCP→UI (Verified ✅)
**Setup:**
- Created new board via MCP (100x80mm)
- Placed R1 (10k resistor) at (30, 30) mm
- Placed D1 (RED LED) at (50, 30) mm
**Result:**
- Opened KiCAD PCB editor
- Both components visible at correct positions ✅
- All properties (reference, value, rotation) correct ✅
### Test 2: UI→MCP (Verified ✅)
**Setup:**
- User moved R1 from (30, 30) mm to (59.175, 49.0) mm in UI
- User saved file (Ctrl+S)
**Result:**
- MCP read board via `get_board_info`
- New position detected correctly ✅
- D1 position unchanged (as expected) ✅
## Current Capabilities
### What Works
1. **Bidirectional sync** (via file save/reload)
2. **Component placement** (MCP→UI)
3. **Component reading** (UI→MCP)
4. **Position/rotation updates** (both directions)
5. **Value/reference changes** (both directions)
6. **Trace routing** (both directions)
7. **Net information** (both directions)
8. **Board properties** (size, layers, design rules)
### MCP Tools for Collaboration
**Reading board state:**
- `get_board_info` - Get all components and their positions
- `get_project_info` - Get project metadata
- `list_components` - List all components (if implemented)
**Modifying board:**
- `place_component` - Add new components
- `add_trace` - Add copper traces
- `add_via` - Add vias
- `add_copper_pour` - Add copper zones
- `add_mounting_hole` - Add mounting holes
- `add_board_text` - Add text to board
## Limitations
### Current Limitations
1. **Manual Save Required**
- UI changes require manual save (Ctrl+S)
- No automatic file watching (yet)
- Workaround: Always save before asking Claude
2. **Manual Reload Required**
- MCP changes require reload in UI
- Options: File → Revert, or close/reopen
- Future: Could implement auto-reload trigger
3. **No Live Sync**
- Changes not visible until save/reload
- Not truly "real-time" (more like "near-time")
- File-based sync has ~1-5 second latency
4. **No Conflict Detection**
- If both edit simultaneously, last save wins
- No merge conflict resolution
- Best practice: Take turns editing
5. **No Change Notifications**
- MCP doesn't know when UI saves
- UI doesn't know when MCP saves
- Future: Could add file system watchers
### Known Issues
1. **Zone Filling:** Copper pours created by MCP won't be filled (requires UI to fill)
2. **Undo History:** UI undo history lost after MCP changes
3. **DRC Errors:** MCP doesn't run design rule checks automatically
## Best Practices
### For AI-Human Collaboration
1. **Establish Turn-Taking:**
```
User: "I'm going to add some components, one sec"
[User edits in UI]
User: "Done, saved the file"
Claude: [reads changes] "I see you added C1 and C2..."
```
2. **Always Save/Reload:**
- Human: Save after every change (Ctrl+S)
- Human: Reload after Claude makes changes
- Claude: Always read fresh before making decisions
3. **Communicate Changes:**
```
Claude: "I'm placing R1-R4 now..."
[MCP places components]
Claude: "Done! Reload the board to see them"
User: [File → Revert]
```
4. **Use Descriptive References:**
- Good: R1, R2, C1, C2 (sequential)
- Bad: R_temp, R_test (unclear)
### Workflow Patterns
**Pattern 1: AI Does Layout, Human Reviews**
```
1. Claude places all components via MCP
2. Claude routes critical traces via MCP
3. Human opens in KiCAD UI
4. Human fine-tunes positions
5. Human completes routing
6. Saves → Claude reads final result
```
**Pattern 2: Human Sketches, AI Refines**
```
1. Human places major components in UI
2. Saves → Claude reads layout
3. Claude suggests improvements
4. Claude places remaining components via MCP
5. Human reloads and reviews
6. Iterate until satisfied
```
**Pattern 3: Pair Programming Style**
```
User: "Place a 10k pull-up resistor on pin 3"
Claude: [places R1 at calculated position]
"Done! Check position (45, 20) mm"
User: [reloads] "Looks good, now add the LED"
Claude: [places D1]
[Continue back-and-forth]
```
## Future Enhancements
### Planned Improvements
1. **File System Watchers** (Week 4+)
- Auto-detect when UI saves file
- Auto-reload UI when MCP saves (via IPC)
- Near-instant sync (<100ms)
2. **IPC Backend** (Week 3)
- Direct communication with KiCAD process
- Live sync without file save/reload
- True real-time collaboration
3. **Change Notifications**
- MCP sends notification when it modifies board
- UI shows toast: "Claude added 4 components"
- Automatic reload option
4. **Conflict Detection**
- Detect when both edited same component
- Show diff/merge UI
- Allow choosing which changes to keep
5. **Collaborative Cursor**
- Show Claude's "cursor" in UI
- Highlight component being placed
- Visual feedback for AI actions
### Long-Term Vision
**Fully Real-Time Collaboration:**
- Both AI and human see changes instantly
- No manual save/reload required
- Conflict detection and resolution
- Visual indicators for who's editing what
- Chat/comment system for design discussion
**Example Future Workflow:**
```
[Claude and human both have board open]
Claude: [starts placing R1]
[R1 appears in UI with "Claude is placing..." indicator]
User: [sees R1 appear in real-time]
[moves D1 to better position]
[Claude sees D1 move instantly]
Claude: "Good position for D1! I'll route them now"
[traces appear as Claude creates them]
```
## Technical Details
### File Format
KiCAD uses S-expression format (`.kicad_pcb`):
```lisp
(kicad_pcb (version 20240108) (generator "pcbnew")
(footprint "Resistor_SMD:R_0603_1608Metric"
(layer "F.Cu")
(at 30.0 30.0 0)
(property "Reference" "R1")
(property "Value" "10k")
...
)
)
```
### Sync Mechanism
**Current (File-based):**
1. MCP: `pcbnew.SaveBoard(path, board)` → writes file
2. UI: File → Revert → reads file
3. Latency: ~1-5 seconds (manual)
**Future (IPC-based):**
1. MCP: `kicad.AddFootprint(...)` → sends IPC command
2. KiCAD: Receives command → updates internal state
3. UI: Automatically refreshes display
4. Latency: ~50-100ms (automatic)
### Python API Used
```python
import pcbnew
# Load board
board = pcbnew.LoadBoard('project.kicad_pcb')
# Read components
for fp in board.GetFootprints():
ref = fp.Reference().GetText()
pos = fp.GetPosition()
x_mm = pos.x / 1_000_000.0
y_mm = pos.y / 1_000_000.0
# Modify board
module = pcbnew.FootprintLoad(lib_path, 'R_0603')
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
board.Add(module)
# Save changes
pcbnew.SaveBoard('project.kicad_pcb', board)
```
## Troubleshooting
### "I don't see MCP changes in KiCAD UI"
**Cause:** UI hasn't reloaded the file
**Solution:**
1. File → Revert (or Ctrl+R if configured)
2. Or close PCB editor and reopen
3. Or restart KiCAD
### "MCP doesn't see my UI changes"
**Cause:** File not saved
**Solution:**
1. Save file: Ctrl+S or File → Save
2. Verify save: Check file modification time
3. Ask Claude to read board again
### "Changes disappeared after reload"
**Cause:** File overwritten by other party
**Solution:**
1. Always save before asking MCP to make changes
2. Don't edit while MCP is working
3. Take turns to avoid conflicts
### "Components appear in wrong positions"
**Cause:** Unit conversion error or coordinate system mismatch
**Solution:**
1. Check KiCAD units (View → Switch Units)
2. MCP uses millimeters internally
3. Report issue if positions consistently wrong
## Conclusion
**The real-time collaboration workflow is WORKING and TESTED! ✅**
The KiCAD MCP Server successfully enables paired circuit board design between AI (Claude) and human designers. While it requires manual save/reload steps, both MCP→UI and UI→MCP workflows function correctly.
**Current State:** "Near-real-time" collaboration (1-5 second latency)
**Future State:** True real-time with IPC backend (<100ms latency)
**Mission Accomplished:** Real-time paired circuit board design is operational and ready for use! 🎉
---
## Related Documentation
- [LIBRARY_INTEGRATION.md](./LIBRARY_INTEGRATION.md) - Component library system
- [STATUS_SUMMARY.md](./STATUS_SUMMARY.md) - Current implementation status
- [ROADMAP.md](./ROADMAP.md) - Future development plans
- [API.md](./API.md) - Full MCP API reference
## Changelog
**2025-11-01 - v2.1.0-alpha**
- Tested MCPUI workflow (placing components via MCP, viewing in UI)
- Tested UIMCP workflow (editing in UI, reading via MCP)
- Documented best practices and limitations
- Confirmed real-time collaboration mission is met

View File

@@ -1,314 +1,124 @@
# KiCAD MCP Roadmap
**Vision:** Enable anyone to design professional PCBs through natural conversation with AI
**Current Version:** 2.1.0-alpha
**Target:** 2.0.0 stable by end of Week 12
---
## Week 2: Component Integration & Routing
**Goal:** Make the MCP server useful for real PCB design
**Status:** 80% Complete (2025-11-01)
### High Priority
**1. Component Library Integration****COMPLETE**
- [x] Detect KiCAD footprint library paths
- [x] Add configuration for custom library paths
- [x] Create footprint search/autocomplete
- [x] Test component placement end-to-end
- [x] Document supported footprints
**Deliverable:** ✅ Place components with actual footprints from libraries (153 libraries discovered!)
**2. Routing Operations****COMPLETE**
- [x] Test `route_trace` with KiCAD 9.0
- [x] Test `add_via` with KiCAD 9.0
- [x] Test `add_copper_pour` with KiCAD 9.0
- [x] Fix any API compatibility issues
- [x] Add routing examples to docs
**Deliverable:** ✅ Successfully route a simple board (tested with nets, traces, vias, copper pours)
**3. JLCPCB Parts Database** 📋 **PLANNED**
- [x] Research JLCPCB API and data format
- [x] Design integration architecture
- [ ] Download/parse JLCPCB parts database (~108k parts)
- [ ] Map parts to KiCAD footprints
- [ ] Create search by part number
- [ ] Add price/stock information
- [ ] Integrate with component placement
**Deliverable:** "Add a 10k resistor (JLCPCB basic part)" - Ready to implement
### Medium Priority
**4. Fix get_board_info** 🟡 **DEFERRED**
- [ ] Update layer constants for KiCAD 9.0
- [ ] Add backward compatibility
- [ ] Test with real boards
**Status:** Low priority, workarounds available
**5. Example Projects** 🟢
- [ ] LED blinker (555 timer)
- [ ] Arduino Uno shield template
- [ ] Raspberry Pi HAT template
- [ ] Video tutorial of complete workflow
### Bonus Achievements ✨
**Real-time Collaboration****COMPLETE**
- [x] Test MCP→UI workflow (AI places, human sees)
- [x] Test UI→MCP workflow (human edits, AI reads)
- [x] Document best practices and limitations
- [x] Verify bidirectional sync works correctly
**Documentation****COMPLETE**
- [x] LIBRARY_INTEGRATION.md (comprehensive library guide)
- [x] REALTIME_WORKFLOW.md (collaboration workflows)
- [x] JLCPCB_INTEGRATION_PLAN.md (implementation plan)
---
## Week 3: IPC Backend & Real-time Updates
**Goal:** Eliminate manual reload - see changes instantly
**Status:** 🟢 **IMPLEMENTED** (2025-11-30)
### High Priority
**1. IPC Connection****COMPLETE**
- [x] Establish socket connection to KiCAD
- [x] Handle connection errors gracefully
- [x] Auto-reconnect if KiCAD restarts
- [x] Fall back to SWIG if IPC unavailable
**2. IPC Operations****COMPLETE**
- [x] Port project operations to IPC
- [x] Port board operations to IPC
- [x] Port component operations to IPC
- [x] Port routing operations to IPC
**3. Real-time UI Updates****COMPLETE**
- [x] Changes appear instantly in UI
- [x] No reload prompt
- [x] Visual feedback within 100ms
- [ ] Demo video showing real-time design
**Deliverable:** ✅ Design a board with live updates as Claude works
### Medium Priority
**4. Dual Backend Support****COMPLETE**
- [x] Auto-detect if IPC is available
- [x] Switch between SWIG/IPC seamlessly
- [x] Document when to use each
- [ ] Performance comparison
---
## Week 4-5: Smart BOM & Supplier Integration
**Goal:** Optimize component selection for cost and availability
**1. Digikey Integration**
- [ ] API authentication
- [ ] Part search by specs
- [ ] Price/stock checking
- [ ] Parametric search (e.g., "10k resistor, 0603, 1%")
**2. Smart BOM Management**
- [ ] Auto-suggest component substitutions
- [ ] Calculate total board cost
- [ ] Check component availability
- [ ] Generate purchase links
**3. Cost Optimization**
- [ ] Suggest JLCPCB basic parts (free assembly)
- [ ] Warn about expensive/obsolete parts
- [ ] Batch component suggestions
**Deliverable:** "Design a low-cost LED driver under $5 BOM"
---
## Week 6-7: Design Patterns & Templates
**Goal:** Accelerate common design tasks
**1. Circuit Patterns Library**
- [ ] Voltage regulators (LDO, switching)
- [ ] USB interfaces (USB-C, micro-USB)
- [ ] Microcontroller circuits (ESP32, STM32, RP2040)
- [ ] Power protection (reverse polarity, ESD)
- [ ] Common interfaces (I2C, SPI, UART)
**2. Board Templates**
- [ ] Arduino form factors (Uno, Nano, Mega)
- [ ] Raspberry Pi HATs
- [ ] Feather wings
- [ ] Custom PCB shapes (badges, wearables)
**3. Auto-routing Helpers**
- [ ] Suggest trace widths by current
- [ ] Auto-create ground pours
- [ ] Match differential pair lengths
- [ ] Check impedance requirements
**Deliverable:** "Create an ESP32 dev board with USB-C"
---
## Week 8-9: Guided Workflows & Education
**Goal:** Make PCB design accessible to beginners
**1. Interactive Tutorials**
- [ ] First PCB (LED blinker)
- [ ] Understanding layers and vias
- [ ] Routing best practices
- [ ] Design rule checking
**2. Design Validation**
- [ ] Check for common mistakes
- [ ] Suggest improvements
- [ ] Explain DRC violations
- [ ] Manufacturing feasibility check
**3. Documentation Generation**
- [ ] Auto-generate assembly drawings
- [ ] Create BOM spreadsheets
- [ ] Export fabrication files
- [ ] Generate user manual
**Deliverable:** Complete beginner-to-fabrication tutorial
---
## Week 10-11: Advanced Features
**Goal:** Support complex professional designs
**1. Multi-board Projects**
- [ ] Panel designs for manufacturing
- [ ] Shared schematics across boards
- [ ] Version management
**2. High-speed Design**
- [ ] Impedance-controlled traces
- [ ] Length matching for DDR/PCIe
- [ ] Signal integrity analysis
- [ ] Via stitching for EMI
**3. Advanced Components**
- [ ] BGAs and fine-pitch packages
- [ ] Flex PCB support
- [ ] Rigid-flex designs
---
## Week 12: Polish & Release
**Goal:** Production-ready v2.0 release
**1. Performance**
- [ ] Optimize large board operations
- [ ] Cache library searches
- [ ] Parallel operations where possible
**2. Testing**
- [ ] Unit tests for all commands
- [ ] Integration tests for workflows
- [ ] Test on Windows/macOS/Linux
- [ ] Load testing with complex boards
**3. Documentation**
- [ ] Complete API reference
- [ ] Video tutorial series
- [ ] Blog post/announcement
- [ ] Example project gallery
**4. Community**
- [ ] Contribution guidelines
- [ ] Plugin system for custom tools
- [ ] Discord/forum for support
**Deliverable:** KiCAD MCP v2.0 stable release
---
## Future (Post-v2.0)
**Big Ideas for v3.0+**
**1. AI-Powered Design**
- Generate circuits from specifications
- Optimize layouts for size/cost/performance
- Suggest alternative designs
- Learn from user preferences
**2. Collaboration**
- Multi-user design sessions
- Design reviews and comments
- Version control integration (Git)
- Share design patterns
**3. Manufacturing Integration**
- Direct order to PCB fabs
- Assembly service integration
- Track order status
- Automated quoting
**4. Simulation**
- SPICE integration for circuit sim
- Thermal simulation
- Signal integrity
- Power integrity
**5. Extended Platform Support**
- Altium import/export
- Eagle compatibility
- EasyEDA integration
- Web-based viewer
---
## Success Metrics
**v2.0 Release Criteria:**
- [ ] 95%+ of commands working reliably
- [ ] Component placement with 10,000+ footprints
- [ ] IPC backend working on all platforms
- [ ] 10+ example projects
- [ ] 5+ video tutorials
- [ ] 100+ GitHub stars
- [ ] 10+ community contributors
**User Success Stories:**
- "Designed my first PCB with Claude Code in 30 minutes"
- "Cut PCB design time by 80% using MCP"
- "Got my board manufactured - it works!"
---
## How to Contribute
See the roadmap and want to help?
**High-value contributions:**
1. Component library mappings (JLCPCB → KiCAD)
2. Design pattern library (circuits you use often)
3. Testing on Windows/macOS
4. Documentation and tutorials
5. Bug reports with reproductions
Check [CONTRIBUTING.md](../CONTRIBUTING.md) for details.
---
**Last Updated:** 2025-11-30
**Maintained by:** KiCAD MCP Team
# KiCAD MCP Roadmap
**Vision:** Enable anyone to design professional PCBs through natural conversation with AI
**Current Version:** 2.2.3
**Last Updated:** 2026-03-21
---
## Completed Milestones
### v1.0.0 - Core Foundation (October 2025)
- [x] MCP protocol implementation (JSON-RPC 2.0, MCP 2025-06-18)
- [x] Project management (create, open, save)
- [x] Board operations (size, outline, layers, mounting holes, text)
- [x] Component placement with 153+ footprint libraries
- [x] Basic routing (traces, vias, copper pours)
- [x] Design rule checking
- [x] Export (Gerber, PDF, SVG, 3D, BOM)
- [x] Cross-platform support (Linux, Windows, macOS)
- [x] UI auto-launch and detection
### v2.0.0-alpha - Router and IPC (November-December 2025)
- [x] Tool router pattern -- 70% AI context reduction
- [x] IPC backend for real-time KiCAD UI synchronization (21 commands)
- [x] Hybrid SWIG/IPC backend with automatic fallback
- [x] Comprehensive Windows support with automated setup
### v2.1.0-alpha - Schematics and JLCPCB (January 2026)
- [x] Complete schematic workflow fix (Issue #26)
- [x] Dynamic symbol loading -- access to all ~10,000 KiCad symbols
- [x] Intelligent wiring system with pin discovery and smart routing
- [x] Power symbol support (VCC, GND, +3V3, +5V)
- [x] Wire graph analysis for net connectivity
- [x] JLCPCB parts integration (2.5M+ parts, dual-mode architecture)
- [x] Local symbol library search (contributor: @l3wi)
### v2.2.0 through v2.2.3 - Routing, Creators, Autorouting (February-March 2026)
- [x] 13 new routing/component tools (delete/query/modify traces, arrays, alignment)
- [x] route_pad_to_pad with auto-via insertion for cross-layer connections
- [x] copy_routing_pattern for trace replication
- [x] route_differential_pair for matched signals
- [x] Custom footprint creator (4 tools)
- [x] Custom symbol creator (4 tools)
- [x] Datasheet enrichment tools (LCSC integration)
- [x] 11 schematic inspection/editing tools (contributor: @Mehanik)
- [x] FFC/ribbon cable passthrough workflow (connect_passthrough, sync_schematic_to_board)
- [x] SVG logo import for PCB silkscreen
- [x] ERC validation
- [x] Project snapshot system
- [x] Freerouting autorouter integration with Docker/Podman (contributor: @jflaflamme)
- [x] Project-local library resolution
- [x] Developer mode (KICAD_MCP_DEV=1)
---
## Current Focus: v2.3+
### Documentation Overhaul (In Progress)
- [ ] Per-feature documentation for all 122 tools
- [ ] Architecture guide for contributors
- [ ] End-to-end PCB design workflow guide
- [ ] Documentation index
### Quality and Stability
- [ ] Expand test coverage across all tool categories
- [ ] Performance profiling for large boards
- [ ] Update package.json version to match CHANGELOG
---
## Planned Features
### Supplier Integration
- [ ] Digikey API integration
- [ ] Mouser API integration
- [ ] Smart BOM management with real-time pricing
- [ ] Cost optimization across suppliers
### Design Patterns and Templates
- [ ] Circuit patterns library (voltage regulators, USB, microcontrollers)
- [ ] Board templates (Arduino shields, RPi HATs, Feather wings)
- [ ] Auto-suggest trace widths by current
- [ ] Impedance-controlled trace support
### Advanced Capabilities
- [ ] Panelization support
- [ ] Multi-board project management
- [ ] High-speed design helpers (length matching, via stitching)
- [ ] SPICE simulation integration
### Community and Education
- [ ] Example project gallery with tutorials
- [ ] Video walkthrough series
- [ ] Interactive beginner tutorials
- [ ] Plugin system for custom tools
---
## How to Contribute
See the roadmap items above and want to help? High-value contributions:
1. Testing on Windows/macOS with KiCAD 9
2. Example projects and workflow documentation
3. Bug reports with reproduction steps
4. New tool implementations (see [ARCHITECTURE.md](ARCHITECTURE.md))
5. Design pattern library contributions
Check [CONTRIBUTING.md](../CONTRIBUTING.md) for details.
---
_Maintained by: KiCAD MCP Team and community contributors_

View File

@@ -1,353 +1,383 @@
# Router Architecture Design
## Overview
This document describes the router pattern implementation for the KiCAD MCP Server. The router reduces context window consumption from ~40K tokens (59 tools) to ~12K tokens (16 visible tools).
## Architecture Layers
```
┌─────────────────────────────────────────────────────────────┐
│ MCP Client (Claude) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ KiCAD MCP Server │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ DIRECT TOOLS (Always Visible - 12) ││
│ │ • create_project • open_project • save_project ││
│ │ • get_project_info • place_component • move_component ││
│ │ • add_net • route_trace • get_board_info ││
│ │ • set_board_size • add_board_outline • check_kicad_ui││
│ └─────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ROUTER TOOLS (Discovery - 4) ││
│ │ • list_tool_categories • get_category_tools ││
│ │ • execute_tool • search_tools ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ROUTED TOOLS (Hidden - 47) ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ board │ │component │ │ export │ │ drc │ ││
│ │ │(9 tools) │ │(8 tools) │ │(8 tools) │ │(9 tools) │ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │schematic │ │ library │ │ routing │ ┌──────────┐ ││
│ │ │(9 tools) │ │(4 tools) │ │(3 tools) │ │ui (1 tool)│ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
```
## Tool Categories
### Direct Tools (12 tools - always visible)
These cover the primary workflow (80%+ of use cases):
1. **Project Lifecycle** (4):
- `create_project` - Create new KiCAD project
- `open_project` - Open existing project
- `save_project` - Save current project
- `get_project_info` - Get project information
2. **Core PCB Operations** (6):
- `place_component` - Place component on board
- `move_component` - Move component to new position
- `add_net` - Create a new net
- `route_trace` - Route trace between points
- `get_board_info` - Get board information
- `set_board_size` - Set board dimensions
3. **Board Setup** (1):
- `add_board_outline` - Add board outline
4. **UI Management** (1):
- `check_kicad_ui` - Check if KiCAD UI is running
### Routed Categories (7 categories, 47 tools)
#### 1. `board` - Board Configuration & Layout (9 tools)
Setup and configuration operations.
**Tools:**
- `add_layer` - Add PCB layer
- `set_active_layer` - Set active layer
- `get_layer_list` - List all layers
- `add_mounting_hole` - Add mounting hole
- `add_board_text` - Add text to board
- `add_zone` - Add copper zone/pour
- `get_board_extents` - Get board boundaries
- `get_board_2d_view` - Get 2D visualization
- `launch_kicad_ui` - Launch KiCAD UI
#### 2. `component` - Advanced Component Operations (8 tools)
Beyond basic placement.
**Tools:**
- `rotate_component` - Rotate component
- `delete_component` - Delete component
- `edit_component` - Edit component properties
- `find_component` - Find component by reference/value
- `get_component_properties` - Get component properties
- `add_component_annotation` - Add component annotation
- `group_components` - Group components together
- `replace_component` - Replace component with another
#### 3. `export` - File Export & Manufacturing (8 tools)
Generate output files for fabrication and documentation.
**Tools:**
- `export_gerber` - Export Gerber files
- `export_pdf` - Export PDF
- `export_svg` - Export SVG
- `export_3d` - Export 3D model (STEP/STL/VRML/OBJ)
- `export_bom` - Export bill of materials
- `export_netlist` - Export netlist
- `export_position_file` - Export component positions
- `export_vrml` - Export VRML 3D model
#### 4. `drc` - Design Rules & Validation (9 tools)
Design rule checking and electrical validation.
**Tools:**
- `set_design_rules` - Configure design rules
- `get_design_rules` - Get current rules
- `run_drc` - Run design rule check
- `add_net_class` - Add net class
- `assign_net_to_class` - Assign net to class
- `set_layer_constraints` - Set layer constraints
- `check_clearance` - Check clearance between items
- `get_drc_violations` - Get DRC violations
#### 5. `schematic` - Schematic Operations (9 tools)
Schematic editor operations.
**Tools:**
- `create_schematic` - Create new schematic
- `add_schematic_component` - Add component to schematic
- `add_wire` - Add wire connection
- `add_schematic_connection` - Connect component pins
- `add_schematic_net_label` - Add net label
- `connect_to_net` - Connect pin to net
- `get_net_connections` - Get net connections
- `generate_netlist` - Generate netlist
#### 6. `library` - Footprint Library Access (4 tools)
Search and browse footprint libraries.
**Tools:**
- `list_libraries` - List available libraries
- `search_footprints` - Search footprints
- `list_library_footprints` - List library footprints
- `get_footprint_info` - Get footprint details
#### 7. `routing` - Advanced Routing (3 tools)
Advanced routing operations beyond basic trace routing.
**Tools:**
- `add_via` - Add via
- `add_copper_pour` - Add copper pour
**Note:** `add_net` and `route_trace` are direct tools as they're core operations.
## Router Tools
### 1. `list_tool_categories`
**Description:** List all available tool categories with descriptions and tool counts.
**Parameters:** None
**Returns:**
```json
{
"total_categories": 7,
"total_tools": 47,
"categories": [
{
"name": "board",
"description": "Board configuration: layers, mounting holes, zones, visualization",
"tool_count": 9
},
// ... more categories
]
}
```
### 2. `get_category_tools`
**Description:** Get detailed information about all tools in a specific category.
**Parameters:**
- `category` (string) - Category name from `list_tool_categories`
**Returns:**
```json
{
"category": "export",
"description": "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
"tools": [
{
"name": "export_gerber",
"description": "Export Gerber files for PCB fabrication",
"parameters": { /* zod schema */ }
},
// ... more tools
]
}
```
### 3. `execute_tool`
**Description:** Execute a tool from any category.
**Parameters:**
- `tool_name` (string) - Tool name from `get_category_tools`
- `params` (object, optional) - Tool parameters
**Returns:** Tool execution result
### 4. `search_tools`
**Description:** Search for tools by keyword across all categories.
**Parameters:**
- `query` (string) - Search term (e.g., "gerber", "zone", "export")
**Returns:**
```json
{
"query": "export",
"count": 8,
"matches": [
{
"category": "export",
"tool": "export_gerber",
"description": "Export Gerber files for PCB fabrication"
},
// ... more matches
]
}
```
## Implementation Files
### New Files to Create
1. **`src/tools/registry.ts`**
- Tool category definitions
- Tool metadata storage
- Lookup maps (by name, by category)
- Search functionality
2. **`src/tools/router.ts`**
- Router tool implementations
- `list_tool_categories` handler
- `get_category_tools` handler
- `execute_tool` handler
- `search_tools` handler
3. **`src/tools/direct.ts`**
- Export direct tool definitions
- Keep existing tool implementations but organized
### Modified Files
1. **`src/server.ts`** or **`src/kicad-server.ts`**
- Register only direct tools + router tools
- Remove registration of routed tools
- Tools still callable via `execute_tool`
## Migration Strategy
### Phase 1: Create Infrastructure
1. Create `registry.ts` with all tool definitions
2. Create `router.ts` with router tools
3. Create `direct.ts` with direct tool list
### Phase 2: Update Server
1. Modify server registration to use direct + router only
2. Keep all existing tool handlers intact
3. Route through `execute_tool`
### Phase 3: Testing
1. Test direct tools work as before
2. Test router tools (list/get/execute/search)
3. Test routed tools via `execute_tool`
### Phase 4: Optimization (Optional)
1. Add caching for tool lookups
2. Add tool usage analytics
3. Implement intelligent tool suggestions
## Benefits
1. **Context Efficiency**: 70% reduction in tokens (~28K saved)
2. **Better Organization**: Tools grouped by function
3. **Discoverability**: Easy to find the right tool
4. **Scalability**: Can add unlimited tools without bloating context
5. **Backwards Compatible**: Existing Python commands still work
## Usage Examples
### Example 1: User Wants to Export Gerbers
```
User: "Export gerbers for this board"
Claude's workflow:
1. Sees "export" keyword
2. Calls search_tools({ query: "gerber" })
→ Returns: { category: "export", tool: "export_gerber", ... }
3. Calls execute_tool({
tool_name: "export_gerber",
params: { outputDir: "./gerbers" }
})
→ Returns: { success: true, files: [...] }
Claude: "I've exported the Gerber files to ./gerbers/"
```
### Example 2: User Wants to Place Component
```
User: "Add a 0805 resistor at position 10,20"
Claude's workflow:
1. Sees place_component in direct tools
2. Calls place_component({
componentId: "R_0805",
position: { x: 10, y: 20, unit: "mm" }
})
→ Returns: { success: true, reference: "R1" }
Claude: "Added R1 (0805 resistor) at position (10, 20) mm"
```
### Example 3: User Wants Unknown Operation
```
User: "Check the board for design rule violations"
Claude's workflow:
1. Uncertain which tool to use
2. Calls search_tools({ query: "design rule violations" })
→ Returns: { category: "drc", tool: "run_drc", ...}
3. Calls get_category_tools({ category: "drc" })
→ Returns full DRC category tools with parameters
4. Calls execute_tool({
tool_name: "run_drc",
params: {}
})
→ Returns: DRC results
Claude: "I ran the design rule check. Found 3 violations: ..."
```
## Success Metrics
- ✅ Token usage: ~12K (vs 40K before)
- ✅ Tool discovery time: <2 calls (search execute)
- User experience: Unchanged (seamless)
- Maintainability: Improved (organized categories)
- Scalability: Can add 100+ more tools easily
# Router Architecture Design
## Overview
This document describes the router pattern implementation for the KiCAD MCP Server. The router reduces context window consumption by organizing 122+ tools into 8 discoverable categories, keeping only the most frequently used tools directly visible.
## Architecture Layers
```
┌─────────────────────────────────────────────────────────────┐
│ MCP Client (Claude) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ KiCAD MCP Server │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ DIRECT TOOLS (Always Visible - 12) ││
│ │ • create_project • open_project • save_project ││
│ │ • get_project_info • place_component • move_component ││
│ │ • add_net • route_trace • get_board_info ││
│ │ • set_board_size • add_board_outline • check_kicad_ui││
│ └─────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ROUTER TOOLS (Discovery - 4) ││
│ │ • list_tool_categories • get_category_tools ││
│ │ • execute_tool • search_tools ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ROUTED TOOLS (Hidden - 110+) ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ board │ │component │ │ export │ │ drc │ ││
│ │ │ tools │ │ tools │ │ tools │ │ tools │ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │schematic │ │ library │ │ routing │ │footprint │ ││
│ │ │ tools │ │ tools │ │ tools │ │ tools │ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
```
## Tool Categories
### Direct Tools (12 tools - always visible)
These cover the primary workflow (80%+ of use cases):
1. **Project Lifecycle** (4):
- `create_project` - Create new KiCAD project
- `open_project` - Open existing project
- `save_project` - Save current project
- `get_project_info` - Get project information
2. **Core PCB Operations** (6):
- `place_component` - Place component on board
- `move_component` - Move component to new position
- `add_net` - Create a new net
- `route_trace` - Route trace between points
- `get_board_info` - Get board information
- `set_board_size` - Set board dimensions
3. **Board Setup** (1):
- `add_board_outline` - Add board outline
4. **UI Management** (1):
- `check_kicad_ui` - Check if KiCAD UI is running
### Routed Categories (8+ categories, 110+ tools)
#### 1. `board` - Board Configuration & Layout (9 tools)
Setup and configuration operations.
**Tools:**
- `add_layer` - Add PCB layer
- `set_active_layer` - Set active layer
- `get_layer_list` - List all layers
- `add_mounting_hole` - Add mounting hole
- `add_board_text` - Add text to board
- `add_zone` - Add copper zone/pour
- `get_board_extents` - Get board boundaries
- `get_board_2d_view` - Get 2D visualization
- `launch_kicad_ui` - Launch KiCAD UI
#### 2. `component` - Advanced Component Operations (8 tools)
Beyond basic placement.
**Tools:**
- `rotate_component` - Rotate component
- `delete_component` - Delete component
- `edit_component` - Edit component properties
- `find_component` - Find component by reference/value
- `get_component_properties` - Get component properties
- `add_component_annotation` - Add component annotation
- `group_components` - Group components together
- `replace_component` - Replace component with another
#### 3. `export` - File Export & Manufacturing (8 tools)
Generate output files for fabrication and documentation.
**Tools:**
- `export_gerber` - Export Gerber files
- `export_pdf` - Export PDF
- `export_svg` - Export SVG
- `export_3d` - Export 3D model (STEP/STL/VRML/OBJ)
- `export_bom` - Export bill of materials
- `export_netlist` - Export netlist
- `export_position_file` - Export component positions
- `export_vrml` - Export VRML 3D model
#### 4. `drc` - Design Rules & Validation (9 tools)
Design rule checking and electrical validation.
**Tools:**
- `set_design_rules` - Configure design rules
- `get_design_rules` - Get current rules
- `run_drc` - Run design rule check
- `add_net_class` - Add net class
- `assign_net_to_class` - Assign net to class
- `set_layer_constraints` - Set layer constraints
- `check_clearance` - Check clearance between items
- `get_drc_violations` - Get DRC violations
#### 5. `schematic` - Schematic Operations (9 tools)
Schematic editor operations.
**Tools:**
- `create_schematic` - Create new schematic
- `add_schematic_component` - Add component to schematic
- `add_wire` - Add wire connection
- `add_schematic_connection` - Connect component pins
- `add_schematic_net_label` - Add net label
- `connect_to_net` - Connect pin to net
- `get_net_connections` - Get net connections
- `generate_netlist` - Generate netlist
#### 6. `library` - Footprint Library Access (4 tools)
Search and browse footprint libraries.
**Tools:**
- `list_libraries` - List available libraries
- `search_footprints` - Search footprints
- `list_library_footprints` - List library footprints
- `get_footprint_info` - Get footprint details
#### 7. `routing` - Advanced Routing (3 tools)
Advanced routing operations beyond basic trace routing.
**Tools:**
- `add_via` - Add via
- `add_copper_pour` - Add copper pour
**Note:** `add_net` and `route_trace` are direct tools as they're core operations.
## Router Tools
### 1. `list_tool_categories`
**Description:** List all available tool categories with descriptions and tool counts.
**Parameters:** None
**Returns:**
```json
{
"total_categories": 7,
"total_tools": 47,
"categories": [
{
"name": "board",
"description": "Board configuration: layers, mounting holes, zones, visualization",
"tool_count": 9
}
// ... more categories
]
}
```
### 2. `get_category_tools`
**Description:** Get detailed information about all tools in a specific category.
**Parameters:**
- `category` (string) - Category name from `list_tool_categories`
**Returns:**
```json
{
"category": "export",
"description": "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
"tools": [
{
"name": "export_gerber",
"description": "Export Gerber files for PCB fabrication",
"parameters": {
/* zod schema */
}
}
// ... more tools
]
}
```
### 3. `execute_tool`
**Description:** Execute a tool from any category.
**Parameters:**
- `tool_name` (string) - Tool name from `get_category_tools`
- `params` (object, optional) - Tool parameters
**Returns:** Tool execution result
### 4. `search_tools`
**Description:** Search for tools by keyword across all categories.
**Parameters:**
- `query` (string) - Search term (e.g., "gerber", "zone", "export")
**Returns:**
```json
{
"query": "export",
"count": 8,
"matches": [
{
"category": "export",
"tool": "export_gerber",
"description": "Export Gerber files for PCB fabrication"
}
// ... more matches
]
}
```
## Implementation Files
### New Files to Create
1. **`src/tools/registry.ts`**
- Tool category definitions
- Tool metadata storage
- Lookup maps (by name, by category)
- Search functionality
2. **`src/tools/router.ts`**
- Router tool implementations
- `list_tool_categories` handler
- `get_category_tools` handler
- `execute_tool` handler
- `search_tools` handler
3. **`src/tools/direct.ts`**
- Export direct tool definitions
- Keep existing tool implementations but organized
### Modified Files
1. **`src/server.ts`** or **`src/kicad-server.ts`**
- Register only direct tools + router tools
- Remove registration of routed tools
- Tools still callable via `execute_tool`
## Migration Strategy
### Phase 1: Create Infrastructure
1. Create `registry.ts` with all tool definitions
2. Create `router.ts` with router tools
3. Create `direct.ts` with direct tool list
### Phase 2: Update Server
1. Modify server registration to use direct + router only
2. Keep all existing tool handlers intact
3. Route through `execute_tool`
### Phase 3: Testing
1. Test direct tools work as before
2. Test router tools (list/get/execute/search)
3. Test routed tools via `execute_tool`
### Phase 4: Optimization (Optional)
1. Add caching for tool lookups
2. Add tool usage analytics
3. Implement intelligent tool suggestions
## Benefits
1. **Context Efficiency**: 70% reduction in tokens (~28K saved)
2. **Better Organization**: Tools grouped by function
3. **Discoverability**: Easy to find the right tool
4. **Scalability**: Can add unlimited tools without bloating context
5. **Backwards Compatible**: Existing Python commands still work
## Usage Examples
### Example 1: User Wants to Export Gerbers
```
User: "Export gerbers for this board"
Claude's workflow:
1. Sees "export" keyword
2. Calls search_tools({ query: "gerber" })
→ Returns: { category: "export", tool: "export_gerber", ... }
3. Calls execute_tool({
tool_name: "export_gerber",
params: { outputDir: "./gerbers" }
})
→ Returns: { success: true, files: [...] }
Claude: "I've exported the Gerber files to ./gerbers/"
```
### Example 2: User Wants to Place Component
```
User: "Add a 0805 resistor at position 10,20"
Claude's workflow:
1. Sees place_component in direct tools
2. Calls place_component({
componentId: "R_0805",
position: { x: 10, y: 20, unit: "mm" }
})
→ Returns: { success: true, reference: "R1" }
Claude: "Added R1 (0805 resistor) at position (10, 20) mm"
```
### Example 3: User Wants Unknown Operation
```
User: "Check the board for design rule violations"
Claude's workflow:
1. Uncertain which tool to use
2. Calls search_tools({ query: "design rule violations" })
→ Returns: { category: "drc", tool: "run_drc", ...}
3. Calls get_category_tools({ category: "drc" })
→ Returns full DRC category tools with parameters
4. Calls execute_tool({
tool_name: "run_drc",
params: {}
})
→ Returns: DRC results
Claude: "I ran the design rule check. Found 3 violations: ..."
```
## Success Metrics
- ✅ Token usage: ~12K (vs 40K before)
- ✅ Tool discovery time: <2 calls (search execute)
- User experience: Unchanged (seamless)
- Maintainability: Improved (organized categories)
- Scalability: Can add 100+ more tools easily

View File

@@ -1,168 +1,197 @@
# Router Quick Start Guide
## What is the Router?
The KiCAD MCP Server now includes an intelligent tool router that organizes 59 tools into 7 discoverable categories. This reduces AI context usage by up to 70% while maintaining full access to all functionality.
## How It Works
Instead of loading all 59 tool schemas into every conversation, Claude now sees:
- **12 direct tools** for high-frequency operations (always visible)
- **4 router tools** for discovering and executing the other 47 tools
When you ask Claude to do something (like "export gerber files"), it will:
1. Search for relevant tools using `search_tools`
2. Find the `export_gerber` tool in the "export" category
3. Execute it via `execute_tool` with your parameters
4. Return the results
**You don't need to change how you interact with Claude** - the discovery happens automatically!
## Tool Categories
The 47 routed tools are organized into these categories:
### 1. board (9 tools)
Board configuration: layers, mounting holes, zones, visualization
- add_layer, set_active_layer, get_layer_list
- add_mounting_hole, add_board_text
- add_zone, get_board_extents, get_board_2d_view
- launch_kicad_ui
### 2. component (8 tools)
Advanced component operations: edit, delete, search, group, annotate
- rotate_component, delete_component, edit_component
- find_component, get_component_properties
- add_component_annotation, group_components, replace_component
### 3. export (8 tools)
File export for fabrication and documentation
- export_gerber, export_pdf, export_svg, export_3d
- export_bom, export_netlist, export_position_file, export_vrml
### 4. drc (8 tools)
Design rule checking and electrical validation
- set_design_rules, get_design_rules, run_drc
- add_net_class, assign_net_to_class, set_layer_constraints
- check_clearance, get_drc_violations
### 5. schematic (8 tools)
Schematic operations: create, add components, wire connections
- create_schematic, add_schematic_component, add_wire
- add_schematic_connection, add_schematic_net_label
- connect_to_net, get_net_connections, generate_netlist
### 6. library (4 tools)
Footprint library access and search
- list_libraries, search_footprints
- list_library_footprints, get_footprint_info
### 7. routing (2 tools)
Advanced routing operations
- add_via, add_copper_pour
## Direct Tools (Always Available)
These 12 tools are always visible for common operations:
**Project Lifecycle:**
- create_project, open_project, save_project, get_project_info
**Core PCB Operations:**
- place_component, move_component
- add_net, route_trace
- get_board_info, set_board_size
- add_board_outline
**UI Management:**
- check_kicad_ui
## Router Tools
### list_tool_categories
Browse all available tool categories.
**Example:**
```
Claude, what tool categories are available?
```
### get_category_tools
View all tools in a specific category.
**Example:**
```
Show me all export tools available.
```
### search_tools
Find tools by keyword.
**Example:**
```
Search for tools related to "gerber" or "mounting holes"
```
### execute_tool
Execute any routed tool with parameters.
**Example:**
```
Execute the export_gerber tool with outputDir set to ./fabrication
```
## Usage Examples
### Natural Interaction (Recommended)
Just ask Claude what you want - it handles discovery automatically:
```
"Export gerber files to ./output"
"Add a mounting hole at x=10, y=10"
"Run a design rule check"
"Create a copper pour on the ground layer"
```
### Manual Discovery (Optional)
You can also browse tools explicitly:
```
"List all tool categories"
"What export tools are available?"
"Search for DRC tools"
```
## Benefits
1. **Reduced Context Usage**: 70% less AI context consumed per conversation
2. **Organized Tools**: Logical categorization makes tools easy to find
3. **Seamless Experience**: Works transparently - no changes to how you interact
4. **Extensible**: Easy to add new tools and categories
5. **Backwards Compatible**: All existing tools still work
## Technical Details
- **Registry**: `src/tools/registry.ts` - Tool categorization and lookup
- **Router**: `src/tools/router.ts` - Discovery and execution implementation
- **Server Integration**: `src/server.ts` - Router tools registered at startup
For implementation details, see:
- [ROUTER_ARCHITECTURE.md](ROUTER_ARCHITECTURE.md) - Design specification
- [ROUTER_IMPLEMENTATION_STATUS.md](ROUTER_IMPLEMENTATION_STATUS.md) - Current status
- [TOOL_INVENTORY.md](TOOL_INVENTORY.md) - Complete tool catalog
## Token Savings
**Before Router:**
- 59 tools × ~700 tokens each = ~42K tokens per conversation
**After Router (Current - Phase 1):**
- 12 direct tools + 4 router tools = 16 tools visible
- Still ~42K tokens (all tools still registered for backwards compatibility)
**After Phase 2 (Optional Optimization):**
- Only 16 tools visible to Claude
- ~12K tokens per conversation
- **70% reduction** in context usage
Phase 1 is complete and functional. Phase 2 (hiding routed tools) is optional and can be implemented when desired.
# Router Quick Start Guide
## What is the Router?
The KiCAD MCP Server includes an intelligent tool router that organizes 122+ tools into 8 discoverable categories. This reduces AI context usage by up to 70% while maintaining full access to all functionality.
## How It Works
Instead of loading all 59 tool schemas into every conversation, Claude now sees:
- **12 direct tools** for high-frequency operations (always visible)
- **4 router tools** for discovering and executing the other 47 tools
When you ask Claude to do something (like "export gerber files"), it will:
1. Search for relevant tools using `search_tools`
2. Find the `export_gerber` tool in the "export" category
3. Execute it via `execute_tool` with your parameters
4. Return the results
**You don't need to change how you interact with Claude** - the discovery happens automatically!
## Tool Categories
The 110+ routed tools are organized into these categories:
### 1. board (9 tools)
Board configuration: layers, mounting holes, zones, visualization
- add_layer, set_active_layer, get_layer_list
- add_mounting_hole, add_board_text
- add_zone, get_board_extents, get_board_2d_view
- launch_kicad_ui
### 2. component (8 tools)
Advanced component operations: edit, delete, search, group, annotate
- rotate_component, delete_component, edit_component
- find_component, get_component_properties
- add_component_annotation, group_components, replace_component
### 3. export (8 tools)
File export for fabrication and documentation
- export_gerber, export_pdf, export_svg, export_3d
- export_bom, export_netlist, export_position_file, export_vrml
### 4. drc (8 tools)
Design rule checking and electrical validation
- set_design_rules, get_design_rules, run_drc
- add_net_class, assign_net_to_class, set_layer_constraints
- check_clearance, get_drc_violations
### 5. schematic (8 tools)
Schematic operations: create, add components, wire connections
- create_schematic, add_schematic_component, add_wire
- add_schematic_connection, add_schematic_net_label
- connect_to_net, get_net_connections, generate_netlist
### 6. library (4 tools)
Footprint library access and search
- list_libraries, search_footprints
- list_library_footprints, get_footprint_info
### 7. routing (2 tools)
Advanced routing operations
- add_via, add_copper_pour
## Direct Tools (Always Available)
These 12 tools are always visible for common operations:
**Project Lifecycle:**
- create_project, open_project, save_project, get_project_info
**Core PCB Operations:**
- place_component, move_component
- add_net, route_trace
- get_board_info, set_board_size
- add_board_outline
**UI Management:**
- check_kicad_ui
## Router Tools
### list_tool_categories
Browse all available tool categories.
**Example:**
```
Claude, what tool categories are available?
```
### get_category_tools
View all tools in a specific category.
**Example:**
```
Show me all export tools available.
```
### search_tools
Find tools by keyword.
**Example:**
```
Search for tools related to "gerber" or "mounting holes"
```
### execute_tool
Execute any routed tool with parameters.
**Example:**
```
Execute the export_gerber tool with outputDir set to ./fabrication
```
## Usage Examples
### Natural Interaction (Recommended)
Just ask Claude what you want - it handles discovery automatically:
```
"Export gerber files to ./output"
"Add a mounting hole at x=10, y=10"
"Run a design rule check"
"Create a copper pour on the ground layer"
```
### Manual Discovery (Optional)
You can also browse tools explicitly:
```
"List all tool categories"
"What export tools are available?"
"Search for DRC tools"
```
## Benefits
1. **Reduced Context Usage**: 70% less AI context consumed per conversation
2. **Organized Tools**: Logical categorization makes tools easy to find
3. **Seamless Experience**: Works transparently - no changes to how you interact
4. **Extensible**: Easy to add new tools and categories
5. **Backwards Compatible**: All existing tools still work
## Technical Details
- **Registry**: `src/tools/registry.ts` - Tool categorization and lookup
- **Router**: `src/tools/router.ts` - Discovery and execution implementation
- **Server Integration**: `src/server.ts` - Router tools registered at startup
For implementation details, see:
- [ROUTER_ARCHITECTURE.md](ROUTER_ARCHITECTURE.md) - Design specification
- [ROUTER_IMPLEMENTATION_STATUS.md](ROUTER_IMPLEMENTATION_STATUS.md) - Current status
- [TOOL_INVENTORY.md](TOOL_INVENTORY.md) - Complete tool catalog
## Token Savings
**Before Router:**
- 122 tools × ~700 tokens each = ~85K tokens per conversation
**After Router (Current):**
- 12 direct tools + 4 router tools = 16 tools visible
- Routed tools discovered on-demand
- ~12-15K tokens per conversation
- **~80% reduction** in context usage
The router pattern is complete and functional, providing efficient tool discovery while maintaining full access to all 122+ tools.

View File

@@ -0,0 +1,557 @@
# Routing Tools Reference
Added in: v1.0.0, major expansion in v2.2.0-v2.2.3 (PR #44, @Kletternaut)
This document provides comprehensive documentation for the 13 routing tools available in the KiCAD MCP Server. These tools cover basic trace routing, advanced operations like differential pairs, net management, trace operations, and copper zone management.
## Basic Routing (3 tools)
### add_net
Create a new net on the PCB.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | -------------- |
| name | string | Yes | Net name |
| netClass | string | No | Net class name |
**Usage Notes:**
- Creates a new net that can be assigned to traces and pads
- If the net already exists, it will be reused
- Net class assignment is optional; defaults to "Default" if not specified
**Example:**
```json
{
"name": "VCC_3V3",
"netClass": "Power"
}
```
---
### route_trace
Route a trace segment between two XY points on a fixed layer.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------- |
| start | object | Yes | Start position with x, y, and optional unit |
| end | object | Yes | End position with x, y, and optional unit |
| layer | string | Yes | PCB layer |
| width | number | Yes | Trace width in mm |
| net | string | Yes | Net name |
**Usage Notes:**
- WARNING: Does NOT handle layer changes
- If start and end are on different copper layers, use `route_pad_to_pad` instead, which automatically inserts a via
- Coordinates use mm by default unless unit is specified
- This is a low-level tool; prefer `route_pad_to_pad` for component-to-component routing
**Example:**
```json
{
"start": { "x": 100.0, "y": 50.0, "unit": "mm" },
"end": { "x": 120.0, "y": 50.0, "unit": "mm" },
"layer": "F.Cu",
"width": 0.25,
"net": "GND"
}
```
---
### route_pad_to_pad
PREFERRED tool for pad-to-pad routing. Looks up pad positions automatically, detects the net from the pad, and automatically inserts a via if the two pads are on different copper layers.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | ---------------------------------------------------- |
| fromRef | string | Yes | Reference of the source component (e.g. 'U2') |
| fromPad | string/number | Yes | Pad number on the source component (e.g. '6' or 6) |
| toRef | string | Yes | Reference of the target component (e.g. 'U1') |
| toPad | string/number | Yes | Pad number on the target component (e.g. '15' or 15) |
| layer | string | No | PCB layer (default: F.Cu) |
| width | number | No | Trace width in mm (default: board default) |
| net | string | No | Net name override (default: auto-detected from pad) |
**Usage Notes:**
- This is the PREFERRED tool for routing between component pads
- Automatically looks up pad positions - no need to query them separately
- Auto-detects the net from the source pad
- Critically: if pads are on different copper layers (e.g., one on F.Cu and one on B.Cu), automatically inserts a via at an appropriate position to complete the connection
- Always use this instead of `route_trace` when routing between named component pads
- Via is placed at the start pad's X coordinate to avoid stacking issues with back-to-back mirrored connectors
**Example:**
```json
{
"fromRef": "U2",
"fromPad": "6",
"toRef": "U1",
"toPad": "15",
"width": 0.25
}
```
---
## Vias (1 tool)
### add_via
Add a via to the PCB.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ----------------------------------------- |
| position | object | Yes | Via position with x, y, and optional unit |
| net | string | Yes | Net name |
| viaType | string | No | Via type: "through", "blind", or "buried" |
**Usage Notes:**
- Through vias connect all layers (default)
- Blind vias connect an outer layer to one or more inner layers
- Buried vias connect two or more inner layers without reaching outer layers
- Position coordinates use mm by default
**Example:**
```json
{
"position": { "x": 110.0, "y": 50.0, "unit": "mm" },
"net": "GND",
"viaType": "through"
}
```
---
## Advanced Routing (2 tools)
### route_differential_pair
Route a differential pair between two sets of points.
**Parameters:**
| Parameter | Type | Required | Description |
| ----------- | ------ | -------- | ------------------------------------------ |
| positivePad | object | Yes | Positive pad with reference and pad number |
| negativePad | object | Yes | Negative pad with reference and pad number |
| layer | string | Yes | PCB layer |
| width | number | Yes | Trace width in mm |
| gap | number | Yes | Gap between traces in mm |
| positiveNet | string | Yes | Positive net name |
| negativeNet | string | Yes | Negative net name |
**Usage Notes:**
- Used for high-speed signals like USB, Ethernet, HDMI, etc.
- Maintains controlled impedance through consistent trace width and gap
- Both traces are routed in parallel with specified separation
- Pad object format: `{"reference": "U1", "pad": "1"}`
**Example:**
```json
{
"positivePad": { "reference": "J1", "pad": "2" },
"negativePad": { "reference": "J1", "pad": "3" },
"layer": "F.Cu",
"width": 0.2,
"gap": 0.2,
"positiveNet": "USB_DP",
"negativeNet": "USB_DN"
}
```
---
### copy_routing_pattern
Copy routing pattern (traces and vias) from a group of source components to a matching group of target components.
**Parameters:**
| Parameter | Type | Required | Description |
| ----------- | ------------- | -------- | ----------------------------------------------------------------------------------------- |
| sourceRefs | array[string] | Yes | References of the source components (e.g. ['U1', 'R1', 'C1']) |
| targetRefs | array[string] | Yes | References of the target components in same order as sourceRefs (e.g. ['U2', 'R2', 'C2']) |
| includeVias | boolean | No | Also copy vias (default: true) |
| traceWidth | number | No | Override trace width in mm (default: keep original width) |
**Usage Notes:**
- The offset is calculated automatically from the position difference between the first source and first target component
- Useful for replicating routing between identical circuit blocks
- Component arrays must be in matching order (sourceRefs[0] maps to targetRefs[0], etc.)
- Preserves relative routing topology from source to target
- Vias are copied by default unless includeVias is set to false
- Original trace widths are preserved unless traceWidth override is specified
**Example:**
```json
{
"sourceRefs": ["U1", "R1", "C1"],
"targetRefs": ["U2", "R2", "C2"],
"includeVias": true
}
```
---
## Net Management (2 tools)
### get_nets_list
Get a list of all nets in the PCB with optional statistics.
**Parameters:**
| Parameter | Type | Required | Description |
| ------------ | ------- | -------- | ---------------------------------------------------- |
| includeStats | boolean | No | Include statistics (track count, total length, etc.) |
| unit | string | No | Unit for length measurements: "mm" or "inch" |
**Usage Notes:**
- Returns all nets present in the board
- Statistics include track count, via count, and total trace length
- Useful for verifying net connectivity and routing completeness
- Length measurements default to mm
**Example:**
```json
{
"includeStats": true,
"unit": "mm"
}
```
---
### create_netclass
Create a new net class with custom design rules.
**Parameters:**
| Parameter | Type | Required | Description |
| ----------- | ------ | -------- | ------------------------- |
| name | string | Yes | Net class name |
| traceWidth | number | No | Default trace width in mm |
| clearance | number | No | Clearance in mm |
| viaDiameter | number | No | Via diameter in mm |
| viaDrill | number | No | Via drill size in mm |
**Usage Notes:**
- Net classes define design rules for groups of nets
- Common use cases: power nets (wider traces), high-speed signals (controlled impedance)
- Once created, assign nets to the class using the netClass parameter in `add_net`
- All measurements in mm
**Example:**
```json
{
"name": "Power",
"traceWidth": 0.5,
"clearance": 0.3,
"viaDiameter": 0.8,
"viaDrill": 0.4
}
```
---
## Trace Operations (3 tools)
### delete_trace
Delete traces from the PCB. Can delete by UUID, position, or bulk-delete all traces on a net.
**Parameters:**
| Parameter | Type | Required | Description |
| ----------- | ------- | -------- | ----------------------------------------------------------- |
| traceUuid | string | No | UUID of a specific trace to delete |
| position | object | No | Delete trace nearest to this position (x, y, optional unit) |
| net | string | No | Delete all traces on this net (bulk delete) |
| layer | string | No | Filter by layer when using net-based deletion |
| includeVias | boolean | No | Include vias in net-based deletion |
**Usage Notes:**
- Three deletion modes: by UUID (specific), by position (nearest), or by net (bulk)
- Position-based deletion finds the closest trace to the specified coordinates
- Net-based deletion can be filtered by layer
- Vias are excluded from net-based deletion by default unless includeVias is true
**Example (bulk delete):**
```json
{
"net": "GND",
"layer": "F.Cu",
"includeVias": false
}
```
---
### query_traces
Query traces on the board with optional filters by net, layer, or bounding box.
**Parameters:**
| Parameter | Type | Required | Description |
| ----------- | ------ | -------- | ------------------------------------------------------------- |
| net | string | No | Filter by net name |
| layer | string | No | Filter by layer name |
| boundingBox | object | No | Filter by bounding box region (x1, y1, x2, y2, optional unit) |
| unit | string | No | Unit for coordinates: "mm" or "inch" |
**Usage Notes:**
- Returns trace information including UUID, position, width, layer, and net
- Filters can be combined (e.g., specific net on specific layer)
- Bounding box uses rectangular region defined by opposite corners
- Useful for analyzing routing in specific board regions or on specific nets
**Example:**
```json
{
"net": "VCC_3V3",
"layer": "F.Cu"
}
```
---
### modify_trace
Modify an existing trace (change width, layer, or net).
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | --------------------------- |
| traceUuid | string | Yes | UUID of the trace to modify |
| width | number | No | New trace width in mm |
| layer | string | No | New layer name |
| net | string | No | New net name |
**Usage Notes:**
- Requires the trace UUID, which can be obtained from `query_traces`
- At least one modification parameter (width, layer, or net) must be provided
- Use with caution when changing nets - ensure electrical correctness
- Width changes are useful for adjusting impedance or current capacity
**Example:**
```json
{
"traceUuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"width": 0.5
}
```
---
## Copper Zones (2 tools)
### add_copper_pour
Add a copper pour (ground/power plane) to the PCB.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | ----------------------------------------------------------------------------------------- |
| layer | string | Yes | PCB layer |
| net | string | Yes | Net name |
| clearance | number | No | Clearance in mm |
| outline | array[object] | No | Array of {x, y} points defining the pour boundary. If omitted, the board outline is used. |
**Usage Notes:**
- Copper pours are typically used for ground and power planes
- If no outline is specified, the pour fills the entire board area
- Custom outlines are defined as arrays of coordinate points
- Clearance defines the minimum distance from other copper features
- After adding a pour, use `refill_zones` to fill it
**Example:**
```json
{
"layer": "B.Cu",
"net": "GND",
"clearance": 0.2,
"outline": [
{ "x": 10.0, "y": 10.0 },
{ "x": 90.0, "y": 10.0 },
{ "x": 90.0, "y": 60.0 },
{ "x": 10.0, "y": 60.0 }
]
}
```
---
### refill_zones
Refill all copper zones on the board.
**Parameters:**
None
**Usage Notes:**
- WARNING: SWIG path has known segfault risk (see KNOWN_ISSUES.md)
- Prefer using IPC backend (KiCAD open) or triggering zone fill via KiCAD UI instead
- Required after adding or modifying copper pours to calculate the filled areas
- Recalculates all zone fills based on current board state
- May take several seconds on complex boards with many zones
**Example:**
```json
{}
```
---
## Example Workflows
### Point-to-Point Routing with route_pad_to_pad
The simplest and most robust approach for connecting component pads:
```json
// Connect pin 1 of U1 to pin 5 of R1
{
"tool": "route_pad_to_pad",
"params": {
"fromRef": "U1",
"fromPad": "1",
"toRef": "R1",
"toPad": "5",
"width": 0.25
}
}
```
This automatically:
- Looks up the exact pad positions
- Detects the net from the pads
- Creates the trace on the appropriate layer
- Inserts a via if the pads are on different copper layers
### Differential Pair Routing (USB, Ethernet)
For high-speed differential signals like USB D+ and D-:
```json
// 1. Create nets if needed
{
"tool": "add_net",
"params": {"name": "USB_DP"}
}
{
"tool": "add_net",
"params": {"name": "USB_DN"}
}
// 2. Route the differential pair
{
"tool": "route_differential_pair",
"params": {
"positivePad": {"reference": "U1", "pad": "14"},
"negativePad": {"reference": "U1", "pad": "15"},
"layer": "F.Cu",
"width": 0.2,
"gap": 0.2,
"positiveNet": "USB_DP",
"negativeNet": "USB_DN"
}
}
```
### Replicating Routing Patterns
For repeated circuit blocks (e.g., multiple identical LED drivers):
```json
// Route the first instance (U1, R1, C1) manually, then copy to others
{
"tool": "copy_routing_pattern",
"params": {
"sourceRefs": ["U1", "R1", "C1"],
"targetRefs": ["U2", "R2", "C2"],
"includeVias": true
}
}
// Copy the same pattern to a third instance
{
"tool": "copy_routing_pattern",
"params": {
"sourceRefs": ["U1", "R1", "C1"],
"targetRefs": ["U3", "R3", "C3"],
"includeVias": true
}
}
```
### Adding a Ground Plane
```json
// 1. Create the copper pour on bottom layer
{
"tool": "add_copper_pour",
"params": {
"layer": "B.Cu",
"net": "GND",
"clearance": 0.2
}
}
// 2. Fill the zones
{
"tool": "refill_zones",
"params": {}
}
```
Note: Use the IPC backend (keep KiCAD open) when using refill_zones to avoid potential segfaults with the SWIG backend.
---
## Source Files
- **TypeScript Tool Definitions**: `/home/chris/MCP/KiCAD-MCP-Server/src/tools/routing.ts`
- **Python Implementation**: `/home/chris/MCP/KiCAD-MCP-Server/python/commands/routing.py`

View File

@@ -0,0 +1,440 @@
# Schematic Tools Reference
Added in: v2.1.0, expanded in v2.2.0-v2.2.3
Contributors: @Mehanik (PRs #60, #66), @Kletternaut (PR #57)
This document provides a complete reference for the 27 schematic tools in the KiCAD MCP Server. These tools enable a complete schematic design workflow, from creating projects and adding components to wiring, validation, and synchronization with PCB boards. The dynamic symbol loading feature provides access to approximately 10,000 standard KiCad symbols.
## Component Operations (8 tools)
### add_schematic_component
Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3').
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------------------------- |
| schematicPath | string | Yes | Path to the schematic file |
| symbol | string | Yes | Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3) |
| reference | string | Yes | Component reference (e.g., R1, U1) |
| value | string | No | Component value |
| footprint | string | No | KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric) |
| position | object | No | Position on schematic with x and y coordinates |
**Usage Notes:** The dynamic symbol loader provides access to ~10,000 KiCad standard symbols. If a symbol is not in the static template map, it will be loaded dynamically from the specified library.
### delete_schematic_component
Remove a placed symbol from a KiCAD schematic (.kicad_sch). This removes the symbol instance (the placed component) from the schematic. It does NOT remove the symbol definition from lib_symbols. Note: This tool operates on schematic files (.kicad_sch). To remove a footprint from a PCB, use delete_component instead.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| reference | string | Yes | Reference designator of the component to remove (e.g. R1, U3) |
### edit_schematic_component
Update properties of a placed symbol in a KiCAD schematic (.kicad_sch) in-place. Use this tool to assign or update a footprint, change the value, or rename the reference of an already-placed component. This is more efficient than delete + re-add because it preserves the component's position and UUID. Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_component.
| Parameter | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| reference | string | Yes | Current reference designator of the component (e.g. R1, U3) |
| footprint | string | No | New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric) |
| value | string | No | New value string (e.g. 10k, 100nF) |
| newReference | string | No | Rename the reference designator (e.g. R1 → R10) |
| fieldPositions | object | No | Reposition field labels: map of field name to {x, y, angle} (e.g. {"Reference": {"x": 12.5, "y": 17.0}}) |
### get_schematic_component
Get full component info from a schematic: position, field values, and each field's label position (at x/y/angle). Use this to inspect or prepare repositioning of Reference/Value labels.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| reference | string | Yes | Component reference designator (e.g. R1, U1) |
### list_schematic_components
List all components in a schematic with their references, values, positions, and pins. Essential for inspecting what's on the schematic before making edits.
| Parameter | Type | Required | Description |
| ---------------------- | ------ | -------- | --------------------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| filter | object | No | Optional filters with libId and/or referencePrefix fields |
| filter.libId | string | No | Filter by library ID (e.g., 'Device:R') |
| filter.referencePrefix | string | No | Filter by reference prefix (e.g., 'R', 'C', 'U') |
### move_schematic_component
Move a placed symbol to a new position in the schematic.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| reference | string | Yes | Reference designator (e.g., R1, U1) |
| position | object | Yes | New position with x and y coordinates |
### rotate_schematic_component
Rotate a placed symbol in the schematic.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| reference | string | Yes | Reference designator (e.g., R1, U1) |
| angle | number | Yes | Rotation angle in degrees (0, 90, 180, 270) |
| mirror | enum | No | Optional mirror axis ("x" or "y") |
### annotate_schematic
Assign reference designators to unannotated components (R? → R1, R2, ...). Must be called before tools that require known references.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
## Wiring and Connections (8 tools)
### add_wire
Add a wire connection in the schematic.
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | --------------------------------------- |
| start | object | Yes | Start position with x and y coordinates |
| end | object | Yes | End position with x and y coordinates |
### add_schematic_connection
Connect two component pins with a wire. Use this for individual connections between components with different pin roles (e.g. U1.SDA → J3.2). WARNING: Do NOT use this in a loop to wire N passthrough pins — use connect_passthrough instead (single call, cleaner layout, far fewer tokens).
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------- |
| schematicPath | string | Yes | Path to the schematic file |
| sourceRef | string | Yes | Source component reference (e.g., R1) |
| sourcePin | string | Yes | Source pin name/number (e.g., 1, 2, GND) |
| targetRef | string | Yes | Target component reference (e.g., C1) |
| targetPin | string | Yes | Target pin name/number (e.g., 1, 2, VCC) |
### add_schematic_net_label
Add a net label to the schematic.
**Preferred usage — snap to pin:** supply `componentRef` + `pinNumber` and the label is placed at the exact pin endpoint resolved by `PinLocator`. This guarantees an electrical connection. A 0.01 mm offset is enough to break the connection in KiCad, so this mode eliminates all guesswork.
**Alternative — explicit position:** supply `position [x, y]`. The coordinates must match a pin or wire endpoint exactly; use `get_schematic_pin_locations` first to obtain them.
| Parameter | Type | Required | Description |
| ------------- | -------------- | -------- | ---------------------------------------------------------------------- |
| schematicPath | string | Yes | Path to the schematic file |
| netName | string | Yes | Name of the net (e.g., VCC, GND, SIGNAL_1) |
| position | array [x, y] | No\* | Explicit position. Required when `componentRef`/`pinNumber` not given. |
| componentRef | string | No\* | Component reference to snap to (e.g. U1). Use with `pinNumber`. |
| pinNumber | string\|number | No\* | Pin number or name (e.g. `"1"`, `"GND"`). Use with `componentRef`. |
| labelType | string | No | `label` (default), `global_label`, or `hierarchical_label` |
| orientation | number | No | Rotation angle: 0, 90, 180, 270 (default: 0) |
\* Either `position` **or** (`componentRef` + `pinNumber`) is required.
**Response fields:**
| Field | Description |
| --------------- | ------------------------------------------------------------ |
| success | `true` / `false` |
| actual_position | `[x, y]` coordinates where the label was actually placed |
| snapped_to_pin | `{component, pin}` — present only when pin-snapping was used |
| message | Human-readable status |
### connect_to_net
Connect a component pin to a named net by adding a wire stub from the pin endpoint and placing a net label at the stub's far end. The exact pin coordinates are resolved internally via `PinLocator`.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------- |
| schematicPath | string | Yes | Path to the schematic file |
| componentRef | string | Yes | Component reference (e.g., U1, R1) |
| pinName | string | Yes | Pin name/number to connect |
| netName | string | Yes | Name of the net to connect to |
**Response fields:**
| Field | Description |
| -------------- | ------------------------------------------ |
| success | `true` / `false` |
| pin_location | `[x, y]` exact pin endpoint used |
| label_location | `[x, y]` where the net label was placed |
| wire_stub | `[[x1,y1],[x2,y2]]` the wire segment added |
| message | Human-readable status |
**Usage Notes:** Creates a wire stub from the pin and places a net label at the stub endpoint. The stub direction follows the pin's outward angle. Default stub length is 2.54 mm (0.1 inch, standard grid spacing). Check `pin_location` in the response to confirm the correct pin was found; no separate verification call is needed.
### connect_passthrough
Connects all pins of a source connector (e.g. J1) to matching pins of a target connector (e.g. J2) via shared net labels — pin N gets net '{netPrefix}\_{N}'. Use this for FFC/ribbon cable passthrough adapters instead of calling connect_to_net for every pin.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------------------------------------- |
| schematicPath | string | Yes | Path to the schematic file |
| sourceRef | string | Yes | Source connector reference (e.g. J1) |
| targetRef | string | Yes | Target connector reference (e.g. J2) |
| netPrefix | string | No | Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN) |
| pinOffset | number | No | Add to pin number when building net name (default: 0) |
**Usage Notes:** This is the most efficient way to wire passthrough adapters. For an N-pin connector, this replaces N individual connect_to_net calls with a single operation.
### get_schematic_pin_locations
Returns the exact x/y coordinates of every pin on a schematic component. Useful for inspection or when building custom placement logic. When the goal is to connect a pin to a net, prefer `add_schematic_net_label` with `componentRef`+`pinNumber` (which calls this internally) or `connect_to_net` — both snap to the exact pin endpoint automatically.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------ |
| schematicPath | string | Yes | Path to the schematic file |
| reference | string | Yes | Component reference designator (e.g. U1, R1, J2) |
### delete_schematic_wire
Remove a wire from the schematic by start and end coordinates.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| start | object | Yes | Wire start position with x and y coordinates |
| end | object | Yes | Wire end position with x and y coordinates |
### delete_schematic_net_label
Remove a net label from the schematic.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| netName | string | Yes | Name of the net label to remove |
| position | object | No | Position to disambiguate if multiple labels with same name (x and y coordinates) |
## Net Analysis (5 tools)
### get_net_connections
Get all connections for a named net.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------- |
| schematicPath | string | Yes | Path to the schematic file |
| netName | string | Yes | Name of the net to query |
**Usage Notes:** Uses wire graph analysis to find all component pins connected to the specified net. Returns a list of {component, pin} pairs.
### list_schematic_nets
List all nets in the schematic with their connections.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
### list_schematic_wires
List all wires in the schematic with start/end coordinates.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
### list_schematic_labels
List all net labels, global labels, and power flags in the schematic.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
### get_net_at_point
Return the net name at a given (x, y) coordinate, or `null` if no net label or wire endpoint is present there.
Checks net label / power symbol positions first (exact IU match), then wire endpoints. Faster than `get_wire_connections` when you only need the net name and not full pin traversal.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch schematic file |
| x | number | Yes | X coordinate in mm |
| y | number | Yes | Y coordinate in mm |
**Response fields:**
| Field | Description |
| -------- | ----------------------------------------------------------------------- |
| net_name | Net label string, or `null` if no net found at this point |
| position | `{"x": float, "y": float}` — echoes the query coordinates |
| source | `"net_label"` \| `"wire_endpoint"` \| `null` — how the net was resolved |
## Schematic Creation and Export (6 tools)
### create_schematic
Create a new schematic.
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | -------------- |
| name | string | Yes | Schematic name |
| path | string | No | Optional path |
### export_schematic_svg
Export schematic to SVG format using kicad-cli.
| Parameter | Type | Required | Description |
| ------------- | ------- | -------- | --------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| outputPath | string | Yes | Output SVG file path |
| blackAndWhite | boolean | No | Export in black and white |
### export_schematic_pdf
Export schematic to PDF format using kicad-cli.
| Parameter | Type | Required | Description |
| ------------- | ------- | -------- | --------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| outputPath | string | Yes | Output PDF file path |
| blackAndWhite | boolean | No | Export in black and white |
### get_schematic_view
Return a rasterized image of the schematic (PNG by default, or SVG). Uses kicad-cli to export SVG, then converts to PNG via cairosvg. Use this for visual feedback after placing or wiring components.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch file |
| format | enum | No | Output format ("png" or "svg", default: png) |
| width | number | No | Image width in pixels (default: 1200) |
| height | number | No | Image height in pixels (default: 900) |
### generate_netlist
Return a structured JSON netlist from the schematic for programmatic use. Uses `kicad-cli` internally — the schematic file must be saved to disk first.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------- |
| schematicPath | string | Yes | Absolute path to the .kicad_sch schematic file |
**Returns:** `{ components: [{reference, value, footprint}], nets: [{name, connections: [{component, pin}]}] }`
**Usage Notes:** Use this when you need net membership data in the conversation (e.g., to verify connectivity). For writing a netlist to a file or exporting SPICE/Cadstar/OrcadPCB2 format, use `export_netlist` instead.
### export_netlist
Export a netlist to a file in a standard EDA format using `kicad-cli`. Supports SPICE (for simulation), KiCad XML (for archiving/import), Cadstar, and OrcadPCB2.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------------ |
| schematicPath | string | Yes | Absolute path to the .kicad_sch schematic file |
| outputPath | string | Yes | Absolute path for the output file (e.g. `/tmp/design.spice`) |
| format | enum | No | `KiCad` (default), `Spice`, `Cadstar`, `OrcadPCB2` |
**Usage Notes:** The schematic file must be saved before calling this tool. Use `Spice` format to produce a SPICE netlist for simulation or diff against a reference. The output file is created or overwritten at `outputPath`.
## Validation and Synchronization (6 tools)
### list_floating_labels
Return all net labels that are not connected to any component pin.
A label is "floating" when no component pin's coordinate falls on the wire-network reachable from the label's anchor position. Floating labels indicate misplaced or off-grid labels that will cause ERC errors. Does not require the KiCAD UI to be running.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch schematic file |
**Response fields:** list of `{"name": str, "x": float, "y": float, "type": "label" | "global_label"}`.
### find_orphaned_wires
Find wire segments with at least one dangling endpoint — not connected to a component pin, net label, or another wire. Orphaned wires cause ERC "wire end unconnected" errors. Does not require the KiCAD UI to be running.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch schematic file |
**Response fields:**
| Field | Description |
| -------------- | ---------------------------------------------------------------------------- |
| orphaned_wires | List of `{"start": {x,y}, "end": {x,y}, "dangling_ends": [{x,y}, ...]}` (mm) |
| count | Total number of orphaned wire segments |
### snap_to_grid
Snap schematic element coordinates to the nearest grid point. KiCAD uses exact integer matching (10 000 IU/mm) internally, so even a sub-pixel offset makes wires appear connected visually while failing ERC. Run this before `run_erc` to eliminate that class of error. Modifies the `.kicad_sch` file in place. Does not require the KiCAD UI to be running.
| Parameter | Type | Required | Description |
| ------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch schematic file |
| gridSize | number | No | Grid spacing in mm (default: 2.54 — standard KiCAD schematic grid; use 1.27 for high-density) |
| elements | array\<string\> | No | Types to snap: `"wires"`, `"junctions"`, `"labels"`, `"components"`. Default: `["wires", "junctions", "labels"]`. `"components"` is opt-in — moving a component without re-routing its wires creates new mismatches. |
**Response fields:**
| Field | Description |
| --------------- | --------------------------------------------------------- |
| snapped | Number of elements that had at least one coordinate moved |
| already_on_grid | Number of elements already on the grid |
| grid_size | Grid spacing used (mm) |
### run_erc
Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------- |
| schematicPath | string | Yes | Path to the .kicad_sch schematic file |
**Usage Notes:** Returns violations categorized by severity (error, warning, info) with location coordinates. Essential for catching design errors before PCB layout.
### sync_schematic_to_board
Import the schematic netlist into the PCB board — equivalent to pressing F8 in KiCAD (Tools → Update PCB from Schematic). MUST be called after the schematic is complete and before placing or routing components on the PCB. Without this step, the board has no footprints and no net assignments — place_component and route_pad_to_pad will produce an empty, unroutable board.
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------- |
| schematicPath | string | Yes | Absolute path to the .kicad_sch schematic file |
| boardPath | string | Yes | Absolute path to the .kicad_pcb board file |
**Usage Notes:** This is the F8 equivalent. It synchronizes the schematic design to the PCB, creating footprints on the board and assigning nets. This step is critical in the workflow: design in schematic → sync_schematic_to_board → place and route on PCB.
## Example Workflows
### Basic Circuit Design
1. **Create project:** Use `create_schematic` to initialize a new schematic file
2. **Add components:** Use `add_schematic_component` to place resistors, capacitors, ICs, etc.
- Example: Add a resistor with `symbol: "Device:R"`, `reference: "R1"`, `value: "10k"`
3. **Wire components:** Use `add_schematic_connection` to connect component pins
- Or use `connect_to_net` to connect pins to named nets (VCC, GND, etc.)
4. **Add net labels:** Use `add_schematic_net_label` to label important signals
5. **Validate:** Run `run_erc` to check for electrical rule violations
6. **Review:** Use `list_schematic_components` and `get_schematic_view` to verify the design
7. **Sync to PCB:** Use `sync_schematic_to_board` to transfer the design to the PCB layout
### FFC Passthrough Adapter
1. **Add connectors:** Place two FFC connectors using `add_schematic_component`
- Example: J1 and J2, both 20-pin FFC connectors
2. **Connect passthrough:** Use `connect_passthrough` with `sourceRef: "J1"`, `targetRef: "J2"`, `netPrefix: "CSI"`
- This single call connects all 20 pins (J1.1 ↔ J2.1 via CSI_1, J1.2 ↔ J2.2 via CSI_2, etc.)
3. **Sync to board:** Use `sync_schematic_to_board` to create the PCB layout
4. **Verify:** Use `list_schematic_nets` to confirm all connections are correct
## Source Files
The schematic tools are implemented across the following source files:
- **TypeScript (Tool Definitions):**
- `/home/chris/MCP/KiCAD-MCP-Server/src/tools/schematic.ts` - All 27 schematic tool definitions with parameter schemas and handlers
- **Python (Backend Implementation):**
- `/home/chris/MCP/KiCAD-MCP-Server/python/commands/component_schematic.py` - ComponentManager class (add, delete, edit, list components with dynamic symbol loading)
- `/home/chris/MCP/KiCAD-MCP-Server/python/commands/connection_schematic.py` - ConnectionManager class (wiring, net labels, passthrough, netlist generation)
- `/home/chris/MCP/KiCAD-MCP-Server/python/commands/wire_manager.py` - WireManager class (low-level wire manipulation)
- `/home/chris/MCP/KiCAD-MCP-Server/python/commands/pin_locator.py` - PinLocator class (pin location lookup and angle calculation)
- `/home/chris/MCP/KiCAD-MCP-Server/python/commands/dynamic_symbol_loader.py` - DynamicSymbolLoader class (runtime symbol loading from KiCad libraries)

View File

@@ -1,287 +1,175 @@
# KiCAD MCP - Current Status Summary
**Date:** 2025-12-02
**Version:** 2.1.0-alpha
**Phase:** IPC Backend Implementation and Testing
---
## Quick Stats
| Metric | Value | Status |
|--------|-------|--------|
| Core Features Working | 18/20 | 90% |
| KiCAD 9.0 Compatible | Yes | Verified |
| UI Auto-launch | Working | Verified |
| Component Placement | Working | Verified |
| Component Libraries | 153 libraries | Verified |
| Routing Operations | Working | Verified |
| IPC Backend | Under Testing | Experimental |
| Tests Passing | 18/20 | 90% |
---
## What's Working (Verified 2025-12-02)
### Project Management
- `create_project` - Create new KiCAD projects
- `open_project` - Load existing PCB files
- `save_project` - Save changes to disk
- `get_project_info` - Retrieve project metadata
### Board Design
- `set_board_size` - Set dimensions (KiCAD 9.0 fixed)
- `add_board_outline` - Rectangle, circle, polygon outlines
- `add_mounting_hole` - Mounting holes with pads
- `add_board_text` - Text annotations (KiCAD 9.0 fixed)
- `add_layer` - Custom layer creation
- `set_active_layer` - Layer switching
- `get_layer_list` - List all layers
### Component Operations
- `place_component` - Place components with library footprints (KiCAD 9.0 fixed)
- `move_component` - Move components
- `rotate_component` - Rotate components (EDA_ANGLE fixed)
- `delete_component` - Remove components
- `list_components` - Get all components on board
**Footprint Library Integration:**
- Auto-discovered 153 KiCAD footprint libraries
- Search footprints by pattern (`search_footprints`)
- List library contents (`list_library_footprints`)
- Get footprint info (`get_footprint_info`)
- Support for both `Library:Footprint` and `Footprint` formats
**KiCAD 9.0 API Fixes:**
- `SetOrientation()` uses `EDA_ANGLE(degrees, DEGREES_T)`
- `GetOrientation()` returns `EDA_ANGLE`, call `.AsDegrees()`
- `GetFootprintName()` now `GetFPIDAsString()`
### Routing Operations
- `add_net` - Create electrical nets
- `route_trace` - Add copper traces (KiCAD 9.0 fixed)
- `add_via` - Add vias between layers (KiCAD 9.0 fixed)
- `add_copper_pour` - Add copper zones/pours (KiCAD 9.0 fixed)
- `route_differential_pair` - Differential pair routing
**KiCAD 9.0 API Fixes:**
- `netinfo.FindNet()` now `netinfo.NetsByName()[name]`
- `zone.SetPriority()` now `zone.SetAssignedPriority()`
- `ZONE_FILL_MODE_POLYGON` now `ZONE_FILL_MODE_POLYGONS`
- Zone outline requires `outline.NewOutline()` first
### UI Management
- `check_kicad_ui` - Detect running KiCAD
- `launch_kicad_ui` - Auto-launch with project
### Export
- `export_gerber` - Manufacturing files
- `export_pdf` - Documentation
- `export_svg` - Vector graphics
- `export_3d` - STEP/VRML models
- `export_bom` - Bill of materials
### Design Rules
- `set_design_rules` - DRC configuration
- `get_design_rules` - Rule inspection
- `run_drc` - Design rule check
---
## IPC Backend (Under Development)
We are currently implementing and testing the KiCAD 9.0 IPC API for real-time UI synchronization. This is experimental and may not work perfectly in all scenarios.
### IPC-Capable Commands (21 total)
The following commands have IPC handlers implemented:
| Command | IPC Handler | Notes |
|---------|-------------|-------|
| `route_trace` | `_ipc_route_trace` | Implemented |
| `add_via` | `_ipc_add_via` | Implemented |
| `add_net` | `_ipc_add_net` | Implemented |
| `delete_trace` | `_ipc_delete_trace` | Falls back to SWIG |
| `get_nets_list` | `_ipc_get_nets_list` | Implemented |
| `add_copper_pour` | `_ipc_add_copper_pour` | Implemented |
| `refill_zones` | `_ipc_refill_zones` | Implemented |
| `add_text` | `_ipc_add_text` | Implemented |
| `add_board_text` | `_ipc_add_text` | Implemented |
| `set_board_size` | `_ipc_set_board_size` | Implemented |
| `get_board_info` | `_ipc_get_board_info` | Implemented |
| `add_board_outline` | `_ipc_add_board_outline` | Implemented |
| `add_mounting_hole` | `_ipc_add_mounting_hole` | Implemented |
| `get_layer_list` | `_ipc_get_layer_list` | Implemented |
| `place_component` | `_ipc_place_component` | Hybrid (SWIG+IPC) |
| `move_component` | `_ipc_move_component` | Implemented |
| `rotate_component` | `_ipc_rotate_component` | Implemented |
| `delete_component` | `_ipc_delete_component` | Implemented |
| `get_component_list` | `_ipc_get_component_list` | Implemented |
| `get_component_properties` | `_ipc_get_component_properties` | Implemented |
| `save_project` | `_ipc_save_project` | Implemented |
### How IPC Works
When KiCAD is running with IPC enabled:
1. Commands check if IPC is connected
2. If connected, use IPC handler for real-time UI updates
3. If not connected, fall back to SWIG API
**To enable IPC:**
1. KiCAD 9.0+ must be running
2. Enable IPC API: `Preferences > Plugins > Enable IPC API Server`
3. Have a board open in the PCB editor
### Known Limitations
- KiCAD must be running for IPC to work
- Some commands may not work as expected (still testing)
- Footprint loading uses hybrid approach (SWIG for library, IPC for placement)
- Delete trace falls back to SWIG (IPC API limitation)
---
## What Needs Work
### Minor Issues (NON-BLOCKING)
**1. get_board_info layer constants**
- Error: `AttributeError: 'BOARD' object has no attribute 'LT_USER'`
- Impact: Low (informational command only)
- Workaround: Use `get_project_info` or read components directly
**2. Zone filling via SWIG**
- Copper pours created but not filled automatically via SWIG
- Cause: SWIG API segfault when calling `ZONE_FILLER`
- Workaround: Use IPC backend or zones are filled when opened in KiCAD UI
**3. UI manual reload (SWIG mode)**
- User must manually reload to see MCP changes when using SWIG
- Impact: Workflow friction
- Workaround: Use IPC backend for automatic updates
---
## Architecture Status
### SWIG Backend (File-based)
- **Status:** Stable and functional
- **Pros:** No KiCAD process required, works offline, reliable
- **Cons:** Requires manual file reload for UI updates, no zone filling
- **Use Case:** Offline work, automated pipelines, batch operations
### IPC Backend (Real-time)
- **Status:** Under active development and testing
- **Pros:** Real-time UI updates, no file I/O for many operations, zone filling works
- **Cons:** Requires KiCAD running, experimental
- **Use Case:** Interactive design sessions, paired programming with AI
### Hybrid Approach
The server automatically selects the best backend:
- IPC when KiCAD is running with IPC enabled
- SWIG fallback when IPC is unavailable
---
## Feature Completion Matrix
| Feature Category | Status | Details |
|-----------------|--------|---------|
| Project Management | 100% | Create, open, save, info |
| Board Setup | 100% | Size, outline, mounting holes |
| Component Placement | 100% | Place, move, rotate, delete + 153 libraries |
| Routing | 90% | Traces, vias, copper (zone filling via IPC) |
| Design Rules | 100% | Set, get, run DRC |
| Export | 100% | Gerber, PDF, SVG, 3D, BOM |
| UI Integration | 85% | Launch, check, IPC auto-updates |
| IPC Backend | 60% | Under testing, 21 commands implemented |
| JLCPCB Integration | 0% | Planned |
---
## Developer Setup Status
### Linux - Primary Platform
- KiCAD 9.0 detection: Working
- Process management: Working
- venv support: Working
- Library discovery: Working (153 libraries)
- Testing: Working
- IPC backend: Under testing
### Windows - Supported
- Automated setup script (`setup-windows.ps1`)
- Process detection implemented
- Library paths auto-detected
- Comprehensive error diagnostics
- Startup validation with helpful errors
- Troubleshooting guide (WINDOWS_TROUBLESHOOTING.md)
### macOS - Untested
- Configuration provided
- Process detection implemented
- Library paths configured
- Needs community testing
---
## Documentation Status
### Complete
- [x] README.md
- [x] ROADMAP.md
- [x] IPC_BACKEND_STATUS.md
- [x] IPC_API_MIGRATION_PLAN.md
- [x] REALTIME_WORKFLOW.md
- [x] LIBRARY_INTEGRATION.md
- [x] KNOWN_ISSUES.md
- [x] UI_AUTO_LAUNCH.md
- [x] VISUAL_FEEDBACK.md
- [x] CLIENT_CONFIGURATION.md
- [x] BUILD_AND_TEST_SESSION.md
- [x] STATUS_SUMMARY.md (this document)
- [x] WINDOWS_SETUP.md
- [x] WINDOWS_TROUBLESHOOTING.md
### Needed
- [ ] EXAMPLE_PROJECTS.md
- [ ] CONTRIBUTING.md
- [ ] API_REFERENCE.md
---
## What's Next?
### Immediate Priorities
1. **Complete IPC Testing** - Verify all 21 IPC handlers work correctly
2. **Fix Edge Cases** - Address any issues found during testing
3. **Improve Error Handling** - Better fallback behavior
### Planned Features
- JLCPCB parts integration
- Digikey API integration
- Advanced routing algorithms
- Smart BOM management
- Design pattern library (Arduino shields, RPi HATs)
---
## Getting Help
**For Users:**
1. Check [README.md](../README.md) for installation
2. Review [KNOWN_ISSUES.md](KNOWN_ISSUES.md) for common problems
3. Check logs: `~/.kicad-mcp/logs/kicad_interface.log`
**For Developers:**
1. Read [BUILD_AND_TEST_SESSION.md](BUILD_AND_TEST_SESSION.md)
2. Check [ROADMAP.md](ROADMAP.md) for priorities
3. Review [IPC_BACKEND_STATUS.md](IPC_BACKEND_STATUS.md) for IPC details
**Issues:**
- Open an issue on GitHub with OS, KiCAD version, and error details
---
*Last Updated: 2025-12-02*
*Maintained by: KiCAD MCP Team*
# KiCAD MCP - Current Status Summary
**Date:** 2026-03-21
**Version:** 2.2.3 (package.json shows 2.1.0-alpha -- CHANGELOG is authoritative)
**Phase:** Active development with community contributions
---
## Quick Stats
| Metric | Value |
| -------------------- | --------------------------- |
| Total MCP Tools | 122 |
| Tool Categories | 16 |
| KiCAD 9.0 Compatible | Yes (verified) |
| Platforms | Linux, Windows, macOS |
| JLCPCB Parts Catalog | 2.5M+ components |
| Symbol Access | ~10,000 via dynamic loading |
| Footprint Libraries | 153+ auto-discovered |
| Contributors | 10+ |
| MCP Protocol Version | 2025-06-18 |
---
## Feature Completion Matrix
| Feature Category | Status | Tool Count | Details |
| ------------------- | -------- | ---------- | ----------------------------------------------------------------------- |
| Project Management | Complete | 5 | Create, open, save, info, snapshot |
| Board Setup | Complete | 12 | Size, outline, layers, mounting holes, zones, text, 2D view, SVG import |
| Component Placement | Complete | 16 | Place, move, rotate, delete, edit, find, pads, arrays, align, duplicate |
| Routing | Complete | 13 | Traces, vias, pad-to-pad, differential pairs, netclasses, copy pattern |
| Design Rules / DRC | Complete | 8 | Set/get rules, DRC, net classes, clearance checks |
| Export | Complete | 8 | Gerber, PDF, SVG, 3D, BOM, netlist, position file, VRML |
| Schematic | Complete | 27 | Components, wiring, net labels, connections, ERC, export, sync to board |
| Footprint Libraries | Complete | 4 | List, search, browse, info |
| Symbol Libraries | Complete | 4 | List, search, browse, info |
| Footprint Creator | Complete | 4 | Create custom footprints, edit pads, register libraries |
| Symbol Creator | Complete | 4 | Create custom symbols, register libraries |
| Datasheet Tools | Complete | 2 | LCSC datasheet enrichment |
| JLCPCB Integration | Complete | 5 | Local DB, search, part details, stats, alternatives |
| Freerouting | Complete | 4 | Autoroute, DSN export, SES import, availability check |
| UI Management | Complete | 2 | Check/launch KiCAD |
| Router Tools | Complete | 4 | Category browsing, tool search, execute |
---
## Architecture
### SWIG Backend (File-based) -- Default
- **Status:** Stable
- Direct pcbnew API access via KiCAD's Python bindings
- Requires manual KiCAD UI reload to see changes
- Works without KiCAD running
- Auto-saves after every board-modifying command
### IPC Backend (Real-time) -- Experimental
- **Status:** Functional, 21 commands implemented
- Real-time UI synchronization with KiCAD 9+
- Requires KiCAD running with IPC API enabled
- Automatic fallback to SWIG when unavailable
### Hybrid Approach
The server automatically selects the best backend:
- IPC when KiCAD is running with IPC enabled
- SWIG fallback when IPC is unavailable
- Some operations use both (e.g., footprint placement)
---
## Platform Support
### Linux -- Primary Platform
- KiCAD 9.0 detection: Working
- Process management: Working
- Library discovery: Working (153+ libraries)
- IPC backend: Working
### Windows -- Fully Supported
- Automated setup script (setup-windows.ps1)
- Process detection via Toolhelp32 API
- Library paths auto-detected
- Troubleshooting guide available (WINDOWS_TROUBLESHOOTING.md)
### macOS -- Community Supported
- Automated setup script (setup-macos.sh)
- Auto-detects KiCad Python and pcbnew
- Generates Claude Desktop configuration
- Process detection implemented
- Library paths auto-configured
- Needs community testing
---
## Recent Development Highlights
### v2.2.3 (2026-03-11)
- FFC/ribbon cable passthrough workflow (connect_passthrough, sync_schematic_to_board)
- Project snapshot system
- SVG logo import
- ERC validation
- Developer mode (KICAD_MCP_DEV=1)
- Critical B.Cu routing fixes
### v2.2.2-alpha (2026-03-01)
- route_pad_to_pad with auto-via insertion
- copy_routing_pattern for trace replication
- Project-local library resolution
### v2.2.1-alpha (2026-02-28)
- edit_schematic_component with field position support
- Footprint and symbol creator tools
### v2.2.0-alpha (2026-02-27)
- 13 new routing/component tools
- Datasheet enrichment tools
- SWIG/UUID bug fixes
### v2.1.0-alpha (2026-01-10)
- Complete schematic wiring system
- Dynamic symbol loading (~10,000 symbols)
- JLCPCB parts integration
- Router pattern (70% context reduction)
---
## Community Contributors
| Contributor | Key Contributions |
| ------------- | ------------------------------------------------------------------------------ |
| Kletternaut | Routing tools, footprint/symbol creators, passthrough workflow, template fixes |
| Mehanik | Schematic inspection/editing tools, component field positions |
| jflaflamme | Freerouting autorouter integration with Docker/Podman |
| l3wi | Local symbol library search, JLCPCB third-party library support |
| gwall-ceres | MCP protocol compliance, Windows compatibility |
| fariouche | Bug fixes |
| shuofengzhang | XDG relative path handling |
| sid115 | Windows setup script improvements |
| pasrom | MCP server bug fixes |
---
## Getting Help
**For Users:**
1. Check [README.md](../README.md) for installation
2. Review [KNOWN_ISSUES.md](KNOWN_ISSUES.md) for common problems
3. Check logs: `~/.kicad-mcp/logs/kicad_interface.log`
**For Contributors:**
1. Read [CONTRIBUTING.md](../CONTRIBUTING.md) for development setup
2. Check [ARCHITECTURE.md](ARCHITECTURE.md) for system design
3. Review the [Documentation Index](INDEX.md) for all available docs
**Issues:**
- Open an issue on GitHub with OS, KiCAD version, and error details
---
_Last Updated: 2026-04-11_

119
docs/SVG_IMPORT_GUIDE.md Normal file
View File

@@ -0,0 +1,119 @@
# SVG Logo Import Guide
**Added in:** v2.2.3
The `import_svg_logo` tool converts SVG vector graphics into filled polygons on a KiCAD PCB layer. This is useful for placing company logos, project branding, or custom artwork on your board's silkscreen or copper layers.
---
## Tool Reference
### `import_svg_logo`
Imports an SVG file as filled graphic polygons onto a PCB layer. Curves are linearized automatically.
**Parameters:**
| Parameter | Type | Required | Default | Description |
| ------------- | ------- | -------- | ------- | ----------------------------------------------------------------------- |
| `pcbPath` | string | Yes | -- | Path to the .kicad_pcb file |
| `svgPath` | string | Yes | -- | Path to the SVG logo file |
| `x` | number | Yes | -- | X position of the logo top-left corner in mm |
| `y` | number | Yes | -- | Y position of the logo top-left corner in mm |
| `width` | number | Yes | -- | Target width of the logo in mm (height scales to preserve aspect ratio) |
| `layer` | string | No | F.SilkS | PCB layer name (e.g., F.SilkS, B.SilkS, F.Cu, B.Cu) |
| `strokeWidth` | number | No | 0 | Outline stroke width in mm (0 = no outline) |
| `filled` | boolean | No | true | Fill polygons with solid color |
**Returns:**
- Polygon count
- Final dimensions (width x height in mm)
- Layer used
---
## SVG Requirements
### Supported Features
- Path elements with M, L, H, V, C, S, Q, T, A, Z commands
- Filled shapes (polygons, rectangles, circles, ellipses)
- Nested groups and transforms
- Cubic and quadratic Bezier curves (linearized automatically)
### Recommendations
- Use simple, solid shapes -- avoid complex gradients or filters
- Convert text to paths/outlines before importing
- Ensure shapes are filled (not just stroked) for best results
- Keep the SVG clean -- remove unnecessary metadata and layers
### What Will Not Work
- Raster images embedded in SVG
- CSS-based styling (inline style attributes are preferred)
- Complex SVG filters or effects
- Transparency (PCB layers are binary -- copper or no copper)
---
## Workflow
### 1. Prepare Your SVG
If starting from a raster image (PNG, JPG):
- Use a vector graphics editor (Inkscape, Illustrator, Figma) to trace the image
- In Inkscape: Path > Trace Bitmap to convert
- Export as plain SVG
If starting from a vector logo:
- Open in a vector editor
- Convert all text to paths (Object to Path / Create Outlines)
- Remove unnecessary layers and hidden elements
- Save as plain SVG
### 2. Import the Logo
```
Import my company logo from ~/logos/logo.svg onto the board at position x=25 y=40 with width 15mm on the front silkscreen.
```
### 3. Verify Placement
Use `get_board_2d_view` to preview the board with the logo, or open in KiCAD to check placement.
### 4. Adjust if Needed
Re-run `import_svg_logo` with different position, width, or layer parameters.
---
## Layer Options
| Layer | Use Case |
| --------- | ----------------------------------------------------- |
| `F.SilkS` | Front silkscreen (most common for logos) |
| `B.SilkS` | Back silkscreen |
| `F.Cu` | Front copper (logo as exposed copper) |
| `B.Cu` | Back copper |
| `F.Mask` | Front solder mask opening (exposes copper underneath) |
| `B.Mask` | Back solder mask opening |
---
## Manufacturing Considerations
- **Silkscreen logos** are the safest choice -- no impact on electrical design
- **Copper logos** will be part of the copper layer and may affect DRC. Ensure adequate clearance from traces and pads
- **Minimum feature size** depends on your PCB fabricator. Most support 0.15mm (6mil) minimum line width for silkscreen
- **Logo size** should account for manufacturing tolerances -- very small details may not reproduce well
---
## Source Files
- TypeScript tool definition: `src/tools/board.ts` (import_svg_logo)
- Python implementation: `python/commands/svg_import.py`

View File

@@ -1,115 +1,343 @@
# KiCAD MCP Server - Tool Inventory
**Total Tools: 59**
**Token Impact: ~40K+ tokens before any user interaction**
## Current Tool Categories
### Project Management (4 tools)
- `create_project` - Create a new KiCAD project
- `open_project` - Open an existing KiCAD project
- `save_project` - Save the current KiCAD project
- `get_project_info` - Get information about the current project
### Board Management (12 tools)
- `set_board_size` - Set the board dimensions
- `add_layer` - Add a new layer to the board
- `set_active_layer` - Set the active working layer
- `get_board_info` - Get board information
- `get_layer_list` - Get list of all layers
- `add_board_outline` - Add board outline shape (rectangle/circle/polygon)
- `add_mounting_hole` - Add mounting hole to the board
- `add_board_text` - Add text to the board
- `add_zone` - Add copper zone/pour
- `get_board_extents` - Get board bounding box
- `get_board_2d_view` - Get 2D visualization of board
### Component Management (10 tools)
- `place_component` - Place a component on the board
- `move_component` - Move a component to new position
- `rotate_component` - Rotate a component
- `delete_component` - Delete a component
- `edit_component` - Edit component properties
- `find_component` - Find component by reference or value
- `get_component_properties` - Get component properties
- `add_component_annotation` - Add annotation to component
- `group_components` - Group multiple components
- `replace_component` - Replace component with another
### Routing (4 tools)
- `add_net` - Create a new net
- `route_trace` - Route a trace between two points
- `add_via` - Add a via
- `add_copper_pour` - Add copper pour (ground/power plane)
### Design Rules & DRC (9 tools)
- `set_design_rules` - Configure design rules
- `get_design_rules` - Get current design rules
- `run_drc` - Run design rule check
- `add_net_class` - Add a net class with specific rules
- `assign_net_to_class` - Assign net to a net class
- `set_layer_constraints` - Set layer-specific constraints
- `check_clearance` - Check clearance between items
- `get_drc_violations` - Get DRC violation list
### Export (8 tools)
- `export_gerber` - Export Gerber files for fabrication
- `export_pdf` - Export PDF documentation
- `export_svg` - Export SVG graphics
- `export_3d` - Export 3D model (STEP/STL/VRML/OBJ)
- `export_bom` - Export bill of materials
- `export_netlist` - Export netlist
- `export_position_file` - Export component position file
- `export_vrml` - Export VRML 3D model
### Library (4 tools)
- `list_libraries` - List available footprint libraries
- `search_footprints` - Search for footprints across libraries
- `list_library_footprints` - List footprints in specific library
- `get_footprint_info` - Get detailed footprint information
### Schematic (9 tools)
- `create_schematic` - Create a new schematic
- `add_schematic_component` - Add component to schematic
- `add_wire` - Add wire connection in schematic
- `add_schematic_connection` - Connect component pins
- `add_schematic_net_label` - Add net label
- `connect_to_net` - Connect pin to named net
- `get_net_connections` - Get all connections for a net
- `generate_netlist` - Generate netlist from schematic
### UI Management (2 tools)
- `check_kicad_ui` - Check if KiCAD UI is running
- `launch_kicad_ui` - Launch KiCAD UI
## Router Implementation Plan
### Direct Tools (Always Visible) - 12 tools
High-frequency operations used in 80%+ of sessions:
- `create_project`
- `open_project`
- `save_project`
- `get_project_info`
- `place_component`
- `move_component`
- `add_net`
- `route_trace`
- `get_board_info`
- `set_board_size`
- `add_board_outline`
- `check_kicad_ui`
### Router Tools - 4 tools
Discovery and execution:
- `list_tool_categories`
- `get_category_tools`
- `execute_tool`
- `search_tools`
### Routed Tools (Hidden) - 47 tools
Organized into categories for discovery.
## Expected Impact
**Before Router**: 59 tools = ~40K+ tokens
**After Router**: 16 tools (12 direct + 4 router) = ~12K tokens
**Savings**: ~28K tokens (70% reduction)
# KiCAD MCP Server - Complete Tool Inventory
**Version:** 2.2.3
**Total Tools:** 122 (18 direct + 65 routed + 4 router + 35 additional)
**Last Updated:** 2026-03-21
## How Tools Are Organized
The server uses a **router pattern** to reduce AI context usage. Tools fall into three groups:
- **Direct tools** - Always visible to the AI. High-frequency operations used in most sessions.
- **Routed tools** - Organized into categories. Discovered via the router tools (`list_tool_categories`, `get_category_tools`, `search_tools`) and invoked via `execute_tool`.
- **Additional tools** - Registered directly (always visible) but not part of the router categories.
---
## Project Management (5 tools)
_Source: `src/tools/project.ts`_
| Tool | Description | Access |
| ------------------ | ---------------------------------------------------------------- | ------ |
| `create_project` | Create a new KiCAD project (.kicad_pro, .kicad_pcb, .kicad_sch) | Direct |
| `open_project` | Open an existing KiCAD project | Direct |
| `save_project` | Save the current project | Direct |
| `get_project_info` | Get project metadata and information | Direct |
| `snapshot_project` | Save a named checkpoint snapshot (renders PDF, saves step label) | Direct |
---
## Board Management (12 tools)
_Source: `src/tools/board.ts`_
| Tool | Description | Access |
| ------------------- | ----------------------------------------------------------------- | -------------- |
| `set_board_size` | Set PCB dimensions (width, height, unit) | Direct |
| `add_board_outline` | Add board outline (rectangle, circle, polygon, rounded_rectangle) | Direct |
| `get_board_info` | Get board metadata and properties | Direct |
| `add_layer` | Add copper/technical/signal layer | Routed (board) |
| `set_active_layer` | Change the active working layer | Routed (board) |
| `get_layer_list` | List all layers on the board | Routed (board) |
| `add_mounting_hole` | Add mounting hole with optional pad | Routed (board) |
| `add_board_text` | Add text annotation to board | Routed (board) |
| `add_zone` | Add copper zone/pour with clearance settings | Routed (board) |
| `get_board_extents` | Get bounding box of board | Routed (board) |
| `get_board_2d_view` | Render 2D board view (PNG/JPG/SVG) | Routed (board) |
| `import_svg_logo` | Import SVG file as polygons on silkscreen layer | Additional |
---
## Component Management (16 tools)
_Source: `src/tools/component.ts`_
| Tool | Description | Access |
| -------------------------- | ------------------------------------------------------------- | ------------------ |
| `place_component` | Place footprint on PCB (position, rotation, reference, value) | Direct |
| `move_component` | Move component to new position | Direct |
| `rotate_component` | Rotate component (absolute angle) | Routed (component) |
| `delete_component` | Remove component from board | Routed (component) |
| `edit_component` | Edit component properties (reference, value, footprint) | Routed (component) |
| `find_component` | Search components by reference or value | Routed (component) |
| `get_component_properties` | Get all properties of a component | Routed (component) |
| `add_component_annotation` | Add annotation/comment to component | Routed (component) |
| `group_components` | Group multiple components together | Routed (component) |
| `replace_component` | Replace component with different footprint | Routed (component) |
| `get_component_pads` | Get all pad information for a component | Additional |
| `get_component_list` | List all components with optional filters | Additional |
| `get_pad_position` | Get precise position of a specific pad | Additional |
| `place_component_array` | Place array of components (rows x columns) | Additional |
| `align_components` | Align components (horizontal, vertical, grid) | Additional |
| `duplicate_component` | Duplicate component with offset | Additional |
---
## Routing (13 tools)
_Source: `src/tools/routing.ts`_
| Tool | Description | Access |
| ------------------------- | ---------------------------------------------------- | ---------------- |
| `add_net` | Create a new net on the PCB | Direct |
| `route_trace` | Route trace segment between XY points (single layer) | Direct |
| `add_via` | Add via (through/blind/buried) | Routed (routing) |
| `add_copper_pour` | Add copper pour / ground plane | Routed (routing) |
| `delete_trace` | Delete traces by UUID, position, or bulk by net | Additional |
| `query_traces` | Query/filter traces by net, layer, or bounding box | Additional |
| `get_nets_list` | List all nets with statistics | Additional |
| `modify_trace` | Modify existing trace (width, layer, net) | Additional |
| `create_netclass` | Create net class with design rules | Additional |
| `route_differential_pair` | Route differential pair traces | Additional |
| `refill_zones` | Refill all copper zones | Additional |
| `route_pad_to_pad` | Route trace between two pads with auto-via insertion | Additional |
| `copy_routing_pattern` | Copy routing from source to target component groups | Additional |
---
## Design Rules and DRC (8 tools)
_Source: `src/tools/design-rules.ts`_
| Tool | Description | Access |
| ----------------------- | ----------------------------------------------------------- | ------------ |
| `set_design_rules` | Set global design rules (clearance, track width, via sizes) | Routed (drc) |
| `get_design_rules` | Get current design rules | Routed (drc) |
| `run_drc` | Run design rule check | Routed (drc) |
| `add_net_class` | Add net class with custom rules | Routed (drc) |
| `assign_net_to_class` | Assign net to a net class | Routed (drc) |
| `set_layer_constraints` | Set layer-specific constraints | Routed (drc) |
| `check_clearance` | Check clearance between two items | Routed (drc) |
| `get_drc_violations` | Get DRC violation list (filter by severity) | Routed (drc) |
---
## Export (8 tools)
_Source: `src/tools/export.ts`_
| Tool | Description | Access |
| ---------------------- | ------------------------------------------------- | --------------- |
| `export_gerber` | Export Gerber files for fabrication | Routed (export) |
| `export_pdf` | Export PDF with layer selection and page size | Routed (export) |
| `export_svg` | Export SVG vector graphics | Routed (export) |
| `export_3d` | Export 3D model (STEP, STL, VRML, OBJ) | Routed (export) |
| `export_bom` | Export Bill of Materials (CSV, XML, HTML, JSON) | Routed (export) |
| `export_netlist` | Export netlist (KiCad, Spice, Cadstar, OrcadPCB2) | Routed (export) |
| `export_position_file` | Export component position file for pick and place | Routed (export) |
| `export_vrml` | Export VRML 3D model | Routed (export) |
---
## Schematic (27 tools)
_Source: `src/tools/schematic.ts`_
### Component Operations
| Tool | Description | Access |
| ---------------------------- | ------------------------------------------------------- | ------------------ |
| `add_schematic_component` | Add component to schematic (symbol from library) | Direct |
| `delete_schematic_component` | Remove component from schematic | Additional |
| `edit_schematic_component` | Edit component properties (footprint, value, reference) | Additional |
| `get_schematic_component` | Get component info with field positions | Additional |
| `list_schematic_components` | List all components in schematic | Direct |
| `move_schematic_component` | Move component to new position | Routed (schematic) |
| `rotate_schematic_component` | Rotate component | Routed (schematic) |
| `annotate_schematic` | Auto-annotate reference designators | Direct |
### Wiring and Connections
| Tool | Description | Access |
| ----------------------------- | ------------------------------------------------ | ------------------ |
| `add_wire` | Add wire connection between two points | Routed (schematic) |
| `delete_schematic_wire` | Delete wire segment | Routed (schematic) |
| `add_schematic_connection` | Connect two component pins with wire | Routed (schematic) |
| `add_schematic_net_label` | Add net label to schematic | Direct |
| `delete_schematic_net_label` | Delete net label | Routed (schematic) |
| `connect_to_net` | Connect component pin to named net | Direct |
| `connect_passthrough` | Connect all matching pins between two connectors | Direct |
| `get_schematic_pin_locations` | Get pin locations for a component | Additional |
### Net Analysis
| Tool | Description | Access |
| ----------------------- | ----------------------------- | ------------------ |
| `get_net_connections` | Get all connections for a net | Routed (schematic) |
| `list_schematic_nets` | List all nets in schematic | Routed (schematic) |
| `list_schematic_wires` | List all wires in schematic | Routed (schematic) |
| `list_schematic_labels` | List all net labels | Routed (schematic) |
### Schematic Creation and Export
| Tool | Description | Access |
| ---------------------- | -------------------------------- | ------------------ |
| `create_schematic` | Create a new schematic file | Routed (schematic) |
| `get_schematic_view` | Get schematic as image (PNG/SVG) | Routed (schematic) |
| `export_schematic_svg` | Export schematic to SVG | Routed (schematic) |
| `export_schematic_pdf` | Export schematic to PDF | Routed (schematic) |
### Validation and Synchronization
| Tool | Description | Access |
| ------------------------- | ----------------------------------------------------- | ------------------ |
| `run_erc` | Run electrical rule check | Additional |
| `generate_netlist` | Generate netlist from schematic | Routed (schematic) |
| `sync_schematic_to_board` | Sync schematic components/nets to PCB (F8 equivalent) | Direct |
---
## Footprint Libraries (4 tools)
_Source: `src/tools/library.ts`_
| Tool | Description | Access |
| ------------------------- | ------------------------------------- | ---------------- |
| `list_libraries` | List all footprint libraries | Routed (library) |
| `search_footprints` | Search footprints across libraries | Routed (library) |
| `list_library_footprints` | List footprints in a specific library | Routed (library) |
| `get_footprint_info` | Get detailed footprint information | Routed (library) |
---
## Symbol Libraries (4 tools)
_Source: `src/tools/library-symbol.ts`_
| Tool | Description | Access |
| ----------------------- | ----------------------------------------------- | ---------- |
| `list_symbol_libraries` | List all symbol libraries from sym-lib-table | Additional |
| `search_symbols` | Search symbols by name, LCSC ID, or description | Additional |
| `list_library_symbols` | List symbols in a specific library | Additional |
| `get_symbol_info` | Get detailed symbol information | Additional |
---
## Footprint Creator (4 tools)
_Source: `src/tools/footprint.ts`_
| Tool | Description | Access |
| ---------------------------- | ------------------------------------------------------------------------ | ---------- |
| `create_footprint` | Create custom .kicad_mod footprint (SMD/THT pads, courtyard, silkscreen) | Additional |
| `edit_footprint_pad` | Edit pad in existing footprint (size, position, drill, shape) | Additional |
| `register_footprint_library` | Register .pretty library in fp-lib-table | Additional |
| `list_footprint_libraries` | List available .pretty libraries | Additional |
---
## Symbol Creator (4 tools)
_Source: `src/tools/symbol-creator.ts`_
| Tool | Description | Access |
| ------------------------- | ------------------------------------------------------------- | ---------- |
| `create_symbol` | Create custom .kicad_sym symbol (pins, rectangles, polylines) | Additional |
| `delete_symbol` | Remove symbol from library | Additional |
| `list_symbols_in_library` | List all symbols in a .kicad_sym file | Additional |
| `register_symbol_library` | Register library in sym-lib-table | Additional |
---
## Datasheet Tools (2 tools)
_Source: `src/tools/datasheet.ts`_
| Tool | Description | Access |
| ------------------- | --------------------------------------------------- | ---------- |
| `enrich_datasheets` | Fill missing datasheet URLs using LCSC part numbers | Additional |
| `get_datasheet_url` | Get LCSC datasheet URL for a component | Additional |
---
## JLCPCB Integration (5 tools)
_Source: `src/tools/jlcpcb-api.ts`_
| Tool | Description | Access |
| ----------------------------- | ------------------------------------------------------- | ---------- |
| `download_jlcpcb_database` | Download 2.5M+ parts catalog to local SQLite database | Additional |
| `search_jlcpcb_parts` | Search parts by specs (category, package, library type) | Additional |
| `get_jlcpcb_part` | Get detailed part info with pricing | Additional |
| `get_jlcpcb_database_stats` | Get database statistics | Additional |
| `suggest_jlcpcb_alternatives` | Find cheaper or in-stock alternatives | Additional |
---
## Freerouting Autorouter (4 tools)
_Source: `src/tools/freerouting.ts`_
| Tool | Description | Access |
| ------------------- | ---------------------------------------------------------- | ------------------ |
| `autoroute` | Run Freerouting autorouter (export DSN, route, import SES) | Routed (autoroute) |
| `export_dsn` | Export Specctra DSN file for manual routing | Routed (autoroute) |
| `import_ses` | Import routed SES file back into PCB | Routed (autoroute) |
| `check_freerouting` | Check Java and Freerouting JAR availability | Routed (autoroute) |
---
## UI Management (2 tools)
_Source: `src/tools/ui.ts`_
| Tool | Description | Access |
| ----------------- | ----------------------------------------- | -------------- |
| `check_kicad_ui` | Check if KiCAD UI is running | Direct |
| `launch_kicad_ui` | Launch KiCAD UI (optionally with project) | Routed (board) |
---
## Router Tools (4 tools)
_Source: `src/tools/router.ts`_
These meta-tools provide discovery and execution of routed tools:
| Tool | Description |
| ---------------------- | ------------------------------------ |
| `list_tool_categories` | Browse all available tool categories |
| `get_category_tools` | View tools in a specific category |
| `search_tools` | Find tools by keyword |
| `execute_tool` | Run any routed tool with parameters |
---
## Summary by Access Type
| Access Type | Count | Description |
| ----------- | ------- | --------------------------------------------------- |
| Direct | 18 | Always visible, no router needed |
| Routed | 65 | Discovered via router, invoked via `execute_tool` |
| Router | 4 | Meta-tools for discovering and running routed tools |
| Additional | 35 | Always visible, registered directly |
| **Total** | **122** | |
## Summary by Category
| Category | Tool Count |
| -------------------- | ---------- |
| Project Management | 5 |
| Board Management | 12 |
| Component Management | 16 |
| Routing | 13 |
| Design Rules / DRC | 8 |
| Export | 8 |
| Schematic | 27 |
| Footprint Libraries | 4 |
| Symbol Libraries | 4 |
| Footprint Creator | 4 |
| Symbol Creator | 4 |
| Datasheet | 2 |
| JLCPCB Integration | 5 |
| Freerouting | 4 |
| UI Management | 2 |
| Router | 4 |
| **Total** | **122** |
## Token Impact
**Before Router Pattern:** All 122 tools in context = ~80K+ tokens
**With Router Pattern:** 18 direct + 35 additional + 4 router = 57 always-visible tools
**On-Demand:** 65 routed tools loaded only when their category is requested

View File

@@ -1,399 +1,425 @@
# KiCAD UI Auto-Launch Feature
Automatically detect and launch KiCAD UI when needed, providing seamless visual feedback for PCB design operations.
---
## 🎯 Overview
The KiCAD MCP server can now:
- ✅ Detect if KiCAD UI is running
-Launch KiCAD automatically when needed
-Open projects directly in the UI
-Work across Linux, macOS, and Windows
---
## 🚀 Quick Start
### Enable Auto-Launch
Add to your MCP configuration:
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/path/to/KiCAD-MCP-Server/dist/index.js"],
"env": {
"KICAD_AUTO_LAUNCH": "true"
}
}
}
}
```
### Manual Control (Default)
Without `KICAD_AUTO_LAUNCH=true`, you manually control when KiCAD launches using the new MCP tools.
---
## 🛠️ New MCP Tools
### 1. `check_kicad_ui`
Check if KiCAD is currently running.
**Parameters:** None
**Example:**
```typescript
{
"command": "check_kicad_ui",
"params": {}
}
```
**Response:**
```json
{
"success": true,
"running": true,
"processes": [
{
"pid": "12345",
"name": "pcbnew",
"command": "/usr/bin/pcbnew /tmp/project.kicad_pcb"
}
],
"message": "KiCAD is running"
}
```
### 2. `launch_kicad_ui`
Launch KiCAD UI, optionally with a project file.
**Parameters:**
- `projectPath` (optional): Path to `.kicad_pcb` file to open
- `autoLaunch` (optional): Whether to launch if not running (default: true)
**Example:**
```typescript
{
"command": "launch_kicad_ui",
"params": {
"projectPath": "/tmp/mcp_demo/New_Project.kicad_pcb"
}
}
```
**Response:**
```json
{
"success": true,
"running": true,
"launched": true,
"message": "KiCAD launched successfully",
"project": "/tmp/mcp_demo/New_Project.kicad_pcb",
"processes": [...]
}
```
---
## 🔄 Workflow Examples
### Example 1: Manual Launch
```
User: "Check if KiCAD is running"
Claude: Uses check_kicad_ui → "KiCAD is not running"
User: "Launch it with the demo project"
Claude: Uses launch_kicad_ui → KiCAD opens with project loaded!
```
### Example 2: Auto-Launch Mode
With `KICAD_AUTO_LAUNCH=true`:
```
User: "Create a new Arduino shield PCB"
Claude:
1. Creates project
2. Detects KiCAD not running
3. Automatically launches KiCAD with the new project
4. You see the board in real-time as it's designed!
```
### Example 3: Side-by-Side Design
```
┌────────────────────────────────────────────────────────┐
│ Workflow: AI-Assisted PCB Design │
├────────────────────────────────────────────────────────┤
│ │
│ 1. User: "Create a 100mm square board" │
│ → Claude creates project │
│ → KiCAD auto-launches if not running │
│ 2. User: "Add 4 mounting holes at corners" │
→ Claude adds holes
→ KiCAD detects file change, prompts to reload
│ → User clicks "Yes" → sees holes appear!
3. User: "Perfect! Now add a circular outline..."
→ Iterative design continues...
└────────────────────────────────────────────────────────┘
```
---
## ⚙️ Configuration Options
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `KICAD_AUTO_LAUNCH` | `false` | Auto-launch KiCAD when needed |
| `KICAD_EXECUTABLE` | auto-detect | Override KiCAD executable path |
### Custom Executable Path
If KiCAD is installed in a non-standard location:
```json
{
"env": {
"KICAD_AUTO_LAUNCH": "true",
"KICAD_EXECUTABLE": "/opt/kicad/bin/pcbnew"
}
}
```
---
## 🔍 How It Works
### Process Detection
**Linux:**
```bash
pgrep -f "pcbnew|kicad"
```
**macOS:**
```bash
pgrep -f "KiCad|pcbnew"
```
**Windows:**
```powershell
tasklist /FI "IMAGENAME eq pcbnew.exe"
```
### Auto-Discovery of Executable
The system searches for KiCAD in:
**Linux:**
- `/usr/bin/pcbnew`
- `/usr/local/bin/pcbnew`
- `/usr/bin/kicad`
**macOS:**
- `/Applications/KiCad/KiCad.app/Contents/MacOS/kicad`
- `/Applications/KiCad/pcbnew.app/Contents/MacOS/pcbnew`
**Windows:**
- `C:/Program Files/KiCad/9.0/bin/pcbnew.exe`
- `C:/Program Files/KiCad/8.0/bin/pcbnew.exe`
### Launch Process
1. Check if KiCAD is already running
2. If not, find executable path
3. Spawn process with optional project path
4. Wait up to 5 seconds for process to start
5. Verify process is running
6. Return status to MCP client
---
## 💡 Use Cases
### 1. Beginner-Friendly Workflow
User doesn't need to know how to launch KiCAD manually:
```
User: "Help me design a simple LED board"
Claude: [Auto-launches KiCAD, creates project, designs board]
```
### 2. Streamlined Iteration
For rapid prototyping with visual feedback:
```
1. Claude creates board → KiCAD opens
2. User sees board, requests changes
3. Claude modifies → KiCAD reloads
4. Repeat until satisfied
```
### 3. Batch Processing
Process multiple designs without manual intervention:
```python
for design in designs:
create_project(design)
# KiCAD auto-launches and loads each one
add_components(design)
route_board(design)
export_gerbers(design)
```
---
## 🐛 Troubleshooting
### KiCAD Doesn't Launch
**Check executable path:**
```bash
# Linux/macOS
which pcbnew
# Windows
where pcbnew.exe
```
**Override if needed:**
```json
{
"env": {
"KICAD_EXECUTABLE": "/path/to/pcbnew"
}
}
```
### Process Detection Fails
**Manual check:**
```bash
# Linux/macOS
ps aux | grep kicad
# Windows
tasklist | findstr kicad
```
**Verify permissions:**
- Ensure user can execute `pgrep` (Linux/macOS)
- Ensure user can execute `tasklist` (Windows)
### Auto-Launch Doesn't Work
1. Check `KICAD_AUTO_LAUNCH` is set to `"true"` (string, not boolean)
2. Verify KiCAD is in PATH or set `KICAD_EXECUTABLE`
3. Check MCP server logs for errors
4. Try manual launch first: `launch_kicad_ui`
---
## 📊 Implementation Details
### Files Modified/Created
**New Files:**
- `python/utils/kicad_process.py` - Process management utilities
- `src/tools/ui.ts` - MCP tool definitions
- `docs/UI_AUTO_LAUNCH.md` - This documentation
**Modified Files:**
- `python/kicad_interface.py` - Added UI command handlers
- `src/server.ts` - Registered UI tools
### API Reference
**Python:**
```python
from utils.kicad_process import KiCADProcessManager, check_and_launch_kicad
# Check if running
manager = KiCADProcessManager()
is_running = manager.is_running()
# Launch KiCAD
success = manager.launch(project_path="/path/to/file.kicad_pcb")
# Get process info
processes = manager.get_process_info()
# High-level helper
result = check_and_launch_kicad(
project_path=Path("/path/to/file.kicad_pcb"),
auto_launch=True
)
```
**MCP Tools:**
```typescript
// Check status
await callKicadScript("check_kicad_ui", {});
// Launch
await callKicadScript("launch_kicad_ui", {
projectPath: "/path/to/project.kicad_pcb",
autoLaunch: true
});
```
---
## 🔮 Future Enhancements
### Planned Features
- **Window Management:** Bring KiCAD to front, minimize/maximize
- **Multi-Instance:** Handle multiple KiCAD instances
- **IPC Integration:** Seamless integration with IPC backend
- **Status Notifications:** Push notifications when KiCAD state changes
- **Auto-Close:** Option to close KiCAD after operations complete
### IPC Mode (Coming Weeks 2-3)
When IPC backend is fully implemented:
```
KiCAD runs in background → MCP connects via IPC → Real-time updates
No file reloading needed! Changes appear instantly.
```
---
## 📝 Summary
**Before this feature:**
```
User manually launches KiCAD
User manually opens project
Claude makes changes
User manually reloads
```
**After this feature:**
```
User: "Design a board"
→ KiCAD auto-launches with project
→ Changes appear (with quick reload)
→ Seamless AI-assisted design!
```
---
**Last Updated:** 2025-10-26
**Version:** 2.0.0-alpha.1
**Status:** ✅ Production Ready
# KiCAD UI Auto-Launch Feature
Automatically detect and launch KiCAD UI when needed, providing seamless visual feedback for PCB design operations.
---
## 🎯 Overview
The KiCAD MCP server can now:
-Detect if KiCAD UI is running
-Launch KiCAD automatically when needed
-Open projects directly in the UI
- ✅ Work across Linux, macOS, and Windows
---
## 🚀 Quick Start
### Enable Auto-Launch
Add to your MCP configuration:
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["/path/to/KiCAD-MCP-Server/dist/index.js"],
"env": {
"KICAD_AUTO_LAUNCH": "true"
}
}
}
}
```
### Manual Control (Default)
Without `KICAD_AUTO_LAUNCH=true`, you manually control when KiCAD launches using the new MCP tools.
---
## 🛠️ New MCP Tools
### 1. `check_kicad_ui`
Check if KiCAD is currently running.
**Parameters:** None
**Example:**
```typescript
{
"command": "check_kicad_ui",
"params": {}
}
```
**Response:**
```json
{
"success": true,
"running": true,
"processes": [
{
"pid": "12345",
"name": "pcbnew",
"command": "/usr/bin/pcbnew /tmp/project.kicad_pcb"
}
],
"message": "KiCAD is running"
}
```
### 2. `launch_kicad_ui`
Launch KiCAD UI, optionally with a project file.
**Parameters:**
- `projectPath` (optional): Path to `.kicad_pcb` file to open
- `autoLaunch` (optional): Whether to launch if not running (default: true)
**Example:**
```typescript
{
"command": "launch_kicad_ui",
"params": {
"projectPath": "/tmp/mcp_demo/New_Project.kicad_pcb"
}
}
```
**Response:**
```json
{
"success": true,
"running": true,
"launched": true,
"message": "KiCAD launched successfully",
"project": "/tmp/mcp_demo/New_Project.kicad_pcb",
"processes": [...]
}
```
---
## 🔄 Workflow Examples
### Example 1: Manual Launch
```
User: "Check if KiCAD is running"
Claude: Uses check_kicad_ui → "KiCAD is not running"
User: "Launch it with the demo project"
Claude: Uses launch_kicad_ui → KiCAD opens with project loaded!
```
### Example 2: Auto-Launch Mode
With `KICAD_AUTO_LAUNCH=true`:
```
User: "Create a new Arduino shield PCB"
Claude:
1. Creates project
2. Detects KiCAD not running
3. Automatically launches KiCAD with the new project
4. You see the board in real-time as it's designed!
```
### Example 3: Side-by-Side Design
```
┌────────────────────────────────────────────────────────┐
Workflow: AI-Assisted PCB Design
├────────────────────────────────────────────────────────┤
1. User: "Create a 100mm square board"
│ → Claude creates project
→ KiCAD auto-launches if not running
2. User: "Add 4 mounting holes at corners"
→ Claude adds holes
│ → KiCAD detects file change, prompts to reload │
│ → User clicks "Yes" → sees holes appear! │
│ │
│ 3. User: "Perfect! Now add a circular outline..." │
│ → Iterative design continues... │
│ │
└────────────────────────────────────────────────────────┘
```
---
## ⚙️ Configuration Options
### Environment Variables
| Variable | Default | Description |
| ------------------- | ----------- | ------------------------------ |
| `KICAD_AUTO_LAUNCH` | `false` | Auto-launch KiCAD when needed |
| `KICAD_EXECUTABLE` | auto-detect | Override KiCAD executable path |
### Custom Executable Path
If KiCAD is installed in a non-standard location:
```json
{
"env": {
"KICAD_AUTO_LAUNCH": "true",
"KICAD_EXECUTABLE": "/opt/kicad/bin/pcbnew"
}
}
```
---
## 🔍 How It Works
### Process Detection
**Linux:**
```bash
pgrep -f "pcbnew|kicad"
```
**macOS:**
```bash
pgrep -f "KiCad|pcbnew"
```
**Windows:**
```powershell
tasklist /FI "IMAGENAME eq pcbnew.exe"
```
### Auto-Discovery of Executable
The system searches for KiCAD in:
**Linux:**
- `/usr/bin/pcbnew`
- `/usr/local/bin/pcbnew`
- `/usr/bin/kicad`
**macOS:**
- `/Applications/KiCad/KiCad.app/Contents/MacOS/kicad`
- `/Applications/KiCad/pcbnew.app/Contents/MacOS/pcbnew`
**Windows:**
- `C:/Program Files/KiCad/9.0/bin/pcbnew.exe`
- `C:/Program Files/KiCad/8.0/bin/pcbnew.exe`
### Launch Process
1. Check if KiCAD is already running
2. If not, find executable path
3. Spawn process with optional project path
4. Wait up to 5 seconds for process to start
5. Verify process is running
6. Return status to MCP client
---
## 💡 Use Cases
### 1. Beginner-Friendly Workflow
User doesn't need to know how to launch KiCAD manually:
```
User: "Help me design a simple LED board"
Claude: [Auto-launches KiCAD, creates project, designs board]
```
### 2. Streamlined Iteration
For rapid prototyping with visual feedback:
```
1. Claude creates board → KiCAD opens
2. User sees board, requests changes
3. Claude modifies → KiCAD reloads
4. Repeat until satisfied
```
### 3. Batch Processing
Process multiple designs without manual intervention:
```python
for design in designs:
create_project(design)
# KiCAD auto-launches and loads each one
add_components(design)
route_board(design)
export_gerbers(design)
```
---
## 🐛 Troubleshooting
### KiCAD Doesn't Launch
**Check executable path:**
```bash
# Linux/macOS
which pcbnew
# Windows
where pcbnew.exe
```
**Override if needed:**
```json
{
"env": {
"KICAD_EXECUTABLE": "/path/to/pcbnew"
}
}
```
### Process Detection Fails
**Manual check:**
```bash
# Linux/macOS
ps aux | grep kicad
# Windows
tasklist | findstr kicad
```
**Verify permissions:**
- Ensure user can execute `pgrep` (Linux/macOS)
- Ensure user can execute `tasklist` (Windows)
### Auto-Launch Doesn't Work
1. Check `KICAD_AUTO_LAUNCH` is set to `"true"` (string, not boolean)
2. Verify KiCAD is in PATH or set `KICAD_EXECUTABLE`
3. Check MCP server logs for errors
4. Try manual launch first: `launch_kicad_ui`
---
## 📊 Implementation Details
### Files Modified/Created
**New Files:**
- `python/utils/kicad_process.py` - Process management utilities
- `src/tools/ui.ts` - MCP tool definitions
- `docs/UI_AUTO_LAUNCH.md` - This documentation
**Modified Files:**
- `python/kicad_interface.py` - Added UI command handlers
- `src/server.ts` - Registered UI tools
### API Reference
**Python:**
```python
from utils.kicad_process import KiCADProcessManager, check_and_launch_kicad
# Check if running
manager = KiCADProcessManager()
is_running = manager.is_running()
# Launch KiCAD
success = manager.launch(project_path="/path/to/file.kicad_pcb")
# Get process info
processes = manager.get_process_info()
# High-level helper
result = check_and_launch_kicad(
project_path=Path("/path/to/file.kicad_pcb"),
auto_launch=True
)
```
**MCP Tools:**
```typescript
// Check status
await callKicadScript("check_kicad_ui", {});
// Launch
await callKicadScript("launch_kicad_ui", {
projectPath: "/path/to/project.kicad_pcb",
autoLaunch: true,
});
```
---
## 🔮 Future Enhancements
### Planned Features
- **Window Management:** Bring KiCAD to front, minimize/maximize
- **Multi-Instance:** Handle multiple KiCAD instances
- **IPC Integration:** Seamless integration with IPC backend
- **Status Notifications:** Push notifications when KiCAD state changes
- **Auto-Close:** Option to close KiCAD after operations complete
### IPC Mode (Coming Weeks 2-3)
When IPC backend is fully implemented:
```
KiCAD runs in background → MCP connects via IPC → Real-time updates
No file reloading needed! Changes appear instantly.
```
---
## 📝 Summary
**Before this feature:**
```
User manually launches KiCAD
User manually opens project
Claude makes changes
User manually reloads
```
**After this feature:**
```
User: "Design a board"
→ KiCAD auto-launches with project
→ Changes appear (with quick reload)
→ Seamless AI-assisted design!
```
---
**Last Updated:** 2025-10-26
**Version:** 2.0.0-alpha.1
**Status:** ✅ Production Ready

View File

@@ -1,184 +1,193 @@
# Visual Feedback: Seeing MCP Changes in KiCAD UI
This document explains how to see changes made by the MCP server in the KiCAD UI in real-time or near-real-time.
## Current Status (Week 1 - SWIG Backend)
**Active Backend:** SWIG (legacy pcbnew Python API)
**Real-time Updates:** Not available yet
**IPC Backend:** Skeleton implemented, operations coming in Weeks 2-3
---
## 🎯 Best Current Workflow (SWIG + Manual Reload)
### Setup
1. **Open your project in KiCAD PCB Editor**
```bash
pcbnew /tmp/kicad_test_project/New_Project.kicad_pcb
```
2. **Make changes via MCP** (Claude Code, Claude Desktop, etc.)
- Example: Add board outline, mounting holes, etc.
- Each operation saves the file automatically
3. **Reload in KiCAD UI**
- **Option A (Automatic):** KiCAD 8.0+ detects file changes and shows a reload prompt
- **Option B (Manual):** File → Revert to reload from disk
- **Keyboard shortcut:** None by default (but you can assign one)
### Workflow Example
```
┌─────────────────────────────────────────────────────────┐
│ Terminal: Claude Code │
├─────────────────────────────────────────────────────────┤
│ You: "Create a 100x80mm board with 4 mounting holes" │
Claude: ✓ Added board outline (100x80mm)
✓ Added mounting hole at (5,5)
│ ✓ Added mounting hole at (95,5) │
│ ✓ Added mounting hole at (95,75) │
│ ✓ Added mounting hole at (5,75)
│ ✓ Saved project
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ KiCAD PCB Editor │
├─────────────────────────────────────────────────────────┤
│ [Reload prompt appears] │
"File has been modified. Reload?"
Click "Yes" → Changes appear instantly! 🎉
└─────────────────────────────────────────────────────────┘
```
---
## 🔮 Future: IPC Backend (Weeks 2-3)
When fully implemented, the IPC backend will provide **true real-time updates**:
### How It Will Work
```
Claude MCP → IPC Socket → Running KiCAD → Instant UI Update
```
**No file reloading required** - changes appear as you make them!
### IPC Setup (When Available)
1. **Enable IPC in KiCAD**
- Preferences → Advanced Preferences
- Search for "IPC"
- Enable: "Enable IPC API Server"
- Restart KiCAD
2. **Install kicad-python** (Already installed ✓)
```bash
pip install kicad-python
```
3. **Configure MCP Server**
Add to your MCP config:
```json
{
"env": {
"KICAD_BACKEND": "ipc"
}
}
```
4. **Start KiCAD first, then use MCP**
- Changes will appear in real-time
- No manual reloading needed
### Current IPC Status
| Feature | Status |
|---------|--------|
| Connection to KiCAD | ✅ Working |
| Version checking | ✅ Working |
| Project operations | ⏳ Week 2-3 |
| Board operations | Week 2-3 |
| Component operations | Week 2-3 |
| Routing operations | ⏳ Week 2-3 |
---
## 🛠️ Monitoring Helper (Optional)
A helper script is available to monitor file changes:
```bash
# Watch for changes and notify
./scripts/auto_refresh_kicad.sh /tmp/kicad_test_project/New_Project.kicad_pcb
```
This will print a message each time the MCP server saves changes.
---
## 💡 Tips for Best Experience
### 1. Side-by-Side Windows
```
┌──────────────────┬──────────────────┐
│ Claude Code │ KiCAD PCB │
│ (Terminal) │ Editor │
│ │ │
│ Making changes │ Viewing results │
└──────────────────┴──────────────────┘
```
### 2. Quick Reload Workflow
- Keep KiCAD focused in one window
- Make changes via Claude in another
- Press Alt+Tab → Click "Reload" → See changes
- Repeat
### 3. Save Frequently
The MCP server auto-saves after each operation, so changes are immediately available for reload.
### 4. Verify Before Complex Operations
For complex changes (multiple components, routing, etc.):
1. Make the change
2. Reload in KiCAD
3. Verify it looks correct
4. Proceed with next change
---
## 🔍 Troubleshooting
### KiCAD Doesn't Detect File Changes
**Cause:** Some KiCAD versions or configurations don't auto-detect
**Solution:** Use File → Revert manually
### Changes Don't Appear After Reload
**Cause:** MCP operation may have failed
**Solution:** Check the MCP response for success: true
### File is Locked
**Cause:** KiCAD has the file open exclusively
**Solution:**
- KiCAD should allow external modifications
- If not, close the file in KiCAD, let MCP make changes, then reopen
---
## 📅 Roadmap
**Current (Week 1):** SWIG backend with manual reload
**Week 2-3:** IPC backend implementation
**Week 4+:** Real-time collaboration features
---
**Last Updated:** 2025-10-26
**Version:** 2.0.0-alpha.1
# Visual Feedback: Seeing MCP Changes in KiCAD UI
This document explains how to see changes made by the MCP server in the KiCAD UI in real-time or near-real-time.
## Current Status (Week 1 - SWIG Backend)
**Active Backend:** SWIG (legacy pcbnew Python API)
**Real-time Updates:** Not available yet
**IPC Backend:** Skeleton implemented, operations coming in Weeks 2-3
---
## 🎯 Best Current Workflow (SWIG + Manual Reload)
### Setup
1. **Open your project in KiCAD PCB Editor**
```bash
pcbnew /tmp/kicad_test_project/New_Project.kicad_pcb
```
2. **Make changes via MCP** (Claude Code, Claude Desktop, etc.)
- Example: Add board outline, mounting holes, etc.
- Each operation saves the file automatically
3. **Reload in KiCAD UI**
- **Option A (Automatic):** KiCAD 8.0+ detects file changes and shows a reload prompt
- **Option B (Manual):** File → Revert to reload from disk
- **Keyboard shortcut:** None by default (but you can assign one)
### Workflow Example
```
┌─────────────────────────────────────────────────────────┐
│ Terminal: Claude Code │
├─────────────────────────────────────────────────────────┤
You: "Create a 100x80mm board with 4 mounting holes"
Claude: ✓ Added board outline (100x80mm)
│ ✓ Added mounting hole at (5,5)
│ ✓ Added mounting hole at (95,5)
│ ✓ Added mounting hole at (95,75) │
│ ✓ Added mounting hole at (5,75)
│ ✓ Saved project │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ KiCAD PCB Editor │
├─────────────────────────────────────────────────────────┤
[Reload prompt appears]
"File has been modified. Reload?"
│ Click "Yes" → Changes appear instantly! 🎉 │
└─────────────────────────────────────────────────────────┘
```
---
## 🔮 Future: IPC Backend (Weeks 2-3)
When fully implemented, the IPC backend will provide **true real-time updates**:
### How It Will Work
```
Claude MCP → IPC Socket → Running KiCAD → Instant UI Update
```
**No file reloading required** - changes appear as you make them!
### IPC Setup (When Available)
1. **Enable IPC in KiCAD**
- Preferences → Advanced Preferences
- Search for "IPC"
- Enable: "Enable IPC API Server"
- Restart KiCAD
2. **Install kicad-python** (Already installed ✓)
```bash
pip install kicad-python
```
3. **Configure MCP Server**
Add to your MCP config:
```json
{
"env": {
"KICAD_BACKEND": "ipc"
}
}
```
4. **Start KiCAD first, then use MCP**
- Changes will appear in real-time
- No manual reloading needed
### Current IPC Status
| Feature | Status |
| -------------------- | ----------- |
| Connection to KiCAD | Working |
| Version checking | Working |
| Project operations | ⏳ Week 2-3 |
| Board operations | ⏳ Week 2-3 |
| Component operations | ⏳ Week 2-3 |
| Routing operations | ⏳ Week 2-3 |
---
## 🛠️ Monitoring Helper (Optional)
A helper script is available to monitor file changes:
```bash
# Watch for changes and notify
./scripts/auto_refresh_kicad.sh /tmp/kicad_test_project/New_Project.kicad_pcb
```
This will print a message each time the MCP server saves changes.
---
## 💡 Tips for Best Experience
### 1. Side-by-Side Windows
```
┌──────────────────┬──────────────────┐
│ Claude Code │ KiCAD PCB │
│ (Terminal) │ Editor │
│ │ │
│ Making changes │ Viewing results │
└──────────────────┴──────────────────┘
```
### 2. Quick Reload Workflow
- Keep KiCAD focused in one window
- Make changes via Claude in another
- Press Alt+Tab → Click "Reload" → See changes
- Repeat
### 3. Save Frequently
The MCP server auto-saves after each operation, so changes are immediately available for reload.
### 4. Verify Before Complex Operations
For complex changes (multiple components, routing, etc.):
1. Make the change
2. Reload in KiCAD
3. Verify it looks correct
4. Proceed with next change
---
## 🔍 Troubleshooting
### KiCAD Doesn't Detect File Changes
**Cause:** Some KiCAD versions or configurations don't auto-detect
**Solution:** Use File → Revert manually
### Changes Don't Appear After Reload
**Cause:** MCP operation may have failed
**Solution:** Check the MCP response for success: true
### File is Locked
**Cause:** KiCAD has the file open exclusively
**Solution:**
- KiCAD should allow external modifications
- If not, close the file in KiCAD, let MCP make changes, then reopen
---
## 📅 Roadmap
**Current (Week 1):** SWIG backend with manual reload
**Week 2-3:** IPC backend implementation
**Week 4+:** Real-time collaboration features
---
**Last Updated:** 2025-10-26
**Version:** 2.0.0-alpha.1

View File

@@ -1,475 +1,493 @@
# Windows Troubleshooting Guide
This guide helps diagnose and fix common issues when setting up KiCAD MCP Server on Windows.
## Quick Start: Automated Setup
**Before manually troubleshooting, try the automated setup script:**
```powershell
# Open PowerShell in the KiCAD-MCP-Server directory
.\setup-windows.ps1
```
This script will:
- Detect your KiCAD installation
- Verify all prerequisites
- Install dependencies
- Build the project
- Generate configuration
- Run diagnostic tests
If the automated setup fails, continue with the manual troubleshooting below.
---
## Common Issues and Solutions
### Issue 1: Server Exits Immediately (Most Common)
**Symptom:** Claude Desktop logs show "Server transport closed unexpectedly"
**Cause:** Python process crashes during startup, usually due to missing pcbnew module
**Solution:**
1. **Check the log file** (this has the actual error):
```
%USERPROFILE%\.kicad-mcp\logs\kicad_interface.log
```
Open in Notepad and look at the last 50-100 lines.
2. **Test pcbnew import manually:**
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c "import pcbnew; print(pcbnew.GetBuildVersion())"
```
**Expected:** Prints KiCAD version like `9.0.0`
**If it fails:**
- KiCAD's Python module isn't installed
- Reinstall KiCAD with default options
- Make sure "Install Python" is checked during installation
3. **Verify PYTHONPATH in your config:**
```json
{
"mcpServers": {
"kicad": {
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
}
}
}
}
```
---
### Issue 2: KiCAD Not Found
**Symptom:** Log shows "No KiCAD installations found"
**Solution:**
1. **Check if KiCAD is installed:**
```powershell
Test-Path "C:\Program Files\KiCad\9.0"
```
2. **If KiCAD is installed elsewhere:**
- Find your KiCAD installation directory
- Update PYTHONPATH in config to match your installation
- Example for version 8.0:
```
"PYTHONPATH": "C:\\Program Files\\KiCad\\8.0\\lib\\python3\\dist-packages"
```
3. **If KiCAD is not installed:**
- Download from https://www.kicad.org/download/windows/
- Install version 9.0 or higher
- Use default installation path
---
### Issue 3: Node.js Not Found
**Symptom:** Cannot run `npm install` or `npm run build`
**Solution:**
1. **Check if Node.js is installed:**
```powershell
node --version
npm --version
```
2. **If not installed:**
- Download Node.js 18+ from https://nodejs.org/
- Install with default options
- Restart PowerShell after installation
3. **If installed but not in PATH:**
```powershell
# Add to PATH temporarily
$env:PATH += ";C:\Program Files\nodejs"
```
---
### Issue 4: Build Fails with TypeScript Errors
**Symptom:** `npm run build` shows TypeScript compilation errors
**Solution:**
1. **Clean and reinstall dependencies:**
```powershell
Remove-Item node_modules -Recurse -Force
Remove-Item package-lock.json -Force
npm install
npm run build
```
2. **Check Node.js version:**
```powershell
node --version # Should be v18.0.0 or higher
```
3. **If still failing:**
```powershell
# Try with legacy peer deps
npm install --legacy-peer-deps
npm run build
```
---
### Issue 5: Python Dependencies Missing
**Symptom:** Log shows errors about missing Python packages (Pillow, cairosvg, etc.)
**Solution:**
1. **Install with KiCAD's Python:**
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -m pip install -r requirements.txt
```
2. **If pip is not available:**
```powershell
# Download get-pip.py
Invoke-WebRequest -Uri https://bootstrap.pypa.io/get-pip.py -OutFile get-pip.py
# Install pip
& "C:\Program Files\KiCad\9.0\bin\python.exe" get-pip.py
# Then install requirements
& "C:\Program Files\KiCad\9.0\bin\python.exe" -m pip install -r requirements.txt
```
---
### Issue 6: Permission Denied Errors
**Symptom:** Cannot write to Program Files or access certain directories
**Solution:**
1. **Run PowerShell as Administrator:**
- Right-click PowerShell icon
- Select "Run as Administrator"
- Navigate to KiCAD-MCP-Server directory
- Run setup again
2. **Or clone to user directory:**
```powershell
cd $HOME\Documents
git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
cd KiCAD-MCP-Server
.\setup-windows.ps1
```
---
### Issue 7: Path Issues in Configuration
**Symptom:** Config file paths not working
**Common mistakes:**
```json
// ❌ Wrong - single backslashes
"args": ["C:\Users\Name\KiCAD-MCP-Server\dist\index.js"]
// ❌ Wrong - mixed slashes
"args": ["C:\Users/Name\KiCAD-MCP-Server/dist\index.js"]
// ✅ Correct - double backslashes
"args": ["C:\\Users\\Name\\KiCAD-MCP-Server\\dist\\index.js"]
// ✅ Also correct - forward slashes
"args": ["C:/Users/Name/KiCAD-MCP-Server/dist/index.js"]
```
**Solution:** Use either double backslashes `\\` or forward slashes `/` consistently.
---
### Issue 8: Wrong Python Version
**Symptom:** Errors about Python 2.7 or Python 3.6
**Solution:**
KiCAD MCP requires Python 3.10+. KiCAD 9.0 includes Python 3.11, which is perfect.
**Always use KiCAD's bundled Python:**
```json
{
"mcpServers": {
"kicad": {
"command": "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\python\\kicad_interface.py"]
}
}
}
```
This bypasses Node.js and runs Python directly.
---
## Configuration Examples
### For Claude Desktop
Config location: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages",
"NODE_ENV": "production",
"LOG_LEVEL": "info"
}
}
}
}
```
### For Cline (VSCode)
Config location: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
},
"description": "KiCAD PCB Design Assistant"
}
}
}
```
### Alternative: Python Direct Mode
If Node.js issues persist, run Python directly:
```json
{
"mcpServers": {
"kicad": {
"command": "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\python\\kicad_interface.py"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
}
}
}
}
```
---
## Manual Testing Steps
### Test 1: Verify KiCAD Python
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c @"
import sys
print(f'Python version: {sys.version}')
import pcbnew
print(f'pcbnew version: {pcbnew.GetBuildVersion()}')
print('SUCCESS!')
"@
```
Expected output:
```
Python version: 3.11.x ...
pcbnew version: 9.0.0
SUCCESS!
```
### Test 2: Verify Node.js
```powershell
node --version # Should be v18.0.0+
npm --version # Should be 9.0.0+
```
### Test 3: Build Project
```powershell
cd C:\Users\YourName\KiCAD-MCP-Server
npm install
npm run build
Test-Path .\dist\index.js # Should output: True
```
### Test 4: Run Server Manually
```powershell
$env:PYTHONPATH = "C:\Program Files\KiCad\9.0\lib\python3\dist-packages"
node .\dist\index.js
```
Expected: Server should start and wait for input (doesn't exit immediately)
**To stop:** Press Ctrl+C
### Test 5: Check Log File
```powershell
# View log file
Get-Content "$env:USERPROFILE\.kicad-mcp\logs\kicad_interface.log" -Tail 50
```
Should show successful initialization with no errors.
---
## Advanced Diagnostics
### Enable Verbose Logging
Add to your MCP config:
```json
{
"env": {
"LOG_LEVEL": "debug",
"PYTHONUNBUFFERED": "1"
}
}
```
### Check Python sys.path
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c @"
import sys
for path in sys.path:
print(path)
"@
```
Should include: `C:\Program Files\KiCad\9.0\lib\python3\dist-packages`
### Test MCP Communication
```powershell
# Start server
$env:PYTHONPATH = "C:\Program Files\KiCad\9.0\lib\python3\dist-packages"
$process = Start-Process -FilePath "node" -ArgumentList ".\dist\index.js" -NoNewWindow -PassThru
# Wait 3 seconds
Start-Sleep -Seconds 3
# Check if still running
if ($process.HasExited) {
Write-Host "Server crashed!" -ForegroundColor Red
Write-Host "Exit code: $($process.ExitCode)"
} else {
Write-Host "Server is running!" -ForegroundColor Green
Stop-Process -Id $process.Id
}
```
---
## Getting Help
If none of the above solutions work:
1. **Run the diagnostic script:**
```powershell
.\setup-windows.ps1
```
Copy the entire output.
2. **Collect log files:**
- MCP log: `%USERPROFILE%\.kicad-mcp\logs\kicad_interface.log`
- Claude Desktop log: `%APPDATA%\Claude\logs\mcp*.log`
3. **Open a GitHub issue:**
- Go to: https://github.com/mixelpixx/KiCAD-MCP-Server/issues
- Title: "Windows Setup Issue: [brief description]"
- Include:
- Windows version (10 or 11)
- Output from setup script
- Log file contents
- Output from manual tests above
---
## Known Limitations on Windows
1. **File paths are case-insensitive** but should match actual casing for best results
2. **Long path support** may be needed for deeply nested projects:
```powershell
# Enable long paths (requires admin)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
```
3. **Windows Defender** may slow down file operations. Add exclusion:
```
Settings → Windows Security → Virus & threat protection → Exclusions
Add: C:\Users\YourName\KiCAD-MCP-Server
```
4. **Antivirus software** may block Python/Node processes. Temporarily disable for testing.
---
## Success Checklist
When everything works, you should have:
- [ ] KiCAD 9.0+ installed at `C:\Program Files\KiCad\9.0`
- [ ] Node.js 18+ installed and in PATH
- [ ] Python can import pcbnew successfully
- [ ] `npm run build` completes without errors
- [ ] `dist\index.js` file exists
- [ ] MCP config file created with correct paths
- [ ] Server starts without immediate crash
- [ ] Log file shows successful initialization
- [ ] Claude Desktop/Cline recognizes the MCP server
- [ ] Can execute: "Create a new KiCAD project"
---
**Last Updated:** 2025-11-05
**Maintained by:** KiCAD MCP Team
For the latest updates, see: https://github.com/mixelpixx/KiCAD-MCP-Server
# Windows Troubleshooting Guide
This guide helps diagnose and fix common issues when setting up KiCAD MCP Server on Windows.
## Quick Start: Automated Setup
**Before manually troubleshooting, try the automated setup script:**
```powershell
# Open PowerShell in the KiCAD-MCP-Server directory
.\setup-windows.ps1
```
This script will:
- Detect your KiCAD installation
- Verify all prerequisites
- Install dependencies
- Build the project
- Generate configuration
- Run diagnostic tests
If the automated setup fails, continue with the manual troubleshooting below.
---
## Common Issues and Solutions
### Issue 1: Server Exits Immediately (Most Common)
**Symptom:** Claude Desktop logs show "Server transport closed unexpectedly"
**Cause:** Python process crashes during startup, usually due to missing pcbnew module
**Solution:**
1. **Check the log file** (this has the actual error):
```
%USERPROFILE%\.kicad-mcp\logs\kicad_interface.log
```
Open in Notepad and look at the last 50-100 lines.
2. **Test pcbnew import manually:**
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c "import pcbnew; print(pcbnew.GetBuildVersion())"
```
**Expected:** Prints KiCAD version like `9.0.0`
**If it fails:**
- KiCAD's Python module isn't installed
- Reinstall KiCAD with default options
- Make sure "Install Python" is checked during installation
3. **Verify PYTHONPATH in your config:**
```json
{
"mcpServers": {
"kicad": {
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
}
}
}
}
```
---
### Issue 2: KiCAD Not Found
**Symptom:** Log shows "No KiCAD installations found"
**Solution:**
1. **Check if KiCAD is installed:**
```powershell
Test-Path "C:\Program Files\KiCad\9.0"
```
2. **If KiCAD is installed elsewhere:**
- Find your KiCAD installation directory
- Update PYTHONPATH in config to match your installation
- Example for version 8.0:
```
"PYTHONPATH": "C:\\Program Files\\KiCad\\8.0\\lib\\python3\\dist-packages"
```
3. **If KiCAD is not installed:**
- Download from https://www.kicad.org/download/windows/
- Install version 9.0 or higher
- Use default installation path
---
### Issue 3: Node.js Not Found
**Symptom:** Cannot run `npm install` or `npm run build`
**Solution:**
1. **Check if Node.js is installed:**
```powershell
node --version
npm --version
```
2. **If not installed:**
- Download Node.js 18+ from https://nodejs.org/
- Install with default options
- Restart PowerShell after installation
3. **If installed but not in PATH:**
```powershell
# Add to PATH temporarily
$env:PATH += ";C:\Program Files\nodejs"
```
---
### Issue 4: Build Fails with TypeScript Errors
**Symptom:** `npm run build` shows TypeScript compilation errors
**Solution:**
1. **Clean and reinstall dependencies:**
```powershell
Remove-Item node_modules -Recurse -Force
Remove-Item package-lock.json -Force
npm install
npm run build
```
2. **Check Node.js version:**
```powershell
node --version # Should be v18.0.0 or higher
```
3. **If still failing:**
```powershell
# Try with legacy peer deps
npm install --legacy-peer-deps
npm run build
```
---
### Issue 5: Python Dependencies Missing
**Symptom:** Log shows errors about missing Python packages (Pillow, cairosvg, etc.)
**Solution:**
1. **Install with KiCAD's Python:**
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -m pip install -r requirements.txt
```
2. **If pip is not available:**
```powershell
# Download get-pip.py
Invoke-WebRequest -Uri https://bootstrap.pypa.io/get-pip.py -OutFile get-pip.py
# Install pip
& "C:\Program Files\KiCad\9.0\bin\python.exe" get-pip.py
# Then install requirements
& "C:\Program Files\KiCad\9.0\bin\python.exe" -m pip install -r requirements.txt
```
---
### Issue 6: Permission Denied Errors
**Symptom:** Cannot write to Program Files or access certain directories
**Solution:**
1. **Run PowerShell as Administrator:**
- Right-click PowerShell icon
- Select "Run as Administrator"
- Navigate to KiCAD-MCP-Server directory
- Run setup again
2. **Or clone to user directory:**
```powershell
cd $HOME\Documents
git clone https://github.com/mixelpixx/KiCAD-MCP-Server.git
cd KiCAD-MCP-Server
.\setup-windows.ps1
```
---
### Issue 7: Path Issues in Configuration
**Symptom:** Config file paths not working
**Common mistakes:**
```json
// ❌ Wrong - single backslashes
"args": ["C:\Users\Name\KiCAD-MCP-Server\dist\index.js"]
// ❌ Wrong - mixed slashes
"args": ["C:\Users/Name\KiCAD-MCP-Server/dist\index.js"]
// ✅ Correct - double backslashes
"args": ["C:\\Users\\Name\\KiCAD-MCP-Server\\dist\\index.js"]
// ✅ Also correct - forward slashes
"args": ["C:/Users/Name/KiCAD-MCP-Server/dist/index.js"]
```
**Solution:** Use either double backslashes `\\` or forward slashes `/` consistently.
---
### Issue 8: Wrong Python Version
**Symptom:** Errors about Python 2.7 or Python 3.6
**Solution:**
KiCAD MCP requires Python 3.10+. KiCAD 9.0 includes Python 3.11, which is perfect.
**Always use KiCAD's bundled Python:**
```json
{
"mcpServers": {
"kicad": {
"command": "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\python\\kicad_interface.py"]
}
}
}
```
This bypasses Node.js and runs Python directly.
---
## Configuration Examples
### For Claude Desktop
Config location: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages",
"NODE_ENV": "production",
"LOG_LEVEL": "info"
}
}
}
}
```
### For Cline (VSCode)
Config location: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
```json
{
"mcpServers": {
"kicad": {
"command": "node",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
},
"description": "KiCAD PCB Design Assistant"
}
}
}
```
### Alternative: Python Direct Mode
If Node.js issues persist, run Python directly:
```json
{
"mcpServers": {
"kicad": {
"command": "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\python\\kicad_interface.py"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
}
}
}
}
```
---
## Manual Testing Steps
### Test 1: Verify KiCAD Python
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c @"
import sys
print(f'Python version: {sys.version}')
import pcbnew
print(f'pcbnew version: {pcbnew.GetBuildVersion()}')
print('SUCCESS!')
"@
```
Expected output:
```
Python version: 3.11.x ...
pcbnew version: 9.0.0
SUCCESS!
```
### Test 2: Verify Node.js
```powershell
node --version # Should be v18.0.0+
npm --version # Should be 9.0.0+
```
### Test 3: Build Project
```powershell
cd C:\Users\YourName\KiCAD-MCP-Server
npm install
npm run build
Test-Path .\dist\index.js # Should output: True
```
### Test 4: Run Server Manually
```powershell
$env:PYTHONPATH = "C:\Program Files\KiCad\9.0\lib\python3\dist-packages"
node .\dist\index.js
```
Expected: Server should start and wait for input (doesn't exit immediately)
**To stop:** Press Ctrl+C
### Test 5: Check Log File
```powershell
# View log file
Get-Content "$env:USERPROFILE\.kicad-mcp\logs\kicad_interface.log" -Tail 50
```
Should show successful initialization with no errors.
---
## Advanced Diagnostics
### Enable Verbose Logging
Add to your MCP config:
```json
{
"env": {
"LOG_LEVEL": "debug",
"PYTHONUNBUFFERED": "1"
}
}
```
### Check Python sys.path
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c @"
import sys
for path in sys.path:
print(path)
"@
```
Should include: `C:\Program Files\KiCad\9.0\lib\python3\dist-packages`
### Test MCP Communication
```powershell
# Start server
$env:PYTHONPATH = "C:\Program Files\KiCad\9.0\lib\python3\dist-packages"
$process = Start-Process -FilePath "node" -ArgumentList ".\dist\index.js" -NoNewWindow -PassThru
# Wait 3 seconds
Start-Sleep -Seconds 3
# Check if still running
if ($process.HasExited) {
Write-Host "Server crashed!" -ForegroundColor Red
Write-Host "Exit code: $($process.ExitCode)"
} else {
Write-Host "Server is running!" -ForegroundColor Green
Stop-Process -Id $process.Id
}
```
---
## Getting Help
If none of the above solutions work:
1. **Run the diagnostic script:**
```powershell
.\setup-windows.ps1
```
Copy the entire output.
2. **Collect log files:**
- MCP log: `%USERPROFILE%\.kicad-mcp\logs\kicad_interface.log`
- Claude Desktop log: `%APPDATA%\Claude\logs\mcp*.log`
3. **Open a GitHub issue:**
- Go to: https://github.com/mixelpixx/KiCAD-MCP-Server/issues
- Title: "Windows Setup Issue: [brief description]"
- Include:
- Windows version (10 or 11)
- Output from setup script
- Log file contents
- Output from manual tests above
---
## Known Limitations on Windows
1. **File paths are case-insensitive** but should match actual casing for best results
2. **Long path support** may be needed for deeply nested projects:
```powershell
# Enable long paths (requires admin)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
```
3. **Windows Defender** may slow down file operations. Add exclusion:
```
Settings → Windows Security → Virus & threat protection → Exclusions
Add: C:\Users\YourName\KiCAD-MCP-Server
```
4. **Antivirus software** may block Python/Node processes. Temporarily disable for testing.
---
## Success Checklist
When everything works, you should have:
- [ ] KiCAD 9.0+ installed at `C:\Program Files\KiCad\9.0`
- [ ] Node.js 18+ installed and in PATH
- [ ] Python can import pcbnew successfully
- [ ] `npm run build` completes without errors
- [ ] `dist\index.js` file exists
- [ ] MCP config file created with correct paths
- [ ] Server starts without immediate crash
- [ ] Log file shows successful initialization
- [ ] Claude Desktop/Cline recognizes the MCP server
- [ ] Can execute: "Create a new KiCAD project"
---
**Last Updated:** 2025-11-05
**Maintained by:** KiCAD MCP Team
For the latest updates, see: https://github.com/mixelpixx/KiCAD-MCP-Server

View File

@@ -1,485 +1,509 @@
# Option 2: Dynamic Library Loading Plan
## Executive Summary
Replace the template-based schematic workflow with dynamic symbol loading from KiCad's installed symbol libraries. This would eliminate the 13-component limitation and provide access to ALL KiCad symbols (~10,000+ symbols from standard libraries).
**Current Status (Option 1):**
- ✅ Template-based approach working
- ✅ 13 component types supported
- ❌ Limited symbol variety
- ❌ Requires manual template updates for new types
**Proposed (Option 2):**
- 🎯 Dynamic loading from `.kicad_sym` library files
- 🎯 Access to ~10,000+ KiCad symbols
- 🎯 No template maintenance required
- 🎯 User can specify any library/symbol combination
---
## Problem Analysis
### kicad-skip Library Limitation
**Core Issue:** kicad-skip **cannot create symbols from scratch**. It can only:
1. Clone existing symbols from a loaded schematic
2. Modify properties of cloned symbols
**Current Workaround:** Pre-load template symbols in schematic file
**Proposed Solution:** Load symbols from KiCad's `.kicad_sym` library files, inject them into the schematic's `lib_symbols` section, then clone from there.
---
## KiCad Symbol Library Architecture
### Symbol Library File Format (`.kicad_sym`)
KiCad symbol libraries are S-expression files containing symbol definitions:
```lisp
(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
(symbol "Device:R"
(pin_numbers hide)
(pin_names (offset 0))
(in_bom yes)
(on_board yes)
(property "Reference" "R" ...)
(property "Value" "R" ...)
;; Graphics definitions
(symbol "R_0_1" ...)
(symbol "R_1_1"
(pin passive line ...)
)
)
(symbol "Device:C" ...)
(symbol "Device:L" ...)
;; ... thousands more
)
```
### Standard KiCad Library Locations
**Linux:**
- System libraries: `/usr/share/kicad/symbols/`
- User libraries: `~/.local/share/kicad/8.0/symbols/` or `~/.config/kicad/8.0/symbols/`
**Windows:**
- System libraries: `C:\Program Files\KiCad\9.0\share\kicad\symbols\`
- User libraries: `%APPDATA%\kicad\8.0\symbols\`
**macOS:**
- System libraries: `/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/`
- User libraries: `~/Library/Preferences/kicad/8.0/symbols/`
### Standard Library Files
Common libraries (each containing 50-500 symbols):
- `Device.kicad_sym` - Passives (R, C, L, D, LED, Crystal, etc.)
- `Connector.kicad_sym` - Connectors (headers, USB, etc.)
- `Connector_Generic.kicad_sym` - Generic connectors
- `Transistor_BJT.kicad_sym` - Bipolar transistors
- `Transistor_FET.kicad_sym` - MOSFETs
- `Amplifier_Operational.kicad_sym` - Op-amps
- `Regulator_Linear.kicad_sym` - Voltage regulators
- `MCU_*.kicad_sym` - Microcontrollers
- `Interface_*.kicad_sym` - Interface ICs
- ... 100+ more libraries
---
## Implementation Strategy
### Phase 1: Library Discovery & Indexing
**Goal:** Build an index of all available symbols and their locations
**Implementation:**
```python
class SymbolLibraryManager:
def __init__(self):
self.library_paths = []
self.symbol_index = {} # {"Device:R": "/path/to/Device.kicad_sym", ...}
def discover_libraries(self):
"""Find all KiCad symbol libraries on the system"""
search_paths = [
"/usr/share/kicad/symbols/",
os.path.expanduser("~/.local/share/kicad/8.0/symbols/"),
os.path.expanduser("~/.config/kicad/8.0/symbols/"),
]
for search_path in search_paths:
if os.path.exists(search_path):
for lib_file in os.listdir(search_path):
if lib_file.endswith('.kicad_sym'):
self.library_paths.append(os.path.join(search_path, lib_file))
def index_symbols(self):
"""Parse all libraries and build symbol index"""
for lib_path in self.library_paths:
lib_name = os.path.basename(lib_path).replace('.kicad_sym', '')
symbols = self._parse_library(lib_path)
for symbol_name in symbols:
full_name = f"{lib_name}:{symbol_name}"
self.symbol_index[full_name] = {
'library': lib_name,
'library_path': lib_path,
'symbol_name': symbol_name
}
def _parse_library(self, lib_path):
"""Parse .kicad_sym file and extract symbol names"""
# Use sexpdata (already a dependency of kicad-skip)
import sexpdata
with open(lib_path, 'r') as f:
data = sexpdata.load(f)
symbols = []
for item in data[2:]: # Skip header
if isinstance(item, list) and item[0] == Symbol('symbol'):
symbol_name = item[1] # e.g., "Device:R"
# Extract just the symbol part after ':'
if ':' in symbol_name:
symbol_name = symbol_name.split(':')[1]
symbols.append(symbol_name)
return symbols
```
### Phase 2: Dynamic Symbol Injection
**Goal:** Load symbol definition from library file and inject into schematic
**Challenge:** kicad-skip works with loaded schematics, but we need to dynamically add symbols to the `lib_symbols` section.
**Solution:** Modify the schematic's S-expression data directly before loading with kicad-skip:
```python
def inject_symbol_into_schematic(schematic_path, library_path, symbol_name):
"""
1. Read schematic S-expression
2. Read library S-expression
3. Extract symbol definition from library
4. Inject into schematic's lib_symbols section
5. Save modified schematic
6. Reload with kicad-skip
"""
import sexpdata
# Load schematic
with open(schematic_path, 'r') as f:
sch_data = sexpdata.load(f)
# Load library
with open(library_path, 'r') as f:
lib_data = sexpdata.load(f)
# Find symbol definition in library
symbol_def = None
for item in lib_data[2:]:
if isinstance(item, list) and item[0] == Symbol('symbol'):
if symbol_name in str(item[1]):
symbol_def = item
break
if not symbol_def:
raise ValueError(f"Symbol {symbol_name} not found in {library_path}")
# Find lib_symbols section in schematic
lib_symbols_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and item[0] == Symbol('lib_symbols'):
lib_symbols_index = i
break
# Inject symbol definition
if lib_symbols_index:
sch_data[lib_symbols_index].append(symbol_def)
# Save modified schematic
with open(schematic_path, 'w') as f:
sexpdata.dump(sch_data, f)
# Reload with kicad-skip
return Schematic(schematic_path)
```
### Phase 3: Template Instance Creation
**Goal:** Create offscreen template instances that can be cloned
**After injection:** Symbol definition is in `lib_symbols`, but we need an instance to clone from:
```python
def create_template_instance(schematic, library_name, symbol_name):
"""
Create an offscreen template instance that can be cloned
Similar to our current _TEMPLATE_R approach
"""
# This requires directly manipulating the S-expression
# Add a symbol instance at offscreen position with special reference
template_ref = f"_TEMPLATE_{library_name}_{symbol_name}"
# Create symbol instance (S-expression)
symbol_instance = [
Symbol('symbol'),
[Symbol('lib_id'), f"{library_name}:{symbol_name}"],
[Symbol('at'), -100, -100 - (len(schematic.symbol) * 10), 0],
[Symbol('unit'), 1],
[Symbol('in_bom'), Symbol('no')],
[Symbol('on_board'), Symbol('no')],
[Symbol('dnp'), Symbol('yes')],
[Symbol('uuid'), str(uuid.uuid4())],
[Symbol('property'), "Reference", template_ref, ...],
# ... more properties
]
# Inject into schematic and reload
# ... (similar to inject_symbol_into_schematic)
return template_ref
```
### Phase 4: User-Facing API
**Goal:** Simple interface for users to add any KiCad symbol
**New MCP Tool: `add_schematic_component_dynamic`**
```python
def add_schematic_component_dynamic(params):
"""
Add component by library:symbol notation
Example:
{
"library": "Device",
"symbol": "R",
"reference": "R1",
"value": "10k",
"x": 100,
"y": 100
}
OR using full notation:
{
"lib_symbol": "Device:R", # Full notation
"reference": "R1",
...
}
"""
lib_symbol = params.get('lib_symbol') or f"{params['library']}:{params['symbol']}"
# 1. Check if symbol is already in schematic's lib_symbols
# 2. If not, inject it from library file
# 3. Create template instance if needed
# 4. Clone template and set properties
return {"success": True, "reference": params['reference']}
```
---
## Advantages Over Template Approach
### ✅ Unlimited Symbol Access
- Access to ~10,000+ standard KiCad symbols
- Support for custom user libraries
- Support for 3rd-party libraries (JLCPCB, Espressif, etc.)
### ✅ No Maintenance Required
- Template doesn't need updates for new component types
- Automatically supports new KiCad library additions
- Works with custom symbol libraries
### ✅ Better User Experience
```
User: "Add an STM32F103C8T6 microcontroller at position 100,100"
AI: *Searches symbol index*
*Finds MCU_ST_STM32F1:STM32F103C8Tx*
*Loads from library*
*Injects into schematic*
*Places component*
✓ Done!
```
### ✅ Flexible Symbol Search
```python
# Find all resistors
symbols = lib_manager.search_symbols(query="resistor")
# Returns: ["Device:R", "Device:R_Small", "Device:R_Network", ...]
# Find all STM32 MCUs
symbols = lib_manager.search_symbols(query="STM32", library="MCU_ST_STM32F1")
```
---
## Challenges & Mitigations
### Challenge 1: S-expression Manipulation Complexity
**Problem:** Directly manipulating S-expression data is error-prone
**Mitigation:**
- Use `sexpdata` library (already a dependency)
- Create helper functions for common operations
- Add comprehensive validation and error handling
- Extensive testing with various symbol types
### Challenge 2: Performance
**Problem:** Loading/reloading schematics after injection could be slow
**Mitigation:**
- **Cache loaded symbols**: Once injected, symbol stays in schematic
- **Batch injection**: Inject multiple symbols at once
- **Lazy loading**: Only inject symbols when first used
### Challenge 3: Symbol Compatibility
**Problem:** Some symbols may have complex pin configurations or multiple units
**Mitigation:**
- Start with simple 2-pin passives (R, C, L)
- Gradually add support for multi-pin ICs
- Handle multi-unit symbols (gates, OpAmp sections) explicitly
- Document supported symbol types
### Challenge 4: Library Version Compatibility
**Problem:** KiCad symbol format may change between versions
**Mitigation:**
- Parse KiCad version from library files
- Version-specific handling if needed
- Fallback to template approach for unsupported formats
---
## Implementation Phases
### Phase A: Proof of Concept (1-2 weeks)
- [ ] Create `SymbolLibraryManager` class
- [ ] Implement library discovery (Linux paths only)
- [ ] Implement symbol indexing
- [ ] Test with Device.kicad_sym (R, C, L)
- [ ] Implement basic S-expression injection
- [ ] Test end-to-end with simple components
### Phase B: Core Functionality (2-3 weeks)
- [ ] Cross-platform library discovery (Windows, macOS)
- [ ] Symbol search functionality
- [ ] Template instance creation automation
- [ ] Multi-pin component support
- [ ] Error handling and validation
- [ ] Unit tests for all operations
### Phase C: MCP Integration (1 week)
- [ ] Create `add_schematic_component_dynamic` tool
- [ ] Update `search_symbols` to use library index
- [ ] Add `list_available_symbols` tool
- [ ] Add `list_symbol_libraries` tool
- [ ] Documentation and examples
### Phase D: Advanced Features (2-3 weeks)
- [ ] Multi-unit symbol support (e.g., quad OpAmps)
- [ ] Custom library registration
- [ ] Symbol caching and optimization
- [ ] 3rd-party library support (JLCPCB, etc.)
- [ ] Symbol preview generation
---
## Migration Strategy
### Backward Compatibility
Keep template-based approach as fallback:
```python
def add_schematic_component(params):
"""Smart component addition with fallback"""
# Try dynamic loading first
try:
if 'library' in params or 'lib_symbol' in params:
return add_schematic_component_dynamic(params)
except Exception as e:
logger.warning(f"Dynamic loading failed: {e}, falling back to template")
# Fallback to template-based
return add_schematic_component_template(params)
```
### Gradual Rollout
1. **Week 1-2:** Implement basic dynamic loading
2. **Week 3-4:** Test with power users, gather feedback
3. **Week 5-6:** Make dynamic loading the default
4. **Week 7+:** Deprecate template-only approach (keep as fallback)
---
## Success Criteria
### Must Have
- [ ] Load symbols from Device.kicad_sym (passives)
- [ ] Support R, C, L, D, LED (5 core types)
- [ ] Cross-platform library discovery
- [ ] Proper error handling
### Should Have
- [ ] Support for all Device.kicad_sym symbols (~50 symbols)
- [ ] Support for Connector.kicad_sym symbols
- [ ] Symbol search by name/keyword
- [ ] Performance: < 1 second per symbol injection
### Nice to Have
- [ ] Support for all standard libraries (~10,000 symbols)
- [ ] Multi-unit symbol support
- [ ] Custom library registration
- [ ] Symbol preview/documentation
---
## Risk Assessment
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| S-expression parsing complexity | High | High | Use proven `sexpdata` library, extensive testing |
| Performance degradation | Medium | Medium | Implement caching, lazy loading |
| KiCad version incompatibility | Low | High | Version detection, format validation |
| Template fallback breaks | Low | Medium | Maintain template approach in parallel |
| User confusion | Medium | Low | Clear documentation, gradual rollout |
---
## Conclusion
Dynamic library loading is **feasible and highly beneficial** for the schematic workflow. While the template-based approach (Option 1) provides immediate value with 13 component types, Option 2 would:
1. **Eliminate the 13-component limitation**
2. **Provide access to 10,000+ KiCad symbols**
3. **Remove manual template maintenance**
4. **Enable true "natural language PCB design"**
**Recommendation:**
- ✅ **Keep Option 1 (expanded template) for immediate use**
- ✅ **Implement Option 2 (dynamic loading) over 6-8 weeks**
- ✅ **Maintain template fallback for compatibility**
This gives users immediate value while we build the robust long-term solution.
---
## References
- [KiCad File Formats Documentation](https://dev-docs.kicad.org/en/file-formats/)
- [kicad-skip GitHub](https://github.com/mvnmgrx/kicad-skip)
- [sexpdata Python Library](https://github.com/jd-boyd/sexpdata)
- [KiCad Symbol Library Format Spec](https://dev-docs.kicad.org/en/file-formats/sexpr-intro/)
# Option 2: Dynamic Library Loading Plan
## Executive Summary
Replace the template-based schematic workflow with dynamic symbol loading from KiCad's installed symbol libraries. This would eliminate the 13-component limitation and provide access to ALL KiCad symbols (~10,000+ symbols from standard libraries).
**Current Status (Option 1):**
- ✅ Template-based approach working
- ✅ 13 component types supported
- ❌ Limited symbol variety
- ❌ Requires manual template updates for new types
**Proposed (Option 2):**
- 🎯 Dynamic loading from `.kicad_sym` library files
- 🎯 Access to ~10,000+ KiCad symbols
- 🎯 No template maintenance required
- 🎯 User can specify any library/symbol combination
---
## Problem Analysis
### kicad-skip Library Limitation
**Core Issue:** kicad-skip **cannot create symbols from scratch**. It can only:
1. Clone existing symbols from a loaded schematic
2. Modify properties of cloned symbols
**Current Workaround:** Pre-load template symbols in schematic file
**Proposed Solution:** Load symbols from KiCad's `.kicad_sym` library files, inject them into the schematic's `lib_symbols` section, then clone from there.
---
## KiCad Symbol Library Architecture
### Symbol Library File Format (`.kicad_sym`)
KiCad symbol libraries are S-expression files containing symbol definitions:
```lisp
(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
(symbol "Device:R"
(pin_numbers hide)
(pin_names (offset 0))
(in_bom yes)
(on_board yes)
(property "Reference" "R" ...)
(property "Value" "R" ...)
;; Graphics definitions
(symbol "R_0_1" ...)
(symbol "R_1_1"
(pin passive line ...)
)
)
(symbol "Device:C" ...)
(symbol "Device:L" ...)
;; ... thousands more
)
```
### Standard KiCad Library Locations
**Linux:**
- System libraries: `/usr/share/kicad/symbols/`
- User libraries: `~/.local/share/kicad/8.0/symbols/` or `~/.config/kicad/8.0/symbols/`
**Windows:**
- System libraries: `C:\Program Files\KiCad\9.0\share\kicad\symbols\`
- User libraries: `%APPDATA%\kicad\8.0\symbols\`
**macOS:**
- System libraries: `/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/`
- User libraries: `~/Library/Preferences/kicad/8.0/symbols/`
### Standard Library Files
Common libraries (each containing 50-500 symbols):
- `Device.kicad_sym` - Passives (R, C, L, D, LED, Crystal, etc.)
- `Connector.kicad_sym` - Connectors (headers, USB, etc.)
- `Connector_Generic.kicad_sym` - Generic connectors
- `Transistor_BJT.kicad_sym` - Bipolar transistors
- `Transistor_FET.kicad_sym` - MOSFETs
- `Amplifier_Operational.kicad_sym` - Op-amps
- `Regulator_Linear.kicad_sym` - Voltage regulators
- `MCU_*.kicad_sym` - Microcontrollers
- `Interface_*.kicad_sym` - Interface ICs
- ... 100+ more libraries
---
## Implementation Strategy
### Phase 1: Library Discovery & Indexing
**Goal:** Build an index of all available symbols and their locations
**Implementation:**
```python
class SymbolLibraryManager:
def __init__(self):
self.library_paths = []
self.symbol_index = {} # {"Device:R": "/path/to/Device.kicad_sym", ...}
def discover_libraries(self):
"""Find all KiCad symbol libraries on the system"""
search_paths = [
"/usr/share/kicad/symbols/",
os.path.expanduser("~/.local/share/kicad/8.0/symbols/"),
os.path.expanduser("~/.config/kicad/8.0/symbols/"),
]
for search_path in search_paths:
if os.path.exists(search_path):
for lib_file in os.listdir(search_path):
if lib_file.endswith('.kicad_sym'):
self.library_paths.append(os.path.join(search_path, lib_file))
def index_symbols(self):
"""Parse all libraries and build symbol index"""
for lib_path in self.library_paths:
lib_name = os.path.basename(lib_path).replace('.kicad_sym', '')
symbols = self._parse_library(lib_path)
for symbol_name in symbols:
full_name = f"{lib_name}:{symbol_name}"
self.symbol_index[full_name] = {
'library': lib_name,
'library_path': lib_path,
'symbol_name': symbol_name
}
def _parse_library(self, lib_path):
"""Parse .kicad_sym file and extract symbol names"""
# Use sexpdata (already a dependency of kicad-skip)
import sexpdata
with open(lib_path, 'r') as f:
data = sexpdata.load(f)
symbols = []
for item in data[2:]: # Skip header
if isinstance(item, list) and item[0] == Symbol('symbol'):
symbol_name = item[1] # e.g., "Device:R"
# Extract just the symbol part after ':'
if ':' in symbol_name:
symbol_name = symbol_name.split(':')[1]
symbols.append(symbol_name)
return symbols
```
### Phase 2: Dynamic Symbol Injection
**Goal:** Load symbol definition from library file and inject into schematic
**Challenge:** kicad-skip works with loaded schematics, but we need to dynamically add symbols to the `lib_symbols` section.
**Solution:** Modify the schematic's S-expression data directly before loading with kicad-skip:
```python
def inject_symbol_into_schematic(schematic_path, library_path, symbol_name):
"""
1. Read schematic S-expression
2. Read library S-expression
3. Extract symbol definition from library
4. Inject into schematic's lib_symbols section
5. Save modified schematic
6. Reload with kicad-skip
"""
import sexpdata
# Load schematic
with open(schematic_path, 'r') as f:
sch_data = sexpdata.load(f)
# Load library
with open(library_path, 'r') as f:
lib_data = sexpdata.load(f)
# Find symbol definition in library
symbol_def = None
for item in lib_data[2:]:
if isinstance(item, list) and item[0] == Symbol('symbol'):
if symbol_name in str(item[1]):
symbol_def = item
break
if not symbol_def:
raise ValueError(f"Symbol {symbol_name} not found in {library_path}")
# Find lib_symbols section in schematic
lib_symbols_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and item[0] == Symbol('lib_symbols'):
lib_symbols_index = i
break
# Inject symbol definition
if lib_symbols_index:
sch_data[lib_symbols_index].append(symbol_def)
# Save modified schematic
with open(schematic_path, 'w') as f:
sexpdata.dump(sch_data, f)
# Reload with kicad-skip
return Schematic(schematic_path)
```
### Phase 3: Template Instance Creation
**Goal:** Create offscreen template instances that can be cloned
**After injection:** Symbol definition is in `lib_symbols`, but we need an instance to clone from:
```python
def create_template_instance(schematic, library_name, symbol_name):
"""
Create an offscreen template instance that can be cloned
Similar to our current _TEMPLATE_R approach
"""
# This requires directly manipulating the S-expression
# Add a symbol instance at offscreen position with special reference
template_ref = f"_TEMPLATE_{library_name}_{symbol_name}"
# Create symbol instance (S-expression)
symbol_instance = [
Symbol('symbol'),
[Symbol('lib_id'), f"{library_name}:{symbol_name}"],
[Symbol('at'), -100, -100 - (len(schematic.symbol) * 10), 0],
[Symbol('unit'), 1],
[Symbol('in_bom'), Symbol('no')],
[Symbol('on_board'), Symbol('no')],
[Symbol('dnp'), Symbol('yes')],
[Symbol('uuid'), str(uuid.uuid4())],
[Symbol('property'), "Reference", template_ref, ...],
# ... more properties
]
# Inject into schematic and reload
# ... (similar to inject_symbol_into_schematic)
return template_ref
```
### Phase 4: User-Facing API
**Goal:** Simple interface for users to add any KiCad symbol
**New MCP Tool: `add_schematic_component_dynamic`**
```python
def add_schematic_component_dynamic(params):
"""
Add component by library:symbol notation
Example:
{
"library": "Device",
"symbol": "R",
"reference": "R1",
"value": "10k",
"x": 100,
"y": 100
}
OR using full notation:
{
"lib_symbol": "Device:R", # Full notation
"reference": "R1",
...
}
"""
lib_symbol = params.get('lib_symbol') or f"{params['library']}:{params['symbol']}"
# 1. Check if symbol is already in schematic's lib_symbols
# 2. If not, inject it from library file
# 3. Create template instance if needed
# 4. Clone template and set properties
return {"success": True, "reference": params['reference']}
```
---
## Advantages Over Template Approach
### ✅ Unlimited Symbol Access
- Access to ~10,000+ standard KiCad symbols
- Support for custom user libraries
- Support for 3rd-party libraries (JLCPCB, Espressif, etc.)
### ✅ No Maintenance Required
- Template doesn't need updates for new component types
- Automatically supports new KiCad library additions
- Works with custom symbol libraries
### ✅ Better User Experience
```
User: "Add an STM32F103C8T6 microcontroller at position 100,100"
AI: *Searches symbol index*
*Finds MCU_ST_STM32F1:STM32F103C8Tx*
*Loads from library*
*Injects into schematic*
*Places component*
✓ Done!
```
### ✅ Flexible Symbol Search
```python
# Find all resistors
symbols = lib_manager.search_symbols(query="resistor")
# Returns: ["Device:R", "Device:R_Small", "Device:R_Network", ...]
# Find all STM32 MCUs
symbols = lib_manager.search_symbols(query="STM32", library="MCU_ST_STM32F1")
```
---
## Challenges & Mitigations
### Challenge 1: S-expression Manipulation Complexity
**Problem:** Directly manipulating S-expression data is error-prone
**Mitigation:**
- Use `sexpdata` library (already a dependency)
- Create helper functions for common operations
- Add comprehensive validation and error handling
- Extensive testing with various symbol types
### Challenge 2: Performance
**Problem:** Loading/reloading schematics after injection could be slow
**Mitigation:**
- **Cache loaded symbols**: Once injected, symbol stays in schematic
- **Batch injection**: Inject multiple symbols at once
- **Lazy loading**: Only inject symbols when first used
### Challenge 3: Symbol Compatibility
**Problem:** Some symbols may have complex pin configurations or multiple units
**Mitigation:**
- Start with simple 2-pin passives (R, C, L)
- Gradually add support for multi-pin ICs
- Handle multi-unit symbols (gates, OpAmp sections) explicitly
- Document supported symbol types
### Challenge 4: Library Version Compatibility
**Problem:** KiCad symbol format may change between versions
**Mitigation:**
- Parse KiCad version from library files
- Version-specific handling if needed
- Fallback to template approach for unsupported formats
---
## Implementation Phases
### Phase A: Proof of Concept (1-2 weeks)
- [ ] Create `SymbolLibraryManager` class
- [ ] Implement library discovery (Linux paths only)
- [ ] Implement symbol indexing
- [ ] Test with Device.kicad_sym (R, C, L)
- [ ] Implement basic S-expression injection
- [ ] Test end-to-end with simple components
### Phase B: Core Functionality (2-3 weeks)
- [ ] Cross-platform library discovery (Windows, macOS)
- [ ] Symbol search functionality
- [ ] Template instance creation automation
- [ ] Multi-pin component support
- [ ] Error handling and validation
- [ ] Unit tests for all operations
### Phase C: MCP Integration (1 week)
- [ ] Create `add_schematic_component_dynamic` tool
- [ ] Update `search_symbols` to use library index
- [ ] Add `list_available_symbols` tool
- [ ] Add `list_symbol_libraries` tool
- [ ] Documentation and examples
### Phase D: Advanced Features (2-3 weeks)
- [ ] Multi-unit symbol support (e.g., quad OpAmps)
- [ ] Custom library registration
- [ ] Symbol caching and optimization
- [ ] 3rd-party library support (JLCPCB, etc.)
- [ ] Symbol preview generation
---
## Migration Strategy
### Backward Compatibility
Keep template-based approach as fallback:
```python
def add_schematic_component(params):
"""Smart component addition with fallback"""
# Try dynamic loading first
try:
if 'library' in params or 'lib_symbol' in params:
return add_schematic_component_dynamic(params)
except Exception as e:
logger.warning(f"Dynamic loading failed: {e}, falling back to template")
# Fallback to template-based
return add_schematic_component_template(params)
```
### Gradual Rollout
1. **Week 1-2:** Implement basic dynamic loading
2. **Week 3-4:** Test with power users, gather feedback
3. **Week 5-6:** Make dynamic loading the default
4. **Week 7+:** Deprecate template-only approach (keep as fallback)
---
## Success Criteria
### Must Have
- [ ] Load symbols from Device.kicad_sym (passives)
- [ ] Support R, C, L, D, LED (5 core types)
- [ ] Cross-platform library discovery
- [ ] Proper error handling
### Should Have
- [ ] Support for all Device.kicad_sym symbols (~50 symbols)
- [ ] Support for Connector.kicad_sym symbols
- [ ] Symbol search by name/keyword
- [ ] Performance: < 1 second per symbol injection
### Nice to Have
- [ ] Support for all standard libraries (~10,000 symbols)
- [ ] Multi-unit symbol support
- [ ] Custom library registration
- [ ] Symbol preview/documentation
---
## Risk Assessment
| Risk | Probability | Impact | Mitigation |
| ------------------------------- | ----------- | ------ | ------------------------------------------------ |
| S-expression parsing complexity | High | High | Use proven `sexpdata` library, extensive testing |
| Performance degradation | Medium | Medium | Implement caching, lazy loading |
| KiCad version incompatibility | Low | High | Version detection, format validation |
| Template fallback breaks | Low | Medium | Maintain template approach in parallel |
| User confusion | Medium | Low | Clear documentation, gradual rollout |
---
## Conclusion
Dynamic library loading is **feasible and highly beneficial** for the schematic workflow. While the template-based approach (Option 1) provides immediate value with 13 component types, Option 2 would:
1. **Eliminate the 13-component limitation**
2. **Provide access to 10,000+ KiCad symbols**
3. **Remove manual template maintenance**
4. **Enable true "natural language PCB design"**
**Recommendation:**
- ✅ **Keep Option 1 (expanded template) for immediate use**
- ✅ **Implement Option 2 (dynamic loading) over 6-8 weeks**
- ✅ **Maintain template fallback for compatibility**
This gives users immediate value while we build the robust long-term solution.
---
## References
- [KiCad File Formats Documentation](https://dev-docs.kicad.org/en/file-formats/)
- [kicad-skip GitHub](https://github.com/mvnmgrx/kicad-skip)
- [sexpdata Python Library](https://github.com/jd-boyd/sexpdata)
- [KiCad Symbol Library Format Spec](https://dev-docs.kicad.org/en/file-formats/sexpr-intro/)

View File

@@ -1,390 +1,413 @@
# Dynamic Symbol Loading - Implementation Status
**Date:** 2026-01-10
**Status:** Phase A-C - ✅ **COMPLETE AND PRODUCTION-READY!**
## 🚀 BREAKTHROUGH: Full MCP Integration Complete!
We went from **planning** to **full production integration** in a single session!
**Phase A** (Proof of Concept): ✅ Complete - Core dynamic loading works
**Phase B** (Core Functionality): ✅ ~60% Complete - Cross-platform, caching working
**Phase C** (MCP Integration): ✅ **COMPLETE!** - Fully integrated through MCP interface
The dynamic symbol loading is now **FULLY OPERATIONAL** and accessible through the MCP interface!
---
## What's Working (Core Functionality)
### ✅ Symbol Extraction
- Parse `.kicad_sym` library files using S-expression parser
- Extract specific symbol definitions by name
- Cache parsed libraries for performance
- Tested with Device.kicad_sym (533 symbols)
### ✅ S-Expression Manipulation
- Load schematic files as S-expression trees
- Inject symbol definitions into `lib_symbols` section
- Preserve schematic structure and formatting
- Write modified schematics back to disk
### ✅ Template Instance Creation
- Create offscreen template instances at negative Y coordinates
- Generate unique UUIDs for each template
- Set proper properties (Reference, Value, Footprint, Datasheet)
- Templates marked as: `in_bom: no`, `on_board: no`, `dnp: yes`
### ✅ Component Cloning
- kicad-skip successfully clones from dynamic templates
- Components inherit symbol structure from injected definitions
- Properties can be modified after cloning
- Full integration with existing ComponentManager
### ✅ Cross-Platform Library Discovery
- Linux: `/usr/share/kicad/symbols`, `~/.local/share/kicad/*/symbols`
- Windows: `C:/Program Files/KiCad/*/share/kicad/symbols`
- macOS: `/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols`
- Environment variable support: `KICAD9_SYMBOL_DIR`, etc.
---
## Test Results
### End-to-End Test (Successful)
**Test:** Load 5 symbols dynamically and create components
```python
Symbols Tested:
- Device:R ✓ Injected, template created, cloned successfully
- Device:C ✓ Injected, template created, cloned successfully
- Device:LED ✓ Injected, template created, cloned successfully
- Device:L ✓ Injected, template created, cloned successfully
- Device:D ✓ Injected, template created, cloned successfully
Results:
✓ All 5 symbols extracted from Device.kicad_sym
✓ All 5 symbol definitions injected into schematic
✓ All 5 template instances created
✓ kicad-skip loaded modified schematic without errors
✓ Components successfully cloned from dynamic templates
```
### Performance Metrics
- **Library parsing:** ~0.3s for Device.kicad_sym (first time)
- **Library parsing:** ~0.001s (cached)
- **Symbol extraction:** <0.01s
- **Symbol injection:** ~0.05s
- **Template creation:** ~0.02s
- **Total per symbol:** ~0.08s (first time), ~0.03s (cached)
**Conclusion:** Fast enough for real-time use!
---
## Code Structure
### New File: `python/commands/dynamic_symbol_loader.py`
**Class:** `DynamicSymbolLoader`
**Key Methods:**
```python
# Library Discovery
find_kicad_symbol_libraries() -> List[Path]
find_library_file(library_name: str) -> Optional[Path]
# Parsing & Extraction
parse_library_file(library_path: Path) -> List # Returns S-expression
extract_symbol_definition(library_path: Path, symbol_name: str) -> Optional[List]
# Injection & Template Creation
inject_symbol_into_schematic(schematic_path: Path, library: str, symbol: str) -> bool
create_template_instance(schematic_path: Path, library: str, symbol: str) -> str
# Complete Workflow
load_symbol_dynamically(schematic_path: Path, library: str, symbol: str) -> str
```
**Caching:**
- `library_cache`: Parsed library files (path → S-expression data)
- `symbol_cache`: Extracted symbols (lib:symbol → symbol definition)
---
## What's NOT Yet Done (Integration Layer)
### ⏳ MCP Tool Integration
- Need to create `add_schematic_component_dynamic` MCP tool
- Wire dynamic loader through MCP interface (has schematic path)
- Update existing `add_schematic_component` to auto-detect and use dynamic loading
### ⏳ Smart Symbol Discovery
- Automatic library detection from component type
- Search across all libraries for symbol names
- Fuzzy matching for symbol names
### ⏳ Advanced Features
- Multi-unit symbol support (e.g., quad op-amps)
- Pin configuration handling
- Custom library registration
- Symbol preview generation
---
## Technical Challenges Solved
### Challenge 1: S-Expression Parsing
**Problem:** KiCad files use Lisp-style S-expressions, complex to parse
**Solution:** Used `sexpdata` library (already a dependency of kicad-skip)
**Result:** ✅ Robust parsing with proper handling of nested structures
### Challenge 2: Symbol Structure Complexity
**Problem:** Symbols have complex nested structure with multiple sub-symbols
**Solution:** Extract entire symbol tree as-is, inject without modification
**Result:** ✅ Preserves all symbol details (graphics, pins, properties)
### Challenge 3: kicad-skip Integration
**Problem:** kicad-skip can only clone existing symbols, can't create from scratch
**Solution:** Inject symbol into lib_symbols, create template instance, then clone
**Result:** ✅ Seamless integration, kicad-skip unaware of dynamic loading
### Challenge 4: Schematic File Path Access
**Problem:** kicad-skip Schematic object doesn't expose file path
**Solution:** Pass schematic path explicitly at MCP interface layer
**Result:** ⏳ Workaround identified, integration pending
---
## Example Usage (Current)
### Direct Python Usage
```python
from commands.dynamic_symbol_loader import DynamicSymbolLoader
from pathlib import Path
# Initialize loader
loader = DynamicSymbolLoader()
# Load a symbol dynamically
schematic_path = Path("/path/to/project.kicad_sch")
template_ref = loader.load_symbol_dynamically(
schematic_path,
library_name="Device",
symbol_name="R"
)
# Now use template_ref with kicad-skip to clone components
# template_ref will be something like "_TEMPLATE_Device_R"
```
### Future MCP Tool Usage
```typescript
// This is what it WILL look like after integration:
await mcpServer.callTool("add_schematic_component_dynamic", {
library: "MCU_ST_STM32F1",
symbol: "STM32F103C8Tx",
reference: "U1",
x: 100,
y: 100,
footprint: "Package_QFP:LQFP-48_7x7mm_P0.5mm"
});
// The tool will:
// 1. Check if symbol exists in static templates (no)
// 2. Dynamically load from MCU_ST_STM32F1.kicad_sym
// 3. Inject symbol definition
// 4. Create template instance
// 5. Clone to create actual component
// 6. Set properties (reference, position, footprint)
// All of this happens AUTOMATICALLY!
```
---
## Comparison: Before vs After
| Feature | Static Templates (Current) | Dynamic Loading (New) |
|---------|---------------------------|----------------------|
| **Available Symbols** | 13 types | ~10,000+ types |
| **Maintenance** | Manual template updates | Zero maintenance |
| **Custom Symbols** | Not supported | Fully supported |
| **3rd Party Libs** | Not supported | Fully supported |
| **Setup Time** | Pre-created templates | On-demand loading |
| **Performance** | Instant (pre-loaded) | ~80ms first time, ~30ms cached |
| **Flexibility** | Limited to template list | Any .kicad_sym file |
---
## Phase Progress
### ✅ Phase A: Proof of Concept (COMPLETE)
- [x] Create `DynamicSymbolLoader` class
- [x] Implement library discovery (Linux paths)
- [x] Implement symbol indexing
- [x] Test with Device.kicad_sym (R, C, L)
- [x] Implement basic S-expression injection
- [x] Test end-to-end with simple components
**Time Estimate:** 1-2 weeks
**Actual Time:** 4 hours! 🎉
### ⏳ Phase B: Core Functionality (IN PROGRESS)
- [ ] Cross-platform library discovery (Windows, macOS)
- [ ] Symbol search functionality
- [ ] Template instance creation automation
- [ ] Multi-pin component support
- [ ] Error handling and validation
- [ ] Unit tests for all operations
**Time Estimate:** 2-3 weeks
**Progress:** 25% (cross-platform discovery done)
### ✅ Phase C: MCP Integration (COMPLETE!)
- [x] Integrate dynamic loading into `add_schematic_component` MCP handler
- [x] Implement save → inject → reload → clone orchestration
- [x] Add schematic_path parameter throughout component chain
- [x] Smart detection of when dynamic loading is needed
- [x] Proper error handling and fallback to static templates
- [x] End-to-end integration testing (100% passing!)
**Time Estimate:** 1 week
**Actual Time:** 2 hours! 🎉
**Status:** PRODUCTION READY!
**What Works Now:**
- ✅ Users can add ANY symbol from KiCad libraries via MCP interface
- ✅ Automatic detection and dynamic loading
- ✅ Seamless fallback to static templates
- ✅ Response includes dynamic_loading_used flag and symbol_source info
- ✅ Compatible with all existing MCP clients
### ⏸️ Phase D: Advanced Features (PENDING)
- [ ] Multi-unit symbol support (e.g., quad OpAmps)
- [ ] Custom library registration
- [ ] Symbol caching and optimization
- [ ] 3rd-party library support (JLCPCB, etc.)
- [ ] Symbol preview generation
**Time Estimate:** 2-3 weeks
---
## Next Immediate Steps
1. **Wire Through MCP Interface** (2-3 hours)
- Update `python/kicad_interface.py` to pass schematic path
- Create wrapper function that combines dynamic loading + cloning
- Test with MCP client
2. **Create MCP Tool** (1-2 hours)
- Define `add_schematic_component_dynamic` tool schema
- Register in tool registry
- Add to documentation
3. **Integration Testing** (1-2 hours)
- Test with Claude Desktop/Cline
- Test with complex symbols (ICs, connectors)
- Verify error handling
**Total Time to Full Integration:** ~6 hours
---
## Success Metrics
### Phase A Metrics (All Achieved ✅)
- [x] Load symbols from Device.kicad_sym (passives)
- [x] Support R, C, L, D, LED (5 core types)
- [x] Cross-platform library discovery
- [x] Proper error handling
### Phase B Metrics (Target)
- [ ] Support for all Device.kicad_sym symbols (~500 symbols)
- [ ] Support for Connector.kicad_sym symbols
- [ ] Symbol search by name/keyword
- [ ] Performance: < 1 second per symbol injection
### Overall Success Criteria
- [ ] Access to all standard libraries (~10,000 symbols)
- [ ] Works on Linux, Windows, macOS
- [ ] <100ms latency for cached symbols
- [ ] Zero template maintenance required
- [ ] Backward compatible with static templates
---
## Risks & Mitigations
| Risk | Status | Mitigation |
|------|--------|------------|
| S-expression complexity | ✅ RESOLVED | Used proven sexpdata library |
| Performance degradation | ✅ RESOLVED | Caching works great (<30ms cached) |
| KiCad version compatibility | ⚠️ TESTING | Version detection, format validation |
| Template fallback breaks | ✅ PREVENTED | Maintained static templates in parallel |
| Integration complexity | ⏳ IN PROGRESS | Clean separation of concerns |
---
## Conclusion
**We did it!** The core dynamic symbol loading is **fully functional**. This is a game-changer for the KiCAD MCP Server:
- ✅ No more 13-component limitation
- ✅ Access to thousands of symbols
- ✅ Zero template maintenance
- ✅ Production-ready performance
**The hardest part is DONE.** What remains is integration work (wiring through MCP interface), which is straightforward plumbing.
**Estimated time to full production deployment:** 6-8 hours of integration work.
---
## 🎯 MCP Integration Test Results (2026-01-10)
**Test:** Full MCP interface with dynamic symbol loading
**Status:** ✅ **100% PASSING**
### Test Components
| Component | Type | Library | Dynamic? | Result |
|-----------|------|---------|----------|--------|
| R1 | Resistor | Device | Yes | ✅ Added successfully |
| C1 | Capacitor | Device | Yes | ✅ Added successfully |
| BT1 | Battery | Device | **Yes** | ✅ **Dynamic load + clone** |
| F1 | Fuse | Device | **Yes** | ✅ **Dynamic load + clone** |
| T1 | Transformer_1P_1S | Device | **Yes** | ✅ **Dynamic load + clone** |
### Results Summary
- **Static templates:** 2/2 successful (R, C)
- **Dynamic loading:** 3/3 successful (Battery, Fuse, Transformer)
- **Total success rate:** 5/5 (100%)
- **Templates created:** 5 (all persisted correctly)
- **Reload orchestration:** Working perfectly
- **Error handling:** No failures, all fallbacks untested (no errors!)
### What This Means
✅ Users can now add **ANY symbol from ~10,000 KiCad symbols** through the MCP interface!
✅ The system automatically:
1. Detects if symbol needs dynamic loading
2. Saves current schematic
3. Injects symbol definition from library
4. Creates template instance
5. Reloads schematic
6. Clones template to create component
7. Saves final result
**Zero configuration required** - just specify library and symbol name!
---
**Amazing progress! From planning to full production in one session!** 🚀 🎉
# Dynamic Symbol Loading - Implementation Status
**Date:** 2026-01-10
**Status:** Phase A-C - ✅ **COMPLETE AND PRODUCTION-READY!**
## 🚀 BREAKTHROUGH: Full MCP Integration Complete!
We went from **planning** to **full production integration** in a single session!
**Phase A** (Proof of Concept): ✅ Complete - Core dynamic loading works
**Phase B** (Core Functionality): ✅ ~60% Complete - Cross-platform, caching working
**Phase C** (MCP Integration): ✅ **COMPLETE!** - Fully integrated through MCP interface
The dynamic symbol loading is now **FULLY OPERATIONAL** and accessible through the MCP interface!
---
## What's Working (Core Functionality)
### ✅ Symbol Extraction
- Parse `.kicad_sym` library files using S-expression parser
- Extract specific symbol definitions by name
- Cache parsed libraries for performance
- Tested with Device.kicad_sym (533 symbols)
### ✅ S-Expression Manipulation
- Load schematic files as S-expression trees
- Inject symbol definitions into `lib_symbols` section
- Preserve schematic structure and formatting
- Write modified schematics back to disk
### ✅ Template Instance Creation
- Create offscreen template instances at negative Y coordinates
- Generate unique UUIDs for each template
- Set proper properties (Reference, Value, Footprint, Datasheet)
- Templates marked as: `in_bom: no`, `on_board: no`, `dnp: yes`
### ✅ Component Cloning
- kicad-skip successfully clones from dynamic templates
- Components inherit symbol structure from injected definitions
- Properties can be modified after cloning
- Full integration with existing ComponentManager
### ✅ Cross-Platform Library Discovery
- Linux: `/usr/share/kicad/symbols`, `~/.local/share/kicad/*/symbols`
- Windows: `C:/Program Files/KiCad/*/share/kicad/symbols`
- macOS: `/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols`
- Environment variable support: `KICAD9_SYMBOL_DIR`, etc.
---
## Test Results
### End-to-End Test (Successful)
**Test:** Load 5 symbols dynamically and create components
```python
Symbols Tested:
- Device:R ✓ Injected, template created, cloned successfully
- Device:C ✓ Injected, template created, cloned successfully
- Device:LED ✓ Injected, template created, cloned successfully
- Device:L ✓ Injected, template created, cloned successfully
- Device:D ✓ Injected, template created, cloned successfully
Results:
✓ All 5 symbols extracted from Device.kicad_sym
✓ All 5 symbol definitions injected into schematic
✓ All 5 template instances created
✓ kicad-skip loaded modified schematic without errors
✓ Components successfully cloned from dynamic templates
```
### Performance Metrics
- **Library parsing:** ~0.3s for Device.kicad_sym (first time)
- **Library parsing:** ~0.001s (cached)
- **Symbol extraction:** <0.01s
- **Symbol injection:** ~0.05s
- **Template creation:** ~0.02s
- **Total per symbol:** ~0.08s (first time), ~0.03s (cached)
**Conclusion:** Fast enough for real-time use!
---
## Code Structure
### New File: `python/commands/dynamic_symbol_loader.py`
**Class:** `DynamicSymbolLoader`
**Key Methods:**
```python
# Library Discovery
find_kicad_symbol_libraries() -> List[Path]
find_library_file(library_name: str) -> Optional[Path]
# Parsing & Extraction
parse_library_file(library_path: Path) -> List # Returns S-expression
extract_symbol_definition(library_path: Path, symbol_name: str) -> Optional[List]
# Injection & Template Creation
inject_symbol_into_schematic(schematic_path: Path, library: str, symbol: str) -> bool
create_template_instance(schematic_path: Path, library: str, symbol: str) -> str
# Complete Workflow
load_symbol_dynamically(schematic_path: Path, library: str, symbol: str) -> str
```
**Caching:**
- `library_cache`: Parsed library files (path → S-expression data)
- `symbol_cache`: Extracted symbols (lib:symbol → symbol definition)
---
## What's NOT Yet Done (Integration Layer)
### ⏳ MCP Tool Integration
- Need to create `add_schematic_component_dynamic` MCP tool
- Wire dynamic loader through MCP interface (has schematic path)
- Update existing `add_schematic_component` to auto-detect and use dynamic loading
### ⏳ Smart Symbol Discovery
- Automatic library detection from component type
- Search across all libraries for symbol names
- Fuzzy matching for symbol names
### ⏳ Advanced Features
- Multi-unit symbol support (e.g., quad op-amps)
- Pin configuration handling
- Custom library registration
- Symbol preview generation
---
## Technical Challenges Solved
### Challenge 1: S-Expression Parsing
**Problem:** KiCad files use Lisp-style S-expressions, complex to parse
**Solution:** Used `sexpdata` library (already a dependency of kicad-skip)
**Result:** ✅ Robust parsing with proper handling of nested structures
### Challenge 2: Symbol Structure Complexity
**Problem:** Symbols have complex nested structure with multiple sub-symbols
**Solution:** Extract entire symbol tree as-is, inject without modification
**Result:** ✅ Preserves all symbol details (graphics, pins, properties)
### Challenge 3: kicad-skip Integration
**Problem:** kicad-skip can only clone existing symbols, can't create from scratch
**Solution:** Inject symbol into lib_symbols, create template instance, then clone
**Result:** ✅ Seamless integration, kicad-skip unaware of dynamic loading
### Challenge 4: Schematic File Path Access
**Problem:** kicad-skip Schematic object doesn't expose file path
**Solution:** Pass schematic path explicitly at MCP interface layer
**Result:** ⏳ Workaround identified, integration pending
---
## Example Usage (Current)
### Direct Python Usage
```python
from commands.dynamic_symbol_loader import DynamicSymbolLoader
from pathlib import Path
# Initialize loader
loader = DynamicSymbolLoader()
# Load a symbol dynamically
schematic_path = Path("/path/to/project.kicad_sch")
template_ref = loader.load_symbol_dynamically(
schematic_path,
library_name="Device",
symbol_name="R"
)
# Now use template_ref with kicad-skip to clone components
# template_ref will be something like "_TEMPLATE_Device_R"
```
### Future MCP Tool Usage
```typescript
// This is what it WILL look like after integration:
await mcpServer.callTool("add_schematic_component_dynamic", {
library: "MCU_ST_STM32F1",
symbol: "STM32F103C8Tx",
reference: "U1",
x: 100,
y: 100,
footprint: "Package_QFP:LQFP-48_7x7mm_P0.5mm",
});
// The tool will:
// 1. Check if symbol exists in static templates (no)
// 2. Dynamically load from MCU_ST_STM32F1.kicad_sym
// 3. Inject symbol definition
// 4. Create template instance
// 5. Clone to create actual component
// 6. Set properties (reference, position, footprint)
// All of this happens AUTOMATICALLY!
```
---
## Comparison: Before vs After
| Feature | Static Templates (Current) | Dynamic Loading (New) |
| --------------------- | -------------------------- | ------------------------------ |
| **Available Symbols** | 13 types | ~10,000+ types |
| **Maintenance** | Manual template updates | Zero maintenance |
| **Custom Symbols** | Not supported | Fully supported |
| **3rd Party Libs** | Not supported | Fully supported |
| **Setup Time** | Pre-created templates | On-demand loading |
| **Performance** | Instant (pre-loaded) | ~80ms first time, ~30ms cached |
| **Flexibility** | Limited to template list | Any .kicad_sym file |
---
## Phase Progress
### ✅ Phase A: Proof of Concept (COMPLETE)
- [x] Create `DynamicSymbolLoader` class
- [x] Implement library discovery (Linux paths)
- [x] Implement symbol indexing
- [x] Test with Device.kicad_sym (R, C, L)
- [x] Implement basic S-expression injection
- [x] Test end-to-end with simple components
**Time Estimate:** 1-2 weeks
**Actual Time:** 4 hours! 🎉
### ⏳ Phase B: Core Functionality (IN PROGRESS)
- [ ] Cross-platform library discovery (Windows, macOS)
- [ ] Symbol search functionality
- [ ] Template instance creation automation
- [ ] Multi-pin component support
- [ ] Error handling and validation
- [ ] Unit tests for all operations
**Time Estimate:** 2-3 weeks
**Progress:** 25% (cross-platform discovery done)
### ✅ Phase C: MCP Integration (COMPLETE!)
- [x] Integrate dynamic loading into `add_schematic_component` MCP handler
- [x] Implement save → inject → reload → clone orchestration
- [x] Add schematic_path parameter throughout component chain
- [x] Smart detection of when dynamic loading is needed
- [x] Proper error handling and fallback to static templates
- [x] End-to-end integration testing (100% passing!)
**Time Estimate:** 1 week
**Actual Time:** 2 hours! 🎉
**Status:** PRODUCTION READY!
**What Works Now:**
- ✅ Users can add ANY symbol from KiCad libraries via MCP interface
- ✅ Automatic detection and dynamic loading
- ✅ Seamless fallback to static templates
- ✅ Response includes dynamic_loading_used flag and symbol_source info
- ✅ Compatible with all existing MCP clients
### ⏸️ Phase D: Advanced Features (PENDING)
- [ ] Multi-unit symbol support (e.g., quad OpAmps)
- [ ] Custom library registration
- [ ] Symbol caching and optimization
- [ ] 3rd-party library support (JLCPCB, etc.)
- [ ] Symbol preview generation
**Time Estimate:** 2-3 weeks
---
## Next Immediate Steps
1. **Wire Through MCP Interface** (2-3 hours)
- Update `python/kicad_interface.py` to pass schematic path
- Create wrapper function that combines dynamic loading + cloning
- Test with MCP client
2. **Create MCP Tool** (1-2 hours)
- Define `add_schematic_component_dynamic` tool schema
- Register in tool registry
- Add to documentation
3. **Integration Testing** (1-2 hours)
- Test with Claude Desktop/Cline
- Test with complex symbols (ICs, connectors)
- Verify error handling
**Total Time to Full Integration:** ~6 hours
---
## Success Metrics
### Phase A Metrics (All Achieved ✅)
- [x] Load symbols from Device.kicad_sym (passives)
- [x] Support R, C, L, D, LED (5 core types)
- [x] Cross-platform library discovery
- [x] Proper error handling
### Phase B Metrics (Target)
- [ ] Support for all Device.kicad_sym symbols (~500 symbols)
- [ ] Support for Connector.kicad_sym symbols
- [ ] Symbol search by name/keyword
- [ ] Performance: < 1 second per symbol injection
### Overall Success Criteria
- [ ] Access to all standard libraries (~10,000 symbols)
- [ ] Works on Linux, Windows, macOS
- [ ] <100ms latency for cached symbols
- [ ] Zero template maintenance required
- [ ] Backward compatible with static templates
---
## Risks & Mitigations
| Risk | Status | Mitigation |
| --------------------------- | -------------- | --------------------------------------- |
| S-expression complexity | ✅ RESOLVED | Used proven sexpdata library |
| Performance degradation | ✅ RESOLVED | Caching works great (<30ms cached) |
| KiCad version compatibility | ⚠️ TESTING | Version detection, format validation |
| Template fallback breaks | ✅ PREVENTED | Maintained static templates in parallel |
| Integration complexity | ⏳ IN PROGRESS | Clean separation of concerns |
---
## Conclusion
**We did it!** The core dynamic symbol loading is **fully functional**. This is a game-changer for the KiCAD MCP Server:
- ✅ No more 13-component limitation
- ✅ Access to thousands of symbols
- ✅ Zero template maintenance
- ✅ Production-ready performance
**The hardest part is DONE.** What remains is integration work (wiring through MCP interface), which is straightforward plumbing.
**Estimated time to full production deployment:** 6-8 hours of integration work.
---
## 🎯 MCP Integration Test Results (2026-01-10)
**Test:** Full MCP interface with dynamic symbol loading
**Status:** ✅ **100% PASSING**
### Test Components
| Component | Type | Library | Dynamic? | Result |
| --------- | ----------------- | ------- | -------- | --------------------------- |
| R1 | Resistor | Device | Yes | ✅ Added successfully |
| C1 | Capacitor | Device | Yes | ✅ Added successfully |
| BT1 | Battery | Device | **Yes** | ✅ **Dynamic load + clone** |
| F1 | Fuse | Device | **Yes** | ✅ **Dynamic load + clone** |
| T1 | Transformer_1P_1S | Device | **Yes** | ✅ **Dynamic load + clone** |
### Results Summary
- **Static templates:** 2/2 successful (R, C)
- **Dynamic loading:** 3/3 successful (Battery, Fuse, Transformer)
- **Total success rate:** 5/5 (100%)
- **Templates created:** 5 (all persisted correctly)
- **Reload orchestration:** Working perfectly
- **Error handling:** No failures, all fallbacks untested (no errors!)
### What This Means
✅ Users can now add **ANY symbol from ~10,000 KiCad symbols** through the MCP interface!
✅ The system automatically:
1. Detects if symbol needs dynamic loading
2. Saves current schematic
3. Injects symbol definition from library
4. Creates template instance
5. Reloads schematic
6. Clones template to create component
7. Saves final result
**Zero configuration required** - just specify library and symbol name!
---
**Amazing progress! From planning to full production in one session!** 🚀 🎉

View File

@@ -1,477 +1,493 @@
# KiCAD IPC API Migration Plan
**Status:** 📋 Planning
**Target Completion:** Week 2-3 (November 1-8, 2025)
**Priority:** 🔴 **CRITICAL** - Current SWIG API deprecated
---
## Executive Summary
The current KiCAD MCP Server uses SWIG-based Python bindings (`import pcbnew`) which are **deprecated as of KiCAD 9.0** and will be **removed in KiCAD 10.0**. We must migrate to the official **KiCAD IPC API** to future-proof the project.
### Why Migrate?
| SWIG API (Current) | IPC API (Future) |
|-------------------|------------------|
| ❌ Deprecated | ✅ Official & Supported |
| ❌ Will be removed in KiCAD 10.0 | ✅ Long-term stability |
| ❌ Python-only | ✅ Multi-language (Python, JS, etc.) |
| ❌ Direct linking | ✅ Inter-process communication |
| ⚠️ Synchronous only | ✅ Async support |
| ⚠️ No versioning | ✅ Protocol Buffers versioning |
**Decision: Migrate immediately to avoid technical debt**
---
## IPC API Overview
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ TypeScript MCP Server (Node.js) │
└──────────────────────┬──────────────────────────────────────┘
│ JSON over stdin/stdout
┌──────────────────────▼──────────────────────────────────────┐
│ Python Interface Layer │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ KiCAD API Abstraction (NEW) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────────┘
│ kicad-python library
┌──────────────────────▼──────────────────────────────────────┐
│ KiCAD IPC Server (Protocol Buffers) │
│ Running inside KiCAD Process │
└──────────────────────┬──────────────────────────────────────┘
│ UNIX Sockets / Named Pipes
┌──────────────────────▼──────────────────────────────────────┐
│ KiCAD 9.0+ Application │
└─────────────────────────────────────────────────────────────┘
```
### Key Differences
1. **KiCAD Must Be Running**
- SWIG: Can run headless, no KiCAD GUI needed
- IPC: Requires KiCAD running with IPC server enabled
2. **Communication Method**
- SWIG: Direct Python module import
- IPC: Socket-based RPC (Remote Procedure Call)
3. **API Structure**
- SWIG: `board.SetSize(width, height)`
- IPC: `kicad.get_board().set_size(width, height)`
---
## Migration Strategy
### Phase 1: Research & Preparation (Days 1-2)
**Goals:**
- Understand kicad-python library
- Test IPC connection
- Document API differences
**Tasks:**
```bash
# Install kicad-python
pip install kicad-python>=0.5.0
# Test basic connection
python3 << EOF
from kicad import KiCad
kicad = KiCad()
print(f"Connected to KiCAD: {kicad.check_version()}")
EOF
# Read official documentation
# https://docs.kicad.org/kicad-python-main
```
**Deliverables:**
- [ ] kicad-python installed and tested
- [ ] Connection test script
- [ ] API comparison document (SWIG vs IPC)
---
### Phase 2: Abstraction Layer (Days 3-4)
**Goal:** Create an abstraction layer to support both APIs during transition
**File Structure:**
```
python/kicad_api/
├── __init__.py
├── base.py # Abstract base class
├── ipc_backend.py # NEW: IPC API implementation
├── swig_backend.py # Legacy SWIG implementation
── factory.py # Backend selector
```
**Abstract Interface:**
```python
# python/kicad_api/base.py
from abc import ABC, abstractmethod
from typing import Optional
from pathlib import Path
class KiCADBackend(ABC):
"""Abstract base class for KiCAD API backends"""
@abstractmethod
def connect(self) -> bool:
"""Connect to KiCAD"""
pass
@abstractmethod
def disconnect(self) -> None:
"""Disconnect from KiCAD"""
pass
@abstractmethod
def is_connected(self) -> bool:
"""Check if connected"""
pass
@abstractmethod
def create_project(self, path: Path, name: str) -> dict:
"""Create a new KiCAD project"""
pass
@abstractmethod
def open_project(self, path: Path) -> dict:
"""Open existing project"""
pass
@abstractmethod
def get_board(self) -> 'BoardAPI':
"""Get board API"""
pass
# ... more abstract methods
```
**IPC Implementation:**
```python
# python/kicad_api/ipc_backend.py
from kicad import KiCad
from kicad_api.base import KiCADBackend
class IPCBackend(KiCADBackend):
"""KiCAD IPC API backend"""
def __init__(self):
self.kicad = None
def connect(self) -> bool:
"""Connect to running KiCAD instance"""
try:
self.kicad = KiCad()
# Verify connection
version = self.kicad.check_version()
logger.info(f"Connected to KiCAD via IPC: {version}")
return True
except Exception as e:
logger.error(f"Failed to connect via IPC: {e}")
return False
def create_project(self, path: Path, name: str) -> dict:
"""Create project using IPC API"""
# Implementation here
pass
```
**Backend Factory:**
```python
# python/kicad_api/factory.py
from typing import Optional
from kicad_api.base import KiCADBackend
from kicad_api.ipc_backend import IPCBackend
from kicad_api.swig_backend import SWIGBackend
def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
"""
Create appropriate KiCAD backend
Args:
backend_type: 'ipc', 'swig', or None for auto-detect
Returns:
KiCADBackend instance
"""
if backend_type == 'ipc':
return IPCBackend()
elif backend_type == 'swig':
return SWIGBackend()
else:
# Auto-detect: Try IPC first, fall back to SWIG
try:
backend = IPCBackend()
if backend.connect():
return backend
except ImportError:
pass
# Fall back to SWIG
return SWIGBackend()
```
**Deliverables:**
- [ ] Abstract base class defined
- [ ] IPC backend implemented
- [ ] SWIG backend (wrapper around existing code)
- [ ] Factory with auto-detection
---
### Phase 3: Port Core Modules (Days 5-8)
**Migration Order** (by complexity):
1. **project.py** (Simple - good starting point)
- Create, open, save projects
- Estimated: 2 hours
2. **board.py** (Medium - board properties)
- Set size, layers, outline
- Estimated: 4 hours
3. **component.py** (Complex - many operations)
- Place, move, rotate, delete
- Component arrays and alignment
- Estimated: 8 hours
4. **routing.py** (Complex - trace routing)
- Nets, traces, vias
- Copper pours, differential pairs
- Estimated: 8 hours
5. **design_rules.py** (Medium - DRC)
- Set rules, run DRC
- Estimated: 4 hours
6. **export.py** (Medium - file exports)
- Gerber, PDF, SVG, 3D
- Estimated: 4 hours
**Total Estimated Time: 30 hours (~4 days)**
**Migration Template:**
```python
# OLD (SWIG)
import pcbnew
board = pcbnew.LoadBoard(filename)
board.SetBoardSize(width, height)
# NEW (IPC via abstraction)
from kicad_api import create_backend
backend = create_backend('ipc')
backend.connect()
board_api = backend.get_board()
board_api.set_size(width, height)
```
**Deliverables:**
- [ ] project.py migrated
- [ ] board.py migrated
- [ ] component.py migrated
- [ ] routing.py migrated
- [ ] design_rules.py migrated
- [ ] export.py migrated
---
### Phase 4: Testing & Validation (Days 9-10)
**Testing Strategy:**
1. **Unit Tests**
```python
@pytest.mark.parametrize("backend_type", ["ipc", "swig"])
def test_create_project(backend_type):
backend = create_backend(backend_type)
result = backend.create_project(Path("/tmp/test"), "TestProject")
assert result["success"] is True
```
2. **Integration Tests**
- Run side-by-side: IPC vs SWIG
- Compare outputs for identical operations
- Verify file compatibility
3. **Performance Benchmarks**
```python
# Measure: operations/second for each backend
# Expected: IPC slightly slower due to IPC overhead
```
**Deliverables:**
- [ ] 50+ unit tests passing for IPC backend
- [ ] Side-by-side comparison tests
- [ ] Performance benchmarks documented
---
## API Comparison Reference
### Project Operations
| Operation | SWIG | IPC |
|-----------|------|-----|
| Create project | Custom file creation | `kicad.create_project()` |
| Open project | `pcbnew.LoadBoard()` | `kicad.open_project()` |
| Save project | `board.Save()` | `board.save()` |
### Board Operations
| Operation | SWIG | IPC |
|-----------|------|-----|
| Get board | `pcbnew.LoadBoard()` | `kicad.get_board()` |
| Set size | `board.SetBoardSize()` | `board.set_size()` |
| Add layer | `board.GetLayerCount()` | `board.layers.add()` |
### Component Operations
| Operation | SWIG | IPC |
|-----------|------|-----|
| Place component | `pcbnew.FOOTPRINT()` | `board.add_footprint()` |
| Move component | `fp.SetPosition()` | `footprint.set_position()` |
| Rotate component | `fp.SetOrientation()` | `footprint.set_rotation()` |
### Routing Operations
| Operation | SWIG | IPC |
|-----------|------|-----|
| Add net | `board.GetNetCount()` | `board.nets.add()` |
| Route trace | `pcbnew.PCB_TRACK()` | `board.add_track()` |
| Add via | `pcbnew.PCB_VIA()` | `board.add_via()` |
---
## Configuration Changes
### Update requirements.txt
```diff
+ # KiCAD IPC API (official Python bindings)
+ kicad-python>=0.5.0
# Legacy SWIG support (for backward compatibility)
kicad-skip>=0.1.0
```
### Environment Variables
```bash
# Enable IPC API in KiCAD preferences
# Preferences > Plugins > Enable IPC API Server
# Set backend preference (optional)
export KICAD_BACKEND=ipc # or 'swig' or 'auto'
```
### User Migration Guide
Create `docs/MIGRATING_TO_IPC.md`:
- How to enable IPC in KiCAD
- What changes for users
- Troubleshooting IPC connection issues
---
## Rollback Plan
If IPC migration fails:
1. **Keep SWIG backend** - Already abstracted
2. **Default to SWIG** - Change factory auto-detection
3. **Document limitations** - Note that SWIG will be removed eventually
4. **Plan retry** - Schedule IPC migration for later
---
## Success Criteria
- [ ] ✅ All existing functionality works with IPC backend
- [ ] ✅ Tests pass with both IPC and SWIG backends
- [ ] ✅ Performance acceptable (< 20% slowdown vs SWIG)
- [ ] ✅ Documentation updated
- [ ] ✅ Migration guide created
- [ ] ✅ User-facing tools work without changes
---
## Timeline
| Week | Days | Tasks |
|------|------|-------|
| **Week 2** | Mon-Tue | Research, install kicad-python, test connection |
| | Wed-Thu | Build abstraction layer |
| | Fri | Port project.py and board.py |
| **Week 3** | Mon-Tue | Port component.py and routing.py |
| | Wed | Port design_rules.py and export.py |
| | Thu-Fri | Testing, validation, documentation |
---
## Resources
- **Official Docs:** https://docs.kicad.org/kicad-python-main
- **kicad-python PyPI:** https://pypi.org/project/kicad-python/
- **IPC API Spec:** https://dev-docs.kicad.org/en/apis-and-binding/ipc-api/
- **Protocol Buffers:** Used by IPC for message format
---
## Open Questions
1. **How to handle KiCAD not running?**
- Option A: Auto-launch KiCAD in background
- Option B: Require user to launch KiCAD first
- Option C: Fall back to SWIG if IPC unavailable
- **Decision: Option C for now, A later**
2. **Connection management**
- Should we keep connection open or connect per-operation?
- **Decision: Keep alive with reconnect logic**
3. **Performance vs reliability**
- IPC has overhead but more stable
- **Decision: Reliability > performance**
---
## Next Steps (This Week)
1. **Install kicad-python**
```bash
pip install kicad-python
```
2. **Test IPC connection**
```bash
# Launch KiCAD
# Enable IPC in preferences
python3 -c "from kicad import KiCad; k=KiCad(); print(k.check_version())"
```
3. **Create abstraction layer structure**
```bash
mkdir -p python/kicad_api
touch python/kicad_api/{__init__,base,ipc_backend,swig_backend,factory}.py
```
4. **Begin project.py migration**
- Start with simplest module
- Establish patterns for others
---
**Prepared by:** Claude Code
**Last Updated:** October 25, 2025
**Status:** 📋 Ready to execute
# KiCAD IPC API Migration Plan
**Status:** 📋 Planning
**Target Completion:** Week 2-3 (November 1-8, 2025)
**Priority:** 🔴 **CRITICAL** - Current SWIG API deprecated
---
## Executive Summary
The current KiCAD MCP Server uses SWIG-based Python bindings (`import pcbnew`) which are **deprecated as of KiCAD 9.0** and will be **removed in KiCAD 10.0**. We must migrate to the official **KiCAD IPC API** to future-proof the project.
### Why Migrate?
| SWIG API (Current) | IPC API (Future) |
| -------------------------------- | ------------------------------------ |
| ❌ Deprecated | ✅ Official & Supported |
| ❌ Will be removed in KiCAD 10.0 | ✅ Long-term stability |
| ❌ Python-only | ✅ Multi-language (Python, JS, etc.) |
| ❌ Direct linking | ✅ Inter-process communication |
| ⚠️ Synchronous only | ✅ Async support |
| ⚠️ No versioning | ✅ Protocol Buffers versioning |
**Decision: Migrate immediately to avoid technical debt**
---
## IPC API Overview
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ TypeScript MCP Server (Node.js) │
└──────────────────────┬──────────────────────────────────────┘
│ JSON over stdin/stdout
┌──────────────────────▼──────────────────────────────────────┐
│ Python Interface Layer │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ KiCAD API Abstraction (NEW) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────────┘
│ kicad-python library
┌──────────────────────▼──────────────────────────────────────┐
│ KiCAD IPC Server (Protocol Buffers) │
│ Running inside KiCAD Process │
└──────────────────────┬──────────────────────────────────────┘
│ UNIX Sockets / Named Pipes
┌──────────────────────▼──────────────────────────────────────┐
│ KiCAD 9.0+ Application │
└─────────────────────────────────────────────────────────────┘
```
### Key Differences
1. **KiCAD Must Be Running**
- SWIG: Can run headless, no KiCAD GUI needed
- IPC: Requires KiCAD running with IPC server enabled
2. **Communication Method**
- SWIG: Direct Python module import
- IPC: Socket-based RPC (Remote Procedure Call)
3. **API Structure**
- SWIG: `board.SetSize(width, height)`
- IPC: `kicad.get_board().set_size(width, height)`
---
## Migration Strategy
### Phase 1: Research & Preparation (Days 1-2)
**Goals:**
- Understand kicad-python library
- Test IPC connection
- Document API differences
**Tasks:**
```bash
# Install kicad-python
pip install kicad-python>=0.5.0
# Test basic connection
python3 << EOF
from kicad import KiCad
kicad = KiCad()
print(f"Connected to KiCAD: {kicad.check_version()}")
EOF
# Read official documentation
# https://docs.kicad.org/kicad-python-main
```
**Deliverables:**
- [ ] kicad-python installed and tested
- [ ] Connection test script
- [ ] API comparison document (SWIG vs IPC)
---
### Phase 2: Abstraction Layer (Days 3-4)
**Goal:** Create an abstraction layer to support both APIs during transition
**File Structure:**
```
python/kicad_api/
── __init__.py
├── base.py # Abstract base class
├── ipc_backend.py # NEW: IPC API implementation
├── swig_backend.py # Legacy SWIG implementation
└── factory.py # Backend selector
```
**Abstract Interface:**
```python
# python/kicad_api/base.py
from abc import ABC, abstractmethod
from typing import Optional
from pathlib import Path
class KiCADBackend(ABC):
"""Abstract base class for KiCAD API backends"""
@abstractmethod
def connect(self) -> bool:
"""Connect to KiCAD"""
pass
@abstractmethod
def disconnect(self) -> None:
"""Disconnect from KiCAD"""
pass
@abstractmethod
def is_connected(self) -> bool:
"""Check if connected"""
pass
@abstractmethod
def create_project(self, path: Path, name: str) -> dict:
"""Create a new KiCAD project"""
pass
@abstractmethod
def open_project(self, path: Path) -> dict:
"""Open existing project"""
pass
@abstractmethod
def get_board(self) -> 'BoardAPI':
"""Get board API"""
pass
# ... more abstract methods
```
**IPC Implementation:**
```python
# python/kicad_api/ipc_backend.py
from kicad import KiCad
from kicad_api.base import KiCADBackend
class IPCBackend(KiCADBackend):
"""KiCAD IPC API backend"""
def __init__(self):
self.kicad = None
def connect(self) -> bool:
"""Connect to running KiCAD instance"""
try:
self.kicad = KiCad()
# Verify connection
version = self.kicad.check_version()
logger.info(f"Connected to KiCAD via IPC: {version}")
return True
except Exception as e:
logger.error(f"Failed to connect via IPC: {e}")
return False
def create_project(self, path: Path, name: str) -> dict:
"""Create project using IPC API"""
# Implementation here
pass
```
**Backend Factory:**
```python
# python/kicad_api/factory.py
from typing import Optional
from kicad_api.base import KiCADBackend
from kicad_api.ipc_backend import IPCBackend
from kicad_api.swig_backend import SWIGBackend
def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
"""
Create appropriate KiCAD backend
Args:
backend_type: 'ipc', 'swig', or None for auto-detect
Returns:
KiCADBackend instance
"""
if backend_type == 'ipc':
return IPCBackend()
elif backend_type == 'swig':
return SWIGBackend()
else:
# Auto-detect: Try IPC first, fall back to SWIG
try:
backend = IPCBackend()
if backend.connect():
return backend
except ImportError:
pass
# Fall back to SWIG
return SWIGBackend()
```
**Deliverables:**
- [ ] Abstract base class defined
- [ ] IPC backend implemented
- [ ] SWIG backend (wrapper around existing code)
- [ ] Factory with auto-detection
---
### Phase 3: Port Core Modules (Days 5-8)
**Migration Order** (by complexity):
1. **project.py** (Simple - good starting point)
- Create, open, save projects
- Estimated: 2 hours
2. **board.py** (Medium - board properties)
- Set size, layers, outline
- Estimated: 4 hours
3. **component.py** (Complex - many operations)
- Place, move, rotate, delete
- Component arrays and alignment
- Estimated: 8 hours
4. **routing.py** (Complex - trace routing)
- Nets, traces, vias
- Copper pours, differential pairs
- Estimated: 8 hours
5. **design_rules.py** (Medium - DRC)
- Set rules, run DRC
- Estimated: 4 hours
6. **export.py** (Medium - file exports)
- Gerber, PDF, SVG, 3D
- Estimated: 4 hours
**Total Estimated Time: 30 hours (~4 days)**
**Migration Template:**
```python
# OLD (SWIG)
import pcbnew
board = pcbnew.LoadBoard(filename)
board.SetBoardSize(width, height)
# NEW (IPC via abstraction)
from kicad_api import create_backend
backend = create_backend('ipc')
backend.connect()
board_api = backend.get_board()
board_api.set_size(width, height)
```
**Deliverables:**
- [ ] project.py migrated
- [ ] board.py migrated
- [ ] component.py migrated
- [ ] routing.py migrated
- [ ] design_rules.py migrated
- [ ] export.py migrated
---
### Phase 4: Testing & Validation (Days 9-10)
**Testing Strategy:**
1. **Unit Tests**
```python
@pytest.mark.parametrize("backend_type", ["ipc", "swig"])
def test_create_project(backend_type):
backend = create_backend(backend_type)
result = backend.create_project(Path("/tmp/test"), "TestProject")
assert result["success"] is True
```
2. **Integration Tests**
- Run side-by-side: IPC vs SWIG
- Compare outputs for identical operations
- Verify file compatibility
3. **Performance Benchmarks**
```python
# Measure: operations/second for each backend
# Expected: IPC slightly slower due to IPC overhead
```
**Deliverables:**
- [ ] 50+ unit tests passing for IPC backend
- [ ] Side-by-side comparison tests
- [ ] Performance benchmarks documented
---
## API Comparison Reference
### Project Operations
| Operation | SWIG | IPC |
| -------------- | -------------------- | ------------------------ |
| Create project | Custom file creation | `kicad.create_project()` |
| Open project | `pcbnew.LoadBoard()` | `kicad.open_project()` |
| Save project | `board.Save()` | `board.save()` |
### Board Operations
| Operation | SWIG | IPC |
| --------- | ----------------------- | -------------------- |
| Get board | `pcbnew.LoadBoard()` | `kicad.get_board()` |
| Set size | `board.SetBoardSize()` | `board.set_size()` |
| Add layer | `board.GetLayerCount()` | `board.layers.add()` |
### Component Operations
| Operation | SWIG | IPC |
| ---------------- | --------------------- | -------------------------- |
| Place component | `pcbnew.FOOTPRINT()` | `board.add_footprint()` |
| Move component | `fp.SetPosition()` | `footprint.set_position()` |
| Rotate component | `fp.SetOrientation()` | `footprint.set_rotation()` |
### Routing Operations
| Operation | SWIG | IPC |
| ----------- | --------------------- | ------------------- |
| Add net | `board.GetNetCount()` | `board.nets.add()` |
| Route trace | `pcbnew.PCB_TRACK()` | `board.add_track()` |
| Add via | `pcbnew.PCB_VIA()` | `board.add_via()` |
---
## Configuration Changes
### Update requirements.txt
```diff
+ # KiCAD IPC API (official Python bindings)
+ kicad-python>=0.5.0
# Legacy SWIG support (for backward compatibility)
kicad-skip>=0.1.0
```
### Environment Variables
```bash
# Enable IPC API in KiCAD preferences
# Preferences > Plugins > Enable IPC API Server
# Set backend preference (optional)
export KICAD_BACKEND=ipc # or 'swig' or 'auto'
```
### User Migration Guide
Create `docs/MIGRATING_TO_IPC.md`:
- How to enable IPC in KiCAD
- What changes for users
- Troubleshooting IPC connection issues
---
## Rollback Plan
If IPC migration fails:
1. **Keep SWIG backend** - Already abstracted
2. **Default to SWIG** - Change factory auto-detection
3. **Document limitations** - Note that SWIG will be removed eventually
4. **Plan retry** - Schedule IPC migration for later
---
## Success Criteria
- [ ] ✅ All existing functionality works with IPC backend
- [ ] ✅ Tests pass with both IPC and SWIG backends
- [ ] ✅ Performance acceptable (< 20% slowdown vs SWIG)
- [ ] ✅ Documentation updated
- [ ] ✅ Migration guide created
- [ ] ✅ User-facing tools work without changes
---
## Timeline
| Week | Days | Tasks |
| ---------- | ------- | ----------------------------------------------- |
| **Week 2** | Mon-Tue | Research, install kicad-python, test connection |
| | Wed-Thu | Build abstraction layer |
| | Fri | Port project.py and board.py |
| **Week 3** | Mon-Tue | Port component.py and routing.py |
| | Wed | Port design_rules.py and export.py |
| | Thu-Fri | Testing, validation, documentation |
---
## Resources
- **Official Docs:** https://docs.kicad.org/kicad-python-main
- **kicad-python PyPI:** https://pypi.org/project/kicad-python/
- **IPC API Spec:** https://dev-docs.kicad.org/en/apis-and-binding/ipc-api/
- **Protocol Buffers:** Used by IPC for message format
---
## Open Questions
1. **How to handle KiCAD not running?**
- Option A: Auto-launch KiCAD in background
- Option B: Require user to launch KiCAD first
- Option C: Fall back to SWIG if IPC unavailable
- **Decision: Option C for now, A later**
2. **Connection management**
- Should we keep connection open or connect per-operation?
- **Decision: Keep alive with reconnect logic**
3. **Performance vs reliability**
- IPC has overhead but more stable
- **Decision: Reliability > performance**
---
## Next Steps (This Week)
1. **Install kicad-python**
```bash
pip install kicad-python
```
2. **Test IPC connection**
```bash
# Launch KiCAD
# Enable IPC in preferences
python3 -c "from kicad import KiCad; k=KiCad(); print(k.check_version())"
```
3. **Create abstraction layer structure**
```bash
mkdir -p python/kicad_api
touch python/kicad_api/{__init__,base,ipc_backend,swig_backend,factory}.py
```
4. **Begin project.py migration**
- Start with simplest module
- Establish patterns for others
---
**Prepared by:** Claude Code
**Last Updated:** October 25, 2025
**Status:** 📋 Ready to execute

18
docs/archive/README.md Normal file
View File

@@ -0,0 +1,18 @@
# Archived Documentation
This directory contains historical planning and session documents from the KiCAD MCP Server development. These documents record the design decisions and implementation progress for features that are now complete.
They are preserved for historical reference but are no longer maintained. For current documentation, see the [Documentation Index](../INDEX.md).
## Contents
- **SCHEMATIC_WIRING_PLAN.md** - Original plan for the intelligent wiring system (completed v2.1.0)
- **SCHEMATIC_WORKFLOW_FIX.md** - Fix for broken schematic workflow, Issue #26 (completed v2.1.0)
- **DYNAMIC_LIBRARY_LOADING_PLAN.md** - Plan for dynamic symbol loading (completed v2.1.0)
- **DYNAMIC_LOADING_STATUS.md** - Status tracking for dynamic symbol loader (completed v2.1.0)
- **JLCPCB_INTEGRATION_PLAN.md** - Original JLCPCB integration plan (completed v2.1.0)
- **ROUTER_IMPLEMENTATION_STATUS.md** - Router pattern implementation progress (completed v2.0.0)
- **IPC_API_MIGRATION_PLAN.md** - IPC backend migration plan (completed v2.0.0)
- **BUILD_AND_TEST_SESSION.md** - Early build and test session notes
- **WEEK1_SESSION1_SUMMARY.md** - Week 1 development session 1 notes
- **WEEK1_SESSION2_SUMMARY.md** - Week 1 development session 2 notes

View File

@@ -1,222 +1,241 @@
# Router Implementation Status
## ✅ Phase 1 Complete: Foundation & Infrastructure
**Date:** December 28, 2025
### What Was Implemented
#### 1. Tool Registry (`src/tools/registry.ts`)
- ✅ Complete tool categorization (59 tools → 7 categories)
- ✅ Direct tools list (12 high-frequency tools)
- ✅ Category lookup maps for O(1) access
- ✅ Tool search functionality
- ✅ Registry statistics and metadata
#### 2. Router Tools (`src/tools/router.ts`)
- ✅ `list_tool_categories` - Browse all categories
- ✅ `get_category_tools` - View tools in a category
- ✅ `execute_tool` - Execute any routed tool
- ✅ `search_tools` - Search tools by keyword
#### 3. Server Integration (`src/server.ts`)
- ✅ Router tools registered at server startup
- ✅ All tools remain functional (backwards compatible)
- ✅ Logging added for router pattern status
#### 4. Documentation
- ✅ `TOOL_INVENTORY.md` - Complete tool catalog
- ✅ `ROUTER_ARCHITECTURE.md` - Design specification
- ✅ `ROUTER_IMPLEMENTATION_STATUS.md` - This file
### Current State
**Status:** ✅ **Router Infrastructure Complete**
**Build:** ✅ Compiles successfully (`npm run build`)
**Tool Count:**
- Total Tools: 59 (ALL still registered and visible)
- Direct Tools: 12
- Routed Tools: 47
- Router Tools: 4
- **Currently Visible to Claude:** 63 tools (59 + 4 router)
**Token Impact:**
- **Current:** ~42K tokens (still showing all tools)
- **Target:** ~12K tokens (Phase 2 optimization)
- **Potential Savings:** ~30K tokens (71% reduction)
## 🔄 Phase 2: Token Optimization (Next Step)
### Objective
Hide routed tools from Claude's context while keeping them accessible via `execute_tool`.
### Two Approaches
#### Option A: Registration Filtering (Recommended)
Modify tool registration to conditionally register tools based on whether they're in the direct list.
**Changes needed:**
1. Update each `register*Tools` function to check `isDirectTool()`
2. Only call `server.tool()` for direct tools
3. Routed tools remain accessible via `execute_tool` calling `callKicadScript`
**Pros:**
- Clean separation
- True token savings
- No behavior changes
**Cons:**
- Requires modifying 9 tool files
#### Option B: MCP Filter (If Supported)
If MCP SDK supports tool filtering/hiding, use that instead.
**Pros:**
- No tool file changes
- Centralized control
**Cons:**
- May not be supported by SDK
- Needs investigation
### Implementation Plan for Phase 2
1. **Create Helper Function** (`src/tools/conditional-register.ts`)
```typescript
export function registerToolConditionally(
server: McpServer,
toolName: string,
definition: ToolDefinition,
handler: Function
) {
if (isDirectTool(toolName)) {
// Register with MCP (visible to Claude)
server.tool(toolName, definition, handler);
} else {
// Register handler for execute_tool (hidden from Claude)
registerToolHandler(toolName, handler);
}
}
```
2. **Update Tool Registration Functions**
Modify each `register*Tools` function to use conditional registration.
3. **Test**
- Verify direct tools work normally
- Verify routed tools work via `execute_tool`
- Verify token count reduction
4. **Measure Impact**
Count tools visible to Claude before/after.
## 📊 Categories & Distribution
| Category | Tools | Description |
|----------|-------|-------------|
| **board** | 9 | Board configuration, layers, zones, visualization |
| **component** | 8 | Advanced component operations |
| **export** | 8 | Manufacturing file generation |
| **drc** | 9 | Design rule checking & validation |
| **schematic** | 9 | Schematic editor operations |
| **library** | 4 | Footprint library access |
| **routing** | 3 | Advanced routing (vias, copper pours) |
| **TOTAL** | **47** | **Routed tools** |
| **direct** | **12** | **Always visible tools** |
| **router** | **4** | **Discovery tools** |
## 🧪 Testing the Router
### Test 1: List Categories
```
User: "What tool categories are available?"
Expected: Claude calls list_tool_categories
Result: Returns 7 categories with descriptions
```
### Test 2: Browse Category
```
User: "What export tools are available?"
Expected: Claude calls get_category_tools({ category: "export" })
Result: Returns 8 export tools
```
### Test 3: Search Tools
```
User: "How do I export gerber files?"
Expected: Claude calls search_tools({ query: "gerber" })
Result: Finds export_gerber in export category
```
### Test 4: Execute Tool
```
User: "Export gerbers to ./output"
Expected: Claude calls execute_tool({
tool_name: "export_gerber",
params: { outputDir: "./output" }
})
Result: Executes via router, returns gerber export result
```
## 📝 Benefits Achieved (Phase 1)
1. ✅ **Foundation Ready**: All infrastructure in place
2. ✅ **Organized**: 59 tools categorized into logical groups
3. ✅ **Discoverable**: Tools easily found via search/browse
4. ✅ **Backwards Compatible**: All existing tools still work
5. ✅ **Extensible**: Easy to add new tools and categories
6. ✅ **Documented**: Complete architecture and usage docs
## 🚀 Next Actions
1. **Optional: Complete Phase 2** (Token Optimization)
- Implement conditional registration
- Hide routed tools from context
- Achieve 71% token reduction
2. **Or: Ship Phase 1 As-Is**
- Router tools work perfectly now
- Users can discover and execute tools
- Optimization can be done later
- No breaking changes
## 📚 Related Files
- `src/tools/registry.ts` - Tool registry and categories
- `src/tools/router.ts` - Router tool implementations
- `src/server.ts` - Server integration
- `docs/TOOL_INVENTORY.md` - Complete tool list
- `docs/ROUTER_ARCHITECTURE.md` - Design specification
- `docs/mcp-router-guide.md` - Original implementation guide
## 💡 Usage Example
```typescript
// User: "I need to export gerber files"
// Claude's interaction:
// 1. Sees "export" and "gerber" keywords
// 2. Calls search_tools({ query: "gerber" })
// → Returns: { category: "export", tool: "export_gerber", ... }
// 3. Calls execute_tool({
// tool_name: "export_gerber",
// params: { outputDir: "./gerbers" }
// })
// → Executes and returns result
// 4. "I've exported your Gerber files to ./gerbers/"
```
## Status Summary
✅ **Router Pattern: IMPLEMENTED**
✅ **Build: PASSING**
✅ **Backwards Compatible: YES**
⏳ **Token Optimization: PENDING (Phase 2)**
The router infrastructure is complete and functional. The system now supports tool discovery and organized access to all 59 tools. Phase 2 optimization (hiding routed tools) can be implemented when ready for maximum token savings.
# Router Implementation Status
## ✅ Phase 1 Complete: Foundation & Infrastructure
**Date:** December 28, 2025
### What Was Implemented
#### 1. Tool Registry (`src/tools/registry.ts`)
- ✅ Complete tool categorization (59 tools → 7 categories)
- ✅ Direct tools list (12 high-frequency tools)
- ✅ Category lookup maps for O(1) access
- ✅ Tool search functionality
- ✅ Registry statistics and metadata
#### 2. Router Tools (`src/tools/router.ts`)
- ✅ `list_tool_categories` - Browse all categories
- ✅ `get_category_tools` - View tools in a category
- ✅ `execute_tool` - Execute any routed tool
- ✅ `search_tools` - Search tools by keyword
#### 3. Server Integration (`src/server.ts`)
- ✅ Router tools registered at server startup
- ✅ All tools remain functional (backwards compatible)
- ✅ Logging added for router pattern status
#### 4. Documentation
- ✅ `TOOL_INVENTORY.md` - Complete tool catalog
- ✅ `ROUTER_ARCHITECTURE.md` - Design specification
- ✅ `ROUTER_IMPLEMENTATION_STATUS.md` - This file
### Current State
**Status:** ✅ **Router Infrastructure Complete**
**Build:** ✅ Compiles successfully (`npm run build`)
**Tool Count:**
- Total Tools: 59 (ALL still registered and visible)
- Direct Tools: 12
- Routed Tools: 47
- Router Tools: 4
- **Currently Visible to Claude:** 63 tools (59 + 4 router)
**Token Impact:**
- **Current:** ~42K tokens (still showing all tools)
- **Target:** ~12K tokens (Phase 2 optimization)
- **Potential Savings:** ~30K tokens (71% reduction)
## 🔄 Phase 2: Token Optimization (Next Step)
### Objective
Hide routed tools from Claude's context while keeping them accessible via `execute_tool`.
### Two Approaches
#### Option A: Registration Filtering (Recommended)
Modify tool registration to conditionally register tools based on whether they're in the direct list.
**Changes needed:**
1. Update each `register*Tools` function to check `isDirectTool()`
2. Only call `server.tool()` for direct tools
3. Routed tools remain accessible via `execute_tool` calling `callKicadScript`
**Pros:**
- Clean separation
- True token savings
- No behavior changes
**Cons:**
- Requires modifying 9 tool files
#### Option B: MCP Filter (If Supported)
If MCP SDK supports tool filtering/hiding, use that instead.
**Pros:**
- No tool file changes
- Centralized control
**Cons:**
- May not be supported by SDK
- Needs investigation
### Implementation Plan for Phase 2
1. **Create Helper Function** (`src/tools/conditional-register.ts`)
```typescript
export function registerToolConditionally(
server: McpServer,
toolName: string,
definition: ToolDefinition,
handler: Function,
) {
if (isDirectTool(toolName)) {
// Register with MCP (visible to Claude)
server.tool(toolName, definition, handler);
} else {
// Register handler for execute_tool (hidden from Claude)
registerToolHandler(toolName, handler);
}
}
```
2. **Update Tool Registration Functions**
Modify each `register*Tools` function to use conditional registration.
3. **Test**
- Verify direct tools work normally
- Verify routed tools work via `execute_tool`
- Verify token count reduction
4. **Measure Impact**
Count tools visible to Claude before/after.
## 📊 Categories & Distribution
| Category | Tools | Description |
| ------------- | ------ | ------------------------------------------------- |
| **board** | 9 | Board configuration, layers, zones, visualization |
| **component** | 8 | Advanced component operations |
| **export** | 8 | Manufacturing file generation |
| **drc** | 9 | Design rule checking & validation |
| **schematic** | 9 | Schematic editor operations |
| **library** | 4 | Footprint library access |
| **routing** | 3 | Advanced routing (vias, copper pours) |
| **TOTAL** | **47** | **Routed tools** |
| **direct** | **12** | **Always visible tools** |
| **router** | **4** | **Discovery tools** |
## 🧪 Testing the Router
### Test 1: List Categories
```
User: "What tool categories are available?"
Expected: Claude calls list_tool_categories
Result: Returns 7 categories with descriptions
```
### Test 2: Browse Category
```
User: "What export tools are available?"
Expected: Claude calls get_category_tools({ category: "export" })
Result: Returns 8 export tools
```
### Test 3: Search Tools
```
User: "How do I export gerber files?"
Expected: Claude calls search_tools({ query: "gerber" })
Result: Finds export_gerber in export category
```
### Test 4: Execute Tool
```
User: "Export gerbers to ./output"
Expected: Claude calls execute_tool({
tool_name: "export_gerber",
params: { outputDir: "./output" }
})
Result: Executes via router, returns gerber export result
```
## 📝 Benefits Achieved (Phase 1)
1. ✅ **Foundation Ready**: All infrastructure in place
2. ✅ **Organized**: 59 tools categorized into logical groups
3. ✅ **Discoverable**: Tools easily found via search/browse
4. ✅ **Backwards Compatible**: All existing tools still work
5. ✅ **Extensible**: Easy to add new tools and categories
6. ✅ **Documented**: Complete architecture and usage docs
## 🚀 Next Actions
1. **Optional: Complete Phase 2** (Token Optimization)
- Implement conditional registration
- Hide routed tools from context
- Achieve 71% token reduction
2. **Or: Ship Phase 1 As-Is**
- Router tools work perfectly now
- Users can discover and execute tools
- Optimization can be done later
- No breaking changes
## 📚 Related Files
- `src/tools/registry.ts` - Tool registry and categories
- `src/tools/router.ts` - Router tool implementations
- `src/server.ts` - Server integration
- `docs/TOOL_INVENTORY.md` - Complete tool list
- `docs/ROUTER_ARCHITECTURE.md` - Design specification
- `docs/mcp-router-guide.md` - Original implementation guide
## 💡 Usage Example
```typescript
// User: "I need to export gerber files"
// Claude's interaction:
// 1. Sees "export" and "gerber" keywords
// 2. Calls search_tools({ query: "gerber" })
// → Returns: { category: "export", tool: "export_gerber", ... }
// 3. Calls execute_tool({
// tool_name: "export_gerber",
// params: { outputDir: "./gerbers" }
// })
// → Executes and returns result
// 4. "I've exported your Gerber files to ./gerbers/"
```
## Status Summary
✅ **Router Pattern: IMPLEMENTED**
✅ **Build: PASSING**
✅ **Backwards Compatible: YES**
⏳ **Token Optimization: PENDING (Phase 2)**
The router infrastructure is complete and functional. The system now supports tool discovery and organized access to all 59 tools. Phase 2 optimization (hiding routed tools) can be implemented when ready for maximum token savings.

View File

@@ -1,125 +1,133 @@
# Schematic Workflow Fix - Issue #26
## Problem Summary
The schematic workflow was completely broken due to incorrect usage of the kicad-skip library:
1. **`create_project`** only created PCB files, no schematic
2. **`create_schematic`** created orphaned schematic files not linked to projects
3. **`add_schematic_component`** called non-existent `schematic.add_symbol()` method
4. Project files didn't reference schematics in their structure
## Root Cause
The kicad-skip library **does not support creating symbols from scratch**. The only way to add symbols is by **cloning existing symbol instances**.
From kicad-skip documentation:
> "symbols: these don't have a new()" because they require complex mappings to library elements, pins, and properties.
## Solution
### 1. Template-Based Approach
Created a template schematic (`python/templates/template_with_symbols.kicad_sch`) with:
- Complete `lib_symbols` section defining R, C, LED symbols
- Three template symbol instances placed off-screen at (-100, -110, -120)
- Template symbols marked as `dnp yes`, `in_bom no`, `on_board no` so they don't interfere
### 2. Updated Files
**python/commands/project.py:**
- Now creates both `.kicad_pcb` AND `.kicad_sch` files
- Project file includes schematic reference in `sheets` array
- Copies template schematic with cloneable symbols
**python/commands/schematic.py:**
- Uses template file instead of creating from scratch
- Proper minimal schematic structure when template unavailable
**python/commands/component_schematic.py:**
- Completely rewritten to use `clone()` API
- Maps component types to template symbols
- Proper UUID generation for each cloned symbol
- Correct position setting: `symbol.at.value = [x, y, rotation]`
### 3. Correct Workflow
```python
from commands.project import ProjectCommands
from commands.schematic import SchematicManager
from commands.component_schematic import ComponentManager
# Step 1: Create project (creates both PCB and schematic)
project_cmd = ProjectCommands()
result = project_cmd.create_project({
"name": "MyProject",
"path": "/path/to/project"
})
# Step 2: Load the schematic
sch = SchematicManager.load_schematic(result['project']['schematicPath'])
# Step 3: Add components by cloning templates
component_def = {
"type": "R", # Maps to _TEMPLATE_R
"reference": "R1", # Component reference
"value": "10k", # Component value
"footprint": "Resistor_SMD:R_0603_1608Metric",
"x": 50.8, # Position in mm
"y": 50.8, # Position in mm
"rotation": 0 # Rotation in degrees
}
symbol = ComponentManager.add_component(sch, component_def)
# Step 4: Save the schematic
SchematicManager.save_schematic(sch, result['project']['schematicPath'])
```
## Supported Component Types
Currently supported template symbols:
- `R` - Resistor (maps to `_TEMPLATE_R`)
- `C` - Capacitor (maps to `_TEMPLATE_C`)
- `D` or `LED` - LED (maps to `_TEMPLATE_D`)
To add more component types, update:
1. `python/templates/template_with_symbols.kicad_sch` - Add lib_symbol definition and template instance
2. `python/commands/component_schematic.py` - Add mapping in `TEMPLATE_MAP`
## Testing
Comprehensive test created at `/tmp/test_schematic_workflow.py`:
- Creates project with schematic
- Loads schematic
- Adds R, C, LED components
- Saves schematic
- Validates with `kicad-cli sch export pdf`
All tests passing ✓
## Files Modified
- `python/commands/project.py` - Added schematic creation
- `python/commands/schematic.py` - Fixed template usage
- `python/commands/component_schematic.py` - Rewritten to use clone() API
- `python/templates/empty.kicad_sch` - Minimal template (created)
- `python/templates/template_with_symbols.kicad_sch` - Template with cloneable symbols (created)
## Limitations
1. Can only add components that have templates defined
2. Template symbols remain in schematic (but marked as DNP/not in BOM)
3. Complex symbols (multi-unit, hierarchical) may need custom templates
## Future Improvements
1. Add more component templates (transistors, connectors, ICs)
2. Dynamic template generation from KiCad symbol libraries
3. Auto-hide template symbols in schematic
4. Support for custom user templates
## References
- GitHub Issue: #26
- kicad-skip documentation: https://github.com/psychogenic/kicad-skip
- Test results: `/tmp/test_schematic_workflow/`
# Schematic Workflow Fix - Issue #26
## Problem Summary
The schematic workflow was completely broken due to incorrect usage of the kicad-skip library:
1. **`create_project`** only created PCB files, no schematic
2. **`create_schematic`** created orphaned schematic files not linked to projects
3. **`add_schematic_component`** called non-existent `schematic.add_symbol()` method
4. Project files didn't reference schematics in their structure
## Root Cause
The kicad-skip library **does not support creating symbols from scratch**. The only way to add symbols is by **cloning existing symbol instances**.
From kicad-skip documentation:
> "symbols: these don't have a new()" because they require complex mappings to library elements, pins, and properties.
## Solution
### 1. Template-Based Approach
Created a template schematic (`python/templates/template_with_symbols.kicad_sch`) with:
- Complete `lib_symbols` section defining R, C, LED symbols
- Three template symbol instances placed off-screen at (-100, -110, -120)
- Template symbols marked as `dnp yes`, `in_bom no`, `on_board no` so they don't interfere
### 2. Updated Files
**python/commands/project.py:**
- Now creates both `.kicad_pcb` AND `.kicad_sch` files
- Project file includes schematic reference in `sheets` array
- Copies template schematic with cloneable symbols
**python/commands/schematic.py:**
- Uses template file instead of creating from scratch
- Proper minimal schematic structure when template unavailable
**python/commands/component_schematic.py:**
- Completely rewritten to use `clone()` API
- Maps component types to template symbols
- Proper UUID generation for each cloned symbol
- Correct position setting: `symbol.at.value = [x, y, rotation]`
### 3. Correct Workflow
```python
from commands.project import ProjectCommands
from commands.schematic import SchematicManager
from commands.component_schematic import ComponentManager
# Step 1: Create project (creates both PCB and schematic)
project_cmd = ProjectCommands()
result = project_cmd.create_project({
"name": "MyProject",
"path": "/path/to/project"
})
# Step 2: Load the schematic
sch = SchematicManager.load_schematic(result['project']['schematicPath'])
# Step 3: Add components by cloning templates
component_def = {
"type": "R", # Maps to _TEMPLATE_R
"reference": "R1", # Component reference
"value": "10k", # Component value
"footprint": "Resistor_SMD:R_0603_1608Metric",
"x": 50.8, # Position in mm
"y": 50.8, # Position in mm
"rotation": 0 # Rotation in degrees
}
symbol = ComponentManager.add_component(sch, component_def)
# Step 4: Save the schematic
SchematicManager.save_schematic(sch, result['project']['schematicPath'])
```
## Supported Component Types
Currently supported template symbols:
- `R` - Resistor (maps to `_TEMPLATE_R`)
- `C` - Capacitor (maps to `_TEMPLATE_C`)
- `D` or `LED` - LED (maps to `_TEMPLATE_D`)
To add more component types, update:
1. `python/templates/template_with_symbols.kicad_sch` - Add lib_symbol definition and template instance
2. `python/commands/component_schematic.py` - Add mapping in `TEMPLATE_MAP`
## Testing
Comprehensive test created at `/tmp/test_schematic_workflow.py`:
- Creates project with schematic
- Loads schematic
- Adds R, C, LED components
- Saves schematic
- Validates with `kicad-cli sch export pdf`
All tests passing ✓
## Files Modified
- `python/commands/project.py` - Added schematic creation
- `python/commands/schematic.py` - Fixed template usage
- `python/commands/component_schematic.py` - Rewritten to use clone() API
- `python/templates/empty.kicad_sch` - Minimal template (created)
- `python/templates/template_with_symbols.kicad_sch` - Template with cloneable symbols (created)
## Limitations
1. Can only add components that have templates defined
2. Template symbols remain in schematic (but marked as DNP/not in BOM)
3. Complex symbols (multi-unit, hierarchical) may need custom templates
## Future Improvements
1. Add more component templates (transistors, connectors, ICs)
2. Dynamic template generation from KiCad symbol libraries
3. Auto-hide template symbols in schematic
4. Support for custom user templates
## References
- GitHub Issue: #26
- kicad-skip documentation: https://github.com/psychogenic/kicad-skip
- Test results: `/tmp/test_schematic_workflow/`

View File

@@ -1,422 +1,457 @@
# Week 1 - Session 2 Summary
**Date:** October 25, 2025 (Afternoon)
**Status:** 🚀 **OUTSTANDING PROGRESS**
---
## 🎯 Session Goals
Continue Week 1 implementation while user installs KiCAD:
1. Update README with comprehensive Linux guide
2. Create installation scripts
3. Begin IPC API preparation
4. Set up development infrastructure
---
## ✅ Completed Work
### 1. **README.md Major Update** 📚
**File:** `README.md`
**Changes:**
- ✅ Updated project status to reflect v2.0 rebuild
- ✅ Added collapsible platform-specific installation sections:
- 🐧 **Linux (Ubuntu/Debian)** - Primary, detailed
- 🪟 **Windows 10/11** - Fully supported
- 🍎 **macOS** - Experimental
- ✅ Updated system requirements (Linux primary platform)
- ✅ Added Quick Start section with test commands
- ✅ Better visual organization with emojis and status indicators
**Impact:** New users can now install on Linux in < 10 minutes!
---
### 2. **Linux Installation Script** 🛠️
**File:** `scripts/install-linux.sh`
**Features:**
- ✅ Fully automated Ubuntu/Debian installation
- ✅ Color-coded output (info/success/warning/error)
- ✅ Safety checks (platform detection, command validation)
- ✅ Installs:
- KiCAD 9.0 from PPA
- Node.js 20.x
- Python dependencies
- Builds TypeScript
- ✅ Verification checks after installation
- ✅ Helpful next-steps guidance
**Usage:**
```bash
cd kicad-mcp-server
./scripts/install-linux.sh
```
**Lines of Code:** ~200 lines of robust shell script
---
### 3. **Pre-Commit Hooks Configuration** 🔧
**File:** `.pre-commit-config.yaml`
**Hooks Added:**
- ✅ **Python:**
- Black (code formatting)
- isort (import sorting)
- MyPy (type checking)
- Flake8 (linting)
- Bandit (security checks)
- ✅ **TypeScript/JavaScript:**
- Prettier (formatting)
- ✅ **General:**
- Trailing whitespace removal
- End-of-file fixer
- YAML/JSON validation
- Large file detection
- Merge conflict detection
- Private key detection
- ✅ **Markdown:**
- Markdownlint (formatting)
**Setup:**
```bash
pip install pre-commit
pre-commit install
```
**Impact:** Automatic code quality enforcement on every commit!
---
### 4. **IPC API Migration Plan** 📋
**File:** `docs/IPC_API_MIGRATION_PLAN.md`
**Comprehensive 30-page migration guide:**
- ✅ Why migrate (SWIG deprecation analysis)
- ✅ IPC API architecture overview
- ✅ 4-phase migration strategy (10 days)
- ✅ API comparison tables (SWIG vs IPC)
- ✅ Testing strategy
- ✅ Rollback plan
- ✅ Success criteria
- ✅ Timeline with day-by-day tasks
**Key Insights:**
- SWIG will be removed in KiCAD 10.0
- IPC is faster for some operations
- Protocol Buffers ensure API stability
- Multi-language support opens future possibilities
---
### 5. **IPC API Abstraction Layer** 🏗️
**New Module:** `python/kicad_api/`
**Files Created (5):**
1. **`__init__.py`** (20 lines)
- Package exports
- Version info
- Usage examples
2. **`base.py`** (180 lines)
- `KiCADBackend` abstract base class
- `BoardAPI` abstract interface
- Custom exceptions (`BackendError`, `ConnectionError`, etc.)
- Defines contract for all backends
3. **`factory.py`** (160 lines)
- `create_backend()` - Smart backend selection
- Auto-detection (try IPC, fall back to SWIG)
- Environment variable support (`KICAD_BACKEND`)
- `get_available_backends()` - Diagnostic function
- Comprehensive error handling
4. **`ipc_backend.py`** (210 lines)
- `IPCBackend` class (kicad-python wrapper)
- `IPCBoardAPI` class
- Connection management
- Skeleton methods (to be implemented in Week 2-3)
- Clear TODO markers for migration
5. **`swig_backend.py`** (220 lines)
- `SWIGBackend` class (wraps existing code)
- `SWIGBoardAPI` class
- Backward compatibility layer
- Deprecation warnings
- Bridges old commands to new interface
**Total Lines of Code:** ~800 lines
**Architecture:**
```python
from kicad_api import create_backend
# Auto-detect best backend
backend = create_backend()
# Or specify explicitly
backend = create_backend('ipc') # Use IPC
backend = create_backend('swig') # Use SWIG (deprecated)
# Use unified interface
if backend.connect():
board = backend.get_board()
board.set_size(100, 80)
```
**Key Features:**
- ✅ Abstraction allows painless migration
- ✅ Both backends can coexist during transition
- ✅ Easy testing (compare SWIG vs IPC outputs)
- ✅ Future-proof (add new backends easily)
- ✅ Type hints throughout
- ✅ Comprehensive error handling
---
### 6. **Enhanced package.json** 📦
**File:** `package.json`
**Improvements:**
- ✅ Version bumped to `2.0.0-alpha.1`
- ✅ Better description
- ✅ Enhanced npm scripts:
```json
"build:watch": "tsc --watch"
"clean": "rm -rf dist"
"rebuild": "npm run clean && npm run build"
"test": "npm run test:ts && npm run test:py"
"test:py": "pytest tests/ -v"
"test:coverage": "pytest with coverage"
"lint": "npm run lint:ts && npm run lint:py"
"lint:py": "black + mypy + flake8"
"format": "prettier + black"
```
**Impact:** Better developer experience, easier workflows
---
## 📊 Statistics
### Files Created/Modified (Session 2)
**New Files (10):**
```
docs/IPC_API_MIGRATION_PLAN.md # 500+ lines
docs/WEEK1_SESSION2_SUMMARY.md # This file
scripts/install-linux.sh # 200 lines
.pre-commit-config.yaml # 60 lines
python/kicad_api/__init__.py # 20 lines
python/kicad_api/base.py # 180 lines
python/kicad_api/factory.py # 160 lines
python/kicad_api/ipc_backend.py # 210 lines
python/kicad_api/swig_backend.py # 220 lines
```
**Modified Files (2):**
```
README.md # Major rewrite
package.json # Enhanced scripts
```
**Total New Lines:** ~1,600+ lines of code/documentation
---
### Combined Sessions 1+2 Today
**Files Created:** 27
**Lines Written:** ~3,000+
**Documentation Pages:** 8
**Tests Created:** 20+
---
## 🎯 Week 1 Status
### Progress: **95% Complete** ████████████░
| Task | Status |
|------|--------|
| Linux compatibility | ✅ Complete |
| CI/CD pipeline | ✅ Complete |
| Cross-platform paths | ✅ Complete |
| Developer docs | ✅ Complete |
| pytest framework | ✅ Complete |
| Config templates | ✅ Complete |
| Installation scripts | ✅ Complete |
| Pre-commit hooks | ✅ Complete |
| IPC migration plan | ✅ Complete |
| IPC abstraction layer | ✅ Complete |
| README updates | ✅ Complete |
| Testing on Ubuntu | ⏳ Pending (needs KiCAD install) |
**Only Remaining:** Test with actual KiCAD 9.0 installation!
---
## 🚀 Ready for Week 2
### IPC API Migration Prep ✅
Everything is in place to begin migration:
- ✅ Abstraction layer architecture defined
- ✅ Base classes and interfaces ready
- ✅ Factory pattern for backend selection
- ✅ SWIG wrapper for backward compatibility
- ✅ IPC skeleton awaiting implementation
- ✅ Comprehensive migration plan documented
**Week 2 kickoff tasks:**
1. Install `kicad-python` package
2. Test IPC connection to running KiCAD
3. Begin porting `project.py` module
4. Create side-by-side tests (SWIG vs IPC)
---
## 💡 Key Insights from Session 2
### 1. **Installation Automation**
The bash script reduces setup time from 30+ minutes to < 10 minutes with zero manual intervention.
### 2. **Pre-Commit Hooks**
Automatic code quality checks prevent bugs before they're committed. This will save hours in code review.
### 3. **Abstraction Pattern**
The backend abstraction is elegant - allows gradual migration without breaking existing functionality. Users won't notice the transition.
### 4. **Documentation Quality**
The IPC migration plan is thorough enough that another developer could execute it independently.
---
## 🧪 Testing Readiness
### When KiCAD is Installed
You can immediately test:
**1. Platform Helper:**
```bash
python3 python/utils/platform_helper.py
```
**2. Backend Detection:**
```bash
python3 python/kicad_api/factory.py
```
**3. Installation Script:**
```bash
./scripts/install-linux.sh
```
**4. Pytest Suite:**
```bash
pytest tests/ -v
```
**5. Pre-commit Hooks:**
```bash
pre-commit run --all-files
```
---
## 📈 Impact Assessment
### Developer Onboarding
- **Before:** 2-3 hours setup, Windows-only, manual steps
- **After:** 10 minutes automated, cross-platform, one script
### Code Quality
- **Before:** No automated checks, inconsistent style
- **After:** Pre-commit hooks, 100% type hints, Black formatting
### Future-Proofing
- **Before:** Deprecated SWIG API, no migration path
- **After:** IPC API ready, abstraction layer in place
### Documentation
- **Before:** README only, Windows-focused
- **After:** 8 comprehensive docs, Linux-primary, migration guides
---
## 🎯 Next Actions
### Immediate (Tonight/Tomorrow)
1. Install KiCAD 9.0 on your system
2. Run `./scripts/install-linux.sh`
3. Test backend detection
4. Verify pytest suite passes
### Week 2 Start (Monday)
1. Install `kicad-python` package
2. Test IPC connection
3. Begin project.py migration
4. Create first IPC API tests
---
## 🏆 Session 2 Achievements
### Infrastructure
- ✅ Automated Linux installation
- ✅ Pre-commit hooks for code quality
- ✅ Enhanced npm scripts
- ✅ IPC API abstraction layer (800+ lines)
### Documentation
- ✅ Updated README (Linux-primary)
- ✅ 30-page IPC migration plan
- ✅ Session summaries
### Architecture
- ✅ Backend abstraction pattern
- ✅ Factory with auto-detection
- ✅ SWIG backward compatibility
- ✅ IPC skeleton ready for implementation
---
## 🎉 Overall Day Summary
**Sessions 1+2 Combined:**
- ⏱️ **Time:** ~4-5 hours total
- 📝 **Files:** 27 created
- 💻 **Code:** ~3,000+ lines
- 📚 **Docs:** 8 comprehensive pages
- 🧪 **Tests:** 20+ unit tests
-**Week 1:** 95% complete
**Status:** 🟢 **AHEAD OF SCHEDULE**
---
## 🚀 Momentum Check
**Energy Level:** 🔋🔋🔋🔋🔋 (Maximum)
**Code Quality:** ⭐⭐⭐⭐⭐ (Excellent)
**Documentation:** 📖📖📖📖📖 (Comprehensive)
**Architecture:** 🏗️🏗️🏗️🏗️🏗️ (Solid)
**Ready for Week 2 IPC Migration:** ✅ YES!
---
**End of Session 2**
**Next:** KiCAD installation + testing + Week 2 kickoff
Let's keep this incredible momentum going! 🎉🚀
# Week 1 - Session 2 Summary
**Date:** October 25, 2025 (Afternoon)
**Status:** 🚀 **OUTSTANDING PROGRESS**
---
## 🎯 Session Goals
Continue Week 1 implementation while user installs KiCAD:
1. Update README with comprehensive Linux guide
2. Create installation scripts
3. Begin IPC API preparation
4. Set up development infrastructure
---
## ✅ Completed Work
### 1. **README.md Major Update** 📚
**File:** `README.md`
**Changes:**
- ✅ Updated project status to reflect v2.0 rebuild
- ✅ Added collapsible platform-specific installation sections:
- 🐧 **Linux (Ubuntu/Debian)** - Primary, detailed
- 🪟 **Windows 10/11** - Fully supported
- 🍎 **macOS** - Experimental
- ✅ Updated system requirements (Linux primary platform)
- ✅ Added Quick Start section with test commands
- ✅ Better visual organization with emojis and status indicators
**Impact:** New users can now install on Linux in < 10 minutes!
---
### 2. **Linux Installation Script** 🛠️
**File:** `scripts/install-linux.sh`
**Features:**
- ✅ Fully automated Ubuntu/Debian installation
- ✅ Color-coded output (info/success/warning/error)
- ✅ Safety checks (platform detection, command validation)
- ✅ Installs:
- KiCAD 9.0 from PPA
- Node.js 20.x
- Python dependencies
- Builds TypeScript
- ✅ Verification checks after installation
- ✅ Helpful next-steps guidance
**Usage:**
```bash
cd kicad-mcp-server
./scripts/install-linux.sh
```
**Lines of Code:** ~200 lines of robust shell script
---
### 3. **Pre-Commit Hooks Configuration** 🔧
**File:** `.pre-commit-config.yaml`
**Hooks Added:**
- ✅ **Python:**
- Black (code formatting)
- isort (import sorting)
- MyPy (type checking)
- Flake8 (linting)
- Bandit (security checks)
- ✅ **TypeScript/JavaScript:**
- Prettier (formatting)
- ✅ **General:**
- Trailing whitespace removal
- End-of-file fixer
- YAML/JSON validation
- Large file detection
- Merge conflict detection
- Private key detection
- ✅ **Markdown:**
- Markdownlint (formatting)
**Setup:**
```bash
pip install pre-commit
pre-commit install
```
**Impact:** Automatic code quality enforcement on every commit!
---
### 4. **IPC API Migration Plan** 📋
**File:** `docs/IPC_API_MIGRATION_PLAN.md`
**Comprehensive 30-page migration guide:**
- ✅ Why migrate (SWIG deprecation analysis)
- ✅ IPC API architecture overview
- ✅ 4-phase migration strategy (10 days)
- ✅ API comparison tables (SWIG vs IPC)
- ✅ Testing strategy
- ✅ Rollback plan
- ✅ Success criteria
- ✅ Timeline with day-by-day tasks
**Key Insights:**
- SWIG will be removed in KiCAD 10.0
- IPC is faster for some operations
- Protocol Buffers ensure API stability
- Multi-language support opens future possibilities
---
### 5. **IPC API Abstraction Layer** 🏗️
**New Module:** `python/kicad_api/`
**Files Created (5):**
1. **`__init__.py`** (20 lines)
- Package exports
- Version info
- Usage examples
2. **`base.py`** (180 lines)
- `KiCADBackend` abstract base class
- `BoardAPI` abstract interface
- Custom exceptions (`BackendError`, `ConnectionError`, etc.)
- Defines contract for all backends
3. **`factory.py`** (160 lines)
- `create_backend()` - Smart backend selection
- Auto-detection (try IPC, fall back to SWIG)
- Environment variable support (`KICAD_BACKEND`)
- `get_available_backends()` - Diagnostic function
- Comprehensive error handling
4. **`ipc_backend.py`** (210 lines)
- `IPCBackend` class (kicad-python wrapper)
- `IPCBoardAPI` class
- Connection management
- Skeleton methods (to be implemented in Week 2-3)
- Clear TODO markers for migration
5. **`swig_backend.py`** (220 lines)
- `SWIGBackend` class (wraps existing code)
- `SWIGBoardAPI` class
- Backward compatibility layer
- Deprecation warnings
- Bridges old commands to new interface
**Total Lines of Code:** ~800 lines
**Architecture:**
```python
from kicad_api import create_backend
# Auto-detect best backend
backend = create_backend()
# Or specify explicitly
backend = create_backend('ipc') # Use IPC
backend = create_backend('swig') # Use SWIG (deprecated)
# Use unified interface
if backend.connect():
board = backend.get_board()
board.set_size(100, 80)
```
**Key Features:**
- ✅ Abstraction allows painless migration
- ✅ Both backends can coexist during transition
- ✅ Easy testing (compare SWIG vs IPC outputs)
- ✅ Future-proof (add new backends easily)
- ✅ Type hints throughout
- ✅ Comprehensive error handling
---
### 6. **Enhanced package.json** 📦
**File:** `package.json`
**Improvements:**
- ✅ Version bumped to `2.0.0-alpha.1`
- ✅ Better description
- ✅ Enhanced npm scripts:
```json
"build:watch": "tsc --watch"
"clean": "rm -rf dist"
"rebuild": "npm run clean && npm run build"
"test": "npm run test:ts && npm run test:py"
"test:py": "pytest tests/ -v"
"test:coverage": "pytest with coverage"
"lint": "npm run lint:ts && npm run lint:py"
"lint:py": "black + mypy + flake8"
"format": "prettier + black"
```
**Impact:** Better developer experience, easier workflows
---
## 📊 Statistics
### Files Created/Modified (Session 2)
**New Files (10):**
```
docs/IPC_API_MIGRATION_PLAN.md # 500+ lines
docs/WEEK1_SESSION2_SUMMARY.md # This file
scripts/install-linux.sh # 200 lines
.pre-commit-config.yaml # 60 lines
python/kicad_api/__init__.py # 20 lines
python/kicad_api/base.py # 180 lines
python/kicad_api/factory.py # 160 lines
python/kicad_api/ipc_backend.py # 210 lines
python/kicad_api/swig_backend.py # 220 lines
```
**Modified Files (2):**
```
README.md # Major rewrite
package.json # Enhanced scripts
```
**Total New Lines:** ~1,600+ lines of code/documentation
---
### Combined Sessions 1+2 Today
**Files Created:** 27
**Lines Written:** ~3,000+
**Documentation Pages:** 8
**Tests Created:** 20+
---
## 🎯 Week 1 Status
### Progress: **95% Complete** ████████████░
| Task | Status |
| --------------------- | -------------------------------- |
| Linux compatibility | ✅ Complete |
| CI/CD pipeline | ✅ Complete |
| Cross-platform paths | ✅ Complete |
| Developer docs | ✅ Complete |
| pytest framework | ✅ Complete |
| Config templates | ✅ Complete |
| Installation scripts | ✅ Complete |
| Pre-commit hooks | ✅ Complete |
| IPC migration plan | ✅ Complete |
| IPC abstraction layer | ✅ Complete |
| README updates | ✅ Complete |
| Testing on Ubuntu | ⏳ Pending (needs KiCAD install) |
**Only Remaining:** Test with actual KiCAD 9.0 installation!
---
## 🚀 Ready for Week 2
### IPC API Migration Prep ✅
Everything is in place to begin migration:
- ✅ Abstraction layer architecture defined
- ✅ Base classes and interfaces ready
- ✅ Factory pattern for backend selection
- ✅ SWIG wrapper for backward compatibility
- ✅ IPC skeleton awaiting implementation
- ✅ Comprehensive migration plan documented
**Week 2 kickoff tasks:**
1. Install `kicad-python` package
2. Test IPC connection to running KiCAD
3. Begin porting `project.py` module
4. Create side-by-side tests (SWIG vs IPC)
---
## 💡 Key Insights from Session 2
### 1. **Installation Automation**
The bash script reduces setup time from 30+ minutes to < 10 minutes with zero manual intervention.
### 2. **Pre-Commit Hooks**
Automatic code quality checks prevent bugs before they're committed. This will save hours in code review.
### 3. **Abstraction Pattern**
The backend abstraction is elegant - allows gradual migration without breaking existing functionality. Users won't notice the transition.
### 4. **Documentation Quality**
The IPC migration plan is thorough enough that another developer could execute it independently.
---
## 🧪 Testing Readiness
### When KiCAD is Installed
You can immediately test:
**1. Platform Helper:**
```bash
python3 python/utils/platform_helper.py
```
**2. Backend Detection:**
```bash
python3 python/kicad_api/factory.py
```
**3. Installation Script:**
```bash
./scripts/install-linux.sh
```
**4. Pytest Suite:**
```bash
pytest tests/ -v
```
**5. Pre-commit Hooks:**
```bash
pre-commit run --all-files
```
---
## 📈 Impact Assessment
### Developer Onboarding
- **Before:** 2-3 hours setup, Windows-only, manual steps
- **After:** 10 minutes automated, cross-platform, one script
### Code Quality
- **Before:** No automated checks, inconsistent style
- **After:** Pre-commit hooks, 100% type hints, Black formatting
### Future-Proofing
- **Before:** Deprecated SWIG API, no migration path
- **After:** IPC API ready, abstraction layer in place
### Documentation
- **Before:** README only, Windows-focused
- **After:** 8 comprehensive docs, Linux-primary, migration guides
---
## 🎯 Next Actions
### Immediate (Tonight/Tomorrow)
1. Install KiCAD 9.0 on your system
2. Run `./scripts/install-linux.sh`
3. Test backend detection
4. Verify pytest suite passes
### Week 2 Start (Monday)
1. Install `kicad-python` package
2. Test IPC connection
3. Begin project.py migration
4. Create first IPC API tests
---
## 🏆 Session 2 Achievements
### Infrastructure
- ✅ Automated Linux installation
- ✅ Pre-commit hooks for code quality
- ✅ Enhanced npm scripts
- ✅ IPC API abstraction layer (800+ lines)
### Documentation
- ✅ Updated README (Linux-primary)
- ✅ 30-page IPC migration plan
- ✅ Session summaries
### Architecture
- ✅ Backend abstraction pattern
- ✅ Factory with auto-detection
- ✅ SWIG backward compatibility
- ✅ IPC skeleton ready for implementation
---
## 🎉 Overall Day Summary
**Sessions 1+2 Combined:**
- ⏱️ **Time:** ~4-5 hours total
- 📝 **Files:** 27 created
- 💻 **Code:** ~3,000+ lines
- 📚 **Docs:** 8 comprehensive pages
- 🧪 **Tests:** 20+ unit tests
- ✅ **Week 1:** 95% complete
**Status:** 🟢 **AHEAD OF SCHEDULE**
---
## 🚀 Momentum Check
**Energy Level:** 🔋🔋🔋🔋🔋 (Maximum)
**Code Quality:** ⭐⭐⭐⭐⭐ (Excellent)
**Documentation:** 📖📖📖📖📖 (Comprehensive)
**Architecture:** 🏗️🏗️🏗️🏗️🏗️ (Solid)
**Ready for Week 2 IPC Migration:** ✅ YES!
---
**End of Session 2**
**Next:** KiCAD installation + testing + Week 2 kickoff
Let's keep this incredible momentum going! 🎉🚀

View File

@@ -106,27 +106,27 @@ export const toolCategories: ToolCategory[] = [
inputSchema: {
type: "object",
properties: {
output_dir: {
type: "string",
description: "Output directory path"
output_dir: {
type: "string",
description: "Output directory path",
},
layers: {
type: "array",
layers: {
type: "array",
items: { type: "string" },
description: "Layers to export (default: all copper + silkscreen + mask)"
description: "Layers to export (default: all copper + silkscreen + mask)",
},
format: {
type: "string",
enum: ["rs274x", "x2"],
description: "Gerber format version"
}
description: "Gerber format version",
},
},
required: ["output_dir"]
required: ["output_dir"],
},
handler: async (params) => {
// Your implementation
return { success: true, files: ["..."] };
}
},
},
{
name: "export_drill",
@@ -135,24 +135,31 @@ export const toolCategories: ToolCategory[] = [
type: "object",
properties: {
output_dir: { type: "string" },
format: { type: "string", enum: ["excellon", "excellon2"] }
format: { type: "string", enum: ["excellon", "excellon2"] },
},
required: ["output_dir"]
required: ["output_dir"],
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "export_bom",
description: "Export bill of materials as CSV or XML",
inputSchema: { /* ... */ },
handler: async (params) => { /* ... */ }
inputSchema: {
/* ... */
},
handler: async (params) => {
/* ... */
},
},
// ... more export tools
]
],
},
{
name: "drc",
description: "Design rule checking: clearance validation, electrical rules, manufacturing constraints",
description:
"Design rule checking: clearance validation, electrical rules, manufacturing constraints",
tools: [
{
name: "run_drc",
@@ -160,19 +167,23 @@ export const toolCategories: ToolCategory[] = [
inputSchema: {
type: "object",
properties: {
report_all: {
type: "boolean",
description: "Report all violations or stop at first"
}
}
report_all: {
type: "boolean",
description: "Report all violations or stop at first",
},
},
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "get_drc_errors",
description: "Get current DRC violations without re-running check",
inputSchema: { type: "object", properties: {} },
handler: async (params) => { /* ... */ }
handler: async (params) => {
/* ... */
},
},
{
name: "set_design_rules",
@@ -183,12 +194,14 @@ export const toolCategories: ToolCategory[] = [
min_clearance: { type: "number", description: "Minimum clearance in mm" },
min_track_width: { type: "number", description: "Minimum track width in mm" },
min_via_diameter: { type: "number", description: "Minimum via diameter in mm" },
min_via_drill: { type: "number", description: "Minimum via drill size in mm" }
}
min_via_drill: { type: "number", description: "Minimum via drill size in mm" },
},
},
handler: async (params) => { /* ... */ }
}
]
handler: async (params) => {
/* ... */
},
},
],
},
{
name: "zones",
@@ -208,22 +221,26 @@ export const toolCategories: ToolCategory[] = [
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
}
y: { type: "number" },
},
},
description: "Polygon vertices in mm"
description: "Polygon vertices in mm",
},
priority: { type: "number", description: "Fill priority (higher fills first)" }
priority: { type: "number", description: "Fill priority (higher fills first)" },
},
required: ["net_name", "layer", "points"]
required: ["net_name", "layer", "points"],
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "fill_zones",
description: "Recalculate and fill all copper zones",
inputSchema: { type: "object", properties: {} },
handler: async (params) => { /* ... */ }
handler: async (params) => {
/* ... */
},
},
{
name: "remove_zone",
@@ -231,13 +248,15 @@ export const toolCategories: ToolCategory[] = [
inputSchema: {
type: "object",
properties: {
zone_id: { type: "string", description: "Zone identifier" }
zone_id: { type: "string", description: "Zone identifier" },
},
required: ["zone_id"]
required: ["zone_id"],
},
handler: async (params) => { /* ... */ }
}
]
handler: async (params) => {
/* ... */
},
},
],
},
// Add more categories...
];
@@ -293,7 +312,7 @@ export function searchTools(query: string): Array<{
matches.push({
category: category.name,
tool: tool.name,
description: tool.description
description: tool.description,
});
}
}
@@ -313,36 +332,31 @@ These are the tools that enable discovery and execution.
```typescript
// src/tools/router.ts
import {
getAllCategories,
getCategory,
getTool,
searchTools
} from "./registry.js";
import { getAllCategories, getCategory, getTool, searchTools } from "./registry.js";
export const routerTools = {
list_tool_categories: {
name: "list_tool_categories",
description:
description:
"List all available tool categories. Use this to discover what operations " +
"are available beyond the basic tools exposed directly.",
inputSchema: {
type: "object" as const,
properties: {},
required: []
required: [],
},
handler: async () => {
const categories = getAllCategories();
return {
total_categories: categories.length,
total_tools: categories.reduce((sum, c) => sum + c.tools.length, 0),
categories: categories.map(c => ({
categories: categories.map((c) => ({
name: c.name,
description: c.description,
tool_count: c.tools.length
}))
tool_count: c.tools.length,
})),
};
}
},
},
get_category_tools: {
@@ -356,29 +370,29 @@ export const routerTools = {
properties: {
category: {
type: "string",
description: "Category name from list_tool_categories"
}
description: "Category name from list_tool_categories",
},
},
required: ["category"]
required: ["category"],
},
handler: async (params: { category: string }) => {
const category = getCategory(params.category);
if (!category) {
return {
error: `Unknown category: ${params.category}`,
available_categories: getAllCategories().map(c => c.name)
available_categories: getAllCategories().map((c) => c.name),
};
}
return {
category: category.name,
description: category.description,
tools: category.tools.map(t => ({
tools: category.tools.map((t) => ({
name: t.name,
description: t.description,
parameters: t.inputSchema
}))
parameters: t.inputSchema,
})),
};
}
},
},
execute_tool: {
@@ -391,21 +405,21 @@ export const routerTools = {
properties: {
tool_name: {
type: "string",
description: "Tool name (from get_category_tools)"
description: "Tool name (from get_category_tools)",
},
params: {
type: "object",
description: "Tool parameters (see get_category_tools for schema)"
}
description: "Tool parameters (see get_category_tools for schema)",
},
},
required: ["tool_name"]
required: ["tool_name"],
},
handler: async (input: { tool_name: string; params?: Record<string, unknown> }) => {
const entry = getTool(input.tool_name);
if (!entry) {
return {
error: `Unknown tool: ${input.tool_name}`,
hint: "Use list_tool_categories and get_category_tools to find available tools"
hint: "Use list_tool_categories and get_category_tools to find available tools",
};
}
@@ -414,16 +428,16 @@ export const routerTools = {
return {
tool: input.tool_name,
category: entry.category,
result
result,
};
} catch (err) {
return {
error: `Tool execution failed: ${(err as Error).message}`,
tool: input.tool_name,
category: entry.category
category: entry.category,
};
}
}
},
},
search_tools: {
@@ -436,20 +450,20 @@ export const routerTools = {
properties: {
query: {
type: "string",
description: "Search term (e.g., 'gerber', 'zone', 'differential', 'export')"
}
description: "Search term (e.g., 'gerber', 'zone', 'differential', 'export')",
},
},
required: ["query"]
required: ["query"],
},
handler: async (params: { query: string }) => {
const matches = searchTools(params.query);
return {
query: params.query,
count: matches.length,
matches: matches.slice(0, 20) // Limit results
matches: matches.slice(0, 20), // Limit results
};
}
}
},
},
};
```
@@ -475,18 +489,18 @@ export const directTools: ToolDefinition[] = [
properties: {
name: { type: "string", description: "Project name" },
path: { type: "string", description: "Directory path for project" },
template: {
type: "string",
template: {
type: "string",
description: "Optional template to use",
enum: ["blank", "arduino", "raspberry-pi"]
}
enum: ["blank", "arduino", "raspberry-pi"],
},
},
required: ["name", "path"]
required: ["name", "path"],
},
handler: async (params) => {
// Implementation
return { success: true, project_path: `${params.path}/${params.name}` };
}
},
},
{
name: "open_project",
@@ -494,29 +508,35 @@ export const directTools: ToolDefinition[] = [
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "Path to project file or directory" }
path: { type: "string", description: "Path to project file or directory" },
},
required: ["path"]
required: ["path"],
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "save_project",
description: "Save all project files",
inputSchema: {
type: "object",
properties: {}
properties: {},
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "get_project_info",
description: "Get current project information: path, files, status",
inputSchema: {
type: "object",
properties: {}
properties: {},
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
// === PRIMARY OPERATIONS (your core workflow) ===
@@ -530,11 +550,13 @@ export const directTools: ToolDefinition[] = [
reference: { type: "string", description: "Reference designator (e.g., R1, U1)" },
x: { type: "number", description: "X position" },
y: { type: "number", description: "Y position" },
rotation: { type: "number", description: "Rotation in degrees", default: 0 }
rotation: { type: "number", description: "Rotation in degrees", default: 0 },
},
required: ["type", "reference", "x", "y"]
required: ["type", "reference", "x", "y"],
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "move_component",
@@ -544,11 +566,13 @@ export const directTools: ToolDefinition[] = [
properties: {
reference: { type: "string", description: "Component reference (e.g., R1)" },
x: { type: "number", description: "New X position" },
y: { type: "number", description: "New Y position" }
y: { type: "number", description: "New Y position" },
},
required: ["reference", "x", "y"]
required: ["reference", "x", "y"],
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "list_components",
@@ -556,10 +580,12 @@ export const directTools: ToolDefinition[] = [
inputSchema: {
type: "object",
properties: {
filter: { type: "string", description: "Optional filter (e.g., 'R*' for resistors)" }
}
filter: { type: "string", description: "Optional filter (e.g., 'R*' for resistors)" },
},
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
// === SECONDARY OPERATIONS (still common) ===
@@ -571,36 +597,42 @@ export const directTools: ToolDefinition[] = [
properties: {
start: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" } }
properties: { x: { type: "number" }, y: { type: "number" } },
},
end: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" } }
properties: { x: { type: "number" }, y: { type: "number" } },
},
net: { type: "string", description: "Net name" }
net: { type: "string", description: "Net name" },
},
required: ["start", "end"]
required: ["start", "end"],
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "list_nets",
description: "List all nets/connections",
inputSchema: {
type: "object",
properties: {}
properties: {},
},
handler: async (params) => {
/* ... */
},
handler: async (params) => { /* ... */ }
},
{
name: "get_info",
description: "Get general information about current state",
inputSchema: {
type: "object",
properties: {}
properties: {},
},
handler: async (params) => { /* ... */ }
}
handler: async (params) => {
/* ... */
},
},
];
```
@@ -613,10 +645,7 @@ Wire everything together in your main server file.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { directTools } from "./tools/direct.js";
import { routerTools } from "./tools/router.js";
@@ -634,14 +663,11 @@ const server = new Server(
capabilities: {
tools: {},
},
}
},
);
// Combine all visible tools
const allVisibleTools = [
...directTools,
...Object.values(routerTools)
];
const allVisibleTools = [...directTools, ...Object.values(routerTools)];
// Build a handler map for quick lookup
const toolHandlers = new Map<string, (params: any) => Promise<any>>();
@@ -656,7 +682,7 @@ for (const tool of Object.values(routerTools)) {
// List tools handler - returns only direct + router tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: allVisibleTools.map(tool => ({
tools: allVisibleTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
@@ -676,9 +702,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
type: "text",
text: JSON.stringify({
error: `Unknown tool: ${name}`,
hint: "Use list_tool_categories and search_tools to find available tools"
})
}
hint: "Use list_tool_categories and search_tools to find available tools",
}),
},
],
isError: true,
};
@@ -690,8 +716,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
@@ -700,9 +726,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
{
type: "text",
text: JSON.stringify({
error: `Tool execution failed: ${(error as Error).message}`
})
}
error: `Tool execution failed: ${(error as Error).message}`,
}),
},
],
isError: true,
};
@@ -734,12 +760,12 @@ Include tools that are:
**Examples by domain:**
| Domain | Direct Tools |
|--------|-------------|
| **KiCAD** | create_project, open_project, save_project, add_component, move_component, add_track, list_components, list_nets, get_board_info |
| **IDA Pro** | open_database, save_database, get_function, list_functions, add_comment, rename, get_xrefs, decompile |
| **Git** | status, add, commit, push, pull, checkout, branch, log |
| **Database** | connect, query, list_tables, describe_table |
| Domain | Direct Tools |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **KiCAD** | create_project, open_project, save_project, add_component, move_component, add_track, list_components, list_nets, get_board_info |
| **IDA Pro** | open_database, save_database, get_function, list_functions, add_comment, rename, get_xrefs, decompile |
| **Git** | status, add, commit, push, pull, checkout, branch, log |
| **Database** | connect, query, list_tables, describe_table |
### Routed Tools (Hidden Behind Router)
@@ -753,13 +779,13 @@ Include tools that are:
**Examples:**
| Category | Why Route It |
|----------|-------------|
| `export` | Only used at end of workflow |
| `drc/validation` | Used during review phase |
| `advanced_*` | Specialty operations |
| `bulk_*` | Batch operations |
| `config/settings` | One-time setup |
| Category | Why Route It |
| ----------------- | ---------------------------- |
| `export` | Only used at end of workflow |
| `drc/validation` | Used during review phase |
| `advanced_*` | Specialty operations |
| `bulk_*` | Batch operations |
| `config/settings` | One-time setup |
---
@@ -804,19 +830,19 @@ Include:
const categories = [
// Core operations (might be direct tools instead)
{ name: "project", description: "Project lifecycle: create, open, save, close" },
// Domain-specific operations
{ name: "analysis", description: "Analyze and inspect: find patterns, validate, check" },
{ name: "modification", description: "Modify and transform: edit, rename, restructure" },
{ name: "navigation", description: "Navigate and search: find, list, filter, locate" },
// Output operations
{ name: "export", description: "Export and generate: reports, files, documentation" },
{ name: "import", description: "Import from external sources: files, formats, APIs" },
// Configuration
{ name: "config", description: "Configuration and settings: preferences, rules, templates" },
// Advanced/specialized
{ name: "advanced", description: "Advanced operations: batch processing, automation, scripting" },
];
@@ -832,18 +858,18 @@ For an IDA Pro MCP server with 100+ tools:
```typescript
const directTools = [
"open_database", // Load IDB
"save_database", // Save IDB
"get_function", // Get function by address/name
"list_functions", // List all functions
"decompile", // Decompile function (Hex-Rays)
"add_comment", // Add comment at address
"rename", // Rename address/function
"get_xrefs_to", // Get cross-references to address
"get_xrefs_from", // Get cross-references from address
"get_strings", // List strings
"search_bytes", // Search for byte pattern
"get_segments", // List segments
"open_database", // Load IDB
"save_database", // Save IDB
"get_function", // Get function by address/name
"list_functions", // List all functions
"decompile", // Decompile function (Hex-Rays)
"add_comment", // Add comment at address
"rename", // Rename address/function
"get_xrefs_to", // Get cross-references to address
"get_xrefs_from", // Get cross-references from address
"get_strings", // List strings
"search_bytes", // Search for byte pattern
"get_segments", // List segments
];
```
@@ -854,52 +880,52 @@ const categories = [
{
name: "disassembly",
description: "Disassembly operations: undefine, make code/data, change types",
tools: ["make_code", "make_data", "undefine", "set_type", "make_array", "make_struct"]
tools: ["make_code", "make_data", "undefine", "set_type", "make_array", "make_struct"],
},
{
name: "functions",
description: "Function management: create, delete, modify boundaries, set types",
tools: ["create_function", "delete_function", "set_func_end", "set_func_type", "add_func_arg"]
tools: ["create_function", "delete_function", "set_func_end", "set_func_type", "add_func_arg"],
},
{
name: "types",
description: "Type system: structs, enums, typedefs, parse headers",
tools: ["create_struct", "add_struct_member", "create_enum", "parse_header", "import_types"]
tools: ["create_struct", "add_struct_member", "create_enum", "parse_header", "import_types"],
},
{
name: "patching",
description: "Binary patching: modify bytes, assemble, apply patches",
tools: ["patch_bytes", "patch_word", "patch_dword", "assemble", "apply_patches"]
tools: ["patch_bytes", "patch_word", "patch_dword", "assemble", "apply_patches"],
},
{
name: "scripting",
description: "IDAPython scripting: run scripts, evaluate expressions",
tools: ["run_script", "eval_python", "get_global", "set_global"]
tools: ["run_script", "eval_python", "get_global", "set_global"],
},
{
name: "signatures",
description: "Signatures and patterns: FLIRT, Lumina, create signatures",
tools: ["apply_flirt", "query_lumina", "create_sig", "find_pattern"]
tools: ["apply_flirt", "query_lumina", "create_sig", "find_pattern"],
},
{
name: "debugging",
description: "Debugger control: breakpoints, stepping, memory",
tools: ["set_breakpoint", "step_into", "step_over", "read_memory", "write_memory", "get_regs"]
tools: ["set_breakpoint", "step_into", "step_over", "read_memory", "write_memory", "get_regs"],
},
{
name: "export",
description: "Export: ASM listing, pseudocode, database info, reports",
tools: ["export_asm", "export_c", "export_json", "generate_report"]
tools: ["export_asm", "export_c", "export_json", "generate_report"],
},
{
name: "import",
description: "Import: symbols, types, comments from external sources",
tools: ["import_symbols", "import_pdb", "import_map", "import_comments"]
tools: ["import_symbols", "import_pdb", "import_map", "import_comments"],
},
{
name: "analysis",
description: "Analysis control: reanalyze, find patterns, auto-analysis settings",
tools: ["reanalyze", "find_crypto", "find_strings", "analyze_calls", "set_analysis_options"]
tools: ["reanalyze", "find_crypto", "find_strings", "analyze_calls", "set_analysis_options"],
},
];
```
@@ -962,19 +988,14 @@ Claude: "I've added length tuning meanders to match the trace lengths"
// tests/router.test.ts
import { describe, it, expect } from "vitest";
import {
searchTools,
getCategory,
getTool,
getAllCategories
} from "../src/tools/registry.js";
import { searchTools, getCategory, getTool, getAllCategories } from "../src/tools/registry.js";
import { routerTools } from "../src/tools/router.js";
describe("Tool Registry", () => {
it("should find tools by keyword", () => {
const results = searchTools("export");
expect(results.length).toBeGreaterThan(0);
expect(results.some(r => r.tool.includes("export"))).toBe(true);
expect(results.some((r) => r.tool.includes("export"))).toBe(true);
});
it("should return category info", () => {
@@ -997,16 +1018,16 @@ describe("Router Tools", () => {
});
it("get_category_tools returns tools for valid category", async () => {
const result = await routerTools.get_category_tools.handler({
category: "export"
const result = await routerTools.get_category_tools.handler({
category: "export",
});
expect(result.tools).toBeDefined();
expect(result.tools.length).toBeGreaterThan(0);
});
it("get_category_tools returns error for invalid category", async () => {
const result = await routerTools.get_category_tools.handler({
category: "nonexistent"
const result = await routerTools.get_category_tools.handler({
category: "nonexistent",
});
expect(result.error).toBeDefined();
});
@@ -1014,7 +1035,7 @@ describe("Router Tools", () => {
it("execute_tool runs valid tool", async () => {
const result = await routerTools.execute_tool.handler({
tool_name: "export_gerber",
params: { output_dir: "/tmp/test" }
params: { output_dir: "/tmp/test" },
});
expect(result.error).toBeUndefined();
});
@@ -1022,7 +1043,7 @@ describe("Router Tools", () => {
it("execute_tool returns error for invalid tool", async () => {
const result = await routerTools.execute_tool.handler({
tool_name: "nonexistent_tool",
params: {}
params: {},
});
expect(result.error).toBeDefined();
});
@@ -1141,11 +1162,11 @@ Before shipping your router-based MCP server:
## Summary
| Before | After |
|--------|-------|
| 100 tools visible | 15-18 tools visible |
| ~60K+ tokens consumed | ~10K tokens consumed |
| Tool selection confusion | Clear categories |
| Context starvation | Room for conversation |
| Before | After |
| ------------------------ | --------------------- |
| 100 tools visible | 15-18 tools visible |
| ~60K+ tokens consumed | ~10K tokens consumed |
| Tool selection confusion | Clear categories |
| Context starvation | Room for conversation |
The router pattern gives you unlimited tool capacity while keeping Claude focused and efficient.

285
download_jlcpcb.py Normal file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Download JLCPCB parts database from yaqwsx/jlcparts pre-built cache.
This downloads the full JLCPCB catalog (~421MB compressed, ~1.5GB SQLite)
from GitHub Pages in ~5 minutes instead of the broken JLCSearch API approach.
The cache.sqlite3 file contains all JLCPCB parts with stock, pricing,
and category data. We then convert it into the format expected by the
KiCad MCP server's JLCPCBPartsManager.
"""
import json
import os
import sqlite3
import subprocess
import sys
import time
from pathlib import Path
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(exist_ok=True)
CACHE_DIR = DATA_DIR / "jlcparts_cache"
CACHE_DIR.mkdir(exist_ok=True)
BASE_URL = "https://yaqwsx.github.io/jlcparts/data"
PARTS = [f"cache.z{i:02d}" for i in range(1, 20)] + ["cache.zip"]
TARGET_DB = DATA_DIR / "jlcpcb_parts.db"
def download_files():
"""Download all split archive parts."""
print("Downloading jlcparts database (~421MB)...")
for part in PARTS:
dest = CACHE_DIR / part
if dest.exists() and dest.stat().st_size > 1000:
print(f" {part} already exists, skipping")
continue
url = f"{BASE_URL}/{part}"
print(f" Downloading {part}...")
result = subprocess.run(
["curl", "-L", "-o", str(dest), "--progress-bar", url], capture_output=False
)
if result.returncode != 0:
print(f" ERROR downloading {part}")
return False
return True
def extract_database():
"""Extract the split 7z archive to get cache.sqlite3."""
cache_sqlite = CACHE_DIR / "cache.sqlite3"
if cache_sqlite.exists() and cache_sqlite.stat().st_size > 100_000_000:
print(f"cache.sqlite3 already extracted ({cache_sqlite.stat().st_size // (1024*1024)}MB)")
return True
print("Extracting archive (requires 7z or p7zip)...")
# Try 7z first, then 7zz (homebrew)
for cmd in ["7z", "7zz", "7za"]:
try:
result = subprocess.run(
[cmd, "x", "-y", "-o" + str(CACHE_DIR), str(CACHE_DIR / "cache.zip")],
capture_output=True,
text=True,
)
if result.returncode == 0:
print(f"Extracted with {cmd}")
return True
else:
print(f" {cmd} failed: {result.stderr[:200]}")
except FileNotFoundError:
continue
print("\nERROR: 7z not found. Install with: brew install p7zip")
return False
def convert_to_mcp_format():
"""Convert jlcparts cache.sqlite3 to the MCP server's expected format."""
source = CACHE_DIR / "cache.sqlite3"
if not source.exists():
print("ERROR: cache.sqlite3 not found")
return False
print(f"Reading source database...")
src = sqlite3.connect(str(source))
src.row_factory = sqlite3.Row
# Check schema
tables = [
r[0] for r in src.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
]
print(f" Source tables: {tables}")
# Find the main components table
comp_table = None
for t in tables:
count = src.execute(f"SELECT COUNT(*) FROM [{t}]").fetchone()[0]
print(f" {t}: {count:,} rows")
if count > 10000 and comp_table is None:
comp_table = t
if not comp_table:
# Try 'components' specifically
comp_table = "components" if "components" in tables else tables[0]
# Get column names
cols = [r[1] for r in src.execute(f"PRAGMA table_info([{comp_table}])").fetchall()]
print(f" Using table '{comp_table}' with columns: {cols[:10]}...")
# Remove old target DB
if TARGET_DB.exists():
TARGET_DB.unlink()
# Create target DB in MCP format
dst = sqlite3.connect(str(TARGET_DB))
dst.execute("""
CREATE TABLE components (
lcsc TEXT PRIMARY KEY,
category TEXT,
subcategory TEXT,
mfr_part TEXT,
package TEXT,
solder_joints INTEGER,
manufacturer TEXT,
library_type TEXT,
description TEXT,
datasheet TEXT,
stock INTEGER,
price_json TEXT,
last_updated INTEGER
)
""")
dst.execute("CREATE INDEX idx_category ON components(category, subcategory)")
dst.execute("CREATE INDEX idx_package ON components(package)")
dst.execute("CREATE INDEX idx_manufacturer ON components(manufacturer)")
dst.execute("CREATE INDEX idx_library_type ON components(library_type)")
dst.execute("CREATE INDEX idx_mfr_part ON components(mfr_part)")
# Map source columns to our schema
# jlcparts schema varies but commonly has:
# lcsc, mfr, description, joint, manufacturer, basic, preferred, stock, price, url, etc.
print(f"\nConverting parts to MCP format...")
now = int(time.time())
batch = []
count = 0
for row in src.execute(f"SELECT * FROM [{comp_table}]"):
row_dict = dict(row)
# Adapt column names (jlcparts uses various schemas)
lcsc = row_dict.get("lcsc") or row_dict.get("LCSC_Part") or row_dict.get("lcsc_id")
if lcsc is None:
continue
if isinstance(lcsc, int):
lcsc = f"C{lcsc}"
elif not str(lcsc).startswith("C"):
lcsc = f"C{lcsc}"
mfr_part = row_dict.get("mfr") or row_dict.get("MFR_Part") or row_dict.get("mfr_part") or ""
package = row_dict.get("package") or row_dict.get("Package") or ""
manufacturer = row_dict.get("manufacturer") or row_dict.get("Manufacturer") or ""
description = row_dict.get("description") or row_dict.get("Description") or ""
stock = row_dict.get("stock") or row_dict.get("Stock") or 0
category = row_dict.get("category") or row_dict.get("First Category") or ""
subcategory = row_dict.get("subcategory") or row_dict.get("Second Category") or ""
datasheet = row_dict.get("datasheet") or row_dict.get("url") or ""
# Library type
is_basic = row_dict.get("basic") or row_dict.get("is_basic") or row_dict.get("Basic")
is_preferred = (
row_dict.get("preferred") or row_dict.get("is_preferred") or row_dict.get("Preferred")
)
if is_basic:
lib_type = "Basic"
elif is_preferred:
lib_type = "Preferred"
else:
lib_type = "Extended"
# Price
price = row_dict.get("price") or row_dict.get("Price") or 0
price_json = json.dumps([{"qty": 1, "price": price}] if price else [])
batch.append(
(
str(lcsc),
category,
subcategory,
mfr_part,
package,
0,
manufacturer,
lib_type,
description,
datasheet,
int(stock) if stock else 0,
price_json,
now,
)
)
if len(batch) >= 10000:
dst.executemany(
"""
INSERT OR REPLACE INTO components
(lcsc, category, subcategory, mfr_part, package,
solder_joints, manufacturer, library_type, description,
datasheet, stock, price_json, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
batch,
)
count += len(batch)
batch = []
if count % 100000 == 0:
print(f" Converted {count:,} parts...")
if batch:
dst.executemany(
"""
INSERT OR REPLACE INTO components
(lcsc, category, subcategory, mfr_part, package,
solder_joints, manufacturer, library_type, description,
datasheet, stock, price_json, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
batch,
)
count += len(batch)
# Build FTS index
print(f" Building full-text search index...")
dst.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5(
lcsc, description, mfr_part, manufacturer,
content=components
)
""")
dst.execute("INSERT INTO components_fts(components_fts) VALUES('rebuild')")
dst.commit()
# Stats
total = dst.execute("SELECT COUNT(*) FROM components").fetchone()[0]
basic = dst.execute("SELECT COUNT(*) FROM components WHERE library_type='Basic'").fetchone()[0]
extended = dst.execute(
"SELECT COUNT(*) FROM components WHERE library_type='Extended'"
).fetchone()[0]
dst.close()
src.close()
db_size = TARGET_DB.stat().st_size / (1024 * 1024)
print(f"\nDatabase ready: {TARGET_DB}")
print(f" Total parts: {total:,}")
print(f" Basic parts: {basic:,}")
print(f" Extended parts: {extended:,}")
print(f" DB size: {db_size:.1f} MB")
return True
def main():
print("=" * 60)
print("JLCPCB Parts Database Downloader (jlcparts method)")
print("=" * 60)
start = time.time()
if not download_files():
sys.exit(1)
if not extract_database():
sys.exit(1)
if not convert_to_mcp_format():
sys.exit(1)
elapsed = time.time() - start
print(f"\nTotal time: {elapsed/60:.1f} minutes")
print("Done! Restart the MCP server (/mcp) to use the new database.")
if __name__ == "__main__":
main()

19
eslint.config.js Normal file
View File

@@ -0,0 +1,19 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-unsafe-function-type": "off",
"preserve-caught-error": "off",
},
},
{
ignores: ["dist/", "node_modules/", "**/*.js", "!eslint.config.js"],
},
);

4411
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,53 @@
{
"name": "kicad-mcp",
"version": "2.1.0-alpha",
"description": "AI-assisted PCB design with KiCAD via Model Context Protocol",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"start": "node dist/index.js",
"dev": "npm run build:watch & nodemon dist/index.js",
"clean": "rm -rf dist",
"rebuild": "npm run clean && npm run build",
"test": "npm run test:ts && npm run test:py",
"test:ts": "echo 'TypeScript tests not yet configured'",
"test:py": "pytest tests/ -v",
"test:coverage": "pytest tests/ --cov=python --cov-report=html --cov-report=term",
"lint": "npm run lint:ts && npm run lint:py",
"lint:ts": "eslint src/ || echo 'ESLint not configured'",
"lint:py": "cd python && black . && mypy . && flake8 .",
"format": "prettier --write 'src/**/*.ts' && black python/",
"prepare": "npm run build",
"pretest": "npm run build"
},
"keywords": [
"kicad",
"mcp",
"model-context-protocol",
"pcb-design",
"ai",
"claude"
],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.21.0",
"dotenv": "^17.0.0",
"express": "^5.1.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@types/express": "^5.0.5",
"@types/glob": "^8.1.0",
"@types/node": "^20.19.0",
"nodemon": "^3.0.1",
"typescript": "^5.9.3"
}
}
{
"name": "kicad-mcp",
"version": "2.1.0-alpha",
"description": "AI-assisted PCB design with KiCAD via Model Context Protocol",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"start": "node dist/index.js",
"dev": "npm run build:watch & nodemon dist/index.js",
"clean": "rm -rf dist",
"rebuild": "npm run clean && npm run build",
"test": "npm run test:ts && npm run test:py",
"test:ts": "echo 'TypeScript tests not yet configured'",
"test:py": "pytest tests/ -v",
"test:coverage": "pytest tests/ --cov=python --cov-report=html --cov-report=term",
"lint": "npm run lint:ts && npm run lint:py",
"lint:ts": "eslint src/",
"lint:py": "cd python && black . && mypy . && flake8 .",
"format": "prettier --write 'src/**/*.ts' && black python/",
"prepare": "npm run build",
"pretest": "npm run build"
},
"keywords": [
"kicad",
"mcp",
"model-context-protocol",
"pcb-design",
"ai",
"claude"
],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.21.0",
"dotenv": "^17.0.0",
"express": "^5.1.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@eslint/js": "^10.0.1",
"@types/express": "^5.0.5",
"@types/glob": "^8.1.0",
"@types/node": "^20.19.0",
"eslint": "^10.1.0",
"nodemon": "^3.0.1",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.2"
}
}

42
pyproject.toml Normal file
View File

@@ -0,0 +1,42 @@
[project]
name = "kicad-mcp-server"
version = "2.1.0"
requires-python = ">=3.10"
[tool.black]
line-length = 100
target-version = ["py310"]
[tool.isort]
profile = "black"
line_length = 100
[tool.mypy]
python_version = "3.10"
warn_return_any = false
warn_unused_configs = true
check_untyped_defs = false
disallow_untyped_defs = false
explicit_package_bases = true
mypy_path = ".:python"
disable_error_code = [
"arg-type",
"attr-defined",
"union-attr",
"var-annotated",
"index",
]
[[tool.mypy.overrides]]
module = [
"pcbnew",
"cairosvg",
"sexpdata",
"skip",
"kipy",
"kipy.*",
"schematic",
"PIL",
"PIL.*",
]
ignore_missing_imports = true

View File

@@ -7,7 +7,7 @@ python_classes = Test*
python_functions = test_*
# Test paths
testpaths = tests python/tests
testpaths = tests
# Minimum Python version
minversion = 6.0

View File

@@ -2,18 +2,18 @@
KiCAD command implementations package
"""
from .project import ProjectCommands
from .board import BoardCommands
from .component import ComponentCommands
from .routing import RoutingCommands
from .design_rules import DesignRuleCommands
from .export import ExportCommands
from .project import ProjectCommands
from .routing import RoutingCommands
__all__ = [
'ProjectCommands',
'BoardCommands',
'ComponentCommands',
'RoutingCommands',
'DesignRuleCommands',
'ExportCommands'
"ProjectCommands",
"BoardCommands",
"ComponentCommands",
"RoutingCommands",
"DesignRuleCommands",
"ExportCommands",
]

View File

@@ -8,4 +8,4 @@ It imports and re-exports the BoardCommands class from the board package.
from commands.board import BoardCommands
# Re-export the BoardCommands class for backward compatibility
__all__ = ['BoardCommands']
__all__ = ["BoardCommands"]

View File

@@ -2,17 +2,20 @@
Board-related command implementations for KiCAD interface
"""
import pcbnew
import logging
from typing import Dict, Any, Optional
from typing import Any, Dict, Optional
import pcbnew
from .layers import BoardLayerCommands
from .outline import BoardOutlineCommands
# Import specialized modules
from .size import BoardSizeCommands
from .layers import BoardLayerCommands
from .outline import BoardOutlineCommands
from .view import BoardViewCommands
logger = logging.getLogger('kicad_interface')
logger = logging.getLogger("kicad_interface")
class BoardCommands:
"""Handles board-related KiCAD operations"""
@@ -20,63 +23,63 @@ class BoardCommands:
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
# Initialize specialized command classes
self.size_commands = BoardSizeCommands(board)
self.layer_commands = BoardLayerCommands(board)
self.outline_commands = BoardOutlineCommands(board)
self.view_commands = BoardViewCommands(board)
# Delegate board size commands
def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Set the size of the PCB board"""
self.size_commands.board = self.board
return self.size_commands.set_board_size(params)
# Delegate layer commands
def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a new layer to the PCB"""
self.layer_commands.board = self.board
return self.layer_commands.add_layer(params)
def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Set the active layer for PCB operations"""
self.layer_commands.board = self.board
return self.layer_commands.set_active_layer(params)
def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get a list of all layers in the PCB"""
self.layer_commands.board = self.board
return self.layer_commands.get_layer_list(params)
# Delegate board outline commands
def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a board outline to the PCB"""
self.outline_commands.board = self.board
return self.outline_commands.add_board_outline(params)
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a mounting hole to the PCB"""
self.outline_commands.board = self.board
return self.outline_commands.add_mounting_hole(params)
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add text annotation to the PCB"""
self.outline_commands.board = self.board
return self.outline_commands.add_text(params)
# Delegate view commands
def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get information about the current board"""
self.view_commands.board = self.board
return self.view_commands.get_board_info(params)
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get a 2D image of the PCB"""
self.view_commands.board = self.board
return self.view_commands.get_board_2d_view(params)
def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get the bounding box extents of the board"""
self.view_commands.board = self.board
return self.view_commands.get_board_extents(params)
def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get the bounding box extents of the board"""
self.view_commands.board = self.board
return self.view_commands.get_board_extents(params)

View File

@@ -2,11 +2,13 @@
Board layer command implementations for KiCAD interface
"""
import pcbnew
import logging
from typing import Dict, Any, Optional
from typing import Any, Dict, Optional
import pcbnew
logger = logging.getLogger("kicad_interface")
logger = logging.getLogger('kicad_interface')
class BoardLayerCommands:
"""Handles board layer operations"""
@@ -22,7 +24,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
"errorDetails": "Load or create a board first",
}
name = params.get("name")
@@ -34,7 +36,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "name, type, and position are required"
"errorDetails": "name, type, and position are required",
}
# Get layer stack
@@ -47,7 +49,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "Missing layer number",
"errorDetails": "number is required for inner layers"
"errorDetails": "number is required for inner layers",
}
layer_id = pcbnew.In1_Cu + (number - 1)
elif position == "top":
@@ -59,34 +61,25 @@ class BoardLayerCommands:
return {
"success": False,
"message": "Invalid layer position",
"errorDetails": "position must be 'top', 'bottom', or 'inner'"
"errorDetails": "position must be 'top', 'bottom', or 'inner'",
}
# Set layer properties
layer_stack.SetLayerName(layer_id, name)
layer_stack.SetLayerType(layer_id, self._get_layer_type(layer_type))
# Enable the layer
self.board.SetLayerEnabled(layer_id, True)
return {
"success": True,
"message": f"Added layer: {name}",
"layer": {
"name": name,
"type": layer_type,
"position": position,
"number": number
}
"layer": {"name": name, "type": layer_type, "position": position, "number": number},
}
except Exception as e:
logger.error(f"Error adding layer: {str(e)}")
return {
"success": False,
"message": "Failed to add layer",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to add layer", "errorDetails": str(e)}
def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Set the active layer for PCB operations"""
@@ -95,7 +88,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
"errorDetails": "Load or create a board first",
}
layer = params.get("layer")
@@ -103,7 +96,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "No layer specified",
"errorDetails": "layer parameter is required"
"errorDetails": "layer parameter is required",
}
# Find layer ID by name
@@ -112,7 +105,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "Layer not found",
"errorDetails": f"Layer '{layer}' does not exist"
"errorDetails": f"Layer '{layer}' does not exist",
}
# Set active layer
@@ -121,10 +114,7 @@ class BoardLayerCommands:
return {
"success": True,
"message": f"Set active layer to: {layer}",
"layer": {
"name": layer,
"id": layer_id
}
"layer": {"name": layer, "id": layer_id},
}
except Exception as e:
@@ -132,7 +122,7 @@ class BoardLayerCommands:
return {
"success": False,
"message": "Failed to set active layer",
"errorDetails": str(e)
"errorDetails": str(e),
}
def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
@@ -142,40 +132,35 @@ class BoardLayerCommands:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
"errorDetails": "Load or create a board first",
}
layers = []
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
if self.board.IsLayerEnabled(layer_id):
layers.append({
"name": self.board.GetLayerName(layer_id),
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
"id": layer_id
# Note: isActive removed - GetActiveLayer() doesn't exist in KiCAD 9.0
# Active layer is a UI concept not applicable to headless scripting
})
layers.append(
{
"name": self.board.GetLayerName(layer_id),
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
"id": layer_id,
# Note: isActive removed - GetActiveLayer() doesn't exist in KiCAD 9.0
# Active layer is a UI concept not applicable to headless scripting
}
)
return {
"success": True,
"layers": layers
}
return {"success": True, "layers": layers}
except Exception as e:
logger.error(f"Error getting layer list: {str(e)}")
return {
"success": False,
"message": "Failed to get layer list",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to get layer list", "errorDetails": str(e)}
def _get_layer_type(self, type_name: str) -> int:
"""Convert layer type name to KiCAD layer type constant"""
type_map = {
"copper": pcbnew.LT_SIGNAL,
"technical": pcbnew.LT_SIGNAL,
"user": pcbnew.LT_SIGNAL, # LT_USER removed in KiCAD 9.0, use LT_SIGNAL instead
"signal": pcbnew.LT_SIGNAL
"signal": pcbnew.LT_SIGNAL,
}
return type_map.get(type_name.lower(), pcbnew.LT_SIGNAL)
@@ -185,7 +170,7 @@ class BoardLayerCommands:
pcbnew.LT_SIGNAL: "signal",
pcbnew.LT_POWER: "power",
pcbnew.LT_MIXED: "mixed",
pcbnew.LT_JUMPER: "jumper"
pcbnew.LT_JUMPER: "jumper",
}
# Note: LT_USER was removed in KiCAD 9.0
return type_map.get(type_id, "unknown")

View File

@@ -2,10 +2,11 @@
Board outline command implementations for KiCAD interface
"""
import pcbnew
import logging
import math
from typing import Dict, Any, Optional
from typing import Any, Dict, Optional
import pcbnew
logger = logging.getLogger("kicad_interface")
@@ -224,9 +225,7 @@ class BoardOutlineCommands:
}
# Convert to internal units (nanometers)
scale = (
1000000 if position.get("unit", "mm") == "mm" else 25400000
) # mm or inch to nm
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
diameter_nm = int(diameter * scale)
@@ -252,9 +251,7 @@ class BoardOutlineCommands:
pad = pcbnew.PAD(module)
pad.SetNumber(1)
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
pad.SetAttribute(
pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH
)
pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
@@ -311,9 +308,7 @@ class BoardOutlineCommands:
}
# Convert to internal units (nanometers)
scale = (
1000000 if position.get("unit", "mm") == "mm" else 25400000
) # mm or inch to nm
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
size_nm = int(size * scale)
@@ -372,9 +367,7 @@ class BoardOutlineCommands:
"errorDetails": str(e),
}
def _add_edge_line(
self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int
) -> None:
def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None:
"""Add a line to the edge cuts layer"""
line = pcbnew.PCB_SHAPE(self.board)
line.SetShape(pcbnew.SHAPE_T_SEGMENT)
@@ -396,18 +389,12 @@ class BoardOutlineCommands:
"""Add a rounded rectangle to the edge cuts layer"""
if radius_nm <= 0:
# If no radius, create regular rectangle
top_left = pcbnew.VECTOR2I(
center_x_nm - width_nm // 2, center_y_nm - height_nm // 2
)
top_right = pcbnew.VECTOR2I(
center_x_nm + width_nm // 2, center_y_nm - height_nm // 2
)
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
bottom_right = pcbnew.VECTOR2I(
center_x_nm + width_nm // 2, center_y_nm + height_nm // 2
)
bottom_left = pcbnew.VECTOR2I(
center_x_nm - width_nm // 2, center_y_nm + height_nm // 2
)
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
self._add_edge_line(top_left, top_right, layer)
self._add_edge_line(top_right, bottom_right, layer)

View File

@@ -2,11 +2,13 @@
Board size command implementations for KiCAD interface
"""
import pcbnew
import logging
from typing import Dict, Any, Optional
from typing import Any, Dict, Optional
import pcbnew
logger = logging.getLogger("kicad_interface")
logger = logging.getLogger('kicad_interface')
class BoardSizeCommands:
"""Handles board size operations"""
@@ -22,7 +24,7 @@ class BoardSizeCommands:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
"errorDetails": "Load or create a board first",
}
width = params.get("width")
@@ -33,41 +35,36 @@ class BoardSizeCommands:
return {
"success": False,
"message": "Missing dimensions",
"errorDetails": "Both width and height are required"
"errorDetails": "Both width and height are required",
}
# Create board outline using BoardOutlineCommands
# This properly creates edge cuts on Edge.Cuts layer
from commands.board.outline import BoardOutlineCommands
outline_commands = BoardOutlineCommands(self.board)
# Create rectangular outline centered at origin
result = outline_commands.add_board_outline({
"shape": "rectangle",
"centerX": width / 2, # Center X
"centerY": height / 2, # Center Y
"width": width,
"height": height,
"unit": unit
})
result = outline_commands.add_board_outline(
{
"shape": "rectangle",
"centerX": width / 2, # Center X
"centerY": height / 2, # Center Y
"width": width,
"height": height,
"unit": unit,
}
)
if result.get("success"):
return {
"success": True,
"message": f"Created board outline: {width}x{height} {unit}",
"size": {
"width": width,
"height": height,
"unit": unit
}
"size": {"width": width, "height": height, "unit": unit},
}
else:
return result
except Exception as e:
logger.error(f"Error setting board size: {str(e)}")
return {
"success": False,
"message": "Failed to set board size",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to set board size", "errorDetails": str(e)}

View File

@@ -2,15 +2,17 @@
Board view command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
from PIL import Image
import io
import base64
import io
import logging
import os
from typing import Any, Dict, List, Optional, Tuple
import pcbnew
from PIL import Image
logger = logging.getLogger("kicad_interface")
logger = logging.getLogger('kicad_interface')
class BoardViewCommands:
"""Handles board viewing operations"""
@@ -26,7 +28,7 @@ class BoardViewCommands:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
"errorDetails": "Load or create a board first",
}
# Get board dimensions
@@ -42,26 +44,24 @@ class BoardViewCommands:
layers = []
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
if self.board.IsLayerEnabled(layer_id):
layers.append({
"name": self.board.GetLayerName(layer_id),
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
"id": layer_id
})
layers.append(
{
"name": self.board.GetLayerName(layer_id),
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
"id": layer_id,
}
)
return {
"success": True,
"board": {
"filename": self.board.GetFileName(),
"size": {
"width": width_mm,
"height": height_mm,
"unit": "mm"
},
"size": {"width": width_mm, "height": height_mm, "unit": "mm"},
"layers": layers,
"title": self.board.GetTitleBlock().GetTitle()
"title": self.board.GetTitleBlock().GetTitle(),
# Note: activeLayer removed - GetActiveLayer() doesn't exist in KiCAD 9.0
# Active layer is a UI concept not applicable to headless scripting
}
},
}
except Exception as e:
@@ -69,7 +69,7 @@ class BoardViewCommands:
return {
"success": False,
"message": "Failed to get board information",
"errorDetails": str(e)
"errorDetails": str(e),
}
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
@@ -79,7 +79,7 @@ class BoardViewCommands:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
"errorDetails": "Load or create a board first",
}
# Get parameters
@@ -90,7 +90,7 @@ class BoardViewCommands:
# Create plot controller
plotter = pcbnew.PLOT_CONTROLLER(self.board)
# Set up plot options
plot_opts = plotter.GetPlotOptions()
plot_opts.SetOutputDirectory(os.path.dirname(self.board.GetFileName()))
@@ -100,7 +100,7 @@ class BoardViewCommands:
plot_opts.SetPlotFrameRef(False)
plot_opts.SetPlotValue(True)
plot_opts.SetPlotReference(True)
# Plot to SVG first (for vector output)
# Note: KiCAD 9.0 prepends the project name to the filename, so we use GetPlotFileName() to get the actual path
plotter.OpenPlotfile("temp_view", pcbnew.PLOT_FORMAT_SVG, "Temporary View")
@@ -126,36 +126,33 @@ class BoardViewCommands:
# Convert SVG to requested format
if format == "svg":
with open(temp_svg, 'r') as f:
with open(temp_svg, "r") as f:
svg_data = f.read()
os.remove(temp_svg)
return {
"success": True,
"imageData": svg_data,
"format": "svg"
}
return {"success": True, "imageData": svg_data, "format": "svg"}
else:
# Use PIL to convert SVG to PNG/JPG
from cairosvg import svg2png
png_data = svg2png(url=temp_svg, output_width=width, output_height=height)
os.remove(temp_svg)
if format == "jpg":
# Convert PNG to JPG
img = Image.open(io.BytesIO(png_data))
jpg_buffer = io.BytesIO()
img.convert('RGB').save(jpg_buffer, format='JPEG')
img.convert("RGB").save(jpg_buffer, format="JPEG")
jpg_data = jpg_buffer.getvalue()
return {
"success": True,
"imageData": base64.b64encode(jpg_data).decode('utf-8'),
"format": "jpg"
"imageData": base64.b64encode(jpg_data).decode("utf-8"),
"format": "jpg",
}
else:
return {
"success": True,
"imageData": base64.b64encode(png_data).decode('utf-8'),
"format": "png"
"imageData": base64.b64encode(png_data).decode("utf-8"),
"format": "png",
}
except Exception as e:
@@ -163,70 +160,67 @@ class BoardViewCommands:
return {
"success": False,
"message": "Failed to get board 2D view",
"errorDetails": str(e)
"errorDetails": str(e),
}
def _get_layer_type_name(self, type_id: int) -> str:
"""Convert KiCAD layer type constant to name"""
type_map = {
pcbnew.LT_SIGNAL: "signal",
pcbnew.LT_POWER: "power",
pcbnew.LT_MIXED: "mixed",
pcbnew.LT_JUMPER: "jumper"
pcbnew.LT_JUMPER: "jumper",
}
# Note: LT_USER was removed in KiCAD 9.0
return type_map.get(type_id, "unknown")
def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get the bounding box extents of the board"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
# Get unit preference (default to mm)
unit = params.get("unit", "mm")
scale = 1000000 if unit == "mm" else 25400000 # nm to mm or inch
# Get board bounding box
board_box = self.board.GetBoardEdgesBoundingBox()
# Extract bounds in nanometers, then convert
left = board_box.GetLeft() / scale
top = board_box.GetTop() / scale
right = board_box.GetRight() / scale
bottom = board_box.GetBottom() / scale
width = board_box.GetWidth() / scale
height = board_box.GetHeight() / scale
# Get center point
center_x = board_box.GetCenter().x / scale
center_y = board_box.GetCenter().y / scale
return {
"success": True,
"extents": {
"left": left,
"top": top,
"right": right,
"bottom": bottom,
"width": width,
"height": height,
"center": {
"x": center_x,
"y": center_y
},
"unit": unit
}
}
except Exception as e:
logger.error(f"Error getting board extents: {str(e)}")
return {
"success": False,
"message": "Failed to get board extents",
"errorDetails": str(e)
}
def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get the bounding box extents of the board"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first",
}
# Get unit preference (default to mm)
unit = params.get("unit", "mm")
scale = 1000000 if unit == "mm" else 25400000 # nm to mm or inch
# Get board bounding box
board_box = self.board.GetBoardEdgesBoundingBox()
# Extract bounds in nanometers, then convert
left = board_box.GetLeft() / scale
top = board_box.GetTop() / scale
right = board_box.GetRight() / scale
bottom = board_box.GetBottom() / scale
width = board_box.GetWidth() / scale
height = board_box.GetHeight() / scale
# Get center point
center_x = board_box.GetCenter().x / scale
center_y = board_box.GetCenter().y / scale
return {
"success": True,
"extents": {
"left": left,
"top": top,
"right": right,
"bottom": bottom,
"width": width,
"height": height,
"center": {"x": center_x, "y": center_y},
"unit": unit,
},
}
except Exception as e:
logger.error(f"Error getting board extents: {str(e)}")
return {
"success": False,
"message": "Failed to get board extents",
"errorDetails": str(e),
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
from skip import Schematic
import logging
import os
import uuid
import logging
from pathlib import Path
from typing import Optional
from typing import Any, Dict, List, Optional, Tuple
from skip import Schematic
logger = logging.getLogger(__name__)
@@ -13,9 +14,7 @@ try:
DYNAMIC_LOADING_AVAILABLE = True
except ImportError:
logger.warning(
"Dynamic symbol loader not available - falling back to template-only mode"
)
logger.warning("Dynamic symbol loader not available - falling back to template-only mode")
DYNAMIC_LOADING_AVAILABLE = False
@@ -26,7 +25,7 @@ class ComponentManager:
_dynamic_loader = None
@classmethod
def get_dynamic_loader(cls):
def get_dynamic_loader(cls) -> Any:
"""Get or create dynamic symbol loader instance"""
if cls._dynamic_loader is None and DYNAMIC_LOADING_AVAILABLE:
cls._dynamic_loader = DynamicSymbolLoader()
@@ -87,7 +86,7 @@ class ComponentManager:
"""
# Helper function to check if template exists in schematic
def template_exists(schematic, template_ref):
def template_exists(schematic: Any, template_ref: str) -> bool:
"""Check if template exists by iterating symbols (handles special characters)"""
for symbol in schematic.symbol:
if (
@@ -135,32 +134,22 @@ class ComponentManager:
# Check if schematic path is available
if schematic_path is None:
logger.warning(
"Dynamic loading requires schematic file path but none was provided"
)
logger.warning("Dynamic loading requires schematic file path but none was provided")
fallback = cls.TEMPLATE_MAP.get(comp_type, "_TEMPLATE_R")
return (fallback, False)
# Determine library name
if library is None:
# Default library for common component types
library = (
"Device" # Most passives and basic components are in Device library
)
library = "Device" # Most passives and basic components are in Device library
try:
logger.info(
f"Attempting dynamic load: {library}:{comp_type} from {schematic_path}"
)
logger.info(f"Attempting dynamic load: {library}:{comp_type} from {schematic_path}")
# Use dynamic symbol loader to inject symbol and create template
template_ref = loader.load_symbol_dynamically(
schematic_path, library, comp_type
)
template_ref = loader.load_symbol_dynamically(schematic_path, library, comp_type)
logger.info(
f"Successfully loaded symbol dynamically. Template ref: {template_ref}"
)
logger.info(f"Successfully loaded symbol dynamically. Template ref: {template_ref}")
# Signal that schematic needs reload to see new template
return (template_ref, True)
@@ -176,7 +165,7 @@ class ComponentManager:
@staticmethod
def add_component(
schematic: Schematic, component_def: dict, schematic_path: Optional[Path] = None
):
) -> Any:
"""
Add a component to the schematic by cloning from template
@@ -198,9 +187,7 @@ class ComponentManager:
# Get component type and determine template
comp_type = component_def.get("type", "R")
library = component_def.get(
"library", None
) # Optional library specification
library = component_def.get("library", None) # Optional library specification
# Get template reference (static or dynamic)
template_ref, needs_reload = ComponentManager.get_or_create_template(
@@ -209,9 +196,7 @@ class ComponentManager:
# If dynamic loading occurred, reload schematic to see new template
if needs_reload and schematic_path:
logger.info(
f"Reloading schematic after dynamic loading: {schematic_path}"
)
logger.info(f"Reloading schematic after dynamic loading: {schematic_path}")
schematic = SchematicManager.load_schematic(str(schematic_path))
# Find template symbol by reference (handles special characters like +)
@@ -280,7 +265,7 @@ class ComponentManager:
raise
@staticmethod
def remove_component(schematic: Schematic, component_ref: str):
def remove_component(schematic: Schematic, component_ref: str) -> bool:
"""Remove a component from the schematic by reference designator"""
try:
# kicad-skip doesn't have a direct remove_symbol method by reference.
@@ -293,19 +278,17 @@ class ComponentManager:
if symbol_to_remove:
schematic.symbol._elements.remove(symbol_to_remove)
print(f"Removed component {component_ref} from schematic.")
logger.info(f"Removed component {component_ref} from schematic.")
return True
else:
print(f"Component with reference {component_ref} not found.")
logger.warning(f"Component with reference {component_ref} not found.")
return False
except Exception as e:
print(f"Error removing component {component_ref}: {e}")
logger.error(f"Error removing component {component_ref}: {e}")
return False
@staticmethod
def update_component(
schematic: Schematic, component_ref: str, new_properties: dict
):
def update_component(schematic: Schematic, component_ref: str, new_properties: dict) -> bool:
"""Update component properties by reference designator"""
try:
symbol_to_update = None
@@ -319,29 +302,28 @@ class ComponentManager:
if key in symbol_to_update.property:
symbol_to_update.property[key].value = value
else:
# Add as a new property if it doesn't exist
symbol_to_update.property.append(key, value)
print(f"Updated properties for component {component_ref}.")
logger.info(f"Updated properties for component {component_ref}.")
return True
else:
print(f"Component with reference {component_ref} not found.")
logger.warning(f"Component with reference {component_ref} not found.")
return False
except Exception as e:
print(f"Error updating component {component_ref}: {e}")
logger.error(f"Error updating component {component_ref}: {e}")
return False
@staticmethod
def get_component(schematic: Schematic, component_ref: str):
def get_component(schematic: Schematic, component_ref: str) -> Any:
"""Get a component by reference designator"""
for symbol in schematic.symbol:
if symbol.reference == component_ref:
print(f"Found component with reference {component_ref}.")
logger.debug(f"Found component with reference {component_ref}.")
return symbol
print(f"Component with reference {component_ref} not found.")
logger.warning(f"Component with reference {component_ref} not found.")
return None
@staticmethod
def search_components(schematic: Schematic, query: str):
def search_components(schematic: Schematic, query: str) -> List[Any]:
"""Search for components matching criteria (basic implementation)"""
# This is a basic search, could be expanded to use regex or more complex logic
matching_components = []
@@ -356,21 +338,21 @@ class ComponentManager:
)
):
matching_components.append(symbol)
print(f"Found {len(matching_components)} components matching query '{query}'.")
logger.debug(f"Found {len(matching_components)} components matching query '{query}'.")
return matching_components
@staticmethod
def get_all_components(schematic: Schematic):
def get_all_components(schematic: Schematic) -> List[Any]:
"""Get all components in schematic"""
print(f"Retrieving all {len(schematic.symbol)} components.")
logger.debug(f"Retrieving all {len(schematic.symbol)} components.")
return list(schematic.symbol)
if __name__ == "__main__":
# Example Usage (for testing)
from schematic import (
from schematic import ( # Assuming schematic.py is in the same directory
SchematicManager,
) # Assuming schematic.py is in the same directory
)
# Create a new schematic
test_sch = SchematicManager.create_schematic("ComponentTestSchematic")
@@ -401,19 +383,13 @@ if __name__ == "__main__":
# Get a component
retrieved_comp = ComponentManager.get_component(test_sch, "C1")
if retrieved_comp:
print(
f"Retrieved component: {retrieved_comp.reference} ({retrieved_comp.value})"
)
print(f"Retrieved component: {retrieved_comp.reference} ({retrieved_comp.value})")
# Update a component
ComponentManager.update_component(
test_sch, "R1", {"value": "20k", "Tolerance": "5%"}
)
ComponentManager.update_component(test_sch, "R1", {"value": "20k", "Tolerance": "5%"})
# Search components
matching_comps = ComponentManager.search_components(
test_sch, "100"
) # Search by position
matching_comps = ComponentManager.search_components(test_sch, "100") # Search by position
print(f"Search results for '100': {[c.reference for c in matching_comps]}")
# Get all components
@@ -423,9 +399,7 @@ if __name__ == "__main__":
# Remove a component
ComponentManager.remove_component(test_sch, "D1")
all_comps_after_remove = ComponentManager.get_all_components(test_sch)
print(
f"Components after removing D1: {[c.reference for c in all_comps_after_remove]}"
)
print(f"Components after removing D1: {[c.reference for c in all_comps_after_remove]}")
# Save the schematic (optional)
# SchematicManager.save_schematic(test_sch, "component_test.kicad_sch")

View File

@@ -1,15 +1,16 @@
from skip import Schematic
import os
import logging
import os
from pathlib import Path
from typing import Optional
from typing import Any, Dict, List, Optional
from skip import Schematic
logger = logging.getLogger(__name__)
# Import new wire and pin managers
try:
from commands.wire_manager import WireManager
from commands.pin_locator import PinLocator
from commands.wire_manager import WireManager
WIRE_MANAGER_AVAILABLE = True
except ImportError:
@@ -24,180 +25,14 @@ class ConnectionManager:
_pin_locator = None
@classmethod
def get_pin_locator(cls):
def get_pin_locator(cls) -> Any:
"""Get or create pin locator instance"""
if cls._pin_locator is None and WIRE_MANAGER_AVAILABLE:
cls._pin_locator = PinLocator()
return cls._pin_locator
@staticmethod
def add_wire(
schematic_path: Path,
start_point: list,
end_point: list,
properties: dict = None,
):
"""
Add a wire between two points using WireManager
Args:
schematic_path: Path to .kicad_sch file
start_point: [x, y] coordinates for wire start
end_point: [x, y] coordinates for wire end
properties: Optional wire properties (stroke_width, stroke_type)
Returns:
True if successful, False otherwise
"""
try:
if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager not available")
return False
stroke_width = properties.get("stroke_width", 0) if properties else 0
stroke_type = (
properties.get("stroke_type", "default") if properties else "default"
)
success = WireManager.add_wire(
schematic_path,
start_point,
end_point,
stroke_width=stroke_width,
stroke_type=stroke_type,
)
return success
except Exception as e:
logger.error(f"Error adding wire: {e}")
return False
@staticmethod
def get_pin_location(symbol, pin_name: str):
"""
Get the absolute location of a pin on a symbol
Args:
symbol: Symbol object
pin_name: Name or number of the pin (e.g., "1", "GND", "VCC")
Returns:
[x, y] coordinates or None if pin not found
"""
try:
if not hasattr(symbol, "pin"):
logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
return None
# Find the pin by name
target_pin = None
for pin in symbol.pin:
if pin.name == pin_name:
target_pin = pin
break
if not target_pin:
logger.warning(
f"Pin '{pin_name}' not found on {symbol.property.Reference.value}"
)
return None
# Get pin location relative to symbol
pin_loc = target_pin.location
# Get symbol location
symbol_at = symbol.at.value
# Calculate absolute position
# pin_loc is relative to symbol origin, need to add symbol position
abs_x = symbol_at[0] + pin_loc[0]
abs_y = symbol_at[1] + pin_loc[1]
return [abs_x, abs_y]
except Exception as e:
logger.error(f"Error getting pin location: {e}")
return None
@staticmethod
def add_connection(
schematic_path: Path,
source_ref: str,
source_pin: str,
target_ref: str,
target_pin: str,
routing: str = "direct",
):
"""
Add a wire connection between two component pins
Args:
schematic_path: Path to .kicad_sch file
source_ref: Reference designator of source component (e.g., "R1", "R1_")
source_pin: Pin name/number on source component
target_ref: Reference designator of target component (e.g., "C1", "C1_")
target_pin: Pin name/number on target component
routing: Routing style ('direct', 'orthogonal_h', 'orthogonal_v')
Returns:
True if connection was successful, False otherwise
"""
try:
if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager/PinLocator not available")
return False
locator = ConnectionManager.get_pin_locator()
if not locator:
logger.error("Pin locator unavailable")
return False
# Get pin locations
source_loc = locator.get_pin_location(
schematic_path, source_ref, source_pin
)
target_loc = locator.get_pin_location(
schematic_path, target_ref, target_pin
)
if not source_loc or not target_loc:
logger.error("Could not determine pin locations")
return False
# Create wire based on routing style
if routing == "direct":
# Simple direct wire
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
elif routing == "orthogonal_h":
# Orthogonal routing (horizontal first)
path = WireManager.create_orthogonal_path(
source_loc, target_loc, prefer_horizontal_first=True
)
success = WireManager.add_polyline_wire(schematic_path, path)
elif routing == "orthogonal_v":
# Orthogonal routing (vertical first)
path = WireManager.create_orthogonal_path(
source_loc, target_loc, prefer_horizontal_first=False
)
success = WireManager.add_polyline_wire(schematic_path, path)
else:
logger.error(f"Unknown routing style: {routing}")
return False
if success:
logger.info(
f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error adding connection: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_net_label(schematic: Schematic, net_name: str, position: list):
def add_net_label(schematic: Schematic, net_name: str, position: list) -> Any:
"""
Add a net label to the schematic
@@ -214,9 +49,7 @@ class ConnectionManager:
logger.error("Schematic does not have label collection")
return None
label = schematic.label.append(
text=net_name, at={"x": position[0], "y": position[1]}
)
label = schematic.label.append(text=net_name, at={"x": position[0], "y": position[1]})
logger.info(f"Added net label '{net_name}' at {position}")
return label
except Exception as e:
@@ -226,9 +59,9 @@ class ConnectionManager:
@staticmethod
def connect_to_net(
schematic_path: Path, component_ref: str, pin_name: str, net_name: str
):
) -> Dict[str, Any]:
"""
Connect a component pin to a named net using a wire stub and label
Connect a component pin to a named net using a wire stub and label.
Args:
schematic_path: Path to .kicad_sch file
@@ -237,59 +70,78 @@ class ConnectionManager:
net_name: Name of the net to connect to (e.g., "VCC", "GND", "SIGNAL_1")
Returns:
True if successful, False otherwise
Dict with keys:
success bool
pin_location [x, y] exact pin endpoint used (present on success)
label_location [x, y] where the net label was placed (present on success)
wire_stub [[x1,y1],[x2,y2]] the wire segment added (present on success)
message human-readable status
"""
try:
if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager/PinLocator not available")
return False
return {"success": False, "message": "WireManager/PinLocator not available"}
locator = ConnectionManager.get_pin_locator()
if not locator:
logger.error("Pin locator unavailable")
return False
return {"success": False, "message": "Pin locator unavailable"}
# Get pin location using PinLocator
pin_loc = locator.get_pin_location(schematic_path, component_ref, pin_name)
if not pin_loc:
logger.error(f"Could not locate pin {component_ref}/{pin_name}")
return False
msg = f"Could not locate pin {component_ref}/{pin_name}"
logger.error(msg)
return {"success": False, "message": msg}
# Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing)
# Stub direction follows the pin's outward angle from the PinLocator
pin_angle_deg = getattr(locator, '_last_pin_angle', 0)
try:
pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
except Exception:
except Exception as e:
logger.warning(
f"Could not get pin angle for {component_ref}/{pin_name}, defaulting to 0: {e}"
)
pin_angle_deg = 0
import math as _math
angle_rad = _math.radians(pin_angle_deg)
stub_end = [round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4)]
stub_end = [
round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4),
]
# Create wire stub using WireManager
wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end)
if not wire_success:
logger.error(f"Failed to create wire stub for net connection")
return False
msg = "Failed to create wire stub for net connection"
logger.error(msg)
return {"success": False, "message": msg}
# Add label at the end of the stub using WireManager
label_success = WireManager.add_label(
schematic_path, net_name, stub_end, label_type="label"
)
if not label_success:
logger.error(f"Failed to add net label '{net_name}'")
return False
msg = f"Failed to add net label '{net_name}'"
logger.error(msg)
return {"success": False, "message": msg}
logger.info(f"Connected {component_ref}/{pin_name} to net '{net_name}'")
return True
return {
"success": True,
"message": f"Connected {component_ref}/{pin_name} to net '{net_name}'",
"pin_location": pin_loc,
"label_location": stub_end,
"wire_stub": [pin_loc, stub_end],
}
except Exception as e:
logger.error(f"Error connecting to net: {e}")
import traceback
logger.error(traceback.format_exc())
return False
return {"success": False, "message": str(e)}
@staticmethod
def connect_passthrough(
@@ -298,7 +150,7 @@ class ConnectionManager:
target_ref: str,
net_prefix: str = "PIN",
pin_offset: int = 0,
):
) -> Dict[str, List[str]]:
"""
Connect all pins of source_ref to matching pins of target_ref via shared net labels.
Useful for passthrough adapters: J1 pin N <-> J2 pin N on net {net_prefix}_{N}.
@@ -335,20 +187,24 @@ class ConnectionManager:
for pin_num in sorted(src_pins.keys(), key=lambda x: int(x) if x.isdigit() else 0):
try:
net_name = f"{net_prefix}_{int(pin_num) + pin_offset}" if pin_num.isdigit() else f"{net_prefix}_{pin_num}"
net_name = (
f"{net_prefix}_{int(pin_num) + pin_offset}"
if pin_num.isdigit()
else f"{net_prefix}_{pin_num}"
)
ok_src = ConnectionManager.connect_to_net(
res_src = ConnectionManager.connect_to_net(
schematic_path, source_ref, pin_num, net_name
)
if not ok_src:
if not res_src.get("success"):
failed.append(f"{source_ref}/{pin_num}")
continue
if pin_num in tgt_pins:
ok_tgt = ConnectionManager.connect_to_net(
res_tgt = ConnectionManager.connect_to_net(
schematic_path, target_ref, pin_num, net_name
)
if not ok_tgt:
if not res_tgt.get("success"):
failed.append(f"{target_ref}/{pin_num}")
continue
else:
@@ -365,7 +221,7 @@ class ConnectionManager:
@staticmethod
def get_net_connections(
schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None
):
) -> List[Dict]:
"""
Get all connections for a named net using wire graph analysis
@@ -383,7 +239,7 @@ class ConnectionManager:
connections = []
tolerance = 0.5 # 0.5mm tolerance for point coincidence (grid spacing consideration)
def points_coincide(p1, p2):
def points_coincide(p1: Any, p2: Any) -> bool:
"""Check if two points are the same (within tolerance)"""
if not p1 or not p2:
return False
@@ -407,52 +263,55 @@ class ConnectionManager:
logger.info(f"No labels found for net '{net_name}'")
return connections
logger.debug(
f"Found {len(net_label_positions)} labels for net '{net_name}'"
)
logger.debug(f"Found {len(net_label_positions)} labels for net '{net_name}'")
# 2. Find all wires connected to these label positions
if not hasattr(schematic, "wire"):
logger.warning("Schematic has no wires")
return connections
connected_wire_points = set()
if hasattr(schematic, "wire"):
for wire in schematic.wire:
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
# Get all points in this wire (polyline)
wire_points = []
for point in wire.pts.xy:
if hasattr(point, "value"):
wire_points.append(
[float(point.value[0]), float(point.value[1])]
)
for wire in schematic.wire:
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
# Get all points in this wire (polyline)
wire_points = []
for point in wire.pts.xy:
if hasattr(point, "value"):
wire_points.append([float(point.value[0]), float(point.value[1])])
# Check if any wire point touches a label
wire_connected = False
for wire_pt in wire_points:
for label_pt in net_label_positions:
if points_coincide(wire_pt, label_pt):
wire_connected = True
break
if wire_connected:
# Check if any wire point touches a label
wire_connected = False
for wire_pt in wire_points:
for label_pt in net_label_positions:
if points_coincide(wire_pt, label_pt):
wire_connected = True
break
# If this wire is connected to the net, add all its points
if wire_connected:
for pt in wire_points:
connected_wire_points.add((pt[0], pt[1]))
break
# Build the full set of candidate match points:
# wire endpoints that touch this net PLUS label positions themselves.
# This handles labels placed directly at pin endpoints (no wire needed).
# If this wire is connected to the net, add all its points
if wire_connected:
for pt in wire_points:
connected_wire_points.add((pt[0], pt[1]))
# Build match points: union of wire endpoints AND label positions.
# This handles the valid KiCad style where a net label is placed
# directly at a pin endpoint with no wire segment in between.
all_match_points = connected_wire_points | {
(p[0], p[1]) for p in net_label_positions
}
if not all_match_points:
logger.debug(f"No connection points found for net '{net_name}'")
return connections
logger.debug(
f"Net '{net_name}': {len(connected_wire_points)} wire points, "
f"Found {len(connected_wire_points)} wire points, "
f"{len(net_label_positions)} direct label positions, "
f"{len(all_match_points)} total match points"
f"{len(all_match_points)} total match points for net '{net_name}'"
)
# 3. Find component pins at wire endpoints or direct label positions
# 3. Find component pins at wire endpoints
if not hasattr(schematic, "symbol"):
logger.warning("Schematic has no symbols")
return connections
@@ -487,19 +346,15 @@ class ConnectionManager:
# Check each pin
for pin_num, pin_data in pins.items():
# Get pin location
pin_loc = locator.get_pin_location(
schematic_path, ref, pin_num
)
pin_loc = locator.get_pin_location(schematic_path, ref, pin_num)
if not pin_loc:
continue
# Check if pin coincides with any wire point or label position
for match_pt in all_match_points:
if points_coincide(pin_loc, list(match_pt)):
connections.append(
{"component": ref, "pin": pin_num}
)
break # Pin found, no need to check more points
# Check if pin coincides with any match point
for wire_pt_tup in all_match_points:
if points_coincide(pin_loc, list(wire_pt_tup)):
connections.append({"component": ref, "pin": pin_num})
break # Pin found, no need to check more wire points
except Exception as e:
logger.warning(f"Error matching pins for {ref}: {e}")
@@ -515,10 +370,10 @@ class ConnectionManager:
symbol_x = float(symbol_pos[0])
symbol_y = float(symbol_pos[1])
# Check if symbol is near any wire point or label position (within 10mm)
for wire_pt in all_match_points:
# Check if symbol is near any match point (within 10mm)
for wire_pt_tup in all_match_points:
dist = (
(symbol_x - wire_pt[0]) ** 2 + (symbol_y - wire_pt[1]) ** 2
(symbol_x - wire_pt_tup[0]) ** 2 + (symbol_y - wire_pt_tup[1]) ** 2
) ** 0.5
if dist < 10.0: # 10mm proximity threshold
connections.append({"component": ref, "pin": "unknown"})
@@ -535,7 +390,9 @@ class ConnectionManager:
return []
@staticmethod
def generate_netlist(schematic: Schematic, schematic_path: Optional[Path] = None):
def generate_netlist(
schematic: Schematic, schematic_path: Optional[Path] = None
) -> Dict[str, Any]:
"""
Generate a netlist from the schematic
@@ -572,9 +429,7 @@ class ConnectionManager:
component_info = {
"reference": symbol.property.Reference.value,
"value": (
symbol.property.Value.value
if hasattr(symbol.property, "Value")
else ""
symbol.property.Value.value if hasattr(symbol.property, "Value") else ""
),
"footprint": (
symbol.property.Footprint.value
@@ -597,9 +452,7 @@ class ConnectionManager:
schematic, net_name, schematic_path
)
if connections:
netlist["nets"].append(
{"name": net_name, "connections": connections}
)
netlist["nets"].append({"name": net_name, "connections": connections})
logger.info(
f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components"
@@ -609,35 +462,3 @@ class ConnectionManager:
except Exception as e:
logger.error(f"Error generating netlist: {e}")
return {"nets": [], "components": []}
if __name__ == "__main__":
# Example Usage (for testing)
from schematic import (
SchematicManager,
) # Assuming schematic.py is in the same directory
# Create a new schematic
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
# Add some wires
wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
# Note: add_connection, remove_connection, get_net_connections are placeholders
# and require more complex implementation based on kicad-skip's structure.
# Example of how you might add a net label (requires finding a point on a wire)
# from skip import Label
# if wire1:
# net_label_pos = wire1.start # Or calculate a point on the wire
# net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
# print(f"Added net label 'Net_01' at {net_label_pos}")
# Save the schematic (optional)
# SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
# Clean up (if saved)
# if os.path.exists("connection_test.kicad_sch"):
# os.remove("connection_test.kicad_sch")
# print("Cleaned up connection_test.kicad_sch")

View File

@@ -9,10 +9,10 @@ URL schema: https://www.lcsc.com/datasheet/{LCSC#}.pdf
No API key required.
"""
import re
import logging
import re
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple
logger = logging.getLogger("kicad_interface")
@@ -49,7 +49,7 @@ class DatasheetManager:
return None
@staticmethod
def _find_lib_symbols_range(lines: List[str]):
def _find_lib_symbols_range(lines: List[str]) -> Tuple[Optional[int], Optional[int]]:
"""
Find the line range of the (lib_symbols ...) section.
Returns (start, end) line indices or (None, None) if not found.
@@ -81,9 +81,7 @@ class DatasheetManager:
return lib_sym_start, lib_sym_end
@staticmethod
def _process_symbol_block(
lines: List[str], block_start: int, block_end: int
) -> Optional[Dict]:
def _process_symbol_block(lines: List[str], block_start: int, block_end: int) -> Optional[Dict]:
"""
Extract LCSC and Datasheet info from a placed symbol block.
@@ -114,9 +112,7 @@ class DatasheetManager:
"datasheet_value": datasheet_current,
}
def enrich_schematic(
self, schematic_path: Path, dry_run: bool = False
) -> Dict:
def enrich_schematic(self, schematic_path: Path, dry_run: bool = False) -> Dict:
"""
Scan a .kicad_sch file and fill in missing LCSC datasheet URLs.
@@ -223,9 +219,7 @@ class DatasheetManager:
no_lcsc += 1
elif ds_value not in EMPTY_DATASHEET_VALUES:
already_set += 1
logger.debug(
f"Symbol {reference}: Datasheet already set to {ds_value!r}"
)
logger.debug(f"Symbol {reference}: Datasheet already set to {ds_value!r}")
else:
url = LCSC_DATASHEET_URL.format(lcsc=lcsc_norm)
if not dry_run:
@@ -256,9 +250,7 @@ class DatasheetManager:
if not dry_run and updated > 0:
with open(schematic_path, "w", encoding="utf-8") as f:
f.write("\n".join(new_lines))
logger.info(
f"Saved {schematic_path.name}: {updated} datasheet URLs written"
)
logger.info(f"Saved {schematic_path.name}: {updated} datasheet URLs written")
return {
"success": True,

View File

@@ -2,10 +2,11 @@
Design rules command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
import os
from typing import Any, Dict, List, Optional, Tuple
import pcbnew
logger = logging.getLogger("kicad_interface")
@@ -58,13 +59,9 @@ class DesignRuleCommands:
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
if "microViaDiameter" in params:
design_settings.m_MicroViasMinSize = int(
params["microViaDiameter"] * scale
)
design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale)
if "microViaDrill" in params:
design_settings.m_MicroViasMinDrill = int(
params["microViaDrill"] * scale
)
design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale)
# Set minimum values
if "minTrackWidth" in params:
@@ -77,19 +74,13 @@ class DesignRuleCommands:
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
if "minMicroViaDiameter" in params:
design_settings.m_MicroViasMinSize = int(
params["minMicroViaDiameter"] * scale
)
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
if "minMicroViaDrill" in params:
design_settings.m_MicroViasMinDrill = int(
params["minMicroViaDrill"] * scale
)
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
# KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
if "minHoleDiameter" in params:
design_settings.m_MinThroughDrill = int(
params["minHoleDiameter"] * scale
)
design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale)
# KiCAD 9.0: Added hole clearance settings
if "holeClearance" in params:
@@ -181,11 +172,11 @@ class DesignRuleCommands:
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Run Design Rule Check using kicad-cli"""
import subprocess
import json
import tempfile
import platform
import shutil
import subprocess
import tempfile
try:
if not self.board:
@@ -216,9 +207,7 @@ class DesignRuleCommands:
}
# Create temporary JSON output file
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as tmp:
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
json_output = tmp.name
try:
@@ -297,9 +286,7 @@ class DesignRuleCommands:
# Determine where to save the violations file
board_dir = os.path.dirname(board_file)
board_name = os.path.splitext(os.path.basename(board_file))[0]
violations_file = os.path.join(
board_dir, f"{board_name}_drc_violations.json"
)
violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json")
# Always save violations to JSON file (for large result sets)
with open(violations_file, "w", encoding="utf-8") as f:
@@ -453,9 +440,7 @@ class DesignRuleCommands:
# Filter by severity if specified
if severity != "all":
filtered_violations = [
v for v in all_violations if v.get("severity") == severity
]
filtered_violations = [v for v in all_violations if v.get("severity") == severity]
else:
filtered_violations = all_violations

View File

@@ -7,10 +7,10 @@ on-the-fly using TEXT MANIPULATION (not sexpdata) to preserve file formatting.
This enables access to all ~10,000+ KiCad symbols dynamically.
"""
import logging
import os
import re
import uuid
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@@ -41,10 +41,17 @@ class DynamicSymbolLoader:
Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"),
Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"),
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
Path.home() / ".local" / "share" / "kicad" / "10.0" / "symbols",
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
Path.home() / "Documents" / "KiCad" / "10.0" / "3rdparty" / "symbols",
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
]
for env_var in ["KICAD9_SYMBOL_DIR", "KICAD8_SYMBOL_DIR", "KICAD_SYMBOL_DIR"]:
for env_var in [
"KICAD10_SYMBOL_DIR",
"KICAD9_SYMBOL_DIR",
"KICAD8_SYMBOL_DIR",
"KICAD_SYMBOL_DIR",
]:
if env_var in os.environ:
possible_paths.insert(0, Path(os.environ[env_var]))
@@ -81,7 +88,9 @@ class DynamicSymbolLoader:
with open(table_path, "r", encoding="utf-8") as f:
content = f.read()
lib_pattern = r'\(lib\s+\(name\s+"?([^"\)\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^"\)\s]+)"?'
lib_pattern = (
r'\(lib\s+\(name\s+"?([^"\)\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^"\)\s]+)"?'
)
for match in re.finditer(lib_pattern, content, re.IGNORECASE):
nickname = match.group(1)
if nickname != library_name:
@@ -97,6 +106,11 @@ class DynamicSymbolLoader:
def _resolve_sym_uri(self, uri: str) -> Optional[str]:
"""Resolve environment variables in a sym-lib-table URI."""
env_map = {
"KICAD10_SYMBOL_DIR": [
"/usr/share/kicad/symbols",
"C:/Program Files/KiCad/10.0/share/kicad/symbols",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
],
"KICAD9_SYMBOL_DIR": [
"C:/Program Files/KiCad/9.0/share/kicad/symbols",
"/usr/share/kicad/symbols",
@@ -201,9 +215,7 @@ class DynamicSymbolLoader:
return items
def _inline_extends_symbol(
self, lib_content: str, symbol_name: str, child_block: str
) -> str:
def _inline_extends_symbol(self, lib_content: str, symbol_name: str, child_block: str) -> str:
"""
Fully inline a child symbol that uses (extends "ParentName") by merging
the parent's pins / graphics into the child definition.
@@ -248,22 +260,16 @@ class DynamicSymbolLoader:
for item in self._iter_top_level_items(parent_block):
prop_match = re.match(r'[\s\t]*\(property "([^"]+)"', item)
sub_match = re.search(
r'\(symbol "' + re.escape(parent_name) + r'_\d+_\d+"', item
)
sub_match = re.search(r'\(symbol "' + re.escape(parent_name) + r'_\d+_\d+"', item)
if prop_match:
pname = prop_match.group(1)
parent_prop_names.add(pname)
body_lines.append(
child_props[pname] if pname in child_props else item
)
body_lines.append(child_props[pname] if pname in child_props else item)
elif sub_match:
# Rename ParentName_0_1 → ChildName_0_1
body_lines.append(
item.replace(f'"{parent_name}_', f'"{symbol_name}_')
)
elif re.match(r'[\s\t]*\(extends ', item):
body_lines.append(item.replace(f'"{parent_name}_', f'"{symbol_name}_'))
elif re.match(r"[\s\t]*\(extends ", item):
pass # drop extends clause
else:
body_lines.append(item) # pin_names, in_bom, on_board …
@@ -273,16 +279,12 @@ class DynamicSymbolLoader:
if pname not in parent_prop_names:
body_lines.append(pblock)
first_line = parent_block.split("\n")[0].replace(
f'"{parent_name}"', f'"{symbol_name}"'
)
first_line = parent_block.split("\n")[0].replace(f'"{parent_name}"', f'"{symbol_name}"')
last_line = parent_block.split("\n")[-1]
return first_line + "\n" + "\n".join(body_lines) + "\n" + last_line
def extract_symbol_from_library(
self, library_name: str, symbol_name: str
) -> Optional[str]:
def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
"""
Extract a symbol definition from a KiCad .kicad_sym library file.
Returns the raw text block, ready to be injected into a schematic.
@@ -304,9 +306,7 @@ class DynamicSymbolLoader:
block = self._extract_symbol_block(lib_content, symbol_name)
if block is None:
logger.warning(
f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym"
)
logger.warning(f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym")
return None
# If the symbol uses (extends "ParentName"), inline the parent content
@@ -315,9 +315,7 @@ class DynamicSymbolLoader:
# load a schematic whose lib_symbols section contains it.
if re.search(r'\(extends "([^"]+)"\)', block):
parent_name = re.search(r'\(extends "([^"]+)"\)', block).group(1)
logger.info(
f"Symbol {symbol_name} extends {parent_name}, inlining parent content"
)
logger.info(f"Symbol {symbol_name} extends {parent_name}, inlining parent content")
block = self._inline_extends_symbol(lib_content, symbol_name, block)
# Prefix top-level symbol name with library
@@ -355,9 +353,7 @@ class DynamicSymbolLoader:
# Extract symbol from library
symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
if not symbol_block:
raise ValueError(
f"Symbol '{symbol_name}' not found in library '{library_name}'"
)
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
# Indent the block to match lib_symbols indentation (4 spaces for top-level)
indented_lines = []
@@ -392,11 +388,7 @@ class DynamicSymbolLoader:
f.write(content)
# Handle both Path objects and strings
sch_name = (
schematic_path.name
if hasattr(schematic_path, "name")
else str(schematic_path)
)
sch_name = schematic_path.name if hasattr(schematic_path, "name") else str(schematic_path)
logger.info(f"Injected symbol {full_name} into {sch_name}")
return True
@@ -433,6 +425,14 @@ class DynamicSymbolLoader:
(property "Datasheet" "~" (at {x} {y} 0)
(effects (font (size 1.27 1.27)) (hide yes))
)
(instances
(project "project"
(path "/"
(reference "{reference}")
(unit 1)
)
)
)
)"""
with open(schematic_path, "r", encoding="utf-8") as f:
@@ -450,9 +450,7 @@ class DynamicSymbolLoader:
with open(schematic_path, "w", encoding="utf-8") as f:
f.write(content)
logger.info(
f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})"
)
logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
return True
def load_symbol_dynamically(

View File

@@ -2,13 +2,14 @@
Export command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
import base64
import logging
import os
import shutil
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
import pcbnew
logger = logging.getLogger("kicad_interface")
@@ -105,22 +106,16 @@ class ExportCommands:
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode == 0:
# Get list of generated drill files
for file in os.listdir(output_dir):
if file.endswith((".drl", ".cnc")):
drill_files.append(file)
else:
logger.warning(
f"Drill file generation failed: {result.stderr}"
)
logger.warning(f"Drill file generation failed: {result.stderr}")
except Exception as drill_error:
logger.warning(
f"Could not generate drill files: {str(drill_error)}"
)
logger.warning(f"Could not generate drill files: {str(drill_error)}")
else:
logger.warning("kicad-cli not available for drill file generation")
@@ -236,9 +231,7 @@ class ExportCommands:
# Get the actual output filename that was created
board_name = os.path.splitext(os.path.basename(self.board.GetFileName()))[0]
actual_filename = f"{board_name}-{base_name}.pdf"
actual_output_path = os.path.join(
os.path.dirname(output_path), actual_filename
)
actual_output_path = os.path.join(os.path.dirname(output_path), actual_filename)
return {
"success": True,
@@ -329,9 +322,9 @@ class ExportCommands:
def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Export 3D model files using kicad-cli (KiCAD 9.0 compatible)"""
import subprocess
import platform
import shutil
import subprocess
try:
if not self.board:
@@ -395,9 +388,7 @@ class ExportCommands:
if not include_components:
cmd.append("--no-components")
if include_copper:
cmd.extend(
["--include-tracks", "--include-pads", "--include-zones"]
)
cmd.extend(["--include-tracks", "--include-pads", "--include-zones"])
if include_silkscreen:
cmd.append("--include-silkscreen")
if include_solder_mask:
@@ -618,8 +609,8 @@ class ExportCommands:
Returns:
Path to kicad-cli executable, or None if not found
"""
import shutil
import platform
import shutil
# Try system PATH first
cli_path = shutil.which("kicad-cli")
@@ -696,6 +687,7 @@ class ExportCommands:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
from pathlib import Path
logs_dir = Path(project_dir) / "logs"
logs_dir.mkdir(exist_ok=True)
dest = str(logs_dir / f"mcp_log_{timestamp}.txt")

View File

@@ -8,15 +8,15 @@ KiCAD 9 .kicad_mod format reference:
https://dev-docs.kicad.org/en/file-formats/sexpr-footprint/
"""
import logging
import os
import re
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = logging.getLogger("kicad_interface")
KICAD9_FORMAT_VERSION = "20250114" # .kicad_sch schematic files
KICAD9_FORMAT_VERSION = "20250114" # .kicad_sch schematic files
KICAD9_FOOTPRINT_VERSION = "20241229" # .kicad_mod footprint files
@@ -106,7 +106,7 @@ class FootprintCreator:
# ---- header ----
lines.append(f'(footprint "{name}"')
lines.append(f' (version {KICAD9_FOOTPRINT_VERSION})')
lines.append(f" (version {KICAD9_FOOTPRINT_VERSION})")
lines.append(f' (generator "kicad-mcp")')
lines.append(f' (generator_version "9.0")')
lines.append(f' (layer "F.Cu")')
@@ -122,25 +122,21 @@ class FootprintCreator:
val_x = value_position.get("x", 0.0) if value_position else 0.0
val_y = value_position.get("y", 1.27) if value_position else 1.27
lines.append(
f' (property "Reference" "REF**" (at {_fmt(ref_x)} {_fmt(ref_y)} 0)'
)
lines.append(f' (property "Reference" "REF**" (at {_fmt(ref_x)} {_fmt(ref_y)} 0)')
lines.append(f' (layer "F.SilkS")')
lines.append(f' (uuid "{_new_uuid()}")')
lines.append(f' (effects (font (size 1 1) (thickness 0.15)))')
lines.append(f' )')
lines.append(
f' (property "Value" "{_esc(name)}" (at {_fmt(val_x)} {_fmt(val_y)} 0)'
)
lines.append(f" (effects (font (size 1 1) (thickness 0.15)))")
lines.append(f" )")
lines.append(f' (property "Value" "{_esc(name)}" (at {_fmt(val_x)} {_fmt(val_y)} 0)')
lines.append(f' (layer "F.Fab")')
lines.append(f' (uuid "{_new_uuid()}")')
lines.append(f' (effects (font (size 1 1) (thickness 0.15)))')
lines.append(f' )')
lines.append(f" (effects (font (size 1 1) (thickness 0.15)))")
lines.append(f" )")
lines.append(f' (property "Datasheet" "" (at 0 0 0)')
lines.append(f' (layer "F.Fab")')
lines.append(f' (uuid "{_new_uuid()}")')
lines.append(f' (effects (font (size 1 1) (thickness 0.15)))')
lines.append(f' )')
lines.append(f" (effects (font (size 1 1) (thickness 0.15)))")
lines.append(f" )")
lines.append("")
# ---- courtyard ----
@@ -217,33 +213,35 @@ class FootprintCreator:
changes = []
if size:
new_size = f'(size {_fmt(size["w"])} {_fmt(size["h"])})'
block, n = re.subn(r'\(size\s+[\d.]+\s+[\d.]+\)', new_size, block)
block, n = re.subn(r"\(size\s+[\d.]+\s+[\d.]+\)", new_size, block)
if n:
changes.append(f"size→{new_size}")
if at:
angle = at.get("angle", 0)
new_at = f'(at {_fmt(at["x"])} {_fmt(at["y"])} {_fmt(angle)})'
block, n = re.subn(r'\(at\s+[-\d.]+\s+[-\d.]+(?:\s+[-\d.]+)?\)', new_at, block)
block, n = re.subn(r"\(at\s+[-\d.]+\s+[-\d.]+(?:\s+[-\d.]+)?\)", new_at, block)
if n:
changes.append(f"at→{new_at}")
if drill is not None:
if isinstance(drill, (int, float)):
new_drill = f'(drill {_fmt(drill)})'
new_drill = f"(drill {_fmt(drill)})"
else:
new_drill = f'(drill oval {_fmt(drill["w"])} {_fmt(drill["h"])})'
block, n = re.subn(r'\(drill(?:\s+oval)?\s+[-\d.]+(?:\s+[-\d.]+)?\)', new_drill, block)
block, n = re.subn(
r"\(drill(?:\s+oval)?\s+[-\d.]+(?:\s+[-\d.]+)?\)", new_drill, block
)
if n:
changes.append(f"drill→{new_drill}")
else:
# Insert drill before closing paren of pad block
block = block.rstrip().rstrip(')') + f'\n {new_drill}\n )'
block = block.rstrip().rstrip(")") + f"\n {new_drill}\n )"
changes.append(f"drill (inserted)→{new_drill}")
if shape:
block, n = re.subn(
r'(pad\s+"[^"]*"\s+\w+\s+)\w+',
lambda m: m.group(1) + shape,
lambda m: str(m.group(1)) + shape,
block,
count=1
count=1,
)
if n:
changes.append(f"shape→{shape}")
@@ -280,7 +278,7 @@ class FootprintCreator:
if not updated:
return {
"success": False,
"error": f"Pad \"{pad_number}\" not found or no changes made in {footprint_path}",
"error": f'Pad "{pad_number}" not found or no changes made in {footprint_path}',
}
path.write_text("\n".join(result_lines), encoding="utf-8")
@@ -429,6 +427,7 @@ class FootprintCreator:
# Internal helpers #
# ------------------------------------------------------------------ #
def _esc(s: str) -> str:
"""Escape double-quotes inside S-Expression string values."""
return s.replace('"', '\\"')
@@ -436,6 +435,7 @@ def _esc(s: str) -> str:
def _new_uuid() -> str:
import uuid
return str(uuid.uuid4())
@@ -445,8 +445,8 @@ _DEFAULT_THT_LAYERS = ["*.Cu", "*.Mask"]
def _pad_lines(pad: Dict[str, Any]) -> List[str]:
number = str(pad.get("number", "1"))
ptype = pad.get("type", "smd").lower() # smd | thru_hole | np_thru_hole
shape = pad.get("shape", "rect").lower() # rect | circle | oval | roundrect
ptype = pad.get("type", "smd").lower() # smd | thru_hole | np_thru_hole
shape = pad.get("shape", "rect").lower() # rect | circle | oval | roundrect
at = pad.get("at", {"x": 0.0, "y": 0.0})
size = pad.get("size", {"w": 1.0, "h": 1.0})
drill = pad.get("drill", None)
@@ -462,7 +462,9 @@ def _pad_lines(pad: Dict[str, Any]) -> List[str]:
sh = _fmt(size.get("h", 1.0))
if layers is None:
layers = _DEFAULT_THT_LAYERS if ptype in ("thru_hole", "np_thru_hole") else _DEFAULT_SMD_LAYERS
layers = (
_DEFAULT_THT_LAYERS if ptype in ("thru_hole", "np_thru_hole") else _DEFAULT_SMD_LAYERS
)
layers_str = " ".join(f'"{l}"' for l in layers)
lines = [f' (pad "{number}" {ptype} {shape}']
@@ -494,13 +496,13 @@ def _rect_lines(rect: Dict[str, Any], layer: str, default_width: float = 0.05) -
y2 = _fmt(rect.get("y2", 1.0))
w = _fmt(rect.get("width", default_width))
return [
f' (fp_rect',
f' (start {x1} {y1})',
f' (end {x2} {y2})',
f' (stroke (width {w}) (type default))',
f' (fill none)',
f" (fp_rect",
f" (start {x1} {y1})",
f" (end {x2} {y2})",
f" (stroke (width {w}) (type default))",
f" (fill none)",
f' (layer "{layer}")',
f' (uuid "{_new_uuid()}")',
f' )',
f" )",
"",
]

View File

@@ -9,22 +9,20 @@ Supports two execution modes:
- Docker: docker run eclipse-temurin:21-jre (requires Docker)
"""
import os
import subprocess
import shutil
import time
import logging
import os
import shutil
import subprocess
import time
from pathlib import Path
from typing import Dict, Any, Optional, List
from typing import Any, Dict, List, Optional
logger = logging.getLogger("kicad_interface")
# Default Freerouting JAR location
DEFAULT_FREEROUTING_JAR = os.environ.get(
"FREEROUTING_JAR",
os.path.join(
os.path.expanduser("~"), ".kicad-mcp", "freerouting.jar"
),
os.path.join(os.path.expanduser("~"), ".kicad-mcp", "freerouting.jar"),
)
DOCKER_IMAGE = "eclipse-temurin:21-jre"
@@ -97,38 +95,55 @@ def _build_freerouting_cmd(
"""Build the command to run Freerouting."""
if use_docker:
docker_exe = _find_docker()
if docker_exe is None:
raise RuntimeError("Docker/Podman executable not found")
board_dir = os.path.dirname(dsn_path)
dsn_name = os.path.basename(dsn_path)
ses_name = os.path.basename(ses_path)
jar_name = os.path.basename(jar_path)
return [
docker_exe, "run", "--rm",
"-v", f"{jar_path}:/app/{jar_name}:ro",
"-v", f"{board_dir}:/work",
docker_exe,
"run",
"--rm",
"-v",
f"{jar_path}:/app/{jar_name}:ro",
"-v",
f"{board_dir}:/work",
DOCKER_IMAGE,
"java", "-jar", f"/app/{jar_name}",
"-de", f"/work/{dsn_name}",
"-do", f"/work/{ses_name}",
"-mp", str(passes),
"java",
"-jar",
f"/app/{jar_name}",
"-de",
f"/work/{dsn_name}",
"-do",
f"/work/{ses_name}",
"-mp",
str(passes),
]
else:
java_exe = _find_java()
if java_exe is None:
raise RuntimeError("Java executable not found")
return [
java_exe, "-jar", jar_path,
"-de", dsn_path, "-do", ses_path,
"-mp", str(passes),
java_exe,
"-jar",
jar_path,
"-de",
dsn_path,
"-do",
ses_path,
"-mp",
str(passes),
]
class FreeroutingCommands:
"""Handles Freerouting autoroute operations."""
def __init__(self, board=None):
def __init__(self, board: Any = None) -> None:
self.board = board
def _resolve_execution_mode(
self, jar_path: str
) -> Dict[str, Any]:
def _resolve_execution_mode(self, jar_path: str) -> Dict[str, Any]:
"""Determine how to run Freerouting: direct or docker.
Returns dict with 'mode', 'use_docker', or 'error'.
@@ -152,8 +167,7 @@ class FreeroutingCommands:
return {
"mode": "error",
"error": (
"Neither Java 21+ nor Docker found. "
"Install one of them to use Freerouting."
"Neither Java 21+ nor Docker found. " "Install one of them to use Freerouting."
),
}
@@ -190,14 +204,10 @@ class FreeroutingCommands:
return {
"success": False,
"message": "No board file path available",
"errorDetails": (
"Provide boardPath or open a project first"
),
"errorDetails": ("Provide boardPath or open a project first"),
}
jar_path = params.get(
"freeroutingJar", DEFAULT_FREEROUTING_JAR
)
jar_path = params.get("freeroutingJar", DEFAULT_FREEROUTING_JAR)
timeout = params.get("timeout", 300)
passes = params.get("maxPasses", 20)
@@ -238,9 +248,7 @@ class FreeroutingCommands:
return {
"success": False,
"message": "DSN export failed",
"errorDetails": (
f"ExportSpecctraDSN returned: {result}"
),
"errorDetails": (f"ExportSpecctraDSN returned: {result}"),
}
except Exception as e:
return {
@@ -260,14 +268,10 @@ class FreeroutingCommands:
logger.info(f"DSN exported: {dsn_size} bytes")
# Step 2: Run Freerouting
cmd = _build_freerouting_cmd(
jar_path, dsn_path, ses_path, passes, use_docker
)
cmd = _build_freerouting_cmd(jar_path, dsn_path, ses_path, passes, use_docker)
mode_label = "docker" if use_docker else "direct"
logger.info(
f"Running Freerouting ({mode_label}): {' '.join(cmd)}"
)
logger.info(f"Running Freerouting ({mode_label}): {' '.join(cmd)}")
start_time = time.time()
try:
@@ -283,10 +287,7 @@ class FreeroutingCommands:
if proc.returncode != 0:
return {
"success": False,
"message": (
f"Freerouting exited with code "
f"{proc.returncode}"
),
"message": (f"Freerouting exited with code " f"{proc.returncode}"),
"errorDetails": proc.stderr or proc.stdout,
"elapsed_seconds": elapsed,
"mode": mode_label,
@@ -294,12 +295,8 @@ class FreeroutingCommands:
except subprocess.TimeoutExpired:
return {
"success": False,
"message": (
f"Freerouting timed out after {timeout}s"
),
"errorDetails": (
"Increase timeout or reduce board complexity"
),
"message": (f"Freerouting timed out after {timeout}s"),
"errorDetails": ("Increase timeout or reduce board complexity"),
}
except Exception as e:
return {
@@ -313,10 +310,7 @@ class FreeroutingCommands:
return {
"success": False,
"message": "Freerouting did not produce SES output",
"errorDetails": (
f"Expected at: {ses_path}. "
f"Stdout: {proc.stdout[:500]}"
),
"errorDetails": (f"Expected at: {ses_path}. " f"Stdout: {proc.stdout[:500]}"),
"elapsed_seconds": elapsed,
}
@@ -331,9 +325,7 @@ class FreeroutingCommands:
return {
"success": False,
"message": "SES import failed",
"errorDetails": (
f"ImportSpecctraSES returned: {result}"
),
"errorDetails": (f"ImportSpecctraSES returned: {result}"),
"elapsed_seconds": elapsed,
}
except Exception as e:
@@ -348,9 +340,7 @@ class FreeroutingCommands:
try:
self.board.Save(board_path)
except Exception as e:
logger.warning(
f"Board save after autoroute failed: {e}"
)
logger.warning(f"Board save after autoroute failed: {e}")
# Collect stats
tracks = self.board.GetTracks()
@@ -373,9 +363,7 @@ class FreeroutingCommands:
"tracks": track_count,
"vias": via_count,
},
"freerouting_stdout": (
proc.stdout[:1000] if proc.stdout else ""
),
"freerouting_stdout": (proc.stdout[:1000] if proc.stdout else ""),
}
def export_dsn(self, params: Dict[str, Any]) -> Dict[str, Any]:
@@ -396,36 +384,26 @@ class FreeroutingCommands:
"errorDetails": "Load or create a board first",
}
board_path = (
params.get("boardPath") or self.board.GetFileName()
)
board_path = params.get("boardPath") or self.board.GetFileName()
output_path = params.get("outputPath")
if not output_path:
if board_path:
output_path = (
os.path.splitext(board_path)[0] + ".dsn"
)
output_path = os.path.splitext(board_path)[0] + ".dsn"
else:
return {
"success": False,
"message": "No output path",
"errorDetails": (
"Provide outputPath or have a board open"
),
"errorDetails": ("Provide outputPath or have a board open"),
}
try:
result = pcbnew.ExportSpecctraDSN(
self.board, output_path
)
result = pcbnew.ExportSpecctraDSN(self.board, output_path)
if result is not True and result != 0:
return {
"success": False,
"message": "DSN export failed",
"errorDetails": (
f"ExportSpecctraDSN returned: {result}"
),
"errorDetails": (f"ExportSpecctraDSN returned: {result}"),
}
except Exception as e:
return {
@@ -434,11 +412,7 @@ class FreeroutingCommands:
"errorDetails": str(e),
}
file_size = (
os.path.getsize(output_path)
if os.path.isfile(output_path)
else 0
)
file_size = os.path.getsize(output_path) if os.path.isfile(output_path) else 0
return {
"success": True,
"message": f"Exported DSN to {output_path}",
@@ -469,9 +443,7 @@ class FreeroutingCommands:
return {
"success": False,
"message": "Missing sesPath parameter",
"errorDetails": (
"Provide the path to the .ses file"
),
"errorDetails": ("Provide the path to the .ses file"),
}
if not os.path.isfile(ses_path):
@@ -482,16 +454,12 @@ class FreeroutingCommands:
}
try:
result = pcbnew.ImportSpecctraSES(
self.board, ses_path
)
result = pcbnew.ImportSpecctraSES(self.board, ses_path)
if result is not True and result != 0:
return {
"success": False,
"message": "SES import failed",
"errorDetails": (
f"ImportSpecctraSES returned: {result}"
),
"errorDetails": (f"ImportSpecctraSES returned: {result}"),
}
except Exception as e:
return {
@@ -500,24 +468,16 @@ class FreeroutingCommands:
"errorDetails": str(e),
}
board_path = (
params.get("boardPath") or self.board.GetFileName()
)
board_path = params.get("boardPath") or self.board.GetFileName()
if board_path:
try:
self.board.Save(board_path)
except Exception as e:
logger.warning(
f"Board save after SES import failed: {e}"
)
logger.warning(f"Board save after SES import failed: {e}")
tracks = self.board.GetTracks()
track_count = sum(
1 for t in tracks if t.GetClass() != "PCB_VIA"
)
via_count = sum(
1 for t in tracks if t.GetClass() == "PCB_VIA"
)
track_count = sum(1 for t in tracks if t.GetClass() != "PCB_VIA")
via_count = sum(1 for t in tracks if t.GetClass() == "PCB_VIA")
return {
"success": True,
@@ -528,13 +488,9 @@ class FreeroutingCommands:
},
}
def check_freerouting(
self, params: Dict[str, Any]
) -> Dict[str, Any]:
def check_freerouting(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Check if Freerouting and Java/Docker are available."""
jar_path = params.get(
"freeroutingJar", DEFAULT_FREEROUTING_JAR
)
jar_path = params.get("freeroutingJar", DEFAULT_FREEROUTING_JAR)
# Check local Java
java_exe = _find_java()
@@ -548,11 +504,7 @@ class FreeroutingCommands:
text=True,
timeout=10,
)
java_version = (
(proc.stderr or proc.stdout)
.strip()
.split("\n")[0]
)
java_version = (proc.stderr or proc.stdout).strip().split("\n")[0]
java_21_ok = _java_version_ok(java_exe)
except Exception:
pass

View File

@@ -5,20 +5,21 @@ Handles authentication and downloading the JLCPCB parts library
for integration with KiCAD component selection.
"""
import os
import logging
import requests
import time
import hmac
import base64
import hashlib
import hmac
import json
import logging
import os
import secrets
import string
import base64
import json
from typing import Optional, Dict, List, Callable
import time
from pathlib import Path
from typing import Callable, Dict, List, Optional
logger = logging.getLogger('kicad_interface')
import requests
logger = logging.getLogger("kicad_interface")
class JLCPCBClient:
@@ -31,7 +32,12 @@ class JLCPCBClient:
BASE_URL = "https://jlcpcb.com/external"
def __init__(self, app_id: Optional[str] = None, access_key: Optional[str] = None, secret_key: Optional[str] = None):
def __init__(
self,
app_id: Optional[str] = None,
access_key: Optional[str] = None,
secret_key: Optional[str] = None,
):
"""
Initialize JLCPCB API client
@@ -40,20 +46,24 @@ class JLCPCBClient:
access_key: JLCPCB Access Key (or reads from JLCPCB_API_KEY env var)
secret_key: JLCPCB Secret Key (or reads from JLCPCB_API_SECRET env var)
"""
self.app_id = app_id or os.getenv('JLCPCB_APP_ID')
self.access_key = access_key or os.getenv('JLCPCB_API_KEY')
self.secret_key = secret_key or os.getenv('JLCPCB_API_SECRET')
self.app_id = app_id or os.getenv("JLCPCB_APP_ID")
self.access_key = access_key or os.getenv("JLCPCB_API_KEY")
self.secret_key = secret_key or os.getenv("JLCPCB_API_SECRET")
if not self.app_id or not self.access_key or not self.secret_key:
logger.warning("JLCPCB API credentials not found. Set JLCPCB_APP_ID, JLCPCB_API_KEY, and JLCPCB_API_SECRET environment variables.")
logger.warning(
"JLCPCB API credentials not found. Set JLCPCB_APP_ID, JLCPCB_API_KEY, and JLCPCB_API_SECRET environment variables."
)
@staticmethod
def _generate_nonce() -> str:
"""Generate a 32-character random nonce"""
chars = string.ascii_letters + string.digits
return ''.join(secrets.choice(chars) for _ in range(32))
return "".join(secrets.choice(chars) for _ in range(32))
def _build_signature_string(self, method: str, path: str, timestamp: int, nonce: str, body: str) -> str:
def _build_signature_string(
self, method: str, path: str, timestamp: int, nonce: str, body: str
) -> str:
"""
Build the signature string according to JLCPCB spec
@@ -87,11 +97,9 @@ class JLCPCBClient:
Base64-encoded signature
"""
signature_bytes = hmac.new(
self.secret_key.encode('utf-8'),
signature_string.encode('utf-8'),
hashlib.sha256
self.secret_key.encode("utf-8"), signature_string.encode("utf-8"), hashlib.sha256
).digest()
return base64.b64encode(signature_bytes).decode('utf-8')
return base64.b64encode(signature_bytes).decode("utf-8")
def _get_auth_header(self, method: str, path: str, body: str = "") -> str:
"""
@@ -106,7 +114,9 @@ class JLCPCBClient:
Authorization header value
"""
if not self.app_id or not self.access_key or not self.secret_key:
raise Exception("JLCPCB API credentials not configured. Please set JLCPCB_APP_ID, JLCPCB_API_KEY, and JLCPCB_API_SECRET environment variables.")
raise Exception(
"JLCPCB API credentials not configured. Please set JLCPCB_APP_ID, JLCPCB_API_KEY, and JLCPCB_API_SECRET environment variables."
)
nonce = self._generate_nonce()
timestamp = int(time.time())
@@ -116,7 +126,9 @@ class JLCPCBClient:
logger.debug(f"Signature string:\n{repr(signature_string)}")
logger.debug(f"Signature: {signature}")
logger.debug(f"Auth header: JOP appid=\"{self.app_id}\",accesskey=\"{self.access_key}\",nonce=\"{nonce}\",timestamp=\"{timestamp}\",signature=\"{signature}\"")
logger.debug(
f'Auth header: JOP appid="{self.app_id}",accesskey="{self.access_key}",nonce="{nonce}",timestamp="{timestamp}",signature="{signature}"'
)
return f'JOP appid="{self.app_id}",accesskey="{self.access_key}",nonce="{nonce}",timestamp="{timestamp}",signature="{signature}"'
@@ -138,22 +150,16 @@ class JLCPCBClient:
# Convert payload to JSON string for signing
# For POST requests, we always send JSON, even if empty dict
body_str = json.dumps(payload, separators=(',', ':'))
body_str = json.dumps(payload, separators=(",", ":"))
# Generate authorization header
auth_header = self._get_auth_header("POST", path, body_str)
headers = {
"Authorization": auth_header,
"Content-Type": "application/json"
}
headers = {"Authorization": auth_header, "Content-Type": "application/json"}
try:
response = requests.post(
f"{self.BASE_URL}{path}",
headers=headers,
json=payload,
timeout=60
f"{self.BASE_URL}{path}", headers=headers, json=payload, timeout=60
)
logger.debug(f"Response status: {response.status_code}")
@@ -163,18 +169,19 @@ class JLCPCBClient:
response.raise_for_status()
data = response.json()
if data.get('code') != 200:
raise Exception(f"API request failed (code {data.get('code')}): {data.get('msg', 'Unknown error')} - Full response: {data}")
if data.get("code") != 200:
raise Exception(
f"API request failed (code {data.get('code')}): {data.get('msg', 'Unknown error')} - Full response: {data}"
)
return data['data']
return data["data"]
except requests.exceptions.RequestException as e:
logger.error(f"Failed to fetch parts page: {e}")
raise Exception(f"JLCPCB API request failed: {e}")
def download_full_database(
self,
callback: Optional[Callable[[int, int, str], None]] = None
self, callback: Optional[Callable[[int, int, str], None]] = None
) -> List[Dict]:
"""
Download entire parts library from JLCPCB
@@ -197,10 +204,10 @@ class JLCPCBClient:
try:
data = self.fetch_parts_page(last_key)
parts = data.get('componentInfos', [])
parts = data.get("componentInfos", [])
all_parts.extend(parts)
last_key = data.get('lastKey')
last_key = data.get("lastKey")
if callback:
callback(page, len(all_parts), f"Downloaded {len(all_parts)} parts...")
@@ -245,7 +252,9 @@ class JLCPCBClient:
return None
def test_jlcpcb_connection(app_id: Optional[str] = None, access_key: Optional[str] = None, secret_key: Optional[str] = None) -> bool:
def test_jlcpcb_connection(
app_id: Optional[str] = None, access_key: Optional[str] = None, secret_key: Optional[str] = None
) -> bool:
"""
Test JLCPCB API connection
@@ -268,7 +277,7 @@ def test_jlcpcb_connection(app_id: Optional[str] = None, access_key: Optional[st
return False
if __name__ == '__main__':
if __name__ == "__main__":
# Test the JLCPCB client
logging.basicConfig(level=logging.INFO)
@@ -279,7 +288,7 @@ if __name__ == '__main__':
client = JLCPCBClient()
print("\nFetching first page of parts...")
data = client.fetch_parts_page()
parts = data.get('componentInfos', [])
parts = data.get("componentInfos", [])
print(f"✓ Retrieved {len(parts)} parts in first page")
if parts:

View File

@@ -5,15 +5,15 @@ Manages local SQLite database of JLCPCB parts for fast searching
and component selection.
"""
import os
import sqlite3
import json
import logging
from pathlib import Path
from typing import List, Dict, Optional
import os
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
logger = logging.getLogger('kicad_interface')
logger = logging.getLogger("kicad_interface")
class JLCPCBPartsManager:
@@ -38,10 +38,10 @@ class JLCPCBPartsManager:
db_path = str(data_dir / "jlcpcb_parts.db")
self.db_path = db_path
self.conn = None
self.conn: Optional[sqlite3.Connection] = None
self._init_database()
def _init_database(self):
def _init_database(self) -> None:
"""Initialize SQLite database with schema"""
self.conn = sqlite3.connect(self.db_path)
self.conn.row_factory = sqlite3.Row # Return rows as dicts
@@ -49,7 +49,7 @@ class JLCPCBPartsManager:
cursor = self.conn.cursor()
# Create components table
cursor.execute('''
cursor.execute("""
CREATE TABLE IF NOT EXISTS components (
lcsc TEXT PRIMARY KEY,
category TEXT,
@@ -65,17 +65,19 @@ class JLCPCBPartsManager:
price_json TEXT,
last_updated INTEGER
)
''')
""")
# Create indexes for fast searching
cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON components(category, subcategory)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_package ON components(package)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manufacturer ON components(manufacturer)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_library_type ON components(library_type)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_mfr_part ON components(mfr_part)')
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_category ON components(category, subcategory)"
)
cursor.execute("CREATE INDEX IF NOT EXISTS idx_package ON components(package)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_manufacturer ON components(manufacturer)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_library_type ON components(library_type)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_mfr_part ON components(mfr_part)")
# Full-text search index for descriptions
cursor.execute('''
cursor.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5(
lcsc,
description,
@@ -83,12 +85,14 @@ class JLCPCBPartsManager:
manufacturer,
content=components
)
''')
""")
self.conn.commit()
logger.info(f"Initialized JLCPCB parts database at {self.db_path}")
def import_parts(self, parts: List[Dict], progress_callback=None):
def import_parts(
self, parts: List[Dict], progress_callback: Optional[Callable[..., Any]] = None
) -> None:
"""
Import parts into database from JLCPCB API response
@@ -103,32 +107,35 @@ class JLCPCBPartsManager:
for i, part in enumerate(parts):
try:
# Extract price breaks
price_json = json.dumps(part.get('prices', []))
price_json = json.dumps(part.get("prices", []))
# Determine library type
library_type = self._determine_library_type(part)
cursor.execute('''
cursor.execute(
"""
INSERT OR REPLACE INTO components (
lcsc, category, subcategory, mfr_part, package,
solder_joints, manufacturer, library_type, description,
datasheet, stock, price_json, last_updated
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
part.get('componentCode'), # lcsc
part.get('firstSortName'), # category
part.get('secondSortName'), # subcategory
part.get('componentModelEn'), # mfr_part
part.get('componentSpecificationEn'), # package
part.get('soldPoint'), # solder_joints
part.get('componentBrandEn'), # manufacturer
library_type, # library_type
part.get('describe'), # description
part.get('dataManualUrl'), # datasheet
part.get('stockCount', 0), # stock
price_json, # price_json
int(datetime.now().timestamp()) # last_updated
))
""",
(
part.get("componentCode"), # lcsc
part.get("firstSortName"), # category
part.get("secondSortName"), # subcategory
part.get("componentModelEn"), # mfr_part
part.get("componentSpecificationEn"), # package
part.get("soldPoint"), # solder_joints
part.get("componentBrandEn"), # manufacturer
library_type, # library_type
part.get("describe"), # description
part.get("dataManualUrl"), # datasheet
part.get("stockCount", 0), # stock
price_json, # price_json
int(datetime.now().timestamp()), # last_updated
),
)
imported += 1
@@ -140,10 +147,10 @@ class JLCPCBPartsManager:
skipped += 1
# Update FTS index
cursor.execute('''
cursor.execute("""
INSERT INTO components_fts(components_fts, rowid, lcsc, description, mfr_part, manufacturer)
SELECT 'rebuild', rowid, lcsc, description, mfr_part, manufacturer FROM components
''')
""")
self.conn.commit()
logger.info(f"Import complete: {imported} parts imported, {skipped} skipped")
@@ -151,18 +158,20 @@ class JLCPCBPartsManager:
def _determine_library_type(self, part: Dict) -> str:
"""Determine if part is Basic, Extended, or Preferred"""
# JLCPCB API should provide this, but if not, we infer from assembly type
assembly_type = part.get('assemblyType', '')
assembly_type = part.get("assemblyType", "")
if 'Basic' in assembly_type or part.get('libraryType') == 'base':
return 'Basic'
elif 'Extended' in assembly_type:
return 'Extended'
elif 'Prefer' in assembly_type:
return 'Preferred'
if "Basic" in assembly_type or part.get("libraryType") == "base":
return "Basic"
elif "Extended" in assembly_type:
return "Extended"
elif "Prefer" in assembly_type:
return "Preferred"
else:
return 'Extended' # Default to Extended
return "Extended" # Default to Extended
def import_jlcsearch_parts(self, parts: List[Dict], progress_callback=None):
def import_jlcsearch_parts(
self, parts: List[Dict], progress_callback: Optional[Callable[..., Any]] = None
) -> None:
"""
Import parts into database from JLCSearch API response
@@ -178,56 +187,59 @@ class JLCPCBPartsManager:
try:
# JLCSearch format is different from official API
# LCSC is an integer, we need to add 'C' prefix
lcsc = part.get('lcsc')
lcsc = part.get("lcsc")
if isinstance(lcsc, int):
lcsc = f"C{lcsc}"
# Build price JSON from jlcsearch single price
price = part.get('price') or part.get('price1')
price = part.get("price") or part.get("price1")
price_json = json.dumps([{"qty": 1, "price": price}] if price else [])
# Determine library type from is_basic flag
library_type = 'Basic' if part.get('is_basic') else 'Extended'
if part.get('is_preferred'):
library_type = 'Preferred'
library_type = "Basic" if part.get("is_basic") else "Extended"
if part.get("is_preferred"):
library_type = "Preferred"
# Extract description from various fields
description_parts = []
if 'resistance' in part:
if "resistance" in part:
description_parts.append(f"{part['resistance']}Ω")
if 'capacitance' in part:
if "capacitance" in part:
description_parts.append(f"{part['capacitance']}F")
if 'tolerance_fraction' in part:
tol = part['tolerance_fraction'] * 100
if "tolerance_fraction" in part:
tol = part["tolerance_fraction"] * 100
description_parts.append(f"±{tol}%")
if 'power_watts' in part:
if "power_watts" in part:
description_parts.append(f"{part['power_watts']}mW")
if 'voltage' in part:
if "voltage" in part:
description_parts.append(f"{part['voltage']}V")
description = part.get('description', ' '.join(description_parts))
description = part.get("description", " ".join(description_parts))
cursor.execute('''
cursor.execute(
"""
INSERT OR REPLACE INTO components (
lcsc, category, subcategory, mfr_part, package,
solder_joints, manufacturer, library_type, description,
datasheet, stock, price_json, last_updated
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
lcsc, # lcsc with C prefix
part.get('category', ''), # category
part.get('subcategory', ''), # subcategory
part.get('mfr', ''), # mfr_part
part.get('package', ''), # package
0, # solder_joints (not in jlcsearch)
part.get('manufacturer', ''), # manufacturer
library_type, # library_type
description, # description
'', # datasheet (not in jlcsearch)
part.get('stock', 0), # stock
price_json, # price_json
int(datetime.now().timestamp()) # last_updated
))
""",
(
lcsc, # lcsc with C prefix
part.get("category", ""), # category
part.get("subcategory", ""), # subcategory
part.get("mfr", ""), # mfr_part
part.get("package", ""), # package
0, # solder_joints (not in jlcsearch)
part.get("manufacturer", ""), # manufacturer
library_type, # library_type
description, # description
"", # datasheet (not in jlcsearch)
part.get("stock", 0), # stock
price_json, # price_json
int(datetime.now().timestamp()), # last_updated
),
)
imported += 1
@@ -239,10 +251,10 @@ class JLCPCBPartsManager:
skipped += 1
# Update FTS index
cursor.execute('''
cursor.execute("""
INSERT INTO components_fts(components_fts)
VALUES('rebuild')
''')
""")
self.conn.commit()
logger.info(f"Import complete: {imported} parts imported, {skipped} skipped")
@@ -255,7 +267,7 @@ class JLCPCBPartsManager:
library_type: Optional[str] = None,
manufacturer: Optional[str] = None,
in_stock: bool = True,
limit: int = 20
limit: int = 20,
) -> List[Dict]:
"""
Search for parts with filters
@@ -280,13 +292,18 @@ class JLCPCBPartsManager:
if query:
# Use FTS for text search
sql_parts.append('''
# Add prefix wildcard to each term for partial matching
# (e.g., "BQ25895" becomes "BQ25895*" so FTS matches "BQ25895RTWR")
fts_query = " ".join(
f"{term}*" if not term.endswith("*") else term for term in query.strip().split()
)
sql_parts.append("""
AND lcsc IN (
SELECT lcsc FROM components_fts
WHERE components_fts MATCH ?
)
''')
params.append(query)
""")
params.append(fts_query)
if category:
sql_parts.append("AND category LIKE ?")
@@ -337,11 +354,11 @@ class JLCPCBPartsManager:
if row:
part = dict(row)
# Parse price JSON
if part.get('price_json'):
if part.get("price_json"):
try:
part['price_breaks'] = json.loads(part['price_json'])
part["price_breaks"] = json.loads(part["price_json"])
except:
part['price_breaks'] = []
part["price_breaks"] = []
return part
return None
@@ -350,23 +367,25 @@ class JLCPCBPartsManager:
cursor = self.conn.cursor()
cursor.execute("SELECT COUNT(*) as total FROM components")
total = cursor.fetchone()['total']
total = cursor.fetchone()["total"]
cursor.execute("SELECT COUNT(*) as basic FROM components WHERE library_type = 'Basic'")
basic = cursor.fetchone()['basic']
basic = cursor.fetchone()["basic"]
cursor.execute("SELECT COUNT(*) as extended FROM components WHERE library_type = 'Extended'")
extended = cursor.fetchone()['extended']
cursor.execute(
"SELECT COUNT(*) as extended FROM components WHERE library_type = 'Extended'"
)
extended = cursor.fetchone()["extended"]
cursor.execute("SELECT COUNT(*) as in_stock FROM components WHERE stock > 0")
in_stock = cursor.fetchone()['in_stock']
in_stock = cursor.fetchone()["in_stock"]
return {
'total_parts': total,
'basic_parts': basic,
'extended_parts': extended,
'in_stock': in_stock,
'db_path': self.db_path
"total_parts": total,
"basic_parts": basic,
"extended_parts": extended,
"in_stock": in_stock,
"db_path": self.db_path,
}
def map_package_to_footprint(self, package: str) -> List[str]:
@@ -384,43 +403,22 @@ class JLCPCBPartsManager:
"0402": [
"Resistor_SMD:R_0402_1005Metric",
"Capacitor_SMD:C_0402_1005Metric",
"LED_SMD:LED_0402_1005Metric"
"LED_SMD:LED_0402_1005Metric",
],
"0603": [
"Resistor_SMD:R_0603_1608Metric",
"Capacitor_SMD:C_0603_1608Metric",
"LED_SMD:LED_0603_1608Metric"
"LED_SMD:LED_0603_1608Metric",
],
"0805": [
"Resistor_SMD:R_0805_2012Metric",
"Capacitor_SMD:C_0805_2012Metric"
],
"1206": [
"Resistor_SMD:R_1206_3216Metric",
"Capacitor_SMD:C_1206_3216Metric"
],
"SOT-23": [
"Package_TO_SOT_SMD:SOT-23",
"Package_TO_SOT_SMD:SOT-23-3"
],
"SOT-23-5": [
"Package_TO_SOT_SMD:SOT-23-5"
],
"SOT-23-6": [
"Package_TO_SOT_SMD:SOT-23-6"
],
"SOIC-8": [
"Package_SO:SOIC-8_3.9x4.9mm_P1.27mm"
],
"SOIC-16": [
"Package_SO:SOIC-16_3.9x9.9mm_P1.27mm"
],
"QFN-20": [
"Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm"
],
"QFN-32": [
"Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm"
]
"0805": ["Resistor_SMD:R_0805_2012Metric", "Capacitor_SMD:C_0805_2012Metric"],
"1206": ["Resistor_SMD:R_1206_3216Metric", "Capacitor_SMD:C_1206_3216Metric"],
"SOT-23": ["Package_TO_SOT_SMD:SOT-23", "Package_TO_SOT_SMD:SOT-23-3"],
"SOT-23-5": ["Package_TO_SOT_SMD:SOT-23-5"],
"SOT-23-6": ["Package_TO_SOT_SMD:SOT-23-6"],
"SOIC-8": ["Package_SO:SOIC-8_3.9x4.9mm_P1.27mm"],
"SOIC-16": ["Package_SO:SOIC-16_3.9x9.9mm_P1.27mm"],
"QFN-20": ["Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm"],
"QFN-32": ["Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm"],
}
# Normalize package name
@@ -451,24 +449,21 @@ class JLCPCBPartsManager:
# Search for parts in same category with same package
alternatives = self.search_parts(
category=part['subcategory'],
package=part['package'],
in_stock=True,
limit=limit * 3
category=part["subcategory"], package=part["package"], in_stock=True, limit=limit * 3
)
# Filter out the original part
alternatives = [p for p in alternatives if p['lcsc'] != lcsc_number]
alternatives = [p for p in alternatives if p["lcsc"] != lcsc_number]
# Sort by: Basic first, then by price, then by stock
def sort_key(p):
is_basic = 1 if p.get('library_type') == 'Basic' else 0
def sort_key(p: Dict[str, Any]) -> Tuple[int, float, int]:
is_basic = 1 if p.get("library_type") == "Basic" else 0
try:
prices = json.loads(p.get('price_json', '[]'))
price = float(prices[0].get('price', 999)) if prices else 999
prices = json.loads(p.get("price_json", "[]"))
price = float(prices[0].get("price", 999)) if prices else 999
except:
price = 999
stock = p.get('stock', 0)
stock = p.get("stock", 0)
return (-is_basic, price, -stock)
@@ -476,13 +471,13 @@ class JLCPCBPartsManager:
return alternatives[:limit]
def close(self):
def close(self) -> None:
"""Close database connection"""
if self.conn:
self.conn.close()
if __name__ == '__main__':
if __name__ == "__main__":
# Test the parts manager
logging.basicConfig(level=logging.INFO)
@@ -497,8 +492,10 @@ if __name__ == '__main__':
print(f" In stock: {stats['in_stock']}")
print(f" Database: {stats['db_path']}")
if stats['total_parts'] > 0:
if stats["total_parts"] > 0:
print("\nSearching for '10k resistor'...")
results = manager.search_parts(query="10k resistor", limit=5)
for part in results:
print(f" {part['lcsc']}: {part['mfr_part']} - {part['description']} ({part['library_type']})")
print(
f" {part['lcsc']}: {part['mfr_part']} - {part['description']} ({part['library_type']})"
)

View File

@@ -6,11 +6,12 @@ jlcsearch service at https://jlcsearch.tscircuit.com/
"""
import logging
import requests
from typing import Optional, Dict, List, Callable
import time
from typing import Any, Callable, Dict, List, Optional, Union
logger = logging.getLogger('kicad_interface')
import requests
logger = logging.getLogger("kicad_interface")
class JLCSearchClient:
@@ -23,16 +24,12 @@ class JLCSearchClient:
BASE_URL = "https://jlcsearch.tscircuit.com"
def __init__(self):
def __init__(self) -> None:
"""Initialize JLCSearch API client"""
pass
def search_components(
self,
category: str = "components",
limit: int = 100,
offset: int = 0,
**filters
self, category: str = "components", limit: int = 100, offset: int = 0, **filters: Dict
) -> List[Dict]:
"""
Search components in JLCSearch database
@@ -48,11 +45,7 @@ class JLCSearchClient:
"""
url = f"{self.BASE_URL}/{category}/list.json"
params = {
"limit": limit,
"offset": offset,
**filters
}
params = {"limit": limit, "offset": offset, **filters}
try:
response = requests.get(url, params=params, timeout=30)
@@ -71,7 +64,9 @@ class JLCSearchClient:
logger.error(f"Failed to search JLCSearch: {e}")
raise Exception(f"JLCSearch API request failed: {e}")
def search_resistors(self, resistance: Optional[int] = None, package: Optional[str] = None, limit: int = 100) -> List[Dict]:
def search_resistors(
self, resistance: Optional[int] = None, package: Optional[str] = None, limit: int = 100
) -> List[Dict]:
"""
Search for resistors
@@ -92,7 +87,7 @@ class JLCSearchClient:
- stock: Available stock
- price1: Price per unit
"""
filters = {}
filters: Dict[str, Any] = {}
if resistance is not None:
filters["resistance"] = resistance
if package:
@@ -100,7 +95,9 @@ class JLCSearchClient:
return self.search_components("resistors", limit=limit, **filters)
def search_capacitors(self, capacitance: Optional[float] = None, package: Optional[str] = None, limit: int = 100) -> List[Dict]:
def search_capacitors(
self, capacitance: Optional[float] = None, package: Optional[str] = None, limit: int = 100
) -> List[Dict]:
"""
Search for capacitors
@@ -112,7 +109,7 @@ class JLCSearchClient:
Returns:
List of capacitor dicts
"""
filters = {}
filters: Dict[str, Any] = {}
if capacitance is not None:
filters["capacitance"] = capacitance
if package:
@@ -141,9 +138,7 @@ class JLCSearchClient:
return None
def download_all_components(
self,
callback: Optional[Callable[[int, str], None]] = None,
batch_size: int = 100
self, callback: Optional[Callable[[int, str], None]] = None, batch_size: int = 100
) -> List[Dict]:
"""
Download all components from jlcsearch database
@@ -165,11 +160,7 @@ class JLCSearchClient:
while True:
try:
batch = self.search_components(
"components",
limit=batch_size,
offset=offset
)
batch = self.search_components("components", limit=batch_size, offset=offset)
# Stop if no results returned (end of catalog)
if not batch or len(batch) == 0:
@@ -219,7 +210,7 @@ def test_jlcsearch_connection() -> bool:
return False
if __name__ == '__main__':
if __name__ == "__main__":
# Test the JLCSearch client
logging.basicConfig(level=logging.INFO)

View File

@@ -5,12 +5,12 @@ Handles parsing fp-lib-table files, discovering footprints,
and providing search functionality for component placement.
"""
import glob
import logging
import os
import re
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import glob
logger = logging.getLogger("kicad_interface")
@@ -35,7 +35,7 @@ class LibraryManager:
self.footprint_cache: Dict[str, List[str]] = {} # library -> [footprint names]
self._load_libraries()
def _load_libraries(self):
def _load_libraries(self) -> None:
"""Load libraries from fp-lib-table files"""
# Load global libraries
global_table = self._get_global_fp_lib_table()
@@ -58,13 +58,16 @@ class LibraryManager:
"""Get path to global fp-lib-table file"""
# Try different possible locations
kicad_config_paths = [
Path.home() / ".config" / "kicad" / "10.0" / "fp-lib-table",
Path.home() / ".config" / "kicad" / "9.0" / "fp-lib-table",
Path.home() / ".config" / "kicad" / "8.0" / "fp-lib-table",
Path.home() / ".config" / "kicad" / "fp-lib-table",
# Windows paths
Path.home() / "AppData" / "Roaming" / "kicad" / "10.0" / "fp-lib-table",
Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "fp-lib-table",
Path.home() / "AppData" / "Roaming" / "kicad" / "8.0" / "fp-lib-table",
# macOS paths
Path.home() / "Library" / "Preferences" / "kicad" / "10.0" / "fp-lib-table",
Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "fp-lib-table",
Path.home() / "Library" / "Preferences" / "kicad" / "8.0" / "fp-lib-table",
]
@@ -75,7 +78,7 @@ class LibraryManager:
return None
def _parse_fp_lib_table(self, table_path: Path):
def _parse_fp_lib_table(self, table_path: Path) -> None:
"""
Parse fp-lib-table file
@@ -90,11 +93,21 @@ class LibraryManager:
# Simple regex-based parser for lib entries
# Pattern: (lib (name "NAME")(type TYPE)(uri "URI")...)
lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^")\s]+)"?'
lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+"?([^")\s]+)"?\)\s*\(uri\s+"?([^")\s]+)"?'
for match in re.finditer(lib_pattern, content, re.IGNORECASE):
nickname = match.group(1)
uri = match.group(2)
lib_type = match.group(2)
uri = match.group(3)
if lib_type.lower() == "table":
table_uri = uri
if os.path.isabs(table_uri) and os.path.isfile(table_uri):
logger.info(f" Following Table reference: {nickname} -> {table_uri}")
self._parse_fp_lib_table(Path(table_uri))
else:
logger.warning(f" Could not resolve Table URI: {table_uri}")
continue
# Resolve environment variables in URI
resolved_uri = self._resolve_uri(uri)
@@ -103,9 +116,7 @@ class LibraryManager:
self.libraries[nickname] = resolved_uri
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
else:
logger.warning(
f" Could not resolve URI for library {nickname}: {uri}"
)
logger.warning(f" Could not resolve URI for library {nickname}: {uri}")
except Exception as e:
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
@@ -126,10 +137,12 @@ class LibraryManager:
# Common KiCAD environment variables
env_vars = {
"KICAD10_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
"KICAD9_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
"KICAD8_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
"KICAD_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
"KISYSMOD": self._find_kicad_footprint_dir(),
"KICAD10_3RD_PARTY": self._find_kicad_3rdparty_dir(),
"KICAD9_3RD_PARTY": self._find_kicad_3rdparty_dir(),
"KICAD8_3RD_PARTY": self._find_kicad_3rdparty_dir(),
}
@@ -206,12 +219,7 @@ class LibraryManager:
/ "9.0"
/ "kicad_common.json", # macOS
Path.home() / ".config" / "kicad" / "9.0" / "kicad_common.json", # Linux
Path.home()
/ "AppData"
/ "Roaming"
/ "kicad"
/ "9.0"
/ "kicad_common.json", # Windows
Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "kicad_common.json", # Windows
]
for config_path in kicad_common_paths:
@@ -337,9 +345,7 @@ class LibraryManager:
for library_nickname, library_path in self.libraries.items():
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
if fp_file.exists():
logger.info(
f"Found footprint {footprint_name} in library {library_nickname}"
)
logger.info(f"Found footprint {footprint_name} in library {library_nickname}")
return (library_path, footprint_name)
logger.warning(f"Footprint not found in any library: {footprint_name}")
@@ -446,9 +452,7 @@ class LibraryCommands:
# Filter by library if specified
if library_filter:
results = [
r
for r in results
if r.get("library", "").lower() == library_filter.lower()
r for r in results if r.get("library", "").lower() == library_filter.lower()
]
results = results[:limit]
@@ -492,7 +496,7 @@ class LibraryCommands:
def get_footprint_info(self, params: Dict) -> Dict:
"""Get information about a specific footprint"""
try:
footprint_spec = params.get("footprint")
footprint_spec = params.get("footprint_name")
if not footprint_spec:
return {"success": False, "message": "Missing footprint parameter"}
@@ -508,19 +512,39 @@ class LibraryCommands:
library_nickname = nick
break
info = {
"library": library_nickname,
"footprint": footprint_name,
"full_name": f"{library_nickname}:{footprint_name}",
"library_path": library_path,
}
# Minimal info — always returned even if the parser fails
info: Dict = {
"library": library_nickname,
"name": footprint_name,
"full_name": f"{library_nickname}:{footprint_name}",
"library_path": library_path,
}
return {"success": True, "footprint_info": info}
else:
return {
"success": False,
"message": f"Footprint not found: {footprint_spec}",
}
# Attempt to enrich with parsed .kicad_mod data
try:
from pathlib import Path as _Path
from parsers.kicad_mod_parser import parse_kicad_mod
mod_file = str(_Path(library_path) / f"{footprint_name}.kicad_mod")
parsed = parse_kicad_mod(mod_file)
if parsed:
# Merge parser output into info; keep our resolved library context
info.update(parsed)
info["name"] = footprint_name # entry name wins over in-file name
info["library"] = library_nickname
info["full_name"] = f"{library_nickname}:{footprint_name}"
info["library_path"] = library_path
else:
logger.warning(
f"get_footprint_info: parser returned nothing for {mod_file}, using minimal info"
)
except Exception as parse_err:
logger.warning(
f"get_footprint_info: parser error ({parse_err}), using minimal info"
)
return {"success": True, "info": info}
except Exception as e:
logger.error(f"Error getting footprint info: {e}")

View File

@@ -1,22 +1,31 @@
from skip import Schematic
import glob
import logging
# Symbol class might not be directly importable in the current version
import os
import glob
from typing import Any, Dict, List, Optional
from skip import Schematic
logger = logging.getLogger(__name__)
class LibraryManager:
"""Manage symbol libraries"""
@staticmethod
def list_available_libraries(search_paths=None):
def list_available_libraries(search_paths: Optional[List[str]] = None) -> Dict[str, List[str]]:
"""List all available symbol libraries"""
if search_paths is None:
# Default library paths based on common KiCAD installations
# This would need to be configured for the specific environment
search_paths = [
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym", # Windows path pattern
"/usr/share/kicad/symbols/*.kicad_sym", # Linux path pattern
"/usr/share/kicad/symbols/*.kicad_sym", # Linux path pattern
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", # macOS path pattern
os.path.expanduser("~/Documents/KiCad/*/symbols/*.kicad_sym") # User libraries pattern
os.path.expanduser(
"~/Documents/KiCad/*/symbols/*.kicad_sym"
), # User libraries pattern
]
libraries = []
@@ -26,70 +35,80 @@ class LibraryManager:
matching_libs = glob.glob(path_pattern, recursive=True)
libraries.extend(matching_libs)
except Exception as e:
print(f"Error searching for libraries at {path_pattern}: {e}")
logger.error(f"Error searching for libraries at {path_pattern}: {e}")
# Extract library names from paths
library_names = [os.path.splitext(os.path.basename(lib))[0] for lib in libraries]
print(f"Found {len(library_names)} libraries: {', '.join(library_names[:10])}{'...' if len(library_names) > 10 else ''}")
logger.info(
f"Found {len(library_names)} libraries: {', '.join(library_names[:10])}{'...' if len(library_names) > 10 else ''}"
)
# Return both full paths and library names
return {"paths": libraries, "names": library_names}
@staticmethod
def list_library_symbols(library_path):
def list_library_symbols(library_path: str) -> List[Any]:
"""List all symbols in a library"""
try:
# kicad-skip doesn't provide a direct way to simply list symbols in a library
# without loading each one. We might need to implement this using KiCAD's Python API
# directly, or by using a different approach.
# For now, this is a placeholder implementation.
# A potential approach would be to load the library file using KiCAD's Python API
# or by parsing the library file format.
# KiCAD symbol libraries are .kicad_sym files which are S-expression format
print(f"Attempted to list symbols in library {library_path}. This requires advanced implementation.")
logger.warning(
f"Attempted to list symbols in library {library_path}. This requires advanced implementation."
)
return []
except Exception as e:
print(f"Error listing symbols in library {library_path}: {e}")
logger.error(f"Error listing symbols in library {library_path}: {e}")
return []
@staticmethod
def get_symbol_details(library_path, symbol_name):
def get_symbol_details(library_path: str, symbol_name: str) -> Dict[str, Any]:
"""Get detailed information about a symbol"""
try:
# Similar to list_library_symbols, this might require a more direct approach
# using KiCAD's Python API or by parsing the symbol library.
print(f"Attempted to get details for symbol {symbol_name} in library {library_path}. This requires advanced implementation.")
logger.warning(
f"Attempted to get details for symbol {symbol_name} in library {library_path}. This requires advanced implementation."
)
return {}
except Exception as e:
print(f"Error getting symbol details for {symbol_name} in {library_path}: {e}")
logger.error(f"Error getting symbol details for {symbol_name} in {library_path}: {e}")
return {}
@staticmethod
def search_symbols(query, search_paths=None):
def search_symbols(query: str, search_paths: Optional[List[str]] = None) -> List[Any]:
"""Search for symbols matching criteria"""
try:
# This would typically involve:
# 1. Getting a list of all libraries using list_available_libraries
# 2. For each library, getting a list of all symbols
# 3. Filtering symbols based on the query
# For now, this is a placeholder implementation
libraries = LibraryManager.list_available_libraries(search_paths)
results = []
print(f"Searched for symbols matching '{query}'. This requires advanced implementation.")
logger.warning(
f"Searched for symbols matching '{query}'. This requires advanced implementation."
)
return results
except Exception as e:
print(f"Error searching for symbols matching '{query}': {e}")
logger.error(f"Error searching for symbols matching '{query}': {e}")
return []
@staticmethod
def get_default_symbol_for_component_type(component_type, search_paths=None):
def get_default_symbol_for_component_type(
component_type: str, search_paths: Optional[List[str]] = None
) -> Dict[str, str]:
"""Get a recommended default symbol for a given component type"""
# This method provides a simplified way to get a symbol for common component types
# It's useful when the user doesn't specify a particular library/symbol
# Define common mappings from component type to library/symbol
common_mappings = {
"resistor": {"library": "Device", "symbol": "R"},
@@ -103,23 +122,24 @@ class LibraryManager:
"microcontroller": {"library": "MCU_Module", "symbol": "Arduino_UNO_R3"},
# Add more common components as needed
}
# Normalize input to lowercase
component_type_lower = component_type.lower()
# Try direct match first
if component_type_lower in common_mappings:
return common_mappings[component_type_lower]
# Try partial matches
for key, value in common_mappings.items():
if component_type_lower in key or key in component_type_lower:
return value
# Default fallback
return {"library": "Device", "symbol": "R"}
if __name__ == '__main__':
if __name__ == "__main__":
# Example Usage (for testing)
# List available libraries
libraries = LibraryManager.list_available_libraries()
@@ -127,15 +147,15 @@ if __name__ == '__main__':
first_lib = libraries["paths"][0]
lib_name = libraries["names"][0]
print(f"Testing with first library: {lib_name} ({first_lib})")
# List symbols in the first library
symbols = LibraryManager.list_library_symbols(first_lib)
# This will report that it requires advanced implementation
# Get default symbol for a component type
resistor_sym = LibraryManager.get_default_symbol_for_component_type("resistor")
print(f"Default symbol for resistor: {resistor_sym['library']}/{resistor_sym['symbol']}")
# Try a partial match
cap_sym = LibraryManager.get_default_symbol_for_component_type("cap")
print(f"Default symbol for 'cap': {cap_sym['library']}/{cap_sym['symbol']}")

View File

@@ -5,33 +5,34 @@ Handles parsing sym-lib-table files, discovering symbols,
and providing search functionality for component selection.
"""
import logging
import os
import re
import logging
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
logger = logging.getLogger('kicad_interface')
logger = logging.getLogger("kicad_interface")
@dataclass
class SymbolInfo:
"""Information about a symbol in a library"""
name: str # Symbol name (without library prefix)
library: str # Library nickname
full_ref: str # "Library:SymbolName"
value: str = "" # Value property
description: str = "" # Description property
footprint: str = "" # Footprint reference if present
lcsc_id: str = "" # LCSC property if present
name: str # Symbol name (without library prefix)
library: str # Library nickname
full_ref: str # "Library:SymbolName"
value: str = "" # Value property
description: str = "" # Description property
footprint: str = "" # Footprint reference if present
lcsc_id: str = "" # LCSC property if present
manufacturer: str = "" # Manufacturer property
mpn: str = "" # Part/MPN property
category: str = "" # Category property
datasheet: str = "" # Datasheet URL
stock: str = "" # Stock (from JLCPCB libs)
price: str = "" # Price (from JLCPCB libs)
lib_class: str = "" # Basic/Preferred/Extended
mpn: str = "" # Part/MPN property
category: str = "" # Category property
datasheet: str = "" # Datasheet URL
stock: str = "" # Stock (from JLCPCB libs)
price: str = "" # Price (from JLCPCB libs)
lib_class: str = "" # Basic/Preferred/Extended
class SymbolLibraryManager:
@@ -54,7 +55,7 @@ class SymbolLibraryManager:
self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo]
self._load_libraries()
def _load_libraries(self):
def _load_libraries(self) -> None:
"""Load libraries from sym-lib-table files"""
# Load global libraries
global_table = self._get_global_sym_lib_table()
@@ -77,13 +78,16 @@ class SymbolLibraryManager:
"""Get path to global sym-lib-table file"""
# Try different possible locations (same as fp-lib-table but for symbols)
kicad_config_paths = [
Path.home() / ".config" / "kicad" / "10.0" / "sym-lib-table",
Path.home() / ".config" / "kicad" / "9.0" / "sym-lib-table",
Path.home() / ".config" / "kicad" / "8.0" / "sym-lib-table",
Path.home() / ".config" / "kicad" / "sym-lib-table",
# Windows paths
Path.home() / "AppData" / "Roaming" / "kicad" / "10.0" / "sym-lib-table",
Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "sym-lib-table",
Path.home() / "AppData" / "Roaming" / "kicad" / "8.0" / "sym-lib-table",
# macOS paths
Path.home() / "Library" / "Preferences" / "kicad" / "10.0" / "sym-lib-table",
Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "sym-lib-table",
Path.home() / "Library" / "Preferences" / "kicad" / "8.0" / "sym-lib-table",
]
@@ -94,7 +98,7 @@ class SymbolLibraryManager:
return None
def _parse_sym_lib_table(self, table_path: Path):
def _parse_sym_lib_table(self, table_path: Path) -> None:
"""
Parse sym-lib-table file
@@ -104,16 +108,26 @@ class SymbolLibraryManager:
)
"""
try:
with open(table_path, 'r', encoding='utf-8') as f:
with open(table_path, "r", encoding="utf-8") as f:
content = f.read()
# Simple regex-based parser for lib entries
# Pattern: (lib (name "NAME")(type TYPE)(uri "URI")...)
lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^")\s]+)"?'
lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+"?([^")\s]+)"?\)\s*\(uri\s+"?([^")\s]+)"?'
for match in re.finditer(lib_pattern, content, re.IGNORECASE):
nickname = match.group(1)
uri = match.group(2)
lib_type = match.group(2)
uri = match.group(3)
if lib_type.lower() == "table":
table_uri = uri
if os.path.isabs(table_uri) and os.path.isfile(table_uri):
logger.info(f" Following Table reference: {nickname} -> {table_uri}")
self._parse_sym_lib_table(Path(table_uri))
else:
logger.warning(f" Could not resolve Table URI: {table_uri}")
continue
# Resolve environment variables in URI
resolved_uri = self._resolve_uri(uri)
@@ -142,23 +156,25 @@ class SymbolLibraryManager:
# Common KiCAD environment variables
env_vars = {
'KICAD9_SYMBOL_DIR': self._find_kicad_symbol_dir(),
'KICAD8_SYMBOL_DIR': self._find_kicad_symbol_dir(),
'KICAD_SYMBOL_DIR': self._find_kicad_symbol_dir(),
'KICAD9_3RD_PARTY': self._find_3rd_party_dir(),
'KICAD8_3RD_PARTY': self._find_3rd_party_dir(),
'KISYSSYM': self._find_kicad_symbol_dir(),
"KICAD10_SYMBOL_DIR": self._find_kicad_symbol_dir(),
"KICAD9_SYMBOL_DIR": self._find_kicad_symbol_dir(),
"KICAD8_SYMBOL_DIR": self._find_kicad_symbol_dir(),
"KICAD_SYMBOL_DIR": self._find_kicad_symbol_dir(),
"KICAD10_3RD_PARTY": self._find_3rd_party_dir(),
"KICAD9_3RD_PARTY": self._find_3rd_party_dir(),
"KICAD8_3RD_PARTY": self._find_3rd_party_dir(),
"KISYSSYM": self._find_kicad_symbol_dir(),
}
# Project directory
if self.project_path:
env_vars['KIPRJMOD'] = str(self.project_path)
env_vars["KIPRJMOD"] = str(self.project_path)
# Replace environment variables
for var, value in env_vars.items():
if value:
resolved = resolved.replace(f'${{{var}}}', value)
resolved = resolved.replace(f'${var}', value)
resolved = resolved.replace(f"${{{var}}}", value)
resolved = resolved.replace(f"${var}", value)
# Expand ~ to home directory
resolved = os.path.expanduser(resolved)
@@ -184,10 +200,10 @@ class SymbolLibraryManager:
]
# Check environment variable
if 'KICAD9_SYMBOL_DIR' in os.environ:
possible_paths.insert(0, os.environ['KICAD9_SYMBOL_DIR'])
if 'KICAD8_SYMBOL_DIR' in os.environ:
possible_paths.insert(0, os.environ['KICAD8_SYMBOL_DIR'])
if "KICAD9_SYMBOL_DIR" in os.environ:
possible_paths.insert(0, os.environ["KICAD9_SYMBOL_DIR"])
if "KICAD8_SYMBOL_DIR" in os.environ:
possible_paths.insert(0, os.environ["KICAD8_SYMBOL_DIR"])
for path in possible_paths:
if os.path.isdir(path):
@@ -198,15 +214,18 @@ class SymbolLibraryManager:
def _find_3rd_party_dir(self) -> Optional[str]:
"""Find KiCAD 3rd party library directory (PCM installed libs)"""
possible_paths = [
str(Path.home() / "Documents" / "KiCad" / "10.0" / "3rdparty"),
str(Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty"),
str(Path.home() / "Documents" / "KiCad" / "8.0" / "3rdparty"),
]
# Check environment variable
if 'KICAD9_3RD_PARTY' in os.environ:
possible_paths.insert(0, os.environ['KICAD9_3RD_PARTY'])
if 'KICAD8_3RD_PARTY' in os.environ:
possible_paths.insert(0, os.environ['KICAD8_3RD_PARTY'])
if "KICAD10_3RD_PARTY" in os.environ:
possible_paths.insert(0, os.environ["KICAD10_3RD_PARTY"])
if "KICAD9_3RD_PARTY" in os.environ:
possible_paths.insert(0, os.environ["KICAD9_3RD_PARTY"])
if "KICAD8_3RD_PARTY" in os.environ:
possible_paths.insert(0, os.environ["KICAD8_3RD_PARTY"])
for path in possible_paths:
if os.path.isdir(path):
@@ -228,7 +247,7 @@ class SymbolLibraryManager:
symbols = []
try:
with open(library_path, 'r', encoding='utf-8') as f:
with open(library_path, "r", encoding="utf-8") as f:
content = f.read()
# Find all top-level symbol definitions
@@ -243,7 +262,7 @@ class SymbolLibraryManager:
symbol_name = match.group(1)
# Skip sub-symbols (they contain _0_, _1_, etc. suffixes)
if re.search(r'_\d+_\d+$', symbol_name):
if re.search(r"_\d+_\d+$", symbol_name):
continue
# Find the start position of this symbol
@@ -262,17 +281,17 @@ class SymbolLibraryManager:
name=symbol_name,
library=library_name,
full_ref=f"{library_name}:{symbol_name}",
value=properties.get('Value', ''),
description=properties.get('Description', ''),
footprint=properties.get('Footprint', ''),
lcsc_id=properties.get('LCSC', ''),
manufacturer=properties.get('Manufacturer', ''),
mpn=properties.get('Part', properties.get('MPN', '')),
category=properties.get('Category', ''),
datasheet=properties.get('Datasheet', ''),
stock=properties.get('Stock', ''),
price=properties.get('Price', ''),
lib_class=properties.get('Class', ''),
value=properties.get("Value", ""),
description=properties.get("Description", ""),
footprint=properties.get("Footprint", ""),
lcsc_id=properties.get("LCSC", ""),
manufacturer=properties.get("Manufacturer", ""),
mpn=properties.get("Part", properties.get("MPN", "")),
category=properties.get("Category", ""),
datasheet=properties.get("Datasheet", ""),
stock=properties.get("Stock", ""),
price=properties.get("Price", ""),
lib_class=properties.get("Class", ""),
)
symbols.append(symbol_info)
@@ -333,7 +352,9 @@ class SymbolLibraryManager:
return symbols
def search_symbols(self, query: str, limit: int = 20, library_filter: Optional[str] = None) -> List[SymbolInfo]:
def search_symbols(
self, query: str, limit: int = 20, library_filter: Optional[str] = None
) -> List[SymbolInfo]:
"""
Search for symbols matching a query
@@ -349,10 +370,12 @@ class SymbolLibraryManager:
query_lower = query.lower()
# Determine which libraries to search
libraries_to_search = self.libraries.keys()
libraries_to_search: list[str] = list(self.libraries.keys())
if library_filter:
filter_lower = library_filter.lower()
libraries_to_search = [lib for lib in libraries_to_search if filter_lower in lib.lower()]
libraries_to_search = [
lib for lib in libraries_to_search if filter_lower in lib.lower()
]
for library_nickname in libraries_to_search:
symbols = self.list_symbols(library_nickname)
@@ -477,17 +500,13 @@ class SymbolLibraryCommands:
"""List all available symbol libraries"""
try:
libraries = self.library_manager.list_libraries()
return {
"success": True,
"libraries": libraries,
"count": len(libraries)
}
return {"success": True, "libraries": libraries, "count": len(libraries)}
except Exception as e:
logger.error(f"Error listing symbol libraries: {e}")
return {
"success": False,
"message": "Failed to list symbol libraries",
"errorDetails": str(e)
"errorDetails": str(e),
}
def search_symbols(self, params: Dict) -> Dict:
@@ -495,10 +514,7 @@ class SymbolLibraryCommands:
try:
query = params.get("query", "")
if not query:
return {
"success": False,
"message": "Missing query parameter"
}
return {"success": False, "message": "Missing query parameter"}
limit = params.get("limit", 20)
library_filter = params.get("library")
@@ -509,25 +525,18 @@ class SymbolLibraryCommands:
"success": True,
"symbols": [asdict(s) for s in results],
"count": len(results),
"query": query
"query": query,
}
except Exception as e:
logger.error(f"Error searching symbols: {e}")
return {
"success": False,
"message": "Failed to search symbols",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to search symbols", "errorDetails": str(e)}
def list_library_symbols(self, params: Dict) -> Dict:
"""List all symbols in a specific library"""
try:
library = params.get("library")
if not library:
return {
"success": False,
"message": "Missing library parameter"
}
return {"success": False, "message": "Missing library parameter"}
# Check if library exists in sym-lib-table
if library not in self.library_manager.libraries:
@@ -536,10 +545,10 @@ class SymbolLibraryCommands:
"success": False,
"message": f"Library '{library}' not found in sym-lib-table",
"errorDetails": f"Library '{library}' is not registered in your KiCad symbol library table. "
f"Found {len(available_libs)} libraries. "
f"Please add this library to your sym-lib-table file, or use one of the available libraries.",
f"Found {len(available_libs)} libraries. "
f"Please add this library to your sym-lib-table file, or use one of the available libraries.",
"available_libraries_count": len(available_libs),
"suggestion": "Use 'list_symbol_libraries' to see all available libraries"
"suggestion": "Use 'list_symbol_libraries' to see all available libraries",
}
symbols = self.library_manager.list_symbols(library)
@@ -548,14 +557,14 @@ class SymbolLibraryCommands:
"success": True,
"library": library,
"symbols": [asdict(s) for s in symbols],
"count": len(symbols)
"count": len(symbols),
}
except Exception as e:
logger.error(f"Error listing library symbols: {e}")
return {
"success": False,
"message": "Failed to list library symbols",
"errorDetails": str(e)
"errorDetails": str(e),
}
def get_symbol_info(self, params: Dict) -> Dict:
@@ -563,34 +572,25 @@ class SymbolLibraryCommands:
try:
symbol_spec = params.get("symbol")
if not symbol_spec:
return {
"success": False,
"message": "Missing symbol parameter"
}
return {"success": False, "message": "Missing symbol parameter"}
result = self.library_manager.find_symbol(symbol_spec)
if result:
return {
"success": True,
"symbol_info": asdict(result)
}
return {"success": True, "symbol_info": asdict(result)}
else:
return {
"success": False,
"message": f"Symbol not found: {symbol_spec}"
}
return {"success": False, "message": f"Symbol not found: {symbol_spec}"}
except Exception as e:
logger.error(f"Error getting symbol info: {e}")
return {
"success": False,
"message": "Failed to get symbol info",
"errorDetails": str(e)
"errorDetails": str(e),
}
if __name__ == '__main__':
if __name__ == "__main__":
# Test the symbol library manager
import json

View File

@@ -9,7 +9,8 @@ import logging
import math
import tempfile
from pathlib import Path
from typing import List, Tuple, Optional, Dict
from typing import Any, Dict, List, Optional, Tuple
import sexpdata
from sexpdata import Symbol
from skip import Schematic
@@ -20,7 +21,7 @@ logger = logging.getLogger("kicad_interface")
class PinLocator:
"""Locate pins on symbol instances in KiCad schematics"""
def __init__(self):
def __init__(self) -> None:
"""Initialize pin locator with empty cache"""
self.pin_definition_cache = {} # Cache: "lib_id:symbol_name" -> pin_data
self._schematic_cache: Dict[str, object] = {} # Cache: path -> loaded Schematic
@@ -40,9 +41,9 @@ class PinLocator:
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"}
}
"""
pins = {}
pins: Dict[str, Dict[str, Any]] = {}
def extract_pins_recursive(sexp):
def extract_pins_recursive(sexp: Any) -> None:
"""Recursively search for pin definitions"""
if not isinstance(sexp, list):
return
@@ -117,11 +118,7 @@ class PinLocator:
# Find lib_symbols section
lib_symbols = None
for item in sch_data:
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("lib_symbols")
):
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol("lib_symbols"):
lib_symbols = item
break
@@ -131,11 +128,7 @@ class PinLocator:
# Find the specific symbol definition
for item in lib_symbols[1:]: # Skip 'lib_symbols' itself
if (
isinstance(item, list)
and len(item) > 1
and item[0] == Symbol("symbol")
):
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol("symbol"):
symbol_name = str(item[1]).strip('"')
if symbol_name == lib_id:
# Found the symbol, parse pins
@@ -284,9 +277,7 @@ class PinLocator:
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
# Get symbol lib_id
lib_id = (
target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
)
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
if not lib_id:
logger.error(f"Symbol {symbol_reference} has no lib_id")
return None
@@ -309,7 +300,9 @@ class PinLocator:
None,
)
if matched_num:
logger.debug(f"Resolved pin name '{pin_number}' to pin number '{matched_num}' on {symbol_reference}")
logger.debug(
f"Resolved pin name '{pin_number}' to pin number '{matched_num}' on {symbol_reference}"
)
pin_number = matched_num
else:
logger.error(
@@ -324,26 +317,18 @@ class PinLocator:
pin_rel_x = pin_data["x"]
pin_rel_y = pin_data["y"]
logger.debug(
f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})"
)
logger.debug(f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})")
# Apply symbol rotation to pin position
if symbol_rotation != 0:
pin_rel_x, pin_rel_y = self.rotate_point(
pin_rel_x, pin_rel_y, symbol_rotation
)
logger.debug(
f"After rotation {symbol_rotation}°: ({pin_rel_x}, {pin_rel_y})"
)
pin_rel_x, pin_rel_y = self.rotate_point(pin_rel_x, pin_rel_y, symbol_rotation)
logger.debug(f"After rotation {symbol_rotation}°: ({pin_rel_x}, {pin_rel_y})")
# Calculate absolute position
abs_x = symbol_x + pin_rel_x
abs_y = symbol_y + pin_rel_y
logger.info(
f"Pin {symbol_reference}/{pin_number} located at ({abs_x}, {abs_y})"
)
logger.info(f"Pin {symbol_reference}/{pin_number} located at ({abs_x}, {abs_y})")
return [abs_x, abs_y]
except Exception as e:
@@ -385,9 +370,7 @@ class PinLocator:
return {}
# Get lib_id
lib_id = (
target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
)
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
if not lib_id:
logger.error(f"Symbol {symbol_reference} has no lib_id")
return {}
@@ -400,9 +383,7 @@ class PinLocator:
# Calculate location for each pin
result = {}
for pin_num in pins.keys():
location = self.get_pin_location(
schematic_path, symbol_reference, pin_num
)
location = self.get_pin_location(schematic_path, symbol_reference, pin_num)
if location:
result[pin_num] = location
@@ -416,14 +397,14 @@ class PinLocator:
if __name__ == "__main__":
# Test pin location discovery
import shutil
import sys
sys.path.insert(0, "/home/chris/MCP/KiCAD-MCP-Server/python")
from pathlib import Path
from commands.component_schematic import ComponentManager
from commands.schematic import SchematicManager
import shutil
sys.path.insert(0, str(Path(__file__).parent.parent))
print("=" * 80)
print("PIN LOCATOR TEST")
@@ -431,8 +412,8 @@ if __name__ == "__main__":
# Create test schematic with components (cross-platform temp directory)
test_path = Path(tempfile.gettempdir()) / "test_pin_locator.kicad_sch"
template_path = Path(
"/home/chris/MCP/KiCAD-MCP-Server/python/templates/template_with_symbols_expanded.kicad_sch"
template_path = (
Path(__file__).parent.parent / "templates" / "template_with_symbols_expanded.kicad_sch"
)
shutil.copy(template_path, test_path)

View File

@@ -2,11 +2,12 @@
Project-related command implementations for KiCAD interface
"""
import os
import pcbnew # type: ignore
import logging
import os
import shutil
from typing import Dict, Any, Optional
from typing import Any, Dict, Optional
import pcbnew # type: ignore
logger = logging.getLogger("kicad_interface")
@@ -22,9 +23,7 @@ class ProjectCommands:
"""Create a new KiCAD project"""
try:
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
project_name = params.get("name") or params.get(
"projectName", "New_Project"
)
project_name = params.get("name") or params.get("projectName", "New_Project")
path = params.get("path", os.getcwd())
template = params.get("template")
@@ -101,9 +100,7 @@ class ProjectCommands:
schematic_uuid = str(uuid_module.uuid4())
with open(schematic_path, "w", encoding="utf-8", newline="\n") as f:
f.write(
'(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n'
)
f.write('(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n')
f.write(f" (uuid {schematic_uuid})\n\n")
f.write(' (paper "A4")\n\n')
f.write(" (lib_symbols\n )\n\n")
@@ -207,9 +204,7 @@ class ProjectCommands:
"success": True,
"message": f"Saved project to: {self.board.GetFileName()}",
"project": {
"name": os.path.splitext(
os.path.basename(self.board.GetFileName())
)[0],
"name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
"path": self.board.GetFileName(),
},
}

View File

@@ -2,11 +2,12 @@
Routing-related command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
import math
from typing import Dict, Any, Optional, List, Tuple
import os
from typing import Any, Dict, List, Optional, Tuple
import pcbnew
logger = logging.getLogger("kicad_interface")
@@ -114,7 +115,7 @@ class RoutingCommands:
"errorDetails": f"'{ref}' does not exist on the board",
}
def find_pad(ref: str, pad_num: str):
def find_pad(ref: str, pad_num: str) -> Any:
fp = footprints[ref]
for pad in fp.Pads():
if pad.GetNumber() == pad_num:
@@ -149,9 +150,9 @@ class RoutingCommands:
# KiCAD 9 SWIG. Use footprint.GetLayer() instead — it always reflects
# the actual placed layer after Flip().
fp_start = footprints[from_ref]
fp_end = footprints[to_ref]
fp_end = footprints[to_ref]
start_layer = self.board.GetLayerName(fp_start.GetLayer())
end_layer = self.board.GetLayerName(fp_end.GetLayer())
end_layer = self.board.GetLayerName(fp_end.GetLayer())
copper_layers = {"F.Cu", "B.Cu"}
needs_via = (
start_layer in copper_layers
@@ -168,24 +169,34 @@ class RoutingCommands:
via_y = (start_pos.y + end_pos.y) / 2 / scale
# Trace on start layer: start_pad → via
r1 = self.route_trace({
"start": {"x": start_pos.x / scale, "y": start_pos.y / scale, "unit": "mm"},
"end": {"x": via_x, "y": via_y, "unit": "mm"},
"layer": start_layer, "width": width, "net": net,
})
r1 = self.route_trace(
{
"start": {"x": start_pos.x / scale, "y": start_pos.y / scale, "unit": "mm"},
"end": {"x": via_x, "y": via_y, "unit": "mm"},
"layer": start_layer,
"width": width,
"net": net,
}
)
# Via connecting both layers
self.add_via({
"position": {"x": via_x, "y": via_y, "unit": "mm"},
"net": net,
"from_layer": start_layer,
"to_layer": end_layer,
})
self.add_via(
{
"position": {"x": via_x, "y": via_y, "unit": "mm"},
"net": net,
"from_layer": start_layer,
"to_layer": end_layer,
}
)
# Trace on end layer: via → end_pad
r2 = self.route_trace({
"start": {"x": via_x, "y": via_y, "unit": "mm"},
"end": {"x": end_pos.x / scale, "y": end_pos.y / scale, "unit": "mm"},
"layer": end_layer, "width": width, "net": net,
})
r2 = self.route_trace(
{
"start": {"x": via_x, "y": via_y, "unit": "mm"},
"end": {"x": end_pos.x / scale, "y": end_pos.y / scale, "unit": "mm"},
"layer": end_layer,
"width": width,
"net": net,
}
)
success = r1.get("success") and r2.get("success")
result = {
"success": success,
@@ -195,21 +206,28 @@ class RoutingCommands:
}
else:
# Same layer — direct trace
result = self.route_trace({
"start": {"x": start_pos.x / scale, "y": start_pos.y / scale, "unit": "mm"},
"end": {"x": end_pos.x / scale, "y": end_pos.y / scale, "unit": "mm"},
"layer": layer if layer else start_layer,
"width": width, "net": net,
})
result = self.route_trace(
{
"start": {"x": start_pos.x / scale, "y": start_pos.y / scale, "unit": "mm"},
"end": {"x": end_pos.x / scale, "y": end_pos.y / scale, "unit": "mm"},
"layer": layer if layer else start_layer,
"width": width,
"net": net,
}
)
if result.get("success"):
result["fromPad"] = {
"ref": from_ref, "pad": from_pad,
"x": start_pos.x / scale, "y": start_pos.y / scale,
"ref": from_ref,
"pad": from_pad,
"x": start_pos.x / scale,
"y": start_pos.y / scale,
}
result["toPad"] = {
"ref": to_ref, "pad": to_pad,
"x": end_pos.x / scale, "y": end_pos.y / scale,
"ref": to_ref,
"pad": to_pad,
"x": end_pos.x / scale,
"y": end_pos.y / scale,
}
return result
@@ -352,21 +370,15 @@ class RoutingCommands:
via = pcbnew.PCB_VIA(self.board)
# Set position
scale = (
1000000 if position["unit"] == "mm" else 25400000
) # mm or inch to nm
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
# Set size and drill (default to board's current via settings)
design_settings = self.board.GetDesignSettings()
via.SetWidth(
int(size * 1000000) if size else design_settings.GetCurrentViaSize()
)
via.SetDrill(
int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill()
)
via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
# Set layers
from_id = self.board.GetLayerID(from_layer)
@@ -500,9 +512,7 @@ class RoutingCommands:
# Find track by position
if position:
scale = (
1000000 if position["unit"] == "mm" else 25400000
) # mm or inch to nm
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
x_nm = int(position["x"] * scale)
y_nm = int(position["y"] * scale)
point = pcbnew.VECTOR2I(x_nm, y_nm)
@@ -940,9 +950,7 @@ class RoutingCommands:
else:
traces_to_copy.append(track)
filter_method = (
"net-based" if use_net_filter else "geometric (pads have no nets)"
)
filter_method = "net-based" if use_net_filter else "geometric (pads have no nets)"
logger.info(
f"copy_routing_pattern: {len(traces_to_copy)} traces, "
f"{len(vias_to_copy)} vias selected via {filter_method}"
@@ -958,9 +966,7 @@ class RoutingCommands:
# Create new track
new_track = pcbnew.PCB_TRACK(self.board)
new_track.SetStart(
pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y)
)
new_track.SetStart(pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y))
new_track.SetEnd(pcbnew.VECTOR2I(end.x + offset_x, end.y + offset_y))
new_track.SetLayer(track.GetLayer())
@@ -1320,15 +1326,11 @@ class RoutingCommands:
pos_start = pcbnew.VECTOR2I(
int(start_point.x + offset_x), int(start_point.y + offset_y)
)
pos_end = pcbnew.VECTOR2I(
int(end_point.x + offset_x), int(end_point.y + offset_y)
)
pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
neg_start = pcbnew.VECTOR2I(
int(start_point.x - offset_x), int(start_point.y - offset_y)
)
neg_end = pcbnew.VECTOR2I(
int(end_point.x - offset_x), int(end_point.y - offset_y)
)
neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
# Create positive trace
pos_track = pcbnew.PCB_TRACK(self.board)
@@ -1395,9 +1397,7 @@ class RoutingCommands:
return pad.GetPosition()
raise ValueError("Invalid point specification")
def _point_to_track_distance(
self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK
) -> float:
def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
"""Calculate distance from point to track segment"""
start = track.GetStart()
end = track.GetEnd()

View File

@@ -1,8 +1,10 @@
from skip import Schematic
import logging
import os
import shutil
import logging
import uuid
from typing import Any, Optional
from skip import Schematic
logger = logging.getLogger("kicad_interface")
@@ -11,7 +13,7 @@ class SchematicManager:
"""Core schematic operations using kicad-skip"""
@staticmethod
def create_schematic(name, metadata=None):
def create_schematic(name: str, metadata: Optional[Any] = None) -> Any:
"""Create a new empty schematic from template"""
try:
# Determine template path (use template_with_symbols for component cloning support)
@@ -31,31 +33,28 @@ class SchematicManager:
# Regenerate UUID to ensure uniqueness for each created schematic
import re
with open(output_path, 'r', encoding='utf-8') as f:
with open(output_path, "r", encoding="utf-8") as f:
content = f.read()
new_uuid = str(uuid.uuid4())
content = re.sub(
r'\(uuid [0-9a-fA-F-]+\)',
f'(uuid {new_uuid})',
r"\(uuid [0-9a-fA-F-]+\)",
f"(uuid {new_uuid})",
content,
count=1 # Only replace first (schematic) UUID
count=1, # Only replace first (schematic) UUID
)
with open(output_path, 'w', encoding='utf-8', newline='\n') as f:
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
f.write(content)
logger.info(f"Created schematic from template: {output_path}")
else:
# Fallback: create minimal schematic
logger.warning(
f"Template not found at {template_path}, creating minimal schematic"
)
logger.warning(f"Template not found at {template_path}, creating minimal schematic")
# Generate unique UUID for this schematic
schematic_uuid = str(uuid.uuid4())
# Write with explicit UTF-8 encoding and Unix line endings for cross-platform compatibility
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
f.write(
'(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n'
)
f.write('(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n')
f.write(f" (uuid {schematic_uuid})\n\n")
f.write(' (paper "A4")\n\n')
f.write(" (lib_symbols\n )\n\n")
@@ -72,7 +71,7 @@ class SchematicManager:
raise
@staticmethod
def load_schematic(file_path):
def load_schematic(file_path: str) -> Optional[Any]:
"""Load an existing schematic"""
if not os.path.exists(file_path):
logger.error(f"Schematic file not found at {file_path}")
@@ -86,7 +85,7 @@ class SchematicManager:
return None
@staticmethod
def save_schematic(schematic, file_path):
def save_schematic(schematic: Any, file_path: str) -> bool:
"""Save a schematic to file"""
try:
# kicad-skip uses write method, not save
@@ -98,7 +97,7 @@ class SchematicManager:
return False
@staticmethod
def get_schematic_metadata(schematic):
def get_schematic_metadata(schematic: Any) -> dict[str, Any]:
"""Extract metadata from schematic"""
# kicad-skip doesn't expose a direct metadata object on Schematic.
# We can return basic info like version and generator.

View File

@@ -0,0 +1,976 @@
"""
Schematic Analysis Tools for KiCad Schematics
Read-only analysis tools for detecting spatial problems, querying regions,
and checking connectivity in KiCad schematic files.
"""
import logging
import math
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
import sexpdata
from commands.pin_locator import PinLocator
from commands.wire_connectivity import _parse_virtual_connections, _to_iu
from sexpdata import Symbol
from skip import Schematic
logger = logging.getLogger("kicad_interface")
# ---------------------------------------------------------------------------
# S-expression parsing helpers
# ---------------------------------------------------------------------------
def _load_sexp(schematic_path: Path) -> list:
"""Load schematic file and return parsed S-expression data."""
with open(schematic_path, "r", encoding="utf-8") as f:
return sexpdata.loads(f.read())
def _parse_wires(sexp_data: list) -> List[Dict[str, Any]]:
"""
Parse all wire segments from the schematic S-expression.
Returns list of dicts: {start: (x_mm, y_mm), end: (x_mm, y_mm)}
"""
wires = []
for item in sexp_data:
if not isinstance(item, list) or len(item) < 2:
continue
if item[0] != Symbol("wire"):
continue
pts = None
for sub in item:
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
pts = sub
break
if not pts:
continue
coords = []
for sub in pts:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == Symbol("xy"):
coords.append((float(sub[1]), float(sub[2])))
if len(coords) >= 2:
wires.append({"start": coords[0], "end": coords[1]})
return wires
def _parse_labels(sexp_data: list) -> List[Dict[str, Any]]:
"""
Parse all labels (label and global_label) from the schematic S-expression.
Returns list of dicts: {name, type ('label'|'global_label'), x, y}
"""
labels = []
for item in sexp_data:
if not isinstance(item, list) or len(item) < 2:
continue
tag = item[0]
if tag not in (Symbol("label"), Symbol("global_label")):
continue
name = str(item[1]).strip('"')
label_type = str(tag)
x, y = 0.0, 0.0
for sub in item:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == Symbol("at"):
x = float(sub[1])
y = float(sub[2])
break
labels.append({"name": name, "type": label_type, "x": x, "y": y})
return labels
def _parse_symbols(sexp_data: list) -> List[Dict[str, Any]]:
"""
Parse all placed symbol instances from the schematic S-expression.
Returns list of dicts: {reference, lib_id, x, y, rotation, mirror_x, mirror_y, is_power}
"""
symbols = []
for item in sexp_data:
if not isinstance(item, list) or len(item) < 2:
continue
if item[0] != Symbol("symbol"):
continue
lib_id = ""
x, y, rotation = 0.0, 0.0, 0.0
reference = ""
is_power = False
mirror_x = False
mirror_y = False
for sub in item:
if isinstance(sub, list) and len(sub) >= 2:
if sub[0] == Symbol("lib_id"):
lib_id = str(sub[1]).strip('"')
elif sub[0] == Symbol("at") and len(sub) >= 3:
x = float(sub[1])
y = float(sub[2])
if len(sub) >= 4:
rotation = float(sub[3])
elif sub[0] == Symbol("mirror"):
m = str(sub[1])
if m == "x":
mirror_x = True
elif m == "y":
mirror_y = True
elif sub[0] == Symbol("property") and len(sub) >= 3:
prop_name = str(sub[1]).strip('"')
if prop_name == "Reference":
reference = str(sub[2]).strip('"')
is_power = reference.startswith("#PWR") or reference.startswith("#FLG")
symbols.append(
{
"reference": reference,
"lib_id": lib_id,
"x": x,
"y": y,
"rotation": rotation,
"mirror_x": mirror_x,
"mirror_y": mirror_y,
"is_power": is_power,
}
)
return symbols
def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
"""
Parse graphical body elements from a lib_symbol definition and return
local-coordinate bounding points.
Extracts points from rectangle, polyline, circle, arc, and bezier
elements found in sub-symbols (typically the ``_0_1`` layers that
contain body shapes).
Returns a list of ``(x, y)`` points in local symbol coordinates.
"""
points: List[Tuple[float, float]] = []
def _extract_graphics_recursive(sexp: list) -> None:
if not isinstance(sexp, list) or len(sexp) == 0:
return
tag = sexp[0]
if tag == Symbol("rectangle"):
# (rectangle (start x y) (end x y) ...)
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) >= 3:
if sub[0] in (Symbol("start"), Symbol("end")):
points.append((float(sub[1]), float(sub[2])))
elif tag == Symbol("polyline"):
# (polyline (pts (xy x y) (xy x y) ...) ...)
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
for pt in sub[1:]:
if isinstance(pt, list) and len(pt) >= 3 and pt[0] == Symbol("xy"):
points.append((float(pt[1]), float(pt[2])))
elif tag == Symbol("circle"):
# (circle (center x y) (radius r) ...)
cx, cy, r = 0.0, 0.0, 0.0
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == Symbol("center"):
cx, cy = float(sub[1]), float(sub[2])
elif isinstance(sub, list) and len(sub) >= 2 and sub[0] == Symbol("radius"):
r = float(sub[1])
if r > 0:
points.extend(
[
(cx - r, cy - r),
(cx + r, cy + r),
]
)
elif tag == Symbol("arc"):
# (arc (start x y) (mid x y) (end x y) ...)
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) >= 3:
if sub[0] in (Symbol("start"), Symbol("mid"), Symbol("end")):
points.append((float(sub[1]), float(sub[2])))
elif tag == Symbol("bezier"):
# (bezier (pts (xy x y) ...) ...)
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
for pt in sub[1:]:
if isinstance(pt, list) and len(pt) >= 3 and pt[0] == Symbol("xy"):
points.append((float(pt[1]), float(pt[2])))
else:
# Recurse into sub-symbols to find graphics in nested definitions
for sub in sexp[1:]:
if isinstance(sub, list):
_extract_graphics_recursive(sub)
# Search the top-level symbol definition and its sub-symbols
for item in symbol_def[1:]:
if isinstance(item, list):
_extract_graphics_recursive(item)
return points
def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
"""
Walk the lib_symbols section of already-parsed sexp_data and return
pin definitions and graphics points for every symbol definition.
Returns:
Dict mapping lib_id → {"pins": pin_defs, "graphics_points": [(x,y), ...]}.
"""
lib_symbols_section = None
for item in sexp_data:
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol("lib_symbols"):
lib_symbols_section = item
break
if not lib_symbols_section:
return {}
result: Dict[str, Dict] = {}
for item in lib_symbols_section[1:]:
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol("symbol"):
symbol_name = str(item[1]).strip('"')
result[symbol_name] = {
"pins": PinLocator.parse_symbol_definition(item),
"graphics_points": _parse_lib_symbol_graphics(item),
}
return result
# ---------------------------------------------------------------------------
# Geometry helpers
# ---------------------------------------------------------------------------
def compute_symbol_bbox(
schematic_path: Path,
reference: str,
locator: PinLocator,
) -> Optional[Tuple[float, float, float, float]]:
"""
Compute bounding box of a symbol from its pin positions.
Returns (min_x, min_y, max_x, max_y) in mm, or None if no pins found.
"""
pins = locator.get_all_symbol_pins(schematic_path, reference)
if not pins:
return None
xs = [p[0] for p in pins.values()]
ys = [p[1] for p in pins.values()]
return (min(xs), min(ys), max(xs), max(ys))
def _line_segment_intersects_aabb(
x1: float,
y1: float,
x2: float,
y2: float,
box_min_x: float,
box_min_y: float,
box_max_x: float,
box_max_y: float,
) -> bool:
"""
Test whether line segment (x1,y1)→(x2,y2) intersects an axis-aligned bounding box.
Uses the Liang-Barsky clipping algorithm.
"""
dx = x2 - x1
dy = y2 - y1
p = [-dx, dx, -dy, dy]
q = [x1 - box_min_x, box_max_x - x1, y1 - box_min_y, box_max_y - y1]
t_min = 0.0
t_max = 1.0
for i in range(4):
if abs(p[i]) < 1e-12:
# Parallel to this edge
if q[i] < 0:
return False
else:
t = q[i] / p[i]
if p[i] < 0:
t_min = max(t_min, t)
else:
t_max = min(t_max, t)
if t_min > t_max:
return False
return True
def _point_in_rect(
px: float,
py: float,
min_x: float,
min_y: float,
max_x: float,
max_y: float,
) -> bool:
"""Check if a point is within a rectangle."""
return min_x <= px <= max_x and min_y <= py <= max_y
def _distance(p1: Tuple[float, float], p2: Tuple[float, float]) -> float:
"""Euclidean distance between two points."""
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
def _aabb_overlap(
a: Tuple[float, float, float, float],
b: Tuple[float, float, float, float],
) -> bool:
"""Check if two axis-aligned bounding boxes overlap.
Each bbox is (min_x, min_y, max_x, max_y).
"""
return a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]
def _transform_local_point(
lx: float,
ly: float,
sym_x: float,
sym_y: float,
rotation: float,
mirror_x: bool,
mirror_y: bool,
) -> Tuple[float, float]:
"""
Transform a point from local symbol coordinates to absolute schematic
coordinates using KiCad's transform order:
negate-y (lib y-up → schematic y-down) → mirror → rotate → translate.
"""
# Library symbols use y-up; schematic uses y-down
ly = -ly
# Apply mirroring in local coords
if mirror_x:
ly = -ly
if mirror_y:
lx = -lx
# Apply rotation
if rotation != 0:
lx, ly = PinLocator.rotate_point(lx, ly, rotation)
return (sym_x + lx, sym_y + ly)
def _compute_symbol_bbox_direct(
sym: Dict[str, Any],
pin_defs: Dict[str, Dict],
margin: float = 0.0,
graphics_points: Optional[List[Tuple[float, float]]] = None,
) -> Optional[Tuple[float, float, float, float]]:
"""
Compute bounding box of a symbol from its graphics and pin definitions.
When graphics_points are available (from lib_symbol body shapes), uses
those for the bbox and unions with pin positions. Falls back to
pin-only estimation with degenerate expansion when no graphics data
is available.
Args:
sym: Parsed symbol dict with x, y, rotation, mirror_x, mirror_y.
pin_defs: Pin definitions from PinLocator.get_symbol_pins().
margin: Shrink bbox by this amount on each side (mm).
graphics_points: Local-coordinate points from symbol body graphics.
Returns (min_x, min_y, max_x, max_y) in mm, or None if no pins.
"""
pin_positions = _compute_pin_positions_direct(sym, pin_defs)
if not pin_positions:
return None
if graphics_points:
# Transform graphics points to absolute coordinates
sym_x, sym_y = sym["x"], sym["y"]
rotation = sym["rotation"]
mirror_x = sym.get("mirror_x", False)
mirror_y = sym.get("mirror_y", False)
abs_points = [
_transform_local_point(lx, ly, sym_x, sym_y, rotation, mirror_x, mirror_y)
for lx, ly in graphics_points
]
# Union with pin positions so pins extending beyond body are included
all_xs = [p[0] for p in abs_points] + [p[0] for p in pin_positions.values()]
all_ys = [p[1] for p in abs_points] + [p[1] for p in pin_positions.values()]
min_x, min_y = min(all_xs), min(all_ys)
max_x, max_y = max(all_xs), max(all_ys)
else:
# Fallback: pin-only estimation with degenerate expansion
xs = [p[0] for p in pin_positions.values()]
ys = [p[1] for p in pin_positions.values()]
min_x, min_y, max_x, max_y = min(xs), min(ys), max(xs), max(ys)
min_body = 1.5 # mm minimum half-extent for component body
if max_x - min_x < 2 * min_body:
cx = (min_x + max_x) / 2
min_x = cx - min_body
max_x = cx + min_body
if max_y - min_y < 2 * min_body:
cy = (min_y + max_y) / 2
min_y = cy - min_body
max_y = cy + min_body
# Shrink bbox by margin
min_x += margin
min_y += margin
max_x -= margin
max_y -= margin
# Skip degenerate bboxes
if max_x <= min_x or max_y <= min_y:
return None
return (min_x, min_y, max_x, max_y)
# ---------------------------------------------------------------------------
# Tool 3: find_overlapping_elements
# ---------------------------------------------------------------------------
def find_overlapping_elements(schematic_path: Path, tolerance: float = 0.5) -> Dict[str, Any]:
"""
Detect spatially overlapping symbols, wires, and labels.
Args:
schematic_path: Path to .kicad_sch file
tolerance: Distance threshold in mm for label proximity and wire collinearity checks. Symbol overlap uses bounding-box intersection.
Returns dict: {overlappingSymbols, overlappingLabels, overlappingWires, totalOverlaps}
"""
sexp_data = _load_sexp(schematic_path)
symbols = _parse_symbols(sexp_data)
wires = _parse_wires(sexp_data)
labels = _parse_labels(sexp_data)
overlapping_symbols = []
overlapping_labels = []
overlapping_wires = []
lib_defs = _extract_lib_symbols(sexp_data)
# --- Symbol-symbol overlap using bounding-box intersection (O(n²)) ---
non_template_symbols = [
s for s in symbols if not s["reference"].startswith("_TEMPLATE") and s["reference"]
]
# Pre-compute bounding boxes for all non-template symbols
symbol_bboxes = []
for sym in non_template_symbols:
lib_data = lib_defs.get(sym["lib_id"], {})
pin_defs = lib_data.get("pins", {})
graphics_points = lib_data.get("graphics_points", [])
bbox = None
if pin_defs:
bbox = _compute_symbol_bbox_direct(sym, pin_defs, graphics_points=graphics_points)
symbol_bboxes.append((sym, bbox))
for i in range(len(symbol_bboxes)):
s1, bbox1 = symbol_bboxes[i]
for j in range(i + 1, len(symbol_bboxes)):
s2, bbox2 = symbol_bboxes[j]
dist = _distance((s1["x"], s1["y"]), (s2["x"], s2["y"]))
overlap_detected = False
if bbox1 is not None and bbox2 is not None:
# Use bounding box intersection
overlap_detected = _aabb_overlap(bbox1, bbox2)
else:
# Fallback to center distance when pin data is unavailable
overlap_detected = dist < tolerance
if overlap_detected:
entry = {
"element1": {
"reference": s1["reference"],
"libId": s1["lib_id"],
"position": {"x": s1["x"], "y": s1["y"]},
},
"element2": {
"reference": s2["reference"],
"libId": s2["lib_id"],
"position": {"x": s2["x"], "y": s2["y"]},
},
"distance": round(dist, 4),
}
# Flag power symbol pairs specifically
if s1["is_power"] and s2["is_power"]:
entry["type"] = "power_symbol_overlap"
else:
entry["type"] = "symbol_overlap"
overlapping_symbols.append(entry)
# --- Label-label overlap ---
for i in range(len(labels)):
for j in range(i + 1, len(labels)):
l1 = labels[i]
l2 = labels[j]
dist = _distance((l1["x"], l1["y"]), (l2["x"], l2["y"]))
if dist < tolerance:
overlapping_labels.append(
{
"element1": {
"name": l1["name"],
"type": l1["type"],
"position": {"x": l1["x"], "y": l1["y"]},
},
"element2": {
"name": l2["name"],
"type": l2["type"],
"position": {"x": l2["x"], "y": l2["y"]},
},
"distance": round(dist, 4),
}
)
# --- Wire-wire collinear overlap ---
for i in range(len(wires)):
for j in range(i + 1, len(wires)):
w1 = wires[i]
w2 = wires[j]
overlap = _check_wire_overlap(w1, w2, tolerance)
if overlap:
overlapping_wires.append(overlap)
total = len(overlapping_symbols) + len(overlapping_labels) + len(overlapping_wires)
return {
"overlappingSymbols": overlapping_symbols,
"overlappingLabels": overlapping_labels,
"overlappingWires": overlapping_wires,
"totalOverlaps": total,
}
def _check_wire_overlap(
w1: Dict[str, Any], w2: Dict[str, Any], tolerance: float
) -> Optional[Dict[str, Any]]:
"""
Check if two wire segments are collinear and overlapping.
Works for horizontal, vertical, and diagonal wires. Uses direction
vectors, cross-product parallelism, point-to-line distance for
collinearity, and 1D projection overlap.
Returns overlap info dict or None.
"""
s1, e1 = w1["start"], w1["end"]
s2, e2 = w2["start"], w2["end"]
d1 = (e1[0] - s1[0], e1[1] - s1[1])
d2 = (e2[0] - s2[0], e2[1] - s2[1])
len1 = math.sqrt(d1[0] ** 2 + d1[1] ** 2)
len2 = math.sqrt(d2[0] ** 2 + d2[1] ** 2)
if len1 < 1e-12 or len2 < 1e-12:
return None # degenerate zero-length segment
# Cross product to check parallel
cross = d1[0] * d2[1] - d1[1] * d2[0]
if abs(cross) > tolerance * max(len1, len2):
return None # not parallel
# Point-to-line distance: s2 relative to line through s1 along d1
ds = (s2[0] - s1[0], s2[1] - s1[1])
perp_dist = abs(ds[0] * d1[1] - ds[1] * d1[0]) / len1
if perp_dist > tolerance:
return None # parallel but offset
# Project onto d1 direction for 1D overlap check
u1 = (d1[0] / len1, d1[1] / len1)
proj_s1 = s1[0] * u1[0] + s1[1] * u1[1]
proj_e1 = e1[0] * u1[0] + e1[1] * u1[1]
proj_s2 = s2[0] * u1[0] + s2[1] * u1[1]
proj_e2 = e2[0] * u1[0] + e2[1] * u1[1]
min1, max1 = min(proj_s1, proj_e1), max(proj_s1, proj_e1)
min2, max2 = min(proj_s2, proj_e2), max(proj_s2, proj_e2)
if min1 < max2 and min2 < max1:
return {
"wire1": {
"start": {"x": s1[0], "y": s1[1]},
"end": {"x": e1[0], "y": e1[1]},
},
"wire2": {
"start": {"x": s2[0], "y": s2[1]},
"end": {"x": e2[0], "y": e2[1]},
},
"type": "collinear_overlap",
}
return None
# ---------------------------------------------------------------------------
# Tool 4: get_elements_in_region
# ---------------------------------------------------------------------------
def get_elements_in_region(
schematic_path: Path,
x1: float,
y1: float,
x2: float,
y2: float,
) -> Dict[str, Any]:
"""
List all wires, labels, and symbols within a rectangular region.
Args:
schematic_path: Path to .kicad_sch file
x1, y1, x2, y2: Bounding box corners in schematic mm
Returns dict: {symbols, wires, labels, counts}
"""
min_x, max_x = min(x1, x2), max(x1, x2)
min_y, max_y = min(y1, y2), max(y1, y2)
sexp_data = _load_sexp(schematic_path)
symbols = _parse_symbols(sexp_data)
wires = _parse_wires(sexp_data)
labels = _parse_labels(sexp_data)
lib_defs = _extract_lib_symbols(sexp_data)
# Symbols: include if position is within bounds
region_symbols = []
for sym in symbols:
if not sym["reference"] or sym["reference"].startswith("_TEMPLATE"):
continue
if _point_in_rect(sym["x"], sym["y"], min_x, min_y, max_x, max_y):
entry = {
"reference": sym["reference"],
"libId": sym["lib_id"],
"position": {"x": sym["x"], "y": sym["y"]},
"isPower": sym["is_power"],
}
# Include pin positions (compute directly to handle unannotated duplicates)
lib_data = lib_defs.get(sym["lib_id"], {})
pin_defs = lib_data.get("pins", {})
if pin_defs:
pin_positions = _compute_pin_positions_direct(sym, pin_defs)
if pin_positions:
entry["pins"] = {
pn: {"x": round(pos[0], 4), "y": round(pos[1], 4)}
for pn, pos in pin_positions.items()
}
region_symbols.append(entry)
# Wires: include if any part of the wire intersects the region
region_wires = []
for w in wires:
s, e = w["start"], w["end"]
if (
_point_in_rect(s[0], s[1], min_x, min_y, max_x, max_y)
or _point_in_rect(e[0], e[1], min_x, min_y, max_x, max_y)
or _line_segment_intersects_aabb(s[0], s[1], e[0], e[1], min_x, min_y, max_x, max_y)
):
region_wires.append(
{
"start": {"x": s[0], "y": s[1]},
"end": {"x": e[0], "y": e[1]},
}
)
# Labels: include if position is within bounds
region_labels = []
for lbl in labels:
if _point_in_rect(lbl["x"], lbl["y"], min_x, min_y, max_x, max_y):
region_labels.append(
{
"name": lbl["name"],
"type": lbl["type"],
"position": {"x": lbl["x"], "y": lbl["y"]},
}
)
return {
"symbols": region_symbols,
"wires": region_wires,
"labels": region_labels,
"counts": {
"symbols": len(region_symbols),
"wires": len(region_wires),
"labels": len(region_labels),
},
}
# ---------------------------------------------------------------------------
# Tool 5: check_wire_collisions
# ---------------------------------------------------------------------------
def _compute_pin_positions_direct(
sym: Dict[str, Any], pin_defs: Dict[str, Dict]
) -> Dict[str, List[float]]:
"""
Compute absolute schematic pin positions for a symbol instance directly from
its parsed position/rotation/mirror data and pin definitions in local coords.
Unlike PinLocator.get_all_symbol_pins, this does NOT do a reference-name
lookup in the schematic, so it works correctly when multiple symbols share
the same reference designator (e.g. unannotated "Q?").
KiCad transform order: mirror (in local coords) → rotate → translate.
"""
sym_x = sym["x"]
sym_y = sym["y"]
rotation = sym["rotation"]
mirror_x = sym.get("mirror_x", False)
mirror_y = sym.get("mirror_y", False)
result: Dict[str, List[float]] = {}
for pin_num, pin_data in pin_defs.items():
rel_x = float(pin_data["x"])
rel_y = float(pin_data["y"])
# Apply mirroring in local symbol coordinates
if mirror_x:
rel_y = -rel_y
if mirror_y:
rel_x = -rel_x
# Apply symbol rotation
if rotation != 0:
rel_x, rel_y = PinLocator.rotate_point(rel_x, rel_y, rotation)
result[pin_num] = [sym_x + rel_x, sym_y + rel_y]
return result
def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
"""
Find all wires that cross over component symbol bodies.
Wires passing over symbols are unacceptable in schematics — they indicate
routing mistakes where a wire was drawn across a component instead of
around it.
For each non-power, non-template symbol:
1. Compute bounding box from pin positions (shrunk by margin).
2. For each wire segment, test intersection with the bbox.
3. If intersects and the wire is not simply terminating at a pin from
outside, report it as a crossing.
Returns list of crossing dicts.
"""
sexp_data = _load_sexp(schematic_path)
symbols = _parse_symbols(sexp_data)
wires = _parse_wires(sexp_data)
lib_defs = _extract_lib_symbols(sexp_data)
margin = 0.5 # mm margin to shrink bbox (avoids false positives at pin tips)
pin_tolerance = 0.05 # mm
collisions = []
# Pre-compute per-symbol data
symbol_data: List[Dict[str, Any]] = []
for sym in symbols:
ref = sym["reference"]
if sym["is_power"] or ref.startswith("_TEMPLATE") or not ref:
continue
lib_data = lib_defs.get(sym["lib_id"], {})
pin_defs = lib_data.get("pins", {})
if not pin_defs:
continue
graphics_points = lib_data.get("graphics_points", [])
bbox = _compute_symbol_bbox_direct(
sym, pin_defs, margin=margin, graphics_points=graphics_points
)
if bbox is None:
continue
pin_positions = _compute_pin_positions_direct(sym, pin_defs)
pin_set = set()
for pos in pin_positions.values():
pin_set.add((pos[0], pos[1]))
symbol_data.append(
{
"sym": sym,
"bbox": bbox,
"pin_set": pin_set,
}
)
# Test each wire against each symbol bbox
for w in wires:
sx, sy = w["start"]
ex, ey = w["end"]
for sd in symbol_data:
bx1, by1, bx2, by2 = sd["bbox"]
if not _line_segment_intersects_aabb(sx, sy, ex, ey, bx1, by1, bx2, by2):
continue
# Check which endpoints land on a pin of this symbol
start_at_pin = any(
abs(sx - px) < pin_tolerance and abs(sy - py) < pin_tolerance
for px, py in sd["pin_set"]
)
end_at_pin = any(
abs(ex - px) < pin_tolerance and abs(ey - py) < pin_tolerance
for px, py in sd["pin_set"]
)
# When exactly one endpoint is at a pin, check whether the wire
# just terminates at the pin (valid connection) or continues through
# the component body (pass-through → collision).
# Nudge the pin endpoint slightly toward the other end; if the
# shortened segment still intersects the bbox, the wire extends
# into/through the body.
if (start_at_pin or end_at_pin) and not (start_at_pin and end_at_pin):
dx, dy = ex - sx, ey - sy
length = math.sqrt(dx * dx + dy * dy)
if length > 0:
nudge = min(0.2, length * 0.5)
ux, uy = dx / length, dy / length
if start_at_pin:
nsx, nsy = sx + ux * nudge, sy + uy * nudge
if not _line_segment_intersects_aabb(nsx, nsy, ex, ey, bx1, by1, bx2, by2):
continue # Wire terminates at pin from outside
else:
nex, ney = ex - ux * nudge, ey - uy * nudge
if not _line_segment_intersects_aabb(sx, sy, nex, ney, bx1, by1, bx2, by2):
continue # Wire terminates at pin from outside
sym = sd["sym"]
collisions.append(
{
"wire": {
"start": {"x": sx, "y": sy},
"end": {"x": ex, "y": ey},
},
"component": {
"reference": sym["reference"],
"libId": sym["lib_id"],
"position": {"x": sym["x"], "y": sym["y"]},
},
"intersectionType": "passes_through",
}
)
return collisions
def find_orphaned_wires(schematic_path: Path) -> Dict[str, Any]:
"""
Find wire segments with at least one dangling endpoint.
A wire endpoint is dangling when the IU point at that endpoint satisfies
all three conditions simultaneously:
1. No other wire shares that IU endpoint (would imply a junction / T-join)
2. No component pin is at that IU point
3. No net label or power symbol pin is at that IU point
Uses exact KiCad IU matching (10 000 IU/mm) — same strategy as
wire_connectivity.py — to avoid floating-point tolerance issues.
Returns:
{
"orphaned_wires": [
{
"start": {"x": float, "y": float},
"end": {"x": float, "y": float},
"dangling_ends": [{"x": float, "y": float}, ...]
},
...
],
"count": int
}
"""
sexp_data = _load_sexp(schematic_path)
# --- wire endpoints in mm and IU ---
wires_mm = _parse_wires(sexp_data)
wires_iu: List[Tuple[Tuple[int, int], Tuple[int, int]]] = [
(_to_iu(*w["start"]), _to_iu(*w["end"])) for w in wires_mm
]
# Count how many wires touch each IU endpoint
iu_to_count: Dict[Tuple[int, int], int] = defaultdict(int)
for s_iu, e_iu in wires_iu:
iu_to_count[s_iu] += 1
iu_to_count[e_iu] += 1
# --- anchors: component pins ---
pin_iu: Set[Tuple[int, int]] = set()
try:
locator = PinLocator()
sch = Schematic(str(schematic_path))
for symbol in sch.symbol:
try:
if not hasattr(symbol, "property") or not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
all_pins = locator.get_all_symbol_pins(schematic_path, ref)
for coords in all_pins.values():
pin_iu.add(_to_iu(float(coords[0]), float(coords[1])))
except Exception as e:
logger.warning(f"Error reading pins for symbol: {e}")
except Exception as e:
logger.warning(f"Could not load schematic via skip for pin extraction: {e}")
sch = None
# --- anchors: net labels and global_labels ---
labels = _parse_labels(sexp_data)
label_iu: Set[Tuple[int, int]] = {_to_iu(lbl["x"], lbl["y"]) for lbl in labels}
# --- anchors: power symbol pins (VCC, GND …) ---
power_iu: Set[Tuple[int, int]] = set()
if sch is not None:
try:
point_to_label, _ = _parse_virtual_connections(sch, schematic_path)
power_iu = set(point_to_label.keys())
except Exception as e:
logger.warning(f"Could not extract power symbol anchors: {e}")
anchored_iu = pin_iu | label_iu | power_iu
# --- classify each wire ---
orphaned: List[Dict[str, Any]] = []
for i, (s_iu, e_iu) in enumerate(wires_iu):
w = wires_mm[i]
dangling_ends: List[Dict[str, float]] = []
for pt_iu, pt_mm in [(s_iu, w["start"]), (e_iu, w["end"])]:
if iu_to_count[pt_iu] > 1:
continue # shared with another wire → connected
if pt_iu in anchored_iu:
continue # touches a pin or label → connected
dangling_ends.append({"x": pt_mm[0], "y": pt_mm[1]})
if dangling_ends:
orphaned.append(
{
"start": {"x": w["start"][0], "y": w["start"][1]},
"end": {"x": w["end"][0], "y": w["end"][1]},
"dangling_ends": dangling_ends,
}
)
return {"orphaned_wires": orphaned, "count": len(orphaned)}

View File

@@ -0,0 +1,211 @@
"""
Snap-to-grid tool for KiCAD schematics.
Snaps wire endpoints, junction positions, net labels, and optionally component
positions to the nearest grid point. Modifies the schematic file in place.
The standard KiCAD schematic grid is 50 mil (1.27 mm). Component pins are
placed at multiples of 1.27 mm relative to the symbol origin, so absolute pin
coordinates end up as odd multiples of 1.27 mm (e.g. 26.67 mm = 21 × 1.27 mm).
These are valid on-grid positions that must not be moved.
The coarser 2.54 mm (100-mil) grid is a common mistake: exactly half of all
valid 1.27 mm positions are not multiples of 2.54 mm and would be displaced by
1.27 mm — moving labels or wire endpoints off their pins and breaking
connectivity.
Off-grid coordinates cause wires that appear visually connected to fail ERC
connectivity checks because KiCAD uses exact integer (IU) matching internally.
"""
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
import sexpdata
from sexpdata import Symbol
logger = logging.getLogger("kicad_interface")
_DEFAULT_GRID_MM: float = 1.27
# Element type names exposed in the public API
_VALID_ELEMENTS = frozenset({"wires", "junctions", "labels", "components"})
# Tags treated as net labels (all have (at x y angle) structure)
_LABEL_TAGS = frozenset(
{
Symbol("label"),
Symbol("global_label"),
Symbol("hierarchical_label"),
Symbol("net_tie"),
Symbol("no_connect"),
}
)
def _snap_mm(value: float, grid_mm: float) -> float:
"""Snap a single coordinate to the nearest grid multiple."""
return round(value / grid_mm) * grid_mm
def _is_on_grid(value: float, grid_mm: float, eps: float = 1e-9) -> bool:
"""Return True if *value* is already within *eps* of a grid point."""
snapped = _snap_mm(value, grid_mm)
return abs(value - snapped) < eps
def _snap_xy_pair(item: list, grid_mm: float) -> int:
"""
Snap a ``(xy x y)`` S-expression item in place.
Returns 1 if at least one coordinate changed, 0 otherwise.
"""
if not (isinstance(item, list) and len(item) >= 3 and item[0] == Symbol("xy")):
return 0
x_orig, y_orig = float(item[1]), float(item[2])
x_new = _snap_mm(x_orig, grid_mm)
y_new = _snap_mm(y_orig, grid_mm)
changed = not (_is_on_grid(x_orig, grid_mm) and _is_on_grid(y_orig, grid_mm))
item[1] = x_new
item[2] = y_new
return 1 if changed else 0
def _snap_at_xy(item: list, grid_mm: float) -> int:
"""
Snap an ``(at x y ...)`` S-expression item in place (indices 1 and 2 only).
Preserves rotation / angle at index 3+ unchanged.
Returns 1 if at least one coordinate changed, 0 otherwise.
"""
if not (isinstance(item, list) and len(item) >= 3 and item[0] == Symbol("at")):
return 0
x_orig, y_orig = float(item[1]), float(item[2])
x_new = _snap_mm(x_orig, grid_mm)
y_new = _snap_mm(y_orig, grid_mm)
changed = not (_is_on_grid(x_orig, grid_mm) and _is_on_grid(y_orig, grid_mm))
item[1] = x_new
item[2] = y_new
return 1 if changed else 0
def snap_to_grid(
schematic_path: Path,
grid_size: float = _DEFAULT_GRID_MM,
elements: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
Snap element coordinates in a ``.kicad_sch`` file to the nearest grid point.
Modifies the file in place and returns statistics.
Args:
schematic_path: Path to the ``.kicad_sch`` file.
grid_size: Grid spacing in mm (default 1.27 mm = 50 mil).
Do NOT use 2.54 mm — half of all valid KiCAD pin
positions fall between 2.54 mm grid lines and would
be displaced 1.27 mm, breaking connectivity.
elements: List of element types to snap. Valid values:
``"wires"``, ``"junctions"``, ``"labels"``,
``"components"``. Defaults to
``["wires", "junctions", "labels"]`` when ``None``.
Returns:
``{"snapped": int, "already_on_grid": int, "grid_size": float}``
where *snapped* is the number of elements that had at least one
coordinate moved.
"""
if grid_size <= 0:
raise ValueError(f"grid_size must be positive, got {grid_size}")
if elements is None:
active: frozenset = frozenset({"wires", "junctions", "labels"})
else:
unknown = set(elements) - _VALID_ELEMENTS
if unknown:
raise ValueError(
f"Unknown element type(s): {sorted(unknown)}. "
f"Valid types: {sorted(_VALID_ELEMENTS)}"
)
active = frozenset(elements)
with open(schematic_path, "r", encoding="utf-8") as fh:
sch_data = sexpdata.loads(fh.read())
snapped = 0
already_on_grid = 0
snap_wires = "wires" in active
snap_junctions = "junctions" in active
snap_labels = "labels" in active
snap_components = "components" in active
for item in sch_data:
if not isinstance(item, list) or not item:
continue
tag = item[0]
# -----------------------------------------------------------------
# Wires: (wire (pts (xy x y) (xy x y)) ...)
# -----------------------------------------------------------------
if snap_wires and tag == Symbol("wire"):
changed = 0
for sub in item[1:]:
if isinstance(sub, list) and sub and sub[0] == Symbol("pts"):
for pt in sub[1:]:
changed += _snap_xy_pair(pt, grid_size)
if changed:
snapped += 1
else:
already_on_grid += 1
continue
# -----------------------------------------------------------------
# Junctions: (junction (at x y) ...)
# -----------------------------------------------------------------
if snap_junctions and tag == Symbol("junction"):
changed = 0
for sub in item[1:]:
changed += _snap_at_xy(sub, grid_size)
if changed:
snapped += 1
else:
already_on_grid += 1
continue
# -----------------------------------------------------------------
# Labels: (label|global_label|hierarchical_label|no_connect … (at x y angle) …)
# -----------------------------------------------------------------
if snap_labels and tag in _LABEL_TAGS:
changed = 0
for sub in item[1:]:
changed += _snap_at_xy(sub, grid_size)
if changed:
snapped += 1
else:
already_on_grid += 1
continue
# -----------------------------------------------------------------
# Components: (symbol (lib_id …) (at x y rotation) …)
# Snap only the top-level (at …) — not property sub-positions.
# -----------------------------------------------------------------
if snap_components and tag == Symbol("symbol"):
changed = 0
for sub in item[1:]:
if isinstance(sub, list) and sub and sub[0] == Symbol("at"):
changed += _snap_at_xy(sub, grid_size)
break # only the first (at …) belongs to the symbol itself
if changed:
snapped += 1
else:
already_on_grid += 1
continue
with open(schematic_path, "w", encoding="utf-8") as fh:
fh.write(sexpdata.dumps(sch_data))
return {
"snapped": snapped,
"already_on_grid": already_on_grid,
"grid_size": grid_size,
}

View File

@@ -15,13 +15,13 @@ Supported SVG elements:
SVG coordinate system: Y increases downward (same as KiCAD mm), so no Y-flip needed.
"""
import re
import math
import uuid
import os
import logging
from typing import List, Tuple, Dict, Any, Optional
import math
import os
import re
import uuid
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger("kicad_interface")
@@ -34,9 +34,7 @@ Polygon = List[Point]
# ---------------------------------------------------------------------------
# SVG path tokenizer
# ---------------------------------------------------------------------------
_TOKEN_RE = re.compile(
r"([MmZzLlHhVvCcSsQqTtAa])|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)"
)
_TOKEN_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)")
def _tokenize_path(d: str) -> List[str]:
@@ -57,9 +55,9 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
"""
polygons: List[Polygon] = []
current: Polygon = []
cx, cy = 0.0, 0.0 # current point
sx, sy = 0.0, 0.0 # subpath start
last_ctrl = None # last bezier control point (for S/T commands)
cx, cy = 0.0, 0.0 # current point
sx, sy = 0.0, 0.0 # subpath start
last_ctrl = None # last bezier control point (for S/T commands)
last_cmd = ""
i = 0
@@ -73,13 +71,15 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
i += n
return vals
def cubic_bezier_points(p0: Point, p1: Point, p2: Point, p3: Point, steps: int = 16) -> List[Point]:
def cubic_bezier_points(
p0: Point, p1: Point, p2: Point, p3: Point, steps: int = 16
) -> List[Point]:
pts = []
for k in range(1, steps + 1):
t = k / steps
mt = 1 - t
x = mt**3*p0[0] + 3*mt**2*t*p1[0] + 3*mt*t**2*p2[0] + t**3*p3[0]
y = mt**3*p0[1] + 3*mt**2*t*p1[1] + 3*mt*t**2*p2[1] + t**3*p3[1]
x = mt**3 * p0[0] + 3 * mt**2 * t * p1[0] + 3 * mt * t**2 * p2[0] + t**3 * p3[0]
y = mt**3 * p0[1] + 3 * mt**2 * t * p1[1] + 3 * mt * t**2 * p2[1] + t**3 * p3[1]
pts.append((x, y))
return pts
@@ -88,13 +88,23 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
for k in range(1, steps + 1):
t = k / steps
mt = 1 - t
x = mt**2*p0[0] + 2*mt*t*p1[0] + t**2*p2[0]
y = mt**2*p0[1] + 2*mt*t*p1[1] + t**2*p2[1]
x = mt**2 * p0[0] + 2 * mt * t * p1[0] + t**2 * p2[0]
y = mt**2 * p0[1] + 2 * mt * t * p1[1] + t**2 * p2[1]
pts.append((x, y))
return pts
def arc_points(x1: float, y1: float, rx: float, ry: float, phi_deg: float,
large_arc: int, sweep: int, x2: float, y2: float, steps: int = 20) -> List[Point]:
def arc_points(
x1: float,
y1: float,
rx: float,
ry: float,
phi_deg: float,
large_arc: int,
sweep: int,
x2: float,
y2: float,
steps: int = 20,
) -> List[Point]:
"""Approximate SVG arc as polygon points (endpoint parameterization → centre)."""
if rx == 0 or ry == 0:
return [(x2, y2)]
@@ -104,13 +114,13 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
x1p = cos_phi * dx + sin_phi * dy
y1p = -sin_phi * dx + cos_phi * dy
rx, ry = abs(rx), abs(ry)
lam = (x1p / rx)**2 + (y1p / ry)**2
lam = (x1p / rx) ** 2 + (y1p / ry) ** 2
if lam > 1:
lam = math.sqrt(lam)
rx *= lam
ry *= lam
num = max(0.0, (rx*ry)**2 - (rx*y1p)**2 - (ry*x1p)**2)
den = (rx*y1p)**2 + (ry*x1p)**2
num = max(0.0, (rx * ry) ** 2 - (rx * y1p) ** 2 - (ry * x1p) ** 2)
den = (rx * y1p) ** 2 + (ry * x1p) ** 2
sq = math.sqrt(num / den) if den != 0 else 0
if large_arc == sweep:
sq = -sq
@@ -119,9 +129,11 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
cx_ = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2
cy_ = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2
def angle(ux, uy, vx, vy):
a = math.acos(max(-1, min(1, (ux*vx + uy*vy) / (math.hypot(ux, uy) * math.hypot(vx, vy)))))
if ux*vy - uy*vx < 0:
def angle(ux: float, uy: float, vx: float, vy: float) -> float:
a = math.acos(
max(-1, min(1, (ux * vx + uy * vy) / (math.hypot(ux, uy) * math.hypot(vx, vy))))
)
if ux * vy - uy * vx < 0:
a = -a
return a
@@ -144,8 +156,9 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
# --- main loop ---
while i < len(tokens):
tok = tokens[i]
if tok.lstrip('+-').replace('.', '', 1).replace('e', '', 1).replace('E', '', 1).lstrip('+-').isdigit() or \
re.match(r'^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$', tok):
if tok.lstrip("+-").replace(".", "", 1).replace("e", "", 1).replace("E", "", 1).lstrip(
"+-"
).isdigit() or re.match(r"^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$", tok):
# implicit repeat of last command
pass
else:
@@ -155,7 +168,7 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
rel = cmd.islower()
if cmd in ('M', 'm'):
if cmd in ("M", "m"):
x, y = consume(2)
if rel:
cx, cy = cx + x, cy + y
@@ -166,9 +179,9 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
current = [(cx, cy)]
sx, sy = cx, cy
# subsequent coordinates are implicit L/l
cmd = 'l' if rel else 'L'
cmd = "l" if rel else "L"
elif cmd in ('L', 'l'):
elif cmd in ("L", "l"):
x, y = consume(2)
if rel:
cx, cy = cx + x, cy + y
@@ -176,36 +189,46 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
cx, cy = x, y
current.append((cx, cy))
elif cmd in ('H', 'h'):
x = float(tokens[i]); i += 1
elif cmd in ("H", "h"):
x = float(tokens[i])
i += 1
cx = cx + x if rel else x
current.append((cx, cy))
elif cmd in ('V', 'v'):
y = float(tokens[i]); i += 1
elif cmd in ("V", "v"):
y = float(tokens[i])
i += 1
cy = cy + y if rel else y
current.append((cx, cy))
elif cmd in ('Z', 'z'):
elif cmd in ("Z", "z"):
current.append((sx, sy)) # close
polygons.append(current)
current = []
cx, cy = sx, sy
elif cmd in ('C', 'c'):
elif cmd in ("C", "c"):
x1, y1, x2, y2, x, y = consume(6)
if rel:
x1 += cx; y1 += cy; x2 += cx; y2 += cy; x += cx; y += cy
x1 += cx
y1 += cy
x2 += cx
y2 += cy
x += cx
y += cy
pts = cubic_bezier_points((cx, cy), (x1, y1), (x2, y2), (x, y))
current.extend(pts)
last_ctrl = (x2, y2)
cx, cy = x, y
elif cmd in ('S', 's'):
elif cmd in ("S", "s"):
x2, y2, x, y = consume(4)
if rel:
x2 += cx; y2 += cy; x += cx; y += cy
if last_ctrl and last_cmd in ('C', 'c', 'S', 's'):
x2 += cx
y2 += cy
x += cx
y += cy
if last_ctrl and last_cmd in ("C", "c", "S", "s"):
x1 = 2 * cx - last_ctrl[0]
y1 = 2 * cy - last_ctrl[1]
else:
@@ -215,20 +238,24 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
last_ctrl = (x2, y2)
cx, cy = x, y
elif cmd in ('Q', 'q'):
elif cmd in ("Q", "q"):
x1, y1, x, y = consume(4)
if rel:
x1 += cx; y1 += cy; x += cx; y += cy
x1 += cx
y1 += cy
x += cx
y += cy
pts = quad_bezier_points((cx, cy), (x1, y1), (x, y))
current.extend(pts)
last_ctrl = (x1, y1)
cx, cy = x, y
elif cmd in ('T', 't'):
elif cmd in ("T", "t"):
x, y = consume(2)
if rel:
x += cx; y += cy
if last_ctrl and last_cmd in ('Q', 'q', 'T', 't'):
x += cx
y += cy
if last_ctrl and last_cmd in ("Q", "q", "T", "t"):
x1 = 2 * cx - last_ctrl[0]
y1 = 2 * cy - last_ctrl[1]
else:
@@ -238,11 +265,12 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
last_ctrl = (x1, y1)
cx, cy = x, y
elif cmd in ('A', 'a'):
elif cmd in ("A", "a"):
rx, ry, phi, large, sweep, x, y = consume(7)
large, sweep = int(large), int(sweep)
if rel:
x += cx; y += cy
x += cx
y += cy
pts = arc_points(cx, cy, rx, ry, phi, large, sweep, x, y)
current.extend(pts)
cx, cy = x, y
@@ -264,48 +292,45 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
# ---------------------------------------------------------------------------
def _parse_transform(transform_str: str) -> List[List[float]]:
"""Parse SVG transform attribute, return list of 3×3 matrix rows [a,b,c; d,e,f; 0,0,1]."""
def identity():
def identity() -> List[List[float]]:
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
def mat_mul(A, B):
return [
[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)]
for r in range(3)
]
def mat_mul(A: List[List[float]], B: List[List[float]]) -> List[List[float]]:
return [[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)] for r in range(3)]
result = identity()
for m in re.finditer(
r'(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)',
transform_str
r"(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)", transform_str
):
func = m.group(1)
args = [float(v) for v in re.split(r'[\s,]+', m.group(2).strip()) if v]
args = [float(v) for v in re.split(r"[\s,]+", m.group(2).strip()) if v]
mat = identity()
if func == 'matrix' and len(args) == 6:
if func == "matrix" and len(args) == 6:
a, b, c, d, e, f = args
mat = [[a, c, e], [b, d, f], [0, 0, 1]]
elif func == 'translate':
elif func == "translate":
tx = args[0]
ty = args[1] if len(args) > 1 else 0
mat = [[1, 0, tx], [0, 1, ty], [0, 0, 1]]
elif func == 'scale':
elif func == "scale":
sx = args[0]
sy = args[1] if len(args) > 1 else sx
mat = [[sx, 0, 0], [0, sy, 0], [0, 0, 1]]
elif func == 'rotate':
elif func == "rotate":
angle = math.radians(args[0])
cos, sin = math.cos(angle), math.sin(angle)
if len(args) == 3:
cx_, cy_ = args[1], args[2]
t1 = [[1, 0, cx_], [0, 1, cy_], [0, 0, 1]]
r = [[cos, -sin, 0], [sin, cos, 0], [0, 0, 1]]
r = [[cos, -sin, 0], [sin, cos, 0], [0, 0, 1]]
t2 = [[1, 0, -cx_], [0, 1, -cy_], [0, 0, 1]]
mat = mat_mul(mat_mul(t1, r), t2)
else:
mat = [[cos, -sin, 0], [sin, cos, 0], [0, 0, 1]]
elif func == 'skewX':
elif func == "skewX":
mat = [[1, math.tan(math.radians(args[0])), 0], [0, 1, 0], [0, 0, 1]]
elif func == 'skewY':
elif func == "skewY":
mat = [[1, 0, 0], [math.tan(math.radians(args[0])), 1, 0], [0, 0, 1]]
result = mat_mul(result, mat)
return result
@@ -320,44 +345,41 @@ def _apply_transform(pts: List[Point], mat: List[List[float]]) -> List[Point]:
return out
def _mat_mul(A, B):
return [
[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)]
for r in range(3)
]
def _mat_mul(A: List[List[float]], B: List[List[float]]) -> List[List[float]]:
return [[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)] for r in range(3)]
# ---------------------------------------------------------------------------
# SVG element → polygon extractor
# ---------------------------------------------------------------------------
SVG_NS = re.compile(r'\{[^}]+\}')
SVG_NS = re.compile(r"\{[^}]+\}")
def _tag(el: ET.Element) -> str:
return SVG_NS.sub('', el.tag)
return SVG_NS.sub("", el.tag)
def _get_attr(el: ET.Element, name: str, default: Optional[str] = None) -> Optional[str]:
for key in el.attrib:
if SVG_NS.sub('', key) == name:
if SVG_NS.sub("", key) == name:
return el.attrib[key]
return default
def _identity():
def _identity() -> List[List[float]]:
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
def _extract_polygons_from_element(el: ET.Element, parent_mat: List[List[float]]) -> List[Polygon]:
"""Recursively extract all polygons from an SVG element tree."""
tag = _tag(el)
display = _get_attr(el, 'display', 'inline')
visibility = _get_attr(el, 'visibility', 'visible')
if display == 'none' or visibility == 'hidden':
display = _get_attr(el, "display", "inline")
visibility = _get_attr(el, "visibility", "visible")
if display == "none" or visibility == "hidden":
return []
# Accumulate transform
transform_str = _get_attr(el, 'transform', '')
transform_str = _get_attr(el, "transform", "")
if transform_str:
local_mat = _parse_transform(transform_str)
mat = _mat_mul(parent_mat, local_mat)
@@ -366,65 +388,73 @@ def _extract_polygons_from_element(el: ET.Element, parent_mat: List[List[float]]
result: List[Polygon] = []
if tag == 'g' or tag == 'svg':
if tag == "g" or tag == "svg":
for child in el:
result.extend(_extract_polygons_from_element(child, mat))
elif tag == 'path':
d = _get_attr(el, 'd', '')
elif tag == "path":
d = _get_attr(el, "d", "")
if d:
tokens = _tokenize_path(d)
polygons = _parse_path_tokens(tokens)
for poly in polygons:
result.append(_apply_transform(poly, mat))
elif tag == 'rect':
x = float(_get_attr(el, 'x', '0') or 0)
y = float(_get_attr(el, 'y', '0') or 0)
w = float(_get_attr(el, 'width', '0') or 0)
h = float(_get_attr(el, 'height', '0') or 0)
elif tag == "rect":
x = float(_get_attr(el, "x", "0") or 0)
y = float(_get_attr(el, "y", "0") or 0)
w = float(_get_attr(el, "width", "0") or 0)
h = float(_get_attr(el, "height", "0") or 0)
if w > 0 and h > 0:
pts = [(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)]
result.append(_apply_transform(pts, mat))
elif tag == 'circle':
cx_ = float(_get_attr(el, 'cx', '0') or 0)
cy_ = float(_get_attr(el, 'cy', '0') or 0)
r = float(_get_attr(el, 'r', '0') or 0)
elif tag == "circle":
cx_ = float(_get_attr(el, "cx", "0") or 0)
cy_ = float(_get_attr(el, "cy", "0") or 0)
r = float(_get_attr(el, "r", "0") or 0)
if r > 0:
steps = 36
pts = [(cx_ + r * math.cos(2 * math.pi * k / steps),
cy_ + r * math.sin(2 * math.pi * k / steps))
for k in range(steps + 1)]
pts = [
(
cx_ + r * math.cos(2 * math.pi * k / steps),
cy_ + r * math.sin(2 * math.pi * k / steps),
)
for k in range(steps + 1)
]
result.append(_apply_transform(pts, mat))
elif tag == 'ellipse':
cx_ = float(_get_attr(el, 'cx', '0') or 0)
cy_ = float(_get_attr(el, 'cy', '0') or 0)
rx = float(_get_attr(el, 'rx', '0') or 0)
ry = float(_get_attr(el, 'ry', '0') or 0)
elif tag == "ellipse":
cx_ = float(_get_attr(el, "cx", "0") or 0)
cy_ = float(_get_attr(el, "cy", "0") or 0)
rx = float(_get_attr(el, "rx", "0") or 0)
ry = float(_get_attr(el, "ry", "0") or 0)
if rx > 0 and ry > 0:
steps = 36
pts = [(cx_ + rx * math.cos(2 * math.pi * k / steps),
cy_ + ry * math.sin(2 * math.pi * k / steps))
for k in range(steps + 1)]
pts = [
(
cx_ + rx * math.cos(2 * math.pi * k / steps),
cy_ + ry * math.sin(2 * math.pi * k / steps),
)
for k in range(steps + 1)
]
result.append(_apply_transform(pts, mat))
elif tag in ('polygon', 'polyline'):
points_str = _get_attr(el, 'points', '')
elif tag in ("polygon", "polyline"):
points_str = _get_attr(el, "points", "")
if points_str:
nums = [float(v) for v in re.split(r'[\s,]+', points_str.strip()) if v]
nums = [float(v) for v in re.split(r"[\s,]+", points_str.strip()) if v]
pts = [(nums[k], nums[k + 1]) for k in range(0, len(nums) - 1, 2)]
if tag == 'polygon' and pts:
if tag == "polygon" and pts:
pts.append(pts[0]) # close
if pts:
result.append(_apply_transform(pts, mat))
elif tag == 'line':
x1 = float(_get_attr(el, 'x1', '0') or 0)
y1 = float(_get_attr(el, 'y1', '0') or 0)
x2 = float(_get_attr(el, 'x2', '0') or 0)
y2 = float(_get_attr(el, 'y2', '0') or 0)
elif tag == "line":
x1 = float(_get_attr(el, "x1", "0") or 0)
y1 = float(_get_attr(el, "y1", "0") or 0)
x2 = float(_get_attr(el, "x2", "0") or 0)
y2 = float(_get_attr(el, "y2", "0") or 0)
pts = [(x1, y1), (x2, y2)]
result.append(_apply_transform(pts, mat))
@@ -453,20 +483,24 @@ def _build_gr_poly(points: List[Point], layer: str, stroke_width: float, filled:
row = []
fill_str = "yes" if filled else "none"
uid = str(uuid.uuid4())
lines = [
"\t(gr_poly",
"\t\t(pts",
] + pts_lines + [
"\t\t)",
"\t\t(stroke",
f"\t\t\t(width {stroke_width:.4f})",
"\t\t\t(type solid)",
"\t\t)",
f"\t\t(fill {fill_str})",
f'\t\t(layer "{layer}")',
f'\t\t(uuid "{uid}")',
"\t)",
]
lines = (
[
"\t(gr_poly",
"\t\t(pts",
]
+ pts_lines
+ [
"\t\t)",
"\t\t(stroke",
f"\t\t\t(width {stroke_width:.4f})",
"\t\t\t(type solid)",
"\t\t)",
f"\t\t(fill {fill_str})",
f'\t\t(layer "{layer}")',
f'\t\t(uuid "{uid}")',
"\t)",
]
)
return "\n".join(lines)
@@ -510,15 +544,15 @@ def import_svg_to_pcb(
root = tree.getroot()
# Determine SVG viewport
vb = _get_attr(root, 'viewBox')
vb = _get_attr(root, "viewBox")
if vb:
parts = [float(v) for v in re.split(r'[\s,]+', vb.strip()) if v]
parts = [float(v) for v in re.split(r"[\s,]+", vb.strip()) if v]
svg_x0, svg_y0, svg_w, svg_h = parts[0], parts[1], parts[2], parts[3]
else:
w_str = _get_attr(root, 'width', '100') or '100'
h_str = _get_attr(root, 'height', '100') or '100'
svg_w = float(re.sub(r'[^\d.]', '', w_str) or 100)
svg_h = float(re.sub(r'[^\d.]', '', h_str) or 100)
w_str = _get_attr(root, "width", "100") or "100"
h_str = _get_attr(root, "height", "100") or "100"
svg_w = float(re.sub(r"[^\d.]", "", w_str) or 100)
svg_h = float(re.sub(r"[^\d.]", "", h_str) or 100)
svg_x0, svg_y0 = 0.0, 0.0
if svg_w == 0 or svg_h == 0:
@@ -569,7 +603,10 @@ def import_svg_to_pcb(
insert_block = "\n" + "\n".join(gr_lines) + "\n"
last_paren = pcb_content.rfind(")")
if last_paren == -1:
return {"success": False, "message": "PCB file format error: no closing parenthesis found"}
return {
"success": False,
"message": "PCB file format error: no closing parenthesis found",
}
new_content = pcb_content[:last_paren] + insert_block + pcb_content[last_paren:]
@@ -597,5 +634,6 @@ def import_svg_to_pcb(
except Exception as e:
logger.error(f"SVG import failed: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}

View File

@@ -12,9 +12,9 @@ KiCAD 9 .kicad_sym format:
- All coordinates in mm, 2.54mm grid typical for schematic symbols
"""
import logging
import os
import re
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
@@ -24,15 +24,31 @@ KICAD9_SYMBOL_LIB_VERSION = "20241209"
# Pin electrical types
PIN_TYPES = {
"input", "output", "bidirectional", "tri_state", "passive",
"free", "unspecified", "power_in", "power_out",
"open_collector", "open_emitter", "no_connect",
"input",
"output",
"bidirectional",
"tri_state",
"passive",
"free",
"unspecified",
"power_in",
"power_out",
"open_collector",
"open_emitter",
"no_connect",
}
# Pin graphic shapes
PIN_SHAPES = {
"line", "inverted", "clock", "inverted_clock", "input_low",
"clock_low", "output_low", "falling_edge_clock", "non_logic",
"line",
"inverted",
"clock",
"inverted_clock",
"input_low",
"clock_low",
"output_low",
"falling_edge_clock",
"non_logic",
}
@@ -125,11 +141,11 @@ class SymbolCreator:
lib_content = lib_path.read_text(encoding="utf-8")
else:
lib_content = (
f'(kicad_symbol_lib\n'
f' (version {KICAD9_SYMBOL_LIB_VERSION})\n'
f"(kicad_symbol_lib\n"
f" (version {KICAD9_SYMBOL_LIB_VERSION})\n"
f' (generator "kicad-mcp")\n'
f' (generator_version "9.0")\n'
f')\n'
f")\n"
)
# Check for duplicate
@@ -209,7 +225,7 @@ class SymbolCreator:
# Only top-level symbols (not sub-symbols like _0_1 or _1_1)
names = re.findall(r'^\s*\(symbol "([^"_][^"]*)"', content, re.MULTILINE)
# Filter out sub-symbols (contain _N_N suffix)
symbols = [n for n in names if not re.search(r'_\d+_\d+$', n)]
symbols = [n for n in names if not re.search(r"_\d+_\d+$", n)]
return {
"success": True,
"library_path": str(lib_path),
@@ -332,9 +348,9 @@ class SymbolCreator:
board_str = "yes" if on_board else "no"
lines.append(f' (symbol "{name}"')
lines.append(f' (exclude_from_sim no)')
lines.append(f' (in_bom {bom_str})')
lines.append(f' (on_board {board_str})')
lines.append(f" (exclude_from_sim no)")
lines.append(f" (in_bom {bom_str})")
lines.append(f" (on_board {board_str})")
# Properties
lines.extend(_property_block("Reference", reference_prefix, 2.54, 0, visible=True))
@@ -351,15 +367,15 @@ class SymbolCreator:
lines.extend(_rect_sym_lines(rect))
for pl in polylines:
lines.extend(_polyline_lines(pl))
lines.append(f' )')
lines.append(f" )")
# Sub-symbol _1_1: pins
lines.append(f' (symbol "{name}_1_1"')
for pin in pins:
lines.extend(_pin_lines(pin))
lines.append(f' )')
lines.append(f" )")
lines.append(f' )')
lines.append(f" )")
return "\n".join(lines)
def _remove_symbol(self, content: str, name: str) -> str:
@@ -372,8 +388,9 @@ class SymbolCreator:
for line in lines:
stripped = line.strip()
if not skip:
if re.match(rf'^\s*\(symbol "{re.escape(name)}"', line) and \
not re.search(r'_\d+_\d+"', line):
if re.match(rf'^\s*\(symbol "{re.escape(name)}"', line) and not re.search(
r'_\d+_\d+"', line
):
skip = True
depth = stripped.count("(") - stripped.count(")")
continue
@@ -390,17 +407,16 @@ class SymbolCreator:
# S-Expression helper functions #
# ------------------------------------------------------------------ #
def _property_block(
key: str, value: str, x: float, y: float, visible: bool = True
) -> List[str]:
def _property_block(key: str, value: str, x: float, y: float, visible: bool = True) -> List[str]:
hide = "" if visible else "\n (hide yes)"
return [
f' (property "{_esc(key)}" "{_esc(value)}"',
f' (at {_fmt(x)} {_fmt(y)} 0)',
f' (effects',
f' (font (size 1.27 1.27))',
f' ){hide}',
f' )',
f" (at {_fmt(x)} {_fmt(y)} 0)",
f" (effects",
f" (font (size 1.27 1.27))",
f" ){hide}",
f" )",
]
@@ -412,12 +428,12 @@ def _rect_sym_lines(rect: Dict[str, Any]) -> List[str]:
w = _fmt(rect.get("width", 0.254))
fill = rect.get("fill", "background")
return [
f' (rectangle',
f' (start {x1} {y1})',
f' (end {x2} {y2})',
f' (stroke (width {w}) (type default))',
f' (fill (type {fill}))',
f' )',
f" (rectangle",
f" (start {x1} {y1})",
f" (end {x2} {y2})",
f" (stroke (width {w}) (type default))",
f" (fill (type {fill}))",
f" )",
]
@@ -426,16 +442,16 @@ def _polyline_lines(pl: Dict[str, Any]) -> List[str]:
w = _fmt(pl.get("width", 0.254))
fill = pl.get("fill", "none")
lines = [
f' (polyline',
f' (pts',
f" (polyline",
f" (pts",
]
for pt in pts:
lines.append(f' (xy {_fmt(pt["x"])} {_fmt(pt["y"])})')
lines += [
f' )',
f' (stroke (width {w}) (type default))',
f' (fill (type {fill}))',
f' )',
f" )",
f" (stroke (width {w}) (type default))",
f" (fill (type {fill}))",
f" )",
]
return lines
@@ -452,14 +468,14 @@ def _pin_lines(pin: Dict[str, Any]) -> List[str]:
pin_number = str(pin.get("number", "1"))
return [
f' (pin {ptype} {shape}',
f' (at {x} {y} {angle})',
f' (length {length})',
f" (pin {ptype} {shape}",
f" (at {x} {y} {angle})",
f" (length {length})",
f' (name "{_esc(pin_name)}"',
f' (effects (font (size 1.27 1.27)))',
f' )',
f" (effects (font (size 1.27 1.27)))",
f" )",
f' (number "{_esc(pin_number)}"',
f' (effects (font (size 1.27 1.27)))',
f' )',
f' )',
f" (effects (font (size 1.27 1.27)))",
f" )",
f" )",
]

View File

@@ -0,0 +1,494 @@
"""
Wire Connectivity Analysis for KiCad Schematics
Traces wire networks from a point and finds connected component pins.
Uses KiCad's internal integer unit system (10,000 IU per mm) for exact
coordinate matching, mirroring KiCad's own connectivity algorithm.
"""
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
from commands.pin_locator import PinLocator
logger = logging.getLogger("kicad_interface")
_IU_PER_MM = 10000 # KiCad schematic internal units per millimeter
def _to_iu(x_mm: float, y_mm: float) -> Tuple[int, int]:
"""Convert mm coordinates to KiCad internal units (integer)."""
return (round(x_mm * _IU_PER_MM), round(y_mm * _IU_PER_MM))
def _parse_wires(schematic: Any) -> List[List[Tuple[int, int]]]:
"""Extract wire endpoints from a schematic object as IU tuples."""
all_wires = []
for wire in schematic.wire:
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
pts = []
for point in wire.pts.xy:
if hasattr(point, "value"):
pts.append(_to_iu(float(point.value[0]), float(point.value[1])))
if len(pts) >= 2:
all_wires.append(pts)
return all_wires
def _build_adjacency(
all_wires: List[List[Tuple[int, int]]],
) -> Tuple[List[Set[int]], Dict[Tuple[int, int], Set[int]]]:
"""Build wire adjacency using exact IU coordinate matching.
Wires that share an endpoint are adjacent — this naturally handles
junctions since all wires meeting at the same point get connected.
Returns a tuple of:
- adjacency: list of sets, one per wire, containing adjacent wire indices
- iu_to_wires: dict mapping each IU endpoint to the set of wire indices
that have an endpoint at that exact coordinate (used for seed queries)
"""
# Map each IU endpoint to all wire indices that touch it
iu_to_wires: Dict[Tuple[int, int], Set[int]] = {}
for i, pts in enumerate(all_wires):
for pt in pts:
iu_to_wires.setdefault(pt, set()).add(i)
# Wires that share an IU endpoint are adjacent
adjacency: List[Set[int]] = [set() for _ in range(len(all_wires))]
for wire_set in iu_to_wires.values():
wire_list = list(wire_set)
for a in wire_list:
for b in wire_list:
if a != b:
adjacency[a].add(b)
return adjacency, iu_to_wires
def _parse_virtual_connections(
schematic: Any, schematic_path: Any
) -> Tuple[Dict[Tuple[int, int], str], Dict[str, List[Tuple[int, int]]]]:
"""Return virtual connectivity from net labels and power symbols.
Returns a tuple of:
- point_to_label: Dict[Tuple[int,int], str] — IU position → label name
- label_to_points: Dict[str, List[Tuple[int,int]]] — label name → list of IU positions
"""
point_to_label: Dict[Tuple[int, int], str] = {}
label_to_points: Dict[str, List[Tuple[int, int]]] = {}
if hasattr(schematic, "label"):
for label in schematic.label:
try:
if not hasattr(label, "value"):
continue
name = label.value
if not hasattr(label, "at") or not hasattr(label.at, "value"):
continue
coords = label.at.value
pt = _to_iu(float(coords[0]), float(coords[1]))
point_to_label[pt] = name
label_to_points.setdefault(name, []).append(pt)
except Exception as e:
logger.warning(f"Error parsing net label: {e}")
if hasattr(schematic, "symbol"):
locator = PinLocator()
for symbol in schematic.symbol:
try:
if not hasattr(symbol, "property") or not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if not ref.startswith("#PWR"):
continue
if ref.startswith("_TEMPLATE"):
continue
if not hasattr(symbol.property, "Value"):
continue
name = symbol.property.Value.value
all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref)
if not all_pins or "1" not in all_pins:
continue
pin_data = all_pins["1"]
pt = _to_iu(float(pin_data[0]), float(pin_data[1]))
point_to_label[pt] = name
label_to_points.setdefault(name, []).append(pt)
except Exception as e:
logger.warning(f"Error parsing power symbol: {e}")
return point_to_label, label_to_points
def _find_connected_wires(
x_mm: float,
y_mm: float,
all_wires: List[List[Tuple[int, int]]],
iu_to_wires: Dict[Tuple[int, int], Set[int]],
adjacency: List[Set[int]],
point_to_label: Optional[Dict[Tuple[int, int], str]] = None,
label_to_points: Optional[Dict[str, List[Tuple[int, int]]]] = None,
) -> Tuple:
"""BFS from query point. Returns (visited wire indices, net IU points) or (None, None).
Requires query point (x_mm, y_mm) to be exactly on a wire endpoint (exact IU match).
"""
query_iu = _to_iu(x_mm, y_mm)
# Find seed wires: exact IU match on the query endpoint
seed_set = iu_to_wires.get(query_iu)
if not seed_set:
return (None, None)
seed_indices: Set[int] = set(seed_set)
# BFS flood-fill using pre-compiled adjacency
visited: Set[int] = set(seed_indices)
queue = list(seed_indices)
net_points: Set[Tuple[int, int]] = set()
for i in seed_indices:
net_points.update(all_wires[i])
seen_labels: Set[str] = set()
while queue:
wire_idx = queue.pop()
for neighbor_idx in adjacency[wire_idx]:
if neighbor_idx not in visited:
visited.add(neighbor_idx)
queue.append(neighbor_idx)
net_points.update(all_wires[neighbor_idx])
if point_to_label and label_to_points:
for pt in all_wires[wire_idx]:
label_name = point_to_label.get(pt)
if label_name and label_name not in seen_labels:
seen_labels.add(label_name)
for other_pt in label_to_points.get(label_name, []):
if other_pt == pt:
continue
for idx in iu_to_wires.get(other_pt, set()):
if idx not in visited:
visited.add(idx)
queue.append(idx)
net_points.update(all_wires[idx])
return (visited, net_points)
def _find_pins_on_net(
net_points: Set[Tuple[int, int]],
schematic_path: Any,
schematic: Any,
) -> List[Dict]:
"""Find component pins that land on net points using exact IU matching.
Returns a list of {"component": ref, "pin": pin_num} dicts.
"""
def _on_net(px_mm: float, py_mm: float) -> bool:
return _to_iu(px_mm, py_mm) in net_points
locator = PinLocator()
pins = []
seen: Set[Tuple] = set()
ref = None
for symbol in schematic.symbol:
try:
if not hasattr(symbol, "property") or not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref)
if not all_pins:
continue
for pin_num, pin_data in all_pins.items():
if _on_net(pin_data[0], pin_data[1]):
key = (ref, pin_num)
if key not in seen:
seen.add(key)
pins.append({"component": ref, "pin": pin_num})
except Exception as e:
logger.warning(
f"Error checking pins for {ref if ref is not None else '<unknown>'}: {e}"
)
return pins
def get_wire_connections(
schematic: Any, schematic_path: str, x_mm: float, y_mm: float
) -> Optional[Dict]:
"""Find the net name and all component pins reachable from a point via connected wires.
The query point (x_mm, y_mm) must be exactly on a wire endpoint or junction (exact IU match).
Interior (mid-segment) points are not matched —
use wire endpoint coordinates obtained from the schematic data.
Net labels and power symbols are traversed: wires on the same named net are
treated as connected even when they are not geometrically adjacent.
Returns dict with keys:
- "net": str or None (net label/power name, None if unnamed)
- "pins": list of {"component": str, "pin": str}
- "wires": list of {"start": {"x", "y"}, "end": {"x", "y"}} in mm
- "query_point": {"x": float, "y": float}
Or None if no wire endpoint found within tolerance of the query point.
"""
all_wires = _parse_wires(schematic)
query_point = {"x": x_mm, "y": y_mm}
if not all_wires:
return {"net": None, "pins": [], "wires": [], "query_point": query_point}
adjacency, iu_to_wires = _build_adjacency(all_wires)
point_to_label, label_to_points = _parse_virtual_connections(schematic, schematic_path)
visited, net_points = _find_connected_wires(
x_mm,
y_mm,
all_wires,
iu_to_wires,
adjacency,
point_to_label=point_to_label,
label_to_points=label_to_points,
)
if visited is None:
return None
# Resolve net name: first label anchor that falls on this net's IU points
net: Optional[str] = None
for pt in net_points:
label = point_to_label.get(pt)
if label is not None:
net = label
break
wires_out = [
{
"start": {
"x": all_wires[i][0][0] / _IU_PER_MM,
"y": all_wires[i][0][1] / _IU_PER_MM,
},
"end": {
"x": all_wires[i][-1][0] / _IU_PER_MM,
"y": all_wires[i][-1][1] / _IU_PER_MM,
},
}
for i in visited
]
if not hasattr(schematic, "symbol"):
return {"net": net, "pins": [], "wires": wires_out, "query_point": query_point}
pins = _find_pins_on_net(net_points, schematic_path, schematic)
return {"net": net, "pins": pins, "wires": wires_out, "query_point": query_point}
def count_pins_on_net(
schematic: Any,
schematic_path: str,
net_name: str,
all_wires: List[List[Tuple[int, int]]],
iu_to_wires: Dict[Tuple[int, int], Set[int]],
adjacency: List[Set[int]],
point_to_label: Dict[Tuple[int, int], str],
label_to_points: Dict[str, List[Tuple[int, int]]],
) -> int:
"""Count the number of component pins connected to the named net.
A pin is counted if its IU coordinate falls on the wire-network reachable
from any label anchor for *net_name*, or directly on a label anchor of that
net (pin directly touching a label with no intervening wire).
Returns the count of distinct (component, pin_num) pairs on this net.
"""
label_positions = label_to_points.get(net_name, [])
if not label_positions:
return 0
# Collect the union of all net-points across all label positions for this net
all_net_points: Set[Tuple[int, int]] = set()
for lx, ly in label_positions:
# Include the label anchor itself so pins directly at the label count
all_net_points.add((lx, ly))
# Trace from this label position into the wire graph
x_mm = lx / _IU_PER_MM
y_mm = ly / _IU_PER_MM
visited, net_points = _find_connected_wires(
x_mm,
y_mm,
all_wires,
iu_to_wires,
adjacency,
point_to_label=point_to_label,
label_to_points=label_to_points,
)
if net_points:
all_net_points |= net_points
if not hasattr(schematic, "symbol"):
return 0
locator = PinLocator()
seen: Set[Tuple[str, str]] = set()
ref = None
for symbol in schematic.symbol:
try:
if not hasattr(symbol, "property") or not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref)
if not all_pins:
continue
for pin_num, pin_data in all_pins.items():
pin_iu = _to_iu(float(pin_data[0]), float(pin_data[1]))
if pin_iu in all_net_points:
key = (ref, pin_num)
if key not in seen:
seen.add(key)
except Exception as e:
logger.warning(
f"Error checking pins for {ref if ref is not None else '<unknown>'}: {e}"
)
return len(seen)
def list_floating_labels(schematic: Any, schematic_path: str) -> List[Dict[str, Any]]:
"""Return net labels that are not connected to any component pin.
A label is "floating" when no component pin's IU coordinate falls on the
wire-network reachable from the label's anchor position. These labels are
likely placed off-grid or incorrectly positioned and will cause ERC errors.
Returns a list of dicts with keys:
- "name": str — the net label text
- "x": float — label X position in mm
- "y": float — label Y position in mm
- "type": str — "label" or "global_label"
"""
all_wires = _parse_wires(schematic)
if all_wires:
adjacency, iu_to_wires = _build_adjacency(all_wires)
else:
adjacency = []
iu_to_wires = {}
point_to_label, label_to_points = _parse_virtual_connections(schematic, schematic_path)
# Build a set of all pin IU positions for fast lookup
pin_iu_set: Set[Tuple[int, int]] = set()
if hasattr(schematic, "symbol"):
locator = PinLocator()
for symbol in schematic.symbol:
try:
if not hasattr(symbol, "property") or not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref)
if not all_pins:
continue
for pin_data in all_pins.values():
pin_iu_set.add(_to_iu(float(pin_data[0]), float(pin_data[1])))
except Exception as e:
logger.warning(f"Error reading pins for floating-label check: {e}")
floating: List[Dict[str, Any]] = []
if not hasattr(schematic, "label"):
return floating
for label in schematic.label:
try:
if not hasattr(label, "value"):
continue
name = label.value
if not hasattr(label, "at") or not hasattr(label.at, "value"):
continue
coords = label.at.value
lx_mm = float(coords[0])
ly_mm = float(coords[1])
label_iu = _to_iu(lx_mm, ly_mm)
# Check if the label anchor itself is a pin position
if label_iu in pin_iu_set:
continue
# Trace the wire-network from this label and check for pins
if all_wires:
_, net_points = _find_connected_wires(
lx_mm,
ly_mm,
all_wires,
iu_to_wires,
adjacency,
point_to_label=point_to_label,
label_to_points=label_to_points,
)
else:
net_points = None
if net_points is not None and net_points & pin_iu_set:
continue # at least one pin on this net
floating.append({"name": name, "x": lx_mm, "y": ly_mm, "type": "label"})
except Exception as e:
logger.warning(f"Error checking label for floating status: {e}")
return floating
def get_net_at_point(
schematic: Any, schematic_path: str, x_mm: float, y_mm: float
) -> Dict[str, Any]:
"""Return the net name at the given coordinate, or null if none found.
Checks net label positions first (exact IU match within tolerance), then
wire endpoints. Returns a dict with keys:
- "net_name": str or None
- "position": {"x": float, "y": float}
- "source": "net_label" | "wire_endpoint" | None
"""
query_iu = _to_iu(x_mm, y_mm)
position = {"x": x_mm, "y": y_mm}
# Build label map from schematic
point_to_label, _ = _parse_virtual_connections(schematic, schematic_path)
# Check if query point is exactly on a net label / power symbol position
label_name = point_to_label.get(query_iu)
if label_name is not None:
return {"net_name": label_name, "position": position, "source": "net_label"}
# Check if query point is on a wire endpoint
all_wires = _parse_wires(schematic) if hasattr(schematic, "wire") else []
if all_wires:
adjacency, iu_to_wires = _build_adjacency(all_wires)
if query_iu in iu_to_wires:
# Found a wire endpoint — trace the net to get the name
visited, net_points = _find_connected_wires(
x_mm,
y_mm,
all_wires,
iu_to_wires,
adjacency,
point_to_label=point_to_label,
label_to_points=None,
)
if visited is not None:
net: Optional[str] = None
if net_points:
for pt in net_points:
net = point_to_label.get(pt)
if net is not None:
break
return {"net_name": net, "position": position, "source": "wire_endpoint"}
return {"net_name": None, "position": position, "source": None}

View File

@@ -0,0 +1,439 @@
"""
WireDragger — drag connected wires when a schematic component is moved.
All methods operate on in-memory sexpdata lists (no disk I/O).
"""
import logging
import math
import uuid
from typing import Any, Dict, List, Optional, Tuple
import sexpdata
from sexpdata import Symbol
logger = logging.getLogger("kicad_interface")
# Module-level Symbol constants
_K = {
name: Symbol(name)
for name in [
"symbol",
"at",
"lib_id",
"mirror",
"lib_symbols",
"pts",
"xy",
"wire",
"junction",
"property",
"stroke",
"width",
"type",
"uuid",
]
}
EPS = 1e-4 # mm — coordinate match tolerance
def _rotate(x: float, y: float, angle_deg: float) -> Tuple[float, float]:
"""Rotate (x, y) around the origin by angle_deg degrees (CCW)."""
if angle_deg == 0:
return x, y
rad = math.radians(angle_deg)
c, s = math.cos(rad), math.sin(rad)
return x * c - y * s, x * s + y * c
def _coords_match(ax: float, ay: float, bx: float, by: float, eps: float = EPS) -> bool:
return abs(ax - bx) < eps and abs(ay - by) < eps
class WireDragger:
"""Pure-logic helpers for wire-endpoint dragging during component moves."""
@staticmethod
def find_symbol(sch_data: list, reference: str) -> Any:
"""
Find a placed symbol by reference designator.
Returns (symbol_item, old_x, old_y, rotation, lib_id, mirror_x, mirror_y)
or None if the reference is not found.
mirror_x=True means the symbol has (mirror x) — flips the X local axis.
mirror_y=True means the symbol has (mirror y) — flips the Y local axis.
"""
sym_k = _K["symbol"]
prop_k = _K["property"]
at_k = _K["at"]
lib_id_k = _K["lib_id"]
mirror_k = _K["mirror"]
for item in sch_data:
if not (isinstance(item, list) and item and item[0] == sym_k):
continue
# Check Reference property
ref_val = None
for sub in item[1:]:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == prop_k:
if str(sub[1]).strip('"') == "Reference":
ref_val = str(sub[2]).strip('"')
break
if ref_val != reference:
continue
old_x = old_y = rotation = 0.0
lib_id = ""
mirror_x = mirror_y = False
for sub in item[1:]:
if not isinstance(sub, list) or not sub:
continue
tag = sub[0]
if tag == at_k:
if len(sub) >= 3:
old_x = float(sub[1])
old_y = float(sub[2])
if len(sub) >= 4:
rotation = float(sub[3])
elif tag == lib_id_k and len(sub) >= 2:
lib_id = str(sub[1]).strip('"')
elif tag == mirror_k and len(sub) >= 2:
mv = str(sub[1])
if mv == "x":
mirror_x = True
elif mv == "y":
mirror_y = True
return item, old_x, old_y, rotation, lib_id, mirror_x, mirror_y
return None
@staticmethod
def get_pin_defs(sch_data: list, lib_id: str) -> Dict:
"""
Get pin definitions from lib_symbols for the given lib_id.
Returns the same dict format as PinLocator.parse_symbol_definition:
{pin_num: {"x": ..., "y": ..., ...}}.
"""
from commands.pin_locator import PinLocator
lib_sym_k = _K["lib_symbols"]
symbol_k = _K["symbol"]
for item in sch_data:
if not (isinstance(item, list) and item and item[0] == lib_sym_k):
continue
for sym_def in item[1:]:
if not (isinstance(sym_def, list) and sym_def and sym_def[0] == symbol_k):
continue
if len(sym_def) < 2:
continue
name = str(sym_def[1]).strip('"')
if name == lib_id:
return PinLocator.parse_symbol_definition(sym_def)
break # only one lib_symbols section
return {}
@staticmethod
def pin_world_xy(
px: float,
py: float,
sym_x: float,
sym_y: float,
rotation: float,
mirror_x: bool,
mirror_y: bool,
) -> Tuple[float, float]:
"""
Compute the world coordinate of a pin given the symbol transform.
KiCAD applies mirror first (in local space), then rotation, then translation.
mirror_x negates the local X axis; mirror_y negates the local Y axis.
"""
lx, ly = px, py
if mirror_x:
lx = -lx
if mirror_y:
ly = -ly
rx, ry = _rotate(lx, ly, rotation)
return sym_x + rx, sym_y + ry
@staticmethod
def compute_pin_positions(
sch_data: list,
reference: str,
new_x: float,
new_y: float,
) -> Dict[str, Tuple[Tuple[float, float], Tuple[float, float]]]:
"""
Compute world pin positions before and after a component move.
Returns {pin_num: (old_world_xy, new_world_xy)}.
old_world_xy uses the symbol's current position; new_world_xy uses (new_x, new_y).
"""
found = WireDragger.find_symbol(sch_data, reference)
if found is None:
return {}
_, old_x, old_y, rotation, lib_id, mirror_x, mirror_y = found
pins = WireDragger.get_pin_defs(sch_data, lib_id)
result: Dict[str, Tuple] = {}
for pin_num, pin in pins.items():
px, py = pin["x"], pin["y"]
old_wx, old_wy = WireDragger.pin_world_xy(
px, py, old_x, old_y, rotation, mirror_x, mirror_y
)
new_wx, new_wy = WireDragger.pin_world_xy(
px, py, new_x, new_y, rotation, mirror_x, mirror_y
)
result[pin_num] = (
(round(old_wx, 6), round(old_wy, 6)),
(round(new_wx, 6), round(new_wy, 6)),
)
return result
@staticmethod
def drag_wires(
sch_data: list,
old_to_new: Dict[Tuple[float, float], Tuple[float, float]],
eps: float = EPS,
) -> Dict:
"""
Move wire endpoints and junctions from old positions to new positions.
Removes zero-length wires that result from the move.
Modifies sch_data in place.
old_to_new: {(old_x, old_y): (new_x, new_y)}
Returns {'endpoints_moved': N, 'wires_removed': M}.
"""
wire_k = _K["wire"]
pts_k = _K["pts"]
xy_k = _K["xy"]
junction_k = _K["junction"]
at_k = _K["at"]
def find_new(x: float, y: float) -> Optional[Tuple[float, float]]:
for (ox, oy), (nx, ny) in old_to_new.items():
if _coords_match(x, y, ox, oy, eps):
return nx, ny
return None
endpoints_moved = 0
zero_length_indices = []
# First pass: update wire endpoints
for idx, item in enumerate(sch_data):
if not (isinstance(item, list) and item and item[0] == wire_k):
continue
pts_sub = None
for sub in item[1:]:
if isinstance(sub, list) and sub and sub[0] == pts_k:
pts_sub = sub
break
if pts_sub is None:
continue
xy_items = [
p for p in pts_sub[1:] if isinstance(p, list) and len(p) >= 3 and p[0] == xy_k
]
for xy_item in xy_items:
nc = find_new(float(xy_item[1]), float(xy_item[2]))
if nc is not None:
xy_item[1] = nc[0]
xy_item[2] = nc[1]
endpoints_moved += 1
# Check if this wire is now zero-length
if len(xy_items) >= 2:
x1, y1 = float(xy_items[0][1]), float(xy_items[0][2])
x2, y2 = float(xy_items[-1][1]), float(xy_items[-1][2])
if _coords_match(x1, y1, x2, y2, eps):
zero_length_indices.append(idx)
# Remove zero-length wires (backwards to preserve indices)
for idx in reversed(zero_length_indices):
del sch_data[idx]
# Second pass: update junctions
for item in sch_data:
if not (isinstance(item, list) and item and item[0] == junction_k):
continue
for sub in item[1:]:
if isinstance(sub, list) and sub and sub[0] == at_k and len(sub) >= 3:
nc = find_new(float(sub[1]), float(sub[2]))
if nc is not None:
sub[1] = nc[0]
sub[2] = nc[1]
break
return {
"endpoints_moved": endpoints_moved,
"wires_removed": len(zero_length_indices),
}
@staticmethod
def update_symbol_position(sch_data: list, reference: str, new_x: float, new_y: float) -> bool:
"""
Update the (at x y rot) of the named symbol in sch_data.
Returns True if the symbol was found and updated.
"""
found = WireDragger.find_symbol(sch_data, reference)
if found is None:
return False
item = found[0]
at_k = _K["at"]
prop_k = _K["property"]
# Find current position and compute delta
old_x = old_y = None
for sub in item[1:]:
if isinstance(sub, list) and sub and sub[0] == at_k and len(sub) >= 3:
old_x, old_y = sub[1], sub[2]
sub[1] = new_x
sub[2] = new_y
break
if old_x is None or old_y is None:
return False
dx = new_x - old_x
dy = new_y - old_y
# Shift all property label positions by the same delta
for sub in item[1:]:
if isinstance(sub, list) and sub and sub[0] == prop_k:
for psub in sub[1:]:
if isinstance(psub, list) and psub and psub[0] == at_k and len(psub) >= 3:
psub[1] += dx
psub[2] += dy
break
return True
@staticmethod
def _make_wire_sexp(x1: float, y1: float, x2: float, y2: float) -> list:
"""Build a wire s-expression list in KiCAD schematic format."""
wire_uuid = str(uuid.uuid4())
return [
_K["wire"],
[_K["pts"], [_K["xy"], x1, y1], [_K["xy"], x2, y2]],
[_K["stroke"], [_K["width"], 0], [_K["type"], Symbol("default")]],
[_K["uuid"], wire_uuid],
]
@staticmethod
def get_all_stationary_pin_positions(
sch_data: list,
moved_reference: str,
) -> Dict[Tuple[float, float], str]:
"""
Return a map of {world_xy: reference} for every pin of every symbol
in sch_data *except* moved_reference.
This is used to detect pins of stationary components that coincide
with pins of the moved component (touching-pin connections).
"""
sym_k = _K["symbol"]
prop_k = _K["property"]
result: Dict[Tuple[float, float], str] = {}
for item in sch_data:
if not (isinstance(item, list) and item and item[0] == sym_k):
continue
# Determine reference
ref_val = None
for sub in item[1:]:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == prop_k:
if str(sub[1]).strip('"') == "Reference":
ref_val = str(sub[2]).strip('"')
break
if ref_val is None or ref_val == moved_reference:
continue
# Skip template / power symbols whose references start with special chars
# but we still want to handle them — no filtering needed here.
# Find lib_id and position for this symbol
found = WireDragger.find_symbol(sch_data, ref_val)
if found is None:
continue
_, sx, sy, rotation, lib_id, mirror_x, mirror_y = found
pins = WireDragger.get_pin_defs(sch_data, lib_id)
for pin_num, pin in pins.items():
wx, wy = WireDragger.pin_world_xy(
pin["x"], pin["y"], sx, sy, rotation, mirror_x, mirror_y
)
key = (round(wx, 6), round(wy, 6))
result[key] = ref_val
return result
@staticmethod
def synthesize_touching_pin_wires(
sch_data: list,
moved_reference: str,
pin_positions: Dict[str, Tuple[Tuple[float, float], Tuple[float, float]]],
eps: float = EPS,
) -> int:
"""
Detect touching-pin connections and synthesize wire segments to bridge gaps
created by moving a component.
For each pin of *moved_reference* whose old world position coincides with
a pin of a stationary component:
- If the pin moved (old_xy != new_xy), insert a wire from old_xy to new_xy.
- If the pin now lands on another stationary pin's position, skip (they touch again).
- If old_xy == new_xy, do nothing (no gap was created).
Modifies sch_data in place.
Returns the number of wire segments synthesized.
"""
if not pin_positions:
return 0
stationary_pins = WireDragger.get_all_stationary_pin_positions(sch_data, moved_reference)
if not stationary_pins:
return 0
synthesized = 0
for pin_num, (old_xy, new_xy) in pin_positions.items():
# Check if a stationary pin touches this pin's old position
touching = any(
_coords_match(old_xy[0], old_xy[1], sx, sy, eps) for (sx, sy) in stationary_pins
)
if not touching:
continue
# The pin has moved — check if it actually separated
if _coords_match(old_xy[0], old_xy[1], new_xy[0], new_xy[1], eps):
# Pin didn't actually move; no gap
continue
# Check if the pin's new position happens to touch another stationary pin
# (component moved into a different touching position — no wire needed)
rejoining = any(
_coords_match(new_xy[0], new_xy[1], sx, sy, eps) for (sx, sy) in stationary_pins
)
if rejoining:
logger.debug(
f"Pin {moved_reference}/{pin_num} moved from {old_xy} to {new_xy} "
f"and rejoins another stationary pin; no wire synthesized"
)
continue
logger.info(
f"Synthesizing wire for touching-pin connection: "
f"{moved_reference}/{pin_num} moved from {old_xy} to {new_xy}"
)
wire = WireDragger._make_wire_sexp(old_xy[0], old_xy[1], new_xy[0], new_xy[1])
# Insert before the last item (sheet_instances) to keep file tidy,
# but appending is also valid — just append.
sch_data.append(wire)
synthesized += 1
return synthesized

View File

@@ -6,17 +6,30 @@ kicad-skip's wire API doesn't support creating wires with standard parameters, s
manipulate the .kicad_sch file directly.
"""
import uuid
import logging
import math
import tempfile
import uuid
from pathlib import Path
from typing import List, Tuple, Optional, Dict
from typing import Any, List, Optional, Tuple
import sexpdata
from sexpdata import Symbol
logger = logging.getLogger("kicad_interface")
# Module-level Symbol constants — avoids repeated allocation on every call
_SYM_WIRE = Symbol("wire")
_SYM_PTS = Symbol("pts")
_SYM_XY = Symbol("xy")
_SYM_AT = Symbol("at")
_SYM_LABEL = Symbol("label")
_SYM_STROKE = Symbol("stroke")
_SYM_WIDTH = Symbol("width")
_SYM_TYPE = Symbol("type")
_SYM_UUID = Symbol("uuid")
_SYM_SHEET_INSTANCES = Symbol("sheet_instances")
class WireManager:
"""Manage wires in KiCad schematics using S-expression manipulation"""
@@ -49,31 +62,22 @@ class WireManager:
sch_data = sexpdata.loads(sch_content)
# Break any existing wire that passes through a new endpoint (T-junction support)
for pt in (start_point, end_point):
splits = WireManager._break_wires_at_point(sch_data, pt)
if splits:
logger.info(f"Broke {splits} wire(s) at new wire endpoint {pt}")
# Create wire S-expression
# Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...))
wire_sexp = [
Symbol("wire"),
[
Symbol("pts"),
[Symbol("xy"), start_point[0], start_point[1]],
[Symbol("xy"), end_point[0], end_point[1]],
],
[
Symbol("stroke"),
[Symbol("width"), stroke_width],
[Symbol("type"), Symbol(stroke_type)],
],
[Symbol("uuid"), str(uuid.uuid4())],
]
wire_sexp = WireManager._make_wire_sexp(
start_point, end_point, stroke_width, stroke_type
)
# Find insertion point (before sheet_instances)
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -130,31 +134,23 @@ class WireManager:
sch_data = sexpdata.loads(sch_content)
# Create pts list
pts_list = [Symbol("pts")]
for point in points:
pts_list.append([Symbol("xy"), point[0], point[1]])
# Break any existing wire at the outer endpoints of the new path
for pt in (points[0], points[-1]):
splits = WireManager._break_wires_at_point(sch_data, pt)
if splits:
logger.info(f"Broke {splits} wire(s) at new polyline endpoint {pt}")
# Create wire S-expression with multiple points
wire_sexp = [
Symbol("wire"),
pts_list,
[
Symbol("stroke"),
[Symbol("width"), stroke_width],
[Symbol("type"), Symbol(stroke_type)],
],
[Symbol("uuid"), str(uuid.uuid4())],
# KiCAD wire elements only support exactly 2 pts each.
# Split N waypoints into N-1 individual wire segments.
wire_sexps = [
WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type)
for i in range(len(points) - 1)
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -162,9 +158,12 @@ class WireManager:
logger.error("No sheet_instances section found in schematic")
return False
# Insert wire
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected polyline wire with {len(points)} points")
# Insert all segments (in reverse so order is preserved after inserts)
for wire_sexp in reversed(wire_sexps):
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(
f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline"
)
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
@@ -227,11 +226,7 @@ class WireManager:
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -259,11 +254,121 @@ class WireManager:
return False
@staticmethod
def add_junction(
schematic_path: Path, position: List[float], diameter: float = 0
def _parse_wire(
wire_item: Any,
) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
"""
Parse a wire S-expression item in a single pass.
Returns ((x1,y1), (x2,y2), stroke_width, stroke_type), or None if not a valid wire.
"""
if not (isinstance(wire_item, list) and len(wire_item) >= 2 and wire_item[0] == _SYM_WIRE):
return None
start = end = None
stroke_width: float = 0
stroke_type: str = "default"
for part in wire_item[1:]:
if not isinstance(part, list) or not part:
continue
tag = part[0]
if tag == _SYM_PTS:
found: List[Tuple[float, float]] = []
for p in part[1:]:
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY:
found.append((float(p[1]), float(p[2])))
if len(found) == 2:
break
if len(found) == 2:
start, end = found[0], found[1]
elif tag == _SYM_STROKE:
for sp in part[1:]:
if isinstance(sp, list) and len(sp) >= 2:
if sp[0] == _SYM_WIDTH:
stroke_width = sp[1]
elif sp[0] == _SYM_TYPE:
stroke_type = str(sp[1])
if start is not None and end is not None:
return start, end, stroke_width, stroke_type
return None
@staticmethod
def _point_strictly_on_wire(
px: float,
py: float,
x1: float,
y1: float,
x2: float,
y2: float,
eps: float = 1e-6,
) -> bool:
"""
Add a junction (connection dot) to the schematic
Return True if (px, py) lies strictly between (x1,y1) and (x2,y2)
on a horizontal or vertical wire segment (not at either endpoint).
"""
if abs(y1 - y2) < eps: # horizontal wire
if abs(py - y1) > eps:
return False
lo, hi = min(x1, x2), max(x1, x2)
return lo + eps < px < hi - eps
if abs(x1 - x2) < eps: # vertical wire
if abs(px - x1) > eps:
return False
lo, hi = min(y1, y2), max(y1, y2)
return lo + eps < py < hi - eps
return False
@staticmethod
def _make_wire_sexp(
start: List[float],
end: List[float],
stroke_width: float = 0,
stroke_type: str = "default",
) -> list:
return [
_SYM_WIRE,
[_SYM_PTS, [_SYM_XY, start[0], start[1]], [_SYM_XY, end[0], end[1]]],
[_SYM_STROKE, [_SYM_WIDTH, stroke_width], [_SYM_TYPE, Symbol(stroke_type)]],
[_SYM_UUID, str(uuid.uuid4())],
]
@staticmethod
def _break_wires_at_point(sch_data: list, position: List[float]) -> int:
"""
Split any wire segment that passes through *position* as a strict
midpoint (i.e. position is not an existing endpoint). Mirrors
KiCAD's SCH_LINE_WIRE_BUS_TOOL::BreakSegments behaviour.
Returns the number of wires split.
"""
px, py = float(position[0]), float(position[1])
splits = 0
i = 0
while i < len(sch_data):
parsed = WireManager._parse_wire(sch_data[i])
if parsed is not None:
(x1, y1), (x2, y2), stroke_width, stroke_type = parsed
if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2):
seg_a = WireManager._make_wire_sexp(
[x1, y1], [px, py], stroke_width, stroke_type
)
seg_b = WireManager._make_wire_sexp(
[px, py], [x2, y2], stroke_width, stroke_type
)
sch_data[i : i + 1] = [seg_a, seg_b]
logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})")
splits += 1
i += 2 # skip the two new segments
continue
i += 1
return splits
@staticmethod
def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
"""
Add a junction (connection dot) to the schematic.
Mirrors KiCAD's AddJunction behaviour: any wire whose interior passes
through *position* is split into two segments at that point so that
the BFS-based get_wire_connections tool can traverse the T/X branch.
Args:
schematic_path: Path to .kicad_sch file
@@ -280,6 +385,12 @@ class WireManager:
sch_data = sexpdata.loads(sch_content)
# Split any wire that passes through the junction as a midpoint
# (mirrors KiCAD's AddJunction / BreakSegments behaviour)
splits = WireManager._break_wires_at_point(sch_data, position)
if splits:
logger.info(f"Broke {splits} wire(s) at junction position {position}")
# Create junction S-expression
# Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...))
junction_sexp = [
@@ -293,11 +404,7 @@ class WireManager:
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -354,11 +461,7 @@ class WireManager:
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -414,21 +517,13 @@ class WireManager:
ex, ey = end_point
for i, item in enumerate(sch_data):
if not (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("wire")
):
if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_WIRE):
continue
# Extract pts from the wire s-expression
pts_list = None
for part in item[1:]:
if (
isinstance(part, list)
and len(part) > 0
and part[0] == Symbol("pts")
):
if isinstance(part, list) and len(part) > 0 and part[0] == _SYM_PTS:
pts_list = part
break
@@ -438,7 +533,7 @@ class WireManager:
xy_points = [
p
for p in pts_list[1:]
if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol("xy")
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY
]
if len(xy_points) < 2:
continue
@@ -502,11 +597,7 @@ class WireManager:
sch_data = sexpdata.loads(sch_content)
for i, item in enumerate(sch_data):
if not (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("label")
):
if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_LABEL):
continue
# Second element is the label text
@@ -519,9 +610,7 @@ class WireManager:
(
p
for p in item[1:]
if isinstance(p, list)
and len(p) >= 3
and p[0] == Symbol("at")
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_AT
),
None,
)
@@ -529,8 +618,7 @@ class WireManager:
continue
lx, ly = float(at_entry[1]), float(at_entry[2])
if not (
abs(lx - position[0]) < tolerance
and abs(ly - position[1]) < tolerance
abs(lx - position[0]) < tolerance and abs(ly - position[1]) < tolerance
):
continue
@@ -584,12 +672,11 @@ class WireManager:
if __name__ == "__main__":
# Test wire creation
import sys
sys.path.insert(0, "/home/chris/MCP/KiCAD-MCP-Server/python")
from pathlib import Path
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
print("=" * 80)
print("WIRE MANAGER TEST")
@@ -597,9 +684,7 @@ if __name__ == "__main__":
# Create test schematic (cross-platform temp directory)
test_path = Path(tempfile.gettempdir()) / "test_wire_manager.kicad_sch"
template_path = Path(
"/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch"
)
template_path = Path(__file__).parent.parent / "templates" / "empty.kicad_sch"
shutil.copy(template_path, test_path)
print(f"\n✓ Created test schematic: {test_path}")

View File

@@ -20,8 +20,8 @@ Usage:
board.set_size(100, 80)
"""
from kicad_api.factory import create_backend
from kicad_api.base import KiCADBackend
from kicad_api.factory import create_backend
__all__ = ['create_backend', 'KiCADBackend']
__version__ = '2.0.0-alpha.1'
__all__ = ["create_backend", "KiCADBackend"]
__version__ = "2.0.0-alpha.1"

View File

@@ -3,10 +3,11 @@ Abstract base class for KiCAD API backends
Defines the interface that all KiCAD backends must implement.
"""
import logging
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, Dict, Any, List
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
@@ -97,7 +98,7 @@ class KiCADBackend(ABC):
# Board Operations
@abstractmethod
def get_board(self) -> 'BoardAPI':
def get_board(self) -> "BoardAPI":
"""
Get board API for current project
@@ -126,7 +127,7 @@ class BoardAPI(ABC):
pass
@abstractmethod
def get_size(self) -> Dict[str, float]:
def get_size(self) -> Dict[str, Any]:
"""
Get current board size
@@ -167,7 +168,8 @@ class BoardAPI(ABC):
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu"
layer: str = "F.Cu",
value: str = "",
) -> bool:
"""
Place a component on the board
@@ -194,7 +196,7 @@ class BoardAPI(ABC):
end_y: float,
width: float = 0.25,
layer: str = "F.Cu",
net_name: Optional[str] = None
net_name: Optional[str] = None,
) -> bool:
"""
Add a track (trace) to the board
@@ -220,7 +222,7 @@ class BoardAPI(ABC):
diameter: float = 0.8,
drill: float = 0.4,
net_name: Optional[str] = None,
via_type: str = "through"
via_type: str = "through",
) -> bool:
"""
Add a via to the board
@@ -275,14 +277,17 @@ class BoardAPI(ABC):
class BackendError(Exception):
"""Base exception for backend errors"""
pass
class ConnectionError(BackendError):
"""Raised when connection to KiCAD fails"""
pass
class APINotAvailableError(BackendError):
"""Raised when required API is not available"""
pass

View File

@@ -3,12 +3,13 @@ Backend factory for creating appropriate KiCAD API backend
Auto-detects available backends and provides fallback mechanism.
"""
import os
import logging
from typing import Optional
from pathlib import Path
from kicad_api.base import KiCADBackend, APINotAvailableError
import logging
import os
from pathlib import Path
from typing import Optional
from kicad_api.base import APINotAvailableError, KiCADBackend
logger = logging.getLogger(__name__)
@@ -34,16 +35,16 @@ def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
"""
# Check environment variable override
if backend_type is None:
backend_type = os.environ.get('KICAD_BACKEND', 'auto').lower()
backend_type = os.environ.get("KICAD_BACKEND", "auto").lower()
logger.info(f"Requested backend: {backend_type}")
# Try specific backend if requested
if backend_type == 'ipc':
if backend_type == "ipc":
return _create_ipc_backend()
elif backend_type == 'swig':
elif backend_type == "swig":
return _create_swig_backend()
elif backend_type == 'auto':
elif backend_type == "auto":
return _auto_detect_backend()
else:
raise ValueError(f"Unknown backend type: {backend_type}")
@@ -61,13 +62,13 @@ def _create_ipc_backend() -> KiCADBackend:
"""
try:
from kicad_api.ipc_backend import IPCBackend
logger.info("Creating IPC backend")
return IPCBackend()
except ImportError as e:
logger.error(f"IPC backend not available: {e}")
raise APINotAvailableError(
"IPC backend requires 'kicad-python' package. "
"Install with: pip install kicad-python"
"IPC backend requires 'kicad-python' package. " "Install with: pip install kicad-python"
) from e
@@ -83,6 +84,7 @@ def _create_swig_backend() -> KiCADBackend:
"""
try:
from kicad_api.swig_backend import SWIGBackend
logger.info("Creating SWIG backend")
logger.warning(
"SWIG backend is DEPRECATED and will be removed in KiCAD 10.0. "
@@ -92,8 +94,7 @@ def _create_swig_backend() -> KiCADBackend:
except ImportError as e:
logger.error(f"SWIG backend not available: {e}")
raise APINotAvailableError(
"SWIG backend requires 'pcbnew' module. "
"Ensure KiCAD Python module is in PYTHONPATH."
"SWIG backend requires 'pcbnew' module. " "Ensure KiCAD Python module is in PYTHONPATH."
) from e
@@ -129,8 +130,7 @@ def _auto_detect_backend() -> KiCADBackend:
try:
backend = _create_swig_backend()
logger.warning(
"Using deprecated SWIG backend. "
"For best results, use IPC API with KiCAD running."
"Using deprecated SWIG backend. " "For best results, use IPC API with KiCAD running."
)
return backend
except (ImportError, APINotAvailableError) as e:
@@ -160,22 +160,18 @@ def get_available_backends() -> dict:
# Check IPC (kicad-python uses 'kipy' module name)
try:
import kipy
results['ipc'] = {
'available': True,
'version': getattr(kipy, '__version__', 'unknown')
}
results["ipc"] = {"available": True, "version": getattr(kipy, "__version__", "unknown")}
except ImportError:
results['ipc'] = {'available': False, 'version': None}
results["ipc"] = {"available": False, "version": None}
# Check SWIG
try:
import pcbnew
results['swig'] = {
'available': True,
'version': pcbnew.GetBuildVersion()
}
results["swig"] = {"available": True, "version": pcbnew.GetBuildVersion()}
except ImportError:
results['swig'] = {'available': False, 'version': None}
results["swig"] = {"available": False, "version": None}
return results
@@ -183,6 +179,7 @@ def get_available_backends() -> dict:
if __name__ == "__main__":
# Quick diagnostic
import json
print("KiCAD Backend Availability:")
print(json.dumps(get_available_backends(), indent=2))

View File

@@ -13,18 +13,14 @@ Key Benefits over SWIG:
- Stable API that won't break between versions
- Multi-language support
"""
import logging
import os
import platform
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable
from typing import Any, Callable, Dict, List, Optional
from kicad_api.base import (
KiCADBackend,
BoardAPI,
ConnectionError,
APINotAvailableError
)
from kicad_api.base import APINotAvailableError, BoardAPI, ConnectionError, KiCADBackend
logger = logging.getLogger(__name__)
@@ -44,10 +40,10 @@ class IPCBackend(KiCADBackend):
without requiring manual reload.
"""
def __init__(self):
def __init__(self) -> None:
self._kicad = None
self._connected = False
self._version = None
self._version: Optional[str] = None
self._on_change_callbacks: List[Callable] = []
def connect(self, socket_path: Optional[str] = None) -> bool:
@@ -78,10 +74,10 @@ class IPCBackend(KiCADBackend):
# Common socket locations (Unix-like systems only)
# Windows uses named pipes, handled by auto-detect
if platform.system() != "Windows":
socket_paths_to_try.append('ipc:///tmp/kicad/api.sock') # Linux default
socket_paths_to_try.append("ipc:///tmp/kicad/api.sock") # Linux default
# XDG runtime directory (requires getuid, Unix only)
if hasattr(os, 'getuid'):
socket_paths_to_try.append(f'ipc:///run/user/{os.getuid()}/kicad/api.sock')
if hasattr(os, "getuid"):
socket_paths_to_try.append(f"ipc:///run/user/{os.getuid()}/kicad/api.sock")
# Auto-detect for all platforms (Windows uses named pipes, Unix uses sockets)
socket_paths_to_try.append(None)
@@ -117,8 +113,7 @@ class IPCBackend(KiCADBackend):
except ImportError as e:
logger.error("kicad-python library not found")
raise APINotAvailableError(
"IPC backend requires kicad-python. "
"Install with: pip install kicad-python"
"IPC backend requires kicad-python. " "Install with: pip install kicad-python"
) from e
except Exception as e:
logger.error(f"Failed to connect via IPC: {e}")
@@ -190,7 +185,7 @@ class IPCBackend(KiCADBackend):
return {
"success": False,
"message": "Direct project creation not supported via IPC",
"suggestion": "Open KiCAD and create a new project, or use SWIG backend"
"suggestion": "Open KiCAD and create a new project, or use SWIG backend",
}
def open_project(self, path: Path) -> Dict[str, Any]:
@@ -209,22 +204,18 @@ class IPCBackend(KiCADBackend):
return {
"success": True,
"message": f"Project already open: {path}",
"path": str(path)
"path": str(path),
}
return {
"success": False,
"message": "Project not currently open in KiCAD",
"suggestion": "Open the project in KiCAD first, then connect via IPC"
"suggestion": "Open the project in KiCAD first, then connect via IPC",
}
except Exception as e:
logger.error(f"Failed to check project: {e}")
return {
"success": False,
"message": "Failed to check project",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to check project", "errorDetails": str(e)}
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
"""Save current project via IPC."""
@@ -240,17 +231,10 @@ class IPCBackend(KiCADBackend):
self._notify_change("save", {"path": str(path) if path else "current"})
return {
"success": True,
"message": "Project saved successfully"
}
return {"success": True, "message": "Project saved successfully"}
except Exception as e:
logger.error(f"Failed to save project: {e}")
return {
"success": False,
"message": "Failed to save project",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to save project", "errorDetails": str(e)}
def close_project(self) -> None:
"""Close current project (not supported via IPC)."""
@@ -273,13 +257,13 @@ class IPCBoardAPI(BoardAPI):
Uses transactions for proper undo/redo support.
"""
def __init__(self, kicad_instance, notify_callback: Callable):
def __init__(self, kicad_instance: Any, notify_callback: Callable) -> None:
self._kicad = kicad_instance
self._board = None
self._notify = notify_callback
self._current_commit = None
def _get_board(self):
def _get_board(self) -> Any:
"""Get board instance, connecting if needed."""
if self._board is None:
try:
@@ -333,8 +317,8 @@ class IPCBoardAPI(BoardAPI):
try:
from kipy.board_types import BoardRectangle
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.units import from_mm
board = self._get_board()
@@ -366,7 +350,7 @@ class IPCBoardAPI(BoardAPI):
logger.error(f"Failed to set board size: {e}")
return False
def get_size(self) -> Dict[str, float]:
def get_size(self) -> Dict[str, Any]:
"""Get current board size from bounding box."""
try:
board = self._get_board()
@@ -380,8 +364,8 @@ class IPCBoardAPI(BoardAPI):
# Find bounding box of edge cuts
from kipy.util.units import to_mm
min_x = min_y = float('inf')
max_x = max_y = float('-inf')
min_x = min_y = float("inf")
max_x = max_y = float("-inf")
for shape in shapes:
# Check if on Edge.Cuts layer
@@ -392,14 +376,10 @@ class IPCBoardAPI(BoardAPI):
max_x = max(max_x, bbox.max.x)
max_y = max(max_y, bbox.max.y)
if min_x == float('inf'):
if min_x == float("inf"):
return {"width": 0, "height": 0, "unit": "mm"}
return {
"width": to_mm(max_x - min_x),
"height": to_mm(max_y - min_y),
"unit": "mm"
}
return {"width": to_mm(max_x - min_x), "height": to_mm(max_y - min_y), "unit": "mm"}
except Exception as e:
logger.error(f"Failed to get board size: {e}")
@@ -432,19 +412,23 @@ class IPCBoardAPI(BoardAPI):
for fp in footprints:
try:
pos = fp.position
components.append({
"reference": fp.reference_field.text.value if fp.reference_field else "",
"value": fp.value_field.text.value if fp.value_field else "",
"footprint": str(fp.definition.library_link) if fp.definition else "",
"position": {
"x": to_mm(pos.x) if pos else 0,
"y": to_mm(pos.y) if pos else 0,
"unit": "mm"
},
"rotation": fp.orientation.degrees if fp.orientation else 0,
"layer": str(fp.layer) if hasattr(fp, 'layer') else "F.Cu",
"id": str(fp.id) if hasattr(fp, 'id') else ""
})
components.append(
{
"reference": (
fp.reference_field.text.value if fp.reference_field else ""
),
"value": fp.value_field.text.value if fp.value_field else "",
"footprint": str(fp.definition.library_link) if fp.definition else "",
"position": {
"x": to_mm(pos.x) if pos else 0,
"y": to_mm(pos.y) if pos else 0,
"unit": "mm",
},
"rotation": fp.orientation.degrees if fp.orientation else 0,
"layer": str(fp.layer) if hasattr(fp, "layer") else "F.Cu",
"id": str(fp.id) if hasattr(fp, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing footprint: {e}")
continue
@@ -463,7 +447,7 @@ class IPCBoardAPI(BoardAPI):
y: float,
rotation: float = 0,
layer: str = "F.Cu",
value: str = ""
value: str = "",
) -> bool:
"""
Place a component on the board.
@@ -495,7 +479,9 @@ class IPCBoardAPI(BoardAPI):
)
else:
# Fallback: Create a basic placeholder footprint via IPC
logger.warning(f"Could not load footprint '{footprint}' from library, creating placeholder")
logger.warning(
f"Could not load footprint '{footprint}' from library, creating placeholder"
)
return self._place_placeholder_footprint(
reference, footprint, x, y, rotation, layer, value
)
@@ -504,7 +490,7 @@ class IPCBoardAPI(BoardAPI):
logger.error(f"Failed to place component: {e}")
return False
def _load_footprint_from_library(self, footprint_path: str):
def _load_footprint_from_library(self, footprint_path: str) -> Any:
"""
Load a footprint from the library using pcbnew SWIG API.
@@ -518,8 +504,8 @@ class IPCBoardAPI(BoardAPI):
import pcbnew
# Parse library and footprint name
if ':' in footprint_path:
lib_name, fp_name = footprint_path.split(':', 1)
if ":" in footprint_path:
lib_name, fp_name = footprint_path.split(":", 1)
else:
# Try to find the footprint in all libraries
lib_name = None
@@ -561,13 +547,13 @@ class IPCBoardAPI(BoardAPI):
def _place_loaded_footprint(
self,
loaded_fp,
loaded_fp: Any,
reference: str,
x: float,
y: float,
rotation: float,
layer: str,
value: str
value: str,
) -> bool:
"""
Place a loaded pcbnew footprint onto the board.
@@ -589,7 +575,7 @@ class IPCBoardAPI(BoardAPI):
try:
docs = self._kicad.get_open_documents()
for doc in docs:
if hasattr(doc, 'path') and str(doc.path).endswith('.kicad_pcb'):
if hasattr(doc, "path") and str(doc.path).endswith(".kicad_pcb"):
board_path = str(doc.path)
break
except Exception as e:
@@ -637,24 +623,27 @@ class IPCBoardAPI(BoardAPI):
except Exception as e:
logger.debug(f"Could not refresh IPC board: {e}")
self._notify("component_placed", {
"reference": reference,
"footprint": loaded_fp.GetFPIDAsString(),
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": True
})
self._notify(
"component_placed",
{
"reference": reference,
"footprint": loaded_fp.GetFPIDAsString(),
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": True,
},
)
logger.info(f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm")
logger.info(
f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm"
)
return True
except Exception as e:
logger.error(f"Error placing loaded footprint: {e}")
# Fall back to placeholder
return self._place_placeholder_footprint(
reference, "", x, y, rotation, layer, value
)
return self._place_placeholder_footprint(reference, "", x, y, rotation, layer, value)
def _place_placeholder_footprint(
self,
@@ -664,7 +653,7 @@ class IPCBoardAPI(BoardAPI):
y: float,
rotation: float,
layer: str,
value: str
value: str,
) -> bool:
"""
Place a placeholder footprint when library loading fails.
@@ -673,9 +662,9 @@ class IPCBoardAPI(BoardAPI):
"""
try:
from kipy.board_types import Footprint
from kipy.geometry import Vector2, Angle
from kipy.util.units import from_mm
from kipy.geometry import Angle, Vector2
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.units import from_mm
board = self._get_board()
@@ -701,15 +690,18 @@ class IPCBoardAPI(BoardAPI):
board.create_items(fp)
board.push_commit(commit, f"Placed component {reference}")
self._notify("component_placed", {
"reference": reference,
"footprint": footprint,
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": False,
"is_placeholder": True
})
self._notify(
"component_placed",
{
"reference": reference,
"footprint": footprint,
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": False,
"is_placeholder": True,
},
)
logger.info(f"Placed placeholder component {reference} at ({x}, {y}) mm")
return True
@@ -718,10 +710,12 @@ class IPCBoardAPI(BoardAPI):
logger.error(f"Failed to place placeholder component: {e}")
return False
def move_component(self, reference: str, x: float, y: float, rotation: Optional[float] = None) -> bool:
def move_component(
self, reference: str, x: float, y: float, rotation: Optional[float] = None
) -> bool:
"""Move a component to a new position (updates UI immediately)."""
try:
from kipy.geometry import Vector2, Angle
from kipy.geometry import Angle, Vector2
from kipy.util.units import from_mm
board = self._get_board()
@@ -749,11 +743,10 @@ class IPCBoardAPI(BoardAPI):
board.update_items([target_fp])
board.push_commit(commit, f"Moved component {reference}")
self._notify("component_moved", {
"reference": reference,
"position": {"x": x, "y": y},
"rotation": rotation
})
self._notify(
"component_moved",
{"reference": reference, "position": {"x": x, "y": y}, "rotation": rotation},
)
return True
@@ -799,7 +792,7 @@ class IPCBoardAPI(BoardAPI):
end_y: float,
width: float = 0.25,
layer: str = "F.Cu",
net_name: Optional[str] = None
net_name: Optional[str] = None,
) -> bool:
"""
Add a track (trace) to the board.
@@ -809,8 +802,8 @@ class IPCBoardAPI(BoardAPI):
try:
from kipy.board_types import Track
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.units import from_mm
board = self._get_board()
@@ -842,13 +835,16 @@ class IPCBoardAPI(BoardAPI):
board.create_items(track)
board.push_commit(commit, "Added track")
self._notify("track_added", {
"start": {"x": start_x, "y": start_y},
"end": {"x": end_x, "y": end_y},
"width": width,
"layer": layer,
"net": net_name
})
self._notify(
"track_added",
{
"start": {"x": start_x, "y": start_y},
"end": {"x": end_x, "y": end_y},
"width": width,
"layer": layer,
"net": net_name,
},
)
logger.info(f"Added track from ({start_x}, {start_y}) to ({end_x}, {end_y}) mm")
return True
@@ -864,7 +860,7 @@ class IPCBoardAPI(BoardAPI):
diameter: float = 0.8,
drill: float = 0.4,
net_name: Optional[str] = None,
via_type: str = "through"
via_type: str = "through",
) -> bool:
"""
Add a via to the board.
@@ -874,8 +870,8 @@ class IPCBoardAPI(BoardAPI):
try:
from kipy.board_types import Via
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import ViaType
from kipy.util.units import from_mm
board = self._get_board()
@@ -906,13 +902,16 @@ class IPCBoardAPI(BoardAPI):
board.create_items(via)
board.push_commit(commit, "Added via")
self._notify("via_added", {
"position": {"x": x, "y": y},
"diameter": diameter,
"drill": drill,
"net": net_name,
"type": via_type
})
self._notify(
"via_added",
{
"position": {"x": x, "y": y},
"diameter": diameter,
"drill": drill,
"net": net_name,
"type": via_type,
},
)
logger.info(f"Added via at ({x}, {y}) mm")
return True
@@ -928,14 +927,14 @@ class IPCBoardAPI(BoardAPI):
y: float,
layer: str = "F.SilkS",
size: float = 1.0,
rotation: float = 0
rotation: float = 0,
) -> bool:
"""Add text to the board."""
try:
from kipy.board_types import BoardText
from kipy.geometry import Vector2, Angle
from kipy.util.units import from_mm
from kipy.geometry import Angle, Vector2
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.units import from_mm
board = self._get_board()
@@ -959,11 +958,7 @@ class IPCBoardAPI(BoardAPI):
board.create_items(board_text)
board.push_commit(commit, f"Added text: {text}")
self._notify("text_added", {
"text": text,
"position": {"x": x, "y": y},
"layer": layer
})
self._notify("text_added", {"text": text, "position": {"x": x, "y": y}, "layer": layer})
return True
@@ -982,20 +977,16 @@ class IPCBoardAPI(BoardAPI):
result = []
for track in tracks:
try:
result.append({
"start": {
"x": to_mm(track.start.x),
"y": to_mm(track.start.y)
},
"end": {
"x": to_mm(track.end.x),
"y": to_mm(track.end.y)
},
"width": to_mm(track.width),
"layer": str(track.layer),
"net": track.net.name if track.net else "",
"id": str(track.id) if hasattr(track, 'id') else ""
})
result.append(
{
"start": {"x": to_mm(track.start.x), "y": to_mm(track.start.y)},
"end": {"x": to_mm(track.end.x), "y": to_mm(track.end.y)},
"width": to_mm(track.width),
"layer": str(track.layer),
"net": track.net.name if track.net else "",
"id": str(track.id) if hasattr(track, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing track: {e}")
continue
@@ -1017,17 +1008,16 @@ class IPCBoardAPI(BoardAPI):
result = []
for via in vias:
try:
result.append({
"position": {
"x": to_mm(via.position.x),
"y": to_mm(via.position.y)
},
"diameter": to_mm(via.diameter),
"drill": to_mm(via.drill_diameter),
"net": via.net.name if via.net else "",
"type": str(via.type),
"id": str(via.id) if hasattr(via, 'id') else ""
})
result.append(
{
"position": {"x": to_mm(via.position.x), "y": to_mm(via.position.y)},
"diameter": to_mm(via.diameter),
"drill": to_mm(via.drill_diameter),
"net": via.net.name if via.net else "",
"type": str(via.type),
"id": str(via.id) if hasattr(via, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing via: {e}")
continue
@@ -1047,10 +1037,9 @@ class IPCBoardAPI(BoardAPI):
result = []
for net in nets:
try:
result.append({
"name": net.name,
"code": net.code if hasattr(net, 'code') else 0
})
result.append(
{"name": net.name, "code": net.code if hasattr(net, "code") else 0}
)
except Exception as e:
logger.warning(f"Error processing net: {e}")
continue
@@ -1070,7 +1059,7 @@ class IPCBoardAPI(BoardAPI):
min_thickness: float = 0.25,
priority: int = 0,
fill_mode: str = "solid",
name: str = ""
name: str = "",
) -> bool:
"""
Add a copper pour zone to the board.
@@ -1090,8 +1079,8 @@ class IPCBoardAPI(BoardAPI):
try:
from kipy.board_types import Zone, ZoneFillMode, ZoneType
from kipy.geometry import PolyLine, PolyLineNode, Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.units import from_mm
board = self._get_board()
@@ -1157,12 +1146,10 @@ class IPCBoardAPI(BoardAPI):
board.create_items(zone)
board.push_commit(commit, f"Added copper zone on {layer}")
self._notify("zone_added", {
"layer": layer,
"net": net_name,
"points": len(points),
"priority": priority
})
self._notify(
"zone_added",
{"layer": layer, "net": net_name, "points": len(points), "priority": priority},
)
logger.info(f"Added zone on {layer} with {len(points)} points")
return True
@@ -1182,14 +1169,18 @@ class IPCBoardAPI(BoardAPI):
result = []
for zone in zones:
try:
result.append({
"name": zone.name if hasattr(zone, 'name') else "",
"net": zone.net.name if zone.net else "",
"priority": zone.priority if hasattr(zone, 'priority') else 0,
"layers": [str(l) for l in zone.layers] if hasattr(zone, 'layers') else [],
"filled": zone.filled if hasattr(zone, 'filled') else False,
"id": str(zone.id) if hasattr(zone, 'id') else ""
})
result.append(
{
"name": zone.name if hasattr(zone, "name") else "",
"net": zone.net.name if zone.net else "",
"priority": zone.priority if hasattr(zone, "priority") else 0,
"layers": (
[str(l) for l in zone.layers] if hasattr(zone, "layers") else []
),
"filled": zone.filled if hasattr(zone, "filled") else False,
"id": str(zone.id) if hasattr(zone, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing zone: {e}")
continue
@@ -1219,10 +1210,9 @@ class IPCBoardAPI(BoardAPI):
result = []
for item in selection:
result.append({
"type": type(item).__name__,
"id": str(item.id) if hasattr(item, 'id') else ""
})
result.append(
{"type": type(item).__name__, "id": str(item.id) if hasattr(item, "id") else ""}
)
return result
except Exception as e:
@@ -1241,4 +1231,4 @@ class IPCBoardAPI(BoardAPI):
# Export for factory
__all__ = ['IPCBackend', 'IPCBoardAPI']
__all__ = ["IPCBackend", "IPCBoardAPI"]

View File

@@ -8,16 +8,12 @@ WARNING: SWIG bindings are deprecated as of KiCAD 9.0
and will be removed in KiCAD 10.0.
Please migrate to IPC backend.
"""
import logging
from pathlib import Path
from typing import Optional, Dict, Any, List
from typing import Any, Dict, List, Optional
from kicad_api.base import (
KiCADBackend,
BoardAPI,
ConnectionError,
APINotAvailableError
)
from kicad_api.base import APINotAvailableError, BoardAPI, ConnectionError, KiCADBackend
logger = logging.getLogger(__name__)
@@ -30,7 +26,7 @@ class SWIGBackend(KiCADBackend):
for compatibility during migration period.
"""
def __init__(self):
def __init__(self) -> None:
self._connected = False
self._pcbnew = None
logger.warning(
@@ -48,6 +44,7 @@ class SWIGBackend(KiCADBackend):
"""
try:
import pcbnew
self._pcbnew = pcbnew
version = pcbnew.GetBuildVersion()
logger.info(f"✓ Connected to pcbnew (SWIG): {version}")
@@ -101,7 +98,7 @@ class SWIGBackend(KiCADBackend):
from commands.project import ProjectCommands
try:
result = ProjectCommands.open_project(str(path))
result = ProjectCommands().open_project({"filename": str(path)})
return result
except Exception as e:
logger.error(f"Failed to open project: {e}")
@@ -115,8 +112,10 @@ class SWIGBackend(KiCADBackend):
from commands.project import ProjectCommands
try:
path_str = str(path) if path else None
result = ProjectCommands.save_project(path_str)
params: Dict[str, Any] = {}
if path:
params["filename"] = str(path)
result = ProjectCommands().save_project(params)
return result
except Exception as e:
logger.error(f"Failed to save project: {e}")
@@ -140,7 +139,7 @@ class SWIGBackend(KiCADBackend):
class SWIGBoardAPI(BoardAPI):
"""Board API implementation wrapping SWIG/pcbnew"""
def __init__(self, pcbnew_module):
def __init__(self, pcbnew_module: Any) -> None:
self.pcbnew = pcbnew_module
self._board = None
@@ -149,13 +148,15 @@ class SWIGBoardAPI(BoardAPI):
from commands.board import BoardCommands
try:
result = BoardCommands.set_board_size(width, height, unit)
result = BoardCommands(board=self._board).set_board_size(
{"width": width, "height": height, "unit": unit}
)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to set board size: {e}")
return False
def get_size(self) -> Dict[str, float]:
def get_size(self) -> Dict[str, Any]:
"""Get board size"""
# TODO: Implement using existing SWIG code
raise NotImplementedError("get_size not yet wrapped")
@@ -176,7 +177,7 @@ class SWIGBoardAPI(BoardAPI):
from commands.component import ComponentCommands
try:
result = ComponentCommands.get_component_list()
result = ComponentCommands(board=self._board).get_component_list({})
if result.get("success"):
return result.get("components", [])
return []
@@ -191,18 +192,21 @@ class SWIGBoardAPI(BoardAPI):
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu"
layer: str = "F.Cu",
value: str = "",
) -> bool:
"""Place component using existing implementation"""
from commands.component import ComponentCommands
try:
result = ComponentCommands.place_component(
component_id=footprint,
position={"x": x, "y": y, "unit": "mm"},
reference=reference,
rotation=rotation,
layer=layer
result = ComponentCommands(board=self._board).place_component(
{
"componentId": footprint,
"position": {"x": x, "y": y, "unit": "mm"},
"reference": reference,
"rotation": rotation,
"layer": layer,
}
)
return result.get("success", False)
except Exception as e:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
# parsers package

View File

@@ -0,0 +1,250 @@
"""
Parser for KiCad .kicad_mod footprint files.
Extracts the fields that the MCP get_footprint_info tool exposes to clients:
name footprint name (str)
library library nickname, injected by caller (str)
description (descr "") token (str | None)
keywords (tags "") token (str | None)
pads list of pad objects: [{number, type, shape}, …] (list[dict])
layers sorted unique list of canonical layer names used (list[str])
courtyard {"width": float, "height": float} from F.CrtYd geometry (dict | None)
attributes {"type": str, "board_only": bool, …} (dict | None)
KiCad S-expression file format reference:
https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_footprint
"""
import logging
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger("kicad_interface")
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def parse_kicad_mod(file_path: str) -> Optional[Dict[str, Any]]:
"""
Parse a .kicad_mod file and return a dict whose keys match the fields
expected by the TypeScript MCP tool handler (src/tools/library.ts).
Returns None if the file does not exist or cannot be read.
"""
path = Path(file_path)
if not path.exists():
logger.debug(f"parse_kicad_mod: file not found: {file_path}")
return None
try:
content = path.read_text(encoding="utf-8")
except OSError as e:
logger.warning(f"parse_kicad_mod: cannot read {file_path}: {e}")
return None
logger.debug(f"parse_kicad_mod: parsing {path.name} ({len(content)} chars)")
result: Dict[str, Any] = {}
# ------------------------------------------------------------------
# Footprint name: (footprint "NAME" …
# Per spec, in a library file the name is the ENTRY_NAME only (no lib prefix).
# ------------------------------------------------------------------
m = re.search(r'^\s*\(footprint\s+"((?:[^"\\]|\\.)*)"', content, re.MULTILINE)
if not m:
# Older / unquoted format
m = re.search(r"^\s*\(footprint\s+(\S+)", content, re.MULTILINE)
result["name"] = _unescape(m.group(1)) if m else path.stem
logger.debug(f"parse_kicad_mod: name={result['name']!r}")
# ------------------------------------------------------------------
# Description: (descr "…")
# ------------------------------------------------------------------
m = re.search(r'\(descr\s+"((?:[^"\\]|\\.)*)"\)', content)
result["description"] = _unescape(m.group(1)) if m else None
logger.debug(f"parse_kicad_mod: description={result['description']!r}")
# ------------------------------------------------------------------
# Keywords / tags: (tags "…")
# ------------------------------------------------------------------
m = re.search(r'\(tags\s+"((?:[^"\\]|\\.)*)"\)', content)
result["keywords"] = _unescape(m.group(1)) if m else None
logger.debug(f"parse_kicad_mod: keywords={result['keywords']!r}")
# ------------------------------------------------------------------
# Attributes: (attr TYPE [board_only] [exclude_from_pos_files] [exclude_from_bom])
# TYPE is smd | through_hole (no quotes)
# ------------------------------------------------------------------
m = re.search(r"\(attr\s+([^)]+)\)", content)
if m:
tokens = m.group(1).split()
result["attributes"] = {
"type": tokens[0] if tokens else "unspecified",
"board_only": "board_only" in tokens,
"exclude_from_pos_files": "exclude_from_pos_files" in tokens,
"exclude_from_bom": "exclude_from_bom" in tokens,
}
else:
result["attributes"] = None
logger.debug(f"parse_kicad_mod: attributes={result['attributes']!r}")
# ------------------------------------------------------------------
# Pads: (pad "NUMBER" TYPE SHAPE …)
# Return each pad as an object; deduplicate by number (first wins).
# ------------------------------------------------------------------
result["pads"] = _extract_pads(content)
logger.debug(f"parse_kicad_mod: pads count={len(result['pads'])}, pads={result['pads']}")
# ------------------------------------------------------------------
# Layers: all unique canonical layer names across the whole file.
# Sources:
# (layer "NAME") single-layer items (fp_line, fp_text, …)
# (layers "A" "B" …) pad layer lists
# ------------------------------------------------------------------
layers: set = set()
for m in re.finditer(r'\(layer\s+"([^"]+)"\)', content):
layers.add(m.group(1))
for m in re.finditer(r"\(layers\s+([^)]+)\)", content):
for lyr in re.findall(r'"([^"]+)"', m.group(1)):
layers.add(lyr)
result["layers"] = sorted(layers)
logger.debug(f"parse_kicad_mod: layers={result['layers']}")
# ------------------------------------------------------------------
# Courtyard: derive bounding box from F.CrtYd geometry.
# Prefer fp_rect (most common for standard footprints), fall back to
# fp_line segments.
# ------------------------------------------------------------------
result["courtyard"] = _extract_courtyard(content)
logger.debug(f"parse_kicad_mod: courtyard={result['courtyard']!r}")
return result
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _extract_pads(content: str) -> List[Dict[str, Any]]:
"""
Parse all (pad …) blocks and return a list of pad objects.
Each object has:
number pad number string, e.g. "1", "A1", "GND"
type thru_hole | smd | np_thru_hole | connect
shape rect | circle | oval | roundrect | trapezoid | custom
Pads are deduplicated by number (first occurrence wins) so that the
list represents the logical pads of the footprint, not duplicated
copper entries.
"""
pads: List[Dict[str, Any]] = []
seen_numbers: dict = {}
# KiCad 6+ quoted format: (pad "NUMBER" TYPE SHAPE …)
quoted_pattern = re.compile(
r'\(pad\s+"([^"]*)"\s+'
r"(thru_hole|smd|np_thru_hole|connect)\s+"
r"(rect|circle|oval|roundrect|trapezoid|custom)\b"
)
for m in quoted_pattern.finditer(content):
number, ptype, shape = m.group(1), m.group(2), m.group(3)
if number not in seen_numbers:
seen_numbers[number] = True
pads.append({"number": number, "type": ptype, "shape": shape})
if not pads:
# Older / unquoted format: (pad NUMBER TYPE SHAPE …)
unquoted_pattern = re.compile(
r"\(pad\s+(\S+)\s+"
r"(thru_hole|smd|np_thru_hole|connect)\s+"
r"(rect|circle|oval|roundrect|trapezoid|custom)\b"
)
for m in unquoted_pattern.finditer(content):
number, ptype, shape = m.group(1), m.group(2), m.group(3)
if number not in seen_numbers:
seen_numbers[number] = True
pads.append({"number": number, "type": ptype, "shape": shape})
return pads
def _unescape(s: str) -> str:
"""Reverse KiCad S-expression string escaping."""
return s.replace('\\"', '"').replace("\\\\", "\\")
def _extract_blocks(content: str, token: str) -> List[str]:
"""
Return all S-expression blocks that start with `(token ` by tracking
parenthesis depth. This correctly handles nested parens inside blocks.
"""
blocks: List[str] = []
pattern = re.compile(r"\(" + re.escape(token) + r"\b")
for match in pattern.finditer(content):
start = match.start()
depth = 0
i = start
while i < len(content):
ch = content[i]
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
blocks.append(content[start : i + 1])
break
i += 1
return blocks
def _extract_courtyard(content: str) -> Optional[Dict[str, float]]:
"""
Compute the courtyard bounding box from F.CrtYd geometry.
Strategy:
1. Try fp_rect blocks on F.CrtYd — derive width/height from start/end.
2. Fall back to fp_line segments on F.CrtYd — compute bounding box of
all endpoints.
"""
xs: List[float] = []
ys: List[float] = []
# --- fp_rect pass ---
for block in _extract_blocks(content, "fp_rect"):
if "F.CrtYd" not in block:
continue
s = re.search(r"\(start\s+([-\d.]+)\s+([-\d.]+)\)", block)
e = re.search(r"\(end\s+([-\d.]+)\s+([-\d.]+)\)", block)
if s and e:
xs += [float(s.group(1)), float(e.group(1))]
ys += [float(s.group(2)), float(e.group(2))]
logger.debug(
f"_extract_courtyard: fp_rect F.CrtYd "
f"start=({s.group(1)},{s.group(2)}) end=({e.group(1)},{e.group(2)})"
)
# --- fp_line pass (only if fp_rect found nothing) ---
if not xs:
for block in _extract_blocks(content, "fp_line"):
if "F.CrtYd" not in block:
continue
for m in re.finditer(r"\((?:start|end)\s+([-\d.]+)\s+([-\d.]+)\)", block):
xs.append(float(m.group(1)))
ys.append(float(m.group(2)))
if not xs:
logger.debug("_extract_courtyard: no F.CrtYd geometry found")
return None
width = round(abs(max(xs) - min(xs)), 6)
height = round(abs(max(ys) - min(ys)), 6)
logger.debug(f"_extract_courtyard: result width={width} height={height}")
return {"width": width, "height": height}

View File

@@ -4,4 +4,4 @@ Resource definitions for KiCAD MCP Server
from .resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
__all__ = ['RESOURCE_DEFINITIONS', 'handle_resource_read']
__all__ = ["RESOURCE_DEFINITIONS", "handle_resource_read"]

View File

@@ -5,12 +5,12 @@ Resources follow the MCP 2025-06-18 specification, providing
read-only access to project data for LLM context.
"""
import json
import base64
from typing import Dict, Any, List, Optional
import json
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger('kicad_interface')
logger = logging.getLogger("kicad_interface")
# =============================================================================
# RESOURCE DEFINITIONS
@@ -21,57 +21,58 @@ RESOURCE_DEFINITIONS = [
"uri": "kicad://project/current/info",
"name": "Current Project Information",
"description": "Metadata about the currently open KiCAD project including paths, name, and status",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/board",
"name": "Board Properties",
"description": "Comprehensive board information including dimensions, layer count, and design rules",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/components",
"name": "Component List",
"description": "List of all components on the board with references, footprints, values, and positions",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/nets",
"name": "Electrical Nets",
"description": "List of all electrical nets and their connections",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/layers",
"name": "Layer Stack",
"description": "Board layer configuration and properties",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/design-rules",
"name": "Design Rules",
"description": "Current design rule settings for clearances, track widths, and constraints",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/drc-report",
"name": "DRC Violations",
"description": "Design Rule Check violations and warnings from last DRC run",
"mimeType": "application/json"
"mimeType": "application/json",
},
{
"uri": "kicad://board/preview.png",
"name": "Board Preview Image",
"description": "2D rendering of the current board state",
"mimeType": "image/png"
}
"mimeType": "image/png",
},
]
# =============================================================================
# RESOURCE READ HANDLERS
# =============================================================================
def handle_resource_read(uri: str, interface) -> Dict[str, Any]:
def handle_resource_read(uri: str, interface: Any) -> Dict[str, Any]:
"""
Handle reading a resource by URI
@@ -92,7 +93,7 @@ def handle_resource_read(uri: str, interface) -> Dict[str, Any]:
"kicad://project/current/layers": _get_layers,
"kicad://project/current/design-rules": _get_design_rules,
"kicad://project/current/drc-report": _get_drc_report,
"kicad://board/preview.png": _get_board_preview
"kicad://board/preview.png": _get_board_preview,
}
handler = handlers.get(uri)
@@ -102,193 +103,217 @@ def handle_resource_read(uri: str, interface) -> Dict[str, Any]:
except Exception as e:
logger.error(f"Error reading resource {uri}: {str(e)}")
return {
"contents": [{
"uri": uri,
"mimeType": "text/plain",
"text": f"Error: {str(e)}"
}]
"contents": [{"uri": uri, "mimeType": "text/plain", "text": f"Error: {str(e)}"}]
}
else:
return {
"contents": [{
"uri": uri,
"mimeType": "text/plain",
"text": f"Unknown resource: {uri}"
}]
"contents": [{"uri": uri, "mimeType": "text/plain", "text": f"Unknown resource: {uri}"}]
}
# =============================================================================
# INDIVIDUAL RESOURCE HANDLERS
# =============================================================================
def _get_project_info(interface) -> Dict[str, Any]:
def _get_project_info(interface: Any) -> Dict[str, Any]:
"""Get current project information"""
result = interface.project_commands.get_project_info({})
if result.get("success"):
return {
"contents": [{
"uri": "kicad://project/current/info",
"mimeType": "application/json",
"text": json.dumps(result.get("project", {}), indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/info",
"mimeType": "application/json",
"text": json.dumps(result.get("project", {}), indent=2),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/info",
"mimeType": "text/plain",
"text": "No project currently open"
}]
"contents": [
{
"uri": "kicad://project/current/info",
"mimeType": "text/plain",
"text": "No project currently open",
}
]
}
def _get_board_info(interface) -> Dict[str, Any]:
def _get_board_info(interface: Any) -> Dict[str, Any]:
"""Get board properties and metadata"""
result = interface.board_commands.get_board_info({})
if result.get("success"):
return {
"contents": [{
"uri": "kicad://project/current/board",
"mimeType": "application/json",
"text": json.dumps(result.get("board", {}), indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/board",
"mimeType": "application/json",
"text": json.dumps(result.get("board", {}), indent=2),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/board",
"mimeType": "text/plain",
"text": "No board currently loaded"
}]
"contents": [
{
"uri": "kicad://project/current/board",
"mimeType": "text/plain",
"text": "No board currently loaded",
}
]
}
def _get_components(interface) -> Dict[str, Any]:
def _get_components(interface: Any) -> Dict[str, Any]:
"""Get list of all components"""
result = interface.component_commands.get_component_list({})
if result.get("success"):
components = result.get("components", [])
return {
"contents": [{
"uri": "kicad://project/current/components",
"mimeType": "application/json",
"text": json.dumps({
"count": len(components),
"components": components
}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/components",
"mimeType": "application/json",
"text": json.dumps(
{"count": len(components), "components": components}, indent=2
),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/components",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "components": []}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/components",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "components": []}, indent=2),
}
]
}
def _get_nets(interface) -> Dict[str, Any]:
def _get_nets(interface: Any) -> Dict[str, Any]:
"""Get list of electrical nets"""
result = interface.routing_commands.get_nets_list({})
if result.get("success"):
nets = result.get("nets", [])
return {
"contents": [{
"uri": "kicad://project/current/nets",
"mimeType": "application/json",
"text": json.dumps({
"count": len(nets),
"nets": nets
}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/nets",
"mimeType": "application/json",
"text": json.dumps({"count": len(nets), "nets": nets}, indent=2),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/nets",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "nets": []}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/nets",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "nets": []}, indent=2),
}
]
}
def _get_layers(interface) -> Dict[str, Any]:
def _get_layers(interface: Any) -> Dict[str, Any]:
"""Get layer stack information"""
result = interface.board_commands.get_layer_list({})
if result.get("success"):
layers = result.get("layers", [])
return {
"contents": [{
"uri": "kicad://project/current/layers",
"mimeType": "application/json",
"text": json.dumps({
"count": len(layers),
"layers": layers
}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/layers",
"mimeType": "application/json",
"text": json.dumps({"count": len(layers), "layers": layers}, indent=2),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/layers",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "layers": []}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/layers",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "layers": []}, indent=2),
}
]
}
def _get_design_rules(interface) -> Dict[str, Any]:
def _get_design_rules(interface: Any) -> Dict[str, Any]:
"""Get design rule settings"""
result = interface.design_rule_commands.get_design_rules({})
if result.get("success"):
return {
"contents": [{
"uri": "kicad://project/current/design-rules",
"mimeType": "application/json",
"text": json.dumps(result.get("rules", {}), indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/design-rules",
"mimeType": "application/json",
"text": json.dumps(result.get("rules", {}), indent=2),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/design-rules",
"mimeType": "text/plain",
"text": "Design rules not available"
}]
"contents": [
{
"uri": "kicad://project/current/design-rules",
"mimeType": "text/plain",
"text": "Design rules not available",
}
]
}
def _get_drc_report(interface) -> Dict[str, Any]:
def _get_drc_report(interface: Any) -> Dict[str, Any]:
"""Get DRC violations"""
result = interface.design_rule_commands.get_drc_violations({})
if result.get("success"):
violations = result.get("violations", [])
return {
"contents": [{
"uri": "kicad://project/current/drc-report",
"mimeType": "application/json",
"text": json.dumps({
"count": len(violations),
"violations": violations
}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/drc-report",
"mimeType": "application/json",
"text": json.dumps(
{"count": len(violations), "violations": violations}, indent=2
),
}
]
}
else:
return {
"contents": [{
"uri": "kicad://project/current/drc-report",
"mimeType": "application/json",
"text": json.dumps({
"count": 0,
"violations": [],
"message": "Run DRC first to get violations"
}, indent=2)
}]
"contents": [
{
"uri": "kicad://project/current/drc-report",
"mimeType": "application/json",
"text": json.dumps(
{
"count": 0,
"violations": [],
"message": "Run DRC first to get violations",
},
indent=2,
),
}
]
}
def _get_board_preview(interface) -> Dict[str, Any]:
def _get_board_preview(interface: Any) -> Dict[str, Any]:
"""Get board preview as PNG image"""
result = interface.board_commands.get_board_2d_view({"width": 800, "height": 600})
@@ -296,18 +321,22 @@ def _get_board_preview(interface) -> Dict[str, Any]:
# Image data should already be base64 encoded
image_data = result.get("imageData", "")
return {
"contents": [{
"uri": "kicad://board/preview.png",
"mimeType": "image/png",
"blob": image_data # Base64 encoded PNG
}]
"contents": [
{
"uri": "kicad://board/preview.png",
"mimeType": "image/png",
"blob": image_data, # Base64 encoded PNG
}
]
}
else:
# Return a placeholder message
return {
"contents": [{
"uri": "kicad://board/preview.png",
"mimeType": "text/plain",
"text": "Board preview not available"
}]
"contents": [
{
"uri": "kicad://board/preview.png",
"mimeType": "text/plain",
"text": "Board preview not available",
}
]
}

Some files were not shown because too many files have changed in this diff Show More