Files
kicad-mcp-server/src/tools/routing.ts
Gavin Colonese fa6cdcc0cd feat: add mil unit support across position/coordinate commands (#162)
* feat(units): add mil unit support across all position/coordinate commands

KiCad natively supports mils, so the MCP server should too. Added "mil"
as a valid unit option in tool schemas and updated all unit-to-nanometer
scale conversions across component, routing, outline, view, and IPC
handler code paths. 1 mil = 25400 nm (0.0254 mm).

Also fixes a pre-existing mypy overload error in pin_locator.py (str cast
on dict.get key) that was blocking pre-commit on any Python file change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(units): add mil to TypeScript tool schemas

The Python-side mil support was added but the actual input validation
happens in the TypeScript/Zod schemas. Updated all z.enum(["mm", "inch"])
to include "mil" across board, component, routing, design-rules, and
export tool definitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tools): replace CP-1252 mojibake with correct Unicode in board.ts

Replace U+00C3 U+00D7 (×) with U+00D7 (×) in add_logo size output string.
Character was mangled when file was saved as CP-1252 instead of UTF-8.

* fix: restore em-dash and fix pre-commit mypy in component/routing

component.py: replace CP-1252 mojibake (â€") with correct Unicode
em-dash (—) in the 'Add to board first' comment. Addresses
maintainer review on PR #162.

routing.py: annotate ex/ey as float at first assignment site in
_point_to_segment_distance_nm so mypy pre-commit hook passes
cleanly on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:29:34 -04:00

534 lines
17 KiB
TypeScript

/**
* Routing tools for KiCAD MCP server
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerRoutingTools(server: McpServer, callKicadScript: Function) {
// Add net tool
server.tool(
"add_net",
"Create a new net on the PCB",
{
name: z.string().describe("Net name"),
netClass: z.string().optional().describe("Net class name"),
},
async (args: { name: string; netClass?: string }) => {
const result = await callKicadScript("add_net", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Route trace tool
server.tool(
"route_trace",
"Route a trace segment between two XY points on a fixed layer. WARNING: Does NOT handle layer changes — if start and end are on different copper layers, use route_pad_to_pad instead, which automatically inserts a via.",
{
start: z
.object({
x: z.number(),
y: z.number(),
unit: z.string().optional(),
})
.describe("Start position"),
end: z
.object({
x: z.number(),
y: z.number(),
unit: z.string().optional(),
})
.describe("End position"),
layer: z.string().describe("PCB layer"),
width: z.number().describe("Trace width in mm"),
net: z.string().describe("Net name"),
},
async (args: any) => {
const result = await callKicadScript("route_trace", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Route arc trace tool
server.tool(
"route_arc_trace",
"Route a copper arc trace defined by start/mid/end points. Uses true PCB arc primitives when available.",
{
start: z
.object({
x: z.number(),
y: z.number(),
unit: z.string().optional(),
})
.describe("Arc start position"),
mid: z
.object({
x: z.number(),
y: z.number(),
unit: z.string().optional(),
})
.describe("A point on arc midpoint"),
end: z
.object({
x: z.number(),
y: z.number(),
unit: z.string().optional(),
})
.describe("Arc end position"),
layer: z.string().describe("PCB layer"),
width: z.number().describe("Trace width in mm"),
net: z.string().optional().describe("Net name"),
},
async (args: any) => {
const result = await callKicadScript("route_arc_trace", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Add via tool
server.tool(
"add_via",
"Add a via to the PCB",
{
position: z
.object({
x: z.number(),
y: z.number(),
unit: z.string().optional(),
})
.describe("Via position"),
net: z.string().describe("Net name"),
viaType: z.string().optional().describe("Via type (through, blind, buried)"),
},
async (args: any) => {
const result = await callKicadScript("add_via", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Add copper pour tool
server.tool(
"add_copper_pour",
"Add a copper pour (ground/power plane) to the PCB",
{
layer: z.string().describe("PCB layer"),
net: z.string().describe("Net name"),
clearance: z.number().optional().describe("Clearance in mm"),
outline: z
.array(z.object({ x: z.number(), y: z.number() }))
.optional()
.describe(
"Array of {x, y} points defining the pour boundary. If omitted, the board outline is used.",
),
},
async (args: any) => {
const result = await callKicadScript("add_copper_pour", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Delete trace tool
server.tool(
"delete_trace",
"Delete traces from the PCB. Can delete by UUID, position, or bulk-delete all traces on a net.",
{
traceUuid: z.string().optional().describe("UUID of a specific trace to delete"),
position: z
.object({
x: z.number(),
y: z.number(),
unit: z.enum(["mm", "inch", "mil"]).optional(),
})
.optional()
.describe("Delete trace nearest to this position"),
net: z.string().optional().describe("Delete all traces on this net (bulk delete)"),
layer: z.string().optional().describe("Filter by layer when using net-based deletion"),
includeVias: z.boolean().optional().describe("Include vias in net-based deletion"),
},
async (args: any) => {
const result = await callKicadScript("delete_trace", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Query traces tool
server.tool(
"query_traces",
"Query traces on the board with optional filters by net, layer, or bounding box.",
{
net: z.string().optional().describe("Filter by net name"),
layer: z.string().optional().describe("Filter by layer name"),
boundingBox: z
.object({
x1: z.number(),
y1: z.number(),
x2: z.number(),
y2: z.number(),
unit: z.enum(["mm", "inch", "mil"]).optional(),
})
.optional()
.describe("Filter by bounding box region"),
unit: z.enum(["mm", "inch", "mil"]).optional().describe("Unit for coordinates"),
},
async (args: any) => {
const result = await callKicadScript("query_traces", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Query zones tool
server.tool(
"query_zones",
"Query copper zones (filled pours) on the board with optional filters by net, layer, or bounding box. Returns zone net, layers, priority, fill state, and bounding box. Useful for auditing power planes and GND pours that query_traces does not include.",
{
net: z.string().optional().describe("Filter by net name"),
layer: z
.string()
.optional()
.describe("Filter by layer name (matches zones that include this layer)"),
boundingBox: z
.object({
x1: z.number(),
y1: z.number(),
x2: z.number(),
y2: z.number(),
unit: z.enum(["mm", "inch"]).optional(),
})
.optional()
.describe("Filter to zones whose bounding box overlaps this region"),
},
async (args: any) => {
const result = await callKicadScript("query_zones", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// ------------------------------------------------------
// Add GND Stitching Vias Tool
//
// Drops GND stitching vias across the board with full-stackup
// collision detection: every non-GND segment, via, and pad on every
// copper layer is checked, because a PTH via penetrates the whole
// board. Three combinable strategies: regular grid, around named
// refs (densify under MCUs / regulators / RF parts), and in-zones
// only (vias land on actual GND copper, not silkscreen). Supports
// dryRun to preview placements without writing.
//
// Approach ported from morningfire-pcb-automation:
// https://github.com/NiNjA-CodE/morningfire-pcb-automation
// (scripts/ground/add_gnd_vias.py)
// ------------------------------------------------------
server.tool(
"add_gnd_stitching_vias",
"Drop GND stitching vias across the board with collision checking against every non-GND segment, via, and pad on every copper layer (PTH vias penetrate the full stackup, so missing any one layer is the classic silent-short failure mode). Three combinable strategies: `grid` (regular grid across the interior), `around_refs` (densify around named ICs), and `in_zones` (only place vias inside an actual GND copper zone). Supports `dryRun` to preview placements without writing.",
{
gndNet: z
.string()
.optional()
.describe(
"Name of the ground net (default: auto-detect GND / GROUND / VSS / /GND).",
),
strategies: z
.array(z.enum(["grid", "around_refs", "in_zones"]))
.optional()
.describe(
"Which placement strategies to combine (default: ['grid']). Pass ['grid', 'around_refs', 'in_zones'] for full coverage.",
),
viaSize: z.number().optional().describe("Via pad diameter in mm (default 0.6)."),
viaDrill: z
.number()
.optional()
.describe("Via drill diameter in mm (default 0.3). Must be smaller than viaSize."),
clearance: z
.number()
.optional()
.describe(
"Extra clearance beyond required between each new via and existing copper, in mm (default 0.2).",
),
spacing: z
.number()
.optional()
.describe(
"Grid spacing in mm for `grid` and `around_refs` strategies (default 5.0).",
),
densifyRefs: z
.array(z.string())
.optional()
.describe(
"Reference designators to densify ground around (used by `around_refs`). Targets: MCUs, switching regulators, RF parts.",
),
densifyRadius: z
.number()
.int()
.optional()
.describe(
"How many grid cells around each ref to try (default 2 = 5x5 candidate field per ref).",
),
edgeMargin: z
.number()
.optional()
.describe("Keep-out from the board edge in mm (default 0.5)."),
maxVias: z
.number()
.int()
.optional()
.describe("Cap on total placements across all strategies (default unlimited)."),
dryRun: z
.boolean()
.optional()
.describe(
"If true, return the placements that would be made but don't modify the board (default false).",
),
},
async (args: any) => {
const result = await callKicadScript("add_gnd_stitching_vias", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Get nets list tool
server.tool(
"get_nets_list",
"Get a list of all nets in the PCB with optional statistics.",
{
includeStats: z
.boolean()
.optional()
.describe("Include statistics (track count, total length, etc.)"),
unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit for length measurements"),
},
async (args: any) => {
const result = await callKicadScript("get_nets_list", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Modify trace tool
server.tool(
"modify_trace",
"Modify an existing trace (change width, layer, or net).",
{
traceUuid: z.string().describe("UUID of the trace to modify"),
width: z.number().optional().describe("New trace width in mm"),
layer: z.string().optional().describe("New layer name"),
net: z.string().optional().describe("New net name"),
},
async (args: any) => {
const result = await callKicadScript("modify_trace", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Create netclass tool
server.tool(
"create_netclass",
"Create a new net class with custom design rules.",
{
name: z.string().describe("Net class name"),
traceWidth: z.number().optional().describe("Default trace width in mm"),
clearance: z.number().optional().describe("Clearance in mm"),
viaDiameter: z.number().optional().describe("Via diameter in mm"),
viaDrill: z.number().optional().describe("Via drill size in mm"),
},
async (args: any) => {
const result = await callKicadScript("create_netclass", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Route differential pair tool
server.tool(
"route_differential_pair",
"Route a differential pair between two sets of points.",
{
positivePad: z
.object({
reference: z.string(),
pad: z.string(),
})
.describe("Positive pad (component and pad number)"),
negativePad: z
.object({
reference: z.string(),
pad: z.string(),
})
.describe("Negative pad (component and pad number)"),
layer: z.string().describe("PCB layer"),
width: z.number().describe("Trace width in mm"),
gap: z.number().describe("Gap between traces in mm"),
positiveNet: z.string().describe("Positive net name"),
negativeNet: z.string().describe("Negative net name"),
},
async (args: any) => {
const result = await callKicadScript("route_differential_pair", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Refill zones tool
server.tool(
"refill_zones",
"Refill all copper zones on the board. WARNING: SWIG path has known segfault risk (see KNOWN_ISSUES.md). Prefer using IPC backend (KiCAD open) or triggering zone fill via KiCAD UI instead.",
{},
async (args: any) => {
const result = await callKicadScript("refill_zones", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Route pad to pad tool
server.tool(
"route_pad_to_pad",
"PREFERRED tool for pad-to-pad routing. Looks up pad positions automatically, detects the net from the pad, and — critically — if the two pads are on different copper layers (e.g. J1 on F.Cu and J2 on B.Cu) automatically inserts a via at the midpoint so the connection is complete. Always use this instead of route_trace when routing between named component pads.",
{
fromRef: z.string().describe("Reference of the source component (e.g. 'U2')"),
fromPad: z
.union([z.string(), z.number()])
.describe("Pad number on the source component (e.g. '6' or 6)"),
toRef: z.string().describe("Reference of the target component (e.g. 'U1')"),
toPad: z
.union([z.string(), z.number()])
.describe("Pad number on the target component (e.g. '15' or 15)"),
layer: z.string().optional().describe("PCB layer (default: F.Cu)"),
width: z.number().optional().describe("Trace width in mm (default: board default)"),
net: z.string().optional().describe("Net name override (default: auto-detected from pad)"),
},
async (args: any) => {
const result = await callKicadScript("route_pad_to_pad", args);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
// Copy routing pattern tool
server.tool(
"copy_routing_pattern",
"Copy routing pattern (traces and vias) from a group of source components to a matching group of target components. The offset is calculated automatically from the position difference between the first source and first target component. Useful for replicating routing between identical circuit blocks.",
{
sourceRefs: z
.array(z.string())
.describe("References of the source components (e.g. ['U1', 'R1', 'C1'])"),
targetRefs: z
.array(z.string())
.describe(
"References of the target components in same order as sourceRefs (e.g. ['U2', 'R2', 'C2'])",
),
includeVias: z.boolean().optional().describe("Also copy vias (default: true)"),
traceWidth: z
.number()
.optional()
.describe("Override trace width in mm (default: keep original width)"),
},
async (args: any) => {
const result = await callKicadScript("copy_routing_pattern", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
}