feat: Add missing routing/component tools and fix SWIG/UUID bugs
New MCP tools added (TypeScript layer): - Routing: delete_trace, query_traces, get_nets_list, modify_trace, create_netclass, route_differential_pair, refill_zones (with SWIG warning) - Component: get_component_pads, get_component_list, get_pad_position, place_component_array, align_components, duplicate_component Bug fixes: - routing.py: Fix SwigPyObject UUID comparison (str() -> m_Uuid.AsString()) - routing.py: Fix SWIG iterator invalidation after board.Remove() by converting board.Tracks() to list() before iteration - routing.py: Add board.SetModified() and clear Python refs after Remove() to prevent dangling SWIG pointers crashing subsequent calls - routing.py: Wrap each track access in try/except in query_traces() to gracefully skip invalid track objects after bulk delete - routing.py: Add missing return statement (mypy fix) - library.py: Fix search_footprints param mapping search_term->pattern - library.py: Fix fp.name -> fp.full_name field access - library.py: Accept both 'pattern' and 'search_term' parameter names - library.py: Add library filter support in search_footprints - library.py: Fix loop variable shadowing Path object (mypy fix) - design_rules.py: Add type annotation for violation_counts (mypy fix) Contribution guidelines followed (CONTRIBUTING.md): - black python/ applied (3 touched files pass without changes) - mypy python/ passes with 0 errors on all changed files - npx prettier --write applied to changed TypeScript files - npm run build passes (tsc --noEmit: 0 errors) - Commit messages follow feat:/fix: convention Note on diff size: The large insertion/deletion count in the changed files is due to black and prettier reformatting previously unformatted code (missing trailing commas, quote style, line length). The actual logic changes are limited to the 6 files listed above. Note: refill_zones has known SWIG segfault risk (see KNOWN_ISSUES.md). Prefer IPC backend (KiCAD open) or zone fill via KiCAD UI.
This commit is contained in:
@@ -1,101 +1,324 @@
|
||||
/**
|
||||
* 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 between two points",
|
||||
{
|
||||
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)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// 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"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("add_copper_pour", args);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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 between two points",
|
||||
{
|
||||
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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 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"),
|
||||
},
|
||||
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"]).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"]).optional(),
|
||||
})
|
||||
.optional()
|
||||
.describe("Filter by bounding box region"),
|
||||
unit: z.enum(["mm", "inch"]).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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 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", "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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user