@jlcpcb/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Library Fix Tool
3
+ * Regenerates a symbol with corrections applied (pin renames, swaps, type changes, additions)
4
+ * Follows CDFER approach: regenerate, don't patch
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
9
+ import { existsSync } from 'fs';
10
+ import { readFile } from 'fs/promises';
11
+ import { homedir } from 'os';
12
+ import {
13
+ easyedaClient,
14
+ symbolConverter,
15
+ footprintConverter,
16
+ getLibraryCategory,
17
+ getLibraryFilename,
18
+ getFootprintDirName,
19
+ getFootprintReference as getCategoryFootprintRef,
20
+ ensureDir,
21
+ writeText,
22
+ type EasyEDAPin,
23
+ } from '@jlcpcb/core';
24
+ import { join } from 'path';
25
+
26
+ // KiCad versions to check (newest first)
27
+ const KICAD_VERSIONS = ['9.0', '8.0'];
28
+
29
+ /**
30
+ * Detect KiCad major version from existing user directories
31
+ */
32
+ function detectKicadVersion(): string {
33
+ const home = homedir();
34
+ const baseDir = join(home, 'Documents', 'KiCad');
35
+
36
+ for (const version of KICAD_VERSIONS) {
37
+ if (existsSync(join(baseDir, version))) {
38
+ return version;
39
+ }
40
+ }
41
+ return '9.0'; // Default
42
+ }
43
+
44
+ /**
45
+ * Get library paths for fix operation
46
+ */
47
+ function getLibraryPaths(projectPath?: string): {
48
+ symbolsDir: string;
49
+ footprintDir: string;
50
+ } {
51
+ if (projectPath) {
52
+ const librariesDir = join(projectPath, 'libraries');
53
+ return {
54
+ symbolsDir: join(librariesDir, 'symbols'),
55
+ footprintDir: join(librariesDir, 'footprints', getFootprintDirName()),
56
+ };
57
+ }
58
+
59
+ const home = homedir();
60
+ const version = detectKicadVersion();
61
+ const base = join(home, 'Documents', 'KiCad', version);
62
+
63
+ return {
64
+ symbolsDir: join(base, 'symbols'),
65
+ footprintDir: join(base, 'footprints', getFootprintDirName()),
66
+ };
67
+ }
68
+
69
+ // Pin electrical types enum for validation
70
+ const PinElectricalType = z.enum([
71
+ 'input',
72
+ 'output',
73
+ 'bidirectional',
74
+ 'power_in',
75
+ 'power_out',
76
+ 'passive',
77
+ 'open_collector',
78
+ 'open_emitter',
79
+ 'unconnected',
80
+ 'unspecified',
81
+ ]);
82
+
83
+ // Correction types for pins
84
+ const PinCorrectionSchema = z.discriminatedUnion('action', [
85
+ // Modify existing pin
86
+ z.object({
87
+ action: z.literal('modify'),
88
+ number: z.string(),
89
+ rename: z.string().optional(),
90
+ set_type: PinElectricalType.optional(),
91
+ }),
92
+ // Swap two pins
93
+ z.object({
94
+ action: z.literal('swap'),
95
+ pins: z.tuple([z.string(), z.string()]),
96
+ }),
97
+ // Add new pin
98
+ z.object({
99
+ action: z.literal('add'),
100
+ number: z.string(),
101
+ name: z.string(),
102
+ type: PinElectricalType,
103
+ }),
104
+ // Remove pin
105
+ z.object({
106
+ action: z.literal('remove'),
107
+ number: z.string(),
108
+ }),
109
+ ]);
110
+
111
+ export const LibraryFixParamsSchema = z.object({
112
+ lcsc_id: z.string().regex(/^C\d+$/, 'Invalid LCSC part number'),
113
+ corrections: z.object({
114
+ pins: z.array(PinCorrectionSchema).optional(),
115
+ }),
116
+ force: z.boolean().default(false),
117
+ project_path: z.string().min(1).optional(),
118
+ });
119
+
120
+ export const fixLibraryTool: Tool = {
121
+ name: 'library_fix',
122
+ description: `Regenerate a symbol with corrections applied.
123
+
124
+ Corrections are applied to fresh data fetched from EasyEDA, then the symbol is regenerated.
125
+ Use this tool when Claude detects issues with a symbol (pin names, types, missing pins, etc.).
126
+
127
+ IMPORTANT: The symbol must already exist in the library. Use library_fetch first to add new components.
128
+
129
+ Correction types:
130
+ - modify: Rename pin or change electrical type
131
+ - swap: Swap positions of two pins
132
+ - add: Add a new pin (useful for exposed pads not in symbol)
133
+ - remove: Remove a pin from symbol
134
+
135
+ Example: Add an exposed pad to ground
136
+ {
137
+ "lcsc_id": "C2913199",
138
+ "corrections": {
139
+ "pins": [
140
+ { "action": "add", "number": "EP", "name": "GND", "type": "passive" }
141
+ ]
142
+ }
143
+ }
144
+
145
+ Example: Fix pin types
146
+ {
147
+ "lcsc_id": "C123456",
148
+ "corrections": {
149
+ "pins": [
150
+ { "action": "modify", "number": "1", "set_type": "power_in" },
151
+ { "action": "modify", "number": "2", "rename": "VDD", "set_type": "power_in" }
152
+ ]
153
+ }
154
+ }`,
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ lcsc_id: {
159
+ type: 'string',
160
+ description: 'LCSC part number (e.g., C2040)',
161
+ },
162
+ corrections: {
163
+ type: 'object',
164
+ description: 'Corrections to apply to the symbol',
165
+ properties: {
166
+ pins: {
167
+ type: 'array',
168
+ description: 'Pin corrections to apply',
169
+ items: {
170
+ oneOf: [
171
+ {
172
+ type: 'object',
173
+ properties: {
174
+ action: { type: 'string', const: 'modify' },
175
+ number: { type: 'string' },
176
+ rename: { type: 'string' },
177
+ set_type: { type: 'string', enum: ['input', 'output', 'bidirectional', 'power_in', 'power_out', 'passive', 'open_collector', 'open_emitter', 'unconnected', 'unspecified'] },
178
+ },
179
+ required: ['action', 'number'],
180
+ },
181
+ {
182
+ type: 'object',
183
+ properties: {
184
+ action: { type: 'string', const: 'swap' },
185
+ pins: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 2 },
186
+ },
187
+ required: ['action', 'pins'],
188
+ },
189
+ {
190
+ type: 'object',
191
+ properties: {
192
+ action: { type: 'string', const: 'add' },
193
+ number: { type: 'string' },
194
+ name: { type: 'string' },
195
+ type: { type: 'string', enum: ['input', 'output', 'bidirectional', 'power_in', 'power_out', 'passive', 'open_collector', 'open_emitter', 'unconnected', 'unspecified'] },
196
+ },
197
+ required: ['action', 'number', 'name', 'type'],
198
+ },
199
+ {
200
+ type: 'object',
201
+ properties: {
202
+ action: { type: 'string', const: 'remove' },
203
+ number: { type: 'string' },
204
+ },
205
+ required: ['action', 'number'],
206
+ },
207
+ ],
208
+ },
209
+ },
210
+ },
211
+ },
212
+ force: {
213
+ type: 'boolean',
214
+ description: 'Force regeneration even if symbol does not exist (default: false)',
215
+ },
216
+ project_path: {
217
+ type: 'string',
218
+ description: 'Optional: Project path. If omitted, uses global KiCad library.',
219
+ },
220
+ },
221
+ required: ['lcsc_id', 'corrections'],
222
+ },
223
+ };
224
+
225
+ export async function handleFixLibrary(args: unknown) {
226
+ const params = LibraryFixParamsSchema.parse(args);
227
+ const paths = getLibraryPaths(params.project_path);
228
+
229
+ // 1. Re-fetch component from EasyEDA
230
+ const component = await easyedaClient.getComponentData(params.lcsc_id);
231
+
232
+ if (!component) {
233
+ return {
234
+ content: [{
235
+ type: 'text' as const,
236
+ text: JSON.stringify({
237
+ success: false,
238
+ error: `Component ${params.lcsc_id} not found`,
239
+ lcsc_id: params.lcsc_id,
240
+ }),
241
+ }],
242
+ isError: true,
243
+ };
244
+ }
245
+
246
+ // Determine category and symbol file path
247
+ const category = getLibraryCategory(
248
+ component.info.prefix,
249
+ component.info.category,
250
+ component.info.description
251
+ );
252
+ const symbolLibraryFilename = getLibraryFilename(category);
253
+ const symbolFile = join(paths.symbolsDir, symbolLibraryFilename);
254
+
255
+ // Check if symbol library exists
256
+ if (!existsSync(symbolFile) && !params.force) {
257
+ return {
258
+ content: [{
259
+ type: 'text' as const,
260
+ text: JSON.stringify({
261
+ success: false,
262
+ error: `Symbol library ${symbolLibraryFilename} does not exist. Use library_fetch first, or set force=true.`,
263
+ lcsc_id: params.lcsc_id,
264
+ expected_library: symbolFile,
265
+ }),
266
+ }],
267
+ isError: true,
268
+ };
269
+ }
270
+
271
+ // Check if symbol exists in library (unless force)
272
+ if (existsSync(symbolFile) && !params.force) {
273
+ const existingContent = await readFile(symbolFile, 'utf-8');
274
+ if (!symbolConverter.symbolExistsInLibrary(existingContent, component.info.name)) {
275
+ return {
276
+ content: [{
277
+ type: 'text' as const,
278
+ text: JSON.stringify({
279
+ success: false,
280
+ error: `Symbol "${component.info.name}" not found in ${symbolLibraryFilename}. Use library_fetch first, or set force=true.`,
281
+ lcsc_id: params.lcsc_id,
282
+ symbol_name: component.info.name,
283
+ }),
284
+ }],
285
+ isError: true,
286
+ };
287
+ }
288
+ }
289
+
290
+ // 2. Apply corrections to component.symbol.pins
291
+ const appliedCorrections: string[] = [];
292
+
293
+ if (params.corrections.pins) {
294
+ for (const correction of params.corrections.pins) {
295
+ switch (correction.action) {
296
+ case 'modify': {
297
+ const pin = component.symbol.pins.find(p => p.number === correction.number);
298
+ if (pin) {
299
+ if (correction.rename) {
300
+ appliedCorrections.push(`Renamed pin ${correction.number}: "${pin.name}" → "${correction.rename}"`);
301
+ pin.name = correction.rename;
302
+ }
303
+ if (correction.set_type) {
304
+ appliedCorrections.push(`Changed pin ${correction.number} type: "${pin.electricalType}" → "${correction.set_type}"`);
305
+ pin.electricalType = correction.set_type;
306
+ }
307
+ } else {
308
+ appliedCorrections.push(`Warning: Pin ${correction.number} not found for modify`);
309
+ }
310
+ break;
311
+ }
312
+ case 'swap': {
313
+ const [a, b] = correction.pins;
314
+ const pinA = component.symbol.pins.find(p => p.number === a);
315
+ const pinB = component.symbol.pins.find(p => p.number === b);
316
+ if (pinA && pinB) {
317
+ // Swap positions
318
+ [pinA.x, pinA.y, pinB.x, pinB.y] = [pinB.x, pinB.y, pinA.x, pinA.y];
319
+ appliedCorrections.push(`Swapped pin positions: ${a} ↔ ${b}`);
320
+ } else {
321
+ appliedCorrections.push(`Warning: Could not swap pins ${a} and ${b} - one or both not found`);
322
+ }
323
+ break;
324
+ }
325
+ case 'add': {
326
+ // Find a position for the new pin (at the bottom of the symbol)
327
+ const maxY = Math.max(...component.symbol.pins.map(p => p.y), 0);
328
+ const newPin: EasyEDAPin = {
329
+ number: correction.number,
330
+ name: correction.name,
331
+ electricalType: correction.type,
332
+ x: 0,
333
+ y: maxY + 254, // 1 grid unit below lowest pin (254 = 100mil in EasyEDA units)
334
+ rotation: 0,
335
+ hasDot: false,
336
+ hasClock: false,
337
+ pinLength: 100, // default EasyEDA units
338
+ };
339
+ component.symbol.pins.push(newPin);
340
+ appliedCorrections.push(`Added pin ${correction.number}: "${correction.name}" (${correction.type})`);
341
+ break;
342
+ }
343
+ case 'remove': {
344
+ const pinIndex = component.symbol.pins.findIndex(p => p.number === correction.number);
345
+ if (pinIndex >= 0) {
346
+ const removed = component.symbol.pins.splice(pinIndex, 1)[0];
347
+ appliedCorrections.push(`Removed pin ${correction.number}: "${removed.name}"`);
348
+ } else {
349
+ appliedCorrections.push(`Warning: Pin ${correction.number} not found for remove`);
350
+ }
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ // Update footprint reference
358
+ const footprintResult = footprintConverter.getFootprint(component);
359
+ let footprintRef: string;
360
+
361
+ if (footprintResult.type === 'reference') {
362
+ footprintRef = footprintResult.reference!;
363
+ } else {
364
+ const footprintName = footprintResult.name + '_' + params.lcsc_id;
365
+ footprintRef = getCategoryFootprintRef(footprintName);
366
+
367
+ // Write custom footprint if needed
368
+ await ensureDir(paths.footprintDir);
369
+ const footprintPath = join(paths.footprintDir, `${footprintName}.kicad_mod`);
370
+ await writeText(footprintPath, footprintResult.content!);
371
+ }
372
+
373
+ component.info.package = footprintRef;
374
+
375
+ // 3. Regenerate symbol with corrections
376
+ await ensureDir(paths.symbolsDir);
377
+
378
+ let symbolContent: string;
379
+ let symbolAction: 'replaced' | 'created';
380
+
381
+ if (existsSync(symbolFile)) {
382
+ // Read existing library and replace the symbol
383
+ const existingContent = await readFile(symbolFile, 'utf-8');
384
+ symbolContent = symbolConverter.replaceInLibrary(existingContent, component);
385
+ symbolAction = 'replaced';
386
+ } else {
387
+ // Create new library with this symbol (only if force=true)
388
+ symbolContent = symbolConverter.convert(component);
389
+ symbolAction = 'created';
390
+ }
391
+
392
+ // 4. Write updated library
393
+ await writeText(symbolFile, symbolContent);
394
+
395
+ const symbolName = symbolConverter.getSymbolName(component);
396
+
397
+ return {
398
+ content: [{
399
+ type: 'text' as const,
400
+ text: JSON.stringify({
401
+ success: true,
402
+ lcsc_id: params.lcsc_id,
403
+ symbol_name: symbolName,
404
+ category,
405
+ symbol_action: symbolAction,
406
+ corrections_applied: appliedCorrections.length,
407
+ corrections: appliedCorrections,
408
+ files: {
409
+ symbol_library: symbolFile,
410
+ },
411
+ }, null, 2),
412
+ }],
413
+ };
414
+ }