@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,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyEDA Community Library MCP Tools
|
|
3
|
+
* Search and 3D model download for community-contributed components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js'
|
|
8
|
+
import { easyedaCommunityClient } from '@jlcpcb/core'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import { execSync } from 'child_process'
|
|
11
|
+
import { tmpdir } from 'os'
|
|
12
|
+
|
|
13
|
+
// Tool Definitions
|
|
14
|
+
|
|
15
|
+
export const easyedaSearchTool: Tool = {
|
|
16
|
+
name: 'easyeda_search',
|
|
17
|
+
description:
|
|
18
|
+
'Search EasyEDA community library for user-contributed symbols and footprints. Use this for parts not in LCSC official library (e.g., XIAO, Arduino modules, custom breakouts). Returns results with UUIDs and optionally opens an HTML preview.',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
query: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Search term (e.g., "XIAO RP2040", "ESP32-C3 module")',
|
|
25
|
+
},
|
|
26
|
+
source: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
enum: ['user', 'lcsc', 'easyeda', 'all'],
|
|
29
|
+
description:
|
|
30
|
+
'Filter by source. "user" for community-contributed (default)',
|
|
31
|
+
},
|
|
32
|
+
limit: {
|
|
33
|
+
type: 'number',
|
|
34
|
+
description: 'Max results to return (default: 20)',
|
|
35
|
+
},
|
|
36
|
+
open_preview: {
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
description: 'Generate and open HTML preview in browser',
|
|
39
|
+
default: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ['query'],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const easyedaGet3DModelTool: Tool = {
|
|
47
|
+
name: 'easyeda_get_3d_model',
|
|
48
|
+
description:
|
|
49
|
+
'Download 3D model for an EasyEDA community component. Requires the model UUID from easyeda_get.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
uuid: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: '3D model UUID from easyeda_get result',
|
|
56
|
+
},
|
|
57
|
+
format: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
enum: ['step', 'obj'],
|
|
60
|
+
description: 'Model format: "step" or "obj" (default: step)',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['uuid'],
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Zod Schemas
|
|
68
|
+
|
|
69
|
+
const EasyedaSearchParamsSchema = z.object({
|
|
70
|
+
query: z.string().min(1),
|
|
71
|
+
source: z.enum(['user', 'lcsc', 'easyeda', 'all']).optional(),
|
|
72
|
+
limit: z.number().min(1).max(100).optional(),
|
|
73
|
+
open_preview: z.boolean().optional(),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const EasyedaGet3DModelParamsSchema = z.object({
|
|
77
|
+
uuid: z.string().min(1),
|
|
78
|
+
format: z.enum(['step', 'obj']).default('step'),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Tool Handlers
|
|
82
|
+
|
|
83
|
+
export async function handleEasyedaSearch(args: unknown) {
|
|
84
|
+
const params = EasyedaSearchParamsSchema.parse(args)
|
|
85
|
+
const openPreview = params.open_preview ?? true
|
|
86
|
+
|
|
87
|
+
const results = await easyedaCommunityClient.search({
|
|
88
|
+
query: params.query,
|
|
89
|
+
source: params.source,
|
|
90
|
+
limit: params.limit || 20,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (results.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: 'text' as const,
|
|
98
|
+
text: `No results found for "${params.query}"`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate text output
|
|
105
|
+
let output = `Found ${results.length} results for "${params.query}":\n\n`
|
|
106
|
+
output += '| # | Title | Package | Owner | UUID |\n'
|
|
107
|
+
output += '|---|-------|---------|-------|------|\n'
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < results.length; i++) {
|
|
110
|
+
const r = results[i]
|
|
111
|
+
output += `| ${i + 1} | ${r.title} | ${r.package} | ${r.owner.nickname || r.owner.username} | ${r.uuid} |\n`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
output += '\nUse `library_fetch` with the UUID to add component to global JLC-MCP libraries.'
|
|
115
|
+
output += '\nUse `easyeda_fetch` with the UUID to add to project-local EasyEDA library.'
|
|
116
|
+
|
|
117
|
+
// Generate HTML preview
|
|
118
|
+
if (openPreview) {
|
|
119
|
+
const { filepath, browserOpened } = await generateHtmlPreview(params.query, results)
|
|
120
|
+
if (browserOpened) {
|
|
121
|
+
output += `\n\nHTML preview opened in browser.`
|
|
122
|
+
} else {
|
|
123
|
+
output += `\n\nCould not open browser automatically.`
|
|
124
|
+
}
|
|
125
|
+
output += `\nPreview file: ${filepath}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text' as const,
|
|
132
|
+
text: output,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function handleEasyedaGet3DModel(args: unknown) {
|
|
139
|
+
const params = EasyedaGet3DModelParamsSchema.parse(args)
|
|
140
|
+
|
|
141
|
+
const model = await easyedaCommunityClient.get3DModel(
|
|
142
|
+
params.uuid,
|
|
143
|
+
params.format
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if (!model) {
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: 'text' as const,
|
|
151
|
+
text: `3D model ${params.uuid} not found`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
isError: true,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text' as const,
|
|
162
|
+
text: `3D model downloaded (${model.length} bytes, ${params.format.toUpperCase()} format)\n\nBase64 data:\n${model.toString('base64').slice(0, 500)}...`,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Helper Functions
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate HTML preview file and open it in browser
|
|
172
|
+
* Fetches component details to generate SVG previews for both symbol and footprint
|
|
173
|
+
* Returns the filepath and whether the browser was successfully opened
|
|
174
|
+
*/
|
|
175
|
+
async function generateHtmlPreview(
|
|
176
|
+
query: string,
|
|
177
|
+
results: Awaited<ReturnType<typeof easyedaCommunityClient.search>>
|
|
178
|
+
): Promise<{ filepath: string; browserOpened: boolean }> {
|
|
179
|
+
const timestamp = Date.now()
|
|
180
|
+
const filename = `easyeda-search-${timestamp}.html`
|
|
181
|
+
const filepath = join(tmpdir(), filename)
|
|
182
|
+
|
|
183
|
+
// No-image placeholder SVG
|
|
184
|
+
const noImageSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 150" style="background:#2a2a2a"><text x="100" y="75" text-anchor="middle" fill="#666" font-size="12">No Preview</text></svg>`
|
|
185
|
+
const noImageDataUri = `data:image/svg+xml,${encodeURIComponent(noImageSvg)}`
|
|
186
|
+
|
|
187
|
+
// Generate cards with symbol thumbnail URL and footprint SVG
|
|
188
|
+
const cardsPromises = results.slice(0, 10).map(async (r) => {
|
|
189
|
+
// Symbol image: use EasyEDA's thumbnail URL
|
|
190
|
+
const symbolImageUrl = `https://image.easyeda.com/components/${r.uuid}.png`
|
|
191
|
+
|
|
192
|
+
// Footprint: generate SVG from shape data (need to fetch component details)
|
|
193
|
+
let footprintSvg = ''
|
|
194
|
+
try {
|
|
195
|
+
const component = await easyedaCommunityClient.getComponent(r.uuid)
|
|
196
|
+
if (component) {
|
|
197
|
+
const rawData = component.rawData as any
|
|
198
|
+
// For docType 2: footprint in packageDetail.dataStr
|
|
199
|
+
// For docType 4: footprint in top-level dataStr
|
|
200
|
+
const fpDataStr = rawData?.packageDetail?.dataStr || rawData?.dataStr
|
|
201
|
+
if (fpDataStr?.shape) {
|
|
202
|
+
footprintSvg = generateFootprintSvg(fpDataStr)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Ignore fetch errors, show placeholder
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const footprintDataUri = footprintSvg ? `data:image/svg+xml,${encodeURIComponent(footprintSvg)}` : noImageDataUri
|
|
210
|
+
|
|
211
|
+
return `
|
|
212
|
+
<div class="card">
|
|
213
|
+
<div class="images">
|
|
214
|
+
<div class="image-box">
|
|
215
|
+
<div class="image-label">Symbol</div>
|
|
216
|
+
<img src="${symbolImageUrl}" alt="Symbol" onerror="this.src='${noImageDataUri}'">
|
|
217
|
+
</div>
|
|
218
|
+
<div class="image-box">
|
|
219
|
+
<div class="image-label">Footprint</div>
|
|
220
|
+
<img src="${footprintDataUri}" alt="Footprint">
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<h3>${escapeHtml(r.title)}</h3>
|
|
224
|
+
<div class="meta">
|
|
225
|
+
<div><strong>Package:</strong> ${escapeHtml(r.package || 'N/A')}</div>
|
|
226
|
+
<div><strong>Owner:</strong> ${escapeHtml(r.owner.nickname || r.owner.username)}</div>
|
|
227
|
+
${r.manufacturer ? `<div><strong>Mfr:</strong> ${escapeHtml(r.manufacturer)}</div>` : ''}
|
|
228
|
+
</div>
|
|
229
|
+
<div class="uuid" onclick="navigator.clipboard.writeText('${r.uuid}'); this.classList.add('copied'); setTimeout(() => this.classList.remove('copied'), 1000);">
|
|
230
|
+
${r.uuid}
|
|
231
|
+
</div>
|
|
232
|
+
</div>`
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const cards = (await Promise.all(cardsPromises)).join('\n')
|
|
236
|
+
|
|
237
|
+
const html = `<!DOCTYPE html>
|
|
238
|
+
<html lang="en">
|
|
239
|
+
<head>
|
|
240
|
+
<meta charset="UTF-8">
|
|
241
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
242
|
+
<title>EasyEDA Search: ${escapeHtml(query)}</title>
|
|
243
|
+
<style>
|
|
244
|
+
* { box-sizing: border-box; }
|
|
245
|
+
body {
|
|
246
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
247
|
+
max-width: 1400px;
|
|
248
|
+
margin: 0 auto;
|
|
249
|
+
padding: 20px;
|
|
250
|
+
background: #f9f9f9;
|
|
251
|
+
color: #333;
|
|
252
|
+
}
|
|
253
|
+
h1 { margin-bottom: 8px; }
|
|
254
|
+
.subtitle { color: #666; margin-bottom: 20px; }
|
|
255
|
+
.grid {
|
|
256
|
+
display: grid;
|
|
257
|
+
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
258
|
+
gap: 16px;
|
|
259
|
+
}
|
|
260
|
+
.card {
|
|
261
|
+
background: white;
|
|
262
|
+
border: 1px solid #ddd;
|
|
263
|
+
border-radius: 8px;
|
|
264
|
+
padding: 16px;
|
|
265
|
+
transition: box-shadow 0.2s;
|
|
266
|
+
}
|
|
267
|
+
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
|
268
|
+
.card .images {
|
|
269
|
+
display: flex;
|
|
270
|
+
gap: 8px;
|
|
271
|
+
margin-bottom: 12px;
|
|
272
|
+
}
|
|
273
|
+
.card .image-box {
|
|
274
|
+
flex: 1;
|
|
275
|
+
min-width: 0;
|
|
276
|
+
}
|
|
277
|
+
.card .image-label {
|
|
278
|
+
font-size: 10px;
|
|
279
|
+
color: #888;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
text-align: center;
|
|
282
|
+
margin-bottom: 4px;
|
|
283
|
+
}
|
|
284
|
+
.card img {
|
|
285
|
+
width: 100%;
|
|
286
|
+
height: 120px;
|
|
287
|
+
object-fit: contain;
|
|
288
|
+
border-radius: 4px;
|
|
289
|
+
border: 1px solid #eee;
|
|
290
|
+
}
|
|
291
|
+
.card h3 {
|
|
292
|
+
margin: 0 0 8px;
|
|
293
|
+
font-size: 15px;
|
|
294
|
+
line-height: 1.3;
|
|
295
|
+
overflow: hidden;
|
|
296
|
+
text-overflow: ellipsis;
|
|
297
|
+
white-space: nowrap;
|
|
298
|
+
}
|
|
299
|
+
.card .meta {
|
|
300
|
+
color: #666;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
line-height: 1.6;
|
|
303
|
+
}
|
|
304
|
+
.card .meta div { margin-bottom: 2px; }
|
|
305
|
+
.card .uuid {
|
|
306
|
+
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
307
|
+
font-size: 10px;
|
|
308
|
+
color: #888;
|
|
309
|
+
background: #f5f5f5;
|
|
310
|
+
padding: 6px 8px;
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
margin-top: 12px;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
word-break: break-all;
|
|
315
|
+
transition: background 0.2s;
|
|
316
|
+
}
|
|
317
|
+
.card .uuid:hover { background: #e8e8e8; }
|
|
318
|
+
.card .uuid.copied { background: #d4edda; color: #155724; }
|
|
319
|
+
.instructions {
|
|
320
|
+
background: #e8f4fd;
|
|
321
|
+
border: 1px solid #b8daff;
|
|
322
|
+
border-radius: 8px;
|
|
323
|
+
padding: 16px;
|
|
324
|
+
margin-bottom: 20px;
|
|
325
|
+
font-size: 14px;
|
|
326
|
+
}
|
|
327
|
+
.instructions code {
|
|
328
|
+
background: #fff;
|
|
329
|
+
padding: 2px 6px;
|
|
330
|
+
border-radius: 4px;
|
|
331
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
334
|
+
</head>
|
|
335
|
+
<body>
|
|
336
|
+
<h1>EasyEDA Search: "${escapeHtml(query)}"</h1>
|
|
337
|
+
<p class="subtitle">Found ${results.length} results. Click UUID to copy to clipboard.</p>
|
|
338
|
+
|
|
339
|
+
<div class="instructions">
|
|
340
|
+
<strong>How to use:</strong><br>
|
|
341
|
+
1. Click on a UUID to copy it<br>
|
|
342
|
+
2. Use <code>library_fetch</code> with the UUID to add to global JLC-MCP libraries<br>
|
|
343
|
+
3. Or use <code>easyeda_fetch</code> for project-local EasyEDA library
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div class="grid">
|
|
347
|
+
${cards}
|
|
348
|
+
</div>
|
|
349
|
+
</body>
|
|
350
|
+
</html>`
|
|
351
|
+
|
|
352
|
+
// Write HTML file
|
|
353
|
+
require('fs').writeFileSync(filepath, html, 'utf-8')
|
|
354
|
+
|
|
355
|
+
// Open in default browser (cross-platform)
|
|
356
|
+
const browserOpened = openInBrowser(filepath)
|
|
357
|
+
|
|
358
|
+
return { filepath, browserOpened }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Open a file in the default browser (cross-platform)
|
|
363
|
+
* Returns true if browser was successfully opened, false otherwise
|
|
364
|
+
*/
|
|
365
|
+
function openInBrowser(filepath: string): boolean {
|
|
366
|
+
const platform = process.platform
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
switch (platform) {
|
|
370
|
+
case 'darwin':
|
|
371
|
+
// macOS
|
|
372
|
+
execSync(`open "${filepath}"`, { stdio: 'ignore' })
|
|
373
|
+
return true
|
|
374
|
+
case 'win32':
|
|
375
|
+
// Windows - use start command with empty title
|
|
376
|
+
execSync(`start "" "${filepath}"`, { stdio: 'ignore', shell: 'cmd.exe' })
|
|
377
|
+
return true
|
|
378
|
+
case 'linux':
|
|
379
|
+
default:
|
|
380
|
+
// Linux and other Unix-like systems
|
|
381
|
+
execSync(`xdg-open "${filepath}"`, { stdio: 'ignore' })
|
|
382
|
+
return true
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// If the platform-specific command fails, try alternatives
|
|
386
|
+
const fallbacks = ['xdg-open', 'sensible-browser', 'x-www-browser', 'gnome-open']
|
|
387
|
+
for (const cmd of fallbacks) {
|
|
388
|
+
try {
|
|
389
|
+
execSync(`${cmd} "${filepath}"`, { stdio: 'ignore' })
|
|
390
|
+
return true
|
|
391
|
+
} catch {
|
|
392
|
+
// Try next fallback
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// All attempts failed
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Escape HTML special characters
|
|
402
|
+
*/
|
|
403
|
+
function escapeHtml(str: string): string {
|
|
404
|
+
return str
|
|
405
|
+
.replace(/&/g, '&')
|
|
406
|
+
.replace(/</g, '<')
|
|
407
|
+
.replace(/>/g, '>')
|
|
408
|
+
.replace(/"/g, '"')
|
|
409
|
+
.replace(/'/g, ''')
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Generate SVG from EasyEDA footprint dataStr
|
|
414
|
+
* Renders shapes with proper z-ordering: regions → tracks → pads → holes → text
|
|
415
|
+
*/
|
|
416
|
+
function generateFootprintSvg(dataStr: {
|
|
417
|
+
shape?: string[]
|
|
418
|
+
BBox?: { x: number; y: number; width: number; height: number }
|
|
419
|
+
head?: { x?: number; y?: number }
|
|
420
|
+
}): string {
|
|
421
|
+
if (!dataStr.shape || dataStr.shape.length === 0) {
|
|
422
|
+
return ''
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Get bounding box or calculate from origin
|
|
426
|
+
const bbox = dataStr.BBox || { x: 0, y: 0, width: 100, height: 100 }
|
|
427
|
+
const padding = 5
|
|
428
|
+
const viewBox = `${bbox.x - padding} ${bbox.y - padding} ${bbox.width + padding * 2} ${bbox.height + padding * 2}`
|
|
429
|
+
|
|
430
|
+
// Separate shapes by type for proper z-ordering
|
|
431
|
+
const regions: string[] = []
|
|
432
|
+
const tracks: string[] = []
|
|
433
|
+
const pads: string[] = []
|
|
434
|
+
const holes: string[] = []
|
|
435
|
+
const texts: string[] = []
|
|
436
|
+
|
|
437
|
+
for (const shape of dataStr.shape) {
|
|
438
|
+
if (typeof shape !== 'string') continue
|
|
439
|
+
|
|
440
|
+
if (shape.startsWith('SOLIDREGION~')) {
|
|
441
|
+
const svg = renderSolidRegion(shape)
|
|
442
|
+
if (svg) regions.push(svg)
|
|
443
|
+
} else if (shape.startsWith('TRACK~')) {
|
|
444
|
+
const svg = renderTrackShape(shape)
|
|
445
|
+
if (svg) tracks.push(svg)
|
|
446
|
+
} else if (shape.startsWith('PAD~')) {
|
|
447
|
+
const result = renderPadShape(shape)
|
|
448
|
+
if (result) {
|
|
449
|
+
pads.push(result.pad)
|
|
450
|
+
if (result.hole) holes.push(result.hole)
|
|
451
|
+
}
|
|
452
|
+
} else if (shape.startsWith('TEXT~')) {
|
|
453
|
+
const svg = renderTextShape(shape)
|
|
454
|
+
if (svg) texts.push(svg)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const allElements = [...regions, ...tracks, ...pads, ...holes, ...texts]
|
|
459
|
+
if (allElements.length === 0) {
|
|
460
|
+
return ''
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// KiCAD-style colors: black bg, red pads, grey holes, yellow outlines/text
|
|
464
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" style="background:#000000">
|
|
465
|
+
<style>
|
|
466
|
+
.pad { fill: #CC0000; stroke: none; }
|
|
467
|
+
.pad-hole { fill: #666666; }
|
|
468
|
+
.track { fill: none; stroke: #FFFF00; stroke-linecap: round; stroke-linejoin: round; }
|
|
469
|
+
.region { fill: #CC0000; opacity: 0.6; }
|
|
470
|
+
.text-path { fill: none; stroke: #FFFF00; stroke-width: 0.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
471
|
+
</style>
|
|
472
|
+
${allElements.join('\n ')}
|
|
473
|
+
</svg>`
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Render PAD shape to SVG - returns pad and hole separately for z-ordering
|
|
478
|
+
* Format: PAD~shapeType~cx~cy~width~height~layer~~pinNum~holeDia~points~rot~id~...
|
|
479
|
+
*/
|
|
480
|
+
function renderPadShape(padData: string): { pad: string; hole: string | null } | null {
|
|
481
|
+
const fields = padData.split('~')
|
|
482
|
+
const shapeType = fields[1]
|
|
483
|
+
const cx = parseFloat(fields[2]) || 0
|
|
484
|
+
const cy = parseFloat(fields[3]) || 0
|
|
485
|
+
|
|
486
|
+
if (shapeType === 'POLYGON') {
|
|
487
|
+
// PAD~POLYGON~cx~cy~width~height~layer~~pinNum~holeDia~points...
|
|
488
|
+
// Field indices: 0=PAD, 1=POLYGON, 2=cx, 3=cy, 4=width, 5=height, 6=layer, 7=empty, 8=pinNum, 9=holeDia, 10=points
|
|
489
|
+
const holeDia = parseFloat(fields[9]) || 0
|
|
490
|
+
const pointsStr = fields[10] || ''
|
|
491
|
+
if (!pointsStr) return null
|
|
492
|
+
|
|
493
|
+
// Parse polygon points (space-separated x y pairs)
|
|
494
|
+
const coords = pointsStr.split(' ').map(Number)
|
|
495
|
+
if (coords.length < 4) return null
|
|
496
|
+
|
|
497
|
+
let pathD = `M ${coords[0]} ${coords[1]}`
|
|
498
|
+
for (let i = 2; i < coords.length; i += 2) {
|
|
499
|
+
pathD += ` L ${coords[i]} ${coords[i + 1]}`
|
|
500
|
+
}
|
|
501
|
+
pathD += ' Z'
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
pad: `<path class="pad" d="${pathD}"/>`,
|
|
505
|
+
hole: holeDia > 0 ? `<circle class="pad-hole" cx="${cx}" cy="${cy}" r="${holeDia}"/>` : null,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Standard pads: ELLIPSE, OVAL, RECT, ROUND
|
|
510
|
+
const width = parseFloat(fields[4]) || 0
|
|
511
|
+
const height = parseFloat(fields[5]) || 0
|
|
512
|
+
const holeDia = parseFloat(fields[9]) || 0
|
|
513
|
+
|
|
514
|
+
let padSvg = ''
|
|
515
|
+
|
|
516
|
+
if (shapeType === 'ELLIPSE' || shapeType === 'OVAL' || shapeType === 'ROUND') {
|
|
517
|
+
const rx = width / 2
|
|
518
|
+
const ry = height / 2
|
|
519
|
+
padSvg = `<ellipse class="pad" cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}"/>`
|
|
520
|
+
} else {
|
|
521
|
+
// RECT or default
|
|
522
|
+
const rectX = cx - width / 2
|
|
523
|
+
const rectY = cy - height / 2
|
|
524
|
+
padSvg = `<rect class="pad" x="${rectX}" y="${rectY}" width="${width}" height="${height}"/>`
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
pad: padSvg,
|
|
529
|
+
hole: holeDia > 0 ? `<circle class="pad-hole" cx="${cx}" cy="${cy}" r="${holeDia}"/>` : null,
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Render TRACK shape to SVG
|
|
535
|
+
* Format: TRACK~width~layer~~points
|
|
536
|
+
*/
|
|
537
|
+
function renderTrackShape(trackData: string): string | null {
|
|
538
|
+
const fields = trackData.split('~')
|
|
539
|
+
const strokeWidth = parseFloat(fields[1]) || 0.5
|
|
540
|
+
const pointsStr = fields[4] || ''
|
|
541
|
+
|
|
542
|
+
if (!pointsStr) return null
|
|
543
|
+
|
|
544
|
+
const coords = pointsStr.split(' ').map(Number)
|
|
545
|
+
if (coords.length < 4) return null
|
|
546
|
+
|
|
547
|
+
let pathD = `M ${coords[0]} ${coords[1]}`
|
|
548
|
+
for (let i = 2; i < coords.length; i += 2) {
|
|
549
|
+
pathD += ` L ${coords[i]} ${coords[i + 1]}`
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return `<path class="track" d="${pathD}" stroke-width="${strokeWidth}"/>`
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Render SOLIDREGION shape to SVG
|
|
557
|
+
* Format: SOLIDREGION~layer~~path~fill~id~...
|
|
558
|
+
* Field indices: 0=SOLIDREGION, 1=layer, 2=empty, 3=path, 4=fill
|
|
559
|
+
*/
|
|
560
|
+
function renderSolidRegion(regionData: string): string | null {
|
|
561
|
+
const fields = regionData.split('~')
|
|
562
|
+
const pathD = fields[3] || ''
|
|
563
|
+
|
|
564
|
+
if (!pathD || !pathD.startsWith('M')) return null
|
|
565
|
+
|
|
566
|
+
return `<path class="region" d="${pathD}"/>`
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Render TEXT shape to SVG using pre-rendered path
|
|
571
|
+
* Format: TEXT~align~x~y~strokeWidth~rot~?~layer~~fontSize~content~svgPath~id~~flag~type
|
|
572
|
+
* Field indices: 7=layer, 10=content, 11=svgPath
|
|
573
|
+
*/
|
|
574
|
+
function renderTextShape(textData: string): string | null {
|
|
575
|
+
const fields = textData.split('~')
|
|
576
|
+
const svgPath = fields[11] || ''
|
|
577
|
+
|
|
578
|
+
if (!svgPath || !svgPath.startsWith('M')) return null
|
|
579
|
+
|
|
580
|
+
// Text is pre-rendered as SVG path commands - just use them directly
|
|
581
|
+
return `<path class="text-path" d="${svgPath}"/>`
|
|
582
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool definitions and handlers for LCSC MCP server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
|
|
7
|
+
// Import LCSC tools
|
|
8
|
+
import { searchComponentsTool, handleSearchComponents } from './search.js';
|
|
9
|
+
import { getComponentTool, handleGetComponent } from './details.js';
|
|
10
|
+
import {
|
|
11
|
+
getSymbolKicadTool,
|
|
12
|
+
getFootprintKicadTool,
|
|
13
|
+
fetchLibraryTool,
|
|
14
|
+
get3DModelTool,
|
|
15
|
+
handleGetSymbolKicad,
|
|
16
|
+
handleGetFootprintKicad,
|
|
17
|
+
handleFetchLibrary,
|
|
18
|
+
handleGet3DModel,
|
|
19
|
+
} from './library.js';
|
|
20
|
+
import {
|
|
21
|
+
updateLibraryTool,
|
|
22
|
+
handleUpdateLibrary,
|
|
23
|
+
} from './library-update.js';
|
|
24
|
+
import {
|
|
25
|
+
fixLibraryTool,
|
|
26
|
+
handleFixLibrary,
|
|
27
|
+
} from './library-fix.js';
|
|
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
|
|
38
|
+
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,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Tool handler map
|
|
54
|
+
export const toolHandlers: Record<string, (args: unknown) => Promise<{
|
|
55
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
56
|
+
isError?: boolean;
|
|
57
|
+
}>> = {
|
|
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,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Re-export individual tools
|
|
73
|
+
export { searchComponentsTool, handleSearchComponents } from './search.js';
|
|
74
|
+
export { getComponentTool, handleGetComponent } from './details.js';
|
|
75
|
+
export {
|
|
76
|
+
getSymbolKicadTool,
|
|
77
|
+
getFootprintKicadTool,
|
|
78
|
+
fetchLibraryTool,
|
|
79
|
+
get3DModelTool,
|
|
80
|
+
handleGetSymbolKicad,
|
|
81
|
+
handleGetFootprintKicad,
|
|
82
|
+
handleFetchLibrary,
|
|
83
|
+
handleGet3DModel,
|
|
84
|
+
} from './library.js';
|
|
85
|
+
export {
|
|
86
|
+
updateLibraryTool,
|
|
87
|
+
handleUpdateLibrary,
|
|
88
|
+
} from './library-update.js';
|
|
89
|
+
export {
|
|
90
|
+
fixLibraryTool,
|
|
91
|
+
handleFixLibrary,
|
|
92
|
+
} from './library-fix.js';
|
|
93
|
+
export {
|
|
94
|
+
easyedaSearchTool,
|
|
95
|
+
easyedaGet3DModelTool,
|
|
96
|
+
handleEasyedaSearch,
|
|
97
|
+
handleEasyedaGet3DModel,
|
|
98
|
+
} from './easyeda.js';
|