Merge branch 'main' into pr-3-security-input-validation
This commit is contained in:
commit
0bbb78b0a9
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,11 +1,3 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug or issue with the KiCad MCP Server
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
<!-- A clear and concise description of the bug -->
|
||||
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -120,4 +120,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
path: dist/
|
||||
|
28
.gitignore
vendored
28
.gitignore
vendored
@ -18,6 +18,11 @@ __pycache__/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
*.whl
|
||||
|
||||
# PyPI
|
||||
.pypirc
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
@ -28,6 +33,7 @@ htmlcov/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.pytest_cache/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
@ -42,3 +48,25 @@ logs/
|
||||
|
||||
# MCP specific
|
||||
~/.kicad_mcp/drc_history/
|
||||
|
||||
# UV and modern Python tooling
|
||||
uv.lock
|
||||
.uv-cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Pre-commit
|
||||
.pre-commit-config.yaml
|
||||
|
||||
# KiCad backup files
|
||||
*-backups/
|
||||
fp-info-cache
|
||||
*.bak
|
||||
*.backup
|
||||
*.kicad_pcb-bak
|
||||
*.kicad_sch-bak
|
||||
*.kicad_pro-bak
|
||||
*.kicad_prl
|
||||
*.kicad_prl-bak
|
||||
*.kicad_sch.lck
|
||||
*.kicad_pcb.lck
|
||||
*.kicad_pro.lck
|
||||
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.10
|
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
@ -0,0 +1,6 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include .env.example
|
||||
recursive-include kicad_mcp *.py
|
||||
recursive-include docs *.md
|
39
Makefile
Normal file
39
Makefile
Normal file
@ -0,0 +1,39 @@
|
||||
.PHONY: help install test lint format clean build run
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " install Install dependencies"
|
||||
@echo " test Run tests"
|
||||
@echo " lint Run linting"
|
||||
@echo " format Format code"
|
||||
@echo " clean Clean build artifacts"
|
||||
@echo " build Build package"
|
||||
|
||||
install:
|
||||
uv sync --group dev
|
||||
|
||||
test:
|
||||
uv run python -m pytest tests/ -v
|
||||
|
||||
lint:
|
||||
uv run ruff check kicad_mcp/ tests/
|
||||
uv run mypy kicad_mcp/
|
||||
|
||||
format:
|
||||
uv run ruff format kicad_mcp/ tests/
|
||||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
rm -rf build/
|
||||
rm -rf *.egg-info/
|
||||
rm -rf .pytest_cache/
|
||||
rm -rf htmlcov/
|
||||
rm -f coverage.xml
|
||||
find . -type d -name __pycache__ -delete
|
||||
find . -type f -name "*.pyc" -delete
|
||||
|
||||
build:
|
||||
uv build
|
||||
|
||||
run:
|
||||
uv run python main.py
|
16
README.md
16
README.md
@ -22,6 +22,7 @@ This guide will help you set up a Model Context Protocol (MCP) server for KiCad.
|
||||
- macOS, Windows, or Linux
|
||||
- Python 3.10 or higher
|
||||
- KiCad 9.0 or higher
|
||||
- uv 0.8.0 or higher
|
||||
- Claude Desktop (or another MCP client)
|
||||
|
||||
## Installation Steps
|
||||
@ -32,14 +33,15 @@ First, let's install dependencies and set up our environment:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/lamaalrajih/kicad-mcp.git .
|
||||
git clone https://github.com/lamaalrajih/kicad-mcp.git
|
||||
cd kicad-mcp
|
||||
|
||||
# Create a virtual environment and activate it
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
# Install dependencies – `uv` will create a `.venv/` folder automatically
|
||||
# (Install `uv` first: `brew install uv` on macOS or `pipx install uv`)
|
||||
make install
|
||||
|
||||
# Install the MCP SDK and other dependencies
|
||||
pip install -r requirements.txt
|
||||
# Optional: activate the environment for manual commands
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### 2. Configure Your Environment
|
||||
@ -89,7 +91,7 @@ vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||
{
|
||||
"mcpServers": {
|
||||
"kicad": {
|
||||
"command": "/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/venv/bin/python",
|
||||
"command": "/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/.venv/bin/python",
|
||||
"args": [
|
||||
"/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/main.py"
|
||||
]
|
||||
|
@ -3,5 +3,27 @@ KiCad MCP Server.
|
||||
|
||||
A Model Context Protocol (MCP) server for KiCad electronic design automation (EDA) files.
|
||||
"""
|
||||
from .server import *
|
||||
from .config import *
|
||||
from .context import *
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Lama Al Rajih"
|
||||
__description__ = "Model Context Protocol server for KiCad on Mac, Windows, and Linux"
|
||||
|
||||
__all__ = [
|
||||
# Package metadata
|
||||
"__version__",
|
||||
"__author__",
|
||||
"__description__",
|
||||
|
||||
# Server creation / shutdown helpers
|
||||
"create_server",
|
||||
"add_cleanup_handler",
|
||||
"run_cleanup_handlers",
|
||||
"shutdown_server",
|
||||
|
||||
# Lifespan / context helpers
|
||||
"kicad_lifespan",
|
||||
"KiCadAppContext",
|
||||
]
|
||||
|
@ -5,8 +5,9 @@ import atexit
|
||||
import os
|
||||
import signal
|
||||
import logging
|
||||
import functools
|
||||
from typing import Callable
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Import resource handlers
|
||||
from kicad_mcp.resources.projects import register_project_resources
|
||||
@ -127,9 +128,11 @@ def create_server() -> FastMCP:
|
||||
# Always print this now, as we rely on CLI
|
||||
logging.info(f"KiCad Python module setup removed; relying on kicad-cli for external operations.")
|
||||
|
||||
# Build a lifespan callable with the kwarg baked in (FastMCP 2.x dropped lifespan_kwargs)
|
||||
lifespan_factory = functools.partial(kicad_lifespan, kicad_modules_available=kicad_modules_available)
|
||||
|
||||
# Initialize FastMCP server
|
||||
# Pass the availability flag (always False now) to the lifespan context
|
||||
mcp = FastMCP("KiCad", lifespan=kicad_lifespan, lifespan_kwargs={"kicad_modules_available": kicad_modules_available})
|
||||
mcp = FastMCP("KiCad", lifespan=lifespan_factory)
|
||||
logging.info(f"Created FastMCP server instance with lifespan management")
|
||||
|
||||
# Register resources
|
||||
@ -186,3 +189,43 @@ def create_server() -> FastMCP:
|
||||
|
||||
logging.info(f"Server initialization complete")
|
||||
return mcp
|
||||
|
||||
|
||||
def setup_signal_handlers() -> None:
|
||||
"""Setup signal handlers for graceful shutdown."""
|
||||
# Signal handlers are set up in register_signal_handlers
|
||||
pass
|
||||
|
||||
|
||||
def cleanup_handler() -> None:
|
||||
"""Handle cleanup during shutdown."""
|
||||
run_cleanup_handlers()
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
"""Configure logging for the server."""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start the KiCad MCP server (blocking)."""
|
||||
setup_logging()
|
||||
logging.info("Starting KiCad MCP server...")
|
||||
|
||||
server = create_server()
|
||||
|
||||
try:
|
||||
server.run() # FastMCP manages its own event loop
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Server interrupted by user")
|
||||
except Exception as e:
|
||||
logging.error(f"Server error: {e}")
|
||||
finally:
|
||||
logging.info("Server shutdown complete")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -20,7 +20,7 @@ def register_export_tools(mcp: FastMCP) -> None:
|
||||
"""
|
||||
|
||||
@mcp.tool()
|
||||
async def generate_pcb_thumbnail(project_path: str, ctx: Context) -> Optional[Image]:
|
||||
async def generate_pcb_thumbnail(project_path: str, ctx: Context):
|
||||
"""Generate a thumbnail image of a KiCad PCB layout using kicad-cli.
|
||||
|
||||
Args:
|
||||
@ -88,14 +88,14 @@ def register_export_tools(mcp: FastMCP) -> None:
|
||||
return None
|
||||
|
||||
@mcp.tool()
|
||||
async def generate_project_thumbnail(project_path: str, ctx: Context) -> Optional[Image]:
|
||||
async def generate_project_thumbnail(project_path: str, ctx: Context):
|
||||
"""Generate a thumbnail of a KiCad project's PCB layout (Alias for generate_pcb_thumbnail)."""
|
||||
# This function now just calls the main CLI-based thumbnail generator
|
||||
print(f"generate_project_thumbnail called, redirecting to generate_pcb_thumbnail for {project_path}")
|
||||
return await generate_pcb_thumbnail(project_path, ctx)
|
||||
|
||||
# Helper functions for thumbnail generation
|
||||
async def generate_thumbnail_with_cli(pcb_file: str, ctx: Context) -> Optional[Image]:
|
||||
async def generate_thumbnail_with_cli(pcb_file: str, ctx: Context):
|
||||
"""Generate PCB thumbnail using command line tools.
|
||||
This is a fallback method when the kicad Python module is not available or fails.
|
||||
|
||||
|
8
main.py
8
main.py
@ -9,7 +9,7 @@ import logging # Import logging module
|
||||
|
||||
# Must import config BEFORE env potentially overrides it via os.environ
|
||||
from kicad_mcp.config import KICAD_USER_DIR, ADDITIONAL_SEARCH_PATHS
|
||||
from kicad_mcp.server import create_server
|
||||
from kicad_mcp.server import main as server_main
|
||||
from kicad_mcp.utils.env import load_dotenv
|
||||
|
||||
# --- Setup Logging ---
|
||||
@ -70,10 +70,10 @@ if __name__ == "__main__":
|
||||
else:
|
||||
logging.info(f"No additional search paths configured") # Changed print to logging
|
||||
|
||||
# Create and run server
|
||||
server = create_server()
|
||||
# Run server
|
||||
logging.info(f"Running server with stdio transport") # Changed print to logging
|
||||
server.run(transport='stdio')
|
||||
import asyncio
|
||||
asyncio.run(server_main())
|
||||
except Exception as e:
|
||||
logging.exception(f"Unhandled exception in main") # Log exception details
|
||||
raise
|
||||
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "kicad-mcp"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Model Context Protocol (MCP) server for KiCad electronic design automation (EDA) files"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
@ -43,23 +43,32 @@ classifiers = [
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"mcp[cli]>=1.0.0",
|
||||
"fastmcp>=0.1.0",
|
||||
"fastmcp>=2.0.0",
|
||||
"pandas>=2.0.0",
|
||||
"pyyaml>=6.0.0",
|
||||
"defusedxml>=0.7.0", # Secure XML parsing
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/your-org/kicad-mcp"
|
||||
Documentation = "https://github.com/your-org/kicad-mcp/blob/main/README.md"
|
||||
Repository = "https://github.com/your-org/kicad-mcp"
|
||||
"Bug Tracker" = "https://github.com/your-org/kicad-mcp/issues"
|
||||
Changelog = "https://github.com/your-org/kicad-mcp/blob/main/CHANGELOG.md"
|
||||
"Homepage" = "https://github.com/lamaalrajih/kicad-mcp"
|
||||
"Bug Tracker" = "https://github.com/lamaalrajih/kicad-mcp/issues"
|
||||
"Documentation" = "https://github.com/lamaalrajih/kicad-mcp#readme"
|
||||
|
||||
[project.scripts]
|
||||
kicad-mcp = "kicad_mcp.server:main"
|
||||
|
||||
# UV dependency groups (replaces project.optional-dependencies)
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
@ -91,9 +100,8 @@ visualization = [
|
||||
"playwright>=1.40.0", # Browser automation (optional)
|
||||
]
|
||||
|
||||
# Tool configurations remain the same
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
@ -106,6 +114,7 @@ select = [
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
"SIM", # flake8-simplify
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long, handled by ruff format
|
||||
@ -160,6 +169,7 @@ module = [
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "7.0"
|
||||
addopts = [
|
||||
@ -225,3 +235,11 @@ skips = ["B101", "B601", "B404", "B603", "B110", "B112"] # Skip low-severity su
|
||||
|
||||
[tool.bandit.assert_used]
|
||||
skips = ["*_test.py", "*/test_*.py"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["kicad_mcp*"]
|
||||
exclude = ["tests*", "docs*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"kicad_mcp" = ["prompts/*.txt", "resources/**/*.json"]
|
||||
|
@ -1,5 +0,0 @@
|
||||
mcp[cli]
|
||||
pandas
|
||||
|
||||
# Development/Testing
|
||||
pytest
|
61
run_tests.py
Normal file
61
run_tests.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test runner for KiCad MCP project.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_command(cmd: list[str], description: str) -> int:
|
||||
"""Run a command and return the exit code."""
|
||||
print(f"\n🔍 {description}")
|
||||
print(f"Running: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, check=False)
|
||||
if result.returncode == 0:
|
||||
print(f"✅ {description} passed")
|
||||
else:
|
||||
print(f"❌ {description} failed with exit code {result.returncode}")
|
||||
return result.returncode
|
||||
except FileNotFoundError:
|
||||
print(f"❌ Command not found: {cmd[0]}")
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests and checks."""
|
||||
project_root = Path(__file__).parent
|
||||
|
||||
# Change to project directory
|
||||
import os
|
||||
|
||||
os.chdir(project_root)
|
||||
|
||||
exit_code = 0
|
||||
|
||||
# Run linting
|
||||
exit_code |= run_command(["uv", "run", "ruff", "check", "kicad_mcp/", "tests/"], "Lint check")
|
||||
|
||||
# Run formatting check
|
||||
exit_code |= run_command(
|
||||
["uv", "run", "ruff", "format", "--check", "kicad_mcp/", "tests/"], "Format check"
|
||||
)
|
||||
|
||||
# Run type checking
|
||||
exit_code |= run_command(["uv", "run", "mypy", "kicad_mcp/"], "Type check")
|
||||
|
||||
# Run tests
|
||||
exit_code |= run_command(["uv", "run", "python", "-m", "pytest", "tests/", "-v"], "Unit tests")
|
||||
|
||||
if exit_code == 0:
|
||||
print("\n🎉 All checks passed!")
|
||||
else:
|
||||
print(f"\n💥 Some checks failed (exit code: {exit_code})")
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Loading…
x
Reference in New Issue
Block a user