Skip to content

Adding Tools

This guide covers the process of extending mckicad with new tools, resources, and prompt templates.

Look at the existing modules in tools/ and decide where your tool fits:

  • schematic.py — schematic creation and reading
  • schematic_edit.py — schematic modification and removal
  • schematic_analysis.py — connectivity, ERC, validation
  • schematic_patterns.py — circuit building block patterns
  • batch.py — batch operations
  • project.py — project discovery and management
  • drc.py — design rule checks
  • bom.py — BOM operations
  • export.py — file export (Gerber, PDF, SVG)
  • routing.py — autorouting
  • analysis.py — board-level analysis
  • pcb.py — live PCB manipulation via IPC

If none of the existing modules fit, create a new one in tools/.

Import mcp from mckicad.server and decorate with @mcp.tool():

tools/my_module.py
from typing import Any
from mckicad.server import mcp
@mcp.tool()
def my_new_tool(param: str) -> dict[str, Any]:
"""Clear description of what this tool does.
The docstring is shown to the LLM, so make it specific and actionable.
Describe what the tool returns and when to use it.
"""
try:
# Your implementation
result = do_something(param)
return {"success": True, "data": result}
except Exception as e:
return {"success": False, "error": str(e)}

Follow the return convention: always include success: bool. On failure, include error: str. On success, include relevant data fields.

If you created a new module, add an import in server.py:

# server.py - add with the other tool imports
import mckicad.tools.my_module # noqa: F401

The import triggers the @mcp.tool() decorators, which register the functions with the server. No additional registration code is needed.

Create a test file in tests/:

tests/test_my_module.py
import pytest
from mckicad.tools.my_module import my_new_tool
def test_my_new_tool_success(tmp_path):
result = my_new_tool(str(tmp_path / "test.kicad_sch"))
assert result["success"] is True
def test_my_new_tool_missing_file():
result = my_new_tool("/nonexistent/path.kicad_sch")
assert result["success"] is False
assert "error" in result

Use appropriate test markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.requires_kicad.

Terminal window
make test tests/test_my_module.py
make lint

Resources provide read-only data to the LLM. Add them in resources/:

resources/my_resource.py
from mckicad.server import mcp
@mcp.resource("kicad://my-data/{parameter}")
def my_custom_resource(parameter: str) -> str:
"""Description of what this resource provides."""
# Return formatted text for the LLM
return f"Data about {parameter}"

Import the module in server.py to trigger registration.

Prompts are reusable conversation starters. Add them in prompts/:

prompts/my_prompts.py
from mckicad.server import mcp
@mcp.prompt()
def my_workflow_prompt() -> str:
"""Description of what this prompt helps with."""
return """
I need help with [specific KiCad task]. Please assist me with:
1. [First aspect]
2. [Second aspect]
3. [Third aspect]
My KiCad project is located at:
[Enter the full path to your .kicad_pro file here]
"""

The tool docstring is shown directly to the calling LLM. Make it:

  • Specific — describe exactly what the tool does and returns
  • Actionable — explain when to use this tool vs alternatives
  • Concise — LLMs work better with focused descriptions

Always catch exceptions and return structured errors:

@mcp.tool()
def my_tool(path: str) -> dict[str, Any]:
"""..."""
if not path:
return {"success": False, "error": "Path must be non-empty"}
try:
# ...
return {"success": True, "data": result}
except FileNotFoundError:
return {"success": False, "error": f"File not found: {path}"}
except Exception as e:
return {"success": False, "error": str(e)}

Use the path validator for any file access:

from mckicad.utils.path_validator import validate_path
err = validate_path(path)
if err:
return {"success": False, "error": err}

Use the secure subprocess wrapper for any external command execution:

from mckicad.utils.secure_subprocess import run_command
result = run_command(["kicad-cli", "pcb", "export", "svg", pcb_path])

Use the module logger, never print():

import logging
logger = logging.getLogger(__name__)
logger.info("Processing %s", path)
logger.warning("Unexpected format in %s", path)
logger.error("Failed to process %s: %s", path, e)
  • Use caching for expensive operations (the lifespan context provides a cache dict)
  • Report progress for long-running operations via ctx.report_progress()
  • Use asyncio for concurrent operations where appropriate
  • Keep tool responses focused — return what the LLM needs, not everything available