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 ## Bug Description
<!-- A clear and concise description of the bug --> <!-- A clear and concise description of the bug -->

View File

@ -120,4 +120,4 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: dist name: dist
path: dist/ path: dist/

28
.gitignore vendored
View File

@ -18,6 +18,11 @@ __pycache__/
dist/ dist/
build/ build/
*.egg-info/ *.egg-info/
*.egg
*.whl
# PyPI
.pypirc
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
@ -28,6 +33,7 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
.pytest_cache/
# Logs # Logs
logs/ logs/
@ -42,3 +48,25 @@ logs/
# MCP specific # MCP specific
~/.kicad_mcp/drc_history/ ~/.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 - macOS, Windows, or Linux
- Python 3.10 or higher - Python 3.10 or higher
- KiCad 9.0 or higher - KiCad 9.0 or higher
- uv 0.8.0 or higher
- Claude Desktop (or another MCP client) - Claude Desktop (or another MCP client)
## Installation Steps ## Installation Steps
@ -32,14 +33,15 @@ First, let's install dependencies and set up our environment:
```bash ```bash
# Clone the repository # 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 # Install dependencies `uv` will create a `.venv/` folder automatically
python3 -m venv venv # (Install `uv` first: `brew install uv` on macOS or `pipx install uv`)
source venv/bin/activate # On Windows: venv\Scripts\activate make install
# Install the MCP SDK and other dependencies # Optional: activate the environment for manual commands
pip install -r requirements.txt source .venv/bin/activate
``` ```
### 2. Configure Your Environment ### 2. Configure Your Environment
@ -89,7 +91,7 @@ vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
{ {
"mcpServers": { "mcpServers": {
"kicad": { "kicad": {
"command": "/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/venv/bin/python", "command": "/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/.venv/bin/python",
"args": [ "args": [
"/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/main.py" "/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. 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 os
import signal import signal
import logging import logging
import functools
from typing import Callable from typing import Callable
from mcp.server.fastmcp import FastMCP from fastmcp import FastMCP
# Import resource handlers # Import resource handlers
from kicad_mcp.resources.projects import register_project_resources 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 # Always print this now, as we rely on CLI
logging.info(f"KiCad Python module setup removed; relying on kicad-cli for external operations.") 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 # Initialize FastMCP server
# Pass the availability flag (always False now) to the lifespan context mcp = FastMCP("KiCad", lifespan=lifespan_factory)
mcp = FastMCP("KiCad", lifespan=kicad_lifespan, lifespan_kwargs={"kicad_modules_available": kicad_modules_available})
logging.info(f"Created FastMCP server instance with lifespan management") logging.info(f"Created FastMCP server instance with lifespan management")
# Register resources # Register resources
@ -186,3 +189,43 @@ def create_server() -> FastMCP:
logging.info(f"Server initialization complete") logging.info(f"Server initialization complete")
return mcp 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() @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. """Generate a thumbnail image of a KiCad PCB layout using kicad-cli.
Args: Args:
@ -88,14 +88,14 @@ def register_export_tools(mcp: FastMCP) -> None:
return None return None
@mcp.tool() @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).""" """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 # 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}") print(f"generate_project_thumbnail called, redirecting to generate_pcb_thumbnail for {project_path}")
return await generate_pcb_thumbnail(project_path, ctx) return await generate_pcb_thumbnail(project_path, ctx)
# Helper functions for thumbnail generation # 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. """Generate PCB thumbnail using command line tools.
This is a fallback method when the kicad Python module is not available or fails. 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 # 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.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 from kicad_mcp.utils.env import load_dotenv
# --- Setup Logging --- # --- Setup Logging ---
@ -70,10 +70,10 @@ if __name__ == "__main__":
else: else:
logging.info(f"No additional search paths configured") # Changed print to logging logging.info(f"No additional search paths configured") # Changed print to logging
# Create and run server # Run server
server = create_server()
logging.info(f"Running server with stdio transport") # Changed print to logging 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: except Exception as e:
logging.exception(f"Unhandled exception in main") # Log exception details logging.exception(f"Unhandled exception in main") # Log exception details
raise raise

View File

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "kicad-mcp" name = "kicad-mcp"
version = "0.2.0" version = "0.1.0"
description = "Model Context Protocol (MCP) server for KiCad electronic design automation (EDA) files" description = "Model Context Protocol (MCP) server for KiCad electronic design automation (EDA) files"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
@ -43,23 +43,32 @@ classifiers = [
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"mcp[cli]>=1.0.0", "mcp[cli]>=1.0.0",
"fastmcp>=0.1.0", "fastmcp>=2.0.0",
"pandas>=2.0.0", "pandas>=2.0.0",
"pyyaml>=6.0.0", "pyyaml>=6.0.0",
"defusedxml>=0.7.0", # Secure XML parsing "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] [project.urls]
Homepage = "https://github.com/your-org/kicad-mcp" "Homepage" = "https://github.com/lamaalrajih/kicad-mcp"
Documentation = "https://github.com/your-org/kicad-mcp/blob/main/README.md" "Bug Tracker" = "https://github.com/lamaalrajih/kicad-mcp/issues"
Repository = "https://github.com/your-org/kicad-mcp" "Documentation" = "https://github.com/lamaalrajih/kicad-mcp#readme"
"Bug Tracker" = "https://github.com/your-org/kicad-mcp/issues"
Changelog = "https://github.com/your-org/kicad-mcp/blob/main/CHANGELOG.md"
[project.scripts] [project.scripts]
kicad-mcp = "kicad_mcp.server:main" kicad-mcp = "kicad_mcp.server:main"
# UV dependency groups (replaces project.optional-dependencies)
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pytest>=7.0.0", "pytest>=7.0.0",
@ -91,9 +100,8 @@ visualization = [
"playwright>=1.40.0", # Browser automation (optional) "playwright>=1.40.0", # Browser automation (optional)
] ]
# Tool configurations remain the same
[tool.ruff] [tool.ruff]
target-version = "py311" target-version = "py310"
line-length = 100 line-length = 100
[tool.ruff.lint] [tool.ruff.lint]
@ -106,6 +114,7 @@ select = [
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
"UP", # pyupgrade "UP", # pyupgrade
"SIM", # flake8-simplify "SIM", # flake8-simplify
"UP", # pyupgrade
] ]
ignore = [ ignore = [
"E501", # line too long, handled by ruff format "E501", # line too long, handled by ruff format
@ -160,6 +169,7 @@ module = [
] ]
ignore_missing_imports = true ignore_missing_imports = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "7.0" minversion = "7.0"
addopts = [ addopts = [
@ -225,3 +235,11 @@ skips = ["B101", "B601", "B404", "B603", "B110", "B112"] # Skip low-severity su
[tool.bandit.assert_used] [tool.bandit.assert_used]
skips = ["*_test.py", "*/test_*.py"] 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