feat(schematic): batch component authoring + connection tools
Adds batch authoring tools that collapse the dozens of round-trips needed to stand up a schematic into a handful: batch_add_components, batch_edit_schematic_components, replace_schematic_component, batch_add_no_connects, batch_connect, and batch_add_and_connect (place a set of parts and wire them in one call). These reuse the existing single-item handlers internally, so behavior matches exactly. - python/commands/schematic_batch.py: SchematicBatchCommands(iface) - python/commands/schematic_text_utils.py: shared .kicad_sch text helpers - src/tools/schematic-batch.ts + registry 'schematic_batch' category - python/kicad_interface.py: import + instantiate + dispatch routes - tests/test_schematic_batch.py: 13 unit tests Note: schematic_text_utils.py is also shipped by the field-placement and hierarchy PRs; if those merge first, drop the duplicate copy on rebase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import { registerLibraryTools } from "./tools/library.js";
|
||||
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
||||
import { registerSchematicHierarchyTools } from "./tools/schematic-hierarchy.js";
|
||||
import { registerSchematicLayoutTools } from "./tools/schematic-layout.js";
|
||||
import { registerSchematicBatchTools } from "./tools/schematic-batch.js";
|
||||
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
||||
import { registerDatasheetTools } from "./tools/datasheet.js";
|
||||
import { registerFootprintTools } from "./tools/footprint.js";
|
||||
@@ -300,6 +301,7 @@ export class KiCADMcpServer {
|
||||
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||
registerSchematicHierarchyTools(this.server, this.callKicadScript.bind(this));
|
||||
registerSchematicLayoutTools(this.server, this.callKicadScript.bind(this));
|
||||
registerSchematicBatchTools(this.server, this.callKicadScript.bind(this));
|
||||
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
||||
registerDatasheetTools(this.server, this.callKicadScript.bind(this));
|
||||
registerFootprintTools(this.server, this.callKicadScript.bind(this));
|
||||
|
||||
@@ -138,6 +138,19 @@ export const toolCategories: ToolCategory[] = [
|
||||
"autoplace_schematic_fields",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "schematic_batch",
|
||||
description:
|
||||
"Batch schematic authoring: add/edit/replace components, batch no-connects, batch connect, add-and-connect",
|
||||
tools: [
|
||||
"batch_add_components",
|
||||
"batch_edit_schematic_components",
|
||||
"replace_schematic_component",
|
||||
"batch_add_no_connects",
|
||||
"batch_connect",
|
||||
"batch_add_and_connect",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "routing",
|
||||
description: "Advanced routing operations: vias, copper pours",
|
||||
|
||||
173
src/tools/schematic-batch.ts
Normal file
173
src/tools/schematic-batch.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Batch schematic authoring tools.
|
||||
*
|
||||
* Place / edit / connect many things in one call to avoid per-item round-trips.
|
||||
*/
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerSchematicBatchTools(server: McpServer, callKicadScript: Function) {
|
||||
// Add many components at once
|
||||
server.tool(
|
||||
"batch_add_components",
|
||||
"Add multiple components to a schematic in one call (far fewer round-trips than add_schematic_component). Each component: {symbol:'Library:Name', reference, value?, footprint?, position:{x,y}, rotation?, includePins?}. Reference/Value fields are auto-positioned outside the body (disable with auto_position_fields=false). Returns per-component snapped position, field positions, body_bbox, and an overall placement_bbox.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
components: z
|
||||
.array(
|
||||
z.object({
|
||||
symbol: z.string().describe("'Library:SymbolName' (e.g., Device:R)"),
|
||||
reference: z.string().describe("Reference designator (e.g., R1)"),
|
||||
value: z.string().optional(),
|
||||
footprint: z.string().optional(),
|
||||
position: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||
rotation: z.number().optional(),
|
||||
includePins: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.describe("Components to place"),
|
||||
origin_x: z.number().optional().describe("X offset added to every component position (mm)"),
|
||||
origin_y: z.number().optional().describe("Y offset added to every component position (mm)"),
|
||||
auto_position_fields: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe("Auto-place Ref/Value fields outside the body (default true)"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const r = await callKicadScript("batch_add_components", args);
|
||||
if (r.success === false && r.message)
|
||||
return { content: [{ type: "text", text: `Failed: ${r.message}` }] };
|
||||
const lines = [`Added ${r.added_count} component(s), ${r.error_count} error(s).`];
|
||||
for (const e of r.errors || []) lines.push(` ✗ ${e.reference} (${e.symbol}): ${e.error}`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
},
|
||||
);
|
||||
|
||||
// Edit many components at once
|
||||
server.tool(
|
||||
"batch_edit_schematic_components",
|
||||
"Edit multiple existing components in one call. 'components' is a map {reference: {value?, footprint?, newReference?, ...}}; each entry is applied via the single-component editor. Returns per-reference updated/errors.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
components: z
|
||||
.record(z.string(), z.record(z.string(), z.any()))
|
||||
.describe("Map of reference -> fields to change"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const r = await callKicadScript("batch_edit_schematic_components", args);
|
||||
if (r.success === false && r.message)
|
||||
return { content: [{ type: "text", text: `Failed: ${r.message}` }] };
|
||||
const lines = [`Updated ${r.updated_count}, ${r.error_count} error(s).`];
|
||||
for (const e of r.errors || []) lines.push(` ✗ ${e.reference}: ${e.error}`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
},
|
||||
);
|
||||
|
||||
// Swap a symbol, preserving position/fields
|
||||
server.tool(
|
||||
"replace_schematic_component",
|
||||
"Replace a placed component's symbol with a different one (e.g. Device:R -> Device:R_Potentiometer), preserving its position, rotation, and field values (Value/Footprint/custom). Optionally override rotation with newRotation. Returns the new pin positions.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
reference: z.string().describe("Reference of the component to replace (e.g., D1)"),
|
||||
newSymbol: z.string().describe("New symbol in 'Library:Symbol' form (e.g., Device:D_Zener)"),
|
||||
newRotation: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Override rotation in degrees (default: keep existing)"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const r = await callKicadScript("replace_schematic_component", args);
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: r.success ? r.message : `Failed: ${r.message || "Unknown error"}` },
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// No-connect flags on many pins
|
||||
server.tool(
|
||||
"batch_add_no_connects",
|
||||
"Add no-connect (X) flags to multiple pins in one call, to mark intentionally unconnected pins and silence ERC. 'pins' is a list of {componentRef, pinName}.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
pins: z
|
||||
.array(z.object({ componentRef: z.string(), pinName: z.string() }))
|
||||
.describe("Pins to mark no-connect"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const r = await callKicadScript("batch_add_no_connects", args);
|
||||
if (r.success === false && r.message)
|
||||
return { content: [{ type: "text", text: `Failed: ${r.message}` }] };
|
||||
const lines = [r.message];
|
||||
for (const f of r.failed || []) lines.push(` ✗ ${f.componentRef}/${f.pinName}: ${f.reason}`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
},
|
||||
);
|
||||
|
||||
// Net labels on many pins
|
||||
server.tool(
|
||||
"batch_connect",
|
||||
"Place net labels on multiple pins in one call to wire nets quickly. 'connections' is a map {reference: {pin: netName}} where pin is a number or name. If a facing label for the same net is nearby, a wire is drawn instead of a duplicate label. Set replace=true to clear any existing label at a pin first. Warns about power nets missing a PWR_FLAG.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
connections: z
|
||||
.record(z.string(), z.record(z.string(), z.string()))
|
||||
.describe("Map of reference -> {pin: netName}"),
|
||||
replace: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe("Delete existing labels at each pin before placing (default false)"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const r = await callKicadScript("batch_connect", args);
|
||||
if (r.success === false && r.message)
|
||||
return { content: [{ type: "text", text: `Failed: ${r.message}` }] };
|
||||
const lines = [r.message];
|
||||
for (const f of r.failed || []) lines.push(` ✗ ${f.ref}/${f.pin}: ${f.reason}`);
|
||||
for (const w of r.warnings || []) lines.push(` ⚠ ${w}`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
},
|
||||
);
|
||||
|
||||
// Place + wire in one call
|
||||
server.tool(
|
||||
"batch_add_and_connect",
|
||||
"Place multiple components AND wire their nets in a single call — the fewest-round-trip way to build a subcircuit. Each component is like batch_add_components plus an optional 'nets' map {pin: netName}. Components are placed first, then nets are connected via batch_connect.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
components: z
|
||||
.array(
|
||||
z.object({
|
||||
symbol: z.string(),
|
||||
reference: z.string(),
|
||||
value: z.string().optional(),
|
||||
footprint: z.string().optional(),
|
||||
position: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||
rotation: z.number().optional(),
|
||||
nets: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Map of pin -> netName to label after placement"),
|
||||
}),
|
||||
)
|
||||
.describe("Components to place and connect"),
|
||||
origin_x: z.number().optional(),
|
||||
origin_y: z.number().optional(),
|
||||
},
|
||||
async (args: any) => {
|
||||
const r = await callKicadScript("batch_add_and_connect", args);
|
||||
if (r.success === false && r.message)
|
||||
return { content: [{ type: "text", text: `Failed: ${r.message}` }] };
|
||||
const lines = [r.message];
|
||||
for (const e of r.errors || []) lines.push(` ✗ add ${e.reference}: ${e.error}`);
|
||||
for (const f of r.failed_connections || [])
|
||||
lines.push(` ✗ connect ${f.ref}/${f.pin}: ${f.reason}`);
|
||||
for (const w of r.warnings || []) lines.push(` ⚠ ${w}`);
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user