@jlcpcb/mcp 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jlcpcb/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "MCP server for JLC/EasyEDA component sourcing, library fetching, and conversion to KiCad format",
@@ -21,6 +21,7 @@
21
21
  "build": "bun build ./src/index.ts --outdir ./dist --target node",
22
22
  "start": "bun run ./src/index.ts",
23
23
  "dev": "bun --watch ./src/index.ts",
24
+ "lint": "eslint src --ext .ts",
24
25
  "typecheck": "tsc --noEmit",
25
26
  "test": "bun test",
26
27
  "clean": "rm -rf dist"
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  * EasyEDA (owned by JLC/LCSC) provides the symbol and footprint data.
12
12
  */
13
13
 
14
+ import { createRequire } from 'module';
14
15
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
17
  import {
@@ -21,13 +22,16 @@ import {
21
22
  import { tools, toolHandlers } from './tools/index.js';
22
23
  import { createLogger, ensureGlobalLibraryTables, startHttpServer } from '@jlcpcb/core';
23
24
 
25
+ const require = createRequire(import.meta.url);
26
+ const { version } = require('../package.json');
27
+
24
28
  const logger = createLogger('jlc-mcp');
25
29
 
26
30
  // Create MCP server
27
31
  const server = new Server(
28
32
  {
29
33
  name: 'jlc-mcp',
30
- version: '0.1.0',
34
+ version,
31
35
  },
32
36
  {
33
37
  capabilities: {
package/src/schemas.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared validation schemas for MCP tools
3
+ * Re-exports from @jlcpcb/core for consistency across packages
4
+ */
5
+
6
+ export {
7
+ LCSCPartNumberSchema,
8
+ LCSCPartNumberSchema as LcscIdSchema, // Alias for backwards compatibility
9
+ EasyEDAUuidSchema,
10
+ ComponentIdSchema,
11
+ SafePathSchema,
12
+ isLcscId,
13
+ isEasyEDAUuid,
14
+ } from '@jlcpcb/core';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared service instances for MCP tools
3
+ * Provides singleton access to core services to avoid multiple instantiations
4
+ */
5
+
6
+ import {
7
+ createComponentService,
8
+ createLibraryService,
9
+ type ComponentService,
10
+ type LibraryService,
11
+ } from '@jlcpcb/core';
12
+
13
+ let componentService: ComponentService | null = null;
14
+ let libraryService: LibraryService | null = null;
15
+
16
+ /**
17
+ * Get the shared ComponentService instance
18
+ */
19
+ export function getComponentService(): ComponentService {
20
+ if (!componentService) {
21
+ componentService = createComponentService();
22
+ }
23
+ return componentService;
24
+ }
25
+
26
+ /**
27
+ * Get the shared LibraryService instance
28
+ */
29
+ export function getLibraryService(): LibraryService {
30
+ if (!libraryService) {
31
+ libraryService = createLibraryService();
32
+ }
33
+ return libraryService;
34
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Batch operations for MCP
3
+ * Install multiple components in a single call
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
8
+ import { getLibraryService } from '../services.js';
9
+ import { LcscIdSchema } from '../schemas.js';
10
+
11
+ export const libraryBatchInstallTool: Tool = {
12
+ name: 'library_batch_install',
13
+ description: `Install multiple components to KiCad libraries in a single call.
14
+
15
+ Accepts up to 10 LCSC part numbers. Components are installed in parallel.
16
+ Returns a summary of installed, skipped (already installed), and failed components.
17
+
18
+ Use this when you need to install a bill of materials or multiple components at once.`,
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ ids: {
23
+ type: 'array',
24
+ items: { type: 'string' },
25
+ maxItems: 10,
26
+ description: 'Array of LCSC part numbers (e.g., ["C2040", "C5446", "C14663"])',
27
+ },
28
+ force: {
29
+ type: 'boolean',
30
+ description: 'Reinstall even if already exists (default: false)',
31
+ },
32
+ include_3d: {
33
+ type: 'boolean',
34
+ description: 'Include 3D models if available (default: false)',
35
+ },
36
+ },
37
+ required: ['ids'],
38
+ },
39
+ };
40
+
41
+ export const BatchInstallParamsSchema = z.object({
42
+ ids: z.array(LcscIdSchema).min(1).max(10),
43
+ force: z.boolean().optional(),
44
+ include_3d: z.boolean().optional(),
45
+ });
46
+
47
+ interface BatchResult {
48
+ id: string;
49
+ status: 'installed' | 'skipped' | 'failed';
50
+ symbol_ref?: string;
51
+ footprint_ref?: string;
52
+ reason?: string;
53
+ error?: string;
54
+ }
55
+
56
+ export async function handleLibraryBatchInstall(args: unknown) {
57
+ const params = BatchInstallParamsSchema.parse(args);
58
+
59
+ const results: BatchResult[] = [];
60
+ let installed = 0;
61
+ let skipped = 0;
62
+ let failed = 0;
63
+
64
+ // Install components in parallel (max 10)
65
+ const installPromises = params.ids.map(async (id): Promise<BatchResult> => {
66
+ try {
67
+ const result = await getLibraryService().install(id, {
68
+ include3d: params.include_3d,
69
+ force: params.force,
70
+ });
71
+
72
+ // Check if it was actually installed or already existed
73
+ if (result.symbolAction === 'exists') {
74
+ return {
75
+ id,
76
+ status: 'skipped',
77
+ symbol_ref: result.symbolRef,
78
+ footprint_ref: result.footprintRef,
79
+ reason: 'already installed',
80
+ };
81
+ }
82
+
83
+ return {
84
+ id,
85
+ status: 'installed',
86
+ symbol_ref: result.symbolRef,
87
+ footprint_ref: result.footprintRef,
88
+ };
89
+ } catch (error) {
90
+ return {
91
+ id,
92
+ status: 'failed',
93
+ error: error instanceof Error ? error.message : 'Unknown error',
94
+ };
95
+ }
96
+ });
97
+
98
+ const installResults = await Promise.all(installPromises);
99
+
100
+ // Aggregate results
101
+ for (const result of installResults) {
102
+ results.push(result);
103
+ if (result.status === 'installed') installed++;
104
+ else if (result.status === 'skipped') skipped++;
105
+ else failed++;
106
+ }
107
+
108
+ return {
109
+ content: [{
110
+ type: 'text' as const,
111
+ text: JSON.stringify({
112
+ success: failed < params.ids.length, // At least one succeeded
113
+ summary: {
114
+ total: params.ids.length,
115
+ installed,
116
+ skipped,
117
+ failed,
118
+ },
119
+ results,
120
+ }),
121
+ }],
122
+ };
123
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { toolHandlers, tools } from './index.js';
4
+
5
+ describe('tools', () => {
6
+ it('exposes a handler for every registered tool', () => {
7
+ expect(tools.length).toBeGreaterThan(0);
8
+
9
+ for (const tool of tools) {
10
+ expect(toolHandlers[tool.name]).toBeDefined();
11
+ }
12
+ });
13
+ });
@@ -1,22 +1,22 @@
1
1
  /**
2
- * MCP tool definitions and handlers for LCSC MCP server
2
+ * MCP tool definitions and handlers for JLC-MCP server
3
+ * Streamlined version with 6 consolidated tools
3
4
  */
4
5
 
5
6
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
6
7
 
7
- // Import LCSC tools
8
- import { searchComponentsTool, handleSearchComponents } from './search.js';
9
- import { getComponentTool, handleGetComponent } from './details.js';
8
+ // Import tools
9
+ import { componentSearchTool, handleComponentSearch } from './search.js';
10
10
  import {
11
- getSymbolKicadTool,
12
- getFootprintKicadTool,
13
- fetchLibraryTool,
14
- get3DModelTool,
15
- handleGetSymbolKicad,
16
- handleGetFootprintKicad,
17
- handleFetchLibrary,
18
- handleGet3DModel,
11
+ libraryInstallTool,
12
+ libraryGetComponentTool,
13
+ handleLibraryInstall,
14
+ handleLibraryGetComponent,
19
15
  } from './library.js';
16
+ import {
17
+ libraryBatchInstallTool,
18
+ handleLibraryBatchInstall,
19
+ } from './batch.js';
20
20
  import {
21
21
  updateLibraryTool,
22
22
  handleUpdateLibrary,
@@ -26,62 +26,41 @@ import {
26
26
  handleFixLibrary,
27
27
  } from './library-fix.js';
28
28
 
29
- // Import EasyEDA community tools
30
- import {
31
- easyedaSearchTool,
32
- easyedaGet3DModelTool,
33
- handleEasyedaSearch,
34
- handleEasyedaGet3DModel,
35
- } from './easyeda.js';
36
-
37
- // Export all tool definitions
29
+ // Export all tool definitions (6 tools total)
38
30
  export const tools: Tool[] = [
39
- // LCSC/JLCPCB official library
40
- searchComponentsTool,
41
- getComponentTool,
42
- getSymbolKicadTool,
43
- getFootprintKicadTool,
44
- fetchLibraryTool,
45
- updateLibraryTool,
46
- fixLibraryTool,
47
- get3DModelTool,
48
- // EasyEDA community library
49
- easyedaSearchTool,
50
- easyedaGet3DModelTool,
31
+ componentSearchTool, // Search LCSC or EasyEDA community
32
+ libraryInstallTool, // Install single component
33
+ libraryBatchInstallTool, // Install up to 10 components
34
+ libraryGetComponentTool, // Get installed component metadata
35
+ updateLibraryTool, // Regenerate all components
36
+ fixLibraryTool, // Apply pin corrections
51
37
  ];
52
38
 
53
- // Tool handler map
39
+ // Tool handler map - derived from tool definitions to prevent name mismatches
54
40
  export const toolHandlers: Record<string, (args: unknown) => Promise<{
55
41
  content: Array<{ type: 'text'; text: string }>;
56
42
  isError?: boolean;
57
43
  }>> = {
58
- // LCSC/JLCPCB official library
59
- component_search: handleSearchComponents,
60
- component_get: handleGetComponent,
61
- library_get_symbol: handleGetSymbolKicad,
62
- library_get_footprint: handleGetFootprintKicad,
63
- library_fetch: handleFetchLibrary,
64
- library_update: handleUpdateLibrary,
65
- library_fix: handleFixLibrary,
66
- library_get_3d_model: handleGet3DModel,
67
- // EasyEDA community library
68
- easyeda_search: handleEasyedaSearch,
69
- easyeda_get_3d_model: handleEasyedaGet3DModel,
44
+ [componentSearchTool.name]: handleComponentSearch,
45
+ [libraryInstallTool.name]: handleLibraryInstall,
46
+ [libraryBatchInstallTool.name]: handleLibraryBatchInstall,
47
+ [libraryGetComponentTool.name]: handleLibraryGetComponent,
48
+ [updateLibraryTool.name]: handleUpdateLibrary,
49
+ [fixLibraryTool.name]: handleFixLibrary,
70
50
  };
71
51
 
72
- // Re-export individual tools
73
- export { searchComponentsTool, handleSearchComponents } from './search.js';
74
- export { getComponentTool, handleGetComponent } from './details.js';
52
+ // Re-export for direct imports
53
+ export { componentSearchTool, handleComponentSearch } from './search.js';
75
54
  export {
76
- getSymbolKicadTool,
77
- getFootprintKicadTool,
78
- fetchLibraryTool,
79
- get3DModelTool,
80
- handleGetSymbolKicad,
81
- handleGetFootprintKicad,
82
- handleFetchLibrary,
83
- handleGet3DModel,
55
+ libraryInstallTool,
56
+ libraryGetComponentTool,
57
+ handleLibraryInstall,
58
+ handleLibraryGetComponent,
84
59
  } from './library.js';
60
+ export {
61
+ libraryBatchInstallTool,
62
+ handleLibraryBatchInstall,
63
+ } from './batch.js';
85
64
  export {
86
65
  updateLibraryTool,
87
66
  handleUpdateLibrary,
@@ -90,9 +69,3 @@ export {
90
69
  fixLibraryTool,
91
70
  handleFixLibrary,
92
71
  } from './library-fix.js';
93
- export {
94
- easyedaSearchTool,
95
- easyedaGet3DModelTool,
96
- handleEasyedaSearch,
97
- handleEasyedaGet3DModel,
98
- } from './easyeda.js';
@@ -19,28 +19,11 @@ import {
19
19
  getFootprintReference as getCategoryFootprintRef,
20
20
  ensureDir,
21
21
  writeText,
22
+ detectKicadVersion,
22
23
  type EasyEDAPin,
23
24
  } from '@jlcpcb/core';
24
25
  import { join } from 'path';
25
26
 
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
27
  /**
45
28
  * Get library paths for fix operation
46
29
  */
@@ -21,30 +21,13 @@ import {
21
21
  type LibraryCategory,
22
22
  ensureDir,
23
23
  writeText,
24
+ detectKicadVersion,
24
25
  } from '@jlcpcb/core';
25
26
  import { join } from 'path';
26
27
 
27
- // KiCad versions to check (newest first)
28
- const KICAD_VERSIONS = ['9.0', '8.0'];
29
-
30
28
  // 3rd party library namespace
31
29
  const LIBRARY_NAMESPACE = 'jlc_mcp';
32
30
 
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
31
  /**
49
32
  * Get library paths for update operation
50
33
  * Platform-specific paths matching where ${KICAD9_3RD_PARTY} resolves:
@@ -383,6 +366,8 @@ export async function handleUpdateLibrary(args: unknown) {
383
366
  custom_generated: successful.filter((r) => r.footprintType === 'generated').length,
384
367
  };
385
368
 
369
+ // Compact response - summary only, no detailed failures
370
+ // (failures can be retrieved separately if needed)
386
371
  return {
387
372
  content: [{
388
373
  type: 'text' as const,
@@ -390,23 +375,19 @@ export async function handleUpdateLibrary(args: unknown) {
390
375
  success: true,
391
376
  dry_run: params.dry_run,
392
377
  summary: {
393
- total_components: allLcscIds.size,
378
+ total: allLcscIds.size,
394
379
  updated: successful.length,
395
380
  failed: failed.length,
396
- libraries_generated: categorySymbols.size,
397
381
  },
398
382
  by_category: Object.fromEntries(byCategory),
399
383
  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) => ({
384
+ // Only include first 5 failures to avoid massive responses
385
+ failed_sample: failed.slice(0, 5).map((f) => ({
406
386
  lcsc_id: f.lcscId,
407
387
  error: f.error,
408
388
  })),
409
- }, null, 2),
389
+ has_more_failures: failed.length > 5,
390
+ }),
410
391
  }],
411
392
  };
412
393
  }