kicad-mcp/kicad_mcp/tools/netlist_tools.py
Lama 750dd260c4 Add comprehensive netlist extraction functionality
- Implement schematic netlist parser with S-expression parsing
- Create netlist tools for extraction and connection analysis
- Add resources for netlist and component connection reporting
- Include documentation with usage guide and troubleshooting
- Register new tools and resources in server configuration

This enables extracting component connections from KiCad schematics
and analyzing connectivity between components.
2025-03-21 09:31:15 -04:00

372 lines
15 KiB
Python

"""
Netlist extraction and analysis tools for KiCad schematics.
"""
import os
from typing import Dict, List, Any, Optional
from mcp.server.fastmcp import FastMCP, Context
from kicad_mcp.utils.file_utils import get_project_files
from kicad_mcp.utils.logger import Logger
from kicad_mcp.utils.netlist_parser import extract_netlist, analyze_netlist
# Create logger for this module
logger = Logger()
def register_netlist_tools(mcp: FastMCP) -> None:
"""Register netlist-related tools with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.tool()
async def extract_schematic_netlist(schematic_path: str, ctx: Context) -> Dict[str, Any]:
"""Extract netlist information from a KiCad schematic.
This tool parses a KiCad schematic file and extracts comprehensive
netlist information including components, connections, and labels.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
ctx: MCP context for progress reporting
Returns:
Dictionary with netlist information
"""
logger.info(f"Extracting netlist from schematic: {schematic_path}")
if not os.path.exists(schematic_path):
logger.error(f"Schematic file not found: {schematic_path}")
ctx.info(f"Schematic file not found: {schematic_path}")
return {"success": False, "error": f"Schematic file not found: {schematic_path}"}
# Report progress
await ctx.report_progress(10, 100)
ctx.info(f"Loading schematic file: {os.path.basename(schematic_path)}")
# Extract netlist information
try:
await ctx.report_progress(20, 100)
ctx.info("Parsing schematic structure...")
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
logger.error(f"Error extracting netlist: {netlist_data['error']}")
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
await ctx.report_progress(50, 100)
# Check if the component exists
components = netlist_data.get("components", {})
if component_ref not in components:
logger.error(f"Component {component_ref} not found in schematic")
ctx.info(f"Component {component_ref} not found in schematic")
return {
"success": False,
"error": f"Component {component_ref} not found in schematic",
"available_components": list(components.keys())
}
component_info = components[component_ref]
ctx.info(f"Found component: {component_ref} ({component_info.get('lib_id', 'unknown type')})")
# Find all nets connected to this component
await ctx.report_progress(70, 100)
ctx.info("Analyzing component connections...")
# Build connection information
connections = []
connected_components = set()
nets = netlist_data.get("nets", {})
for net_name, pins in nets.items():
# Check if any pin belongs to our component
component_pins = [pin for pin in pins if pin.get('component') == component_ref]
if component_pins:
# This net connects to our component
net_info = {
"net_name": net_name,
"pins": component_pins,
"connected_to": []
}
# Find other components connected to this net
for pin in pins:
other_component = pin.get('component')
if other_component and other_component != component_ref:
connected_components.add(other_component)
net_info["connected_to"].append({
"component": other_component,
"pin": pin.get('pin', 'unknown')
})
connections.append(net_info)
await ctx.report_progress(90, 100)
# Build result
result = {
"success": True,
"component_ref": component_ref,
"component_info": component_info,
"connections": connections,
"connected_component_count": len(connected_components),
"connected_components": list(connected_components)
}
# Complete progress
await ctx.report_progress(100, 100)
ctx.info(f"Found {len(connections)} connections to component {component_ref}")
return resultf"Error extracting netlist: {netlist_data['error']}")
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
await ctx.report_progress(60, 100)
ctx.info(f"Extracted {netlist_data['component_count']} components and {netlist_data['net_count']} nets")
# Analyze the netlist
await ctx.report_progress(70, 100)
ctx.info("Analyzing netlist data...")
analysis_results = analyze_netlist(netlist_data)
await ctx.report_progress(90, 100)
# Build result
result = {
"success": True,
"schematic_path": schematic_path,
"component_count": netlist_data["component_count"],
"net_count": netlist_data["net_count"],
"components": netlist_data["components"],
"nets": netlist_data["nets"],
"analysis": analysis_results
}
# Complete progress
await ctx.report_progress(100, 100)
ctx.info("Netlist extraction complete")
return result
except Exception as e:
logger.error(f"Error extracting netlist: {str(e)}")
ctx.info(f"Error extracting netlist: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def extract_project_netlist(project_path: str, ctx: Context) -> Dict[str, Any]:
"""Extract netlist from a KiCad project's schematic.
This tool finds the schematic associated with a KiCad project
and extracts its netlist information.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
ctx: MCP context for progress reporting
Returns:
Dictionary with netlist information
"""
logger.info(f"Extracting netlist for project: {project_path}")
if not os.path.exists(project_path):
logger.error(f"Project not found: {project_path}")
ctx.info(f"Project not found: {project_path}")
return {"success": False, "error": f"Project not found: {project_path}"}
# Report progress
await ctx.report_progress(10, 100)
# Get the schematic file
try:
files = get_project_files(project_path)
if "schematic" not in files:
logger.error("Schematic file not found in project")
ctx.info("Schematic file not found in project")
return {"success": False, "error": "Schematic file not found in project"}
schematic_path = files["schematic"]
logger.info(f"Found schematic file: {schematic_path}")
ctx.info(f"Found schematic file: {os.path.basename(schematic_path)}")
# Extract netlist
await ctx.report_progress(20, 100)
# Call the schematic netlist extraction
result = await extract_schematic_netlist(schematic_path, ctx)
# Add project path to result
if "success" in result and result["success"]:
result["project_path"] = project_path
return result
except Exception as e:
logger.error(f"Error extracting project netlist: {str(e)}")
ctx.info(f"Error extracting project netlist: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def analyze_schematic_connections(schematic_path: str, ctx: Context) -> Dict[str, Any]:
"""Analyze connections in a KiCad schematic.
This tool provides detailed analysis of component connections,
including power nets, signal paths, and potential issues.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
ctx: MCP context for progress reporting
Returns:
Dictionary with connection analysis
"""
logger.info(f"Analyzing connections in schematic: {schematic_path}")
if not os.path.exists(schematic_path):
logger.error(f"Schematic file not found: {schematic_path}")
ctx.info(f"Schematic file not found: {schematic_path}")
return {"success": False, "error": f"Schematic file not found: {schematic_path}"}
# Report progress
await ctx.report_progress(10, 100)
ctx.info(f"Extracting netlist from: {os.path.basename(schematic_path)}")
# Extract netlist information
try:
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
logger.error(f"Error extracting netlist: {netlist_data['error']}")
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
await ctx.report_progress(40, 100)
# Advanced connection analysis
ctx.info("Performing connection analysis...")
analysis = {
"component_count": netlist_data["component_count"],
"net_count": netlist_data["net_count"],
"component_types": {},
"power_nets": [],
"signal_nets": [],
"potential_issues": []
}
# Analyze component types
components = netlist_data.get("components", {})
for ref, component in components.items():
# Extract component type from reference (e.g., R1 -> R)
import re
comp_type_match = re.match(r'^([A-Za-z_]+)', ref)
if comp_type_match:
comp_type = comp_type_match.group(1)
if comp_type not in analysis["component_types"]:
analysis["component_types"][comp_type] = 0
analysis["component_types"][comp_type] += 1
await ctx.report_progress(60, 100)
# Identify power nets
nets = netlist_data.get("nets", {})
for net_name, pins in nets.items():
if any(net_name.startswith(prefix) for prefix in ["VCC", "VDD", "GND", "+5V", "+3V3", "+12V"]):
analysis["power_nets"].append({
"name": net_name,
"pin_count": len(pins)
})
else:
analysis["signal_nets"].append({
"name": net_name,
"pin_count": len(pins)
})
await ctx.report_progress(80, 100)
# Check for potential issues
# 1. Nets with only one connection (floating)
for net_name, pins in nets.items():
if len(pins) <= 1 and not any(net_name.startswith(prefix) for prefix in ["VCC", "VDD", "GND", "+5V", "+3V3", "+12V"]):
analysis["potential_issues"].append({
"type": "floating_net",
"net": net_name,
"description": f"Net '{net_name}' appears to be floating (only has {len(pins)} connection)"
})
# 2. Power pins without connections
# This would require more detailed parsing of the schematic
await ctx.report_progress(90, 100)
# Build result
result = {
"success": True,
"schematic_path": schematic_path,
"analysis": analysis
}
# Complete progress
await ctx.report_progress(100, 100)
ctx.info("Connection analysis complete")
return result
except Exception as e:
logger.error(f"Error analyzing connections: {str(e)}")
ctx.info(f"Error analyzing connections: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def find_component_connections(project_path: str, component_ref: str, ctx: Context) -> Dict[str, Any]:
"""Find all connections for a specific component in a KiCad project.
This tool extracts information about how a specific component
is connected to other components in the schematic.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
component_ref: Component reference (e.g., "R1", "U3")
ctx: MCP context for progress reporting
Returns:
Dictionary with component connection information
"""
logger.info(f"Finding connections for component {component_ref} in project: {project_path}")
if not os.path.exists(project_path):
logger.error(f"Project not found: {project_path}")
ctx.info(f"Project not found: {project_path}")
return {"success": False, "error": f"Project not found: {project_path}"}
# Report progress
await ctx.report_progress(10, 100)
# Get the schematic file
try:
files = get_project_files(project_path)
if "schematic" not in files:
logger.error("Schematic file not found in project")
ctx.info("Schematic file not found in project")
return {"success": False, "error": "Schematic file not found in project"}
schematic_path = files["schematic"]
logger.info(f"Found schematic file: {schematic_path}")
ctx.info(f"Found schematic file: {os.path.basename(schematic_path)}")
# Extract netlist
await ctx.report_progress(30, 100)
ctx.info(f"Extracting netlist to find connections for {component_ref}...")
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
logger.error(