@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.
- package/package.json +3 -3
- package/tools/mcp-server.ts +10 -880
- package/tools/mcp-tools/compile.ts +91 -0
- package/tools/mcp-tools/huggingface.ts +653 -0
- package/tools/mcp-tools/icons.ts +60 -0
- package/tools/mcp-tools/lottie.ts +102 -0
- package/tools/mcp-tools/media.ts +110 -0
- package/tools/pull.ts +9 -6
package/tools/mcp-server.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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)
|