Merge branch 'main' into pr-3-security-input-validation

This commit is contained in:
Lama Al Rajih 2025-07-22 20:47:47 -04:00 committed by GitHub
commit 0bbb78b0a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1799 additions and 42 deletions

View File

@ -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 -->

28
.gitignore vendored
View File

@ -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
View File

@ -0,0 +1 @@
3.10

6
MANIFEST.in Normal file
View 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
View 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

View File

@ -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"
]

View File

@ -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",
]

View File

@ -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()

View File

@ -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.

View File

@ -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

View File

@ -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"]

View File

@ -1,5 +0,0 @@
mcp[cli]
pandas
# Development/Testing
pytest

61
run_tests.py Normal file
View 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())

1550
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff