diff --git a/README.md b/README.md index 4a1f61f..b6eb194 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,217 @@ -# kicad-mcp -Model Context Protocol server for KiCad on Mac +# KiCad MCP Server - Setup Guide + +This guide will help you set up a Model Context Protocol (MCP) server for KiCad on macOS, allowing you to interact with KiCad projects through Claude Desktop or other MCP-compatible clients. + +## Prerequisites + +- macOS with KiCad installed +- Python 3.10 or higher +- Claude Desktop (or another MCP client) +- Basic familiarity with the terminal + +## Installation Steps + +### 1. Set Up Your Python Environment + +First, let's install `uv` (a fast Python package installer) and set up our environment: + +```bash +# Create a new directory for our project +mkdir -p ~/Projects/kicad-mcp +cd ~/Projects/kicad-mcp + +# Create a virtual environment and activate it +python3 -m venv venv +source venv/bin/activate + +# Install the MCP SDK +pip install "mcp[cli]" +``` + +### 2. Save the KiCad MCP Server Script + +Create a new file called `kicad_mcp.py` in your project directory and paste the contents of the KiCad MCP Server script. + +```bash +# Make the file executable (optional, but helpful) +chmod +x kicad_mcp.py + +# Run the server in development mode +python -m mcp.dev kicad_mcp.py +``` + +### 3. Test Your Server + +Let's make sure your server works correctly before integrating it with Claude Desktop: + +```bash +# Check if the file exists and has content +cat kicad_mcp.py | head -n 5 +``` + +You should see the server start up and display information about the available tools and resources. + +### 4. Configure Claude Desktop + +Now, let's configure Claude Desktop to use our MCP server: + +1. Create or edit the Claude Desktop configuration file: + +```bash +# Create the directory if it doesn't exist +mkdir -p ~/Library/Application\ Support/Claude + +# Edit the configuration file (or create it if it doesn't exist) +nano ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +2. Add the KiCad MCP server to the configuration: + +```json +{ + "mcpServers": { + "kicad": { + "command": "/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/venv/bin/python", + "args": [ + "/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp/kicad_mcp.py" + ] + } + } +} +``` + +Replace `/ABSOLUTE/PATH/TO/YOUR/PROJECT/kicad-mcp` with the actual path to your project directory (e.g., `/Users/yourusername/Projects/kicad-mcp`). + +3. Save the file and exit the editor. + +### 5. Restart Claude Desktop + +Close and reopen Claude Desktop to load the new configuration. + +## Usage + +Once the server is properly configured, you can interact with KiCad through Claude Desktop: + +1. Open Claude Desktop +2. Look for the tools icon (hammer symbol) in the Claude interface +3. You should see the KiCad MCP tools available in the menu + +Here are some example prompts you can use: + +- "What KiCad projects do I have on my Mac?" +- "Can you help me open my latest KiCad project?" +- "Extract the bill of materials from my project at [path]" +- "Validate my KiCad project at [path]" + +## Troubleshooting + +If you encounter issues: + +1. **Server Not Appearing in Claude Desktop:** + - Check your `claude_desktop_config.json` file for errors + - Make sure the path to your project and Python interpreter is correct + - Ensure Python can access the `mcp` package (check by running `python -c "import mcp; print(mcp.__version__)"` in your venv) + +2. **Server Errors:** + - Check the terminal output when running the server in development mode + - Make sure all required Python packages are installed + - Verify that your KiCad installation is in the standard location + - Run `pip install -U "mcp[cli]"` to ensure you have the latest version + +3. **Permission Issues:** + - Make sure the script is executable (`chmod +x kicad_mcp.py`) + - Check if Claude Desktop has permission to run the script + - If you get permission errors, try using the full path to your Python interpreter in the configuration + +## Extending the Server + +The provided MCP server implements basic KiCad functionality. To extend it: + +1. Add new tools using the `@mcp.tool()` decorator +2. Add new resources using the `@mcp.resource()` decorator +3. Add new prompts using the `@mcp.prompt()` decorator + +The MCP SDK provides a command-line interface for development and deployment. With your virtual environment activated, you can use commands like: + +```bash +# Test your server in development mode +python -m mcp.dev kicad_mcp.py + +# Install your server for use with Claude Desktop +python -m mcp.install kicad_mcp.py --name "KiCad" +``` + +## Additional Resources + +- [MCP Documentation](https://modelcontextprotocol.io/introduction) +- [KiCad Python API Documentation](https://docs.kicad.org/doxygen-python/namespacepcbnew.html) + +## Contributing + +Want to contribute to the KiCad MCP Server? Here's how you can help improve this project: + +### Getting Started + +The project currently consists of just two files: + +- kicad_mcp.py - The main MCP server implementation + +- This setup guide + +If you want to make improvements: + +1. Set up your environment as described in the installation steps + +2. Make your changes to the kicad_mcp.py file + +3. Test your changes with Claude Desktop + +### How to Contribute + +#### Improving the Existing Server + +You can improve the existing server by: + +- Adding more tools and resources for KiCad + +- Fixing bugs in the current implementation + +- Improving error handling and user experience + +- Adding support for more KiCad features + +#### Testing Your Changes + +Before sharing your changes, test them thoroughly: + +```bash + +# Run the server in development mode to check for errors + +python -m mcp.dev kicad_mcp.py + +# Test your changes with Claude Desktop + +``` + +### Future Development Ideas + +If you're looking for ways to improve the server, consider: + +1. Adding support for KiCad's Python API (`pcbnew`) for deeper integration + +2. Creating more specialized tools for PCB design review + +3. Adding visualization capabilities for schematics and layouts + +4. Improving project organization as the codebase grows + +### Best Practices + +- Keep the code simple and focused + +- Document your functions with clear docstrings + +- Handle errors gracefully with informative messages + +- Test with different KiCad project structures diff --git a/kicad_mcp.py b/kicad_mcp.py new file mode 100644 index 0000000..a520f1e --- /dev/null +++ b/kicad_mcp.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +""" +KiCad MCP Server - A Model Context Protocol server for KiCad on macOS. +This server allows Claude and other MCP clients to interact with KiCad projects. +""" +from typing import Dict, List, Any, Tuple, Optional +import os +import json +import subprocess +import asyncio +from pathlib import Path +from mcp.server.fastmcp import FastMCP, Context, Image + +# Initialize FastMCP server +mcp = FastMCP("KiCad") + +# Constants +KICAD_USER_DIR = os.path.expanduser("~/Documents/KiCad") +KICAD_APP_PATH = "/Applications/KiCad/KiCad.app" + +# Helper functions +def find_kicad_projects() -> List[Dict[str, Any]]: + """Find KiCad projects in the user's directory.""" + projects = [] + + for root, _, files in os.walk(KICAD_USER_DIR): + for file in files: + if file.endswith(".kicad_pro"): + project_path = os.path.join(root, file) + rel_path = os.path.relpath(project_path, KICAD_USER_DIR) + project_name = file[:-10] # Remove .kicad_pro extension + + projects.append({ + "name": project_name, + "path": project_path, + "relative_path": rel_path, + "modified": os.path.getmtime(project_path) + }) + + return projects + +def get_project_files(project_path: str) -> Dict[str, str]: + """Get all files related to a KiCad project.""" + project_dir = os.path.dirname(project_path) + project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro + + files = {} + extensions = [ + ".kicad_pcb", # PCB layout + ".kicad_sch", # Schematic + ".kicad_dru", # Design rules + ".kibot.yaml", # KiBot configuration + ".kicad_wks", # Worksheet template + ".kicad_mod", # Footprint module + "_netlist.net", # Netlist + ".csv", # BOM or other data + ".pos", # Component position file + ] + + for ext in extensions: + file_path = os.path.join(project_dir, f"{project_name}{ext}") + if os.path.exists(file_path): + files[ext[1:]] = file_path # Remove leading dot from extension + + return files + +# Resources +@mcp.resource("kicad://projects") +def list_projects_resource() -> str: + """List all KiCad projects as a formatted resource.""" + projects = find_kicad_projects() + + if not projects: + return "No KiCad projects found in your Documents/KiCad directory." + + result = "# KiCad Projects\n\n" + for project in sorted(projects, key=lambda p: p["modified"], reverse=True): + result += f"## {project['name']}\n" + result += f"- **Path**: {project['path']}\n" + result += f"- **Last Modified**: {os.path.getmtime(project['path'])}\n\n" + + return result + +@mcp.resource("kicad://project/{project_path}") +def get_project_details(project_path: str) -> str: + """Get details about a specific KiCad project.""" + if not os.path.exists(project_path): + return f"Project not found: {project_path}" + + try: + # Load project file + with open(project_path, 'r') as f: + project_data = json.load(f) + + # Get related files + files = get_project_files(project_path) + + # Format project details + result = f"# Project: {os.path.basename(project_path)[:-10]}\n\n" + + result += "## Project Files\n" + for file_type, file_path in files.items(): + result += f"- **{file_type}**: {file_path}\n" + + result += "\n## Project Settings\n" + + # Extract metadata + if "metadata" in project_data: + metadata = project_data["metadata"] + for key, value in metadata.items(): + result += f"- **{key}**: {value}\n" + + return result + + except Exception as e: + return f"Error reading project file: {str(e)}" + +@mcp.resource("kicad://schematic/{schematic_path}") +def get_schematic_info(schematic_path: str) -> str: + """Extract information from a KiCad schematic file.""" + if not os.path.exists(schematic_path): + return f"Schematic file not found: {schematic_path}" + + # KiCad schematic files are in S-expression format (not JSON) + # This is a basic extraction of text-based information + try: + with open(schematic_path, 'r') as f: + content = f.read() + + # Basic extraction of components + components = [] + for line in content.split('\n'): + if '(symbol ' in line and 'lib_id' in line: + components.append(line.strip()) + + result = f"# Schematic: {os.path.basename(schematic_path)}\n\n" + result += f"## Components (Estimated Count: {len(components)})\n\n" + + # Extract a sample of components + for i, comp in enumerate(components[:10]): + result += f"{comp}\n" + + if len(components) > 10: + result += f"\n... and {len(components) - 10} more components\n" + + return result + + except Exception as e: + return f"Error reading schematic file: {str(e)}" + +# Tools +@mcp.tool() +def find_projects() -> List[Dict[str, Any]]: + """Find all KiCad projects on this system.""" + return find_kicad_projects() + +@mcp.tool() +def get_project_structure(project_path: str) -> Dict[str, Any]: + """Get the structure and files of a KiCad project.""" + if not os.path.exists(project_path): + return {"error": f"Project not found: {project_path}"} + + project_dir = os.path.dirname(project_path) + project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro extension + + # Get related files + files = get_project_files(project_path) + + # Get project metadata + metadata = {} + try: + with open(project_path, 'r') as f: + project_data = json.load(f) + if "metadata" in project_data: + metadata = project_data["metadata"] + except Exception as e: + metadata = {"error": str(e)} + + return { + "name": project_name, + "path": project_path, + "directory": project_dir, + "files": files, + "metadata": metadata + } + +@mcp.tool() +def open_kicad_project(project_path: str) -> Dict[str, Any]: + """Open a KiCad project in KiCad.""" + if not os.path.exists(project_path): + return {"success": False, "error": f"Project not found: {project_path}"} + + try: + # On MacOS, use the 'open' command to open the project in KiCad + cmd = ["open", "-a", KICAD_APP_PATH, project_path] + result = subprocess.run(cmd, capture_output=True, text=True) + + return { + "success": result.returncode == 0, + "command": " ".join(cmd), + "output": result.stdout, + "error": result.stderr if result.returncode != 0 else None + } + + except Exception as e: + return {"success": False, "error": str(e)} + +@mcp.tool() +def extract_bom(project_path: str) -> Dict[str, Any]: + """Extract a Bill of Materials (BOM) from a KiCad project.""" + if not os.path.exists(project_path): + return {"success": False, "error": f"Project not found: {project_path}"} + + project_dir = os.path.dirname(project_path) + project_name = os.path.basename(project_path)[:-10] + + # Look for existing BOM files + bom_files = [] + for file in os.listdir(project_dir): + if file.startswith(project_name) and file.endswith('.csv') and 'bom' in file.lower(): + bom_files.append(os.path.join(project_dir, file)) + + if not bom_files: + return { + "success": False, + "error": "No BOM files found. You need to generate a BOM using KiCad first." + } + + try: + # Read the first BOM file + bom_path = bom_files[0] + with open(bom_path, 'r') as f: + bom_content = f.read() + + # Parse CSV (simplified) + lines = bom_content.strip().split('\n') + headers = lines[0].split(',') + + components = [] + for line in lines[1:]: + values = line.split(',') + if len(values) >= len(headers): + component = {} + for i, header in enumerate(headers): + component[header.strip()] = values[i].strip() + components.append(component) + + return { + "success": True, + "bom_file": bom_path, + "headers": headers, + "component_count": len(components), + "components": components + } + + except Exception as e: + return {"success": False, "error": str(e)} + +@mcp.tool() +def validate_project(project_path: str) -> Dict[str, Any]: + """Basic validation of a KiCad project.""" + if not os.path.exists(project_path): + return {"valid": False, "error": f"Project not found: {project_path}"} + + issues = [] + files = get_project_files(project_path) + + # Check for essential files + if "kicad_pcb" not in files: + issues.append("Missing PCB layout file") + + if "kicad_sch" not in files: + issues.append("Missing schematic file") + + # Validate project file + try: + with open(project_path, 'r') as f: + project_data = json.load(f) + except json.JSONDecodeError: + issues.append("Invalid project file format (JSON parsing error)") + except Exception as e: + issues.append(f"Error reading project file: {str(e)}") + + return { + "valid": len(issues) == 0, + "path": project_path, + "issues": issues if issues else None, + "files_found": list(files.keys()) + } + +@mcp.tool() +async def generate_project_thumbnail(project_path: str, ctx: Context) -> Optional[Image]: + """Generate a thumbnail of a KiCad project's PCB layout.""" + # This would normally use KiCad's Python API (pcbnew) to render a PCB image + # However, since this is a simulation, we'll return a message instead + + if not os.path.exists(project_path): + ctx.info(f"Project not found: {project_path}") + return None + + # Get PCB file + files = get_project_files(project_path) + if "kicad_pcb" not in files: + ctx.info("PCB file not found in project") + return None + + ctx.info("In a real implementation, this would generate a PCB thumbnail image") + ctx.info("This requires pcbnew Python module from KiCad to render PCB layouts") + + # Placeholder for actual implementation + # In a real implementation, you would: + # 1. Load the PCB file using pcbnew + # 2. Render it to an image + # 3. Return the image + + return None + +# Prompts +@mcp.prompt() +def create_new_component() -> str: + """Prompt for creating a new KiCad component.""" + return """ +I want to create a new component in KiCad for my PCB design. I need help with: + +1. Deciding on the correct component package/footprint +2. Creating the schematic symbol +3. Connecting the schematic symbol to the footprint +4. Adding the component to my design + +Please provide step-by-step instructions on how to create a new component in KiCad. +""" + +@mcp.prompt() +def debug_pcb_issues() -> str: + """Prompt for debugging common PCB issues.""" + return """ +I'm having issues with my KiCad PCB design. Can you help me troubleshoot the following problems: + +1. Design rule check (DRC) errors +2. Electrical rule check (ERC) errors +3. Footprint mismatches +4. Routing challenges + +Please provide a systematic approach to identifying and fixing these issues in KiCad. +""" + +# Run the server +if __name__ == "__main__": + mcp.run(transport='stdio')