diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e83c267 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Example environment file for KiCad MCP Server +# Copy this file to .env and customize the values + +# Additional directories to search for KiCad projects (comma-separated) +# KICAD_SEARCH_PATHS=~/pcb,~/Electronics,~/Projects/KiCad + +# Override the default KiCad user directory +# KICAD_USER_DIR=~/Documents/KiCad + +# Override the default KiCad application path +# macOS: +# KICAD_APP_PATH=/Applications/KiCad/KiCad.app +# Windows: +# KICAD_APP_PATH=C:\Program Files\KiCad +# Linux: +# KICAD_APP_PATH=/usr/share/kicad diff --git a/.gitignore b/.gitignore index 161f283..75782a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ venv/ env/ ENV/ +# Environment files +.env + # Python cache files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 165f959..b3c580c 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,12 @@ The KiCad MCP Server is organized into a modular structure for better maintainab kicad-mcp/ ├── README.md # Project documentation ├── main.py # Entry point that runs the server -├── config.py # Configuration constants and settings ├── requirements.txt # Python dependencies +├── .env.example # Example environment configuration ├── kicad_mcp/ # Main package directory │ ├── __init__.py # Package initialization │ ├── server.py # MCP server setup +│ ├── config.py # Configuration constants and settings │ ├── context.py # Lifespan management and shared context │ ├── resources/ # Resource handlers │ ├── tools/ # Tool handlers @@ -78,7 +79,28 @@ source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt ``` -### 2. Run the Server +### 2. Configure Your Environment + +Create a `.env` file to customize where the server looks for your KiCad projects: + +```bash +# Copy the example environment file +cp .env.example .env + +# Edit the .env file +vim .env +``` + +In the `.env` file, add your custom project directories: + +``` +# Add paths to your KiCad projects (comma-separated) +KICAD_SEARCH_PATHS=~/pcb,~/Electronics,~/Projects/KiCad +``` + +This will tell the server to look for KiCad projects in your custom directories in addition to the standard KiCad user directory. + +### 3. Run the Server Once the environment is set up, you can run the server: @@ -90,9 +112,13 @@ python -m mcp.dev main.py python main.py ``` -### 3. Configure an MCP Client +The server will automatically detect KiCad projects in: -The server can be used with any MCP-compatible client. Here's how to set it up with Claude Desktop as an example: +- The standard KiCad user directory (e.g., ~/Documents/KiCad) +- Any custom directories specified in your .env file +- Common project directories (automatically detected) + +### 4. Configure an MCP Client Now, let's configure Claude Desktop to use our MCP server: @@ -142,7 +168,7 @@ On Windows, the configuration would look like: The configuration should be stored in `%APPDATA%\Claude\claude_desktop_config.json`. -### 4. Restart Your MCP Client +### 5. Restart Your MCP Client Close and reopen your MCP client (e.g., Claude Desktop) to load the new configuration. The KiCad server should appear in the tools dropdown menu or equivalent interface in your client. @@ -328,6 +354,44 @@ Claude will generate and display a thumbnail of your PCB. Then you might ask: Let's run a full DRC check on this project to identify all the issues I need to fix. ``` +## Configuration Options + +The KiCad MCP Server can be configured using environment variables or a `.env` file: + +### Key Configuration Options +| Environment Variable | Description | Example | +|---------------------|-------------|---------| +| `KICAD_SEARCH_PATHS` | Comma-separated list of directories to search for KiCad projects | `~/pcb,~/Electronics,~/Projects` | +| `KICAD_USER_DIR` | Override the default KiCad user directory | `~/Documents/KiCadProjects` | +| `KICAD_APP_PATH` | Override the default KiCad application path | `/Applications/KiCad7/KiCad.app` | +| `LOG_LEVEL` | Set logging verbosity | `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `LOG_DIR` | Directory for log files | `logs` or `~/.kicad_mcp/logs` | + +### Using Environment Variables Directly + +You can set environment variables when launching the server: + +```bash +KICAD_SEARCH_PATHS=~/pcb,~/Electronics LOG_LEVEL=DEBUG python main.py +``` + +### Using a .env File (Recommended) + +Create a `.env` file in the project root with your configuration by copying and renaming `.env.example`: + +``` +# KiCad MCP Server Configuration + +# Directories to search for KiCad projects (comma-separated) +KICAD_SEARCH_PATHS=~/pcb,~/Electronics,~/Projects/KiCad + +# Logging configuration +LOG_LEVEL=INFO +LOG_DIR=logs +``` + +The server automatically detects and loads this configuration on startup. + ## Development Guide ### Adding New Features @@ -375,16 +439,24 @@ If you encounter issues: 2. **Server Errors:** - Check the terminal output when running the server in development mode - - Look for errors in the logs at `~/.kicad_mcp/logs` or `logs/` directory + - Check Claude logs at: + - `~/Library/Logs/Claude/mcp-server-kicad.log` (server-specific logs) + - `~/Library/Logs/Claude/mcp.log` (general MCP logs) - Make sure all required Python packages are installed - Verify that your KiCad installation is in the standard location 3. **KiCad Python Modules Not Found:** - This is a common issue. The server will still work but with limited functionality - Ensure KiCad is installed properly - - Check if the right paths are set in `config.py` + - Check if the right paths are set in `kicad_mcp/config.py` -4. **DRC History Not Saving:** +4. 4. **Projects Not Found:** + - Check your `.env` file to ensure your project directories are correctly specified + - Verify the paths exist and have KiCad project files (.kicad_pro) + - Use absolute paths instead of `~` if there are issues with path expansion + - Check the Claude logs mentioned above to see if there are errors when searching for projects + +5. **DRC History Not Saving:** - Check if the `~/.kicad_mcp/drc_history/` directory exists and is writable - Verify that the project path used is consistent between runs - Check for errors in the logs related to DRC history saving diff --git a/kicad_mcp/config.py b/kicad_mcp/config.py index 31db2b1..40533c7 100644 --- a/kicad_mcp/config.py +++ b/kicad_mcp/config.py @@ -23,8 +23,36 @@ else: KICAD_USER_DIR = os.path.expanduser("~/Documents/KiCad") KICAD_APP_PATH = "/Applications/KiCad/KiCad.app" +# Additional search paths from environment variable +ADDITIONAL_SEARCH_PATHS = [] +env_search_paths = os.environ.get("KICAD_SEARCH_PATHS", "") +if env_search_paths: + for path in env_search_paths.split(","): + expanded_path = os.path.expanduser(path.strip()) + if os.path.exists(expanded_path): + ADDITIONAL_SEARCH_PATHS.append(expanded_path) + +# Try to auto-detect common project locations if not specified +DEFAULT_PROJECT_LOCATIONS = [ + "~/Documents/PCB", + "~/PCB", + "~/Electronics", + "~/Projects/Electronics", + "~/Projects/PCB", + "~/Projects/KiCad" +] + +for location in DEFAULT_PROJECT_LOCATIONS: + expanded_path = os.path.expanduser(location) + if os.path.exists(expanded_path) and expanded_path not in ADDITIONAL_SEARCH_PATHS: + ADDITIONAL_SEARCH_PATHS.append(expanded_path) + # Base path to KiCad's Python framework -KICAD_PYTHON_BASE = os.path.join(KICAD_APP_PATH, "Contents/Frameworks/Python.framework/Versions") +if system == "Darwin": # macOS + KICAD_PYTHON_BASE = os.path.join(KICAD_APP_PATH, "Contents/Frameworks/Python.framework/Versions") +else: + KICAD_PYTHON_BASE = "" # Will be determined dynamically in python_path.py + # File extensions KICAD_EXTENSIONS = { diff --git a/kicad_mcp/utils/env.py b/kicad_mcp/utils/env.py new file mode 100644 index 0000000..54ff6af --- /dev/null +++ b/kicad_mcp/utils/env.py @@ -0,0 +1,104 @@ +""" +Environment variable handling for KiCad MCP Server. +""" +import os +import logging +from pathlib import Path +from typing import Dict, Any, Optional + +def load_dotenv(env_file: str = ".env") -> Dict[str, str]: + """Load environment variables from .env file. + + Args: + env_file: Path to the .env file + + Returns: + Dictionary of loaded environment variables + """ + env_vars = {} + + # Try to find .env file in the current directory or parent directories + env_path = find_env_file(env_file) + + if not env_path: + # No .env file found, return empty dict + return env_vars + + try: + with open(env_path, 'r') as f: + for line in f: + line = line.strip() + + # Skip empty lines and comments + if not line or line.startswith('#'): + continue + + # Parse key-value pairs + if '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + + # Remove quotes if present + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + elif value.startswith("'") and value.endswith("'"): + value = value[1:-1] + + # Expand ~ to user's home directory + if '~' in value: + value = os.path.expanduser(value) + + # Set environment variable + os.environ[key] = value + env_vars[key] = value + + except Exception as e: + logging.warning(f"Error loading .env file: {str(e)}") + + return env_vars + +def find_env_file(filename: str = ".env") -> Optional[str]: + """Find a .env file in the current directory or parent directories. + + Args: + filename: Name of the env file to find + + Returns: + Path to the env file if found, None otherwise + """ + current_dir = os.getcwd() + max_levels = 3 # Limit how far up to search + + for _ in range(max_levels): + env_path = os.path.join(current_dir, filename) + if os.path.exists(env_path): + return env_path + + # Move up one directory + parent_dir = os.path.dirname(current_dir) + if parent_dir == current_dir: # We've reached the root + break + current_dir = parent_dir + + return None + +def get_env_list(env_var: str, default: str = "") -> list: + """Get a list from a comma-separated environment variable. + + Args: + env_var: Name of the environment variable + default: Default value if environment variable is not set + + Returns: + List of values + """ + value = os.environ.get(env_var, default) + if not value: + return [] + + # Split by comma and strip whitespace + items = [item.strip() for item in value.split(",")] + + # Filter out empty items + return [item for item in items if item] diff --git a/kicad_mcp/utils/kicad_utils.py b/kicad_mcp/utils/kicad_utils.py index bd57887..0eac8d5 100644 --- a/kicad_mcp/utils/kicad_utils.py +++ b/kicad_mcp/utils/kicad_utils.py @@ -5,8 +5,7 @@ import os import subprocess from typing import Dict, List, Any -from kicad_mcp.config import KICAD_USER_DIR, KICAD_APP_PATH, KICAD_EXTENSIONS - +from kicad_mcp.config import KICAD_USER_DIR, KICAD_APP_PATH, KICAD_EXTENSIONS, ADDITIONAL_SEARCH_PATHS def find_kicad_projects() -> List[Dict[str, Any]]: """Find KiCad projects in the user's directory. @@ -15,23 +14,33 @@ def find_kicad_projects() -> List[Dict[str, Any]]: List of dictionaries with project information """ projects = [] - - for root, _, files in os.walk(KICAD_USER_DIR): - for file in files: - if file.endswith(KICAD_EXTENSIONS["project"]): - project_path = os.path.join(root, file) - rel_path = os.path.relpath(project_path, KICAD_USER_DIR) - project_name = get_project_name_from_path(project_path) - - projects.append({ - "name": project_name, - "path": project_path, - "relative_path": rel_path, - "modified": os.path.getmtime(project_path) - }) - - return projects + # Search directories to look for KiCad projects + search_dirs = [KICAD_USER_DIR] + ADDITIONAL_SEARCH_PATHS + + for search_dir in search_dirs: + if not os.path.exists(search_dir): + print(f"Search directory does not exist: {search_dir}") + continue + + print(f"Scanning directory: {search_dir}") + for root, _, files in os.walk(search_dir): + for file in files: + if file.endswith(KICAD_EXTENSIONS["project"]): + project_path = os.path.join(root, file) + rel_path = os.path.relpath(project_path, search_dir) + project_name = get_project_name_from_path(project_path) + + print(f"Found KiCad project: {project_path}") + projects.append({ + "name": project_name, + "path": project_path, + "relative_path": rel_path, + "modified": os.path.getmtime(project_path) + }) + + print(f"Found {len(projects)} KiCad projects") + return projects def get_project_name_from_path(project_path: str) -> str: """Extract the project name from a .kicad_pro file path. diff --git a/main.py b/main.py index 2746b85..736de8b 100644 --- a/main.py +++ b/main.py @@ -3,14 +3,31 @@ 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. """ +import os +import sys + +from kicad_mcp.config import KICAD_USER_DIR, ADDITIONAL_SEARCH_PATHS from kicad_mcp.server import create_server +from kicad_mcp.utils.env import load_dotenv from kicad_mcp.utils.logger import Logger +# Load environment variables from .env file if present +load_dotenv() + logger = Logger() if __name__ == "__main__": try: logger.info("Starting KiCad MCP server") + + # Log search paths from config + logger.info(f"Using KiCad user directory: {KICAD_USER_DIR}") + if ADDITIONAL_SEARCH_PATHS: + logger.info(f"Additional search paths: {', '.join(ADDITIONAL_SEARCH_PATHS)}") + else: + logger.info("No additional search paths configured") + + # Create and run server server = create_server() logger.info("Running server with stdio transport") server.run(transport='stdio')