feat: datasheet tools + fix missing delete_schematic_component

New tools - datasheet:
- get_datasheet_url: construct LCSC datasheet PDF URL + product page URL
  without any API key (URL schema: https://www.lcsc.com/datasheet/<C#>.pdf)
- enrich_datasheets: scan .kicad_sch, write LCSC datasheet URL into every
  symbol that has an LCSC property but an empty Datasheet field; supports
  dry_run=true for preview; text-based implementation (no skip writes)
  Implementation: python/commands/datasheet_manager.py

New tool - schematic:
- delete_schematic_component: remove a placed symbol from a .kicad_sch file
  by reference designator (e.g. R1, U3)

Bug fix - delete_schematic_component (two separate root causes):
1. No MCP tool named delete_schematic_component was registered at all.
   Any delete-symbol request fell through to the PCB-only delete_component
   tool which searches pcbnew.BOARD and always returned 'Component not found'
   for schematic symbols.
2. component_schematic.py::remove_component() still used skip for writes.
   PR #40 rewrote DynamicSymbolLoader (add path) to avoid skip-induced
   schematic corruption, but the delete path was not touched by that PR.

Fix: _handle_delete_schematic_component in kicad_interface.py uses direct
text manipulation with parenthesis-depth tracking (same technique as PR #40),
bypassing component_schematic.py entirely. Error message explicitly guides
users: 'use delete_component for PCB footprints'.

Files changed:
- python/commands/datasheet_manager.py (new)
- src/tools/datasheet.ts (new)
- python/kicad_interface.py: 3 new handlers + dispatch entries
- src/tools/schematic.ts: delete_schematic_component tool
- src/server.ts: registerDatasheetTools import + call
- src/tools/index.ts: export registerDatasheetTools
- CHANGELOG.md: document all above
This commit is contained in:
Tom
2026-02-28 13:48:35 +01:00
parent 76503b144c
commit 1ba86f7769
7 changed files with 985 additions and 283 deletions

123
src/tools/datasheet.ts Normal file
View File

@@ -0,0 +1,123 @@
/**
* Datasheet tools for KiCAD MCP server
*
* Enriches KiCAD schematic symbols with LCSC datasheet URLs.
* URL schema: https://www.lcsc.com/datasheet/<LCSC#>.pdf (no API key required)
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerDatasheetTools(
server: McpServer,
callKicadScript: Function,
) {
// ── enrich_datasheets ──────────────────────────────────────────────────────
server.tool(
"enrich_datasheets",
`Fill in missing Datasheet URLs in a KiCAD schematic using LCSC part numbers.
For every placed symbol that has:
• (property "LCSC" "C123456") set
• (property "Datasheet" "~") or empty
Sets the Datasheet field to:
https://www.lcsc.com/datasheet/C123456.pdf
The URL is then visible in KiCAD's footprint browser, symbol properties dialog,
and any tool that reads the standard KiCAD Datasheet field.
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"),
dry_run: z
.boolean()
.optional()
.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);
if (result.success) {
const lines: string[] = [];
if (args.dry_run) {
lines.push(`[DRY RUN] Schematic: ${result.schematic}\n`);
} else {
lines.push(`Schematic: ${result.schematic}\n`);
}
lines.push(`✓ Updated: ${result.updated}`);
lines.push(` Already set: ${result.already_set}`);
lines.push(` No LCSC number: ${result.no_lcsc}`);
lines.push(` No field: ${result.no_datasheet_field}`);
if (result.details && result.details.length > 0) {
lines.push("\nComponents updated:");
for (const d of result.details) {
lines.push(` ${d.reference.padEnd(6)} ${d.lcsc.padEnd(12)}${d.url}`);
}
}
if (result.updated === 0 && !args.dry_run) {
lines.push(
"\nNo changes needed all LCSC components already have a Datasheet URL.",
);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
}
return {
content: [
{
type: "text",
text: `Failed to enrich datasheets: ${result.message || "Unknown error"}`,
},
],
};
},
);
// ── get_datasheet_url ──────────────────────────────────────────────────────
server.tool(
"get_datasheet_url",
`Get the LCSC datasheet URL for a component by LCSC number.
Returns the direct PDF URL and product page URL.
No network request URL is constructed from the LCSC number alone.
Example: get_datasheet_url("C179739")
→ https://www.lcsc.com/datasheet/C179739.pdf`,
{
lcsc: z
.string()
.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 });
if (result.success) {
const lines = [
`LCSC: ${result.lcsc}`,
`Datasheet PDF: ${result.datasheet_url}`,
`Product page: ${result.product_url}`,
];
return { content: [{ type: "text", text: lines.join("\n") }] };
}
return {
content: [
{
type: "text",
text: `Invalid LCSC number: ${args.lcsc}`,
},
],
};
},
);
}

View File

@@ -1,15 +1,16 @@
/**
* Tools index for KiCAD MCP server
*
* Exports all tool registration functions
*/
export { registerProjectTools } from './project.js';
export { registerBoardTools } from './board.js';
export { registerComponentTools } from './component.js';
export { registerRoutingTools } from './routing.js';
export { registerDesignRuleTools } from './design-rules.js';
export { registerExportTools } from './export.js';
export { registerSchematicTools } from './schematic.js';
export { registerLibraryTools } from './library.js';
export { registerUITools } from './ui.js';
/**
* Tools index for KiCAD MCP server
*
* Exports all tool registration functions
*/
export { registerProjectTools } from "./project.js";
export { registerBoardTools } from "./board.js";
export { registerComponentTools } from "./component.js";
export { registerRoutingTools } from "./routing.js";
export { registerDesignRuleTools } from "./design-rules.js";
export { registerExportTools } from "./export.js";
export { registerSchematicTools } from "./schematic.js";
export { registerLibraryTools } from "./library.js";
export { registerUITools } from "./ui.js";
export { registerDatasheetTools } from "./datasheet.js";

View File

@@ -1,268 +1,383 @@
/**
* Schematic tools for KiCAD MCP server
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
export function registerSchematicTools(server: McpServer, callKicadScript: Function) {
// Create schematic tool
server.tool(
"create_schematic",
"Create a new schematic",
{
name: z.string().describe("Schematic name"),
path: z.string().optional().describe("Optional path"),
},
async (args: { name: string; path?: string }) => {
const result = await callKicadScript("create_schematic", args);
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
);
// Add component to schematic
server.tool(
"add_schematic_component",
"Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')",
{
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)"),
reference: z.string().describe("Component reference (e.g., R1, U1)"),
value: z.string().optional().describe("Component value"),
position: z.object({
x: z.number(),
y: z.number()
}).optional().describe("Position on schematic"),
},
async (args: { schematicPath: string; symbol: string; reference: string; value?: string; position?: { x: number; y: number } }) => {
// Transform to what Python backend expects
const [library, symbolName] = args.symbol.includes(':')
? args.symbol.split(':')
: ['Device', args.symbol];
const transformed = {
schematicPath: args.schematicPath,
component: {
library,
type: symbolName,
reference: args.reference,
value: args.value,
// Python expects flat x, y not nested position
x: args.position?.x ?? 0,
y: args.position?.y ?? 0
}
};
const result = await callKicadScript("add_schematic_component", transformed);
if (result.success) {
return {
content: [{
type: "text",
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`
}]
};
} else {
return {
content: [{
type: "text",
text: `Failed to add component: ${result.message || JSON.stringify(result)}`
}]
};
}
}
);
// Connect components with wire
server.tool(
"add_wire",
"Add a wire connection in the schematic",
{
start: z.object({
x: z.number(),
y: z.number()
}).describe("Start position"),
end: z.object({
x: z.number(),
y: z.number()
}).describe("End position"),
},
async (args: any) => {
const result = await callKicadScript("add_wire", args);
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
);
// Add pin-to-pin connection
server.tool(
"add_schematic_connection",
"Connect two component pins with a wire",
{
schematicPath: z.string().describe("Path to the schematic file"),
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
sourcePin: z.string().describe("Source pin name/number (e.g., 1, 2, GND)"),
targetRef: z.string().describe("Target component reference (e.g., C1)"),
targetPin: z.string().describe("Target pin name/number (e.g., 1, 2, VCC)")
},
async (args: { schematicPath: string; sourceRef: string; sourcePin: string; targetRef: string; targetPin: string }) => {
const result = await callKicadScript("add_schematic_connection", args);
if (result.success) {
return {
content: [{
type: "text",
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`
}]
};
} else {
return {
content: [{
type: "text",
text: `Failed to add connection: ${result.message || 'Unknown error'}`
}]
};
}
}
);
// Add net label
server.tool(
"add_schematic_net_label",
"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")
},
async (args: { schematicPath: string; netName: string; position: number[] }) => {
const result = await callKicadScript("add_schematic_net_label", args);
if (result.success) {
return {
content: [{
type: "text",
text: `Successfully added net label '${args.netName}' at position [${args.position}]`
}]
};
} else {
return {
content: [{
type: "text",
text: `Failed to add net label: ${result.message || 'Unknown error'}`
}]
};
}
}
);
// Connect pin to net
server.tool(
"connect_to_net",
"Connect a component pin to a named net",
{
schematicPath: z.string().describe("Path to the schematic file"),
componentRef: z.string().describe("Component reference (e.g., U1, R1)"),
pinName: z.string().describe("Pin name/number to connect"),
netName: z.string().describe("Name of the net to connect to")
},
async (args: { schematicPath: string; componentRef: string; pinName: string; netName: string }) => {
const result = await callKicadScript("connect_to_net", args);
if (result.success) {
return {
content: [{
type: "text",
text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`
}]
};
} else {
return {
content: [{
type: "text",
text: `Failed to connect to net: ${result.message || 'Unknown error'}`
}]
};
}
}
);
// Get net connections
server.tool(
"get_net_connections",
"Get all connections for a named net",
{
schematicPath: z.string().describe("Path to the schematic file"),
netName: z.string().describe("Name of the net to query")
},
async (args: { schematicPath: string; netName: string }) => {
const result = await callKicadScript("get_net_connections", args);
if (result.success && result.connections) {
const connectionList = result.connections.map((conn: any) =>
` - ${conn.component}/${conn.pin}`
).join('\n');
return {
content: [{
type: "text",
text: `Net '${args.netName}' connections:\n${connectionList}`
}]
};
} else {
return {
content: [{
type: "text",
text: `Failed to get net connections: ${result.message || 'Unknown error'}`
}]
};
}
}
);
// Generate netlist
server.tool(
"generate_netlist",
"Generate a netlist from the schematic",
{
schematicPath: z.string().describe("Path to the schematic file")
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("generate_netlist", args);
if (result.success && result.netlist) {
const netlist = result.netlist;
const output = [
`=== Netlist for ${args.schematicPath} ===`,
`\nComponents (${netlist.components.length}):`,
...netlist.components.map((comp: any) =>
` ${comp.reference}: ${comp.value} (${comp.footprint || 'No footprint'})`
),
`\nNets (${netlist.nets.length}):`,
...netlist.nets.map((net: any) => {
const connections = net.connections.map((conn: any) =>
`${conn.component}/${conn.pin}`
).join(', ');
return ` ${net.name}: ${connections}`;
})
].join('\n');
return {
content: [{
type: "text",
text: output
}]
};
} else {
return {
content: [{
type: "text",
text: `Failed to generate netlist: ${result.message || 'Unknown error'}`
}]
};
}
}
);
}
/**
* Schematic tools for KiCAD MCP server
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerSchematicTools(
server: McpServer,
callKicadScript: Function,
) {
// Create schematic tool
server.tool(
"create_schematic",
"Create a new schematic",
{
name: z.string().describe("Schematic name"),
path: z.string().optional().describe("Optional path"),
},
async (args: { name: string; path?: string }) => {
const result = await callKicadScript("create_schematic", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Add component to schematic
server.tool(
"add_schematic_component",
"Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')",
{
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)",
),
reference: z.string().describe("Component reference (e.g., R1, U1)"),
value: z.string().optional().describe("Component value"),
position: z
.object({
x: z.number(),
y: z.number(),
})
.optional()
.describe("Position on schematic"),
},
async (args: {
schematicPath: string;
symbol: string;
reference: string;
value?: string;
position?: { x: number; y: number };
}) => {
// Transform to what Python backend expects
const [library, symbolName] = args.symbol.includes(":")
? args.symbol.split(":")
: ["Device", args.symbol];
const transformed = {
schematicPath: args.schematicPath,
component: {
library,
type: symbolName,
reference: args.reference,
value: args.value,
// Python expects flat x, y not nested position
x: args.position?.x ?? 0,
y: args.position?.y ?? 0,
},
};
const result = await callKicadScript(
"add_schematic_component",
transformed,
);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to add component: ${result.message || JSON.stringify(result)}`,
},
],
};
}
},
);
// Delete component from schematic
server.tool(
"delete_schematic_component",
`Remove a placed symbol from a KiCAD schematic (.kicad_sch).
This removes the symbol instance (the placed component) from the schematic.
It does NOT remove the symbol definition from lib_symbols.
Note: This tool operates on schematic files (.kicad_sch).
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)"),
},
async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("delete_schematic_component", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully removed ${args.reference} from schematic`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Failed to remove component: ${result.message || "Unknown error"}`,
},
],
};
},
);
// Connect components with wire
server.tool(
"add_wire",
"Add a wire connection in the schematic",
{
start: z
.object({
x: z.number(),
y: z.number(),
})
.describe("Start position"),
end: z
.object({
x: z.number(),
y: z.number(),
})
.describe("End position"),
},
async (args: any) => {
const result = await callKicadScript("add_wire", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Add pin-to-pin connection
server.tool(
"add_schematic_connection",
"Connect two component pins with a wire",
{
schematicPath: z.string().describe("Path to the schematic file"),
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
sourcePin: z
.string()
.describe("Source pin name/number (e.g., 1, 2, GND)"),
targetRef: z.string().describe("Target component reference (e.g., C1)"),
targetPin: z
.string()
.describe("Target pin name/number (e.g., 1, 2, VCC)"),
},
async (args: {
schematicPath: string;
sourceRef: string;
sourcePin: string;
targetRef: string;
targetPin: string;
}) => {
const result = await callKicadScript("add_schematic_connection", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to add connection: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Add net label
server.tool(
"add_schematic_net_label",
"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"),
},
async (args: {
schematicPath: string;
netName: string;
position: number[];
}) => {
const result = await callKicadScript("add_schematic_net_label", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully added net label '${args.netName}' at position [${args.position}]`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to add net label: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Connect pin to net
server.tool(
"connect_to_net",
"Connect a component pin to a named net",
{
schematicPath: z.string().describe("Path to the schematic file"),
componentRef: z.string().describe("Component reference (e.g., U1, R1)"),
pinName: z.string().describe("Pin name/number to connect"),
netName: z.string().describe("Name of the net to connect to"),
},
async (args: {
schematicPath: string;
componentRef: string;
pinName: string;
netName: string;
}) => {
const result = await callKicadScript("connect_to_net", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to connect to net: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Get net connections
server.tool(
"get_net_connections",
"Get all connections for a named net",
{
schematicPath: z.string().describe("Path to the schematic file"),
netName: z.string().describe("Name of the net to query"),
},
async (args: { schematicPath: string; netName: string }) => {
const result = await callKicadScript("get_net_connections", args);
if (result.success && result.connections) {
const connectionList = result.connections
.map((conn: any) => ` - ${conn.component}/${conn.pin}`)
.join("\n");
return {
content: [
{
type: "text",
text: `Net '${args.netName}' connections:\n${connectionList}`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to get net connections: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Generate netlist
server.tool(
"generate_netlist",
"Generate a netlist from the schematic",
{
schematicPath: z.string().describe("Path to the schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("generate_netlist", args);
if (result.success && result.netlist) {
const netlist = result.netlist;
const output = [
`=== Netlist for ${args.schematicPath} ===`,
`\nComponents (${netlist.components.length}):`,
...netlist.components.map(
(comp: any) =>
` ${comp.reference}: ${comp.value} (${comp.footprint || "No footprint"})`,
),
`\nNets (${netlist.nets.length}):`,
...netlist.nets.map((net: any) => {
const connections = net.connections
.map((conn: any) => `${conn.component}/${conn.pin}`)
.join(", ");
return ` ${net.name}: ${connections}`;
}),
].join("\n");
return {
content: [
{
type: "text",
text: output,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Failed to generate netlist: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
}