style: apply Prettier formatting to TS/JS/JSON/MD files
Add Prettier as a dev dependency with .prettierrc.json config and .prettierignore. Hook added via mirrors-prettier in pre-commit config. All TypeScript, JSON, Markdown, and YAML files auto-formatted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,384 +1,431 @@
|
||||
/**
|
||||
* Board management tools for KiCAD MCP server
|
||||
*
|
||||
* These tools handle board setup, layer management, and board properties
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register board management tools with the MCP server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerBoardTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info('Registering board management tools');
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Board Size Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_board_size",
|
||||
{
|
||||
width: z.number().describe("Board width"),
|
||||
height: z.number().describe("Board height"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
||||
},
|
||||
async ({ width, height, unit }) => {
|
||||
logger.debug(`Setting board size to ${width}x${height} ${unit}`);
|
||||
const result = await callKicadScript("set_board_size", {
|
||||
width,
|
||||
height,
|
||||
unit
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Layer Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_layer",
|
||||
{
|
||||
name: z.string().describe("Layer name"),
|
||||
type: z.enum([
|
||||
"copper", "technical", "user", "signal"
|
||||
]).describe("Layer type"),
|
||||
position: z.enum([
|
||||
"top", "bottom", "inner"
|
||||
]).describe("Layer position"),
|
||||
number: z.number().optional().describe("Layer number (for inner layers)")
|
||||
},
|
||||
async ({ name, type, position, number }) => {
|
||||
logger.debug(`Adding ${type} layer: ${name}`);
|
||||
const result = await callKicadScript("add_layer", {
|
||||
name,
|
||||
type,
|
||||
position,
|
||||
number
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Active Layer Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_active_layer",
|
||||
{
|
||||
layer: z.string().describe("Layer name to set as active")
|
||||
},
|
||||
async ({ layer }) => {
|
||||
logger.debug(`Setting active layer to: ${layer}`);
|
||||
const result = await callKicadScript("set_active_layer", { layer });
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Board Info Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_board_info",
|
||||
{},
|
||||
async () => {
|
||||
logger.debug('Getting board information');
|
||||
const result = await callKicadScript("get_board_info", {});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Layer List Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_layer_list",
|
||||
{},
|
||||
async () => {
|
||||
logger.debug('Getting layer list');
|
||||
const result = await callKicadScript("get_layer_list", {});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Board Outline Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_board_outline",
|
||||
{
|
||||
shape: z.enum(["rectangle", "circle", "polygon", "rounded_rectangle"]).describe("Shape of the outline"),
|
||||
params: z.object({
|
||||
// For rectangle / rounded_rectangle
|
||||
width: z.number().optional().describe("Width of rectangle"),
|
||||
height: z.number().optional().describe("Height of rectangle"),
|
||||
cornerRadius: z.number().optional().describe("Corner radius for rounded_rectangle (mm)"),
|
||||
// For circle
|
||||
radius: z.number().optional().describe("Radius of circle"),
|
||||
// For polygon
|
||||
points: z.array(
|
||||
z.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate")
|
||||
})
|
||||
).optional().describe("Points of polygon"),
|
||||
// Position: top-left corner for rectangles/rounded_rectangle, center for circle
|
||||
x: z.number().describe("X coordinate of top-left corner for rectangles (default: 0)"),
|
||||
y: z.number().describe("Y coordinate of top-left corner for rectangles (default: 0)"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
||||
}).describe("Parameters for the outline shape")
|
||||
},
|
||||
async ({ shape, params }) => {
|
||||
logger.debug(`Adding ${shape} board outline`);
|
||||
// Pass x/y as-is to Python; outline.py treats them as top-left corner
|
||||
// and computes the center internally (center = x + width/2, y + height/2).
|
||||
const result = await callKicadScript("add_board_outline", {
|
||||
shape,
|
||||
...params
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Mounting Hole Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_mounting_hole",
|
||||
{
|
||||
position: z.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
||||
}).describe("Position of the mounting hole"),
|
||||
diameter: z.number().describe("Diameter of the hole"),
|
||||
padDiameter: z.number().optional().describe("Optional diameter of the pad around the hole")
|
||||
},
|
||||
async ({ position, diameter, padDiameter }) => {
|
||||
logger.debug(`Adding mounting hole at (${position.x},${position.y}) ${position.unit}`);
|
||||
const result = await callKicadScript("add_mounting_hole", {
|
||||
position,
|
||||
diameter,
|
||||
padDiameter
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Text Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_board_text",
|
||||
{
|
||||
text: z.string().describe("Text content"),
|
||||
position: z.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
||||
}).describe("Position of the text"),
|
||||
layer: z.string().describe("Layer to place the text on"),
|
||||
size: z.number().describe("Text size"),
|
||||
thickness: z.number().optional().describe("Line thickness"),
|
||||
rotation: z.number().optional().describe("Rotation angle in degrees"),
|
||||
style: z.enum(["normal", "italic", "bold"]).optional().describe("Text style")
|
||||
},
|
||||
async ({ text, position, layer, size, thickness, rotation, style }) => {
|
||||
logger.debug(`Adding text "${text}" at (${position.x},${position.y}) ${position.unit}`);
|
||||
const result = await callKicadScript("add_board_text", {
|
||||
text,
|
||||
position,
|
||||
layer,
|
||||
size,
|
||||
thickness,
|
||||
rotation,
|
||||
style
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Zone Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_zone",
|
||||
{
|
||||
layer: z.string().describe("Layer for the zone"),
|
||||
net: z.string().describe("Net name for the zone"),
|
||||
points: z.array(
|
||||
z.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate")
|
||||
})
|
||||
).describe("Points defining the zone outline"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
clearance: z.number().optional().describe("Clearance value"),
|
||||
minWidth: z.number().optional().describe("Minimum width"),
|
||||
padConnection: z.enum(["thermal", "solid", "none"]).optional().describe("Pad connection type")
|
||||
},
|
||||
async ({ layer, net, points, unit, clearance, minWidth, padConnection }) => {
|
||||
logger.debug(`Adding zone on layer ${layer} for net ${net}`);
|
||||
const result = await callKicadScript("add_zone", {
|
||||
layer,
|
||||
net,
|
||||
points,
|
||||
unit,
|
||||
clearance,
|
||||
minWidth,
|
||||
padConnection
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Board Extents Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_board_extents",
|
||||
{
|
||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit of measurement for the result")
|
||||
},
|
||||
async ({ unit }) => {
|
||||
logger.debug('Getting board extents');
|
||||
const result = await callKicadScript("get_board_extents", { unit });
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Board 2D View Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_board_2d_view",
|
||||
{
|
||||
layers: z.array(z.string()).optional().describe("Optional array of layer names to include"),
|
||||
width: z.number().optional().describe("Optional width of the image in pixels"),
|
||||
height: z.number().optional().describe("Optional height of the image in pixels"),
|
||||
format: z.enum(["png", "jpg", "svg"]).optional().describe("Image format")
|
||||
},
|
||||
async ({ layers, width, height, format }) => {
|
||||
logger.debug('Getting 2D board view');
|
||||
const result = await callKicadScript("get_board_2d_view", {
|
||||
layers,
|
||||
width,
|
||||
height,
|
||||
format
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Board management tools registered');
|
||||
|
||||
// Import SVG logo onto PCB layer (silkscreen)
|
||||
server.tool(
|
||||
"import_svg_logo",
|
||||
"Imports an SVG file as filled graphic polygons onto a KiCAD PCB layer (default F.SilkS / front silkscreen). Curves are linearised automatically. Ideal for placing a company or project logo on the board.",
|
||||
{
|
||||
pcbPath: z.string().describe("Path to the .kicad_pcb file"),
|
||||
svgPath: z.string().describe("Path to the SVG logo file"),
|
||||
x: z.number().describe("X position of the logo top-left corner in mm"),
|
||||
y: z.number().describe("Y position of the logo top-left corner in mm"),
|
||||
width: z.number().describe("Target width of the logo in mm (height is scaled to preserve aspect ratio)"),
|
||||
layer: z.string().optional().describe("PCB layer name, e.g. F.SilkS or B.SilkS (default: F.SilkS)"),
|
||||
strokeWidth: z.number().optional().describe("Outline stroke width in mm (0 = no outline, default 0)"),
|
||||
filled: z.boolean().optional().describe("Fill polygons with solid colour (default true)"),
|
||||
},
|
||||
async (args: { pcbPath: string; svgPath: string; x: number; y: number; width: number; layer?: string; strokeWidth?: number; filled?: boolean }) => {
|
||||
const result = await callKicadScript("import_svg_logo", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: [
|
||||
result.message,
|
||||
`Polygons: ${result.polygon_count}`,
|
||||
`Size: ${result.logo_width_mm?.toFixed(2)} × ${result.logo_height_mm?.toFixed(2)} mm`,
|
||||
`Layer: ${result.layer}`,
|
||||
].join("\n"),
|
||||
}],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [{ type: "text", text: `SVG import failed: ${result.message || "Unknown error"}` }],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Board management tools for KiCAD MCP server
|
||||
*
|
||||
* These tools handle board setup, layer management, and board properties
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register board management tools with the MCP server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerBoardTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info("Registering board management tools");
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Board Size Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_board_size",
|
||||
{
|
||||
width: z.number().describe("Board width"),
|
||||
height: z.number().describe("Board height"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
},
|
||||
async ({ width, height, unit }) => {
|
||||
logger.debug(`Setting board size to ${width}x${height} ${unit}`);
|
||||
const result = await callKicadScript("set_board_size", {
|
||||
width,
|
||||
height,
|
||||
unit,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Layer Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_layer",
|
||||
{
|
||||
name: z.string().describe("Layer name"),
|
||||
type: z.enum(["copper", "technical", "user", "signal"]).describe("Layer type"),
|
||||
position: z.enum(["top", "bottom", "inner"]).describe("Layer position"),
|
||||
number: z.number().optional().describe("Layer number (for inner layers)"),
|
||||
},
|
||||
async ({ name, type, position, number }) => {
|
||||
logger.debug(`Adding ${type} layer: ${name}`);
|
||||
const result = await callKicadScript("add_layer", {
|
||||
name,
|
||||
type,
|
||||
position,
|
||||
number,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Active Layer Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_active_layer",
|
||||
{
|
||||
layer: z.string().describe("Layer name to set as active"),
|
||||
},
|
||||
async ({ layer }) => {
|
||||
logger.debug(`Setting active layer to: ${layer}`);
|
||||
const result = await callKicadScript("set_active_layer", { layer });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Board Info Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool("get_board_info", {}, async () => {
|
||||
logger.debug("Getting board information");
|
||||
const result = await callKicadScript("get_board_info", {});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Layer List Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool("get_layer_list", {}, async () => {
|
||||
logger.debug("Getting layer list");
|
||||
const result = await callKicadScript("get_layer_list", {});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Board Outline Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_board_outline",
|
||||
{
|
||||
shape: z
|
||||
.enum(["rectangle", "circle", "polygon", "rounded_rectangle"])
|
||||
.describe("Shape of the outline"),
|
||||
params: z
|
||||
.object({
|
||||
// For rectangle / rounded_rectangle
|
||||
width: z.number().optional().describe("Width of rectangle"),
|
||||
height: z.number().optional().describe("Height of rectangle"),
|
||||
cornerRadius: z.number().optional().describe("Corner radius for rounded_rectangle (mm)"),
|
||||
// For circle
|
||||
radius: z.number().optional().describe("Radius of circle"),
|
||||
// For polygon
|
||||
points: z
|
||||
.array(
|
||||
z.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate"),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe("Points of polygon"),
|
||||
// Position: top-left corner for rectangles/rounded_rectangle, center for circle
|
||||
x: z.number().describe("X coordinate of top-left corner for rectangles (default: 0)"),
|
||||
y: z.number().describe("Y coordinate of top-left corner for rectangles (default: 0)"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
})
|
||||
.describe("Parameters for the outline shape"),
|
||||
},
|
||||
async ({ shape, params }) => {
|
||||
logger.debug(`Adding ${shape} board outline`);
|
||||
// Pass x/y as-is to Python; outline.py treats them as top-left corner
|
||||
// and computes the center internally (center = x + width/2, y + height/2).
|
||||
const result = await callKicadScript("add_board_outline", {
|
||||
shape,
|
||||
...params,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Mounting Hole Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_mounting_hole",
|
||||
{
|
||||
position: z
|
||||
.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
})
|
||||
.describe("Position of the mounting hole"),
|
||||
diameter: z.number().describe("Diameter of the hole"),
|
||||
padDiameter: z.number().optional().describe("Optional diameter of the pad around the hole"),
|
||||
},
|
||||
async ({ position, diameter, padDiameter }) => {
|
||||
logger.debug(`Adding mounting hole at (${position.x},${position.y}) ${position.unit}`);
|
||||
const result = await callKicadScript("add_mounting_hole", {
|
||||
position,
|
||||
diameter,
|
||||
padDiameter,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Text Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_board_text",
|
||||
{
|
||||
text: z.string().describe("Text content"),
|
||||
position: z
|
||||
.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
})
|
||||
.describe("Position of the text"),
|
||||
layer: z.string().describe("Layer to place the text on"),
|
||||
size: z.number().describe("Text size"),
|
||||
thickness: z.number().optional().describe("Line thickness"),
|
||||
rotation: z.number().optional().describe("Rotation angle in degrees"),
|
||||
style: z.enum(["normal", "italic", "bold"]).optional().describe("Text style"),
|
||||
},
|
||||
async ({ text, position, layer, size, thickness, rotation, style }) => {
|
||||
logger.debug(`Adding text "${text}" at (${position.x},${position.y}) ${position.unit}`);
|
||||
const result = await callKicadScript("add_board_text", {
|
||||
text,
|
||||
position,
|
||||
layer,
|
||||
size,
|
||||
thickness,
|
||||
rotation,
|
||||
style,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Zone Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_zone",
|
||||
{
|
||||
layer: z.string().describe("Layer for the zone"),
|
||||
net: z.string().describe("Net name for the zone"),
|
||||
points: z
|
||||
.array(
|
||||
z.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
y: z.number().describe("Y coordinate"),
|
||||
}),
|
||||
)
|
||||
.describe("Points defining the zone outline"),
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
clearance: z.number().optional().describe("Clearance value"),
|
||||
minWidth: z.number().optional().describe("Minimum width"),
|
||||
padConnection: z
|
||||
.enum(["thermal", "solid", "none"])
|
||||
.optional()
|
||||
.describe("Pad connection type"),
|
||||
},
|
||||
async ({ layer, net, points, unit, clearance, minWidth, padConnection }) => {
|
||||
logger.debug(`Adding zone on layer ${layer} for net ${net}`);
|
||||
const result = await callKicadScript("add_zone", {
|
||||
layer,
|
||||
net,
|
||||
points,
|
||||
unit,
|
||||
clearance,
|
||||
minWidth,
|
||||
padConnection,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Board Extents Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_board_extents",
|
||||
{
|
||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit of measurement for the result"),
|
||||
},
|
||||
async ({ unit }) => {
|
||||
logger.debug("Getting board extents");
|
||||
const result = await callKicadScript("get_board_extents", { unit });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Board 2D View Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_board_2d_view",
|
||||
{
|
||||
layers: z.array(z.string()).optional().describe("Optional array of layer names to include"),
|
||||
width: z.number().optional().describe("Optional width of the image in pixels"),
|
||||
height: z.number().optional().describe("Optional height of the image in pixels"),
|
||||
format: z.enum(["png", "jpg", "svg"]).optional().describe("Image format"),
|
||||
},
|
||||
async ({ layers, width, height, format }) => {
|
||||
logger.debug("Getting 2D board view");
|
||||
const result = await callKicadScript("get_board_2d_view", {
|
||||
layers,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
logger.info("Board management tools registered");
|
||||
|
||||
// Import SVG logo onto PCB layer (silkscreen)
|
||||
server.tool(
|
||||
"import_svg_logo",
|
||||
"Imports an SVG file as filled graphic polygons onto a KiCAD PCB layer (default F.SilkS / front silkscreen). Curves are linearised automatically. Ideal for placing a company or project logo on the board.",
|
||||
{
|
||||
pcbPath: z.string().describe("Path to the .kicad_pcb file"),
|
||||
svgPath: z.string().describe("Path to the SVG logo file"),
|
||||
x: z.number().describe("X position of the logo top-left corner in mm"),
|
||||
y: z.number().describe("Y position of the logo top-left corner in mm"),
|
||||
width: z
|
||||
.number()
|
||||
.describe("Target width of the logo in mm (height is scaled to preserve aspect ratio)"),
|
||||
layer: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("PCB layer name, e.g. F.SilkS or B.SilkS (default: F.SilkS)"),
|
||||
strokeWidth: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Outline stroke width in mm (0 = no outline, default 0)"),
|
||||
filled: z.boolean().optional().describe("Fill polygons with solid colour (default true)"),
|
||||
},
|
||||
async (args: {
|
||||
pcbPath: string;
|
||||
svgPath: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
layer?: string;
|
||||
strokeWidth?: number;
|
||||
filled?: boolean;
|
||||
}) => {
|
||||
const result = await callKicadScript("import_svg_logo", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
result.message,
|
||||
`Polygons: ${result.polygon_count}`,
|
||||
`Size: ${result.logo_width_mm?.toFixed(2)} × ${result.logo_height_mm?.toFixed(2)} mm`,
|
||||
`Layer: ${result.layer}`,
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `SVG import failed: ${result.message || "Unknown error"}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (
|
||||
command: string,
|
||||
params: Record<string, unknown>,
|
||||
) => Promise<any>;
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register component management tools with the MCP server
|
||||
@@ -18,10 +15,7 @@ type CommandFunction = (
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerComponentTools(
|
||||
server: McpServer,
|
||||
callKicadScript: CommandFunction,
|
||||
): void {
|
||||
export function registerComponentTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info("Registering component management tools");
|
||||
|
||||
// ------------------------------------------------------
|
||||
@@ -40,38 +34,19 @@ export function registerComponentTools(
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
})
|
||||
.describe("Position coordinates and unit"),
|
||||
reference: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional desired reference (e.g., 'R5')"),
|
||||
value: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional component value (e.g., '10k')"),
|
||||
footprint: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional specific footprint name"),
|
||||
reference: z.string().optional().describe("Optional desired reference (e.g., 'R5')"),
|
||||
value: z.string().optional().describe("Optional component value (e.g., '10k')"),
|
||||
footprint: z.string().optional().describe("Optional specific footprint name"),
|
||||
rotation: z.number().optional().describe("Optional rotation in degrees"),
|
||||
layer: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"),
|
||||
layer: z.string().optional().describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"),
|
||||
boardPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Path to the .kicad_pcb file – required when using project-local footprint libraries"),
|
||||
.describe(
|
||||
"Path to the .kicad_pcb file – required when using project-local footprint libraries",
|
||||
),
|
||||
},
|
||||
async ({
|
||||
componentId,
|
||||
position,
|
||||
reference,
|
||||
value,
|
||||
footprint,
|
||||
rotation,
|
||||
layer,
|
||||
boardPath,
|
||||
}) => {
|
||||
async ({ componentId, position, reference, value, footprint, rotation, layer, boardPath }) => {
|
||||
logger.debug(
|
||||
`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`,
|
||||
);
|
||||
@@ -103,9 +78,7 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"move_component",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
||||
position: z
|
||||
.object({
|
||||
x: z.number().describe("X coordinate"),
|
||||
@@ -113,10 +86,7 @@ export function registerComponentTools(
|
||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||
})
|
||||
.describe("New position coordinates and unit"),
|
||||
rotation: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Optional new rotation in degrees"),
|
||||
rotation: z.number().optional().describe("Optional new rotation in degrees"),
|
||||
layer: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -150,12 +120,8 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"rotate_component",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||
angle: z
|
||||
.number()
|
||||
.describe("Rotation angle in degrees (absolute, not relative)"),
|
||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
||||
angle: z.number().describe("Rotation angle in degrees (absolute, not relative)"),
|
||||
},
|
||||
async ({ reference, angle }) => {
|
||||
logger.debug(`Rotating component: ${reference} to ${angle} degrees`);
|
||||
@@ -183,9 +149,7 @@ export function registerComponentTools(
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe(
|
||||
"Reference designator of the component to delete (e.g., 'R5')",
|
||||
),
|
||||
.describe("Reference designator of the component to delete (e.g., 'R5')"),
|
||||
},
|
||||
async ({ reference }) => {
|
||||
logger.debug(`Deleting component: ${reference}`);
|
||||
@@ -208,13 +172,8 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"edit_component",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||
newReference: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional new reference designator"),
|
||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
||||
newReference: z.string().optional().describe("Optional new reference designator"),
|
||||
value: z.string().optional().describe("Optional new component value"),
|
||||
footprint: z.string().optional().describe("Optional new footprint"),
|
||||
},
|
||||
@@ -244,10 +203,7 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"find_component",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Reference designator to search for"),
|
||||
reference: z.string().optional().describe("Reference designator to search for"),
|
||||
value: z.string().optional().describe("Component value to search for"),
|
||||
},
|
||||
async ({ reference, value }) => {
|
||||
@@ -276,9 +232,7 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"get_component_properties",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
||||
},
|
||||
async ({ reference }) => {
|
||||
logger.debug(`Getting properties for component: ${reference}`);
|
||||
@@ -303,9 +257,7 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"add_component_annotation",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
||||
annotation: z.string().describe("Annotation or comment text to add"),
|
||||
visible: z
|
||||
.boolean()
|
||||
@@ -337,15 +289,11 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"group_components",
|
||||
{
|
||||
references: z
|
||||
.array(z.string())
|
||||
.describe("Reference designators of components to group"),
|
||||
references: z.array(z.string()).describe("Reference designators of components to group"),
|
||||
groupName: z.string().describe("Name for the component group"),
|
||||
},
|
||||
async ({ references, groupName }) => {
|
||||
logger.debug(
|
||||
`Grouping components: ${references.join(", ")} as ${groupName}`,
|
||||
);
|
||||
logger.debug(`Grouping components: ${references.join(", ")} as ${groupName}`);
|
||||
const result = await callKicadScript("group_components", {
|
||||
references,
|
||||
groupName,
|
||||
@@ -368,9 +316,7 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"replace_component",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component to replace"),
|
||||
reference: z.string().describe("Reference designator of the component to replace"),
|
||||
newComponentId: z.string().describe("ID of the new component to use"),
|
||||
newFootprint: z.string().optional().describe("Optional new footprint"),
|
||||
newValue: z.string().optional().describe("Optional new component value"),
|
||||
@@ -401,13 +347,8 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"get_component_pads",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Reference designator of the component (e.g., 'U1')"),
|
||||
unit: z
|
||||
.enum(["mm", "inch"])
|
||||
.optional()
|
||||
.describe("Unit for coordinates (default: mm)"),
|
||||
reference: z.string().describe("Reference designator of the component (e.g., 'U1')"),
|
||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
||||
},
|
||||
async ({ reference, unit }) => {
|
||||
logger.debug(`Getting pads for component: ${reference}`);
|
||||
@@ -433,10 +374,7 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"get_component_list",
|
||||
{
|
||||
layer: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter by layer (e.g., 'F.Cu', 'B.Cu')"),
|
||||
layer: z.string().optional().describe("Filter by layer (e.g., 'F.Cu', 'B.Cu')"),
|
||||
boundingBox: z
|
||||
.object({
|
||||
x1: z.number(),
|
||||
@@ -447,10 +385,7 @@ export function registerComponentTools(
|
||||
})
|
||||
.optional()
|
||||
.describe("Filter by bounding box region"),
|
||||
unit: z
|
||||
.enum(["mm", "inch"])
|
||||
.optional()
|
||||
.describe("Unit for coordinates (default: mm)"),
|
||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
||||
},
|
||||
async ({ layer, boundingBox, unit }) => {
|
||||
logger.debug("Getting component list");
|
||||
@@ -477,14 +412,9 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"get_pad_position",
|
||||
{
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Component reference designator (e.g., 'U1')"),
|
||||
reference: z.string().describe("Component reference designator (e.g., 'U1')"),
|
||||
pad: z.string().describe("Pad number or name (e.g., '1', 'A1')"),
|
||||
unit: z
|
||||
.enum(["mm", "inch"])
|
||||
.optional()
|
||||
.describe("Unit for coordinates (default: mm)"),
|
||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
||||
},
|
||||
async ({ reference, pad, unit }) => {
|
||||
logger.debug(`Getting pad position for ${reference} pad ${pad}`);
|
||||
@@ -523,10 +453,7 @@ export function registerComponentTools(
|
||||
columns: z.number().describe("Number of columns"),
|
||||
rowSpacing: z.number().describe("Spacing between rows"),
|
||||
columnSpacing: z.number().describe("Spacing between columns"),
|
||||
startReference: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Starting reference (e.g., 'R1')"),
|
||||
startReference: z.string().optional().describe("Starting reference (e.g., 'R1')"),
|
||||
footprint: z.string().optional().describe("Footprint name"),
|
||||
value: z.string().optional().describe("Component value"),
|
||||
rotation: z.number().optional().describe("Rotation in degrees"),
|
||||
@@ -543,9 +470,7 @@ export function registerComponentTools(
|
||||
value,
|
||||
rotation,
|
||||
}) => {
|
||||
logger.debug(
|
||||
`Placing component array: ${rows}x${columns} of ${componentId}`,
|
||||
);
|
||||
logger.debug(`Placing component array: ${rows}x${columns} of ${componentId}`);
|
||||
const result = await callKicadScript("place_component_array", {
|
||||
componentId,
|
||||
startPosition,
|
||||
@@ -576,20 +501,10 @@ export function registerComponentTools(
|
||||
server.tool(
|
||||
"align_components",
|
||||
{
|
||||
references: z
|
||||
.array(z.string())
|
||||
.describe("Array of component references to align"),
|
||||
alignmentType: z
|
||||
.enum(["horizontal", "vertical", "grid"])
|
||||
.describe("Type of alignment"),
|
||||
spacing: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Spacing between components in mm"),
|
||||
referenceComponent: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Reference component for alignment"),
|
||||
references: z.array(z.string()).describe("Array of component references to align"),
|
||||
alignmentType: z.enum(["horizontal", "vertical", "grid"]).describe("Type of alignment"),
|
||||
spacing: z.number().optional().describe("Spacing between components in mm"),
|
||||
referenceComponent: z.string().optional().describe("Reference component for alignment"),
|
||||
},
|
||||
async ({ references, alignmentType, spacing, referenceComponent }) => {
|
||||
logger.debug(`Aligning components: ${references.join(", ")}`);
|
||||
@@ -626,10 +541,7 @@ export function registerComponentTools(
|
||||
})
|
||||
.describe("Offset from original position"),
|
||||
newReference: z.string().optional().describe("New reference designator"),
|
||||
count: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of duplicates (default: 1)"),
|
||||
count: z.number().optional().describe("Number of duplicates (default: 1)"),
|
||||
},
|
||||
async ({ reference, offset, newReference, count }) => {
|
||||
logger.debug(`Duplicating component: ${reference}`);
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerDatasheetTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerDatasheetTools(server: McpServer, callKicadScript: Function) {
|
||||
// ── enrich_datasheets ──────────────────────────────────────────────────────
|
||||
server.tool(
|
||||
"enrich_datasheets",
|
||||
@@ -30,16 +27,12 @@ No API key or internet lookup required – the URL is constructed directly.
|
||||
|
||||
Use dry_run=true to preview changes without writing.`,
|
||||
{
|
||||
schematic_path: z
|
||||
.string()
|
||||
.describe("Path to the .kicad_sch file to enrich"),
|
||||
schematic_path: z.string().describe("Path to the .kicad_sch file to enrich"),
|
||||
dry_run: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"If true, show what would be changed without writing to disk (default: false)",
|
||||
),
|
||||
.describe("If true, show what would be changed without writing to disk (default: false)"),
|
||||
},
|
||||
async (args: { schematic_path: string; dry_run?: boolean }) => {
|
||||
const result = await callKicadScript("enrich_datasheets", args);
|
||||
@@ -65,9 +58,7 @@ Use dry_run=true to preview changes without writing.`,
|
||||
}
|
||||
|
||||
if (result.updated === 0 && !args.dry_run) {
|
||||
lines.push(
|
||||
"\nNo changes needed – all LCSC components already have a Datasheet URL.",
|
||||
);
|
||||
lines.push("\nNo changes needed – all LCSC components already have a Datasheet URL.");
|
||||
}
|
||||
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
@@ -96,9 +87,7 @@ Example: get_datasheet_url("C179739")
|
||||
{
|
||||
lcsc: z
|
||||
.string()
|
||||
.describe(
|
||||
'LCSC part number, with or without "C" prefix (e.g. "C179739" or "179739")',
|
||||
),
|
||||
.describe('LCSC part number, with or without "C" prefix (e.g. "C179739" or "179739")'),
|
||||
},
|
||||
async (args: { lcsc: string }) => {
|
||||
const result = await callKicadScript("get_datasheet_url", { lcsc: args.lcsc });
|
||||
|
||||
@@ -1,261 +1,314 @@
|
||||
/**
|
||||
* Design rules tools for KiCAD MCP server
|
||||
*
|
||||
* These tools handle design rule checking and configuration
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register design rule tools with the MCP server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerDesignRuleTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info('Registering design rule tools');
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Design Rules Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_design_rules",
|
||||
{
|
||||
clearance: z.number().optional().describe("Minimum clearance between copper items (mm)"),
|
||||
trackWidth: z.number().optional().describe("Default track width (mm)"),
|
||||
viaDiameter: z.number().optional().describe("Default via diameter (mm)"),
|
||||
viaDrill: z.number().optional().describe("Default via drill size (mm)"),
|
||||
microViaDiameter: z.number().optional().describe("Default micro via diameter (mm)"),
|
||||
microViaDrill: z.number().optional().describe("Default micro via drill size (mm)"),
|
||||
minTrackWidth: z.number().optional().describe("Minimum track width (mm)"),
|
||||
minViaDiameter: z.number().optional().describe("Minimum via diameter (mm)"),
|
||||
minViaDrill: z.number().optional().describe("Minimum via drill size (mm)"),
|
||||
minMicroViaDiameter: z.number().optional().describe("Minimum micro via diameter (mm)"),
|
||||
minMicroViaDrill: z.number().optional().describe("Minimum micro via drill size (mm)"),
|
||||
minHoleDiameter: z.number().optional().describe("Minimum hole diameter (mm)"),
|
||||
requireCourtyard: z.boolean().optional().describe("Whether to require courtyards for all footprints"),
|
||||
courtyardClearance: z.number().optional().describe("Minimum clearance between courtyards (mm)")
|
||||
},
|
||||
async (params) => {
|
||||
logger.debug('Setting design rules');
|
||||
const result = await callKicadScript("set_design_rules", params);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Design Rules Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_design_rules",
|
||||
{},
|
||||
async () => {
|
||||
logger.debug('Getting design rules');
|
||||
const result = await callKicadScript("get_design_rules", {});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Run DRC Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"run_drc",
|
||||
{
|
||||
reportPath: z.string().optional().describe("Optional path to save the DRC report")
|
||||
},
|
||||
async ({ reportPath }) => {
|
||||
logger.debug('Running DRC check');
|
||||
const result = await callKicadScript("run_drc", { reportPath });
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Net Class Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_net_class",
|
||||
{
|
||||
name: z.string().describe("Name of the net class"),
|
||||
description: z.string().optional().describe("Optional description of the net class"),
|
||||
clearance: z.number().describe("Clearance for this net class (mm)"),
|
||||
trackWidth: z.number().describe("Track width for this net class (mm)"),
|
||||
viaDiameter: z.number().describe("Via diameter for this net class (mm)"),
|
||||
viaDrill: z.number().describe("Via drill size for this net class (mm)"),
|
||||
uvia_diameter: z.number().optional().describe("Micro via diameter for this net class (mm)"),
|
||||
uvia_drill: z.number().optional().describe("Micro via drill size for this net class (mm)"),
|
||||
diff_pair_width: z.number().optional().describe("Differential pair width for this net class (mm)"),
|
||||
diff_pair_gap: z.number().optional().describe("Differential pair gap for this net class (mm)"),
|
||||
nets: z.array(z.string()).optional().describe("Array of net names to assign to this class")
|
||||
},
|
||||
async ({ name, description, clearance, trackWidth, viaDiameter, viaDrill, uvia_diameter, uvia_drill, diff_pair_width, diff_pair_gap, nets }) => {
|
||||
logger.debug(`Adding net class: ${name}`);
|
||||
const result = await callKicadScript("add_net_class", {
|
||||
name,
|
||||
description,
|
||||
clearance,
|
||||
trackWidth,
|
||||
viaDiameter,
|
||||
viaDrill,
|
||||
uvia_diameter,
|
||||
uvia_drill,
|
||||
diff_pair_width,
|
||||
diff_pair_gap,
|
||||
nets
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Assign Net to Class Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"assign_net_to_class",
|
||||
{
|
||||
net: z.string().describe("Name of the net"),
|
||||
netClass: z.string().describe("Name of the net class")
|
||||
},
|
||||
async ({ net, netClass }) => {
|
||||
logger.debug(`Assigning net ${net} to class ${netClass}`);
|
||||
const result = await callKicadScript("assign_net_to_class", {
|
||||
net,
|
||||
netClass
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Layer Constraints Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_layer_constraints",
|
||||
{
|
||||
layer: z.string().describe("Layer name (e.g., 'F.Cu')"),
|
||||
minTrackWidth: z.number().optional().describe("Minimum track width for this layer (mm)"),
|
||||
minClearance: z.number().optional().describe("Minimum clearance for this layer (mm)"),
|
||||
minViaDiameter: z.number().optional().describe("Minimum via diameter for this layer (mm)"),
|
||||
minViaDrill: z.number().optional().describe("Minimum via drill size for this layer (mm)")
|
||||
},
|
||||
async ({ layer, minTrackWidth, minClearance, minViaDiameter, minViaDrill }) => {
|
||||
logger.debug(`Setting constraints for layer: ${layer}`);
|
||||
const result = await callKicadScript("set_layer_constraints", {
|
||||
layer,
|
||||
minTrackWidth,
|
||||
minClearance,
|
||||
minViaDiameter,
|
||||
minViaDrill
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Check Clearance Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"check_clearance",
|
||||
{
|
||||
item1: z.object({
|
||||
type: z.enum(["track", "via", "pad", "zone", "component"]).describe("Type of the first item"),
|
||||
id: z.string().optional().describe("ID of the first item (if applicable)"),
|
||||
reference: z.string().optional().describe("Reference designator (for component)"),
|
||||
position: z.object({
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
unit: z.enum(["mm", "inch"]).optional()
|
||||
}).optional().describe("Position to check (if ID not provided)")
|
||||
}).describe("First item to check"),
|
||||
item2: z.object({
|
||||
type: z.enum(["track", "via", "pad", "zone", "component"]).describe("Type of the second item"),
|
||||
id: z.string().optional().describe("ID of the second item (if applicable)"),
|
||||
reference: z.string().optional().describe("Reference designator (for component)"),
|
||||
position: z.object({
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
unit: z.enum(["mm", "inch"]).optional()
|
||||
}).optional().describe("Position to check (if ID not provided)")
|
||||
}).describe("Second item to check")
|
||||
},
|
||||
async ({ item1, item2 }) => {
|
||||
logger.debug(`Checking clearance between ${item1.type} and ${item2.type}`);
|
||||
const result = await callKicadScript("check_clearance", {
|
||||
item1,
|
||||
item2
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get DRC Violations Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_drc_violations",
|
||||
{
|
||||
severity: z.enum(["error", "warning", "all"]).optional().describe("Filter violations by severity")
|
||||
},
|
||||
async ({ severity }) => {
|
||||
logger.debug('Getting DRC violations');
|
||||
const result = await callKicadScript("get_drc_violations", { severity });
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Design rule tools registered');
|
||||
}
|
||||
/**
|
||||
* Design rules tools for KiCAD MCP server
|
||||
*
|
||||
* These tools handle design rule checking and configuration
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register design rule tools with the MCP server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerDesignRuleTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info("Registering design rule tools");
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Design Rules Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_design_rules",
|
||||
{
|
||||
clearance: z.number().optional().describe("Minimum clearance between copper items (mm)"),
|
||||
trackWidth: z.number().optional().describe("Default track width (mm)"),
|
||||
viaDiameter: z.number().optional().describe("Default via diameter (mm)"),
|
||||
viaDrill: z.number().optional().describe("Default via drill size (mm)"),
|
||||
microViaDiameter: z.number().optional().describe("Default micro via diameter (mm)"),
|
||||
microViaDrill: z.number().optional().describe("Default micro via drill size (mm)"),
|
||||
minTrackWidth: z.number().optional().describe("Minimum track width (mm)"),
|
||||
minViaDiameter: z.number().optional().describe("Minimum via diameter (mm)"),
|
||||
minViaDrill: z.number().optional().describe("Minimum via drill size (mm)"),
|
||||
minMicroViaDiameter: z.number().optional().describe("Minimum micro via diameter (mm)"),
|
||||
minMicroViaDrill: z.number().optional().describe("Minimum micro via drill size (mm)"),
|
||||
minHoleDiameter: z.number().optional().describe("Minimum hole diameter (mm)"),
|
||||
requireCourtyard: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether to require courtyards for all footprints"),
|
||||
courtyardClearance: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Minimum clearance between courtyards (mm)"),
|
||||
},
|
||||
async (params) => {
|
||||
logger.debug("Setting design rules");
|
||||
const result = await callKicadScript("set_design_rules", params);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get Design Rules Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool("get_design_rules", {}, async () => {
|
||||
logger.debug("Getting design rules");
|
||||
const result = await callKicadScript("get_design_rules", {});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Run DRC Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"run_drc",
|
||||
{
|
||||
reportPath: z.string().optional().describe("Optional path to save the DRC report"),
|
||||
},
|
||||
async ({ reportPath }) => {
|
||||
logger.debug("Running DRC check");
|
||||
const result = await callKicadScript("run_drc", { reportPath });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Add Net Class Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"add_net_class",
|
||||
{
|
||||
name: z.string().describe("Name of the net class"),
|
||||
description: z.string().optional().describe("Optional description of the net class"),
|
||||
clearance: z.number().describe("Clearance for this net class (mm)"),
|
||||
trackWidth: z.number().describe("Track width for this net class (mm)"),
|
||||
viaDiameter: z.number().describe("Via diameter for this net class (mm)"),
|
||||
viaDrill: z.number().describe("Via drill size for this net class (mm)"),
|
||||
uvia_diameter: z.number().optional().describe("Micro via diameter for this net class (mm)"),
|
||||
uvia_drill: z.number().optional().describe("Micro via drill size for this net class (mm)"),
|
||||
diff_pair_width: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Differential pair width for this net class (mm)"),
|
||||
diff_pair_gap: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Differential pair gap for this net class (mm)"),
|
||||
nets: z.array(z.string()).optional().describe("Array of net names to assign to this class"),
|
||||
},
|
||||
async ({
|
||||
name,
|
||||
description,
|
||||
clearance,
|
||||
trackWidth,
|
||||
viaDiameter,
|
||||
viaDrill,
|
||||
uvia_diameter,
|
||||
uvia_drill,
|
||||
diff_pair_width,
|
||||
diff_pair_gap,
|
||||
nets,
|
||||
}) => {
|
||||
logger.debug(`Adding net class: ${name}`);
|
||||
const result = await callKicadScript("add_net_class", {
|
||||
name,
|
||||
description,
|
||||
clearance,
|
||||
trackWidth,
|
||||
viaDiameter,
|
||||
viaDrill,
|
||||
uvia_diameter,
|
||||
uvia_drill,
|
||||
diff_pair_width,
|
||||
diff_pair_gap,
|
||||
nets,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Assign Net to Class Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"assign_net_to_class",
|
||||
{
|
||||
net: z.string().describe("Name of the net"),
|
||||
netClass: z.string().describe("Name of the net class"),
|
||||
},
|
||||
async ({ net, netClass }) => {
|
||||
logger.debug(`Assigning net ${net} to class ${netClass}`);
|
||||
const result = await callKicadScript("assign_net_to_class", {
|
||||
net,
|
||||
netClass,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Set Layer Constraints Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"set_layer_constraints",
|
||||
{
|
||||
layer: z.string().describe("Layer name (e.g., 'F.Cu')"),
|
||||
minTrackWidth: z.number().optional().describe("Minimum track width for this layer (mm)"),
|
||||
minClearance: z.number().optional().describe("Minimum clearance for this layer (mm)"),
|
||||
minViaDiameter: z.number().optional().describe("Minimum via diameter for this layer (mm)"),
|
||||
minViaDrill: z.number().optional().describe("Minimum via drill size for this layer (mm)"),
|
||||
},
|
||||
async ({ layer, minTrackWidth, minClearance, minViaDiameter, minViaDrill }) => {
|
||||
logger.debug(`Setting constraints for layer: ${layer}`);
|
||||
const result = await callKicadScript("set_layer_constraints", {
|
||||
layer,
|
||||
minTrackWidth,
|
||||
minClearance,
|
||||
minViaDiameter,
|
||||
minViaDrill,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Check Clearance Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"check_clearance",
|
||||
{
|
||||
item1: z
|
||||
.object({
|
||||
type: z
|
||||
.enum(["track", "via", "pad", "zone", "component"])
|
||||
.describe("Type of the first item"),
|
||||
id: z.string().optional().describe("ID of the first item (if applicable)"),
|
||||
reference: z.string().optional().describe("Reference designator (for component)"),
|
||||
position: z
|
||||
.object({
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
unit: z.enum(["mm", "inch"]).optional(),
|
||||
})
|
||||
.optional()
|
||||
.describe("Position to check (if ID not provided)"),
|
||||
})
|
||||
.describe("First item to check"),
|
||||
item2: z
|
||||
.object({
|
||||
type: z
|
||||
.enum(["track", "via", "pad", "zone", "component"])
|
||||
.describe("Type of the second item"),
|
||||
id: z.string().optional().describe("ID of the second item (if applicable)"),
|
||||
reference: z.string().optional().describe("Reference designator (for component)"),
|
||||
position: z
|
||||
.object({
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
unit: z.enum(["mm", "inch"]).optional(),
|
||||
})
|
||||
.optional()
|
||||
.describe("Position to check (if ID not provided)"),
|
||||
})
|
||||
.describe("Second item to check"),
|
||||
},
|
||||
async ({ item1, item2 }) => {
|
||||
logger.debug(`Checking clearance between ${item1.type} and ${item2.type}`);
|
||||
const result = await callKicadScript("check_clearance", {
|
||||
item1,
|
||||
item2,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Get DRC Violations Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"get_drc_violations",
|
||||
{
|
||||
severity: z
|
||||
.enum(["error", "warning", "all"])
|
||||
.optional()
|
||||
.describe("Filter violations by severity"),
|
||||
},
|
||||
async ({ severity }) => {
|
||||
logger.debug("Getting DRC violations");
|
||||
const result = await callKicadScript("get_drc_violations", { severity });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
logger.info("Design rule tools registered");
|
||||
}
|
||||
|
||||
@@ -1,260 +1,317 @@
|
||||
/**
|
||||
* Export tools for KiCAD MCP server
|
||||
*
|
||||
* These tools handle exporting PCB data to various formats
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register export tools with the MCP server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerExportTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info('Registering export tools');
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export Gerber Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_gerber",
|
||||
{
|
||||
outputDir: z.string().describe("Directory to save Gerber files"),
|
||||
layers: z.array(z.string()).optional().describe("Optional array of layer names to export (default: all)"),
|
||||
useProtelExtensions: z.boolean().optional().describe("Whether to use Protel filename extensions"),
|
||||
generateDrillFiles: z.boolean().optional().describe("Whether to generate drill files"),
|
||||
generateMapFile: z.boolean().optional().describe("Whether to generate a map file"),
|
||||
useAuxOrigin: z.boolean().optional().describe("Whether to use auxiliary axis as origin")
|
||||
},
|
||||
async ({ outputDir, layers, useProtelExtensions, generateDrillFiles, generateMapFile, useAuxOrigin }) => {
|
||||
logger.debug(`Exporting Gerber files to: ${outputDir}`);
|
||||
const result = await callKicadScript("export_gerber", {
|
||||
outputDir,
|
||||
layers,
|
||||
useProtelExtensions,
|
||||
generateDrillFiles,
|
||||
generateMapFile,
|
||||
useAuxOrigin
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export PDF Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_pdf",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the PDF file"),
|
||||
layers: z.array(z.string()).optional().describe("Optional array of layer names to include (default: all)"),
|
||||
blackAndWhite: z.boolean().optional().describe("Whether to export in black and white"),
|
||||
frameReference: z.boolean().optional().describe("Whether to include frame reference"),
|
||||
pageSize: z.enum(["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]).optional().describe("Page size")
|
||||
},
|
||||
async ({ outputPath, layers, blackAndWhite, frameReference, pageSize }) => {
|
||||
logger.debug(`Exporting PDF to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_pdf", {
|
||||
outputPath,
|
||||
layers,
|
||||
blackAndWhite,
|
||||
frameReference,
|
||||
pageSize
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export SVG Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_svg",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the SVG file"),
|
||||
layers: z.array(z.string()).optional().describe("Optional array of layer names to include (default: all)"),
|
||||
blackAndWhite: z.boolean().optional().describe("Whether to export in black and white"),
|
||||
includeComponents: z.boolean().optional().describe("Whether to include component outlines")
|
||||
},
|
||||
async ({ outputPath, layers, blackAndWhite, includeComponents }) => {
|
||||
logger.debug(`Exporting SVG to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_svg", {
|
||||
outputPath,
|
||||
layers,
|
||||
blackAndWhite,
|
||||
includeComponents
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export 3D Model Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_3d",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the 3D model file"),
|
||||
format: z.enum(["STEP", "STL", "VRML", "OBJ"]).describe("3D model format"),
|
||||
includeComponents: z.boolean().optional().describe("Whether to include 3D component models"),
|
||||
includeCopper: z.boolean().optional().describe("Whether to include copper layers"),
|
||||
includeSolderMask: z.boolean().optional().describe("Whether to include solder mask"),
|
||||
includeSilkscreen: z.boolean().optional().describe("Whether to include silkscreen")
|
||||
},
|
||||
async ({ outputPath, format, includeComponents, includeCopper, includeSolderMask, includeSilkscreen }) => {
|
||||
logger.debug(`Exporting 3D model to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_3d", {
|
||||
outputPath,
|
||||
format,
|
||||
includeComponents,
|
||||
includeCopper,
|
||||
includeSolderMask,
|
||||
includeSilkscreen
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export BOM Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_bom",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the BOM file"),
|
||||
format: z.enum(["CSV", "XML", "HTML", "JSON"]).describe("BOM file format"),
|
||||
groupByValue: z.boolean().optional().describe("Whether to group components by value"),
|
||||
includeAttributes: z.array(z.string()).optional().describe("Optional array of additional attributes to include")
|
||||
},
|
||||
async ({ outputPath, format, groupByValue, includeAttributes }) => {
|
||||
logger.debug(`Exporting BOM to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_bom", {
|
||||
outputPath,
|
||||
format,
|
||||
groupByValue,
|
||||
includeAttributes
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export Netlist Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_netlist",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the netlist file"),
|
||||
format: z.enum(["KiCad", "Spice", "Cadstar", "OrcadPCB2"]).optional().describe("Netlist format (default: KiCad)")
|
||||
},
|
||||
async ({ outputPath, format }) => {
|
||||
logger.debug(`Exporting netlist to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_netlist", {
|
||||
outputPath,
|
||||
format
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export Position File Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_position_file",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the position file"),
|
||||
format: z.enum(["CSV", "ASCII"]).optional().describe("File format (default: CSV)"),
|
||||
units: z.enum(["mm", "inch"]).optional().describe("Units to use (default: mm)"),
|
||||
side: z.enum(["top", "bottom", "both"]).optional().describe("Which board side to include (default: both)")
|
||||
},
|
||||
async ({ outputPath, format, units, side }) => {
|
||||
logger.debug(`Exporting position file to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_position_file", {
|
||||
outputPath,
|
||||
format,
|
||||
units,
|
||||
side
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export VRML Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_vrml",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the VRML file"),
|
||||
includeComponents: z.boolean().optional().describe("Whether to include 3D component models"),
|
||||
useRelativePaths: z.boolean().optional().describe("Whether to use relative paths for 3D models")
|
||||
},
|
||||
async ({ outputPath, includeComponents, useRelativePaths }) => {
|
||||
logger.debug(`Exporting VRML to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_vrml", {
|
||||
outputPath,
|
||||
includeComponents,
|
||||
useRelativePaths
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Export tools registered');
|
||||
}
|
||||
/**
|
||||
* Export tools for KiCAD MCP server
|
||||
*
|
||||
* These tools handle exporting PCB data to various formats
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Register export tools with the MCP server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param callKicadScript Function to call KiCAD script commands
|
||||
*/
|
||||
export function registerExportTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info("Registering export tools");
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export Gerber Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_gerber",
|
||||
{
|
||||
outputDir: z.string().describe("Directory to save Gerber files"),
|
||||
layers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Optional array of layer names to export (default: all)"),
|
||||
useProtelExtensions: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether to use Protel filename extensions"),
|
||||
generateDrillFiles: z.boolean().optional().describe("Whether to generate drill files"),
|
||||
generateMapFile: z.boolean().optional().describe("Whether to generate a map file"),
|
||||
useAuxOrigin: z.boolean().optional().describe("Whether to use auxiliary axis as origin"),
|
||||
},
|
||||
async ({
|
||||
outputDir,
|
||||
layers,
|
||||
useProtelExtensions,
|
||||
generateDrillFiles,
|
||||
generateMapFile,
|
||||
useAuxOrigin,
|
||||
}) => {
|
||||
logger.debug(`Exporting Gerber files to: ${outputDir}`);
|
||||
const result = await callKicadScript("export_gerber", {
|
||||
outputDir,
|
||||
layers,
|
||||
useProtelExtensions,
|
||||
generateDrillFiles,
|
||||
generateMapFile,
|
||||
useAuxOrigin,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export PDF Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_pdf",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the PDF file"),
|
||||
layers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Optional array of layer names to include (default: all)"),
|
||||
blackAndWhite: z.boolean().optional().describe("Whether to export in black and white"),
|
||||
frameReference: z.boolean().optional().describe("Whether to include frame reference"),
|
||||
pageSize: z
|
||||
.enum(["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"])
|
||||
.optional()
|
||||
.describe("Page size"),
|
||||
},
|
||||
async ({ outputPath, layers, blackAndWhite, frameReference, pageSize }) => {
|
||||
logger.debug(`Exporting PDF to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_pdf", {
|
||||
outputPath,
|
||||
layers,
|
||||
blackAndWhite,
|
||||
frameReference,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export SVG Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_svg",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the SVG file"),
|
||||
layers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Optional array of layer names to include (default: all)"),
|
||||
blackAndWhite: z.boolean().optional().describe("Whether to export in black and white"),
|
||||
includeComponents: z.boolean().optional().describe("Whether to include component outlines"),
|
||||
},
|
||||
async ({ outputPath, layers, blackAndWhite, includeComponents }) => {
|
||||
logger.debug(`Exporting SVG to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_svg", {
|
||||
outputPath,
|
||||
layers,
|
||||
blackAndWhite,
|
||||
includeComponents,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export 3D Model Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_3d",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the 3D model file"),
|
||||
format: z.enum(["STEP", "STL", "VRML", "OBJ"]).describe("3D model format"),
|
||||
includeComponents: z.boolean().optional().describe("Whether to include 3D component models"),
|
||||
includeCopper: z.boolean().optional().describe("Whether to include copper layers"),
|
||||
includeSolderMask: z.boolean().optional().describe("Whether to include solder mask"),
|
||||
includeSilkscreen: z.boolean().optional().describe("Whether to include silkscreen"),
|
||||
},
|
||||
async ({
|
||||
outputPath,
|
||||
format,
|
||||
includeComponents,
|
||||
includeCopper,
|
||||
includeSolderMask,
|
||||
includeSilkscreen,
|
||||
}) => {
|
||||
logger.debug(`Exporting 3D model to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_3d", {
|
||||
outputPath,
|
||||
format,
|
||||
includeComponents,
|
||||
includeCopper,
|
||||
includeSolderMask,
|
||||
includeSilkscreen,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export BOM Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_bom",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the BOM file"),
|
||||
format: z.enum(["CSV", "XML", "HTML", "JSON"]).describe("BOM file format"),
|
||||
groupByValue: z.boolean().optional().describe("Whether to group components by value"),
|
||||
includeAttributes: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Optional array of additional attributes to include"),
|
||||
},
|
||||
async ({ outputPath, format, groupByValue, includeAttributes }) => {
|
||||
logger.debug(`Exporting BOM to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_bom", {
|
||||
outputPath,
|
||||
format,
|
||||
groupByValue,
|
||||
includeAttributes,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export Netlist Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_netlist",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the netlist file"),
|
||||
format: z
|
||||
.enum(["KiCad", "Spice", "Cadstar", "OrcadPCB2"])
|
||||
.optional()
|
||||
.describe("Netlist format (default: KiCad)"),
|
||||
},
|
||||
async ({ outputPath, format }) => {
|
||||
logger.debug(`Exporting netlist to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_netlist", {
|
||||
outputPath,
|
||||
format,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export Position File Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_position_file",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the position file"),
|
||||
format: z.enum(["CSV", "ASCII"]).optional().describe("File format (default: CSV)"),
|
||||
units: z.enum(["mm", "inch"]).optional().describe("Units to use (default: mm)"),
|
||||
side: z
|
||||
.enum(["top", "bottom", "both"])
|
||||
.optional()
|
||||
.describe("Which board side to include (default: both)"),
|
||||
},
|
||||
async ({ outputPath, format, units, side }) => {
|
||||
logger.debug(`Exporting position file to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_position_file", {
|
||||
outputPath,
|
||||
format,
|
||||
units,
|
||||
side,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Export VRML Tool
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"export_vrml",
|
||||
{
|
||||
outputPath: z.string().describe("Path to save the VRML file"),
|
||||
includeComponents: z.boolean().optional().describe("Whether to include 3D component models"),
|
||||
useRelativePaths: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether to use relative paths for 3D models"),
|
||||
},
|
||||
async ({ outputPath, includeComponents, useRelativePaths }) => {
|
||||
logger.debug(`Exporting VRML to: ${outputPath}`);
|
||||
const result = await callKicadScript("export_vrml", {
|
||||
outputPath,
|
||||
includeComponents,
|
||||
useRelativePaths,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
logger.info("Export tools registered");
|
||||
}
|
||||
|
||||
@@ -62,10 +62,7 @@ const RectSchema = z.object({
|
||||
|
||||
// ---- tool registration --------------------------------------------------- //
|
||||
|
||||
export function registerFootprintTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerFootprintTools(server: McpServer, callKicadScript: Function) {
|
||||
// ── create_footprint ──────────────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"create_footprint",
|
||||
@@ -80,10 +77,7 @@ export function registerFootprintTools(
|
||||
),
|
||||
name: z.string().describe("Footprint name, e.g. 'R_0603_Custom'"),
|
||||
description: z.string().optional().describe("Human-readable description"),
|
||||
tags: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Space-separated tag string, e.g. 'resistor SMD 0603'"),
|
||||
tags: z.string().optional().describe("Space-separated tag string, e.g. 'resistor SMD 0603'"),
|
||||
pads: z
|
||||
.array(PadSchema)
|
||||
.optional()
|
||||
@@ -91,9 +85,7 @@ export function registerFootprintTools(
|
||||
courtyard: RectSchema.optional().describe(
|
||||
"Courtyard rectangle on F.CrtYd (recommended: 0.25 mm clearance around pads)",
|
||||
),
|
||||
silkscreen: RectSchema.optional().describe(
|
||||
"Silkscreen rectangle on F.SilkS",
|
||||
),
|
||||
silkscreen: RectSchema.optional().describe("Silkscreen rectangle on F.SilkS"),
|
||||
fabLayer: RectSchema.optional().describe(
|
||||
"Fab-layer rectangle on F.Fab (shows component body)",
|
||||
),
|
||||
@@ -139,9 +131,7 @@ export function registerFootprintTools(
|
||||
footprintPath: z
|
||||
.string()
|
||||
.describe("Full path to the .kicad_mod file, e.g. C:/MyLib.pretty/R_Custom.kicad_mod"),
|
||||
padNumber: z
|
||||
.union([z.string(), z.number()])
|
||||
.describe("Pad number to edit, e.g. '1' or 2"),
|
||||
padNumber: z.union([z.string(), z.number()]).describe("Pad number to edit, e.g. '1' or 2"),
|
||||
size: PadSize.optional().describe("New pad size in mm"),
|
||||
at: PadPosition.optional().describe("New pad position in mm"),
|
||||
drill: z
|
||||
@@ -151,10 +141,7 @@ export function registerFootprintTools(
|
||||
])
|
||||
.optional()
|
||||
.describe("New drill size (for THT pads)"),
|
||||
shape: z
|
||||
.enum(["rect", "circle", "oval", "roundrect"])
|
||||
.optional()
|
||||
.describe("New pad shape"),
|
||||
shape: z.enum(["rect", "circle", "oval", "roundrect"]).optional().describe("New pad shape"),
|
||||
},
|
||||
async (args: {
|
||||
footprintPath: string;
|
||||
@@ -177,9 +164,7 @@ export function registerFootprintTools(
|
||||
"Register a .pretty footprint library in KiCAD's fp-lib-table so KiCAD can find the footprints. " +
|
||||
"Run this after create_footprint when KiCAD shows 'library not found in footprint library table'.",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe("Full path to the .pretty directory to register"),
|
||||
libraryPath: z.string().describe("Full path to the .pretty directory to register"),
|
||||
libraryName: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -7,33 +7,21 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerFreeroutingTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerFreeroutingTools(server: McpServer, callKicadScript: Function) {
|
||||
// Full autoroute: export DSN -> run Freerouting -> import SES
|
||||
server.tool(
|
||||
"autoroute",
|
||||
"Run Freerouting autorouter on the current PCB. Exports to Specctra DSN, runs Freerouting CLI, and imports the routed SES result. Requires Java 11+ and freerouting.jar (see check_freerouting).",
|
||||
{
|
||||
boardPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Path to .kicad_pcb file (default: current board)"),
|
||||
boardPath: z.string().optional().describe("Path to .kicad_pcb file (default: current board)"),
|
||||
freeroutingJar: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Path to freerouting.jar (default: ~/.kicad-mcp/freerouting.jar or FREEROUTING_JAR env)",
|
||||
),
|
||||
maxPasses: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Maximum routing passes (default: 20)"),
|
||||
timeout: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Timeout in seconds (default: 300)"),
|
||||
maxPasses: z.number().optional().describe("Maximum routing passes (default: 20)"),
|
||||
timeout: z.number().optional().describe("Timeout in seconds (default: 300)"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("autoroute", args);
|
||||
@@ -53,10 +41,7 @@ export function registerFreeroutingTools(
|
||||
"export_dsn",
|
||||
"Export the current PCB to Specctra DSN format. Useful for manual Freerouting workflow or external autorouters.",
|
||||
{
|
||||
boardPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Path to .kicad_pcb file (default: current board)"),
|
||||
boardPath: z.string().optional().describe("Path to .kicad_pcb file (default: current board)"),
|
||||
outputPath: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -81,10 +66,7 @@ export function registerFreeroutingTools(
|
||||
"Import a Specctra SES (session) file into the current PCB. Use after running Freerouting externally.",
|
||||
{
|
||||
sesPath: z.string().describe("Path to the .ses file to import"),
|
||||
boardPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Path to .kicad_pcb file (default: current board)"),
|
||||
boardPath: z.string().optional().describe("Path to .kicad_pcb file (default: current board)"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("import_ses", args);
|
||||
@@ -104,10 +86,7 @@ export function registerFreeroutingTools(
|
||||
"check_freerouting",
|
||||
"Check if Java and Freerouting JAR are available on the system. Run this before autoroute to verify prerequisites.",
|
||||
{
|
||||
freeroutingJar: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Path to freerouting.jar to check"),
|
||||
freeroutingJar: z.string().optional().describe("Path to freerouting.jar to check"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("check_freerouting", args);
|
||||
|
||||
@@ -1,245 +1,295 @@
|
||||
/**
|
||||
* JLCPCB API tools for KiCAD MCP server
|
||||
* Provides access to JLCPCB's complete parts catalog via their API
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function registerJLCPCBApiTools(server: McpServer, callKicadScript: Function) {
|
||||
// Download JLCPCB parts database
|
||||
server.tool(
|
||||
"download_jlcpcb_database",
|
||||
`Download the complete JLCPCB parts catalog to local database.
|
||||
|
||||
This is a one-time setup that downloads ~2.5M+ parts from JLCSearch API.
|
||||
No API credentials required - uses public JLCSearch API.
|
||||
|
||||
The download takes 5-10 minutes and creates a local SQLite database
|
||||
for fast offline searching.`,
|
||||
{
|
||||
force: z.boolean().optional().default(false)
|
||||
.describe("Force re-download even if database exists")
|
||||
},
|
||||
async (args: { force?: boolean }) => {
|
||||
const result = await callKicadScript("download_jlcpcb_database", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `✓ Successfully downloaded JLCPCB parts database\n\n` +
|
||||
`Total parts: ${result.total_parts}\n` +
|
||||
`Basic parts: ${result.basic_parts}\n` +
|
||||
`Extended parts: ${result.extended_parts}\n` +
|
||||
`Database size: ${result.db_size_mb} MB\n` +
|
||||
`Database path: ${result.db_path}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `✗ Failed to download JLCPCB database: ${result.message || 'Unknown error'}\n\n` +
|
||||
`Make sure JLCPCB_API_KEY and JLCPCB_API_SECRET environment variables are set.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Search JLCPCB parts
|
||||
server.tool(
|
||||
"search_jlcpcb_parts",
|
||||
`Search JLCPCB parts catalog by specifications.
|
||||
|
||||
Searches the local JLCPCB database (must be downloaded first with download_jlcpcb_database).
|
||||
Provides real pricing, stock info, and library type (Basic parts = free assembly).
|
||||
|
||||
Use this to find components with exact specifications and cost optimization.`,
|
||||
{
|
||||
query: z.string().optional()
|
||||
.describe("Free-text search (e.g., '10k resistor 0603', 'ESP32', 'STM32F103')"),
|
||||
category: z.string().optional()
|
||||
.describe("Filter by category (e.g., 'Resistors', 'Capacitors', 'Microcontrollers')"),
|
||||
package: z.string().optional()
|
||||
.describe("Filter by package type (e.g., '0603', 'SOT-23', 'QFN-32')"),
|
||||
library_type: z.enum(["Basic", "Extended", "Preferred", "All"]).optional().default("All")
|
||||
.describe("Filter by library type (Basic = free assembly at JLCPCB)"),
|
||||
manufacturer: z.string().optional()
|
||||
.describe("Filter by manufacturer name"),
|
||||
in_stock: z.boolean().optional().default(true)
|
||||
.describe("Only show parts with available stock"),
|
||||
limit: z.number().optional().default(20)
|
||||
.describe("Maximum number of results to return")
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("search_jlcpcb_parts", args);
|
||||
if (result.success && result.parts) {
|
||||
if (result.parts.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `No JLCPCB parts found matching your criteria.\n\n` +
|
||||
`Try broadening your search or check if the database is populated.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const partsList = result.parts.map((p: any) => {
|
||||
const priceInfo = p.price_breaks && p.price_breaks.length > 0
|
||||
? ` - $${p.price_breaks[0].price}/ea`
|
||||
: '';
|
||||
const stockInfo = p.stock > 0 ? ` (${p.stock} in stock)` : ' (out of stock)';
|
||||
return `${p.lcsc}: ${p.mfr_part} - ${p.description} [${p.library_type}]${priceInfo}${stockInfo}`;
|
||||
}).join('\n');
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Found ${result.count} JLCPCB parts:\n\n${partsList}\n\n` +
|
||||
`💡 Basic parts have free assembly. Extended parts charge $3 setup fee per unique part.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to search JLCPCB parts: ${result.message || 'Unknown error'}\n\n` +
|
||||
`Make sure you've downloaded the database first using download_jlcpcb_database.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Get JLCPCB part details
|
||||
server.tool(
|
||||
"get_jlcpcb_part",
|
||||
"Get detailed information about a specific JLCPCB part by LCSC number",
|
||||
{
|
||||
lcsc_number: z.string()
|
||||
.describe("LCSC part number (e.g., 'C25804', 'C2286')")
|
||||
},
|
||||
async (args: { lcsc_number: string }) => {
|
||||
const result = await callKicadScript("get_jlcpcb_part", args);
|
||||
if (result.success && result.part) {
|
||||
const p = result.part;
|
||||
const priceTable = p.price_breaks && p.price_breaks.length > 0
|
||||
? '\n\nPrice Breaks:\n' + p.price_breaks.map((pb: any) =>
|
||||
` ${pb.qty}+: $${pb.price}/ea`
|
||||
).join('\n')
|
||||
: '';
|
||||
|
||||
const footprints = result.footprints && result.footprints.length > 0
|
||||
? '\n\nSuggested KiCAD Footprints:\n' + result.footprints.map((f: string) =>
|
||||
` - ${f}`
|
||||
).join('\n')
|
||||
: '';
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `LCSC: ${p.lcsc}\n` +
|
||||
`MFR Part: ${p.mfr_part}\n` +
|
||||
`Manufacturer: ${p.manufacturer}\n` +
|
||||
`Category: ${p.category} / ${p.subcategory}\n` +
|
||||
`Package: ${p.package}\n` +
|
||||
`Description: ${p.description}\n` +
|
||||
`Library Type: ${p.library_type} ${p.library_type === 'Basic' ? '(Free assembly!)' : ''}\n` +
|
||||
`Stock: ${p.stock}\n` +
|
||||
(p.datasheet ? `Datasheet: ${p.datasheet}\n` : '') +
|
||||
priceTable +
|
||||
footprints
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Part not found: ${args.lcsc_number}\n\n` +
|
||||
`Make sure you've downloaded the JLCPCB database first.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Get JLCPCB database statistics
|
||||
server.tool(
|
||||
"get_jlcpcb_database_stats",
|
||||
"Get statistics about the local JLCPCB parts database",
|
||||
{},
|
||||
async () => {
|
||||
const result = await callKicadScript("get_jlcpcb_database_stats", {});
|
||||
if (result.success) {
|
||||
const stats = result.stats;
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `JLCPCB Database Statistics:\n\n` +
|
||||
`Total parts: ${stats.total_parts.toLocaleString()}\n` +
|
||||
`Basic parts: ${stats.basic_parts.toLocaleString()} (free assembly)\n` +
|
||||
`Extended parts: ${stats.extended_parts.toLocaleString()} ($3 setup fee each)\n` +
|
||||
`In stock: ${stats.in_stock.toLocaleString()}\n` +
|
||||
`Database path: ${stats.db_path}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `JLCPCB database not found or empty.\n\n` +
|
||||
`Run download_jlcpcb_database first to populate the database.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Suggest alternative parts
|
||||
server.tool(
|
||||
"suggest_jlcpcb_alternatives",
|
||||
`Suggest alternative JLCPCB parts for a given component.
|
||||
|
||||
Finds similar parts that may be cheaper, have more stock, or are Basic library type.
|
||||
Useful for cost optimization and finding alternatives when parts are out of stock.`,
|
||||
{
|
||||
lcsc_number: z.string()
|
||||
.describe("Reference LCSC part number to find alternatives for"),
|
||||
limit: z.number().optional().default(5)
|
||||
.describe("Maximum number of alternatives to return")
|
||||
},
|
||||
async (args: { lcsc_number: string; limit?: number }) => {
|
||||
const result = await callKicadScript("suggest_jlcpcb_alternatives", args);
|
||||
if (result.success && result.alternatives) {
|
||||
if (result.alternatives.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `No alternatives found for ${args.lcsc_number}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const altsList = result.alternatives.map((p: any, i: number) => {
|
||||
const priceInfo = p.price_breaks && p.price_breaks.length > 0
|
||||
? ` - $${p.price_breaks[0].price}/ea`
|
||||
: '';
|
||||
const savings = result.reference_price && p.price_breaks && p.price_breaks.length > 0
|
||||
? ` (${((1 - p.price_breaks[0].price / result.reference_price) * 100).toFixed(0)}% cheaper)`
|
||||
: '';
|
||||
return `${i + 1}. ${p.lcsc}: ${p.mfr_part} [${p.library_type}]${priceInfo}${savings}\n ${p.description}\n Stock: ${p.stock}`;
|
||||
}).join('\n\n');
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Alternative parts for ${args.lcsc_number}:\n\n${altsList}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to find alternatives: ${result.message || 'Unknown error'}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* JLCPCB API tools for KiCAD MCP server
|
||||
* Provides access to JLCPCB's complete parts catalog via their API
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerJLCPCBApiTools(server: McpServer, callKicadScript: Function) {
|
||||
// Download JLCPCB parts database
|
||||
server.tool(
|
||||
"download_jlcpcb_database",
|
||||
`Download the complete JLCPCB parts catalog to local database.
|
||||
|
||||
This is a one-time setup that downloads ~2.5M+ parts from JLCSearch API.
|
||||
No API credentials required - uses public JLCSearch API.
|
||||
|
||||
The download takes 5-10 minutes and creates a local SQLite database
|
||||
for fast offline searching.`,
|
||||
{
|
||||
force: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe("Force re-download even if database exists"),
|
||||
},
|
||||
async (args: { force?: boolean }) => {
|
||||
const result = await callKicadScript("download_jlcpcb_database", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`✓ Successfully downloaded JLCPCB parts database\n\n` +
|
||||
`Total parts: ${result.total_parts}\n` +
|
||||
`Basic parts: ${result.basic_parts}\n` +
|
||||
`Extended parts: ${result.extended_parts}\n` +
|
||||
`Database size: ${result.db_size_mb} MB\n` +
|
||||
`Database path: ${result.db_path}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`✗ Failed to download JLCPCB database: ${result.message || "Unknown error"}\n\n` +
|
||||
`Make sure JLCPCB_API_KEY and JLCPCB_API_SECRET environment variables are set.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Search JLCPCB parts
|
||||
server.tool(
|
||||
"search_jlcpcb_parts",
|
||||
`Search JLCPCB parts catalog by specifications.
|
||||
|
||||
Searches the local JLCPCB database (must be downloaded first with download_jlcpcb_database).
|
||||
Provides real pricing, stock info, and library type (Basic parts = free assembly).
|
||||
|
||||
Use this to find components with exact specifications and cost optimization.`,
|
||||
{
|
||||
query: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Free-text search (e.g., '10k resistor 0603', 'ESP32', 'STM32F103')"),
|
||||
category: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter by category (e.g., 'Resistors', 'Capacitors', 'Microcontrollers')"),
|
||||
package: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter by package type (e.g., '0603', 'SOT-23', 'QFN-32')"),
|
||||
library_type: z
|
||||
.enum(["Basic", "Extended", "Preferred", "All"])
|
||||
.optional()
|
||||
.default("All")
|
||||
.describe("Filter by library type (Basic = free assembly at JLCPCB)"),
|
||||
manufacturer: z.string().optional().describe("Filter by manufacturer name"),
|
||||
in_stock: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe("Only show parts with available stock"),
|
||||
limit: z.number().optional().default(20).describe("Maximum number of results to return"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("search_jlcpcb_parts", args);
|
||||
if (result.success && result.parts) {
|
||||
if (result.parts.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`No JLCPCB parts found matching your criteria.\n\n` +
|
||||
`Try broadening your search or check if the database is populated.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const partsList = result.parts
|
||||
.map((p: any) => {
|
||||
const priceInfo =
|
||||
p.price_breaks && p.price_breaks.length > 0
|
||||
? ` - $${p.price_breaks[0].price}/ea`
|
||||
: "";
|
||||
const stockInfo = p.stock > 0 ? ` (${p.stock} in stock)` : " (out of stock)";
|
||||
return `${p.lcsc}: ${p.mfr_part} - ${p.description} [${p.library_type}]${priceInfo}${stockInfo}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Found ${result.count} JLCPCB parts:\n\n${partsList}\n\n` +
|
||||
`💡 Basic parts have free assembly. Extended parts charge $3 setup fee per unique part.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Failed to search JLCPCB parts: ${result.message || "Unknown error"}\n\n` +
|
||||
`Make sure you've downloaded the database first using download_jlcpcb_database.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Get JLCPCB part details
|
||||
server.tool(
|
||||
"get_jlcpcb_part",
|
||||
"Get detailed information about a specific JLCPCB part by LCSC number",
|
||||
{
|
||||
lcsc_number: z.string().describe("LCSC part number (e.g., 'C25804', 'C2286')"),
|
||||
},
|
||||
async (args: { lcsc_number: string }) => {
|
||||
const result = await callKicadScript("get_jlcpcb_part", args);
|
||||
if (result.success && result.part) {
|
||||
const p = result.part;
|
||||
const priceTable =
|
||||
p.price_breaks && p.price_breaks.length > 0
|
||||
? "\n\nPrice Breaks:\n" +
|
||||
p.price_breaks.map((pb: any) => ` ${pb.qty}+: $${pb.price}/ea`).join("\n")
|
||||
: "";
|
||||
|
||||
const footprints =
|
||||
result.footprints && result.footprints.length > 0
|
||||
? "\n\nSuggested KiCAD Footprints:\n" +
|
||||
result.footprints.map((f: string) => ` - ${f}`).join("\n")
|
||||
: "";
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`LCSC: ${p.lcsc}\n` +
|
||||
`MFR Part: ${p.mfr_part}\n` +
|
||||
`Manufacturer: ${p.manufacturer}\n` +
|
||||
`Category: ${p.category} / ${p.subcategory}\n` +
|
||||
`Package: ${p.package}\n` +
|
||||
`Description: ${p.description}\n` +
|
||||
`Library Type: ${p.library_type} ${p.library_type === "Basic" ? "(Free assembly!)" : ""}\n` +
|
||||
`Stock: ${p.stock}\n` +
|
||||
(p.datasheet ? `Datasheet: ${p.datasheet}\n` : "") +
|
||||
priceTable +
|
||||
footprints,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Part not found: ${args.lcsc_number}\n\n` +
|
||||
`Make sure you've downloaded the JLCPCB database first.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Get JLCPCB database statistics
|
||||
server.tool(
|
||||
"get_jlcpcb_database_stats",
|
||||
"Get statistics about the local JLCPCB parts database",
|
||||
{},
|
||||
async () => {
|
||||
const result = await callKicadScript("get_jlcpcb_database_stats", {});
|
||||
if (result.success) {
|
||||
const stats = result.stats;
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`JLCPCB Database Statistics:\n\n` +
|
||||
`Total parts: ${stats.total_parts.toLocaleString()}\n` +
|
||||
`Basic parts: ${stats.basic_parts.toLocaleString()} (free assembly)\n` +
|
||||
`Extended parts: ${stats.extended_parts.toLocaleString()} ($3 setup fee each)\n` +
|
||||
`In stock: ${stats.in_stock.toLocaleString()}\n` +
|
||||
`Database path: ${stats.db_path}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`JLCPCB database not found or empty.\n\n` +
|
||||
`Run download_jlcpcb_database first to populate the database.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Suggest alternative parts
|
||||
server.tool(
|
||||
"suggest_jlcpcb_alternatives",
|
||||
`Suggest alternative JLCPCB parts for a given component.
|
||||
|
||||
Finds similar parts that may be cheaper, have more stock, or are Basic library type.
|
||||
Useful for cost optimization and finding alternatives when parts are out of stock.`,
|
||||
{
|
||||
lcsc_number: z.string().describe("Reference LCSC part number to find alternatives for"),
|
||||
limit: z.number().optional().default(5).describe("Maximum number of alternatives to return"),
|
||||
},
|
||||
async (args: { lcsc_number: string; limit?: number }) => {
|
||||
const result = await callKicadScript("suggest_jlcpcb_alternatives", args);
|
||||
if (result.success && result.alternatives) {
|
||||
if (result.alternatives.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No alternatives found for ${args.lcsc_number}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const altsList = result.alternatives
|
||||
.map((p: any, i: number) => {
|
||||
const priceInfo =
|
||||
p.price_breaks && p.price_breaks.length > 0
|
||||
? ` - $${p.price_breaks[0].price}/ea`
|
||||
: "";
|
||||
const savings =
|
||||
result.reference_price && p.price_breaks && p.price_breaks.length > 0
|
||||
? ` (${((1 - p.price_breaks[0].price / result.reference_price) * 100).toFixed(0)}% cheaper)`
|
||||
: "";
|
||||
return `${i + 1}. ${p.lcsc}: ${p.mfr_part} [${p.library_type}]${priceInfo}${savings}\n ${p.description}\n Stock: ${p.stock}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Alternative parts for ${args.lcsc_number}:\n\n${altsList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to find alternatives: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Provides search/browse access to local KiCad symbol libraries
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerSymbolLibraryTools(server: McpServer, callKicadScript: Function) {
|
||||
// List available symbol libraries
|
||||
@@ -19,20 +19,20 @@ export function registerSymbolLibraryTools(server: McpServer, callKicadScript: F
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${result.count} symbol libraries:\n${result.libraries.join('\n')}`
|
||||
}
|
||||
]
|
||||
text: `Found ${result.count} symbol libraries:\n${result.libraries.join("\n")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to list symbol libraries: ${result.message || 'Unknown error'}`
|
||||
}
|
||||
]
|
||||
text: `Failed to list symbol libraries: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Search for symbols across all libraries
|
||||
@@ -45,12 +45,12 @@ Use this to find components already in your local libraries (e.g., JLCPCB-KiCad-
|
||||
|
||||
Returns symbol references that can be used directly in schematics.`,
|
||||
{
|
||||
query: z.string()
|
||||
.describe("Search query (e.g., 'ESP32', 'STM32F103', 'C8734' for LCSC ID)"),
|
||||
library: z.string().optional()
|
||||
query: z.string().describe("Search query (e.g., 'ESP32', 'STM32F103', 'C8734' for LCSC ID)"),
|
||||
library: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional: filter to specific library name pattern (e.g., 'JLCPCB')"),
|
||||
limit: z.number().optional().default(20)
|
||||
.describe("Maximum number of results to return")
|
||||
limit: z.number().optional().default(20).describe("Maximum number of results to return"),
|
||||
},
|
||||
async (args: { query: string; library?: string; limit?: number }) => {
|
||||
const result = await callKicadScript("search_symbols", args);
|
||||
@@ -60,38 +60,40 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No symbols found matching "${args.query}"${args.library ? ` in libraries matching "${args.library}"` : ''}`
|
||||
}
|
||||
]
|
||||
text: `No symbols found matching "${args.query}"${args.library ? ` in libraries matching "${args.library}"` : ""}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const symbolList = result.symbols.map((s: any) => {
|
||||
const parts = [`${s.full_ref}`];
|
||||
if (s.lcsc_id) parts.push(`LCSC: ${s.lcsc_id}`);
|
||||
if (s.description) parts.push(s.description);
|
||||
else if (s.value) parts.push(s.value);
|
||||
return parts.join(' | ');
|
||||
}).join('\n');
|
||||
const symbolList = result.symbols
|
||||
.map((s: any) => {
|
||||
const parts = [`${s.full_ref}`];
|
||||
if (s.lcsc_id) parts.push(`LCSC: ${s.lcsc_id}`);
|
||||
if (s.description) parts.push(s.description);
|
||||
else if (s.value) parts.push(s.value);
|
||||
return parts.join(" | ");
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${result.count} symbols matching "${args.query}":\n\n${symbolList}`
|
||||
}
|
||||
]
|
||||
text: `Found ${result.count} symbols matching "${args.query}":\n\n${symbolList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to search symbols: ${result.message || 'Unknown error'}`
|
||||
}
|
||||
]
|
||||
text: `Failed to search symbols: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// List symbols in a specific library
|
||||
@@ -99,36 +101,37 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
"list_library_symbols",
|
||||
"List all symbols in a specific KiCAD symbol library",
|
||||
{
|
||||
library: z.string()
|
||||
.describe("Library name (e.g., 'Device', 'PCM_JLCPCB-MCUs')")
|
||||
library: z.string().describe("Library name (e.g., 'Device', 'PCM_JLCPCB-MCUs')"),
|
||||
},
|
||||
async (args: { library: string }) => {
|
||||
const result = await callKicadScript("list_library_symbols", args);
|
||||
if (result.success && result.symbols) {
|
||||
const symbolList = result.symbols.map((s: any) => {
|
||||
const parts = [` - ${s.name}`];
|
||||
if (s.lcsc_id) parts.push(`(LCSC: ${s.lcsc_id})`);
|
||||
return parts.join(' ');
|
||||
}).join('\n');
|
||||
const symbolList = result.symbols
|
||||
.map((s: any) => {
|
||||
const parts = [` - ${s.name}`];
|
||||
if (s.lcsc_id) parts.push(`(LCSC: ${s.lcsc_id})`);
|
||||
return parts.join(" ");
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Library "${args.library}" contains ${result.count} symbols:\n${symbolList}`
|
||||
}
|
||||
]
|
||||
text: `Library "${args.library}" contains ${result.count} symbols:\n${symbolList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to list symbols in library ${args.library}: ${result.message || 'Unknown error'}`
|
||||
}
|
||||
]
|
||||
text: `Failed to list symbols in library ${args.library}: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Get detailed information about a specific symbol
|
||||
@@ -136,8 +139,9 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
"get_symbol_info",
|
||||
"Get detailed information about a specific symbol",
|
||||
{
|
||||
symbol: z.string()
|
||||
.describe("Symbol specification (e.g., 'Device:R' or 'PCM_JLCPCB-MCUs:STM32F103C8T6')")
|
||||
symbol: z
|
||||
.string()
|
||||
.describe("Symbol specification (e.g., 'Device:R' or 'PCM_JLCPCB-MCUs:STM32F103C8T6')"),
|
||||
},
|
||||
async (args: { symbol: string }) => {
|
||||
const result = await callKicadScript("get_symbol_info", args);
|
||||
@@ -145,34 +149,36 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
const info = result.symbol_info;
|
||||
const details = [
|
||||
`Symbol: ${info.full_ref}`,
|
||||
info.value ? `Value: ${info.value}` : '',
|
||||
info.description ? `Description: ${info.description}` : '',
|
||||
info.lcsc_id ? `LCSC: ${info.lcsc_id}` : '',
|
||||
info.manufacturer ? `Manufacturer: ${info.manufacturer}` : '',
|
||||
info.mpn ? `MPN: ${info.mpn}` : '',
|
||||
info.footprint ? `Footprint: ${info.footprint}` : '',
|
||||
info.category ? `Category: ${info.category}` : '',
|
||||
info.lib_class ? `Class: ${info.lib_class}` : '',
|
||||
info.datasheet ? `Datasheet: ${info.datasheet}` : '',
|
||||
].filter(line => line).join('\n');
|
||||
info.value ? `Value: ${info.value}` : "",
|
||||
info.description ? `Description: ${info.description}` : "",
|
||||
info.lcsc_id ? `LCSC: ${info.lcsc_id}` : "",
|
||||
info.manufacturer ? `Manufacturer: ${info.manufacturer}` : "",
|
||||
info.mpn ? `MPN: ${info.mpn}` : "",
|
||||
info.footprint ? `Footprint: ${info.footprint}` : "",
|
||||
info.category ? `Category: ${info.category}` : "",
|
||||
info.lib_class ? `Class: ${info.lib_class}` : "",
|
||||
info.datasheet ? `Datasheet: ${info.datasheet}` : "",
|
||||
]
|
||||
.filter((line) => line)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: details
|
||||
}
|
||||
]
|
||||
text: details,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to get symbol info: ${result.message || 'Unknown error'}`
|
||||
}
|
||||
]
|
||||
text: `Failed to get symbol info: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerLibraryTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerLibraryTools(server: McpServer, callKicadScript: Function) {
|
||||
// List available footprint libraries
|
||||
server.tool(
|
||||
"list_libraries",
|
||||
@@ -48,18 +45,9 @@ export function registerLibraryTools(
|
||||
"search_footprints",
|
||||
"Search for footprints matching a pattern across all libraries",
|
||||
{
|
||||
search_term: z
|
||||
.string()
|
||||
.describe("Search term or pattern to match footprint names"),
|
||||
library: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional specific library to search in"),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.default(50)
|
||||
.describe("Maximum number of results to return"),
|
||||
search_term: z.string().describe("Search term or pattern to match footprint names"),
|
||||
library: z.string().optional().describe("Optional specific library to search in"),
|
||||
limit: z.number().optional().default(50).describe("Maximum number of results to return"),
|
||||
},
|
||||
async (args: { search_term: string; library?: string; limit?: number }) => {
|
||||
const result = await callKicadScript("search_footprints", {
|
||||
@@ -99,25 +87,14 @@ export function registerLibraryTools(
|
||||
"list_library_footprints",
|
||||
"List all footprints in a specific KiCAD library",
|
||||
{
|
||||
library_name: z
|
||||
.string()
|
||||
.describe("Name of the library to list footprints from"),
|
||||
filter: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional filter pattern for footprint names"),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.default(100)
|
||||
.describe("Maximum number of footprints to list"),
|
||||
library_name: z.string().describe("Name of the library to list footprints from"),
|
||||
filter: z.string().optional().describe("Optional filter pattern for footprint names"),
|
||||
limit: z.number().optional().default(100).describe("Maximum number of footprints to list"),
|
||||
},
|
||||
async (args: { library_name: string; filter?: string; limit?: number }) => {
|
||||
const result = await callKicadScript("list_library_footprints", args);
|
||||
if (result.success && result.footprints) {
|
||||
const footprintList = result.footprints
|
||||
.map((fp: string) => ` - ${fp}`)
|
||||
.join("\n");
|
||||
const footprintList = result.footprints.map((fp: string) => ` - ${fp}`).join("\n");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -143,12 +120,8 @@ export function registerLibraryTools(
|
||||
"get_footprint_info",
|
||||
"Get detailed information about a specific footprint",
|
||||
{
|
||||
library_name: z
|
||||
.string()
|
||||
.describe("Name of the library containing the footprint"),
|
||||
footprint_name: z
|
||||
.string()
|
||||
.describe("Name of the footprint to get information about"),
|
||||
library_name: z.string().describe("Name of the library containing the footprint"),
|
||||
footprint_name: z.string().describe("Name of the footprint to get information about"),
|
||||
},
|
||||
async (args: { library_name: string; footprint_name: string }) => {
|
||||
const result = await callKicadScript("get_footprint_info", args);
|
||||
@@ -156,15 +129,16 @@ export function registerLibraryTools(
|
||||
const info = result.info;
|
||||
|
||||
// pads is a list of {number, type, shape} objects
|
||||
const padsArray: Array<{ number: string; type: string; shape: string }> =
|
||||
Array.isArray(info.pads) ? info.pads : [];
|
||||
const padsArray: Array<{ number: string; type: string; shape: string }> = Array.isArray(
|
||||
info.pads,
|
||||
)
|
||||
? info.pads
|
||||
: [];
|
||||
const padsSummary = padsArray.length
|
||||
? `${padsArray.length} pads: ${padsArray.map((p) => p.number).join(", ")}`
|
||||
: "";
|
||||
const padsDetail = padsArray.length
|
||||
? padsArray
|
||||
.map((p) => ` pad ${p.number}: ${p.type} ${p.shape}`)
|
||||
.join("\n")
|
||||
? padsArray.map((p) => ` pad ${p.number}: ${p.type} ${p.shape}`).join("\n")
|
||||
: "";
|
||||
|
||||
const details = [
|
||||
@@ -178,9 +152,7 @@ export function registerLibraryTools(
|
||||
info.courtyard
|
||||
? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm`
|
||||
: "",
|
||||
info.attributes
|
||||
? `Attributes: ${JSON.stringify(info.attributes)}`
|
||||
: "",
|
||||
info.attributes ? `Attributes: ${JSON.stringify(info.attributes)}` : "",
|
||||
]
|
||||
.filter((line) => line)
|
||||
.join("\n");
|
||||
|
||||
@@ -1,99 +1,116 @@
|
||||
/**
|
||||
* Project management tools for KiCAD MCP server
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function registerProjectTools(server: McpServer, callKicadScript: Function) {
|
||||
// Create project tool
|
||||
server.tool(
|
||||
"create_project",
|
||||
"Create a new KiCAD project",
|
||||
{
|
||||
path: z.string().describe("Project directory path"),
|
||||
name: z.string().describe("Project name"),
|
||||
},
|
||||
async (args: { path: string; name: string }) => {
|
||||
const result = await callKicadScript("create_project", args);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Open project tool
|
||||
server.tool(
|
||||
"open_project",
|
||||
"Open an existing KiCAD project",
|
||||
{
|
||||
filename: z.string().describe("Path to .kicad_pro or .kicad_pcb file"),
|
||||
},
|
||||
async (args: { filename: string }) => {
|
||||
const result = await callKicadScript("open_project", args);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Save project tool
|
||||
server.tool(
|
||||
"save_project",
|
||||
"Save the current KiCAD project",
|
||||
{
|
||||
path: z.string().optional().describe("Optional new path to save to"),
|
||||
},
|
||||
async (args: { path?: string }) => {
|
||||
const result = await callKicadScript("save_project", args);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Get project info tool
|
||||
server.tool(
|
||||
"get_project_info",
|
||||
"Get information about the current KiCAD project",
|
||||
{},
|
||||
async () => {
|
||||
const result = await callKicadScript("get_project_info", {});
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Snapshot project tool — saves a named checkpoint as PDF/image
|
||||
server.tool(
|
||||
"snapshot_project",
|
||||
"Save a named checkpoint snapshot of the current project state (renders board to PDF and records step label). Call after completing each major step — e.g. after Step 1 (schematic_ok) and Step 2 (layout_ok). Required by the demo workflow before waiting for user confirmation.",
|
||||
{
|
||||
step: z.string().describe("Step number or identifier, e.g. '1' or '2'"),
|
||||
label: z.string().describe("Short label for this checkpoint, e.g. 'schematic_ok' or 'layout_ok'"),
|
||||
prompt: z.string().optional().describe("Full prompt text to save as PROMPT_step{step}_{timestamp}.md alongside the snapshot"),
|
||||
},
|
||||
async (args: { step: string; label: string; prompt?: string }) => {
|
||||
const result = await callKicadScript("snapshot_project", args);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Project management tools for KiCAD MCP server
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerProjectTools(server: McpServer, callKicadScript: Function) {
|
||||
// Create project tool
|
||||
server.tool(
|
||||
"create_project",
|
||||
"Create a new KiCAD project",
|
||||
{
|
||||
path: z.string().describe("Project directory path"),
|
||||
name: z.string().describe("Project name"),
|
||||
},
|
||||
async (args: { path: string; name: string }) => {
|
||||
const result = await callKicadScript("create_project", args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Open project tool
|
||||
server.tool(
|
||||
"open_project",
|
||||
"Open an existing KiCAD project",
|
||||
{
|
||||
filename: z.string().describe("Path to .kicad_pro or .kicad_pcb file"),
|
||||
},
|
||||
async (args: { filename: string }) => {
|
||||
const result = await callKicadScript("open_project", args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Save project tool
|
||||
server.tool(
|
||||
"save_project",
|
||||
"Save the current KiCAD project",
|
||||
{
|
||||
path: z.string().optional().describe("Optional new path to save to"),
|
||||
},
|
||||
async (args: { path?: string }) => {
|
||||
const result = await callKicadScript("save_project", args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Get project info tool
|
||||
server.tool(
|
||||
"get_project_info",
|
||||
"Get information about the current KiCAD project",
|
||||
{},
|
||||
async () => {
|
||||
const result = await callKicadScript("get_project_info", {});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Snapshot project tool — saves a named checkpoint as PDF/image
|
||||
server.tool(
|
||||
"snapshot_project",
|
||||
"Save a named checkpoint snapshot of the current project state (renders board to PDF and records step label). Call after completing each major step — e.g. after Step 1 (schematic_ok) and Step 2 (layout_ok). Required by the demo workflow before waiting for user confirmation.",
|
||||
{
|
||||
step: z.string().describe("Step number or identifier, e.g. '1' or '2'"),
|
||||
label: z
|
||||
.string()
|
||||
.describe("Short label for this checkpoint, e.g. 'schematic_ok' or 'layout_ok'"),
|
||||
prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Full prompt text to save as PROMPT_step{step}_{timestamp}.md alongside the snapshot",
|
||||
),
|
||||
},
|
||||
async (args: { step: string; label: string; prompt?: string }) => {
|
||||
const result = await callKicadScript("snapshot_project", args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,308 +1,295 @@
|
||||
/**
|
||||
* Tool Registry for KiCAD MCP Server
|
||||
*
|
||||
* Centralizes all tool definitions and provides lookup/search functionality
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: z.ZodObject<any> | z.ZodType<any>;
|
||||
// Handler will be registered separately in the existing tool files
|
||||
}
|
||||
|
||||
export interface ToolCategory {
|
||||
name: string;
|
||||
description: string;
|
||||
tools: string[]; // Tool names in this category
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool category definitions
|
||||
* Each category groups related tools for better organization
|
||||
*/
|
||||
export const toolCategories: ToolCategory[] = [
|
||||
{
|
||||
name: "board",
|
||||
description: "Board configuration: layers, mounting holes, zones, visualization",
|
||||
tools: [
|
||||
"add_layer",
|
||||
"set_active_layer",
|
||||
"get_layer_list",
|
||||
"add_mounting_hole",
|
||||
"add_board_text",
|
||||
"add_zone",
|
||||
"get_board_extents",
|
||||
"get_board_2d_view",
|
||||
"launch_kicad_ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "component",
|
||||
description: "Advanced component operations: edit, delete, search, group, annotate",
|
||||
tools: [
|
||||
"rotate_component",
|
||||
"delete_component",
|
||||
"edit_component",
|
||||
"find_component",
|
||||
"get_component_properties",
|
||||
"add_component_annotation",
|
||||
"group_components",
|
||||
"replace_component"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "export",
|
||||
description: "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
|
||||
tools: [
|
||||
"export_gerber",
|
||||
"export_pdf",
|
||||
"export_svg",
|
||||
"export_3d",
|
||||
"export_bom",
|
||||
"export_netlist",
|
||||
"export_position_file",
|
||||
"export_vrml"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "drc",
|
||||
description: "Design rule checking and electrical validation: DRC, net classes, clearances",
|
||||
tools: [
|
||||
"set_design_rules",
|
||||
"get_design_rules",
|
||||
"run_drc",
|
||||
"add_net_class",
|
||||
"assign_net_to_class",
|
||||
"set_layer_constraints",
|
||||
"check_clearance",
|
||||
"get_drc_violations"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "schematic",
|
||||
description: "Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation",
|
||||
tools: [
|
||||
"create_schematic",
|
||||
"add_schematic_component",
|
||||
"list_schematic_components",
|
||||
"move_schematic_component",
|
||||
"rotate_schematic_component",
|
||||
"annotate_schematic",
|
||||
"add_schematic_wire",
|
||||
"delete_schematic_wire",
|
||||
"add_schematic_junction",
|
||||
"add_schematic_net_label",
|
||||
"delete_schematic_net_label",
|
||||
"connect_to_net",
|
||||
"connect_passthrough",
|
||||
"get_net_connections",
|
||||
"list_schematic_nets",
|
||||
"list_schematic_wires",
|
||||
"list_schematic_labels",
|
||||
"get_wire_connections",
|
||||
"generate_netlist",
|
||||
"sync_schematic_to_board",
|
||||
"get_schematic_view",
|
||||
"export_schematic_svg",
|
||||
"export_schematic_pdf"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "library",
|
||||
description: "Footprint library access: search, browse, get footprint information",
|
||||
tools: [
|
||||
"list_libraries",
|
||||
"search_footprints",
|
||||
"list_library_footprints",
|
||||
"get_footprint_info"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "routing",
|
||||
description: "Advanced routing operations: vias, copper pours",
|
||||
tools: [
|
||||
"add_via",
|
||||
"add_copper_pour"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "autoroute",
|
||||
description: "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES",
|
||||
tools: [
|
||||
"autoroute",
|
||||
"export_dsn",
|
||||
"import_ses",
|
||||
"check_freerouting"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Direct tools that are always visible (not routed)
|
||||
* These are the most frequently used tools
|
||||
*/
|
||||
export const directToolNames = [
|
||||
// Project lifecycle
|
||||
"create_project",
|
||||
"open_project",
|
||||
"save_project",
|
||||
"snapshot_project",
|
||||
"get_project_info",
|
||||
|
||||
// Core PCB operations
|
||||
"place_component",
|
||||
"move_component",
|
||||
"add_net",
|
||||
"route_trace",
|
||||
"get_board_info",
|
||||
"set_board_size",
|
||||
|
||||
// Board setup
|
||||
"add_board_outline",
|
||||
|
||||
// Schematic essentials (always visible so AI uses them correctly)
|
||||
"add_schematic_component",
|
||||
"list_schematic_components",
|
||||
"annotate_schematic",
|
||||
"connect_passthrough",
|
||||
"connect_to_net",
|
||||
"add_schematic_net_label",
|
||||
|
||||
// Schematic <-> PCB sync (F8 equivalent)
|
||||
"sync_schematic_to_board",
|
||||
|
||||
// UI management
|
||||
"check_kicad_ui"
|
||||
];
|
||||
|
||||
// Build lookup maps at module load time
|
||||
const categoryMap = new Map<string, ToolCategory>();
|
||||
const toolCategoryMap = new Map<string, string>();
|
||||
|
||||
export function initializeRegistry() {
|
||||
// Build category map
|
||||
for (const category of toolCategories) {
|
||||
categoryMap.set(category.name, category);
|
||||
|
||||
// Build tool -> category map
|
||||
for (const toolName of category.tools) {
|
||||
toolCategoryMap.set(toolName, category.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a category by name
|
||||
*/
|
||||
export function getCategory(name: string): ToolCategory | undefined {
|
||||
return categoryMap.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category name for a tool
|
||||
*/
|
||||
export function getToolCategory(toolName: string): string | undefined {
|
||||
return toolCategoryMap.get(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*/
|
||||
export function getAllCategories(): ToolCategory[] {
|
||||
return toolCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all routed tool names (excludes direct tools)
|
||||
*/
|
||||
export function getRoutedToolNames(): string[] {
|
||||
const allRoutedTools: string[] = [];
|
||||
for (const category of toolCategories) {
|
||||
allRoutedTools.push(...category.tools);
|
||||
}
|
||||
return allRoutedTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is a direct tool
|
||||
*/
|
||||
export function isDirectTool(toolName: string): boolean {
|
||||
return directToolNames.includes(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is a routed tool
|
||||
*/
|
||||
export function isRoutedTool(toolName: string): boolean {
|
||||
return toolCategoryMap.has(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tools by keyword
|
||||
* Searches tool names, descriptions, and category names
|
||||
*/
|
||||
export interface SearchResult {
|
||||
category: string;
|
||||
tool: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function searchTools(query: string): SearchResult[] {
|
||||
const q = query.toLowerCase();
|
||||
const matches: SearchResult[] = [];
|
||||
|
||||
// Search direct tools first
|
||||
for (const toolName of directToolNames) {
|
||||
if (toolName.toLowerCase().includes(q)) {
|
||||
matches.push({
|
||||
category: "direct",
|
||||
tool: toolName,
|
||||
description: `${toolName} (direct tool — call directly, no execute_tool needed)`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Search routed tools by name and category
|
||||
for (const category of toolCategories) {
|
||||
const categoryMatch =
|
||||
category.name.toLowerCase().includes(q) ||
|
||||
category.description.toLowerCase().includes(q);
|
||||
|
||||
for (const toolName of category.tools) {
|
||||
if (toolName.toLowerCase().includes(q) || categoryMatch) {
|
||||
matches.push({
|
||||
category: category.name,
|
||||
tool: toolName,
|
||||
description: `${toolName} (${category.name})`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches.slice(0, 20); // Limit results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about the tool registry
|
||||
*/
|
||||
export function getRegistryStats() {
|
||||
const routedToolCount = getRoutedToolNames().length;
|
||||
const directToolCount = directToolNames.length;
|
||||
|
||||
return {
|
||||
total_categories: toolCategories.length,
|
||||
total_routed_tools: routedToolCount,
|
||||
total_direct_tools: directToolCount,
|
||||
total_tools: routedToolCount + directToolCount,
|
||||
categories: toolCategories.map(c => ({
|
||||
name: c.name,
|
||||
tool_count: c.tools.length
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeRegistry();
|
||||
/**
|
||||
* Tool Registry for KiCAD MCP Server
|
||||
*
|
||||
* Centralizes all tool definitions and provides lookup/search functionality
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: z.ZodObject<any> | z.ZodType<any>;
|
||||
// Handler will be registered separately in the existing tool files
|
||||
}
|
||||
|
||||
export interface ToolCategory {
|
||||
name: string;
|
||||
description: string;
|
||||
tools: string[]; // Tool names in this category
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool category definitions
|
||||
* Each category groups related tools for better organization
|
||||
*/
|
||||
export const toolCategories: ToolCategory[] = [
|
||||
{
|
||||
name: "board",
|
||||
description: "Board configuration: layers, mounting holes, zones, visualization",
|
||||
tools: [
|
||||
"add_layer",
|
||||
"set_active_layer",
|
||||
"get_layer_list",
|
||||
"add_mounting_hole",
|
||||
"add_board_text",
|
||||
"add_zone",
|
||||
"get_board_extents",
|
||||
"get_board_2d_view",
|
||||
"launch_kicad_ui",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "component",
|
||||
description: "Advanced component operations: edit, delete, search, group, annotate",
|
||||
tools: [
|
||||
"rotate_component",
|
||||
"delete_component",
|
||||
"edit_component",
|
||||
"find_component",
|
||||
"get_component_properties",
|
||||
"add_component_annotation",
|
||||
"group_components",
|
||||
"replace_component",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "export",
|
||||
description: "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
|
||||
tools: [
|
||||
"export_gerber",
|
||||
"export_pdf",
|
||||
"export_svg",
|
||||
"export_3d",
|
||||
"export_bom",
|
||||
"export_netlist",
|
||||
"export_position_file",
|
||||
"export_vrml",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "drc",
|
||||
description: "Design rule checking and electrical validation: DRC, net classes, clearances",
|
||||
tools: [
|
||||
"set_design_rules",
|
||||
"get_design_rules",
|
||||
"run_drc",
|
||||
"add_net_class",
|
||||
"assign_net_to_class",
|
||||
"set_layer_constraints",
|
||||
"check_clearance",
|
||||
"get_drc_violations",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "schematic",
|
||||
description:
|
||||
"Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation",
|
||||
tools: [
|
||||
"create_schematic",
|
||||
"add_schematic_component",
|
||||
"list_schematic_components",
|
||||
"move_schematic_component",
|
||||
"rotate_schematic_component",
|
||||
"annotate_schematic",
|
||||
"add_schematic_wire",
|
||||
"delete_schematic_wire",
|
||||
"add_schematic_junction",
|
||||
"add_schematic_net_label",
|
||||
"delete_schematic_net_label",
|
||||
"connect_to_net",
|
||||
"connect_passthrough",
|
||||
"get_net_connections",
|
||||
"list_schematic_nets",
|
||||
"list_schematic_wires",
|
||||
"list_schematic_labels",
|
||||
"get_wire_connections",
|
||||
"generate_netlist",
|
||||
"sync_schematic_to_board",
|
||||
"get_schematic_view",
|
||||
"export_schematic_svg",
|
||||
"export_schematic_pdf",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "library",
|
||||
description: "Footprint library access: search, browse, get footprint information",
|
||||
tools: ["list_libraries", "search_footprints", "list_library_footprints", "get_footprint_info"],
|
||||
},
|
||||
{
|
||||
name: "routing",
|
||||
description: "Advanced routing operations: vias, copper pours",
|
||||
tools: ["add_via", "add_copper_pour"],
|
||||
},
|
||||
{
|
||||
name: "autoroute",
|
||||
description: "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES",
|
||||
tools: ["autoroute", "export_dsn", "import_ses", "check_freerouting"],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Direct tools that are always visible (not routed)
|
||||
* These are the most frequently used tools
|
||||
*/
|
||||
export const directToolNames = [
|
||||
// Project lifecycle
|
||||
"create_project",
|
||||
"open_project",
|
||||
"save_project",
|
||||
"snapshot_project",
|
||||
"get_project_info",
|
||||
|
||||
// Core PCB operations
|
||||
"place_component",
|
||||
"move_component",
|
||||
"add_net",
|
||||
"route_trace",
|
||||
"get_board_info",
|
||||
"set_board_size",
|
||||
|
||||
// Board setup
|
||||
"add_board_outline",
|
||||
|
||||
// Schematic essentials (always visible so AI uses them correctly)
|
||||
"add_schematic_component",
|
||||
"list_schematic_components",
|
||||
"annotate_schematic",
|
||||
"connect_passthrough",
|
||||
"connect_to_net",
|
||||
"add_schematic_net_label",
|
||||
|
||||
// Schematic <-> PCB sync (F8 equivalent)
|
||||
"sync_schematic_to_board",
|
||||
|
||||
// UI management
|
||||
"check_kicad_ui",
|
||||
];
|
||||
|
||||
// Build lookup maps at module load time
|
||||
const categoryMap = new Map<string, ToolCategory>();
|
||||
const toolCategoryMap = new Map<string, string>();
|
||||
|
||||
export function initializeRegistry() {
|
||||
// Build category map
|
||||
for (const category of toolCategories) {
|
||||
categoryMap.set(category.name, category);
|
||||
|
||||
// Build tool -> category map
|
||||
for (const toolName of category.tools) {
|
||||
toolCategoryMap.set(toolName, category.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a category by name
|
||||
*/
|
||||
export function getCategory(name: string): ToolCategory | undefined {
|
||||
return categoryMap.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category name for a tool
|
||||
*/
|
||||
export function getToolCategory(toolName: string): string | undefined {
|
||||
return toolCategoryMap.get(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*/
|
||||
export function getAllCategories(): ToolCategory[] {
|
||||
return toolCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all routed tool names (excludes direct tools)
|
||||
*/
|
||||
export function getRoutedToolNames(): string[] {
|
||||
const allRoutedTools: string[] = [];
|
||||
for (const category of toolCategories) {
|
||||
allRoutedTools.push(...category.tools);
|
||||
}
|
||||
return allRoutedTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is a direct tool
|
||||
*/
|
||||
export function isDirectTool(toolName: string): boolean {
|
||||
return directToolNames.includes(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is a routed tool
|
||||
*/
|
||||
export function isRoutedTool(toolName: string): boolean {
|
||||
return toolCategoryMap.has(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tools by keyword
|
||||
* Searches tool names, descriptions, and category names
|
||||
*/
|
||||
export interface SearchResult {
|
||||
category: string;
|
||||
tool: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function searchTools(query: string): SearchResult[] {
|
||||
const q = query.toLowerCase();
|
||||
const matches: SearchResult[] = [];
|
||||
|
||||
// Search direct tools first
|
||||
for (const toolName of directToolNames) {
|
||||
if (toolName.toLowerCase().includes(q)) {
|
||||
matches.push({
|
||||
category: "direct",
|
||||
tool: toolName,
|
||||
description: `${toolName} (direct tool — call directly, no execute_tool needed)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Search routed tools by name and category
|
||||
for (const category of toolCategories) {
|
||||
const categoryMatch =
|
||||
category.name.toLowerCase().includes(q) || category.description.toLowerCase().includes(q);
|
||||
|
||||
for (const toolName of category.tools) {
|
||||
if (toolName.toLowerCase().includes(q) || categoryMatch) {
|
||||
matches.push({
|
||||
category: category.name,
|
||||
tool: toolName,
|
||||
description: `${toolName} (${category.name})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches.slice(0, 20); // Limit results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about the tool registry
|
||||
*/
|
||||
export function getRegistryStats() {
|
||||
const routedToolCount = getRoutedToolNames().length;
|
||||
const directToolCount = directToolNames.length;
|
||||
|
||||
return {
|
||||
total_categories: toolCategories.length,
|
||||
total_routed_tools: routedToolCount,
|
||||
total_direct_tools: directToolCount,
|
||||
total_tools: routedToolCount + directToolCount,
|
||||
categories: toolCategories.map((c) => ({
|
||||
name: c.name,
|
||||
tool_count: c.tools.length,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeRegistry();
|
||||
|
||||
@@ -1,251 +1,296 @@
|
||||
/**
|
||||
* Router Tools for KiCAD MCP Server
|
||||
*
|
||||
* Provides discovery and execution of routed tools
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../logger.js';
|
||||
import {
|
||||
getAllCategories,
|
||||
getCategory,
|
||||
getToolCategory,
|
||||
searchTools as registrySearchTools,
|
||||
getRegistryStats
|
||||
} from './registry.js';
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
// Map to store tool execution handlers
|
||||
// This will be populated by registerToolHandler()
|
||||
const toolHandlers = new Map<string, (params: any) => Promise<any>>();
|
||||
|
||||
/**
|
||||
* Register a tool handler for execution via execute_tool
|
||||
* This should be called by each tool registration function
|
||||
*/
|
||||
export function registerToolHandler(
|
||||
toolName: string,
|
||||
handler: (params: any) => Promise<any>
|
||||
): void {
|
||||
toolHandlers.set(toolName, handler);
|
||||
logger.debug(`Registered handler for routed tool: ${toolName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all router tools with the MCP server
|
||||
*/
|
||||
export function registerRouterTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info('Registering router tools');
|
||||
|
||||
// ============================================================================
|
||||
// list_tool_categories
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"list_tool_categories",
|
||||
{
|
||||
// No parameters
|
||||
},
|
||||
async () => {
|
||||
logger.debug('Listing tool categories');
|
||||
|
||||
const stats = getRegistryStats();
|
||||
const categories = getAllCategories();
|
||||
|
||||
const result = {
|
||||
total_categories: stats.total_categories,
|
||||
total_routed_tools: stats.total_routed_tools,
|
||||
total_direct_tools: stats.total_direct_tools,
|
||||
note: "Use get_category_tools to see tools in each category. Direct tools are always available.",
|
||||
categories: categories.map(c => ({
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
tool_count: c.tools.length
|
||||
}))
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// get_category_tools
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"get_category_tools",
|
||||
{
|
||||
category: z.string().describe("Category name from list_tool_categories")
|
||||
},
|
||||
async ({ category }) => {
|
||||
logger.debug(`Getting tools for category: ${category}`);
|
||||
|
||||
const categoryData = getCategory(category);
|
||||
|
||||
if (!categoryData) {
|
||||
const availableCategories = getAllCategories().map(c => c.name);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: `Unknown category: ${category}`,
|
||||
available_categories: availableCategories
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Return tool names and basic info
|
||||
// Full schema is available via tool introspection once tool is called
|
||||
const result = {
|
||||
category: categoryData.name,
|
||||
description: categoryData.description,
|
||||
tool_count: categoryData.tools.length,
|
||||
tools: categoryData.tools.map(toolName => ({
|
||||
name: toolName,
|
||||
description: `Use execute_tool with tool_name="${toolName}" to run this tool`
|
||||
})),
|
||||
note: "Use execute_tool to run any of these tools with appropriate parameters"
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// execute_tool
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"execute_tool",
|
||||
{
|
||||
tool_name: z.string().describe("Tool name from get_category_tools"),
|
||||
params: z.record(z.unknown()).optional().describe("Tool parameters (optional)")
|
||||
},
|
||||
async ({ tool_name, params }) => {
|
||||
logger.info(`Executing routed tool: ${tool_name}`);
|
||||
|
||||
// Check if tool exists in registry
|
||||
const category = getToolCategory(tool_name);
|
||||
|
||||
if (!category) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: `Unknown tool: ${tool_name}`,
|
||||
hint: "Use list_tool_categories and get_category_tools to find available tools"
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Get the handler
|
||||
const handler = toolHandlers.get(tool_name);
|
||||
|
||||
if (!handler) {
|
||||
// Tool is in registry but handler not registered yet
|
||||
// This means the tool exists but hasn't been migrated to router pattern yet
|
||||
// Fall back to calling KiCAD script directly
|
||||
logger.warn(`Tool ${tool_name} in registry but no handler registered, falling back to direct call`);
|
||||
|
||||
try {
|
||||
const result = await callKicadScript(tool_name, params || {});
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: tool_name,
|
||||
category: category,
|
||||
result: result
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: `Tool execution failed: ${(error as Error).message}`,
|
||||
tool: tool_name,
|
||||
category: category
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the tool via its handler
|
||||
try {
|
||||
const result = await handler(params || {});
|
||||
|
||||
// The handler already returns MCP-formatted response
|
||||
// Just add metadata
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: tool_name,
|
||||
category: category,
|
||||
...result
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: `Tool execution failed: ${(error as Error).message}`,
|
||||
tool: tool_name,
|
||||
category: category
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// search_tools
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"search_tools",
|
||||
{
|
||||
query: z.string().describe("Search term (e.g., 'gerber', 'zone', 'export', 'drc')")
|
||||
},
|
||||
async ({ query }) => {
|
||||
logger.debug(`Searching tools for: ${query}`);
|
||||
|
||||
const matches = registrySearchTools(query);
|
||||
|
||||
const result = {
|
||||
query: query,
|
||||
count: matches.length,
|
||||
matches: matches,
|
||||
note: matches.length > 0
|
||||
? "Use execute_tool with the tool name to run it"
|
||||
: "No tools found matching your query. Try list_tool_categories to browse all categories."
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Router tools registered successfully');
|
||||
}
|
||||
/**
|
||||
* Router Tools for KiCAD MCP Server
|
||||
*
|
||||
* Provides discovery and execution of routed tools
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
import {
|
||||
getAllCategories,
|
||||
getCategory,
|
||||
getToolCategory,
|
||||
searchTools as registrySearchTools,
|
||||
getRegistryStats,
|
||||
} from "./registry.js";
|
||||
|
||||
// Command function type for KiCAD script calls
|
||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
||||
|
||||
// Map to store tool execution handlers
|
||||
// This will be populated by registerToolHandler()
|
||||
const toolHandlers = new Map<string, (params: any) => Promise<any>>();
|
||||
|
||||
/**
|
||||
* Register a tool handler for execution via execute_tool
|
||||
* This should be called by each tool registration function
|
||||
*/
|
||||
export function registerToolHandler(
|
||||
toolName: string,
|
||||
handler: (params: any) => Promise<any>,
|
||||
): void {
|
||||
toolHandlers.set(toolName, handler);
|
||||
logger.debug(`Registered handler for routed tool: ${toolName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all router tools with the MCP server
|
||||
*/
|
||||
export function registerRouterTools(server: McpServer, callKicadScript: CommandFunction): void {
|
||||
logger.info("Registering router tools");
|
||||
|
||||
// ============================================================================
|
||||
// list_tool_categories
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"list_tool_categories",
|
||||
{
|
||||
// No parameters
|
||||
},
|
||||
async () => {
|
||||
logger.debug("Listing tool categories");
|
||||
|
||||
const stats = getRegistryStats();
|
||||
const categories = getAllCategories();
|
||||
|
||||
const result = {
|
||||
total_categories: stats.total_categories,
|
||||
total_routed_tools: stats.total_routed_tools,
|
||||
total_direct_tools: stats.total_direct_tools,
|
||||
note: "Use get_category_tools to see tools in each category. Direct tools are always available.",
|
||||
categories: categories.map((c) => ({
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
tool_count: c.tools.length,
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// get_category_tools
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"get_category_tools",
|
||||
{
|
||||
category: z.string().describe("Category name from list_tool_categories"),
|
||||
},
|
||||
async ({ category }) => {
|
||||
logger.debug(`Getting tools for category: ${category}`);
|
||||
|
||||
const categoryData = getCategory(category);
|
||||
|
||||
if (!categoryData) {
|
||||
const availableCategories = getAllCategories().map((c) => c.name);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
error: `Unknown category: ${category}`,
|
||||
available_categories: availableCategories,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Return tool names and basic info
|
||||
// Full schema is available via tool introspection once tool is called
|
||||
const result = {
|
||||
category: categoryData.name,
|
||||
description: categoryData.description,
|
||||
tool_count: categoryData.tools.length,
|
||||
tools: categoryData.tools.map((toolName) => ({
|
||||
name: toolName,
|
||||
description: `Use execute_tool with tool_name="${toolName}" to run this tool`,
|
||||
})),
|
||||
note: "Use execute_tool to run any of these tools with appropriate parameters",
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// execute_tool
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"execute_tool",
|
||||
{
|
||||
tool_name: z.string().describe("Tool name from get_category_tools"),
|
||||
params: z.record(z.unknown()).optional().describe("Tool parameters (optional)"),
|
||||
},
|
||||
async ({ tool_name, params }) => {
|
||||
logger.info(`Executing routed tool: ${tool_name}`);
|
||||
|
||||
// Check if tool exists in registry
|
||||
const category = getToolCategory(tool_name);
|
||||
|
||||
if (!category) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
error: `Unknown tool: ${tool_name}`,
|
||||
hint: "Use list_tool_categories and get_category_tools to find available tools",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Get the handler
|
||||
const handler = toolHandlers.get(tool_name);
|
||||
|
||||
if (!handler) {
|
||||
// Tool is in registry but handler not registered yet
|
||||
// This means the tool exists but hasn't been migrated to router pattern yet
|
||||
// Fall back to calling KiCAD script directly
|
||||
logger.warn(
|
||||
`Tool ${tool_name} in registry but no handler registered, falling back to direct call`,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await callKicadScript(tool_name, params || {});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
tool: tool_name,
|
||||
category: category,
|
||||
result: result,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
error: `Tool execution failed: ${(error as Error).message}`,
|
||||
tool: tool_name,
|
||||
category: category,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the tool via its handler
|
||||
try {
|
||||
const result = await handler(params || {});
|
||||
|
||||
// The handler already returns MCP-formatted response
|
||||
// Just add metadata
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
tool: tool_name,
|
||||
category: category,
|
||||
...result,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
error: `Tool execution failed: ${(error as Error).message}`,
|
||||
tool: tool_name,
|
||||
category: category,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// search_tools
|
||||
// ============================================================================
|
||||
server.tool(
|
||||
"search_tools",
|
||||
{
|
||||
query: z.string().describe("Search term (e.g., 'gerber', 'zone', 'export', 'drc')"),
|
||||
},
|
||||
async ({ query }) => {
|
||||
logger.debug(`Searching tools for: ${query}`);
|
||||
|
||||
const matches = registrySearchTools(query);
|
||||
|
||||
const result = {
|
||||
query: query,
|
||||
count: matches.length,
|
||||
matches: matches,
|
||||
note:
|
||||
matches.length > 0
|
||||
? "Use execute_tool with the tool name to run it"
|
||||
: "No tools found matching your query. Try list_tool_categories to browse all categories.",
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
logger.info("Router tools registered successfully");
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerRoutingTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerRoutingTools(server: McpServer, callKicadScript: Function) {
|
||||
// Add net tool
|
||||
server.tool(
|
||||
"add_net",
|
||||
@@ -79,10 +76,7 @@ export function registerRoutingTools(
|
||||
})
|
||||
.describe("Via position"),
|
||||
net: z.string().describe("Net name"),
|
||||
viaType: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Via type (through, blind, buried)"),
|
||||
viaType: z.string().optional().describe("Via type (through, blind, buried)"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("add_via", args);
|
||||
@@ -130,10 +124,7 @@ export function registerRoutingTools(
|
||||
"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"),
|
||||
traceUuid: z.string().optional().describe("UUID of a specific trace to delete"),
|
||||
position: z
|
||||
.object({
|
||||
x: z.number(),
|
||||
@@ -142,18 +133,9 @@ export function registerRoutingTools(
|
||||
})
|
||||
.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"),
|
||||
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);
|
||||
@@ -209,10 +191,7 @@ export function registerRoutingTools(
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Include statistics (track count, total length, etc.)"),
|
||||
unit: z
|
||||
.enum(["mm", "inch"])
|
||||
.optional()
|
||||
.describe("Unit for length measurements"),
|
||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for length measurements"),
|
||||
},
|
||||
async (args: any) => {
|
||||
const result = await callKicadScript("get_nets_list", args);
|
||||
@@ -334,9 +313,13 @@ export function registerRoutingTools(
|
||||
"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)"),
|
||||
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)"),
|
||||
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)"),
|
||||
@@ -362,10 +345,7 @@ export function registerRoutingTools(
|
||||
.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)"),
|
||||
includeVias: z.boolean().optional().describe("Also copy vias (default: true)"),
|
||||
traceWidth: z
|
||||
.number()
|
||||
.optional()
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export function registerSchematicTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerSchematicTools(server: McpServer, callKicadScript: Function) {
|
||||
// Create schematic tool
|
||||
server.tool(
|
||||
"create_schematic",
|
||||
@@ -38,9 +35,7 @@ export function registerSchematicTools(
|
||||
schematicPath: z.string().describe("Path to the schematic file"),
|
||||
symbol: z
|
||||
.string()
|
||||
.describe(
|
||||
"Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)",
|
||||
),
|
||||
.describe("Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)"),
|
||||
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
||||
value: z.string().optional().describe("Component value"),
|
||||
footprint: z
|
||||
@@ -82,10 +77,7 @@ export function registerSchematicTools(
|
||||
},
|
||||
};
|
||||
|
||||
const result = await callKicadScript(
|
||||
"add_schematic_component",
|
||||
transformed,
|
||||
);
|
||||
const result = await callKicadScript("add_schematic_component", transformed);
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [
|
||||
@@ -122,9 +114,7 @@ To remove a footprint from a PCB, use delete_component instead.`,
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
reference: z
|
||||
.string()
|
||||
.describe(
|
||||
"Reference designator of the component to remove (e.g. R1, U3)",
|
||||
),
|
||||
.describe("Reference designator of the component to remove (e.g. R1, U3)"),
|
||||
},
|
||||
async (args: { schematicPath: string; reference: string }) => {
|
||||
const result = await callKicadScript("delete_schematic_component", args);
|
||||
@@ -162,14 +152,27 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
reference: z.string().describe("Current reference designator of the component (e.g. R1, U3)"),
|
||||
footprint: z.string().optional().describe("New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)"),
|
||||
footprint: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)"),
|
||||
value: z.string().optional().describe("New value string (e.g. 10k, 100nF)"),
|
||||
newReference: z.string().optional().describe("Rename the reference designator (e.g. R1 → R10)"),
|
||||
fieldPositions: z.record(z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
angle: z.number().optional().default(0),
|
||||
})).optional().describe("Reposition field labels: map of field name to {x, y, angle} (e.g. {\"Reference\": {\"x\": 12.5, \"y\": 17.0}})"),
|
||||
newReference: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Rename the reference designator (e.g. R1 → R10)"),
|
||||
fieldPositions: z
|
||||
.record(
|
||||
z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
angle: z.number().optional().default(0),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
'Reposition field labels: map of field name to {x, y, angle} (e.g. {"Reference": {"x": 12.5, "y": 17.0}})',
|
||||
),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
@@ -220,20 +223,24 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
: "unknown";
|
||||
const fieldLines = Object.entries(result.fields ?? {}).map(
|
||||
([name, f]: [string, any]) =>
|
||||
` ${name}: "${f.value}" @ (${f.x}, ${f.y}, angle=${f.angle}°)`
|
||||
` ${name}: "${f.value}" @ (${f.x}, ${f.y}, angle=${f.angle}°)`,
|
||||
);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Component ${result.reference} at ${pos}\nFields:\n${fieldLines.join("\n")}`,
|
||||
}],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Component ${result.reference} at ${pos}\nFields:\n${fieldLines.join("\n")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to get component: ${result.message || "Unknown error"}`,
|
||||
}],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to get component: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -251,13 +258,8 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
snapToPins: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Snap the first and last waypoints to the nearest pin (default: true)",
|
||||
),
|
||||
snapTolerance: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Maximum snap distance in mm (default: 1.0)"),
|
||||
.describe("Snap the first and last waypoints to the nearest pin (default: true)"),
|
||||
snapTolerance: z.number().optional().describe("Maximum snap distance in mm (default: 1.0)"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
@@ -294,10 +296,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"Place a junction dot at a wire intersection in the schematic. Required at T-branch and X-cross points so KiCAD recognises the electrical connection.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
position: z
|
||||
.array(z.number())
|
||||
.length(2)
|
||||
.describe("Junction position [x, y] in mm"),
|
||||
position: z.array(z.number()).length(2).describe("Junction position [x, y] in mm"),
|
||||
},
|
||||
async (args: { schematicPath: string; position: number[] }) => {
|
||||
const result = await callKicadScript("add_schematic_junction", args);
|
||||
@@ -329,19 +328,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"Add a net label to the schematic",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the schematic file"),
|
||||
netName: z
|
||||
.string()
|
||||
.describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
|
||||
position: z
|
||||
.array(z.number())
|
||||
.length(2)
|
||||
.describe("Position [x, y] for the label"),
|
||||
netName: z.string().describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
|
||||
position: z.array(z.number()).length(2).describe("Position [x, y] for the label"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
netName: string;
|
||||
position: number[];
|
||||
}) => {
|
||||
async (args: { schematicPath: string; netName: string; position: number[] }) => {
|
||||
const result = await callKicadScript("add_schematic_net_label", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
@@ -451,9 +441,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
async (args: { schematicPath: string; x: number; y: number }) => {
|
||||
const result = await callKicadScript("get_wire_connections", args);
|
||||
if (result.success && result.pins) {
|
||||
const pinList = result.pins
|
||||
.map((p: any) => ` - ${p.component}/${p.pin}`)
|
||||
.join("\n");
|
||||
const pinList = result.pins.map((p: any) => ` - ${p.component}/${p.pin}`).join("\n");
|
||||
const wireList = (result.wires ?? [])
|
||||
.map((w: any) => ` - (${w.start.x},${w.start.y}) → (${w.end.x},${w.end.y})`)
|
||||
.join("\n");
|
||||
@@ -484,9 +472,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"Returns the exact x/y coordinates of every pin on a schematic component. Use this before add_schematic_net_label to place labels correctly on pin endpoints.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the schematic file"),
|
||||
reference: z
|
||||
.string()
|
||||
.describe("Component reference designator (e.g. U1, R1, J2)"),
|
||||
reference: z.string().describe("Component reference designator (e.g. U1, R1, J2)"),
|
||||
},
|
||||
async (args: { schematicPath: string; reference: string }) => {
|
||||
const result = await callKicadScript("get_schematic_pin_locations", args);
|
||||
@@ -541,23 +527,16 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
pinOffset?: number;
|
||||
}) => {
|
||||
const result = await callKicadScript("connect_passthrough", args);
|
||||
if (
|
||||
result.success !== false ||
|
||||
(result.connected && result.connected.length > 0)
|
||||
) {
|
||||
if (result.success !== false || (result.connected && result.connected.length > 0)) {
|
||||
const lines: string[] = [];
|
||||
if (result.connected?.length)
|
||||
lines.push(
|
||||
`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`,
|
||||
);
|
||||
if (result.failed?.length)
|
||||
lines.push(
|
||||
`Failed (${result.failed.length}): ${result.failed.join(", ")}`,
|
||||
);
|
||||
lines.push(`Failed (${result.failed.length}): ${result.failed.join(", ")}`);
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: result.message + "\n" + lines.join("\n") },
|
||||
],
|
||||
content: [{ type: "text", text: result.message + "\n" + lines.join("\n") }],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -581,7 +560,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
filter: z
|
||||
.object({
|
||||
libId: z.string().optional().describe("Filter by library ID (e.g., 'Device:R')"),
|
||||
referencePrefix: z.string().optional().describe("Filter by reference prefix (e.g., 'R', 'C', 'U')"),
|
||||
referencePrefix: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter by reference prefix (e.g., 'R', 'C', 'U')"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Optional filters"),
|
||||
@@ -640,9 +622,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
};
|
||||
}
|
||||
const lines = nets.map((n: any) => {
|
||||
const conns = (n.connections || [])
|
||||
.map((c: any) => `${c.component}/${c.pin}`)
|
||||
.join(", ");
|
||||
const conns = (n.connections || []).map((c: any) => `${c.component}/${c.pin}`).join(", ");
|
||||
return ` ${n.name}: ${conns || "(no connections)"}`;
|
||||
});
|
||||
return {
|
||||
@@ -680,8 +660,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
};
|
||||
}
|
||||
const lines = wires.map(
|
||||
(w: any) =>
|
||||
` (${w.start.x}, ${w.start.y}) → (${w.end.x}, ${w.end.y})`,
|
||||
(w: any) => ` (${w.start.x}, ${w.start.y}) → (${w.end.x}, ${w.end.y})`,
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
@@ -718,8 +697,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
};
|
||||
}
|
||||
const lines = labels.map(
|
||||
(l: any) =>
|
||||
` [${l.type}] ${l.name} at (${l.position.x}, ${l.position.y})`,
|
||||
(l: any) => ` [${l.type}] ${l.name} at (${l.position.x}, ${l.position.y})`,
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
@@ -789,10 +767,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
reference: z.string().describe("Reference designator (e.g., R1, U1)"),
|
||||
angle: z.number().describe("Rotation angle in degrees (0, 90, 180, 270)"),
|
||||
mirror: z
|
||||
.enum(["x", "y"])
|
||||
.optional()
|
||||
.describe("Optional mirror axis"),
|
||||
mirror: z.enum(["x", "y"]).optional().describe("Optional mirror axis"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
@@ -836,14 +811,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
const annotated = result.annotated || [];
|
||||
if (annotated.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "All components are already annotated." },
|
||||
],
|
||||
content: [{ type: "text", text: "All components are already annotated." }],
|
||||
};
|
||||
}
|
||||
const lines = annotated.map(
|
||||
(a: any) => ` ${a.oldReference} → ${a.newReference}`,
|
||||
);
|
||||
const lines = annotated.map((a: any) => ` ${a.oldReference} → ${a.newReference}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -871,12 +842,8 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"Remove a wire from the schematic by start and end coordinates.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
start: z
|
||||
.object({ x: z.number(), y: z.number() })
|
||||
.describe("Wire start position"),
|
||||
end: z
|
||||
.object({ x: z.number(), y: z.number() })
|
||||
.describe("Wire end position"),
|
||||
start: z.object({ x: z.number(), y: z.number() }).describe("Wire start position"),
|
||||
end: z.object({ x: z.number(), y: z.number() }).describe("Wire end position"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
@@ -953,16 +920,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
outputPath: z.string().describe("Output SVG file path"),
|
||||
blackAndWhite: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Export in black and white"),
|
||||
blackAndWhite: z.boolean().optional().describe("Export in black and white"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
outputPath: string;
|
||||
blackAndWhite?: boolean;
|
||||
}) => {
|
||||
async (args: { schematicPath: string; outputPath: string; blackAndWhite?: boolean }) => {
|
||||
const result = await callKicadScript("export_schematic_svg", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
@@ -993,16 +953,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
outputPath: z.string().describe("Output PDF file path"),
|
||||
blackAndWhite: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Export in black and white"),
|
||||
blackAndWhite: z.boolean().optional().describe("Export in black and white"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
outputPath: string;
|
||||
blackAndWhite?: boolean;
|
||||
}) => {
|
||||
async (args: { schematicPath: string; outputPath: string; blackAndWhite?: boolean }) => {
|
||||
const result = await callKicadScript("export_schematic_pdf", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
@@ -1032,10 +985,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"Return a rasterized image of the schematic (PNG by default, or SVG). Uses kicad-cli to export SVG, then converts to PNG via cairosvg. Use this for visual feedback after placing or wiring components.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
format: z
|
||||
.enum(["png", "svg"])
|
||||
.optional()
|
||||
.describe("Output format (default: png)"),
|
||||
format: z.enum(["png", "svg"]).optional().describe("Output format (default: png)"),
|
||||
width: z.number().optional().describe("Image width in pixels (default: 1200)"),
|
||||
height: z.number().optional().describe("Image height in pixels (default: 900)"),
|
||||
},
|
||||
@@ -1086,17 +1036,13 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"run_erc",
|
||||
"Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.",
|
||||
{
|
||||
schematicPath: z
|
||||
.string()
|
||||
.describe("Path to the .kicad_sch schematic file"),
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
},
|
||||
async (args: { schematicPath: string }) => {
|
||||
const result = await callKicadScript("run_erc", args);
|
||||
if (result.success) {
|
||||
const violations: any[] = result.violations || [];
|
||||
const lines: string[] = [
|
||||
`ERC result: ${violations.length} violation(s)`,
|
||||
];
|
||||
const lines: string[] = [`ERC result: ${violations.length} violation(s)`];
|
||||
if (result.summary?.by_severity) {
|
||||
const s = result.summary.by_severity;
|
||||
lines.push(
|
||||
@@ -1183,12 +1129,8 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"sync_schematic_to_board",
|
||||
"Import the schematic netlist into the PCB board — equivalent to pressing F8 in KiCAD (Tools → Update PCB from Schematic). MUST be called after the schematic is complete and before placing or routing components on the PCB. Without this step, the board has no footprints and no net assignments — place_component and route_pad_to_pad will produce an empty, unroutable board.",
|
||||
{
|
||||
schematicPath: z
|
||||
.string()
|
||||
.describe("Absolute path to the .kicad_sch schematic file"),
|
||||
boardPath: z
|
||||
.string()
|
||||
.describe("Absolute path to the .kicad_pcb board file"),
|
||||
schematicPath: z.string().describe("Absolute path to the .kicad_sch schematic file"),
|
||||
boardPath: z.string().describe("Absolute path to the .kicad_pcb board file"),
|
||||
},
|
||||
async (args: { schematicPath: string; boardPath: string }) => {
|
||||
const result = await callKicadScript("sync_schematic_to_board", args);
|
||||
@@ -1218,8 +1160,13 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
x1: number; y1: number; x2: number; y2: number;
|
||||
format?: string; width?: number; height?: number;
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
format?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}) => {
|
||||
const result = await callKicadScript("get_schematic_view_region", args);
|
||||
if (result.success && result.imageData) {
|
||||
@@ -1227,11 +1174,13 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
return { content: [{ type: "text", text: result.imageData }] };
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: "image",
|
||||
data: result.imageData,
|
||||
mimeType: "image/png",
|
||||
}],
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
data: result.imageData,
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -1240,14 +1189,18 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
// Find overlapping elements
|
||||
server.tool(
|
||||
"find_overlapping_elements",
|
||||
"Detect spatially overlapping symbols, wires, and labels in the schematic. Finds duplicate power symbols at the same position, collinear overlapping wires, and labels stacked on top of each other.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
||||
tolerance: z.number().optional().describe("Distance threshold in mm for label proximity and wire collinearity checks. Symbol overlap uses bounding-box intersection. (default: 0.5)"),
|
||||
tolerance: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Distance threshold in mm for label proximity and wire collinearity checks. Symbol overlap uses bounding-box intersection. (default: 0.5)",
|
||||
),
|
||||
},
|
||||
async (args: { schematicPath: string; tolerance?: number }) => {
|
||||
const result = await callKicadScript("find_overlapping_elements", args);
|
||||
@@ -1259,7 +1212,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
if (syms.length) {
|
||||
lines.push(`\nOverlapping symbols (${syms.length}):`);
|
||||
syms.slice(0, 20).forEach((o: any) => {
|
||||
lines.push(` ${o.element1.reference} ↔ ${o.element2.reference} (${o.distance}mm) [${o.type}]`);
|
||||
lines.push(
|
||||
` ${o.element1.reference} ↔ ${o.element2.reference} (${o.distance}mm) [${o.type}]`,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (lbls.length) {
|
||||
@@ -1271,7 +1226,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
if (wires.length) {
|
||||
lines.push(`\nOverlapping wires (${wires.length}):`);
|
||||
wires.slice(0, 20).forEach((o: any) => {
|
||||
lines.push(` wire @ (${o.wire1.start.x},${o.wire1.start.y})→(${o.wire1.end.x},${o.wire1.end.y}) overlaps with another`);
|
||||
lines.push(
|
||||
` wire @ (${o.wire1.start.x},${o.wire1.start.y})→(${o.wire1.end.x},${o.wire1.end.y}) overlaps with another`,
|
||||
);
|
||||
});
|
||||
}
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
@@ -1293,20 +1250,21 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
x2: z.number().describe("Right X coordinate of the region in mm"),
|
||||
y2: z.number().describe("Bottom Y coordinate of the region in mm"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
x1: number; y1: number; x2: number; y2: number;
|
||||
}) => {
|
||||
async (args: { schematicPath: string; x1: number; y1: number; x2: number; y2: number }) => {
|
||||
const result = await callKicadScript("get_elements_in_region", args);
|
||||
if (result.success) {
|
||||
const c = result.counts;
|
||||
const lines = [`Region (${args.x1},${args.y1})→(${args.x2},${args.y2}): ${c.symbols} symbols, ${c.wires} wires, ${c.labels} labels`];
|
||||
const lines = [
|
||||
`Region (${args.x1},${args.y1})→(${args.x2},${args.y2}): ${c.symbols} symbols, ${c.wires} wires, ${c.labels} labels`,
|
||||
];
|
||||
const syms: any[] = result.symbols || [];
|
||||
if (syms.length) {
|
||||
lines.push("\nSymbols:");
|
||||
syms.forEach((s: any) => {
|
||||
const pinCount = s.pins ? Object.keys(s.pins).length : 0;
|
||||
lines.push(` ${s.reference} (${s.libId}) @ (${s.position.x}, ${s.position.y}) [${pinCount} pins]`);
|
||||
lines.push(
|
||||
` ${s.reference} (${s.libId}) @ (${s.position.x}, ${s.position.y}) [${pinCount} pins]`,
|
||||
);
|
||||
});
|
||||
}
|
||||
const wires: any[] = result.wires || [];
|
||||
@@ -1346,7 +1304,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
const lines = [`Found ${collisions.length} wire(s) crossing symbols:`];
|
||||
collisions.slice(0, 30).forEach((c: any, i: number) => {
|
||||
lines.push(
|
||||
` ${i + 1}. Wire (${c.wire.start.x},${c.wire.start.y})→(${c.wire.end.x},${c.wire.end.y}) crosses ${c.component.reference} (${c.component.libId})`
|
||||
` ${i + 1}. Wire (${c.wire.start.x},${c.wire.start.y})→(${c.wire.end.x},${c.wire.end.y}) crosses ${c.component.reference} (${c.component.libId})`,
|
||||
);
|
||||
});
|
||||
if (collisions.length > 30) lines.push(` ... and ${collisions.length - 30} more`);
|
||||
|
||||
@@ -15,45 +15,67 @@ const PinSchema = z.object({
|
||||
number: z.union([z.string(), z.number()]).describe("Pin number, e.g. '1', '2', 'A1'"),
|
||||
type: z
|
||||
.enum([
|
||||
"input", "output", "bidirectional", "tri_state", "passive",
|
||||
"free", "unspecified", "power_in", "power_out",
|
||||
"open_collector", "open_emitter", "no_connect",
|
||||
"input",
|
||||
"output",
|
||||
"bidirectional",
|
||||
"tri_state",
|
||||
"passive",
|
||||
"free",
|
||||
"unspecified",
|
||||
"power_in",
|
||||
"power_out",
|
||||
"open_collector",
|
||||
"open_emitter",
|
||||
"no_connect",
|
||||
])
|
||||
.describe("Electrical pin type"),
|
||||
at: z.object({
|
||||
x: z.number().describe("X position in mm"),
|
||||
y: z.number().describe("Y position in mm"),
|
||||
angle: z.number().describe(
|
||||
"Direction the pin wire extends FROM the symbol body: 0=right, 90=up, 180=left, 270=down"
|
||||
),
|
||||
}).describe("Pin endpoint position (where the wire connects)"),
|
||||
at: z
|
||||
.object({
|
||||
x: z.number().describe("X position in mm"),
|
||||
y: z.number().describe("Y position in mm"),
|
||||
angle: z
|
||||
.number()
|
||||
.describe(
|
||||
"Direction the pin wire extends FROM the symbol body: 0=right, 90=up, 180=left, 270=down",
|
||||
),
|
||||
})
|
||||
.describe("Pin endpoint position (where the wire connects)"),
|
||||
length: z.number().optional().describe("Pin length in mm (default 2.54)"),
|
||||
shape: z
|
||||
.enum(["line", "inverted", "clock", "inverted_clock", "input_low",
|
||||
"clock_low", "output_low", "falling_edge_clock", "non_logic"])
|
||||
.enum([
|
||||
"line",
|
||||
"inverted",
|
||||
"clock",
|
||||
"inverted_clock",
|
||||
"input_low",
|
||||
"clock_low",
|
||||
"output_low",
|
||||
"falling_edge_clock",
|
||||
"non_logic",
|
||||
])
|
||||
.optional()
|
||||
.describe("Pin graphic shape (default: line)"),
|
||||
});
|
||||
|
||||
const RectSchema = z.object({
|
||||
x1: z.number(), y1: z.number(),
|
||||
x2: z.number(), y2: z.number(),
|
||||
x1: z.number(),
|
||||
y1: z.number(),
|
||||
x2: z.number(),
|
||||
y2: z.number(),
|
||||
width: z.number().optional().describe("Stroke width in mm (default 0.254)"),
|
||||
fill: z.enum(["none", "outline", "background"]).optional()
|
||||
fill: z
|
||||
.enum(["none", "outline", "background"])
|
||||
.optional()
|
||||
.describe("Fill type (default: background)"),
|
||||
});
|
||||
|
||||
const PolylineSchema = z.object({
|
||||
points: z.array(z.object({ x: z.number(), y: z.number() }))
|
||||
.describe("List of XY points in mm"),
|
||||
points: z.array(z.object({ x: z.number(), y: z.number() })).describe("List of XY points in mm"),
|
||||
width: z.number().optional().describe("Stroke width in mm (default 0.254)"),
|
||||
fill: z.enum(["none", "outline", "background"]).optional(),
|
||||
});
|
||||
|
||||
export function registerSymbolCreatorTools(
|
||||
server: McpServer,
|
||||
callKicadScript: Function,
|
||||
) {
|
||||
export function registerSymbolCreatorTools(server: McpServer, callKicadScript: Function) {
|
||||
// ── create_symbol ────────────────────────────────────────────────────── //
|
||||
server.tool(
|
||||
"create_symbol",
|
||||
@@ -68,14 +90,14 @@ export function registerSymbolCreatorTools(
|
||||
"- Pins on bottom: at.y = body_bottom - length, angle=90 (wire goes up)\n" +
|
||||
"- Standard pin length: 2.54 mm, standard grid: 2.54 mm",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe("Path to the .kicad_sym file (created if missing)"),
|
||||
libraryPath: z.string().describe("Path to the .kicad_sym file (created if missing)"),
|
||||
name: z.string().describe("Symbol name, e.g. 'TMC2209', 'MyOpAmp'"),
|
||||
referencePrefix: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Schematic reference prefix: 'U' (IC), 'R' (resistor), 'J' (connector), etc. Default: 'U'"),
|
||||
.describe(
|
||||
"Schematic reference prefix: 'U' (IC), 'R' (resistor), 'J' (connector), etc. Default: 'U'",
|
||||
),
|
||||
description: z.string().optional().describe("Human-readable description"),
|
||||
keywords: z.string().optional().describe("Space-separated search keywords"),
|
||||
datasheet: z.string().optional().describe("Datasheet URL or '~'"),
|
||||
@@ -161,9 +183,7 @@ export function registerSymbolCreatorTools(
|
||||
"Register a .kicad_sym library in KiCAD's sym-lib-table so symbols can be used in schematics. " +
|
||||
"Run this after create_symbol when KiCAD shows 'library not found'.",
|
||||
{
|
||||
libraryPath: z
|
||||
.string()
|
||||
.describe("Full path to the .kicad_sym file"),
|
||||
libraryPath: z.string().describe("Full path to the .kicad_sym file"),
|
||||
libraryName: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
100
src/tools/ui.ts
100
src/tools/ui.ts
@@ -1,48 +1,52 @@
|
||||
/**
|
||||
* UI/Process management tools for KiCAD MCP server
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
export function registerUITools(server: McpServer, callKicadScript: Function) {
|
||||
// Check if KiCAD UI is running
|
||||
server.tool(
|
||||
"check_kicad_ui",
|
||||
"Check if KiCAD UI is currently running",
|
||||
{},
|
||||
async () => {
|
||||
logger.info('Checking KiCAD UI status');
|
||||
const result = await callKicadScript("check_kicad_ui", {});
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Launch KiCAD UI
|
||||
server.tool(
|
||||
"launch_kicad_ui",
|
||||
"Launch KiCAD UI, optionally with a project file",
|
||||
{
|
||||
projectPath: z.string().optional().describe("Optional path to .kicad_pcb file to open"),
|
||||
autoLaunch: z.boolean().optional().describe("Whether to launch KiCAD if not running (default: true)")
|
||||
},
|
||||
async (args: { projectPath?: string; autoLaunch?: boolean }) => {
|
||||
logger.info(`Launching KiCAD UI${args.projectPath ? ' with project: ' + args.projectPath : ''}`);
|
||||
const result = await callKicadScript("launch_kicad_ui", args);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('UI management tools registered');
|
||||
}
|
||||
/**
|
||||
* UI/Process management tools for KiCAD MCP server
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
export function registerUITools(server: McpServer, callKicadScript: Function) {
|
||||
// Check if KiCAD UI is running
|
||||
server.tool("check_kicad_ui", "Check if KiCAD UI is currently running", {}, async () => {
|
||||
logger.info("Checking KiCAD UI status");
|
||||
const result = await callKicadScript("check_kicad_ui", {});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Launch KiCAD UI
|
||||
server.tool(
|
||||
"launch_kicad_ui",
|
||||
"Launch KiCAD UI, optionally with a project file",
|
||||
{
|
||||
projectPath: z.string().optional().describe("Optional path to .kicad_pcb file to open"),
|
||||
autoLaunch: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether to launch KiCAD if not running (default: true)"),
|
||||
},
|
||||
async (args: { projectPath?: string; autoLaunch?: boolean }) => {
|
||||
logger.info(
|
||||
`Launching KiCAD UI${args.projectPath ? " with project: " + args.projectPath : ""}`,
|
||||
);
|
||||
const result = await callKicadScript("launch_kicad_ui", args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
logger.info("UI management tools registered");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user