Skip to main content
Glama
lucatib

kicad-mcp

by lucatib
README.md
# kicad-mcp

An MCP server for **KiCad 10** on Windows, Linux and macOS. Gives an LLM live access to the board
open in KiCad, file-based access to schematics, symbol library search, schematic
generation from a firmware pinout, and a scripting escape hatch for everything
else.

Verified against:

| KiCad | IPC API | `kicad-python` | `mcp` |
|---|---|---|---|
| **10.0.7** (latest) | 10.0.6 | 0.8.0 | 2.3.0 |
| 10.0.6 | 10.0.1 | 0.7.1 | 2.0.0 |

## Why a venv built from KiCad's own Python

`pcbnew` is a C extension compiled against CPython 3.11's ABI *and* KiCad's
native libraries, so it only loads in KiCad's bundled interpreter. A venv
**created from** that interpreter reuses the same binary, so it can import
`pcbnew`, while `pip` adds `kipy` and the MCP SDK. One process, all three APIs,
no cross-interpreter bridge.

## Install

Create the venv from KiCad's own Python, install the package into it, and run
`doctor`. Only where that Python lives differs between platforms.

### Windows

Where KiCad's Python is depends on how KiCad was installed:

| KiCad installed for | KiCad folder | Its Python |
|---|---|---|
| All users (default) | `C:\Program Files\KiCad\10.0` | `C:\Program Files\KiCad\10.0\bin\python.exe` |
| Only me | `%LOCALAPPDATA%\Programs\KiCad\10.0` | `%LOCALAPPDATA%\Programs\KiCad\10.0\bin\python.exe` |

All users:

```bash
"C:/Program Files/KiCad/10.0/bin/python.exe" -m venv --system-site-packages .venv
```

Only me (`%LOCALAPPDATA%` is `C:\Users\<you>\AppData\Local`):

```bash
"$LOCALAPPDATA/Programs/KiCad/10.0/bin/python.exe" -m venv --system-site-packages .venv
```

The same in PowerShell:

```powershell
& "$env:LOCALAPPDATA\Programs\KiCad\10.0\bin\python.exe" -m venv --system-site-packages .venv
```

```bash
.venv/Scripts/python.exe -m pip install -e .
```

Then check the environment:

```bash
.venv/Scripts/python.exe -m kicad_mcp doctor
```

`doctor` reports every dependency, whether the IPC server is enabled, whether
KiCad is running, and a remedy for anything that fails. Its `KiCad install` line
shows which folder was found.

The server finds KiCad on its own in both locations above, and through the
uninstall entries in the registry. For KiCad anywhere else (an extracted or
portable copy, another drive), or to choose between several installed versions,
set `KICAD_MCP_KICAD_ROOT` to the KiCad folder, the one containing `bin\`. In
the client configuration that is an `env` entry:

```json
{
  "mcpServers": {
    "kicad": {
      "command": "C:\\Workspace\\mcp_kicad\\.venv\\Scripts\\python.exe",
      "args": ["-m", "kicad_mcp"],
      "env": {"KICAD_MCP_KICAD_ROOT": "D:\\Tools\\KiCad\\10.0"}
    }
  }
}
```

### Linux

Install KiCad 10 from your distribution or the KiCad PPA. The package puts
`pcbnew` into the system Python, so the venv is built from that (some
distributions ship it separately, e.g. `python3-kicad`; check with
`python3 -c "import pcbnew"`):

```bash
/usr/bin/python3 -m venv --system-site-packages .venv
```

```bash
.venv/bin/python -m pip install -e .
```

```bash
.venv/bin/python -m kicad_mcp doctor
```

The server finds `kicad-cli` on `PATH`, in `/usr` and in `/usr/local`, and asks
it for the version. Set `KICAD_MCP_KICAD_ROOT` to another prefix (the folder
containing `bin/kicad-cli` and `share/kicad`) for a build elsewhere. Settings
are read from `~/.config/kicad` (or `$XDG_CONFIG_HOME/kicad`).

Flatpak and AppImage builds are not supported: their `kicad-cli` and `pcbnew`
live inside the sandbox, where the host Python cannot import them.

### macOS

Use the official disk image. KiCad.app bundles its own Python framework:

```bash
/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python3 -m venv --system-site-packages .venv
```

```bash
.venv/bin/python -m pip install -e .
```

```bash
.venv/bin/python -m kicad_mcp doctor
```

The server finds `KiCad.app` in `/Applications`, `/Applications/KiCad*/` and
`~/Applications`, and reads the version from the bundle. For a copy elsewhere,
set `KICAD_MCP_KICAD_ROOT` to `KiCad.app` or to the folder holding it. Settings
are read from `~/Library/Preferences/kicad`.

## MCP client configuration

```json
{
  "mcpServers": {
    "kicad": {
      "command": "C:\\Workspace\\mcp_kicad\\.venv\\Scripts\\python.exe",
      "args": ["-m", "kicad_mcp"]
    }
  }
}
```

On Linux and macOS the command is the venv's `bin/python`, e.g.
`"/home/you/mcp_kicad/.venv/bin/python"`.

## Tools

**Environment** — `kicad_status`

**PCB** (the board open in KiCad, or a saved `.kicad_pcb` via `path`) —
`get_board_summary`, `list_footprints`, `get_footprint`, `list_nets`,
`get_layers`, `get_stackup`, `list_tracks_vias`, `list_zones`,
`list_board_shapes`; live only:
`get_selection`; files: `validate_board_outline`, `run_drc`, `render_pcb_3d`

**Schematic** (files; works with KiCad closed) — `open_schematic`,
`list_schematic_symbols`, `find_project_files`, `schematic_netlist`, `run_erc`,
`export_bom`, `export_schematic`, `list_variants`

**Symbol libraries** — `list_symbol_libraries`, `search_symbols`, `get_symbol_pins`,
`create_library_symbol`

**Footprint libraries** — `search_footprints`, `get_library_footprint`

**Generation** — `create_pinout_schematic`

**Editing existing schematics** — `add_symbol_to_schematic`, `label_pins`,
`set_symbol_fields`, `mark_pins_unused`, `rewire_power_symbol`,
`move_symbol_fields`, `add_decoupling_capacitors`, `remove_symbols`,
`disconnect_pins`, `rename_labels`, `edit_notes`, `route_nets`

**Import** (files; works with KiCad closed) — `import_altium`

**Escape hatch** — `run_kicad_script`

### Libraries

The symbol and footprint tools see what KiCad sees. They read the global
library tables, including any tables nested in them, then the project's. A
`${...}` in a table resolves as it does in KiCad: KiCad's own variables, then
those set in Preferences > Configure Paths, then environment variables. So a
vendor library whose tables use variables of its own, such as Würth
Elektronik's `${WE_SYMBOL_DIR}`, shows up once those paths are configured. When
two libraries share a nickname, the later one wins, as in KiCad 10.

### Drawing a symbol: `create_library_symbol`

For a part no library has, such as a module bought as a board:

```json
{
  "project_dir": "C:/proj",
  "library": "proj",
  "name": "LevelShifter_BSS138_4ch",
  "pins": [
    {"number": "3", "name": "LV", "type": "power_in", "side": "left"},
    {"number": "1", "name": "LV1", "type": "bidirectional", "side": "left"},
    {"number": "9", "name": "HV", "type": "power_in", "side": "right"},
    {"number": "7", "name": "HV1", "type": "bidirectional", "side": "right"}
  ],
  "description": "4-channel BSS138 level shifter module"
}
```

The symbol is one rectangle with the pins on its sides. Each side's pins run in
list order, from the top or from the left, 2.54 mm apart with every pin end on
the grid, and the body is sized so no pin names touch. With no `side` on any pin,
the first half go left and the rest right. `type` is a KiCad electrical type,
`passive` when left out.

`library` is a nickname. If the project's `sym-lib-table` has it, the symbol is
added to that file. Otherwise it becomes `<project>/<library>.kicad_sym`,
registered in the project's table, which is created if missing. Both files keep
their existing text byte for byte. The tool refuses, writing nothing, when the
library already has that name, when the nickname belongs to a global library
(a project library would hide it), when a pin number repeats, or when the
folder is in no KiCad project.

The schematic tools find a project's own libraries from the schematic's path,
so `add_symbol_to_schematic` places `proj:LevelShifter_BSS138_4ch` without a
`project_dir`.

### Generating a schematic from a pinout

```json
{
  "output": "C:/proj/esp32.kicad_sch",
  "mcu_lib_id": "RF_Module:ESP32-S3-MINI-1",
  "assignments": {"IO4": "LED_STATUS", "GPIO_8": "SPI_MOSI", "io9": "SPI_MISO"}
}
```

Pin matching ignores case, underscores and hyphens, so `IO4`, `GPIO_4` and `io4`
all resolve to the same pin. Each assigned pin gets a short wire and whatever
joins it to its net (see below); the MCU's unassigned power pins get power
symbols plus `PWR_FLAG` so ERC does not report undriven rails. Unmatched
assignments are reported back rather than silently dropped. Placed symbols take
their library's default footprint unless you pass one.

To wire a symbol that is already on the sheet, `label_pins` takes the same
`assignments` map for a `reference`. It skips any pin that already has something
connected, so an existing net is never shorted to a new one.

### How a pin joins its net

Every wiring tool (`create_pinout_schematic`, `add_symbol_to_schematic`,
`label_pins`) picks one of three, and reports it per pin as `via`:

| Net | Joined by | Reaches |
|---|---|---|
| A rail: a `power` library name (`GND`, `+3V3`, `VBUS`...), a power symbol's value already in the project, or a lib_id such as `"power:+5V"` | power symbol, pointing away from the pin | every sheet |
| A name in `global_nets`, or one already used by a global label in the project | global label, shaped from the pin's direction | every sheet |
| Anything else | local label | this sheet only |

```json
{
  "path": "C:/proj/sensors.kicad_sch",
  "reference": "J3",
  "assignments": {"1": "+3V3", "2": "SDA", "3": "SCL", "4": "GND"},
  "global_nets": ["SDA", "SCL"]
}
```

A local label never leaves its sheet, and a global label does not join a local
one of the same name on another sheet; when a net would end up split that way,
`warnings` names the sheets to fix.

### Sheets of a project

A sheet is not a project of its own. KiCad annotates each symbol under the
project name and the path of sheet symbols from the root schematic down to its
sheet, and a sheet used twice holds one annotation per use. Before writing a
symbol, the tools find the `.kicad_pro` beside the file or up to three folders
above it, walk its hierarchy from the root, and write one instance per use of
the sheet. References are free across every sheet: a part on a sheet used twice
gets two (`R3` and `R4`, listed in `instances`), so both copies reach the board
as separate parts. A file outside any project stays its own root, as before.

New symbols are placed in the first free spot on the page: right of existing
parts, wrapping to a new row, never over the title block. When the nets in
`assignments` already exist on the sheet, the part goes beside the pins it
connects to instead, on the side they face, lined up with them and flipped if
that turns its pins toward them, so the wires `route_nets` draws stay short. A full sheet is
refused rather than drawn off-page; pass `x`/`y` to place explicitly. Footprints
written by `add_symbol_to_schematic` or `set_symbol_fields` are checked against
the project's `fp-lib-table`, and one that will not resolve is reported with the
same footprint under a library that does.

`move_symbol_fields` moves a placed symbol's fields: to an absolute `x`/`y`, by a
`dx`/`dy` offset, and to an `orientation` (`"horizontal"` or `"vertical"`, as it
reads on the sheet whatever the symbol's rotation):

```json
{"path": "C:/proj/sensors.kicad_sch", "reference": "J3",
 "fields": {"Reference": {"x": 120.65, "y": 60.96}, "Value": {"dy": 2.54}}}
```

The symbol is no longer marked autoplaced, so KiCad keeps the new spots. On a
part drawn as several units, an absolute position needs `unit`.

### Removing parts: `remove_symbols`

```json
{"path": "C:/proj/sensors.kicad_sch", "references": ["J3", "C7"]}
```

Every unit of each part goes, along with what was there only to connect it:
wire runs that now end in nothing, and labels, power symbols, `PWR_FLAG`s,
no-connects and junctions that no longer reach any pin. Anything still wired to
another part or a sheet pin stays. Two things that reach no pin are kept and
reported in `warnings` instead: wiring that joins two net names, since removing
it would split those nets, and hierarchical labels, which the parent sheet's pin
refers to. On a sheet used more than once the part goes from every use. An
unknown reference removes nothing; `dry_run` reports without writing.

### Freeing pins: `disconnect_pins`

```json
{"path": "C:/proj/main.kicad_sch", "reference": "U1", "pin_numbers": ["28", "29"]}
```

Takes pins off their nets while the part stays: the pin's no-connect flag, the
wire run from it back to the first point where something else connects, and
labels, power symbols, `PWR_FLAG`s and junctions that then reach no pin -- what
`remove_symbols` would clear for those pins alone. Pins are chosen as for
`mark_pins_unused`, by `pin_numbers` and/or `name_contains`. Use it to retire a
net from a part that stays, or to free a no-connected pin for `label_pins`.

A pin whose wiring carries the net on to other pins keeps it and is listed
under `still_connected`: a label sitting on the pin with a wire beyond it, two
wires meeting on it, or another part's pin on it. So is one of several pins
stacked on one point, unless all of them are chosen. A pin number the part
lacks changes nothing; `dry_run` reports without writing.

### Renaming nets: `rename_labels`

```json
{"path": "C:/proj/inputs.kicad_sch", "renames": {"IN3": "IN1", "IN4": "IN2", "IN5": "IN3", "IN6": "IN4"}}
```

Every label of each name is renamed, local, global and hierarchical, all at
once: a numbered series can shift onto itself and two names can swap. A rename
that would join two nets is refused and nothing is written: onto a name another
label on the sheet keeps, onto a power net, or two names onto one. Only this
sheet changes, so `warnings` says when a global label's old name stays on other
sheets or its new one is already there, and when a hierarchical label leaves the
parent's sheet pin on the old name.

### Rewriting notes: `edit_notes`

```json
{"path": "C:/proj/main.kicad_sch", "replacements": {"spare GPIO8, 9": "spare GPIO35-42"}}
```

Each key is a fragment of one note on the sheet, free text or a text box, and is
replaced by its value; the note keeps its uuid, position and look.
`open_schematic` lists every note, with line breaks as `\n`. Quote a whole note
to replace all of it, or rewrite it to `""` to remove it. All replacements apply
to the text as it was, so two can swap. A fragment found in no note or more than
once is refused and nothing is written; quote more of the text to make it
unique. A text box keeps its size, so `warnings` flags one whose text grew.
Labels are not notes: they name nets, which `rename_labels` renames.

### Drawing wires: `route_nets`

The tools connect pins with net labels. Once placement, labelling and ERC are
done, `route_nets` turns those labels into drawn wires as the last step. It runs
entirely inside the server; the client passes only the file:

```json
{"path": "C:/proj/board.kicad_sch"}
```

Every signal net connected purely by local labels is wired pin to pin around the
parts, keeping one label with its name. Power symbols, global and hierarchical
labels, and anything you wired by hand are left alone. The result is checked with
`kicad-cli` before anything is written: the netlist must be identical and ERC must
report no new violations, or the routing is discarded and the file is untouched.
Use `nets` to route only some nets, `max_length` (mm) to keep long connections as
labels, and `dry_run` to verify without writing.

The output is a normal `.kicad_sch` you can open and keep editing. It is written
directly, so KiCad does not need to be running.

### Reading a board with KiCad closed

The PCB read tools ask the board open in KiCad's PCB editor by default, which
includes unsaved edits. Give them `path` and they read that `.kicad_pcb` instead,
through KiCad's bundled `pcbnew` module, with KiCad closed or a different board
open:

```json
{"reference": "J*", "path": "C:/proj/board.kicad_pcb"}
```

The answer has the same shape either way. Only the source differs: the file is
the last saved state, and it has no GUI state, so `get_layers` reports no active
layer. A board is loaded once per saved version, so repeated calls are cheap
and an edited file is read again. `get_stackup` reads the file's own
`(stackup ...)` section and says so when there is none, because KiCad then uses
its default.

### Board graphics: `list_board_shapes`

Lines, arcs, circles, rectangles, polygons and beziers, each with its uuid,
layer, type, stroke width, fill and geometry in millimetres. The board outline
and its cutouts are on `Edge.Cuts`:

```json
{"layer": "Edge.Cuts", "path": "C:/proj/board.kicad_pcb"}
```

`layer` takes globs, and both name forms work: `F.SilkS` and `F.Silkscreen`,
`Dwgs.User` and `User.Drawings`. A layer the board renamed answers to both its
own name and the standard one, so `User.1` finds shapes on a layer called
`Mechanical 1`. `shape_type` narrows to one kind. Only
board-level drawings are listed unless `include_footprints` is set. Then
footprint graphics are listed too, each tagged with its footprint's reference,
which matters because slots and cutouts often live inside a footprint. A
polygon lists its first 200 points and gives the full count.

### What a footprint is made of: `get_footprint`

Besides its pads' nets, `get_footprint` gives each pad's type (`SMD`, `PTH`,
`NPTH`, `Connector`), copper shape and size, and drill: `circle` or `oblong`
(a slot), with its size in mm, or `null` for SMD. It also lists the
footprint's 3D models with filename, offset (mm), rotation (degrees), scale
and visibility. With `include_shapes` it adds the footprint's graphics grouped
by layer: silkscreen, courtyard, fab, and any `Edge.Cuts` slot.

### Board outline: `validate_board_outline`

KiCad chains the `Edge.Cuts` shapes, the board's own and those inside
footprints, into closed polygons. It does this for the 3D viewer, STEP export
and DRC alike. When the chain does not close, KiCad uses the bounding box
instead, so cutouts and milled slots disappear from the 3D view with no error
in the editor. `validate_board_outline` runs that same builder on a saved
`.kicad_pcb` and reports:

- `valid`, and each outline's bounding box and area with its cutouts;
- how many `Edge.Cuts` shapes are on the board and how many are in footprints;
- `dangling_ends`, the ends that meet no other shape within KiCad's chaining
  tolerance, which is where a gap is.

A board can be invalid with no open ends, because its outline crosses or
overlaps itself. Then `run_drc` with `violation_type: "invalid_outline"` gives
the positions.

### Seeing the board in 3D: `render_pcb_3d`

`render_pcb_3d` runs `kicad-cli pcb render`, which draws the same scene as the
3D viewer: outline, cutouts, milled slots, holes, components and 3D models. It
needs no GUI and KiCad can be closed. The tool returns the image itself as well
as the file path, so a model can look at the board instead of asking you to
open the 3D viewer:

```json
{"path": "C:/proj/board.kicad_pcb", "rotate": "-45,0,45", "quality": "high"}
```

`side` is `top`, `bottom`, `left`, `right`, `front` or `back`. `rotate` takes
`X,Y,Z` degrees (`-45,0,45` is isometric). `zoom` and `perspective` adjust the
camera. Renders go to the temp folder unless `output` names a `.png` or `.jpg`,
so the project does not fill up with images. Set `inline` to `false` to get only
the path.

The renderer needs an OpenGL context. On a Linux machine with no display
(no `DISPLAY` or Wayland session, e.g. over SSH) it fails; run the server under
`xvfb-run` there.

### Design rules: `run_drc`

`run_drc` runs `kicad-cli pcb drc` and parses the JSON report rather than
returning a slice of it. A dense board's report is megabytes long, and its first
screenful is usually one kind of violation. So the result counts every violation
by type and by severity, and gives the unconnected and schematic-parity counts.
The list it returns is capped at `limit` and can be narrowed:

```json
{"path": "C:/proj/board.kicad_pcb", "violation_type": "invalid_outline"}
```

`violation_type` takes globs (`silk_*`) and `severity` is `error` or `warning`.
The complete report stays on disk at `report_file`.

### Design variants: `list_variants`

KiCad 10 projects can have design variants: one schematic, several builds that
differ in which parts are fitted, which go in the BOM, or what some fields say.

```json
{"path": "C:/proj/sensor.kicad_sch"}
```

`list_variants` takes any file of the project and returns each variant with its
description and what it changes: the parts it does not fit, the parts it leaves
out of the BOM or the board, and the fields it gives other values. Pass a name
as `variant` to `export_bom`, `export_schematic`, `schematic_netlist` or
`render_pcb_3d` to get that build:

```json
{"path": "C:/proj/sensor.kicad_sch", "variant": "Lite"}
```

Without `variant` they export the default, the design as drawn. A name the
project does not have is refused with the names it does have, because
kicad-cli would quietly export the default instead.

### Importing from Altium: `import_altium`

```json
{"sources": "C:/altium/eDP_adapter"}
```

`sources` is a folder or a list of `.SchDoc` and `.PcbDoc` files. A `.PrjPcb`
is optional: loose sheets and a board are enough, and so are sheets alone or a
board alone. The result is a KiCad project in `<name>_kicad` beside the
sources, whose schematic and board are already linked. The tool refuses a
folder that is not empty unless `overwrite` is set. The Altium files are only
read.

What it does:

- **The board** is converted by `kicad-cli pcb import`, as KiCad's own
  importer does.
- **The schematic** is converted here, because KiCad has no headless importer
  for `.SchDoc`. Wires, buses, junctions, net labels, ports, power ports, sheet
  symbols, notes, text frames and drawings land where Altium put them. Symbols
  are rebuilt from the graphics placed on each sheet, so no Altium library is
  needed. A multi-part component becomes a symbol with several units.
- **The hierarchy** is found from the sheet symbols. A flat design gets a root
  sheet made for it, and a sheet used by several sheet symbols (a channel) is
  written once, with one instance per use.
- **Designators are never changed.** Each footprint is linked to its symbol
  through Altium's UniqueIds, sheet by sheet and channel by channel, so `R12`
  on the schematic is `R12` on the board, `C10A` to `C10H` included.
- **Net names follow the schematic.** Board nets are renamed to what KiCad
  calls them (`/power/SDA`). The symbols' Altium parameters are copied onto
  their footprints, so the first "Update PCB from Schematic" changes nothing.
- **Variants come along.** When a `.PrjPcb` is given, its project variants
  become KiCad design variants. Each variation lands on the part its UniqueId
  path names, channel copies included, on both the schematic and the board.
  "Not fitted" parts are marked DNP and left out of the BOM and the position
  files; alternate parts get the new value, description and footprint fields.
  `list_variants` then shows them, and `export_bom` takes their names.

What it reports:

- **`aligned`** is true only when KiCad's own checks agree: DRC's schematic
  parity, and a pad-by-pad comparison of every net with the board's.
- **`source_disagreements`** lists what the Altium files already disagree on:
  - a part moved to another sheet but never pushed to the board
    (`moved_sheet`);
  - parts on only one side (`board_only`, `schematic_only`);
  - different values or footprints on the two sides;
  - duplicate designators;
  - parts never annotated (`R?`), counted as `not_annotated`, not as duplicates.

  These are listed, not fixed.
- **`not_converted` and `approximations`** list what has no KiCad counterpart:
  directives, harnesses, images linked to files that are missing, rounded
  corners.
- **`inferred`** lists what the tool had to decide, such as sheet names made
  unique or channel designators worked out with no board to read them from.

The full report is `import_report.json` in the project folder.

## Two things to know about KiCad 10

**The API only serves editors that are open.** KiCad registers API handlers per
editor window. If only the project manager is running, live PCB calls fail with
*"no handler available"*. The server translates that into a message telling you
to open the PCB editor. Open it **from the KiCad project manager**, not as a
standalone `pcbnew.exe` — the API socket is owned by one process, and a
standalone editor is a different one.

**There is no schematic IPC API in KiCad 10.** This is not a binding bug. The
`10.0` branch of `schematic_commands.proto` defines zero commands, and the full
schematic type model exists only on `master` (KiCad 11 development).
`kipy` (0.7.1 and 0.8.0 alike) ships `master`-era Python wrappers against
`10.0`-era generated protobuf, which is why `import kipy.schematic` raises
`ImportError`. Regenerating
the protos would clear the import and still return *"no handler available"* on
every call.

So schematic support here is file-based: `kicad-cli` plus direct `.kicad_sch`
parsing. That has an upside — it works with KiCad closed, which is what makes
schematic *generation* possible at all.

### Seeing schematic changes if the file is already open

Because there's no live schematic API, a change any tool here makes to a
`.kicad_sch` never reaches KiCad's in-memory copy on its own — KiCad only reads
the file from disk when told to. If you have the schematic open while a tool
edits it:

- **Use File → Revert**, not just switching windows or waiting. Revert is what
  makes KiCad re-read the file and show the change.
- **Don't hit Save first.** Saving from the GUI writes KiCad's in-memory state
  back over the file, discarding whatever the tool just wrote — the one order
  that loses work.
- If Revert isn't available or doesn't pick it up, close the schematic tab and
  reopen it from the project manager; that always re-reads from disk.

This applies in both directions: if you're editing by hand in KiCad, save from
KiCad *before* asking for another file-based change, or the tool will act on a
stale copy and your hand edits will be the ones lost.

## The escape hatch

`run_kicad_script` executes Python with `kicad`, `board`, `kipy`, `pcbnew`, and
`sexpr` pre-bound. It exists so the server covers the whole API surface without
hundreds of thin tool wrappers, which measurably degrade tool selection.

When KiCad cannot be reached, `kicad` and `board` are `None` and
`connection_error` says why; a script that fails then also returns a
`live_connection` entry with the error and its remedy, instead of leaving only
`'NoneType' object has no attribute ...`. A cached connection is pinged before
reuse, so a KiCad restart does not leave the server holding a dead socket.

`with board_commit("Move J1"):` groups live edits into one undo step; it drops
the commit if the block raises, so a failing script never leaves one open. In
kipy 0.7.1 and 0.8.0 `KIID` takes keywords only -- `KIID(value="...")`, or reuse
an item's `.id`.

**This is arbitrary local code execution with your privileges.** That is the
deliberate trade for full API access. Run this server only against KiCad
instances and files you trust.

## Tests

```bash
.venv/Scripts/python.exe -m pytest -q
```

(`.venv/bin/python` on Linux and macOS.) Tests marked `requires_kicad` need an
installation for its symbol libraries. Tests marked `slow` import a large Altium
design end to end (a few minutes) and are skipped unless asked for:
`pytest -m slow` runs only them, `pytest -m ""` runs everything. The Altium tests
download their sample files once into `tests/.altium_cache/` and skip offline.
The suite runs in parallel on up to 8 processes (`pytest-xdist`, installed by
`.[dev]`); add `-n0` to run it in one process, for instance under a debugger.
Live-IPC behaviour is verified manually —
it needs a GUI session with a board open, which is not worth automating.

## Scope

Windows, Linux (native packages) and macOS (KiCad.app). `discovery.InstallResolver`
is the single platform-aware seam: each platform's resolver knows where KiCad
installs, how an install is laid out, and where it keeps user settings, so the
rest of the package has no `sys.platform` checks. The discovery tests run every
resolver on every host against fake install trees.

## License

MIT

TDQS

A3.6/5.0

Scored across 23 tools

Disambiguation5/5

Each tool targets a distinct resource and action: schematic vs board vs library vs project vs status. The few symbol-related tools are clearly differentiated by their purpose (listing placed symbols, searching libraries, getting pin data).

Naming Consistency4/5

The majority follow a consistent verb_noun pattern (list_*, get_*, run_*, export_*, search_*, open_*, find_*, create_*). Two tools deviate slightly: 'schematic_netlist' and 'kicad_status' are noun_noun, but they remain readable and the overall style is uniform.

Tool Count3/5

At 23 tools, this sits at the high end of the expected range. The count is justified by KiCad's breadth (schematic, board, libraries, execution), but it feels slightly heavy for a single server and will require careful descriptions to avoid overwhelming agents.

Completeness4/5

The tool surface covers the key read/analyze operations for schematics, boards, and libraries, plus generation via create_pinout_schematic and arbitrary extension via run_kicad_script. Missing are explicit edit/update operations for board or schematic elements, but the script runner mitigates those gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues