Developer Guide
August 1, 2026 · View on GitHub
Internal reference for developing and maintaining the Rust port.
Repository-wide naming, public API, branch, and pull-request rules live in docs/NAMING_CONVENTIONS.md.
Quick Start
# protoc is required for protobuf code generation. If PROTOC is unset, the
# build falls back to `protoc` on PATH (see Build Requirements below).
set PROTOC=C:\path\to\protoc.exe # or install via `choco install protoc`
cargo check # verify everything compiles (~15s)
cargo test --workspace --lib --tests # all tests
cargo build --release -p konnect # build the MCP server binary
# Build the schematic viewer (separate crate)
cd crates/schematic-viewer
cargo build --release
Schematic-viewer build notes (Windows):
- If
cargois not recognized in a fresh shell, add it to the session PATH first:set PATH=%PATH%;%USERPROFILE%\.cargo\bin - Close any running viewer window before rebuilding — Windows locks a running
.exe, so the link step fails while the app is open.
Architecture
Konnect/
├── crates/
│ ├── konnect/ # Main binary + cdylib entry points
│ │ └── src/
│ │ ├── main.rs # CLI: --config, subcommands
│ │ ├── lib.rs # cdylib re-exports ffi
│ │ ├── ffi.rs # C ABI: kicad_plugin_init/version/shutdown
│ │ ├── config.rs # TOML + JSON config, socket path auto-detection
│ │ └── transport/
│ │ ├── stdio.rs # Line-by-line JSON-RPC over stdin/stdout (default)
│ │ └── http.rs # Streamable HTTP: POST + GET (SSE) on /mcp (transport = "http" / "both")
│ │
│ ├── konnect-core/ # All tool logic (18 toolsets)
│ │ └── src/
│ │ ├── mcp/
│ │ │ ├── protocol.rs # MCP JSON-RPC 2.0 types
│ │ │ ├── handler.rs # Dispatch: initialize, tools/list (all tools static), tools/call
│ │ │ └── server.rs # Session state machine
│ │ ├── router/
│ │ │ ├── mod.rs # ToolRouter: load/unload toolsets
│ │ │ ├── registry.rs # Static toolset metadata + tools_for() dispatcher
│ │ │ └── meta_tools.rs # 6 always-visible meta-tools
│ │ └── tools/
│ │ ├── mod.rs # ToolDef, ToolContext, tool! macro, helpers, kicad_config_dir(), resolve_lib_symbol()
│ │ ├── cli.rs # kicad-cli v10 subprocess wrapper (verified against actual binary)
│ │ ├── svg_import.rs # SVG parsing + Bezier flattening for import_svg_logo (usvg-backed)
│ │ ├── project.rs # 6 tools (incl. open_schematic_viewer)
│ │ ├── sch_components.rs # 17 tools (component placement with lib_symbols embedding)
│ │ ├── sch_wiring.rs # 19 tools (incl. connect_pins, power symbol embedding)
│ │ ├── sch_analysis.rs # 15 tools (union-find net graph, connectivity)
│ │ ├── sch_batch.rs # 12 tools (single-read/single-write atomic operations)
│ │ ├── sch_export.rs # 6 tools (SVG/PDF/netlist/ERC)
│ │ ├── sch_hierarchy.rs # 12 tools (typed Sheet model, sheet CRUD + hierarchy/page queries + pin lifecycle)
│ │ ├── pcb_board.rs # 11 tools (S-expr file editing, IPC fallback, SVG logo import)
│ │ ├── pcb_components.rs # 13 tools (IPC real-time via NNG+protobuf)
│ │ ├── pcb_routing.rs # 12 tools (traces, vias, nets, netclasses)
│ │ ├── pcb_export.rs # 13 tools (Gerber, PDF, 3D, DRC, DXF/GenCAD/IPC-2581/ODB++)
│ │ ├── library.rs # 14 tools (symbol/footprint library management)
│ │ ├── integration.rs # 9 tools (JLCPCB SQLite, Freerouting, datasheets)
│ │ ├── verification.rs # 8 tools (DRC, design rules, KiCAD UI)
│ │ ├── config.rs # 7 tools (user/project config, design rules)
│ │ ├── design_review.rs # 6 tools (decoupling/connection/power/DFM audits)
│ │ ├── templates.rs # 4 tools (6 built-in reference circuit templates)
│ │ └── manufacturing.rs # 3 tools (export package, validate, cost estimate)
│ │
│ ├── konnect-sexp/ # S-expression engine (no KiCAD dependency)
│ │ └── src/
│ │ ├── parser.rs # nom-based parser (handles empty strings)
│ │ ├── writer.rs # SexpEdit + apply_edits + write_atomic
│ │ ├── schematic.rs # SymbolInstance, LibPin, extract_*, pin_endpoint
│ │ └── geometry.rs # PinTransform, transform_pin (CANONICAL pin math)
│ │
│ ├── konnect-ipc/ # KiCAD 10 IPC API client
│ │ ├── proto/ # Protobuf definitions (copied from KiCAD v10 source)
│ │ ├── build.rs # prost-build protobuf code generation
│ │ └── src/
│ │ ├── gen.rs # Generated protobuf Rust types
│ │ ├── client.rs # NNG req/rep client, all methods implemented
│ │ ├── builders.rs # Protobuf message construction helpers (mm→nm conversion)
│ │ └── types.rs # Public types (IpcFootprint, IpcTrack, etc.)
│ │
│ └── schematic-viewer/ # Tauri desktop app (separate from workspace)
│ ├── tauri.conf.json
│ ├── capabilities/default.json # Tauri 2 ACL grant (core:default) — without it event.listen() is silently denied
│ ├── src/main.rs # Multi-sheet watcher + snapshot-isolated incremental kicad-cli SVG rendering + Tauri commands, 20 unit tests
│ └── frontend/index.html # Pan/zoom SVG viewer, sheet selector, auto-refresh
│
├── plugin/ # Python thin launcher (runs inside KiCAD)
│ ├── __init__.py # pcbnew.ActionPlugin — settings dialog (PCB Editor only)
│ ├── settings_dialog.py # wxPython settings UI (paths, server control)
│ └── plugin.json # KiCAD 10 IPC plugin manifest
│
├── packaging/
│ ├── build-pcm.ps1 # Build the PCM zip (Windows)
│ ├── build-pcm.sh # Build the PCM zip (macOS/Linux)
│ ├── metadata.json # KiCAD PCM package manifest
│ ├── validate-pcm.py # Validate metadata.json against the PCM schema
│ ├── schema/ # PCM packages.v1 JSON schema
│ └── resources/ # PCM package resources (icon.png)
│
└── .github/workflows/
├── ci.yml # Check + test + clippy on 3 platforms
├── e2e-kicad.yml # End-to-end tests against a real KiCAD install
└── release.yml # Build binaries + GitHub Release on tag push
KiCAD 10 Integration
IPC API (PCB Editor — real-time)
- Transport: NNG (nanomsg-next-gen) over IPC sockets (Windows named pipes)
- Protocol: Protocol Buffers (protobuf3) with ApiRequest/ApiResponse envelope
- Socket path: from
KICAD_API_SOCKETenvironment variable (set by KiCAD when launching plugins) - Scope: PCB editor only — full CRUD on all board items, layer management, design rules
- Schematic editor IPC: export-only (SVG, PDF, BOM, netlist) — NO item CRUD
S-Expression File Editing (Schematic — offline)
- Direct read/write of
.kicad_schfiles - Symbol definitions auto-embedded from KiCAD 10's
.kicad_symdirformat - Power symbols (VCC, GND) embedded from
power.kicad_symdir - Existing-file edits use revision-checked atomic replacement: read the exact source, acquire a cooperative lock, reject any intervening KiCad or Konnect change, write a unique sibling scratch file, fsync, and rename.
- Cooperative lock files live under
KONNECT_STATE_DIR/lockswhen that absolute override is set, otherwise under the platform local-data directory (konnect/locks). Reads never create files in the KiCad project. - Multi-file schematic changes use project-local
.konnect-transaction-*.jsonwrite-ahead journals. These journals contain complete before/after images and must be treated as sensitive project data.
konnect_schematic_editor::Schematic deliberately distinguishes creation from
replacement:
save(new_path)is create-only and refuses to replace an existing path.save(loaded_path)andoverwrite()replace only when the file still exactly matches the source loaded into the model. KiCad autosave therefore produces a conflict that callers must resolve by reloading and reapplying.- Callers that intentionally replace an existing file must use the explicit
revision-aware writer/command APIs; they must not delete the destination or
weaken
save()into an unconditional overwrite.
For journal diagnosis and recovery, use konnect transaction status,
konnect transaction recover, and the explicit force-gated konnect transaction abandon escape hatch documented in
Troubleshooting.
kicad-cli v10 (Subprocess)
- Verified commands:
sch erc,sch export svg/pdf/bom/netlist,pcb drc,pcb export gerbers/drill/pdf/svg/step/vrml/pos/ipcd356,pcb render - Removed in v10:
sch annotate(reimplemented in Rust),pcb sync,pcb export/import specctra - Version format:
20250610
Plugin Installation
- PCM zip is the correct install method
- KiCAD installs to:
C:\KiCad\10.0\share\kicad\scripting\plugins\konnect\ - Both
__init__.py(SWIG ActionPlugin for PCB editor settings dialog) andplugin.json(IPC exec plugin) are included
Structured Errors
Tool-call failures are typed via the ToolErrorKind enum in crates/konnect-core/src/mcp/error.rs. MCP's CallToolResult spec has no top-level data field, so structured errors ride inside the text content as JSON:
{
"message": "Tool 'place_component' is in toolset 'pcb_components' — call load_toolset('pcb_components') first, then retry.",
"error": {
"kind": "toolset_not_loaded",
"toolset": "pcb_components",
"tool": "place_component"
}
}
is_error: true on the result; plain clients show the message field, structured clients match on kind. The observer's error_kind column is populated via extract_error_kind() so JSONL logs use the same vocabulary regardless of where the error originated.
Current kinds
kind | When |
|---|---|
toolset_not_loaded | Tool exists but its toolset isn't loaded yet |
unknown_tool | Tool name doesn't exist in any toolset |
invalid_argument | Required argument missing/malformed |
file_not_found | Referenced file doesn't exist |
handler_error | Catch-all for unmigrated anyhow::Error returns |
Producing structured errors in a handler
if !path.exists() {
return Ok(CallToolResult::error_kind(
ToolErrorKind::FileNotFound { path: path.display().to_string() },
format!("Project file not found: {}", path.display()),
));
}
Adding a new kind: edit mcp/error.rs, add the variant, add the match arm in short_code(), use it from the handler. The short_code_matches_serialized_kind_field test will fail loudly if they drift.
The dispatch-level errors (not-loaded/unknown/handler-panic) are fully structured. So are all missing-argument errors across all 187 tools — tools/mod.rs::require_str / require_f64 emit ToolErrorKind::InvalidArgument { field, reason } automatically. Most in-handler errors still use CallToolResult::error("free text") or bubble anyhow::Error; migrating them is incremental. project.rs::handle_get_project_info demonstrates the structured FileNotFound pattern.
Observability
Every tools/call flows through McpHandler::execute_tool, which wraps the dispatch with:
- A ring buffer of the last 100
CallRecords (surfaced viaget_recent_callsmeta-tool). - Per-tool counters for totals, errors, cumulative duration, last-status, last-error (surfaced via
server_stats). - JSONL append to
<konnect dir>/logs/calls.jsonl(one line per call). Paths:- Windows:
%APPDATA%\konnect\logs\calls.jsonl - macOS:
~/Library/Application Support/konnect/logs/calls.jsonl - Linux:
~/.konnect/logs/calls.jsonl
- Windows:
- Structured
tracingevents (tool_call_start+tool_call_end) carryingcall_id,tool,toolset,status,dur_ms— greppable in the stderr log.
Each CallRecord includes: call_id, ts (unix ms), tool, toolset (optional — None for meta-tools), dur_ms, status (ok / error / not_found), error_kind, args_bytes, result_bytes.
The observer is constructed once by McpHandler::new and stashed on both the handler and ToolContext so meta-tools can reach it. IO failures on the JSONL file never fail the tool call — they tracing::warn! and are silently dropped. Tests construct an in-memory-only observer via ToolContext::new(...) (no log_path).
Source: crates/konnect-core/src/observability.rs.
Tool Routing (Starter Kit + On-Demand Loading)
The server does NOT expose all 187 tools (193 total with the 6 meta-tools) in tools/list by default — that would cost ~23K tokens of context on every listing. Instead:
- Startup: only
STARTER_KITtoolsets are pre-loaded (seerouter/registry.rs::STARTER_KIT). Currently:project,config. Combined with the 6 meta-tools, baselinetools/listis ~19 tools ≈ 2K tokens. - On demand: the LLM reads
list_toolboxes→ callsload_toolset(name)to expose a toolset's tools in subsequenttools/listresponses.unload_toolset(name)prunes them when the task shifts. tools/list_changednotification: sent on every load/unload so MCP clients refresh their local tool cache.- Error recovery: if the LLM calls an unloaded tool,
handler.rsreturns an actionable error naming the toolset that owns it (so the LLM can load it and retry in one hop — no extralist_toolboxesround-trip).
The router is defined in crates/konnect-core/src/router/mod.rs.
Build Requirements
- Rust toolchain pinned by
rust-toolchain.toml(currently 1.96.0) — rustup picks it up automatically, and CI compiles with the same version. The pinned version IS the MSRV: bump it deliberately, in its own commit, after running the full local gate on the new version. protocbinary (for protobuf code generation in konnect-ipc crate)- Set
PROTOCenvironment variable, or leave it unset andkonnect-ipc/build.rsfalls back toprotocfound on PATH - Well-known-type includes are derived from
<PROTOC>/../../include(i.e. a standard protoc release layout withbin/protocnext toinclude/) when that directory exists - Download: https://github.com/protocolbuffers/protobuf/releases
- Set
- For schematic-viewer (built separately from the workspace — see Quick Start):
- Rust toolchain on PATH (Windows:
set PATH=%PATH%;%USERPROFILE%\.cargo\binifcargoisn't recognized in the shell) - Tauri 2 prerequisites: WebView2 runtime on Windows (usually pre-installed on Win 10/11)
- At runtime it discovers
kicad-clifrom the standard KiCAD install paths, then PATH; override with--kicad-cli <path> - Rebuilds fail while a viewer window is open (Windows locks the running
.exe) — close the app beforecargo build
- Rust toolchain on PATH (Windows:
Test Suite
Run all: PROTOC=<path> cargo test --workspace --lib --tests
| Location | What |
|---|---|
konnect-sexp unit tests | Parser, writer, geometry transforms |
konnect-core unit tests | Router load/unload, starter-kit, registry invariants, observability, error taxonomy, arg helpers |
konnect-core integration tests | Fixture files: parse, edit, write, observability, structured errors |
konnect-schematic-editor tests | Typed schematic model + round-tripping |
schematic-viewer is excluded from the workspace (Cargo.toml's [workspace] exclude) since
it's a Tauri app built separately — cargo test --workspace never touches it, and neither does
CI (.github/workflows/ci.yml runs everything with --workspace). Run its tests explicitly:
cd crates/schematic-viewer && cargo test. Its 20 unit tests cover the pure sheet-tree-walking,
watch-directory, render-snapshot, event-debounce, and incremental-render-selection logic
(walk_sheet_tree, compute_watch_dirs, snapshot_tree, drain_until_quiet,
files_needing_render, render_all's error handling) — the actual kicad-cli subprocess call
and Tauri command/event plumbing stay thin and untested, matching this codebase's existing
convention for other kicad-cli-calling code.
Adding a New Tool
- Add the
tool!(...)definition to the appropriate toolset'stools()vec - Write the
async fn handle_*()handler below the tools vec - Update
tool_countinrouter/registry.rs::ALL_TOOLSETS— this is the declared count shown inlist_toolboxes - If the new tool belongs in the default-available set, add its toolset to
registry.rs::STARTER_KIT - Run
cargo checkand re-run the tool-directory extraction (seetool-directory.mdheader) to keep the docs in sync
Current Stats
- 18 toolsets, 187 tools + 6 meta-tools (4 routing + 2 observability — see
tool-directory.md) - Baseline
tools/list: ~19 tools / ~2K tokens (starter kit + meta-tools) - Full-catalog
tools/list(all loaded): 193 tools (187 registered + 6 meta) / ~25K tokens - 0 IPC stubs (all protobuf methods implemented)
- 0 unimplemented tools
- 3 CLI commands removed in KiCAD v10 (specctra DSN/SES, pcb sync — return clear errors)