@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.
- package/CHANGELOG.md +15 -0
- package/README.md +241 -0
- package/debug-text.ts +24 -0
- package/dist/assets/search.html +528 -0
- package/dist/index.js +32364 -0
- package/dist/src/index.js +28521 -0
- package/package.json +49 -0
- package/scripts/build-search-page.ts +68 -0
- package/src/assets/search-built.html +528 -0
- package/src/assets/search.html +458 -0
- package/src/browser/index.ts +381 -0
- package/src/browser/kicad-renderer.ts +646 -0
- package/src/browser/sexpr-parser.ts +321 -0
- package/src/http/routes.ts +253 -0
- package/src/http/server.ts +74 -0
- package/src/index.ts +117 -0
- package/src/tools/details.ts +66 -0
- package/src/tools/easyeda.ts +582 -0
- package/src/tools/index.ts +98 -0
- package/src/tools/library-fix.ts +414 -0
- package/src/tools/library-update.ts +412 -0
- package/src/tools/library.ts +263 -0
- package/src/tools/search.ts +58 -0
- package/tsconfig.json +9 -0
|
@@ -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
|
+
}
|