initial commit
This commit is contained in:
parent
5148a39a3e
commit
09ca184da6
219
README.md
219
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
|
||||
|
349
kicad_mcp.py
Normal file
349
kicad_mcp.py
Normal file
@ -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')
|
Loading…
x
Reference in New Issue
Block a user