remove old simple mcp server
This commit is contained in:
parent
646dfe4589
commit
b5b5835939
349
kicad_mcp.py
349
kicad_mcp.py
@ -1,349 +0,0 @@
|
|||||||
#!/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