Files
kicad-mcp-server/python/resources/resource_definitions.py
Eugene Mikhantyev 9b1024a8f3 chore: enable strict mypy checks and fix pre-commit mypy hook
Add type annotations to all previously untyped functions and remove 9
suppressed error codes (call-arg, assignment, return-value, operator,
has-type, dict-item, misc, list-item, annotation-unchecked) by fixing
the underlying type issues.

Add [[tool.mypy.overrides]] with ignore_missing_imports for KiCAD-specific
modules (pcbnew, sexpdata, skip, cairosvg, kipy, PIL) so the pre-commit
mypy hook passes in its isolated venv. Add types-requests and pytest to
additional_dependencies in .pre-commit-config.yaml.

Also fixes several real bugs uncovered by stricter checks: incorrect static
calls to instance methods in swig_backend, wrong return type on get_size,
missing value param in BoardAPI.place_component, variable shadowing in
kicad_process.py, unqualified LibraryManager reference in kicad_interface,
and missing top-level Path import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:50:54 +01:00

343 lines
11 KiB
Python

"""
Resource definitions for exposing KiCAD project state via MCP
Resources follow the MCP 2025-06-18 specification, providing
read-only access to project data for LLM context.
"""
import base64
import json
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger("kicad_interface")
# =============================================================================
# RESOURCE DEFINITIONS
# =============================================================================
RESOURCE_DEFINITIONS = [
{
"uri": "kicad://project/current/info",
"name": "Current Project Information",
"description": "Metadata about the currently open KiCAD project including paths, name, and status",
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/board",
"name": "Board Properties",
"description": "Comprehensive board information including dimensions, layer count, and design rules",
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/components",
"name": "Component List",
"description": "List of all components on the board with references, footprints, values, and positions",
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/nets",
"name": "Electrical Nets",
"description": "List of all electrical nets and their connections",
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/layers",
"name": "Layer Stack",
"description": "Board layer configuration and properties",
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/design-rules",
"name": "Design Rules",
"description": "Current design rule settings for clearances, track widths, and constraints",
"mimeType": "application/json",
},
{
"uri": "kicad://project/current/drc-report",
"name": "DRC Violations",
"description": "Design Rule Check violations and warnings from last DRC run",
"mimeType": "application/json",
},
{
"uri": "kicad://board/preview.png",
"name": "Board Preview Image",
"description": "2D rendering of the current board state",
"mimeType": "image/png",
},
]
# =============================================================================
# RESOURCE READ HANDLERS
# =============================================================================
def handle_resource_read(uri: str, interface: Any) -> Dict[str, Any]:
"""
Handle reading a resource by URI
Args:
uri: Resource URI to read
interface: KiCADInterface instance with access to board state
Returns:
Dict with resource contents following MCP spec
"""
logger.info(f"Reading resource: {uri}")
handlers = {
"kicad://project/current/info": _get_project_info,
"kicad://project/current/board": _get_board_info,
"kicad://project/current/components": _get_components,
"kicad://project/current/nets": _get_nets,
"kicad://project/current/layers": _get_layers,
"kicad://project/current/design-rules": _get_design_rules,
"kicad://project/current/drc-report": _get_drc_report,
"kicad://board/preview.png": _get_board_preview,
}
handler = handlers.get(uri)
if handler:
try:
return handler(interface)
except Exception as e:
logger.error(f"Error reading resource {uri}: {str(e)}")
return {
"contents": [{"uri": uri, "mimeType": "text/plain", "text": f"Error: {str(e)}"}]
}
else:
return {
"contents": [{"uri": uri, "mimeType": "text/plain", "text": f"Unknown resource: {uri}"}]
}
# =============================================================================
# INDIVIDUAL RESOURCE HANDLERS
# =============================================================================
def _get_project_info(interface: Any) -> Dict[str, Any]:
"""Get current project information"""
result = interface.project_commands.get_project_info({})
if result.get("success"):
return {
"contents": [
{
"uri": "kicad://project/current/info",
"mimeType": "application/json",
"text": json.dumps(result.get("project", {}), indent=2),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/info",
"mimeType": "text/plain",
"text": "No project currently open",
}
]
}
def _get_board_info(interface: Any) -> Dict[str, Any]:
"""Get board properties and metadata"""
result = interface.board_commands.get_board_info({})
if result.get("success"):
return {
"contents": [
{
"uri": "kicad://project/current/board",
"mimeType": "application/json",
"text": json.dumps(result.get("board", {}), indent=2),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/board",
"mimeType": "text/plain",
"text": "No board currently loaded",
}
]
}
def _get_components(interface: Any) -> Dict[str, Any]:
"""Get list of all components"""
result = interface.component_commands.get_component_list({})
if result.get("success"):
components = result.get("components", [])
return {
"contents": [
{
"uri": "kicad://project/current/components",
"mimeType": "application/json",
"text": json.dumps(
{"count": len(components), "components": components}, indent=2
),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/components",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "components": []}, indent=2),
}
]
}
def _get_nets(interface: Any) -> Dict[str, Any]:
"""Get list of electrical nets"""
result = interface.routing_commands.get_nets_list({})
if result.get("success"):
nets = result.get("nets", [])
return {
"contents": [
{
"uri": "kicad://project/current/nets",
"mimeType": "application/json",
"text": json.dumps({"count": len(nets), "nets": nets}, indent=2),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/nets",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "nets": []}, indent=2),
}
]
}
def _get_layers(interface: Any) -> Dict[str, Any]:
"""Get layer stack information"""
result = interface.board_commands.get_layer_list({})
if result.get("success"):
layers = result.get("layers", [])
return {
"contents": [
{
"uri": "kicad://project/current/layers",
"mimeType": "application/json",
"text": json.dumps({"count": len(layers), "layers": layers}, indent=2),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/layers",
"mimeType": "application/json",
"text": json.dumps({"count": 0, "layers": []}, indent=2),
}
]
}
def _get_design_rules(interface: Any) -> Dict[str, Any]:
"""Get design rule settings"""
result = interface.design_rule_commands.get_design_rules({})
if result.get("success"):
return {
"contents": [
{
"uri": "kicad://project/current/design-rules",
"mimeType": "application/json",
"text": json.dumps(result.get("rules", {}), indent=2),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/design-rules",
"mimeType": "text/plain",
"text": "Design rules not available",
}
]
}
def _get_drc_report(interface: Any) -> Dict[str, Any]:
"""Get DRC violations"""
result = interface.design_rule_commands.get_drc_violations({})
if result.get("success"):
violations = result.get("violations", [])
return {
"contents": [
{
"uri": "kicad://project/current/drc-report",
"mimeType": "application/json",
"text": json.dumps(
{"count": len(violations), "violations": violations}, indent=2
),
}
]
}
else:
return {
"contents": [
{
"uri": "kicad://project/current/drc-report",
"mimeType": "application/json",
"text": json.dumps(
{
"count": 0,
"violations": [],
"message": "Run DRC first to get violations",
},
indent=2,
),
}
]
}
def _get_board_preview(interface: Any) -> Dict[str, Any]:
"""Get board preview as PNG image"""
result = interface.board_commands.get_board_2d_view({"width": 800, "height": 600})
if result.get("success") and "imageData" in result:
# Image data should already be base64 encoded
image_data = result.get("imageData", "")
return {
"contents": [
{
"uri": "kicad://board/preview.png",
"mimeType": "image/png",
"blob": image_data, # Base64 encoded PNG
}
]
}
else:
# Return a placeholder message
return {
"contents": [
{
"uri": "kicad://board/preview.png",
"mimeType": "text/plain",
"text": "Board preview not available",
}
]
}