@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,412 @@
1
+ /**
2
+ * Library Update Tool
3
+ * Parses existing JLC-MCP-* libraries and regenerates all components
4
+ * with latest data, normalization, and hybrid footprint logic
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
9
+ import { existsSync, readdirSync } from 'fs';
10
+ import { readFile } from 'fs/promises';
11
+ import { homedir, platform } from 'os';
12
+ import {
13
+ easyedaClient,
14
+ symbolConverter,
15
+ footprintConverter,
16
+ getLibraryCategory,
17
+ getLibraryFilename,
18
+ getFootprintDirName,
19
+ get3DModelsDirName,
20
+ getAllCategories,
21
+ type LibraryCategory,
22
+ ensureDir,
23
+ writeText,
24
+ } from '@jlcpcb/core';
25
+ import { join } from 'path';
26
+
27
+ // KiCad versions to check (newest first)
28
+ const KICAD_VERSIONS = ['9.0', '8.0'];
29
+
30
+ // 3rd party library namespace
31
+ const LIBRARY_NAMESPACE = 'jlc_mcp';
32
+
33
+ /**
34
+ * Detect KiCad major version from existing user directories
35
+ */
36
+ function detectKicadVersion(): string {
37
+ const home = homedir();
38
+ const baseDir = join(home, 'Documents', 'KiCad');
39
+
40
+ for (const version of KICAD_VERSIONS) {
41
+ if (existsSync(join(baseDir, version))) {
42
+ return version;
43
+ }
44
+ }
45
+ return '9.0'; // Default
46
+ }
47
+
48
+ /**
49
+ * Get library paths for update operation
50
+ * Platform-specific paths matching where ${KICAD9_3RD_PARTY} resolves:
51
+ * - macOS/Windows: ~/Documents/KiCad/{version}/3rdparty/jlc_mcp/
52
+ * - Linux: ~/.local/share/kicad/{version}/3rdparty/jlc_mcp/
53
+ */
54
+ function getLibraryPaths(projectPath?: string): {
55
+ symbolsDir: string;
56
+ footprintDir: string;
57
+ models3dDir: string;
58
+ } {
59
+ if (projectPath) {
60
+ const librariesDir = join(projectPath, 'libraries');
61
+ return {
62
+ symbolsDir: join(librariesDir, 'symbols'),
63
+ footprintDir: join(librariesDir, 'footprints', getFootprintDirName()),
64
+ models3dDir: join(librariesDir, '3dmodels', get3DModelsDirName()),
65
+ };
66
+ }
67
+
68
+ const home = homedir();
69
+ const version = detectKicadVersion();
70
+ const plat = platform();
71
+
72
+ let base: string;
73
+ if (plat === 'linux') {
74
+ base = join(home, '.local', 'share', 'kicad', version, '3rdparty', LIBRARY_NAMESPACE);
75
+ } else {
76
+ base = join(home, 'Documents', 'KiCad', version, '3rdparty', LIBRARY_NAMESPACE);
77
+ }
78
+
79
+ return {
80
+ symbolsDir: join(base, 'symbols'),
81
+ footprintDir: join(base, 'footprints', getFootprintDirName()),
82
+ models3dDir: join(base, '3dmodels', get3DModelsDirName()),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Extract LCSC IDs from a symbol library file
88
+ */
89
+ function extractLcscIdsFromLibrary(content: string): string[] {
90
+ const pattern = /\(property\s+"LCSC"\s+"(C\d+)"/g;
91
+ const ids: string[] = [];
92
+ let match;
93
+ while ((match = pattern.exec(content)) !== null) {
94
+ ids.push(match[1]);
95
+ }
96
+ return ids;
97
+ }
98
+
99
+ /**
100
+ * Generate standard KiCad symbol library header
101
+ */
102
+ function generateEmptyLibrary(): string {
103
+ return `(kicad_symbol_lib
104
+ \t(version 20241209)
105
+ \t(generator "jlc-mcp")
106
+ \t(generator_version "9.0")
107
+ )\n`;
108
+ }
109
+
110
+ /**
111
+ * Find all JLC-MCP-*.kicad_sym files in a directory
112
+ */
113
+ function findJlcLibraries(symbolsDir: string): string[] {
114
+ if (!existsSync(symbolsDir)) {
115
+ return [];
116
+ }
117
+
118
+ try {
119
+ const files = readdirSync(symbolsDir);
120
+ return files
121
+ .filter((f) => f.startsWith('JLC-MCP-') && f.endsWith('.kicad_sym'))
122
+ .map((f) => join(symbolsDir, f));
123
+ } catch {
124
+ return [];
125
+ }
126
+ }
127
+
128
+ export const updateLibraryTool: Tool = {
129
+ name: 'library_update',
130
+ description: `Parse existing JLC-MCP-* libraries and regenerate all components with latest data.
131
+
132
+ If no JLC-MCP-* libraries exist, initializes empty library files for all categories:
133
+ - JLC-MCP-Resistors, JLC-MCP-Capacitors, JLC-MCP-Inductors, JLC-MCP-Diodes
134
+ - JLC-MCP-Transistors, JLC-MCP-ICs, JLC-MCP-Connectors, JLC-MCP-Misc
135
+
136
+ Also creates footprint directory (JLC-MCP.pretty) and 3D models directory (JLC-MCP.3dshapes).
137
+
138
+ When libraries exist, this tool:
139
+ 1. Finds all JLC-MCP-*.kicad_sym files in the symbols directory
140
+ 2. Extracts LCSC IDs from each symbol's properties
141
+ 3. Fetches fresh data from EasyEDA for each component
142
+ 4. Regenerates symbols with value normalization
143
+ 5. Applies hybrid footprint logic (KiCad standard vs generated)
144
+ 6. Rebuilds category-based libraries
145
+
146
+ Use dry_run=true to preview changes without writing files.`,
147
+ inputSchema: {
148
+ type: 'object',
149
+ properties: {
150
+ project_path: {
151
+ type: 'string',
152
+ description: 'Optional: Project path. If omitted, uses global KiCad library.',
153
+ },
154
+ dry_run: {
155
+ type: 'boolean',
156
+ description: 'Preview changes without writing files (default: false)',
157
+ },
158
+ },
159
+ },
160
+ };
161
+
162
+ export const UpdateLibraryParamsSchema = z.object({
163
+ project_path: z.string().min(1).optional(),
164
+ dry_run: z.boolean().default(false),
165
+ });
166
+
167
+ interface UpdateResult {
168
+ lcscId: string;
169
+ category: LibraryCategory;
170
+ symbolName: string;
171
+ footprintType: 'reference' | 'generated';
172
+ footprintRef: string;
173
+ status: 'updated' | 'failed';
174
+ error?: string;
175
+ }
176
+
177
+ export async function handleUpdateLibrary(args: unknown) {
178
+ const params = UpdateLibraryParamsSchema.parse(args);
179
+ const paths = getLibraryPaths(params.project_path);
180
+
181
+ // Find all existing JLC libraries
182
+ const libraryFiles = findJlcLibraries(paths.symbolsDir);
183
+
184
+ if (libraryFiles.length === 0) {
185
+ // No existing libraries - initialize empty libraries for all categories
186
+ const allCategories = getAllCategories();
187
+
188
+ if (params.dry_run) {
189
+ return {
190
+ content: [{
191
+ type: 'text' as const,
192
+ text: JSON.stringify({
193
+ success: true,
194
+ action: 'would_initialize',
195
+ dry_run: true,
196
+ summary: {
197
+ categories_to_create: allCategories.length,
198
+ directories_to_create: [
199
+ paths.symbolsDir,
200
+ paths.footprintDir,
201
+ paths.models3dDir,
202
+ ],
203
+ },
204
+ libraries_to_create: allCategories.map((cat) => ({
205
+ category: cat,
206
+ filename: getLibraryFilename(cat),
207
+ path: join(paths.symbolsDir, getLibraryFilename(cat)),
208
+ })),
209
+ }, null, 2),
210
+ }],
211
+ };
212
+ }
213
+
214
+ // Create directories
215
+ await ensureDir(paths.symbolsDir);
216
+ await ensureDir(paths.footprintDir);
217
+ await ensureDir(paths.models3dDir);
218
+
219
+ // Create empty library files for all categories
220
+ const emptyContent = generateEmptyLibrary();
221
+ const createdLibraries: string[] = [];
222
+
223
+ for (const category of allCategories) {
224
+ const filename = getLibraryFilename(category);
225
+ const filepath = join(paths.symbolsDir, filename);
226
+ await writeText(filepath, emptyContent);
227
+ createdLibraries.push(filepath);
228
+ }
229
+
230
+ return {
231
+ content: [{
232
+ type: 'text' as const,
233
+ text: JSON.stringify({
234
+ success: true,
235
+ action: 'initialized',
236
+ dry_run: false,
237
+ summary: {
238
+ categories_initialized: allCategories.length,
239
+ directories_created: [
240
+ paths.symbolsDir,
241
+ paths.footprintDir,
242
+ paths.models3dDir,
243
+ ],
244
+ },
245
+ libraries_created: createdLibraries,
246
+ next_steps: [
247
+ 'Use library_fetch to add components to your libraries',
248
+ 'Run library_update again after adding components to regenerate with latest data',
249
+ ],
250
+ }, null, 2),
251
+ }],
252
+ };
253
+ }
254
+
255
+ // Extract all LCSC IDs from existing libraries
256
+ const allLcscIds: Set<string> = new Set();
257
+ const libraryInfo: { file: string; ids: string[] }[] = [];
258
+
259
+ for (const file of libraryFiles) {
260
+ const content = await readFile(file, 'utf-8');
261
+ const ids = extractLcscIdsFromLibrary(content);
262
+ libraryInfo.push({ file, ids });
263
+ ids.forEach((id) => allLcscIds.add(id));
264
+ }
265
+
266
+ if (allLcscIds.size === 0) {
267
+ return {
268
+ content: [{
269
+ type: 'text' as const,
270
+ text: JSON.stringify({
271
+ success: false,
272
+ error: 'No LCSC IDs found in existing libraries',
273
+ libraries_scanned: libraryFiles.length,
274
+ }),
275
+ }],
276
+ isError: true,
277
+ };
278
+ }
279
+
280
+ // Process each component
281
+ const results: UpdateResult[] = [];
282
+ const categorySymbols: Map<LibraryCategory, string[]> = new Map();
283
+
284
+ for (const lcscId of allLcscIds) {
285
+ try {
286
+ // Fetch fresh component data
287
+ const component = await easyedaClient.getComponentData(lcscId);
288
+
289
+ if (!component) {
290
+ results.push({
291
+ lcscId,
292
+ category: 'Misc',
293
+ symbolName: '',
294
+ footprintType: 'generated',
295
+ footprintRef: '',
296
+ status: 'failed',
297
+ error: 'Component not found',
298
+ });
299
+ continue;
300
+ }
301
+
302
+ // Determine category
303
+ const category = getLibraryCategory(
304
+ component.info.prefix,
305
+ component.info.category,
306
+ component.info.description
307
+ );
308
+
309
+ // Generate symbol entry (without library wrapper)
310
+ const symbolEntry = symbolConverter.convertToSymbolEntry(component);
311
+ const symbolName = symbolConverter.getSymbolName(component);
312
+
313
+ // Collect symbols by category
314
+ if (!categorySymbols.has(category)) {
315
+ categorySymbols.set(category, []);
316
+ }
317
+ categorySymbols.get(category)!.push(symbolEntry);
318
+
319
+ // Handle footprint
320
+ const footprintResult = footprintConverter.getFootprint(component);
321
+
322
+ // Generate custom footprint if needed (not in dry run)
323
+ if (!params.dry_run && footprintResult.type === 'generated') {
324
+ await ensureDir(paths.footprintDir);
325
+ const footprintName = footprintResult.name + '_' + lcscId;
326
+ const footprintPath = join(paths.footprintDir, `${footprintName}.kicad_mod`);
327
+ await writeText(footprintPath, footprintResult.content!);
328
+ }
329
+
330
+ results.push({
331
+ lcscId,
332
+ category,
333
+ symbolName,
334
+ footprintType: footprintResult.type,
335
+ footprintRef: footprintResult.type === 'reference'
336
+ ? footprintResult.reference!
337
+ : `JLC:${footprintResult.name}_${lcscId}`,
338
+ status: 'updated',
339
+ });
340
+ } catch (error) {
341
+ results.push({
342
+ lcscId,
343
+ category: 'Misc',
344
+ symbolName: '',
345
+ footprintType: 'generated',
346
+ footprintRef: '',
347
+ status: 'failed',
348
+ error: error instanceof Error ? error.message : String(error),
349
+ });
350
+ }
351
+ }
352
+
353
+ // Write category-based libraries (not in dry run)
354
+ if (!params.dry_run) {
355
+ await ensureDir(paths.symbolsDir);
356
+
357
+ for (const [category, entries] of categorySymbols) {
358
+ const filename = getLibraryFilename(category);
359
+ const filepath = join(paths.symbolsDir, filename);
360
+
361
+ // Create library file with all symbols
362
+ const header = `(kicad_symbol_lib
363
+ \t(version 20241209)
364
+ \t(generator "jlc-mcp")
365
+ \t(generator_version "9.0")
366
+ `;
367
+ const content = header + entries.join('') + ')\n';
368
+ await writeText(filepath, content);
369
+ }
370
+ }
371
+
372
+ // Summarize results
373
+ const successful = results.filter((r) => r.status === 'updated');
374
+ const failed = results.filter((r) => r.status === 'failed');
375
+ const byCategory = new Map<LibraryCategory, number>();
376
+
377
+ for (const r of successful) {
378
+ byCategory.set(r.category, (byCategory.get(r.category) || 0) + 1);
379
+ }
380
+
381
+ const footprintStats = {
382
+ kicad_standard: successful.filter((r) => r.footprintType === 'reference').length,
383
+ custom_generated: successful.filter((r) => r.footprintType === 'generated').length,
384
+ };
385
+
386
+ return {
387
+ content: [{
388
+ type: 'text' as const,
389
+ text: JSON.stringify({
390
+ success: true,
391
+ dry_run: params.dry_run,
392
+ summary: {
393
+ total_components: allLcscIds.size,
394
+ updated: successful.length,
395
+ failed: failed.length,
396
+ libraries_generated: categorySymbols.size,
397
+ },
398
+ by_category: Object.fromEntries(byCategory),
399
+ footprint_stats: footprintStats,
400
+ libraries_written: params.dry_run
401
+ ? []
402
+ : Array.from(categorySymbols.keys()).map((cat) =>
403
+ join(paths.symbolsDir, getLibraryFilename(cat))
404
+ ),
405
+ failed_components: failed.map((f) => ({
406
+ lcsc_id: f.lcscId,
407
+ error: f.error,
408
+ })),
409
+ }, null, 2),
410
+ }],
411
+ };
412
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Library fetching and conversion tools for MCP
3
+ * Uses jlc-core for all business logic
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
8
+ import {
9
+ createLibraryService,
10
+ createComponentService,
11
+ easyedaClient,
12
+ symbolConverter,
13
+ footprintConverter,
14
+ } from '@jlcpcb/core';
15
+
16
+ const libraryService = createLibraryService();
17
+ const componentService = createComponentService();
18
+
19
+ export const getSymbolKicadTool: Tool = {
20
+ name: 'library_get_symbol',
21
+ description: 'Get a KiCad-compatible symbol definition by LCSC part number. Returns the symbol in .kicad_sym format. LCSC is JLC PCB\'s preferred supplier for assembly.',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ lcsc_id: {
26
+ type: 'string',
27
+ description: 'LCSC part number (e.g., C2040)',
28
+ },
29
+ },
30
+ required: ['lcsc_id'],
31
+ },
32
+ };
33
+
34
+ export const getFootprintKicadTool: Tool = {
35
+ name: 'library_get_footprint',
36
+ description: 'Get a KiCad-compatible footprint definition by LCSC part number. Returns the footprint in .kicad_mod format. LCSC is JLC PCB\'s preferred supplier for assembly.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ lcsc_id: {
41
+ type: 'string',
42
+ description: 'LCSC part number (e.g., C2040)',
43
+ },
44
+ },
45
+ required: ['lcsc_id'],
46
+ },
47
+ };
48
+
49
+ export const fetchLibraryTool: Tool = {
50
+ name: 'library_fetch',
51
+ description: `Fetch a component and add it to KiCad libraries.
52
+
53
+ Accepts:
54
+ - LCSC part numbers (e.g., C2040) → global JLC-MCP libraries
55
+ - EasyEDA UUIDs (e.g., 8007c710c0b9406db963b55df6990340) → project-local EasyEDA library (requires project_path)
56
+
57
+ LCSC components are routed to category-based global libraries:
58
+ - JLC-MCP-Resistors.kicad_sym, JLC-MCP-Capacitors.kicad_sym, JLC-MCP-ICs.kicad_sym, etc.
59
+ - Stored at ~/Documents/KiCad/{version}/3rdparty/jlc_mcp/
60
+
61
+ EasyEDA community components are stored project-locally:
62
+ - <project>/libraries/symbols/EasyEDA.kicad_sym
63
+ - <project>/libraries/footprints/EasyEDA.pretty/
64
+
65
+ Returns symbol_ref and footprint_ref for immediate use with add_schematic_component.`,
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ id: {
70
+ type: 'string',
71
+ description: 'LCSC part number (e.g., C2040) or EasyEDA community UUID',
72
+ },
73
+ project_path: {
74
+ type: 'string',
75
+ description: 'Project path (required for EasyEDA UUIDs, optional for LCSC IDs)',
76
+ },
77
+ include_3d: {
78
+ type: 'boolean',
79
+ description: 'Include 3D model if available (default: false for LCSC, true for EasyEDA)',
80
+ },
81
+ },
82
+ required: ['id'],
83
+ },
84
+ };
85
+
86
+ export const get3DModelTool: Tool = {
87
+ name: 'library_get_3d_model',
88
+ description: 'Download a 3D model for a component. Requires the model UUID from component_get. Returns the model as base64-encoded STEP data.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ uuid: {
93
+ type: 'string',
94
+ description: '3D model UUID from component_get result',
95
+ },
96
+ format: {
97
+ type: 'string',
98
+ enum: ['step', 'obj'],
99
+ description: 'Model format: "step" or "obj" (default: step)',
100
+ },
101
+ },
102
+ required: ['uuid'],
103
+ },
104
+ };
105
+
106
+ export const LibraryParamsSchema = z.object({
107
+ lcsc_id: z.string().regex(/^C\d+$/, 'Invalid LCSC part number'),
108
+ });
109
+
110
+ export const FetchLibraryParamsSchema = z.object({
111
+ id: z.string().min(1),
112
+ project_path: z.string().min(1).optional(),
113
+ include_3d: z.boolean().optional(),
114
+ });
115
+
116
+ export const Model3DParamsSchema = z.object({
117
+ uuid: z.string().min(1),
118
+ format: z.enum(['step', 'obj']).default('step'),
119
+ });
120
+
121
+ export async function handleGetSymbolKicad(args: unknown) {
122
+ const params = LibraryParamsSchema.parse(args);
123
+
124
+ const component = await easyedaClient.getComponentData(params.lcsc_id);
125
+
126
+ if (!component) {
127
+ return {
128
+ content: [{
129
+ type: 'text' as const,
130
+ text: `Component ${params.lcsc_id} not found`,
131
+ }],
132
+ isError: true,
133
+ };
134
+ }
135
+
136
+ const symbol = symbolConverter.convert(component);
137
+ return {
138
+ content: [{
139
+ type: 'text' as const,
140
+ text: symbol,
141
+ }],
142
+ };
143
+ }
144
+
145
+ export async function handleGetFootprintKicad(args: unknown) {
146
+ const params = LibraryParamsSchema.parse(args);
147
+
148
+ const component = await easyedaClient.getComponentData(params.lcsc_id);
149
+
150
+ if (!component) {
151
+ return {
152
+ content: [{
153
+ type: 'text' as const,
154
+ text: `Component ${params.lcsc_id} not found`,
155
+ }],
156
+ isError: true,
157
+ };
158
+ }
159
+
160
+ const footprint = footprintConverter.convert(component);
161
+ return {
162
+ content: [{
163
+ type: 'text' as const,
164
+ text: footprint,
165
+ }],
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Check if ID is an LCSC part number (C followed by digits)
171
+ */
172
+ function isLcscId(id: string): boolean {
173
+ return /^C\d+$/.test(id);
174
+ }
175
+
176
+ export async function handleFetchLibrary(args: unknown) {
177
+ const params = FetchLibraryParamsSchema.parse(args);
178
+ const isCommunityComponent = !isLcscId(params.id);
179
+
180
+ // Community components require project_path
181
+ if (isCommunityComponent && !params.project_path) {
182
+ return {
183
+ content: [{
184
+ type: 'text' as const,
185
+ text: JSON.stringify({
186
+ success: false,
187
+ error: 'EasyEDA community components require project_path for local storage',
188
+ id: params.id,
189
+ hint: 'Provide project_path to store in <project>/libraries/EasyEDA.*',
190
+ }),
191
+ }],
192
+ isError: true,
193
+ };
194
+ }
195
+
196
+ try {
197
+ const result = await libraryService.install(params.id, {
198
+ projectPath: params.project_path,
199
+ include3d: params.include_3d,
200
+ });
201
+
202
+ return {
203
+ content: [{
204
+ type: 'text' as const,
205
+ text: JSON.stringify({
206
+ success: true,
207
+ id: params.id,
208
+ source: result.source,
209
+ storage_mode: result.storageMode,
210
+ category: result.category,
211
+ symbol_name: result.symbolName,
212
+ symbol_ref: result.symbolRef,
213
+ footprint_ref: result.footprintRef,
214
+ footprint_type: result.footprintType,
215
+ datasheet: result.datasheet,
216
+ files: {
217
+ symbol_library: result.files.symbolLibrary,
218
+ footprint: result.files.footprint,
219
+ model_3d: result.files.model3d,
220
+ },
221
+ symbol_action: result.symbolAction,
222
+ validation_data: result.validationData,
223
+ }, null, 2),
224
+ }],
225
+ };
226
+ } catch (error) {
227
+ return {
228
+ content: [{
229
+ type: 'text' as const,
230
+ text: JSON.stringify({
231
+ success: false,
232
+ error: error instanceof Error ? error.message : 'Unknown error',
233
+ id: params.id,
234
+ source: isCommunityComponent ? 'easyeda_community' : 'lcsc',
235
+ }),
236
+ }],
237
+ isError: true,
238
+ };
239
+ }
240
+ }
241
+
242
+ export async function handleGet3DModel(args: unknown) {
243
+ const params = Model3DParamsSchema.parse(args);
244
+
245
+ const model = await easyedaClient.get3DModel(params.uuid, params.format);
246
+
247
+ if (!model) {
248
+ return {
249
+ content: [{
250
+ type: 'text' as const,
251
+ text: `3D model ${params.uuid} not found`,
252
+ }],
253
+ isError: true,
254
+ };
255
+ }
256
+
257
+ return {
258
+ content: [{
259
+ type: 'text' as const,
260
+ text: `3D model downloaded (${model.length} bytes, ${params.format.toUpperCase()} format)\n\nBase64 data:\n${model.toString('base64').slice(0, 500)}...`,
261
+ }],
262
+ };
263
+ }