@fugood/bricks-project 2.23.2 → 2.23.4

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.
Files changed (95) hide show
  1. package/compile/index.ts +343 -125
  2. package/package.json +4 -2
  3. package/skills/bricks-project/rules/automations.md +7 -7
  4. package/tools/deploy.ts +39 -10
  5. package/tools/mcp-server.ts +10 -877
  6. package/tools/mcp-tools/compile.ts +91 -0
  7. package/tools/mcp-tools/huggingface.ts +653 -0
  8. package/tools/mcp-tools/icons.ts +60 -0
  9. package/tools/mcp-tools/lottie.ts +102 -0
  10. package/tools/mcp-tools/media.ts +110 -0
  11. package/tools/postinstall.ts +11 -6
  12. package/tools/pull.ts +25 -9
  13. package/tsconfig.json +16 -0
  14. package/types/bricks/Camera.ts +1 -1
  15. package/types/bricks/Chart.ts +1 -1
  16. package/types/bricks/GenerativeMedia.ts +1 -1
  17. package/types/bricks/Icon.ts +1 -1
  18. package/types/bricks/Image.ts +1 -1
  19. package/types/bricks/Items.ts +1 -1
  20. package/types/bricks/Lottie.ts +1 -1
  21. package/types/bricks/Maps.ts +1 -1
  22. package/types/bricks/QrCode.ts +1 -1
  23. package/types/bricks/Rect.ts +1 -1
  24. package/types/bricks/RichText.ts +1 -1
  25. package/types/bricks/Rive.ts +1 -1
  26. package/types/bricks/Slideshow.ts +1 -1
  27. package/types/bricks/Svg.ts +1 -1
  28. package/types/bricks/Text.ts +1 -1
  29. package/types/bricks/TextInput.ts +1 -1
  30. package/types/bricks/Video.ts +1 -1
  31. package/types/bricks/VideoStreaming.ts +1 -1
  32. package/types/bricks/WebRtcStream.ts +1 -1
  33. package/types/bricks/WebView.ts +1 -1
  34. package/types/canvas.ts +2 -2
  35. package/types/common.ts +4 -4
  36. package/types/generators/AlarmClock.ts +1 -1
  37. package/types/generators/Assistant.ts +1 -1
  38. package/types/generators/BleCentral.ts +1 -1
  39. package/types/generators/BlePeripheral.ts +1 -1
  40. package/types/generators/CanvasMap.ts +1 -1
  41. package/types/generators/CastlesPay.ts +1 -1
  42. package/types/generators/DataBank.ts +1 -1
  43. package/types/generators/File.ts +1 -1
  44. package/types/generators/GraphQl.ts +1 -1
  45. package/types/generators/Http.ts +1 -1
  46. package/types/generators/HttpServer.ts +1 -1
  47. package/types/generators/Information.ts +1 -1
  48. package/types/generators/Intent.ts +1 -1
  49. package/types/generators/Iterator.ts +1 -1
  50. package/types/generators/Keyboard.ts +1 -1
  51. package/types/generators/LlmAnthropicCompat.ts +1 -1
  52. package/types/generators/LlmAppleBuiltin.ts +1 -1
  53. package/types/generators/LlmGgml.ts +1 -1
  54. package/types/generators/LlmOnnx.ts +1 -1
  55. package/types/generators/LlmOpenAiCompat.ts +1 -1
  56. package/types/generators/LlmQualcommAiEngine.ts +1 -1
  57. package/types/generators/Mcp.ts +1 -1
  58. package/types/generators/McpServer.ts +1 -1
  59. package/types/generators/MediaFlow.ts +1 -1
  60. package/types/generators/MqttBroker.ts +1 -1
  61. package/types/generators/MqttClient.ts +1 -1
  62. package/types/generators/Question.ts +1 -1
  63. package/types/generators/RealtimeTranscription.ts +1 -1
  64. package/types/generators/RerankerGgml.ts +1 -1
  65. package/types/generators/SerialPort.ts +1 -1
  66. package/types/generators/SoundPlayer.ts +1 -1
  67. package/types/generators/SoundRecorder.ts +1 -1
  68. package/types/generators/SpeechToTextGgml.ts +1 -1
  69. package/types/generators/SpeechToTextOnnx.ts +1 -1
  70. package/types/generators/SpeechToTextPlatform.ts +1 -1
  71. package/types/generators/SqLite.ts +1 -1
  72. package/types/generators/Step.ts +1 -1
  73. package/types/generators/SttAppleBuiltin.ts +1 -1
  74. package/types/generators/Tcp.ts +1 -1
  75. package/types/generators/TcpServer.ts +1 -1
  76. package/types/generators/TextToSpeechAppleBuiltin.ts +1 -1
  77. package/types/generators/TextToSpeechGgml.ts +1 -1
  78. package/types/generators/TextToSpeechOnnx.ts +1 -1
  79. package/types/generators/TextToSpeechOpenAiLike.ts +1 -1
  80. package/types/generators/ThermalPrinter.ts +1 -1
  81. package/types/generators/Tick.ts +1 -1
  82. package/types/generators/Udp.ts +1 -1
  83. package/types/generators/VadGgml.ts +1 -1
  84. package/types/generators/VadOnnx.ts +1 -1
  85. package/types/generators/VadTraditional.ts +1 -1
  86. package/types/generators/VectorStore.ts +1 -1
  87. package/types/generators/Watchdog.ts +1 -1
  88. package/types/generators/WebCrawler.ts +1 -1
  89. package/types/generators/WebRtc.ts +1 -1
  90. package/types/generators/WebSocket.ts +1 -1
  91. package/types/system.ts +1 -1
  92. package/utils/calc.ts +16 -10
  93. package/utils/id.ts +4 -0
  94. package/api/index.ts +0 -1
  95. package/api/instance.ts +0 -213
@@ -0,0 +1,653 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import { JSON5 } from 'bun'
4
+ import * as TOON from '@toon-format/toon'
5
+ import { gguf } from '@huggingface/gguf'
6
+
7
+ // Hugging Face API configuration
8
+ const HF_API_URL = 'https://huggingface.co/api'
9
+ const { HF_TOKEN } = process.env
10
+
11
+ // Helper function to convert BigInt to number for JSON serialization
12
+ const convertBigIntToNumber = (value: unknown): number | unknown => {
13
+ if (typeof value === 'bigint') {
14
+ return Number(value)
15
+ }
16
+ return value
17
+ }
18
+
19
+ // Extract GGUF metadata from a GGUF file URL with 10s timeout
20
+ const extractGGUFMetadata = async (url: string) => {
21
+ const timeoutPromise = new Promise<never>((_, reject) =>
22
+ setTimeout(() => reject(new Error('GGUF metadata extraction timeout')), 10000),
23
+ )
24
+
25
+ try {
26
+ const { metadata } = (await Promise.race([gguf(url), timeoutPromise])) as { metadata: any }
27
+ const architecture = metadata['general.architecture']
28
+
29
+ const ggufSimplifiedMetadata: Record<string, unknown> = {
30
+ name: metadata['general.name'],
31
+ size_label: metadata['general.size_label'],
32
+ basename: metadata['general.basename'],
33
+ architecture: metadata['general.architecture'],
34
+ file_type: convertBigIntToNumber(metadata['general.file_type']),
35
+ quantization_version: convertBigIntToNumber(metadata['general.quantization_version']),
36
+ }
37
+
38
+ if (!architecture) return ggufSimplifiedMetadata
39
+
40
+ // Helper to add converted value if defined
41
+ const addIfDefined = (target: Record<string, unknown>, key: string, value: unknown) => {
42
+ if (value !== undefined) target[key] = convertBigIntToNumber(value)
43
+ }
44
+
45
+ // Architecture-specific transformer parameters
46
+ addIfDefined(ggufSimplifiedMetadata, 'n_ctx_train', metadata[`${architecture}.context_length`])
47
+ addIfDefined(ggufSimplifiedMetadata, 'n_layer', metadata[`${architecture}.block_count`])
48
+ addIfDefined(ggufSimplifiedMetadata, 'n_embd', metadata[`${architecture}.embedding_length`])
49
+ addIfDefined(ggufSimplifiedMetadata, 'n_head', metadata[`${architecture}.attention.head_count`])
50
+ addIfDefined(
51
+ ggufSimplifiedMetadata,
52
+ 'n_head_kv',
53
+ metadata[`${architecture}.attention.head_count_kv`],
54
+ )
55
+ addIfDefined(
56
+ ggufSimplifiedMetadata,
57
+ 'n_embd_head_k',
58
+ metadata[`${architecture}.attention.key_length`],
59
+ )
60
+ addIfDefined(
61
+ ggufSimplifiedMetadata,
62
+ 'n_embd_head_v',
63
+ metadata[`${architecture}.attention.value_length`],
64
+ )
65
+ addIfDefined(
66
+ ggufSimplifiedMetadata,
67
+ 'n_swa',
68
+ metadata[`${architecture}.attention.sliding_window`],
69
+ )
70
+
71
+ // SSM (Mamba) parameters for recurrent/hybrid models
72
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_d_conv', metadata[`${architecture}.ssm.conv_kernel`])
73
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_d_state', metadata[`${architecture}.ssm.state_size`])
74
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_d_inner', metadata[`${architecture}.ssm.inner_size`])
75
+ addIfDefined(ggufSimplifiedMetadata, 'ssm_n_group', metadata[`${architecture}.ssm.group_count`])
76
+ addIfDefined(
77
+ ggufSimplifiedMetadata,
78
+ 'ssm_dt_rank',
79
+ metadata[`${architecture}.ssm.time_step_rank`],
80
+ )
81
+
82
+ // RWKV parameters
83
+ addIfDefined(
84
+ ggufSimplifiedMetadata,
85
+ 'rwkv_head_size',
86
+ metadata[`${architecture}.rwkv.head_size`],
87
+ )
88
+ addIfDefined(
89
+ ggufSimplifiedMetadata,
90
+ 'rwkv_token_shift_count',
91
+ metadata[`${architecture}.rwkv.token_shift_count`],
92
+ )
93
+
94
+ return ggufSimplifiedMetadata
95
+ } catch (error) {
96
+ console.error('Failed to extract GGUF metadata:', error)
97
+ return null
98
+ }
99
+ }
100
+
101
+ type HFSibling = {
102
+ rfilename: string
103
+ size?: number
104
+ lfs?: { sha256?: string }
105
+ }
106
+
107
+ type HFModel = {
108
+ id: string
109
+ author?: string
110
+ downloads?: number
111
+ likes?: number
112
+ tags?: string[]
113
+ pipeline_tag?: string
114
+ siblings?: HFSibling[]
115
+ config?: { model_type?: string }
116
+ cardData?: { model_type?: string }
117
+ }
118
+
119
+ const supportedLlmTasks = [
120
+ 'text-generation',
121
+ 'image-text-to-text',
122
+ 'text2text-generation',
123
+ 'conversational',
124
+ ]
125
+
126
+ type GeneratorType =
127
+ | 'GeneratorLLM'
128
+ | 'GeneratorVectorStore'
129
+ | 'GeneratorReranker'
130
+ | 'GeneratorGGMLTTS'
131
+ | 'GeneratorGGMLTTSVocoder'
132
+ | 'GeneratorOnnxLLM'
133
+ | 'GeneratorOnnxSTT'
134
+ | 'GeneratorTTS'
135
+
136
+ type ModelKind = 'gguf' | 'onnx'
137
+
138
+ interface GeneratorConfig {
139
+ modelKind: ModelKind
140
+ filter: string
141
+ taskFilter?: string[]
142
+ filePattern?: RegExp
143
+ hasValidStructure?: (siblings: HFSibling[]) => boolean
144
+ }
145
+
146
+ // Helper to check valid ONNX structure
147
+ const hasValidOnnxStructure = (siblings: HFSibling[]): boolean => {
148
+ const hasConfigJson = siblings.some((file) => file.rfilename === 'config.json')
149
+ const hasOnnxModel = siblings.some(
150
+ (file) => file.rfilename.includes('onnx/') && file.rfilename.endsWith('.onnx'),
151
+ )
152
+ return hasConfigJson && hasOnnxModel
153
+ }
154
+
155
+ // Detect quantization types from ONNX files
156
+ const detectOnnxQuantizationTypes = (siblings: HFSibling[]): string[] => {
157
+ const onnxFiles = siblings.filter((file) => file.rfilename.endsWith('.onnx'))
158
+ const quantTypes = new Set<string>()
159
+
160
+ onnxFiles.forEach((file) => {
161
+ const filename = file.rfilename
162
+ if (!filename.endsWith('.onnx')) return
163
+ const postfix = /_(q8|q4|q4f16|fp16|int8|int4|uint8|bnb4|quantized)\.onnx$/.exec(filename)?.[1]
164
+ if (!postfix) {
165
+ quantTypes.add('auto')
166
+ quantTypes.add('none')
167
+ } else {
168
+ quantTypes.add(postfix === 'quantized' ? 'q8' : postfix)
169
+ }
170
+ })
171
+
172
+ return Array.from(quantTypes)
173
+ }
174
+
175
+ // Find speaker embedding files for TTS models
176
+ const findSpeakerEmbedFiles = (siblings: HFSibling[]): HFSibling[] =>
177
+ siblings.filter((file) => file.rfilename.startsWith('voices/') && file.rfilename.endsWith('.bin'))
178
+
179
+ const generatorConfigs: Record<GeneratorType, GeneratorConfig> = {
180
+ GeneratorLLM: {
181
+ modelKind: 'gguf',
182
+ filter: 'gguf',
183
+ taskFilter: supportedLlmTasks,
184
+ filePattern: /\.gguf$/,
185
+ },
186
+ GeneratorVectorStore: {
187
+ modelKind: 'gguf',
188
+ filter: 'gguf',
189
+ filePattern: /\.gguf$/,
190
+ },
191
+ GeneratorReranker: {
192
+ modelKind: 'gguf',
193
+ filter: 'gguf,reranker',
194
+ filePattern: /\.gguf$/,
195
+ },
196
+ GeneratorGGMLTTS: {
197
+ modelKind: 'gguf',
198
+ filter: 'gguf,text-to-speech',
199
+ filePattern: /\.gguf$/,
200
+ },
201
+ GeneratorGGMLTTSVocoder: {
202
+ modelKind: 'gguf',
203
+ filter: 'gguf,feature-extraction',
204
+ filePattern: /\.gguf$/,
205
+ },
206
+ GeneratorOnnxLLM: {
207
+ modelKind: 'onnx',
208
+ filter: 'onnx',
209
+ taskFilter: supportedLlmTasks,
210
+ hasValidStructure: hasValidOnnxStructure,
211
+ },
212
+ GeneratorOnnxSTT: {
213
+ modelKind: 'onnx',
214
+ filter: 'onnx,automatic-speech-recognition',
215
+ hasValidStructure: hasValidOnnxStructure,
216
+ },
217
+ GeneratorTTS: {
218
+ modelKind: 'onnx',
219
+ filter: 'onnx,text-to-speech',
220
+ hasValidStructure: hasValidOnnxStructure,
221
+ },
222
+ }
223
+
224
+ const searchHFModels = async (filter: string, search?: string, limit = 50): Promise<HFModel[]> => {
225
+ const params = new URLSearchParams({
226
+ limit: String(limit),
227
+ full: 'true',
228
+ config: 'true',
229
+ sort: 'likes',
230
+ direction: '-1',
231
+ })
232
+ if (filter) params.set('filter', filter)
233
+ if (search) params.set('search', search)
234
+
235
+ const headers: Record<string, string> = {}
236
+ if (HF_TOKEN) {
237
+ headers['Authorization'] = `Bearer ${HF_TOKEN}`
238
+ }
239
+
240
+ const response = await fetch(`${HF_API_URL}/models?${params.toString()}`, { headers })
241
+ if (!response.ok) {
242
+ throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
243
+ }
244
+ return response.json()
245
+ }
246
+
247
+ const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
248
+ const params = new URLSearchParams({ blobs: 'true' })
249
+
250
+ const headers: Record<string, string> = {}
251
+ if (HF_TOKEN) {
252
+ headers['Authorization'] = `Bearer ${HF_TOKEN}`
253
+ }
254
+
255
+ const response = await fetch(`${HF_API_URL}/models/${modelId}?${params.toString()}`, { headers })
256
+ if (!response.ok) {
257
+ throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
258
+ }
259
+ return response.json()
260
+ }
261
+
262
+ // Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
263
+ const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
264
+
265
+ export function register(server: McpServer) {
266
+ server.tool(
267
+ 'huggingface_search',
268
+ {
269
+ generatorType: z
270
+ .enum([
271
+ 'GeneratorLLM',
272
+ 'GeneratorVectorStore',
273
+ 'GeneratorReranker',
274
+ 'GeneratorGGMLTTS',
275
+ 'GeneratorGGMLTTSVocoder',
276
+ 'GeneratorOnnxLLM',
277
+ 'GeneratorOnnxSTT',
278
+ 'GeneratorTTS',
279
+ ])
280
+ .describe('Generator type to search models for')
281
+ .default('GeneratorLLM'),
282
+ query: z.string().describe('Search keywords for models on Hugging Face').optional(),
283
+ limit: z.number().min(1).max(100).optional().default(20),
284
+ includeFiles: z
285
+ .boolean()
286
+ .optional()
287
+ .default(false)
288
+ .describe('Include list of model files (requires additional API calls)'),
289
+ },
290
+ async ({ generatorType, query, limit, includeFiles }) => {
291
+ try {
292
+ const config = generatorConfigs[generatorType]
293
+ const models = await searchHFModels(config.filter, query, limit)
294
+
295
+ // Filter models based on generator configuration
296
+ const filteredModels = models.filter((model) => {
297
+ const modelTags = model.tags || []
298
+
299
+ // Check task filter if configured
300
+ if (config.taskFilter && !config.taskFilter.some((t) => modelTags.includes(t))) {
301
+ return false
302
+ }
303
+
304
+ // Check structure validation for ONNX models
305
+ if (config.hasValidStructure && model.siblings) {
306
+ if (!config.hasValidStructure(model.siblings)) {
307
+ return false
308
+ }
309
+ }
310
+
311
+ return true
312
+ })
313
+
314
+ // Build result models
315
+ let results: Array<{
316
+ id: string
317
+ author?: string
318
+ downloads?: number
319
+ likes?: number
320
+ pipeline_tag?: string
321
+ model_type?: string
322
+ model_kind: ModelKind
323
+ files?: Array<{ filename: string; size?: number }>
324
+ quantization_types?: string[]
325
+ speaker_embed_files?: Array<{ filename: string; size?: number }>
326
+ }> = filteredModels.map((model) => ({
327
+ id: model.id,
328
+ author: model.author,
329
+ downloads: model.downloads,
330
+ likes: model.likes,
331
+ pipeline_tag: model.pipeline_tag,
332
+ model_type: model.config?.model_type || model.cardData?.model_type,
333
+ model_kind: config.modelKind,
334
+ }))
335
+
336
+ if (includeFiles) {
337
+ results = await Promise.all(
338
+ results.map(async (model) => {
339
+ try {
340
+ const details = await fetchHFModelDetails(model.id)
341
+ const siblings = details.siblings || []
342
+
343
+ if (config.modelKind === 'gguf') {
344
+ const ggufFiles = siblings
345
+ .filter((file) => config.filePattern?.test(file.rfilename))
346
+ .map((file) => ({
347
+ filename: file.rfilename,
348
+ size: file.size,
349
+ }))
350
+ return { ...model, files: ggufFiles }
351
+ } else {
352
+ // ONNX models
353
+ const quantTypes = detectOnnxQuantizationTypes(siblings)
354
+ const speakerFiles =
355
+ generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
356
+
357
+ return {
358
+ ...model,
359
+ quantization_types: quantTypes,
360
+ ...(speakerFiles.length > 0 && {
361
+ speaker_embed_files: speakerFiles.map((f) => ({
362
+ filename: f.rfilename,
363
+ size: f.size,
364
+ })),
365
+ }),
366
+ }
367
+ }
368
+ } catch {
369
+ return model
370
+ }
371
+ }),
372
+ )
373
+ }
374
+
375
+ return {
376
+ content: [
377
+ {
378
+ type: 'text',
379
+ text: TOON.encode({
380
+ count: results.length,
381
+ generatorType,
382
+ modelKind: config.modelKind,
383
+ models: results,
384
+ hf_token_configured: !!HF_TOKEN,
385
+ }),
386
+ },
387
+ ],
388
+ }
389
+ } catch (err: any) {
390
+ return {
391
+ content: [{ type: 'text', text: `Failed to search models: ${err.message}` }],
392
+ }
393
+ }
394
+ },
395
+ )
396
+
397
+ server.tool(
398
+ 'huggingface_select',
399
+ {
400
+ generatorType: z
401
+ .enum([
402
+ 'GeneratorLLM',
403
+ 'GeneratorVectorStore',
404
+ 'GeneratorReranker',
405
+ 'GeneratorGGMLTTS',
406
+ 'GeneratorGGMLTTSVocoder',
407
+ 'GeneratorOnnxLLM',
408
+ 'GeneratorOnnxSTT',
409
+ 'GeneratorTTS',
410
+ ])
411
+ .describe('Generator type for model selection')
412
+ .default('GeneratorLLM'),
413
+ // eslint-disable-next-line camelcase
414
+ model_id: z
415
+ .string()
416
+ .describe('Hugging Face model ID (e.g., "unsloth/Llama-3.2-1B-Instruct-GGUF")'),
417
+ filename: z
418
+ .string()
419
+ .describe('Model filename to select (required for GGUF models)')
420
+ .optional(),
421
+ quantize_type: z
422
+ .string()
423
+ .describe('Quantization type for ONNX models (e.g., "q8", "fp16", "auto")')
424
+ .optional()
425
+ .default('auto'),
426
+ speaker_embed_file: z.string().describe('Speaker embedding file for TTS models').optional(),
427
+ },
428
+ // eslint-disable-next-line camelcase
429
+ async ({
430
+ generatorType,
431
+ model_id: modelId,
432
+ filename,
433
+ quantize_type: quantizeType,
434
+ speaker_embed_file: speakerEmbedFile,
435
+ }) => {
436
+ try {
437
+ const config = generatorConfigs[generatorType]
438
+ const details = await fetchHFModelDetails(modelId)
439
+ const siblings = details.siblings || []
440
+
441
+ // Handle ONNX models
442
+ // ONNX generators expect: model (HF model ID), modelType, quantizeType
443
+ if (config.modelKind === 'onnx') {
444
+ const quantTypes = detectOnnxQuantizationTypes(siblings)
445
+ const speakerFiles =
446
+ generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
447
+ const selectedSpeakerFile = speakerEmbedFile
448
+ ? siblings.find((f) => f.rfilename === speakerEmbedFile)
449
+ : undefined
450
+
451
+ const selectedQuantType = quantTypes.includes(quantizeType || 'auto')
452
+ ? quantizeType
453
+ : 'auto'
454
+ const modelType = details.config?.model_type || details.cardData?.model_type
455
+
456
+ // Result format matches ONNX generator property names (camelCase)
457
+ const result = {
458
+ // Primary model fields for generator
459
+ model: modelId,
460
+ modelType,
461
+ quantizeType: selectedQuantType,
462
+ // Speaker embedding for TTS generators
463
+ ...(selectedSpeakerFile && {
464
+ speakerEmbedUrl: `https://huggingface.co/${modelId}/resolve/main/${selectedSpeakerFile.rfilename}?download=true`,
465
+ speakerEmbedHash: selectedSpeakerFile.lfs?.sha256,
466
+ speakerEmbedHashType: 'sha256',
467
+ }),
468
+ // Additional info
469
+ availableQuantizeTypes: quantTypes,
470
+ ...(speakerFiles.length > 0 && {
471
+ availableSpeakerEmbedFiles: speakerFiles.map((f) => f.rfilename),
472
+ }),
473
+ _hfRepoInfo: {
474
+ repo: modelId,
475
+ model: {
476
+ id: details.id,
477
+ downloads: details.downloads,
478
+ likes: details.likes,
479
+ author: details.author,
480
+ },
481
+ generatorType,
482
+ modelType,
483
+ quantizeType: selectedQuantType,
484
+ ...(selectedSpeakerFile && {
485
+ speakerEmbedFile: selectedSpeakerFile.rfilename,
486
+ }),
487
+ },
488
+ }
489
+
490
+ // Return JSON for huggingface_select to allow direct parsing by consumers
491
+ return {
492
+ content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
493
+ }
494
+ }
495
+
496
+ // Handle GGUF models
497
+ if (!filename) {
498
+ // List available GGUF files
499
+ const ggufFiles = siblings
500
+ .filter((file) => config.filePattern?.test(file.rfilename))
501
+ .map((file) => ({
502
+ filename: file.rfilename,
503
+ size: file.size,
504
+ }))
505
+
506
+ // Return JSON for huggingface_select to allow direct parsing by consumers
507
+ return {
508
+ content: [
509
+ {
510
+ type: 'text',
511
+ text: JSON.stringify(
512
+ {
513
+ error: 'filename is required for GGUF models',
514
+ available_files: ggufFiles,
515
+ },
516
+ null,
517
+ 2,
518
+ ),
519
+ },
520
+ ],
521
+ }
522
+ }
523
+
524
+ // Find the selected file
525
+ const selectedFile = siblings.find((f) => f.rfilename === filename)
526
+ if (!selectedFile) {
527
+ return {
528
+ content: [{ type: 'text', text: `File "${filename}" not found in model ${modelId}` }],
529
+ }
530
+ }
531
+
532
+ // Check if it's a split file
533
+ const matched = filename.match(ggufSplitPattern)
534
+ const isSplit = !!matched
535
+
536
+ // Find mmproj file if available (only for LLM generators)
537
+ const mmprojFile =
538
+ generatorType === 'GeneratorLLM'
539
+ ? siblings.find((f) => /^mmproj-/i.test(f.rfilename))
540
+ : undefined
541
+
542
+ // Extract GGUF metadata (for split files, metadata is in the first split)
543
+ const metadataFilename = isSplit
544
+ ? filename.replace(ggufSplitPattern, '-00001-of-$2.gguf')
545
+ : filename
546
+ const ggufUrl = `https://huggingface.co/${modelId}/resolve/main/${metadataFilename}`
547
+ const ggufSimplifiedMetadata = await extractGGUFMetadata(ggufUrl)
548
+
549
+ if (isSplit) {
550
+ const [, , splitTotal] = matched!
551
+ const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
552
+ const split = String(i + 1).padStart(5, '0')
553
+ const splitRFilename = filename.replace(
554
+ ggufSplitPattern,
555
+ `-${split}-of-${splitTotal}.gguf`,
556
+ )
557
+ const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
558
+ return {
559
+ rfilename: splitRFilename,
560
+ size: sibling?.size,
561
+ lfs: sibling?.lfs,
562
+ }
563
+ })
564
+
565
+ const first = splitFiles[0]
566
+ const rest = splitFiles.slice(1)
567
+
568
+ const result = {
569
+ url: `https://huggingface.co/${modelId}/resolve/main/${first.rfilename}?download=true`,
570
+ hash: first.lfs?.sha256,
571
+ hash_type: 'sha256',
572
+ _ggufSplitFiles: rest.map((split) => ({
573
+ url: `https://huggingface.co/${modelId}/resolve/main/${split.rfilename}?download=true`,
574
+ hash: split.lfs?.sha256,
575
+ hash_type: 'sha256',
576
+ })),
577
+ ...(mmprojFile && {
578
+ mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
579
+ mmproj_hash: mmprojFile.lfs?.sha256,
580
+ mmproj_hash_type: 'sha256',
581
+ }),
582
+ _hfRepoInfo: {
583
+ repo: modelId,
584
+ model: {
585
+ id: details.id,
586
+ downloads: details.downloads,
587
+ likes: details.likes,
588
+ author: details.author,
589
+ },
590
+ generatorType,
591
+ isSplit: true,
592
+ files: splitFiles.map((f) => f.rfilename),
593
+ sizes: splitFiles.map((f) => f.size),
594
+ size: splitFiles.reduce((acc, f) => acc + (f.size ?? 0), 0),
595
+ ...(mmprojFile && {
596
+ mmprojFile: mmprojFile.rfilename,
597
+ mmprojSize: mmprojFile.size,
598
+ }),
599
+ ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
600
+ },
601
+ }
602
+
603
+ // Return JSON for huggingface_select to allow direct parsing by consumers
604
+ return {
605
+ content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
606
+ }
607
+ } else {
608
+ const result = {
609
+ url: `https://huggingface.co/${modelId}/resolve/main/${filename}?download=true`,
610
+ hash: selectedFile.lfs?.sha256,
611
+ hash_type: 'sha256',
612
+ ...(mmprojFile && {
613
+ mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
614
+ mmproj_hash: mmprojFile.lfs?.sha256,
615
+ mmproj_hash_type: 'sha256',
616
+ }),
617
+ _hfRepoInfo: {
618
+ repo: modelId,
619
+ model: {
620
+ id: details.id,
621
+ downloads: details.downloads,
622
+ likes: details.likes,
623
+ author: details.author,
624
+ },
625
+ generatorType,
626
+ file: filename,
627
+ size: selectedFile.size,
628
+ ...(mmprojFile && {
629
+ mmprojFile: mmprojFile.rfilename,
630
+ mmprojSize: mmprojFile.size,
631
+ }),
632
+ ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
633
+ },
634
+ }
635
+
636
+ // Return JSON for huggingface_select to allow direct parsing by consumers
637
+ return {
638
+ content: [
639
+ {
640
+ type: 'text',
641
+ text: `${JSON5.stringify(result, null, 2)} // Please use _hfRepoInfo undocumented field with @ts-ignore`,
642
+ },
643
+ ],
644
+ }
645
+ }
646
+ } catch (err: any) {
647
+ return {
648
+ content: [{ type: 'text', text: `Failed to select model: ${err.message}` }],
649
+ }
650
+ }
651
+ },
652
+ )
653
+ }
@@ -0,0 +1,60 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import * as TOON from '@toon-format/toon'
4
+ import Fuse from 'fuse.js'
5
+ import glyphmap from '../icons/fa6pro-glyphmap.json'
6
+ import glyphmapMeta from '../icons/fa6pro-meta.json'
7
+
8
+ type IconStyle = 'brands' | 'duotone' | 'light' | 'regular' | 'solid' | 'thin'
9
+ const iconMeta = glyphmapMeta as Record<IconStyle, string[]>
10
+
11
+ const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
12
+ const styles = (Object.keys(iconMeta) as IconStyle[]).filter((style) =>
13
+ iconMeta[style].includes(name),
14
+ )
15
+ return { name, code, styles }
16
+ })
17
+
18
+ const iconFuse = new Fuse(iconList, {
19
+ keys: ['name'],
20
+ threshold: 0.3,
21
+ includeScore: true,
22
+ })
23
+
24
+ export function register(server: McpServer) {
25
+ server.tool(
26
+ 'icon_search',
27
+ {
28
+ query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
29
+ limit: z.number().min(1).max(100).optional().default(10),
30
+ style: z
31
+ .enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
32
+ .optional()
33
+ .describe('Filter by icon style'),
34
+ },
35
+ async ({ query, limit, style }) => {
36
+ let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
37
+
38
+ if (style) {
39
+ results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
40
+ }
41
+
42
+ const icons = results.map((r) => ({
43
+ name: r.item.name,
44
+ code: r.item.code,
45
+ unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
46
+ styles: r.item.styles,
47
+ score: r.score,
48
+ }))
49
+
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: TOON.encode({ count: icons.length, icons }),
55
+ },
56
+ ],
57
+ }
58
+ },
59
+ )
60
+ }