@fugood/bricks-project 2.24.0-beta.12 → 2.24.0-beta.13

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.
@@ -1,106 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
- import { z } from 'zod'
4
- import { $, JSON5 } from 'bun'
5
- import * as TOON from '@toon-format/toon'
6
- import Fuse from 'fuse.js'
7
- import { gguf } from '@huggingface/gguf'
8
- import glyphmap from './icons/fa6pro-glyphmap.json'
9
- import glyphmapMeta from './icons/fa6pro-meta.json'
10
3
 
11
- // Hugging Face API configuration
12
- const HF_API_URL = 'https://huggingface.co/api'
13
- const { HF_TOKEN } = process.env
14
-
15
- // Helper function to convert BigInt to number for JSON serialization
16
- const convertBigIntToNumber = (value: unknown): number | unknown => {
17
- if (typeof value === 'bigint') {
18
- return Number(value)
19
- }
20
- return value
21
- }
22
-
23
- // Extract GGUF metadata from a GGUF file URL with 10s timeout
24
- const extractGGUFMetadata = async (url: string) => {
25
- const timeoutPromise = new Promise<never>((_, reject) =>
26
- setTimeout(() => reject(new Error('GGUF metadata extraction timeout')), 10000),
27
- )
28
-
29
- try {
30
- const { metadata } = (await Promise.race([gguf(url), timeoutPromise])) as { metadata: any }
31
- const architecture = metadata['general.architecture']
32
-
33
- const ggufSimplifiedMetadata: Record<string, unknown> = {
34
- name: metadata['general.name'],
35
- size_label: metadata['general.size_label'],
36
- basename: metadata['general.basename'],
37
- architecture: metadata['general.architecture'],
38
- file_type: convertBigIntToNumber(metadata['general.file_type']),
39
- quantization_version: convertBigIntToNumber(metadata['general.quantization_version']),
40
- }
41
-
42
- if (!architecture) return ggufSimplifiedMetadata
43
-
44
- // Helper to add converted value if defined
45
- const addIfDefined = (target: Record<string, unknown>, key: string, value: unknown) => {
46
- if (value !== undefined) target[key] = convertBigIntToNumber(value)
47
- }
48
-
49
- // Architecture-specific transformer parameters
50
- addIfDefined(ggufSimplifiedMetadata, 'n_ctx_train', metadata[`${architecture}.context_length`])
51
- addIfDefined(ggufSimplifiedMetadata, 'n_layer', metadata[`${architecture}.block_count`])
52
- addIfDefined(ggufSimplifiedMetadata, 'n_embd', metadata[`${architecture}.embedding_length`])
53
- addIfDefined(ggufSimplifiedMetadata, 'n_head', metadata[`${architecture}.attention.head_count`])
54
- addIfDefined(
55
- ggufSimplifiedMetadata,
56
- 'n_head_kv',
57
- metadata[`${architecture}.attention.head_count_kv`],
58
- )
59
- addIfDefined(
60
- ggufSimplifiedMetadata,
61
- 'n_embd_head_k',
62
- metadata[`${architecture}.attention.key_length`],
63
- )
64
- addIfDefined(
65
- ggufSimplifiedMetadata,
66
- 'n_embd_head_v',
67
- metadata[`${architecture}.attention.value_length`],
68
- )
69
- addIfDefined(
70
- ggufSimplifiedMetadata,
71
- 'n_swa',
72
- metadata[`${architecture}.attention.sliding_window`],
73
- )
74
-
75
- // SSM (Mamba) parameters for recurrent/hybrid models
76
- addIfDefined(ggufSimplifiedMetadata, 'ssm_d_conv', metadata[`${architecture}.ssm.conv_kernel`])
77
- addIfDefined(ggufSimplifiedMetadata, 'ssm_d_state', metadata[`${architecture}.ssm.state_size`])
78
- addIfDefined(ggufSimplifiedMetadata, 'ssm_d_inner', metadata[`${architecture}.ssm.inner_size`])
79
- addIfDefined(ggufSimplifiedMetadata, 'ssm_n_group', metadata[`${architecture}.ssm.group_count`])
80
- addIfDefined(
81
- ggufSimplifiedMetadata,
82
- 'ssm_dt_rank',
83
- metadata[`${architecture}.ssm.time_step_rank`],
84
- )
85
-
86
- // RWKV parameters
87
- addIfDefined(
88
- ggufSimplifiedMetadata,
89
- 'rwkv_head_size',
90
- metadata[`${architecture}.rwkv.head_size`],
91
- )
92
- addIfDefined(
93
- ggufSimplifiedMetadata,
94
- 'rwkv_token_shift_count',
95
- metadata[`${architecture}.rwkv.token_shift_count`],
96
- )
97
-
98
- return ggufSimplifiedMetadata
99
- } catch (error) {
100
- console.error('Failed to extract GGUF metadata:', error)
101
- return null
102
- }
103
- }
4
+ import { register as registerCompile } from './mcp-tools/compile'
5
+ import { register as registerLottie } from './mcp-tools/lottie'
6
+ import { register as registerIcons } from './mcp-tools/icons'
7
+ import { register as registerHuggingface } from './mcp-tools/huggingface'
8
+ import { register as registerMedia } from './mcp-tools/media'
104
9
 
105
10
  const server = new McpServer({
106
11
  name: 'bricks-project',
@@ -110,789 +15,14 @@ const server = new McpServer({
110
15
  const { dirname } = import.meta
111
16
  const projectDir = String(dirname).split('/node_modules/')[0]
112
17
 
113
- server.tool('compile', {}, async () => {
114
- let log = ''
115
- try {
116
- log += 'Type checking & Compiling...'
117
- log += await $`bun compile`.cwd(projectDir).text()
118
- } catch (err) {
119
- log += `${err.stdout.toString()}\n${err.stderr.toString()}`
120
- }
121
- return {
122
- content: [{ type: 'text', text: log }],
123
- }
124
- })
125
-
126
18
  // NOTE: Cursor (Or VSCode) seems set ELECTRON_RUN_AS_NODE to 1, so we need to unset it
127
19
  process.env.ELECTRON_RUN_AS_NODE = ''
128
20
 
129
- server.tool(
130
- 'preview',
131
- {
132
- delay: z
133
- .number()
134
- .describe('Delay in milliseconds before taking screenshot')
135
- .optional()
136
- .default(3000),
137
- width: z.number().describe('Width of the screenshot').optional().default(600),
138
- height: z.number().optional().default(480),
139
- responseImage: z
140
- .boolean()
141
- .describe(
142
- 'Whether to response image content (base64 encoded jpeg). If false, only saved path will be responded as text.',
143
- )
144
- .optional()
145
- .default(false),
146
- testId: z.string().describe('Automation test ID to trigger').optional(),
147
- testTitleLike: z
148
- .string()
149
- .describe('Find automation test by partial title match (case-insensitive)')
150
- .optional(),
151
- } as any,
152
- async ({ delay, width, height, responseImage, testId, testTitleLike }: any) => {
153
- let log = ''
154
- let error = false
155
- try {
156
- const args = [
157
- '--no-keep-open',
158
- '--take-screenshot',
159
- JSON.stringify({
160
- delay,
161
- width,
162
- height,
163
- path: `${dirname}/screenshot.jpg`,
164
- closeAfter: true,
165
- headless: true,
166
- }),
167
- ]
168
- if (testId) args.push('--test-id', testId)
169
- if (testTitleLike) args.push('--test-title-like', testTitleLike)
170
- log = await $`bunx --bun electron ${dirname}/preview-main.mjs ${args}`.cwd(projectDir).text()
171
- } catch (err) {
172
- log = `${err.stdout.toString()}\n${err.stderr.toString()}`
173
- error = true
174
- }
175
- let screenshotBase64: string | null = null
176
- if (!error && responseImage) {
177
- const screenshot = await Bun.file(`${dirname}/screenshot.jpg`).arrayBuffer()
178
- screenshotBase64 = Buffer.from(screenshot).toString('base64')
179
- }
180
- const content: Array<
181
- { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }
182
- > = [{ type: 'text', text: log }]
183
- if (screenshotBase64) {
184
- content.push({
185
- type: 'image',
186
- data: screenshotBase64,
187
- mimeType: 'image/jpeg',
188
- })
189
- }
190
- return {
191
- content,
192
- }
193
- },
194
- )
195
-
196
- // LottieFiles API tools
197
- const LOTTIEFILES_API_URL = 'https://lottiefiles.com/api'
198
-
199
- server.tool(
200
- 'lottie_search',
201
- {
202
- query: z.string().describe('Search keywords for animations'),
203
- page: z.number().min(1).optional().default(1),
204
- limit: z.number().min(1).max(100).optional().default(10),
205
- },
206
- async ({ query, page, limit }) => {
207
- try {
208
- const url = new URL(`${LOTTIEFILES_API_URL}/search/get-animations`)
209
- url.searchParams.set('query', query)
210
- url.searchParams.set('page', String(page))
211
- url.searchParams.set('limit', String(limit))
212
- url.searchParams.set('format', 'json')
213
-
214
- const response = await fetch(url.toString())
215
- const data = await response.json()
216
- const animations = data?.data?.data ?? []
217
-
218
- return {
219
- content: [
220
- {
221
- type: 'text',
222
- text: TOON.encode({ count: animations.length, animations }),
223
- },
224
- ],
225
- }
226
- } catch (err: any) {
227
- return {
228
- content: [{ type: 'text', text: `Failed to search animations: ${err.message}` }],
229
- }
230
- }
231
- },
232
- )
233
-
234
- server.tool(
235
- 'lottie_get_details',
236
- {
237
- id: z.string().describe('Animation file ID'),
238
- },
239
- async ({ id }) => {
240
- try {
241
- const url = new URL(`${LOTTIEFILES_API_URL}/animations/get-animation-data`)
242
- url.searchParams.set('fileId', id)
243
- url.searchParams.set('format', 'json')
244
-
245
- const response = await fetch(url.toString())
246
- const data = await response.json()
247
-
248
- return {
249
- content: [{ type: 'text', text: TOON.encode(data) }],
250
- }
251
- } catch (err: any) {
252
- return {
253
- content: [{ type: 'text', text: `Failed to get animation: ${err.message}` }],
254
- }
255
- }
256
- },
257
- )
258
-
259
- server.tool(
260
- 'lottie_popular',
261
- {
262
- page: z.number().min(1).optional().default(1),
263
- limit: z.number().min(1).max(100).optional().default(10),
264
- },
265
- async ({ page, limit }) => {
266
- try {
267
- const url = new URL(
268
- `${LOTTIEFILES_API_URL}/iconscout/popular-animations-weekly?api=%26sort%3Dpopular`,
269
- )
270
- url.searchParams.set('page', String(page))
271
- url.searchParams.set('limit', String(limit))
272
- url.searchParams.set('format', 'json')
273
-
274
- const response = await fetch(url.toString())
275
- const data = await response.json()
276
- const animations = data?.popularWeeklyData?.data ?? []
277
-
278
- return {
279
- content: [
280
- {
281
- type: 'text',
282
- text: TOON.encode({ count: animations.length, animations }),
283
- },
284
- ],
285
- }
286
- } catch (err: any) {
287
- return {
288
- content: [{ type: 'text', text: `Failed to get popular animations: ${err.message}` }],
289
- }
290
- }
291
- },
292
- )
293
-
294
- // FontAwesome 6 Pro icon search
295
- type IconStyle = 'brands' | 'duotone' | 'light' | 'regular' | 'solid' | 'thin'
296
- const iconMeta = glyphmapMeta as Record<IconStyle, string[]>
297
-
298
- const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
299
- const styles = (Object.keys(iconMeta) as IconStyle[]).filter((style) =>
300
- iconMeta[style].includes(name),
301
- )
302
- return { name, code, styles }
303
- })
304
-
305
- const iconFuse = new Fuse(iconList, {
306
- keys: ['name'],
307
- threshold: 0.3,
308
- includeScore: true,
309
- })
310
-
311
- server.tool(
312
- 'icon_search',
313
- {
314
- query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
315
- limit: z.number().min(1).max(100).optional().default(10),
316
- style: z
317
- .enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
318
- .optional()
319
- .describe('Filter by icon style'),
320
- },
321
- async ({ query, limit, style }) => {
322
- let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
323
-
324
- if (style) {
325
- results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
326
- }
327
-
328
- const icons = results.map((r) => ({
329
- name: r.item.name,
330
- code: r.item.code,
331
- unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
332
- styles: r.item.styles,
333
- score: r.score,
334
- }))
335
-
336
- return {
337
- content: [
338
- {
339
- type: 'text',
340
- text: TOON.encode({ count: icons.length, icons }),
341
- },
342
- ],
343
- }
344
- },
345
- )
346
-
347
- // Hugging Face model search
348
- type HFSibling = {
349
- rfilename: string
350
- size?: number
351
- lfs?: { sha256?: string }
352
- }
353
-
354
- type HFModel = {
355
- id: string
356
- author?: string
357
- downloads?: number
358
- likes?: number
359
- tags?: string[]
360
- pipeline_tag?: string
361
- siblings?: HFSibling[]
362
- config?: { model_type?: string }
363
- cardData?: { model_type?: string }
364
- }
365
-
366
- const supportedLlmTasks = [
367
- 'text-generation',
368
- 'image-text-to-text',
369
- 'text2text-generation',
370
- 'conversational',
371
- ]
372
-
373
- // Generator type configurations for Hugging Face model search
374
- type GeneratorType =
375
- | 'GeneratorLLM'
376
- | 'GeneratorVectorStore'
377
- | 'GeneratorReranker'
378
- | 'GeneratorGGMLTTS'
379
- | 'GeneratorGGMLTTSVocoder'
380
- | 'GeneratorOnnxLLM'
381
- | 'GeneratorOnnxSTT'
382
- | 'GeneratorTTS'
383
-
384
- type ModelKind = 'gguf' | 'onnx'
385
-
386
- interface GeneratorConfig {
387
- modelKind: ModelKind
388
- filter: string
389
- taskFilter?: string[]
390
- filePattern?: RegExp
391
- hasValidStructure?: (siblings: HFSibling[]) => boolean
392
- }
393
-
394
- // Helper to check valid ONNX structure
395
- const hasValidOnnxStructure = (siblings: HFSibling[]): boolean => {
396
- const hasConfigJson = siblings.some((file) => file.rfilename === 'config.json')
397
- const hasOnnxModel = siblings.some(
398
- (file) => file.rfilename.includes('onnx/') && file.rfilename.endsWith('.onnx'),
399
- )
400
- return hasConfigJson && hasOnnxModel
401
- }
402
-
403
- // Detect quantization types from ONNX files
404
- const detectOnnxQuantizationTypes = (siblings: HFSibling[]): string[] => {
405
- const onnxFiles = siblings.filter((file) => file.rfilename.endsWith('.onnx'))
406
- const quantTypes = new Set<string>()
407
-
408
- onnxFiles.forEach((file) => {
409
- const filename = file.rfilename
410
- if (!filename.endsWith('.onnx')) return
411
- const postfix = /_(q8|q4|q4f16|fp16|int8|int4|uint8|bnb4|quantized)\.onnx$/.exec(filename)?.[1]
412
- if (!postfix) {
413
- quantTypes.add('auto')
414
- quantTypes.add('none')
415
- } else {
416
- quantTypes.add(postfix === 'quantized' ? 'q8' : postfix)
417
- }
418
- })
419
-
420
- return Array.from(quantTypes)
421
- }
422
-
423
- // Find speaker embedding files for TTS models
424
- const findSpeakerEmbedFiles = (siblings: HFSibling[]): HFSibling[] =>
425
- siblings.filter((file) => file.rfilename.startsWith('voices/') && file.rfilename.endsWith('.bin'))
426
-
427
- const generatorConfigs: Record<GeneratorType, GeneratorConfig> = {
428
- GeneratorLLM: {
429
- modelKind: 'gguf',
430
- filter: 'gguf',
431
- taskFilter: supportedLlmTasks,
432
- filePattern: /\.gguf$/,
433
- },
434
- GeneratorVectorStore: {
435
- modelKind: 'gguf',
436
- filter: 'gguf',
437
- filePattern: /\.gguf$/,
438
- },
439
- GeneratorReranker: {
440
- modelKind: 'gguf',
441
- filter: 'gguf,reranker',
442
- filePattern: /\.gguf$/,
443
- },
444
- GeneratorGGMLTTS: {
445
- modelKind: 'gguf',
446
- filter: 'gguf,text-to-speech',
447
- filePattern: /\.gguf$/,
448
- },
449
- GeneratorGGMLTTSVocoder: {
450
- modelKind: 'gguf',
451
- filter: 'gguf,feature-extraction',
452
- filePattern: /\.gguf$/,
453
- },
454
- GeneratorOnnxLLM: {
455
- modelKind: 'onnx',
456
- filter: 'onnx',
457
- taskFilter: supportedLlmTasks,
458
- hasValidStructure: hasValidOnnxStructure,
459
- },
460
- GeneratorOnnxSTT: {
461
- modelKind: 'onnx',
462
- filter: 'onnx,automatic-speech-recognition',
463
- hasValidStructure: hasValidOnnxStructure,
464
- },
465
- GeneratorTTS: {
466
- modelKind: 'onnx',
467
- filter: 'onnx,text-to-speech',
468
- hasValidStructure: hasValidOnnxStructure,
469
- },
470
- }
471
-
472
- const searchHFModels = async (filter: string, search?: string, limit = 50): Promise<HFModel[]> => {
473
- const params = new URLSearchParams({
474
- limit: String(limit),
475
- full: 'true',
476
- config: 'true',
477
- sort: 'likes',
478
- direction: '-1',
479
- })
480
- if (filter) params.set('filter', filter)
481
- if (search) params.set('search', search)
482
-
483
- const headers: Record<string, string> = {}
484
- if (HF_TOKEN) {
485
- headers['Authorization'] = `Bearer ${HF_TOKEN}`
486
- }
487
-
488
- const response = await fetch(`${HF_API_URL}/models?${params.toString()}`, { headers })
489
- if (!response.ok) {
490
- throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
491
- }
492
- return response.json()
493
- }
494
-
495
- const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
496
- const params = new URLSearchParams({ blobs: 'true' })
497
-
498
- const headers: Record<string, string> = {}
499
- if (HF_TOKEN) {
500
- headers['Authorization'] = `Bearer ${HF_TOKEN}`
501
- }
502
-
503
- const response = await fetch(`${HF_API_URL}/models/${modelId}?${params.toString()}`, { headers })
504
- if (!response.ok) {
505
- throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
506
- }
507
- return response.json()
508
- }
509
-
510
- server.tool(
511
- 'huggingface_search',
512
- {
513
- generatorType: z
514
- .enum([
515
- 'GeneratorLLM',
516
- 'GeneratorVectorStore',
517
- 'GeneratorReranker',
518
- 'GeneratorGGMLTTS',
519
- 'GeneratorGGMLTTSVocoder',
520
- 'GeneratorOnnxLLM',
521
- 'GeneratorOnnxSTT',
522
- 'GeneratorTTS',
523
- ])
524
- .describe('Generator type to search models for')
525
- .default('GeneratorLLM'),
526
- query: z.string().describe('Search keywords for models on Hugging Face').optional(),
527
- limit: z.number().min(1).max(100).optional().default(20),
528
- includeFiles: z
529
- .boolean()
530
- .optional()
531
- .default(false)
532
- .describe('Include list of model files (requires additional API calls)'),
533
- },
534
- async ({ generatorType, query, limit, includeFiles }) => {
535
- try {
536
- const config = generatorConfigs[generatorType]
537
- const models = await searchHFModels(config.filter, query, limit)
538
-
539
- // Filter models based on generator configuration
540
- const filteredModels = models.filter((model) => {
541
- const modelTags = model.tags || []
542
-
543
- // Check task filter if configured
544
- if (config.taskFilter && !config.taskFilter.some((t) => modelTags.includes(t))) {
545
- return false
546
- }
547
-
548
- // Check structure validation for ONNX models
549
- if (config.hasValidStructure && model.siblings) {
550
- if (!config.hasValidStructure(model.siblings)) {
551
- return false
552
- }
553
- }
554
-
555
- return true
556
- })
557
-
558
- // Build result models
559
- let results: Array<{
560
- id: string
561
- author?: string
562
- downloads?: number
563
- likes?: number
564
- pipeline_tag?: string
565
- model_type?: string
566
- model_kind: ModelKind
567
- files?: Array<{ filename: string; size?: number }>
568
- quantization_types?: string[]
569
- speaker_embed_files?: Array<{ filename: string; size?: number }>
570
- }> = filteredModels.map((model) => ({
571
- id: model.id,
572
- author: model.author,
573
- downloads: model.downloads,
574
- likes: model.likes,
575
- pipeline_tag: model.pipeline_tag,
576
- model_type: model.config?.model_type || model.cardData?.model_type,
577
- model_kind: config.modelKind,
578
- }))
579
-
580
- if (includeFiles) {
581
- results = await Promise.all(
582
- results.map(async (model) => {
583
- try {
584
- const details = await fetchHFModelDetails(model.id)
585
- const siblings = details.siblings || []
586
-
587
- if (config.modelKind === 'gguf') {
588
- const ggufFiles = siblings
589
- .filter((file) => config.filePattern?.test(file.rfilename))
590
- .map((file) => ({
591
- filename: file.rfilename,
592
- size: file.size,
593
- }))
594
- return { ...model, files: ggufFiles }
595
- } else {
596
- // ONNX models
597
- const quantTypes = detectOnnxQuantizationTypes(siblings)
598
- const speakerFiles =
599
- generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
600
-
601
- return {
602
- ...model,
603
- quantization_types: quantTypes,
604
- ...(speakerFiles.length > 0 && {
605
- speaker_embed_files: speakerFiles.map((f) => ({
606
- filename: f.rfilename,
607
- size: f.size,
608
- })),
609
- }),
610
- }
611
- }
612
- } catch {
613
- return model
614
- }
615
- }),
616
- )
617
- }
618
-
619
- return {
620
- content: [
621
- {
622
- type: 'text',
623
- text: TOON.encode({
624
- count: results.length,
625
- generatorType,
626
- modelKind: config.modelKind,
627
- models: results,
628
- hf_token_configured: !!HF_TOKEN,
629
- }),
630
- },
631
- ],
632
- }
633
- } catch (err: any) {
634
- return {
635
- content: [{ type: 'text', text: `Failed to search models: ${err.message}` }],
636
- }
637
- }
638
- },
639
- )
640
-
641
- // Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
642
- const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
643
-
644
- server.tool(
645
- 'huggingface_select',
646
- {
647
- generatorType: z
648
- .enum([
649
- 'GeneratorLLM',
650
- 'GeneratorVectorStore',
651
- 'GeneratorReranker',
652
- 'GeneratorGGMLTTS',
653
- 'GeneratorGGMLTTSVocoder',
654
- 'GeneratorOnnxLLM',
655
- 'GeneratorOnnxSTT',
656
- 'GeneratorTTS',
657
- ])
658
- .describe('Generator type for model selection')
659
- .default('GeneratorLLM'),
660
- // eslint-disable-next-line camelcase
661
- model_id: z
662
- .string()
663
- .describe('Hugging Face model ID (e.g., "unsloth/Llama-3.2-1B-Instruct-GGUF")'),
664
- filename: z.string().describe('Model filename to select (required for GGUF models)').optional(),
665
- quantize_type: z
666
- .string()
667
- .describe('Quantization type for ONNX models (e.g., "q8", "fp16", "auto")')
668
- .optional()
669
- .default('auto'),
670
- speaker_embed_file: z.string().describe('Speaker embedding file for TTS models').optional(),
671
- },
672
- // eslint-disable-next-line camelcase
673
- async ({
674
- generatorType,
675
- model_id: modelId,
676
- filename,
677
- quantize_type: quantizeType,
678
- speaker_embed_file: speakerEmbedFile,
679
- }) => {
680
- try {
681
- const config = generatorConfigs[generatorType]
682
- const details = await fetchHFModelDetails(modelId)
683
- const siblings = details.siblings || []
684
-
685
- // Handle ONNX models
686
- // ONNX generators expect: model (HF model ID), modelType, quantizeType
687
- if (config.modelKind === 'onnx') {
688
- const quantTypes = detectOnnxQuantizationTypes(siblings)
689
- const speakerFiles = generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
690
- const selectedSpeakerFile = speakerEmbedFile
691
- ? siblings.find((f) => f.rfilename === speakerEmbedFile)
692
- : undefined
693
-
694
- const selectedQuantType = quantTypes.includes(quantizeType || 'auto')
695
- ? quantizeType
696
- : 'auto'
697
- const modelType = details.config?.model_type || details.cardData?.model_type
698
-
699
- // Result format matches ONNX generator property names (camelCase)
700
- const result = {
701
- // Primary model fields for generator
702
- model: modelId,
703
- modelType,
704
- quantizeType: selectedQuantType,
705
- // Speaker embedding for TTS generators
706
- ...(selectedSpeakerFile && {
707
- speakerEmbedUrl: `https://huggingface.co/${modelId}/resolve/main/${selectedSpeakerFile.rfilename}?download=true`,
708
- speakerEmbedHash: selectedSpeakerFile.lfs?.sha256,
709
- speakerEmbedHashType: 'sha256',
710
- }),
711
- // Additional info
712
- availableQuantizeTypes: quantTypes,
713
- ...(speakerFiles.length > 0 && {
714
- availableSpeakerEmbedFiles: speakerFiles.map((f) => f.rfilename),
715
- }),
716
- _hfRepoInfo: {
717
- repo: modelId,
718
- model: {
719
- id: details.id,
720
- downloads: details.downloads,
721
- likes: details.likes,
722
- author: details.author,
723
- },
724
- generatorType,
725
- modelType,
726
- quantizeType: selectedQuantType,
727
- ...(selectedSpeakerFile && {
728
- speakerEmbedFile: selectedSpeakerFile.rfilename,
729
- }),
730
- },
731
- }
732
-
733
- // Return JSON for huggingface_select to allow direct parsing by consumers
734
- return {
735
- content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
736
- }
737
- }
738
-
739
- // Handle GGUF models
740
- if (!filename) {
741
- // List available GGUF files
742
- const ggufFiles = siblings
743
- .filter((file) => config.filePattern?.test(file.rfilename))
744
- .map((file) => ({
745
- filename: file.rfilename,
746
- size: file.size,
747
- }))
748
-
749
- // Return JSON for huggingface_select to allow direct parsing by consumers
750
- return {
751
- content: [
752
- {
753
- type: 'text',
754
- text: JSON.stringify(
755
- {
756
- error: 'filename is required for GGUF models',
757
- available_files: ggufFiles,
758
- },
759
- null,
760
- 2,
761
- ),
762
- },
763
- ],
764
- }
765
- }
766
-
767
- // Find the selected file
768
- const selectedFile = siblings.find((f) => f.rfilename === filename)
769
- if (!selectedFile) {
770
- return {
771
- content: [{ type: 'text', text: `File "${filename}" not found in model ${modelId}` }],
772
- }
773
- }
774
-
775
- // Check if it's a split file
776
- const matched = filename.match(ggufSplitPattern)
777
- const isSplit = !!matched
778
-
779
- // Find mmproj file if available (only for LLM generators)
780
- const mmprojFile =
781
- generatorType === 'GeneratorLLM'
782
- ? siblings.find((f) => /^mmproj-/i.test(f.rfilename))
783
- : undefined
784
-
785
- // Extract GGUF metadata (for split files, metadata is in the first split)
786
- const metadataFilename = isSplit
787
- ? filename.replace(ggufSplitPattern, '-00001-of-$2.gguf')
788
- : filename
789
- const ggufUrl = `https://huggingface.co/${modelId}/resolve/main/${metadataFilename}`
790
- const ggufSimplifiedMetadata = await extractGGUFMetadata(ggufUrl)
791
-
792
- if (isSplit) {
793
- const [, , splitTotal] = matched!
794
- const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
795
- const split = String(i + 1).padStart(5, '0')
796
- const splitRFilename = filename.replace(
797
- ggufSplitPattern,
798
- `-${split}-of-${splitTotal}.gguf`,
799
- )
800
- const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
801
- return {
802
- rfilename: splitRFilename,
803
- size: sibling?.size,
804
- lfs: sibling?.lfs,
805
- }
806
- })
807
-
808
- const first = splitFiles[0]
809
- const rest = splitFiles.slice(1)
810
-
811
- const result = {
812
- url: `https://huggingface.co/${modelId}/resolve/main/${first.rfilename}?download=true`,
813
- hash: first.lfs?.sha256,
814
- hash_type: 'sha256',
815
- _ggufSplitFiles: rest.map((split) => ({
816
- url: `https://huggingface.co/${modelId}/resolve/main/${split.rfilename}?download=true`,
817
- hash: split.lfs?.sha256,
818
- hash_type: 'sha256',
819
- })),
820
- ...(mmprojFile && {
821
- mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
822
- mmproj_hash: mmprojFile.lfs?.sha256,
823
- mmproj_hash_type: 'sha256',
824
- }),
825
- _hfRepoInfo: {
826
- repo: modelId,
827
- model: {
828
- id: details.id,
829
- downloads: details.downloads,
830
- likes: details.likes,
831
- author: details.author,
832
- },
833
- generatorType,
834
- isSplit: true,
835
- files: splitFiles.map((f) => f.rfilename),
836
- sizes: splitFiles.map((f) => f.size),
837
- size: splitFiles.reduce((acc, f) => acc + (f.size ?? 0), 0),
838
- ...(mmprojFile && {
839
- mmprojFile: mmprojFile.rfilename,
840
- mmprojSize: mmprojFile.size,
841
- }),
842
- ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
843
- },
844
- }
845
-
846
- // Return JSON for huggingface_select to allow direct parsing by consumers
847
- return {
848
- content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
849
- }
850
- } else {
851
- const result = {
852
- url: `https://huggingface.co/${modelId}/resolve/main/${filename}?download=true`,
853
- hash: selectedFile.lfs?.sha256,
854
- hash_type: 'sha256',
855
- ...(mmprojFile && {
856
- mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
857
- mmproj_hash: mmprojFile.lfs?.sha256,
858
- mmproj_hash_type: 'sha256',
859
- }),
860
- _hfRepoInfo: {
861
- repo: modelId,
862
- model: {
863
- id: details.id,
864
- downloads: details.downloads,
865
- likes: details.likes,
866
- author: details.author,
867
- },
868
- generatorType,
869
- file: filename,
870
- size: selectedFile.size,
871
- ...(mmprojFile && {
872
- mmprojFile: mmprojFile.rfilename,
873
- mmprojSize: mmprojFile.size,
874
- }),
875
- ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
876
- },
877
- }
878
-
879
- // Return JSON for huggingface_select to allow direct parsing by consumers
880
- return {
881
- content: [
882
- {
883
- type: 'text',
884
- text: `${JSON5.stringify(result, null, 2)} // Please use _hfRepoInfo undocumented field with @ts-ignore`,
885
- },
886
- ],
887
- }
888
- }
889
- } catch (err: any) {
890
- return {
891
- content: [{ type: 'text', text: `Failed to select model: ${err.message}` }],
892
- }
893
- }
894
- },
895
- )
21
+ registerCompile(server, projectDir)
22
+ registerLottie(server)
23
+ registerIcons(server)
24
+ registerHuggingface(server)
25
+ registerMedia(server, projectDir)
896
26
 
897
27
  const transport = new StdioServerTransport()
898
28
  await server.connect(transport)