@fragments-sdk/mcp 0.5.18 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2258 +0,0 @@
1
- // src/server.ts
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema
7
- } from "@modelcontextprotocol/sdk/types.js";
8
- import { readFile } from "fs/promises";
9
- import { existsSync as existsSync2 } from "fs";
10
- import { readFileSync as readFileSync3 } from "fs";
11
- import { join as join2 } from "path";
12
- import { fileURLToPath } from "url";
13
-
14
- // src/constants.ts
15
- var BRAND = {
16
- /** Display name (e.g., "Fragments") */
17
- name: "Fragments",
18
- /** Lowercase name for file paths and CLI (e.g., "fragments") */
19
- nameLower: "fragments",
20
- /** File extension for fragment definition files (e.g., ".fragment.tsx") */
21
- fileExtension: ".fragment.tsx",
22
- /** Legacy file extension for segments (still supported for migration) */
23
- legacyFileExtension: ".segment.tsx",
24
- /** JSON file extension for compiled output */
25
- jsonExtension: ".fragment.json",
26
- /** Default output file name (e.g., "fragments.json") */
27
- outFile: "fragments.json",
28
- /** Config file name (e.g., "fragments.config.ts") */
29
- configFile: "fragments.config.ts",
30
- /** Legacy config file name (still supported for migration) */
31
- legacyConfigFile: "segments.config.ts",
32
- /** CLI command name (e.g., "fragments") */
33
- cliCommand: "fragments",
34
- /** Package scope (e.g., "@fragments") */
35
- packageScope: "@fragments",
36
- /** Directory for storing fragments, registry, and cache */
37
- dataDir: ".fragments",
38
- /** Components subdirectory within .fragments/ */
39
- componentsDir: "components",
40
- /** Registry file name */
41
- registryFile: "registry.json",
42
- /** Context file name (AI-ready markdown) */
43
- contextFile: "context.md",
44
- /** Screenshots subdirectory */
45
- screenshotsDir: "screenshots",
46
- /** Cache subdirectory (gitignored) */
47
- cacheDir: "cache",
48
- /** Diff output subdirectory (gitignored) */
49
- diffDir: "diff",
50
- /** Manifest filename */
51
- manifestFile: "manifest.json",
52
- /** Prefix for localStorage keys (e.g., "fragments-") */
53
- storagePrefix: "fragments-",
54
- /** Static viewer HTML file name */
55
- viewerHtmlFile: "fragments-viewer.html",
56
- /** MCP tool name prefix (e.g., "fragments_") */
57
- mcpToolPrefix: "fragments_",
58
- /** File extension for block definition files */
59
- blockFileExtension: ".block.ts",
60
- /** @deprecated Use blockFileExtension instead */
61
- recipeFileExtension: ".recipe.ts",
62
- /** Vite plugin namespace */
63
- vitePluginNamespace: "fragments-core-shim"
64
- };
65
- var DEFAULTS = {
66
- /** Default viewport dimensions */
67
- viewport: {
68
- width: 1280,
69
- height: 800
70
- },
71
- /** Default diff threshold (percentage) */
72
- diffThreshold: 5,
73
- /** Browser pool size */
74
- poolSize: 3,
75
- /** Idle timeout before browser shutdown (ms) - 5 minutes */
76
- idleTimeoutMs: 5 * 60 * 1e3,
77
- /** Delay after render before capture (ms) */
78
- captureDelayMs: 100,
79
- /** Font loading timeout (ms) */
80
- fontTimeoutMs: 3e3,
81
- /** Default theme */
82
- theme: "light",
83
- /** Dev server port */
84
- port: 6006
85
- };
86
-
87
- // src/server.ts
88
- import { generateContext, filterPlaceholders } from "@fragments-sdk/context/generate";
89
- import { buildMcpTools, buildToolNames } from "@fragments-sdk/context/mcp-tools";
90
-
91
- // src/discovery.ts
92
- import { existsSync, readFileSync, readdirSync } from "fs";
93
- import { join, dirname, resolve } from "path";
94
- import { createRequire } from "module";
95
- function resolveWorkspaceGlob(baseDir, pattern) {
96
- const parts = pattern.split("/");
97
- let dirs = [baseDir];
98
- for (const part of parts) {
99
- if (part === "**") continue;
100
- const next = [];
101
- for (const d of dirs) {
102
- if (part === "*") {
103
- try {
104
- for (const entry of readdirSync(d, { withFileTypes: true })) {
105
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
106
- next.push(join(d, entry.name));
107
- }
108
- }
109
- } catch {
110
- }
111
- } else {
112
- const candidate = join(d, part);
113
- if (existsSync(candidate)) next.push(candidate);
114
- }
115
- }
116
- dirs = next;
117
- }
118
- return dirs;
119
- }
120
- function getWorkspaceDirs(rootDir) {
121
- const dirs = [];
122
- const rootPkgPath = join(rootDir, "package.json");
123
- if (existsSync(rootPkgPath)) {
124
- try {
125
- const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
126
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
127
- if (Array.isArray(workspaces)) {
128
- for (const pattern of workspaces) {
129
- dirs.push(...resolveWorkspaceGlob(rootDir, pattern));
130
- }
131
- return dirs;
132
- }
133
- } catch {
134
- }
135
- }
136
- const pnpmWsPath = join(rootDir, "pnpm-workspace.yaml");
137
- if (existsSync(pnpmWsPath)) {
138
- try {
139
- const content = readFileSync(pnpmWsPath, "utf-8");
140
- const lines = content.split("\n");
141
- let inPackages = false;
142
- for (const line of lines) {
143
- if (/^packages\s*:/.test(line)) {
144
- inPackages = true;
145
- continue;
146
- }
147
- if (inPackages) {
148
- const match = line.match(/^\s+-\s+['"]?([^'"#\n]+)['"]?/);
149
- if (match) {
150
- dirs.push(...resolveWorkspaceGlob(rootDir, match[1].trim()));
151
- } else if (/^\S/.test(line) && line.trim()) {
152
- break;
153
- }
154
- }
155
- }
156
- } catch {
157
- }
158
- }
159
- return dirs;
160
- }
161
- function resolveDepPackageJson(localRequire, depName) {
162
- try {
163
- return localRequire.resolve(`${depName}/package.json`);
164
- } catch {
165
- }
166
- try {
167
- const mainPath = localRequire.resolve(depName);
168
- let dir = dirname(mainPath);
169
- while (true) {
170
- const candidate = join(dir, "package.json");
171
- if (existsSync(candidate)) {
172
- const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
173
- if (pkg.name === depName) return candidate;
174
- }
175
- const parent = dirname(dir);
176
- if (parent === dir) break;
177
- dir = parent;
178
- }
179
- } catch {
180
- }
181
- return null;
182
- }
183
- function findFragmentsInDeps(dir, found) {
184
- const pkgJsonPath = join(dir, "package.json");
185
- if (!existsSync(pkgJsonPath)) return;
186
- try {
187
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
188
- const allDeps = {
189
- ...pkgJson.dependencies,
190
- ...pkgJson.devDependencies
191
- };
192
- const localRequire = createRequire(join(dir, "noop.js"));
193
- for (const depName of Object.keys(allDeps)) {
194
- try {
195
- const depPkgPath = resolveDepPackageJson(localRequire, depName);
196
- if (!depPkgPath) continue;
197
- const depPkg = JSON.parse(readFileSync(depPkgPath, "utf-8"));
198
- if (depPkg.fragments) {
199
- const fragmentsPath = join(dirname(depPkgPath), depPkg.fragments);
200
- if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
201
- found.push(fragmentsPath);
202
- }
203
- }
204
- } catch {
205
- }
206
- }
207
- } catch {
208
- }
209
- }
210
- function findFragmentsJson(startDir) {
211
- const found = [];
212
- const resolvedStart = resolve(startDir);
213
- let dir = resolvedStart;
214
- while (true) {
215
- const candidate = join(dir, BRAND.outFile);
216
- if (existsSync(candidate)) {
217
- found.push(candidate);
218
- break;
219
- }
220
- const parent = dirname(dir);
221
- if (parent === dir) break;
222
- dir = parent;
223
- }
224
- findFragmentsInDeps(resolvedStart, found);
225
- if (found.length === 0 || existsSync(join(resolvedStart, "pnpm-workspace.yaml"))) {
226
- const workspaceDirs = getWorkspaceDirs(resolvedStart);
227
- for (const wsDir of workspaceDirs) {
228
- findFragmentsInDeps(wsDir, found);
229
- }
230
- }
231
- return found;
232
- }
233
-
234
- // src/orama-index.ts
235
- import { create, insertMultiple, search } from "@orama/orama";
236
- var SYNONYM_MAP = {
237
- "form": ["input", "field", "submit", "validation"],
238
- "input": ["form", "field", "text", "entry"],
239
- "button": ["action", "click", "submit", "trigger"],
240
- "action": ["button", "click", "trigger"],
241
- "submit": ["button", "form", "action", "send"],
242
- "alert": ["notification", "message", "warning", "error", "feedback"],
243
- "notification": ["alert", "message", "toast"],
244
- "feedback": ["form", "comment", "review", "rating"],
245
- "card": ["container", "panel", "box", "content"],
246
- "toggle": ["switch", "checkbox", "boolean", "on/off"],
247
- "switch": ["toggle", "checkbox", "boolean"],
248
- "badge": ["tag", "label", "status", "indicator"],
249
- "status": ["badge", "indicator", "state"],
250
- "login": ["auth", "signin", "authentication", "form"],
251
- "auth": ["login", "signin", "authentication"],
252
- "chat": ["message", "conversation", "ai"],
253
- "table": ["data", "grid", "list", "rows"],
254
- "textarea": ["text", "input", "multiline", "area", "comment"],
255
- "area": ["textarea", "multiline", "text"],
256
- "landing": ["page", "hero", "marketing", "section", "layout"],
257
- "hero": ["landing", "marketing", "banner", "headline", "section"],
258
- "marketing": ["landing", "hero", "pricing", "testimonial", "cta"],
259
- "cta": ["marketing", "banner", "action", "button"],
260
- "testimonial": ["marketing", "review", "quote", "feedback"],
261
- "layout": ["stack", "grid", "box", "container", "page"],
262
- "page": ["layout", "landing", "section", "container"],
263
- "section": ["hero", "feature", "testimonial", "cta", "faq"],
264
- "pricing": ["card", "plan", "tier", "marketing"],
265
- "plan": ["pricing", "card", "tier", "subscription"],
266
- "dashboard": ["metrics", "stats", "chart", "card", "grid"],
267
- "metrics": ["dashboard", "stats", "progress", "number"],
268
- "stats": ["metrics", "dashboard", "progress", "badge"],
269
- "chart": ["dashboard", "metrics", "data", "graph"]
270
- };
271
- function expandQuery(query) {
272
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
273
- const expanded = new Set(terms);
274
- for (const term of terms) {
275
- const synonyms = SYNONYM_MAP[term];
276
- if (synonyms) {
277
- for (const syn of synonyms) expanded.add(syn);
278
- }
279
- }
280
- return Array.from(expanded).join(" ");
281
- }
282
- function twoPassSearch(config) {
283
- const { index, query, properties, boost, limit, kind } = config;
284
- const baseConfig = {
285
- mode: "fulltext",
286
- properties,
287
- boost,
288
- limit
289
- };
290
- const originalTermsQuery = query.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
291
- const expandedQuery = expandQuery(query);
292
- const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });
293
- const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });
294
- const origHits = originalResults.hits;
295
- const expHits = expandedResults.hits;
296
- const scoreMap = /* @__PURE__ */ new Map();
297
- for (const hit of origHits) {
298
- scoreMap.set(hit.document.name, (hit.score || 0) * 2);
299
- }
300
- for (const hit of expHits) {
301
- const name = hit.document.name;
302
- const existing = scoreMap.get(name) ?? 0;
303
- scoreMap.set(name, existing + (hit.score || 0));
304
- }
305
- const scored = [];
306
- for (const [name, score] of scoreMap) {
307
- if (score > 0) {
308
- scored.push({ name, kind, rank: scored.length, score });
309
- }
310
- }
311
- scored.sort((a, b) => b.score - a.score);
312
- scored.forEach((s, i) => {
313
- s.rank = i;
314
- });
315
- return scored;
316
- }
317
- var componentSchema = {
318
- name: "string",
319
- description: "string",
320
- category: "string",
321
- tags: "string",
322
- whenUsed: "string",
323
- variants: "string",
324
- status: "string"
325
- };
326
- function buildComponentIndex(fragments) {
327
- const db = create({ schema: componentSchema, language: "english" });
328
- const docs = fragments.map((f) => ({
329
- name: f.meta.name,
330
- description: f.meta.description ?? "",
331
- category: f.meta.category ?? "",
332
- tags: (f.meta.tags ?? []).join(" "),
333
- whenUsed: (f.usage?.when ?? []).join(" "),
334
- variants: f.variants.map((v) => `${v.name} ${v.description || ""}`).join(" "),
335
- status: f.meta.status ?? "stable"
336
- }));
337
- insertMultiple(db, docs);
338
- return db;
339
- }
340
- function searchComponents(query, index, fragments, limit = 50) {
341
- const boostConfig = {
342
- mode: "fulltext",
343
- properties: ["name", "whenUsed", "description", "category", "tags", "variants"],
344
- boost: {
345
- name: 3,
346
- whenUsed: 2.5,
347
- description: 2,
348
- category: 1.5,
349
- tags: 1.5,
350
- variants: 1
351
- },
352
- limit
353
- };
354
- const originalTermsList = query.toLowerCase().split(/\s+/).filter(Boolean);
355
- const originalTermsQuery = originalTermsList.join(" ");
356
- const expandedQuery = expandQuery(query);
357
- const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });
358
- const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });
359
- const origHits = originalResults.hits;
360
- const expHits = expandedResults.hits;
361
- const scoreMap = /* @__PURE__ */ new Map();
362
- for (const hit of origHits) {
363
- scoreMap.set(hit.document.name, (hit.score || 0) * 2);
364
- }
365
- for (const hit of expHits) {
366
- const name = hit.document.name;
367
- const existing = scoreMap.get(name) ?? 0;
368
- scoreMap.set(name, existing + (hit.score || 0));
369
- }
370
- const fragmentMap = /* @__PURE__ */ new Map();
371
- for (const f of fragments) {
372
- fragmentMap.set(f.meta.name.toLowerCase(), f);
373
- }
374
- const originalTermsSet = new Set(originalTermsList);
375
- const scored = [];
376
- for (const [name, rawScore] of scoreMap) {
377
- let score = rawScore;
378
- const nameLower = name.toLowerCase();
379
- const fragment = fragmentMap.get(nameLower);
380
- if (originalTermsSet.has(nameLower)) {
381
- score += 25;
382
- }
383
- if (fragment) {
384
- if (fragment.meta.status === "stable") score += 5;
385
- else if (fragment.meta.status === "beta") score += 2;
386
- if (fragment.meta.status === "deprecated") score -= 25;
387
- }
388
- if (score > 0) {
389
- scored.push({ name, kind: "component", rank: scored.length, score });
390
- }
391
- }
392
- scored.sort((a, b) => b.score - a.score);
393
- scored.forEach((s, i) => {
394
- s.rank = i;
395
- });
396
- return scored;
397
- }
398
- var blockSchema = {
399
- name: "string",
400
- description: "string",
401
- category: "string",
402
- tags: "string",
403
- components: "string"
404
- };
405
- function buildBlockIndex(blocks) {
406
- const db = create({ schema: blockSchema, language: "english" });
407
- const docs = blocks.map((b) => ({
408
- name: b.name,
409
- description: b.description ?? "",
410
- category: b.category ?? "",
411
- tags: (b.tags ?? []).join(" "),
412
- components: b.components.join(" ")
413
- }));
414
- insertMultiple(db, docs);
415
- return db;
416
- }
417
- function searchBlocks(query, index, limit = 50) {
418
- return twoPassSearch({
419
- index,
420
- query,
421
- properties: ["name", "description", "components", "tags", "category"],
422
- boost: {
423
- name: 3,
424
- description: 2,
425
- components: 1.5,
426
- tags: 1.5,
427
- category: 1.5
428
- },
429
- limit,
430
- kind: "block"
431
- });
432
- }
433
- var tokenSchema = {
434
- name: "string",
435
- category: "string",
436
- description: "string"
437
- };
438
- function buildTokenIndex(tokenData) {
439
- const db = create({ schema: tokenSchema, language: "english" });
440
- const docs = [];
441
- for (const [cat, tokens] of Object.entries(tokenData.categories)) {
442
- for (const token of tokens) {
443
- docs.push({
444
- name: token.name,
445
- category: cat,
446
- description: token.description ?? ""
447
- });
448
- }
449
- }
450
- insertMultiple(db, docs);
451
- return db;
452
- }
453
- function searchTokens(query, index, limit = 50) {
454
- return twoPassSearch({
455
- index,
456
- query,
457
- properties: ["name", "category", "description"],
458
- boost: {
459
- name: 2.5,
460
- category: 2,
461
- description: 1.5
462
- },
463
- limit,
464
- kind: "token"
465
- });
466
- }
467
- var USE_CASE_TOKEN_CATEGORIES = {
468
- "table": ["spacing", "borders", "surfaces", "text"],
469
- "data": ["spacing", "borders", "surfaces"],
470
- "grid": ["spacing", "layout"],
471
- "form": ["spacing", "borders", "radius", "focus"],
472
- "input": ["spacing", "borders", "radius", "focus"],
473
- "card": ["surfaces", "shadows", "radius", "borders", "spacing"],
474
- "button": ["colors", "radius", "spacing", "focus"],
475
- "layout": ["spacing", "layout", "surfaces"],
476
- "dashboard": ["spacing", "surfaces", "borders", "shadows"],
477
- "chat": ["spacing", "surfaces", "radius", "shadows"],
478
- "modal": ["shadows", "surfaces", "radius", "spacing"],
479
- "dialog": ["shadows", "surfaces", "radius", "spacing"],
480
- "navigation": ["spacing", "surfaces", "borders"],
481
- "sidebar": ["spacing", "surfaces", "borders"],
482
- "hero": ["spacing", "typography", "colors"],
483
- "landing": ["spacing", "typography", "colors"],
484
- "pricing": ["spacing", "surfaces", "borders", "radius"],
485
- "auth": ["spacing", "borders", "radius", "focus"],
486
- "login": ["spacing", "borders", "radius", "focus"],
487
- "dark": ["colors", "surfaces"],
488
- "theme": ["colors", "surfaces", "text"]
489
- };
490
- function extractTokenCategories(query) {
491
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
492
- const categories = /* @__PURE__ */ new Set();
493
- for (const term of terms) {
494
- const cats = USE_CASE_TOKEN_CATEGORIES[term];
495
- if (cats) {
496
- for (const cat of cats) categories.add(cat);
497
- }
498
- }
499
- if (categories.size === 0) {
500
- return ["spacing", "colors", "surfaces"];
501
- }
502
- return Array.from(categories);
503
- }
504
-
505
- // src/search.ts
506
- var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
507
- var CONVEX_TIMEOUT_MS = 3e3;
508
- async function searchConvex(query, apiKey, limit = 10, kind) {
509
- try {
510
- const controller = new AbortController();
511
- const timeout = setTimeout(() => controller.abort(), CONVEX_TIMEOUT_MS);
512
- const response = await fetch(CONVEX_SEARCH_URL, {
513
- method: "POST",
514
- headers: {
515
- "Content-Type": "application/json",
516
- "Authorization": `Bearer ${apiKey}`
517
- },
518
- body: JSON.stringify({ query, limit, ...kind && { kind } }),
519
- signal: controller.signal
520
- });
521
- clearTimeout(timeout);
522
- if (!response.ok) {
523
- return [];
524
- }
525
- const data = await response.json();
526
- return data.results.map((r, i) => ({
527
- name: r.name,
528
- kind: r.kind ?? "component",
529
- rank: i,
530
- score: r.score
531
- }));
532
- } catch {
533
- return [];
534
- }
535
- }
536
- function keywordScoreComponents(query, fragments, componentIndex) {
537
- const index = componentIndex ?? buildComponentIndex(fragments);
538
- return searchComponents(query, index, fragments);
539
- }
540
- function keywordScoreBlocks(query, blocks, blockIndex) {
541
- const index = blockIndex ?? buildBlockIndex(blocks);
542
- return searchBlocks(query, index);
543
- }
544
- function keywordScoreTokens(query, tokenData, tokenIndex) {
545
- const index = tokenIndex ?? buildTokenIndex(tokenData);
546
- return searchTokens(query, index);
547
- }
548
- function reciprocalRankFusion(resultSets, k = 60) {
549
- const scoreMap = /* @__PURE__ */ new Map();
550
- for (const { results } of resultSets) {
551
- for (let rank = 0; rank < results.length; rank++) {
552
- const result = results[rank];
553
- const key = `${result.kind}:${result.name}`;
554
- const rrfScore = 1 / (k + rank + 1);
555
- const existing = scoreMap.get(key);
556
- if (existing) {
557
- existing.score += rrfScore;
558
- } else {
559
- scoreMap.set(key, { score: rrfScore, kind: result.kind, name: result.name });
560
- }
561
- }
562
- }
563
- const fused = [];
564
- for (const [, { score, kind, name }] of scoreMap) {
565
- fused.push({ name, kind, rank: 0, score });
566
- }
567
- fused.sort((a, b) => b.score - a.score);
568
- fused.forEach((r, i) => {
569
- r.rank = i;
570
- });
571
- return fused;
572
- }
573
- async function hybridSearch(query, data, limit = 10, kind, apiKey) {
574
- const keywordResults = [];
575
- if (!kind || kind === "component") {
576
- keywordResults.push(...keywordScoreComponents(query, data.fragments, data.componentIndex));
577
- }
578
- if ((!kind || kind === "block") && data.blocks) {
579
- keywordResults.push(...keywordScoreBlocks(query, data.blocks, data.blockIndex));
580
- }
581
- if ((!kind || kind === "token") && data.tokenData) {
582
- keywordResults.push(...keywordScoreTokens(query, data.tokenData, data.tokenIndex));
583
- }
584
- keywordResults.sort((a, b) => b.score - a.score);
585
- keywordResults.forEach((r, i) => {
586
- r.rank = i;
587
- });
588
- if (!apiKey) {
589
- return keywordResults.slice(0, limit);
590
- }
591
- const vectorResults = await searchConvex(query, apiKey, limit, kind);
592
- if (vectorResults.length === 0) {
593
- return keywordResults.slice(0, limit);
594
- }
595
- const graphBoostResults = [];
596
- if (data.graph) {
597
- try {
598
- const { ComponentGraphEngine: ComponentGraphEngine2, deserializeGraph: deserializeGraph2 } = await import("@fragments-sdk/context/graph");
599
- const graph = deserializeGraph2(data.graph);
600
- const engine = new ComponentGraphEngine2(graph);
601
- const topComponents = [...keywordResults, ...vectorResults].filter((r) => r.kind === "component").slice(0, 5);
602
- const neighborSet = /* @__PURE__ */ new Set();
603
- for (const result of topComponents) {
604
- const neighbors = engine.neighbors(result.name, 1);
605
- for (const n of neighbors.neighbors) {
606
- if (!neighborSet.has(n.component)) {
607
- neighborSet.add(n.component);
608
- graphBoostResults.push({
609
- name: n.component,
610
- kind: "component",
611
- rank: graphBoostResults.length,
612
- score: 1
613
- // Will be normalized through RRF
614
- });
615
- }
616
- }
617
- }
618
- } catch {
619
- }
620
- }
621
- const resultSets = [
622
- { label: "vector", results: vectorResults },
623
- { label: "keyword", results: keywordResults }
624
- ];
625
- if (graphBoostResults.length > 0) {
626
- resultSets.push({ label: "graph", results: graphBoostResults });
627
- }
628
- const fused = reciprocalRankFusion(resultSets);
629
- return fused.slice(0, limit);
630
- }
631
-
632
- // src/scoring.ts
633
- var MINIMUM_SCORE_THRESHOLD = 5;
634
- function assignConfidence(score, maxScore) {
635
- if (maxScore <= 0) return "low";
636
- const ratio = score / maxScore;
637
- if (ratio >= 0.7) return "high";
638
- if (ratio >= 0.4) return "medium";
639
- return "low";
640
- }
641
- function meetsMinimumThreshold(maxScore) {
642
- return maxScore >= MINIMUM_SCORE_THRESHOLD;
643
- }
644
- function levenshtein(a, b) {
645
- const la = a.length;
646
- const lb = b.length;
647
- const dp = Array.from({ length: lb + 1 }, (_, i) => i);
648
- for (let i = 1; i <= la; i++) {
649
- let prev = i - 1;
650
- dp[0] = i;
651
- for (let j = 1; j <= lb; j++) {
652
- const temp = dp[j];
653
- dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
654
- prev = temp;
655
- }
656
- }
657
- return dp[lb];
658
- }
659
- function findClosestMatch(input, candidates, maxDistance = 3) {
660
- const inputLower = input.toLowerCase();
661
- let bestMatch = null;
662
- let bestDist = maxDistance + 1;
663
- for (const candidate of candidates) {
664
- const candidateLower = candidate.toLowerCase();
665
- const dist = levenshtein(inputLower, candidateLower);
666
- if (dist < bestDist) {
667
- bestDist = dist;
668
- bestMatch = candidate;
669
- } else if (dist === bestDist && bestMatch) {
670
- const currentLenDiff = Math.abs(bestMatch.length - input.length);
671
- const newLenDiff = Math.abs(candidate.length - input.length);
672
- if (newLenDiff < currentLenDiff) {
673
- bestMatch = candidate;
674
- }
675
- }
676
- }
677
- return bestDist <= maxDistance ? bestMatch : null;
678
- }
679
- var BLOCK_BOOST_PER_OCCURRENCE = 5;
680
- function buildBlockComponentFrequency(blocks) {
681
- const freq = /* @__PURE__ */ new Map();
682
- for (const block of blocks) {
683
- for (const comp of block.components) {
684
- const key = comp.toLowerCase();
685
- freq.set(key, (freq.get(key) ?? 0) + 1);
686
- }
687
- }
688
- return freq;
689
- }
690
- function boostByBlockFrequency(results, freq) {
691
- for (const result of results) {
692
- const count = freq.get(result.name.toLowerCase()) ?? 0;
693
- if (count > 0) {
694
- result.score += count * BLOCK_BOOST_PER_OCCURRENCE;
695
- }
696
- }
697
- results.sort((a, b) => b.score - a.score);
698
- results.forEach((r, i) => {
699
- r.rank = i;
700
- });
701
- return results;
702
- }
703
-
704
- // src/service.ts
705
- async function renderComponent(viewerUrl, request) {
706
- const renderUrl = `${viewerUrl}/fragments/render`;
707
- const response = await fetch(renderUrl, {
708
- method: "POST",
709
- headers: { "Content-Type": "application/json" },
710
- body: JSON.stringify({
711
- component: request.component,
712
- props: request.props ?? {},
713
- variant: request.variant,
714
- viewport: request.viewport ?? { width: 800, height: 600 }
715
- })
716
- });
717
- return await response.json();
718
- }
719
- async function compareComponent(viewerUrl, request) {
720
- const compareUrl = `${viewerUrl}/fragments/compare`;
721
- const response = await fetch(compareUrl, {
722
- method: "POST",
723
- headers: { "Content-Type": "application/json" },
724
- body: JSON.stringify(request)
725
- });
726
- return await response.json();
727
- }
728
- async function fixComponent(viewerUrl, request) {
729
- const fixUrl = `${viewerUrl}/fragments/fix`;
730
- const response = await fetch(fixUrl, {
731
- method: "POST",
732
- headers: { "Content-Type": "application/json" },
733
- body: JSON.stringify(request)
734
- });
735
- return await response.json();
736
- }
737
- async function auditComponent(viewerUrl, request) {
738
- const a11yUrl = `${viewerUrl}/fragments/a11y`;
739
- const response = await fetch(a11yUrl, {
740
- method: "POST",
741
- headers: { "Content-Type": "application/json" },
742
- body: JSON.stringify({
743
- component: request.component,
744
- variant: request.variant,
745
- standard: request.standard,
746
- includeFixPatches: request.includeFixPatches
747
- })
748
- });
749
- const raw = await response.json();
750
- if (raw.error) {
751
- return {
752
- component: request.component,
753
- results: [],
754
- score: 0,
755
- aaPercent: 0,
756
- aaaPercent: 0,
757
- passed: false,
758
- standard: request.standard ?? "AA",
759
- error: raw.error
760
- };
761
- }
762
- const results = raw.results ?? [];
763
- const standard = request.standard ?? "AA";
764
- let totalCritical = 0;
765
- let totalSerious = 0;
766
- let totalModerate = 0;
767
- let totalMinor = 0;
768
- for (const r of results) {
769
- totalCritical += r.summary.critical;
770
- totalSerious += r.summary.serious;
771
- totalModerate += r.summary.moderate;
772
- totalMinor += r.summary.minor;
773
- }
774
- const deductions = totalCritical * 10 + totalSerious * 5 + totalModerate * 2 + totalMinor * 1;
775
- const score = Math.max(0, 100 - deductions);
776
- const variantCount = results.length;
777
- const aaPassCount = results.filter((r) => {
778
- const critical = r.summary.critical;
779
- const serious = r.summary.serious;
780
- return critical === 0 && serious === 0;
781
- }).length;
782
- const aaaPassCount = results.filter((r) => {
783
- const total = r.summary.critical + r.summary.serious + r.summary.moderate + r.summary.minor;
784
- return total === 0;
785
- }).length;
786
- const totalPasses = results.reduce((sum, r) => sum + r.passes, 0);
787
- const totalViolations = totalCritical + totalSerious + totalModerate + totalMinor;
788
- const emptyAudit = results.length > 0 && totalPasses === 0 && totalViolations === 0;
789
- const aaPercent = variantCount > 0 ? Math.round(aaPassCount / variantCount * 100) : 100;
790
- const aaaPercent = variantCount > 0 ? Math.round(aaaPassCount / variantCount * 100) : 100;
791
- const aaPass = !emptyAudit && totalCritical === 0 && totalSerious === 0;
792
- const aaaPass = !emptyAudit && totalViolations === 0;
793
- const passed = standard === "AAA" ? aaaPass : aaPass;
794
- return {
795
- component: request.component,
796
- results,
797
- score: emptyAudit ? 0 : score,
798
- aaPercent: emptyAudit ? 0 : aaPercent,
799
- aaaPercent: emptyAudit ? 0 : aaaPercent,
800
- ...emptyAudit && { emptyAudit },
801
- passed,
802
- standard
803
- };
804
- }
805
-
806
- // src/utils.ts
807
- function projectFields(obj, fields) {
808
- if (!fields || fields.length === 0) {
809
- return obj;
810
- }
811
- const result = {};
812
- for (const field of fields) {
813
- const parts = field.split(".");
814
- let source = obj;
815
- let target = result;
816
- for (let i = 0; i < parts.length; i++) {
817
- const part = parts[i];
818
- const isLast = i === parts.length - 1;
819
- if (source === null || source === void 0 || typeof source !== "object") {
820
- break;
821
- }
822
- const sourceObj = source;
823
- const value = sourceObj[part];
824
- if (isLast) {
825
- target[part] = value;
826
- } else {
827
- if (!(part in target)) {
828
- target[part] = {};
829
- }
830
- target = target[part];
831
- source = value;
832
- }
833
- }
834
- }
835
- return result;
836
- }
837
-
838
- // src/graph-handler.ts
839
- import {
840
- ComponentGraphEngine,
841
- deserializeGraph
842
- } from "@fragments-sdk/context/graph";
843
- function handleGraphTool(args, serializedGraph, blocks, componentNames) {
844
- if (!serializedGraph) {
845
- return {
846
- text: JSON.stringify({
847
- error: "No graph data available. Run `fragments build` to generate the component graph.",
848
- hint: "The graph is built automatically during `fragments build` and embedded in fragments.json."
849
- }),
850
- isError: true
851
- };
852
- }
853
- const graph = deserializeGraph(serializedGraph);
854
- const blockData = blocks ? Object.fromEntries(
855
- Object.entries(blocks).map(([k, v]) => [k, { components: v.components }])
856
- ) : void 0;
857
- const engine = new ComponentGraphEngine(graph, blockData);
858
- const edgeTypes = args.edgeTypes;
859
- switch (args.mode) {
860
- case "health": {
861
- const health = engine.getHealth();
862
- const blockCount = blocks ? Object.keys(blocks).length : 0;
863
- return {
864
- text: JSON.stringify({
865
- mode: "health",
866
- ...health,
867
- ...health.compositionCoverage === 0 && blockCount === 0 && {
868
- compositionNote: "No composition blocks defined yet \u2014 compositionCoverage will increase as blocks are added"
869
- },
870
- summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
871
- })
872
- };
873
- }
874
- case "dependencies": {
875
- if (!args.component) {
876
- return { text: JSON.stringify({ error: "component is required for dependencies mode" }), isError: true };
877
- }
878
- if (!engine.hasNode(args.component)) {
879
- const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
880
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
881
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
882
- }
883
- const deps = engine.dependencies(args.component, edgeTypes);
884
- return {
885
- text: JSON.stringify({
886
- mode: "dependencies",
887
- component: args.component,
888
- count: deps.length,
889
- dependencies: deps.map((e) => ({
890
- component: e.target,
891
- type: e.type,
892
- weight: e.weight,
893
- note: e.note,
894
- provenance: e.provenance
895
- }))
896
- })
897
- };
898
- }
899
- case "dependents": {
900
- if (!args.component) {
901
- return { text: JSON.stringify({ error: "component is required for dependents mode" }), isError: true };
902
- }
903
- if (!engine.hasNode(args.component)) {
904
- const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
905
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
906
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
907
- }
908
- const deps = engine.dependents(args.component, edgeTypes);
909
- return {
910
- text: JSON.stringify({
911
- mode: "dependents",
912
- component: args.component,
913
- count: deps.length,
914
- dependents: deps.map((e) => ({
915
- component: e.source,
916
- type: e.type,
917
- weight: e.weight,
918
- note: e.note,
919
- provenance: e.provenance
920
- }))
921
- })
922
- };
923
- }
924
- case "impact": {
925
- if (!args.component) {
926
- return { text: JSON.stringify({ error: "component is required for impact mode" }), isError: true };
927
- }
928
- if (!engine.hasNode(args.component)) {
929
- const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
930
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
931
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
932
- }
933
- const result = engine.impact(args.component, args.maxDepth ?? 3);
934
- return {
935
- text: JSON.stringify({
936
- mode: "impact",
937
- ...result,
938
- summary: `Changing ${args.component} affects ${result.totalAffected} component(s) and ${result.affectedBlocks.length} block(s)`
939
- })
940
- };
941
- }
942
- case "path": {
943
- if (!args.component || !args.target) {
944
- return { text: JSON.stringify({ error: "component and target are required for path mode" }), isError: true };
945
- }
946
- const result = engine.path(args.component, args.target);
947
- return {
948
- text: JSON.stringify({
949
- mode: "path",
950
- from: args.component,
951
- to: args.target,
952
- ...result,
953
- edges: result.edges.map((e) => ({
954
- source: e.source,
955
- target: e.target,
956
- type: e.type
957
- }))
958
- })
959
- };
960
- }
961
- case "composition": {
962
- if (!args.component) {
963
- return { text: JSON.stringify({ error: "component is required for composition mode" }), isError: true };
964
- }
965
- if (!engine.hasNode(args.component)) {
966
- const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
967
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
968
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
969
- }
970
- const tree = engine.composition(args.component);
971
- return {
972
- text: JSON.stringify({
973
- mode: "composition",
974
- ...tree
975
- })
976
- };
977
- }
978
- case "alternatives": {
979
- if (!args.component) {
980
- return { text: JSON.stringify({ error: "component is required for alternatives mode" }), isError: true };
981
- }
982
- if (!engine.hasNode(args.component)) {
983
- const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
984
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
985
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
986
- }
987
- const alts = engine.alternatives(args.component);
988
- return {
989
- text: JSON.stringify({
990
- mode: "alternatives",
991
- component: args.component,
992
- count: alts.length,
993
- alternatives: alts
994
- })
995
- };
996
- }
997
- case "islands": {
998
- const islands = engine.islands();
999
- return {
1000
- text: JSON.stringify({
1001
- mode: "islands",
1002
- count: islands.length,
1003
- islands: islands.map((island, i) => ({
1004
- id: i + 1,
1005
- size: island.length,
1006
- components: island
1007
- }))
1008
- })
1009
- };
1010
- }
1011
- default:
1012
- return {
1013
- text: JSON.stringify({
1014
- error: `Unknown mode: "${args.mode}". Valid modes: dependencies, dependents, impact, path, composition, alternatives, islands, health`
1015
- }),
1016
- isError: true
1017
- };
1018
- }
1019
- }
1020
-
1021
- // src/server-helpers.ts
1022
- function normalizeFilter(value) {
1023
- const normalized = value?.trim().toLowerCase();
1024
- return normalized && normalized.length > 0 ? normalized : void 0;
1025
- }
1026
- function categoryMatches(category, categoryFilter) {
1027
- if (!categoryFilter) return true;
1028
- return normalizeFilter(category) === categoryFilter;
1029
- }
1030
- function buildLocalSearchData(data, indexes) {
1031
- const allFragments = Object.values(data.fragments);
1032
- const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1033
- const localData = {
1034
- fragments: allFragments,
1035
- blocks: allBlocks,
1036
- tokenData: data.tokens,
1037
- graph: data.graph,
1038
- componentIndex: indexes.componentIndex ?? void 0,
1039
- blockIndex: indexes.blockIndex ?? void 0,
1040
- tokenIndex: indexes.tokenIndex ?? void 0
1041
- };
1042
- return { allFragments, allBlocks, localData };
1043
- }
1044
- async function buildImportStatements(components, resolvePackageName) {
1045
- const grouped = /* @__PURE__ */ new Map();
1046
- for (const component of components) {
1047
- if (!component) continue;
1048
- const packageName = await resolvePackageName(component);
1049
- const existing = grouped.get(packageName);
1050
- if (!existing) {
1051
- grouped.set(packageName, [component]);
1052
- continue;
1053
- }
1054
- if (!existing.includes(component)) {
1055
- existing.push(component);
1056
- }
1057
- }
1058
- return Array.from(grouped.entries()).map(
1059
- ([packageName, componentNames]) => `import { ${componentNames.join(", ")} } from '${packageName}';`
1060
- );
1061
- }
1062
- function limitTokensPerCategory(categories, limit) {
1063
- if (limit === void 0) {
1064
- return {
1065
- categories,
1066
- total: Object.values(categories).reduce((sum, tokens) => sum + tokens.length, 0)
1067
- };
1068
- }
1069
- const limited = {};
1070
- let total = 0;
1071
- for (const [category, tokens] of Object.entries(categories)) {
1072
- const sliced = tokens.slice(0, limit);
1073
- if (sliced.length === 0) continue;
1074
- limited[category] = sliced;
1075
- total += sliced.length;
1076
- }
1077
- return { categories: limited, total };
1078
- }
1079
-
1080
- // src/version.ts
1081
- import { readFileSync as readFileSync2 } from "fs";
1082
- function readPackageVersion() {
1083
- try {
1084
- const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
1085
- const pkg = JSON.parse(raw);
1086
- return pkg.version ?? "0.0.0";
1087
- } catch {
1088
- return "0.0.0";
1089
- }
1090
- }
1091
- var MCP_SERVER_VERSION = readPackageVersion();
1092
-
1093
- // src/server.ts
1094
- var TOOL_NAMES = buildToolNames(BRAND.nameLower);
1095
- var NO_VIEWER_MSG = "This tool requires a running dev server. Add --viewer-url http://localhost:PORT to this server's config args. Start the viewer with 'fragments dev' in your component library directory.";
1096
- var TOOLS = buildMcpTools(BRAND.nameLower);
1097
- function createMcpServer(config) {
1098
- const server = new Server(
1099
- {
1100
- name: `${BRAND.nameLower}-mcp`,
1101
- version: MCP_SERVER_VERSION
1102
- },
1103
- {
1104
- capabilities: {
1105
- tools: {}
1106
- }
1107
- }
1108
- );
1109
- let fragmentsData = null;
1110
- const fragmentPackageMap = /* @__PURE__ */ new Map();
1111
- let defaultPackageName = null;
1112
- let resolvedRoot = null;
1113
- let componentIndex = null;
1114
- let blockIndex = null;
1115
- let tokenIndex = null;
1116
- async function resolveProjectRoot() {
1117
- if (resolvedRoot) return resolvedRoot;
1118
- try {
1119
- const result = await server.listRoots();
1120
- if (result.roots?.length > 0) {
1121
- const rootUri = result.roots[0].uri;
1122
- resolvedRoot = fileURLToPath(rootUri);
1123
- return resolvedRoot;
1124
- }
1125
- } catch {
1126
- }
1127
- resolvedRoot = config.projectRoot;
1128
- return resolvedRoot;
1129
- }
1130
- async function loadFragments() {
1131
- if (fragmentsData) return fragmentsData;
1132
- const projectRoot = await resolveProjectRoot();
1133
- const paths = findFragmentsJson(projectRoot);
1134
- if (paths.length === 0) {
1135
- throw new Error(
1136
- `No ${BRAND.outFile} found. Searched ${projectRoot} and package.json dependencies.
1137
-
1138
- Fix: Add a project-level MCP config so the server runs from your workspace root:
1139
-
1140
- Cursor: .cursor/mcp.json
1141
- VS Code: .vscode/mcp.json
1142
- Claude: claude mcp add ${BRAND.nameLower} -- npx @fragments-sdk/mcp
1143
- Windsurf: .windsurf/mcp.json
1144
-
1145
- Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
1146
-
1147
- If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1148
- );
1149
- }
1150
- const content = await readFile(paths[0], "utf-8");
1151
- fragmentsData = JSON.parse(content);
1152
- if (!fragmentsData.blocks && fragmentsData.recipes) {
1153
- fragmentsData.blocks = fragmentsData.recipes;
1154
- }
1155
- if (fragmentsData.packageName) {
1156
- for (const name of Object.keys(fragmentsData.fragments)) {
1157
- fragmentPackageMap.set(name, fragmentsData.packageName);
1158
- }
1159
- }
1160
- for (let i = 1; i < paths.length; i++) {
1161
- const extra = JSON.parse(await readFile(paths[i], "utf-8"));
1162
- if (extra.packageName) {
1163
- for (const name of Object.keys(extra.fragments)) {
1164
- fragmentPackageMap.set(name, extra.packageName);
1165
- }
1166
- }
1167
- Object.assign(fragmentsData.fragments, extra.fragments);
1168
- const extraBlocks = extra.blocks ?? extra.recipes;
1169
- if (extraBlocks) {
1170
- fragmentsData.blocks = { ...fragmentsData.blocks, ...extraBlocks };
1171
- }
1172
- }
1173
- const allFragments = Object.values(fragmentsData.fragments);
1174
- const allBlocks = Object.values(fragmentsData.blocks ?? fragmentsData.recipes ?? {});
1175
- componentIndex = buildComponentIndex(allFragments);
1176
- if (allBlocks.length > 0) {
1177
- blockIndex = buildBlockIndex(allBlocks);
1178
- }
1179
- if (fragmentsData.tokens && fragmentsData.tokens.total > 0) {
1180
- tokenIndex = buildTokenIndex(fragmentsData.tokens);
1181
- }
1182
- return fragmentsData;
1183
- }
1184
- async function getPackageName(fragmentName) {
1185
- await loadFragments();
1186
- if (fragmentName) {
1187
- const segPkg = fragmentPackageMap.get(fragmentName);
1188
- if (segPkg) return segPkg;
1189
- }
1190
- if (defaultPackageName) return defaultPackageName;
1191
- if (fragmentsData?.packageName) {
1192
- defaultPackageName = fragmentsData.packageName;
1193
- return defaultPackageName;
1194
- }
1195
- const root = resolvedRoot ?? config.projectRoot;
1196
- const packageJsonPath = join2(root, "package.json");
1197
- if (existsSync2(packageJsonPath)) {
1198
- try {
1199
- const content = readFileSync3(packageJsonPath, "utf-8");
1200
- const pkg = JSON.parse(content);
1201
- if (pkg.name) {
1202
- defaultPackageName = pkg.name;
1203
- return defaultPackageName;
1204
- }
1205
- } catch {
1206
- }
1207
- }
1208
- defaultPackageName = "your-component-library";
1209
- return defaultPackageName;
1210
- }
1211
- const VIEWER_TOOL_KEYS = /* @__PURE__ */ new Set(["render", "fix", "a11y"]);
1212
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1213
- if (config.viewerUrl) {
1214
- return { tools: TOOLS };
1215
- }
1216
- const availableTools = TOOLS.filter(
1217
- (t) => !VIEWER_TOOL_KEYS.has(t.name.replace(`${BRAND.nameLower}_`, ""))
1218
- );
1219
- return { tools: availableTools };
1220
- });
1221
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1222
- const { name, arguments: args } = request.params;
1223
- try {
1224
- switch (name) {
1225
- // ================================================================
1226
- // DISCOVER — list, suggest, context, alternatives
1227
- // ================================================================
1228
- case TOOL_NAMES.discover: {
1229
- const data = await loadFragments();
1230
- const useCase = args?.useCase ?? void 0;
1231
- const componentForAlts = args?.component ?? void 0;
1232
- const category = normalizeFilter(args?.category);
1233
- const search2 = args?.search?.toLowerCase() ?? void 0;
1234
- const status = args?.status ?? void 0;
1235
- const format = args?.format ?? "markdown";
1236
- const compact = args?.compact ?? false;
1237
- const includeCode = args?.includeCode ?? false;
1238
- const includeRelations = args?.includeRelations ?? false;
1239
- const limit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : 10;
1240
- const verbosity = args?.verbosity ?? "standard";
1241
- if (compact || args?.format && !useCase && !componentForAlts) {
1242
- let fragments2 = Object.values(data.fragments);
1243
- const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1244
- if (category) {
1245
- fragments2 = fragments2.filter((f) => categoryMatches(f.meta.category, category));
1246
- }
1247
- if (search2) {
1248
- fragments2 = fragments2.filter(
1249
- (f) => f.meta.name.toLowerCase().includes(search2) || f.meta.description?.toLowerCase().includes(search2) || f.meta.tags?.some((t) => t.toLowerCase().includes(search2))
1250
- );
1251
- }
1252
- if (status) {
1253
- fragments2 = fragments2.filter((f) => f.meta.status === status);
1254
- }
1255
- const { content: ctxContent, tokenEstimate } = generateContext(fragments2, {
1256
- format,
1257
- compact,
1258
- include: {
1259
- code: includeCode,
1260
- relations: includeRelations
1261
- }
1262
- }, allBlocks);
1263
- return {
1264
- content: [{ type: "text", text: ctxContent }],
1265
- _meta: { tokenEstimate }
1266
- };
1267
- }
1268
- if (useCase) {
1269
- const { allFragments, allBlocks, localData } = buildLocalSearchData(
1270
- data,
1271
- {
1272
- componentIndex,
1273
- blockIndex,
1274
- tokenIndex
1275
- }
1276
- );
1277
- const context = args?.context?.toLowerCase() ?? "";
1278
- const fullQuery = context ? `${useCase} ${context}` : useCase;
1279
- const searchResults = await hybridSearch(fullQuery, localData, limit, "component", config.apiKey);
1280
- const blockMatches = keywordScoreBlocks(fullQuery, allBlocks, blockIndex ?? void 0).slice(0, 5);
1281
- if (blockMatches.length > 0) {
1282
- const matchedBlocks = blockMatches.map((bm) => allBlocks.find((b) => b.name.toLowerCase() === bm.name.toLowerCase())).filter(Boolean);
1283
- const blockFreq = buildBlockComponentFrequency(matchedBlocks);
1284
- boostByBlockFrequency(searchResults, blockFreq);
1285
- }
1286
- const maxScore = searchResults.length > 0 ? searchResults[0].score : 0;
1287
- const scored = searchResults.map((result) => {
1288
- const fragment = allFragments.find(
1289
- (s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
1290
- );
1291
- if (!fragment) return null;
1292
- const filteredWhen = filterPlaceholders(fragment.usage?.when).slice(0, 3);
1293
- const filteredWhenNot = filterPlaceholders(fragment.usage?.whenNot).slice(0, 2);
1294
- return {
1295
- component: fragment.meta.name,
1296
- category: fragment.meta.category,
1297
- description: fragment.meta.description,
1298
- confidence: assignConfidence(result.score, maxScore),
1299
- reasons: [`Matched via hybrid search (score: ${result.score.toFixed(4)})`],
1300
- usage: { when: filteredWhen, whenNot: filteredWhenNot },
1301
- variantCount: fragment.variants.length,
1302
- status: fragment.meta.status
1303
- };
1304
- }).filter(Boolean);
1305
- const suggestions = [];
1306
- const categoryCount = {};
1307
- for (const item of scored) {
1308
- if (!item) continue;
1309
- const cat = item.category || "uncategorized";
1310
- const count = categoryCount[cat] || 0;
1311
- if (count < 2 || suggestions.length < 3) {
1312
- suggestions.push(item);
1313
- categoryCount[cat] = count + 1;
1314
- if (suggestions.length >= limit) break;
1315
- }
1316
- }
1317
- const compositionHint = suggestions.length >= 2 ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map((s) => s.component).join(" and ")}.` : void 0;
1318
- const useCaseLower = useCase.toLowerCase();
1319
- const STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "variable", "token", "css", "theme", "dark mode", "background", "hover"];
1320
- const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
1321
- const noMatch = suggestions.length === 0;
1322
- const belowThreshold = !noMatch && maxScore > 1 && !meetsMinimumThreshold(maxScore);
1323
- const weakMatch = !noMatch && (belowThreshold || suggestions.every((s) => s.confidence === "low"));
1324
- let recommendation;
1325
- let nextStep;
1326
- if (noMatch) {
1327
- recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${TOOL_NAMES.tokens} to find CSS custom properties.` : "No matching components found. Try different keywords or browse all components with fragments_discover.";
1328
- nextStep = isStyleQuery ? `Use ${TOOL_NAMES.tokens}(search: "${useCaseLower.split(/\s+/)[0]}") to find design tokens.` : void 0;
1329
- } else if (weakMatch) {
1330
- recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${TOOL_NAMES.tokens}.` : ""}`;
1331
- nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
1332
- } else {
1333
- recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
1334
- nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
1335
- }
1336
- const tokenHint = isStyleQuery && !noMatch ? `Your query includes styling terms. For CSS custom properties, also try ${TOOL_NAMES.tokens}(search: "${useCaseLower.split(/\s+/)[0]}").` : void 0;
1337
- const blockNames = blockMatches.map((bm) => allBlocks.find((b) => b.name.toLowerCase() === bm.name.toLowerCase())).filter(Boolean).slice(0, 3).map((b) => b.name);
1338
- const blockHint = blockNames.length > 0 ? `Related blocks: ${blockNames.join(", ")}. Use ${TOOL_NAMES.blocks}(search: "${useCase}") for ready-to-use patterns.` : void 0;
1339
- const suggestResponse = verbosity === "compact" ? {
1340
- useCase,
1341
- suggestions: suggestions.map((s) => ({
1342
- component: s.component,
1343
- description: s.description,
1344
- confidence: s.confidence
1345
- })),
1346
- recommendation
1347
- } : {
1348
- useCase,
1349
- context: context || void 0,
1350
- suggestions,
1351
- noMatch,
1352
- weakMatch,
1353
- recommendation,
1354
- compositionHint,
1355
- ...tokenHint && { tokenHint },
1356
- ...blockHint && { blockHint },
1357
- nextStep
1358
- };
1359
- return {
1360
- content: [{
1361
- type: "text",
1362
- text: JSON.stringify(suggestResponse)
1363
- }]
1364
- };
1365
- }
1366
- if (componentForAlts) {
1367
- const fragment = Object.values(data.fragments).find(
1368
- (s) => s.meta.name.toLowerCase() === componentForAlts.toLowerCase()
1369
- );
1370
- if (!fragment) {
1371
- const allNames = Object.values(data.fragments).map((s) => s.meta.name);
1372
- const closest = findClosestMatch(componentForAlts, allNames);
1373
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1374
- throw new Error(`Component "${componentForAlts}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
1375
- }
1376
- const relations = fragment.relations ?? [];
1377
- const referencedBy = Object.values(data.fragments).filter(
1378
- (s) => s.relations?.some((r) => r.component.toLowerCase() === componentForAlts.toLowerCase())
1379
- ).map((s) => ({
1380
- component: s.meta.name,
1381
- relationship: s.relations?.find(
1382
- (r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
1383
- )?.relationship,
1384
- note: s.relations?.find(
1385
- (r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
1386
- )?.note
1387
- }));
1388
- const sameCategory = Object.values(data.fragments).filter(
1389
- (s) => s.meta.category === fragment.meta.category && s.meta.name.toLowerCase() !== componentForAlts.toLowerCase()
1390
- ).map((s) => ({
1391
- component: s.meta.name,
1392
- description: s.meta.description
1393
- }));
1394
- return {
1395
- content: [{
1396
- type: "text",
1397
- text: JSON.stringify({
1398
- component: fragment.meta.name,
1399
- category: fragment.meta.category,
1400
- directRelations: relations,
1401
- referencedBy,
1402
- sameCategory,
1403
- suggestion: relations.find((r) => r.relationship === "alternative") ? `Consider ${relations.find((r) => r.relationship === "alternative")?.component}: ${relations.find((r) => r.relationship === "alternative")?.note}` : void 0
1404
- })
1405
- }]
1406
- };
1407
- }
1408
- const fragments = Object.values(data.fragments).filter((s) => {
1409
- if (category && !categoryMatches(s.meta.category, category)) return false;
1410
- if (status && (s.meta.status ?? "stable") !== status) return false;
1411
- if (search2) {
1412
- const nameMatch = s.meta.name.toLowerCase().includes(search2);
1413
- const descMatch = s.meta.description?.toLowerCase().includes(search2);
1414
- const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search2));
1415
- if (!nameMatch && !descMatch && !tagMatch) return false;
1416
- }
1417
- return true;
1418
- }).map((s) => {
1419
- if (verbosity === "compact") {
1420
- return { name: s.meta.name, category: s.meta.category };
1421
- }
1422
- return {
1423
- name: s.meta.name,
1424
- category: s.meta.category,
1425
- description: s.meta.description,
1426
- status: s.meta.status ?? "stable",
1427
- variantCount: s.variants.length,
1428
- tags: s.meta.tags ?? [],
1429
- ...(includeCode || verbosity === "full") && s.variants[0]?.code && {
1430
- example: s.variants[0].code
1431
- }
1432
- };
1433
- });
1434
- return {
1435
- content: [{
1436
- type: "text",
1437
- text: JSON.stringify({
1438
- total: fragments.length,
1439
- fragments,
1440
- categories: [...new Set(fragments.map((s) => s.category))],
1441
- hint: fragments.length === 0 ? "No components found. Try broader search terms or check available categories." : fragments.length > 5 ? "Use fragments_discover with useCase for recommendations, or fragments_inspect for details on a specific component." : void 0
1442
- })
1443
- }]
1444
- };
1445
- }
1446
- // ================================================================
1447
- // INSPECT
1448
- // ================================================================
1449
- case TOOL_NAMES.inspect: {
1450
- const data = await loadFragments();
1451
- const componentName = args?.component;
1452
- const fields = args?.fields;
1453
- const variantName = args?.variant ?? void 0;
1454
- const maxExamples = args?.maxExamples;
1455
- const maxLines = args?.maxLines;
1456
- const verbosity = args?.verbosity ?? "standard";
1457
- if (!componentName) {
1458
- throw new Error("component is required");
1459
- }
1460
- const fragment = Object.values(data.fragments).find(
1461
- (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1462
- );
1463
- if (!fragment) {
1464
- const allNames = Object.values(data.fragments).map((s) => s.meta.name);
1465
- const closest = findClosestMatch(componentName, allNames);
1466
- const suggestion = closest ? ` Did you mean "${closest}"? Use ${TOOL_NAMES.inspect}("${closest}") to inspect it.` : "";
1467
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
1468
- }
1469
- const pkgName = await getPackageName(fragment.meta.name);
1470
- let variants = fragment.variants;
1471
- if (variantName) {
1472
- const query = variantName.toLowerCase();
1473
- let filtered = variants.filter((v) => v.name.toLowerCase() === query);
1474
- if (filtered.length === 0) {
1475
- filtered = variants.filter((v) => v.name.toLowerCase().startsWith(query));
1476
- }
1477
- if (filtered.length === 0) {
1478
- filtered = variants.filter((v) => v.name.toLowerCase().includes(query));
1479
- }
1480
- if (filtered.length > 0) {
1481
- variants = filtered;
1482
- } else {
1483
- throw new Error(
1484
- `Variant "${variantName}" not found for ${componentName}. Available: ${fragment.variants.map((v) => v.name).join(", ")}`
1485
- );
1486
- }
1487
- }
1488
- if (maxExamples && maxExamples > 0) {
1489
- variants = variants.slice(0, maxExamples);
1490
- }
1491
- const truncateCode = (code) => {
1492
- if (!maxLines || maxLines <= 0) return code;
1493
- const lines = code.split("\n");
1494
- if (lines.length <= maxLines) return code;
1495
- return lines.slice(0, maxLines).join("\n") + "\n// ... truncated";
1496
- };
1497
- const examples = variants.map((variant) => {
1498
- if (variant.code) {
1499
- return {
1500
- variant: variant.name,
1501
- description: variant.description,
1502
- code: truncateCode(variant.code)
1503
- };
1504
- }
1505
- return {
1506
- variant: variant.name,
1507
- description: variant.description,
1508
- code: `<${fragment.meta.name} />`,
1509
- note: "No code example provided in fragment. Refer to props for customization."
1510
- };
1511
- });
1512
- const propsReference = Object.entries(fragment.props ?? {}).map(([propName, prop]) => ({
1513
- name: propName,
1514
- type: prop.type,
1515
- required: prop.required,
1516
- default: prop.default,
1517
- description: prop.description
1518
- }));
1519
- const propConstraints = Object.entries(fragment.props ?? {}).filter(([, prop]) => prop.constraints && prop.constraints.length > 0).map(([pName, prop]) => ({
1520
- prop: pName,
1521
- constraints: prop.constraints
1522
- }));
1523
- const fullResult = {
1524
- meta: fragment.meta,
1525
- props: fragment.props,
1526
- variants: fragment.variants,
1527
- relations: fragment.relations,
1528
- contract: fragment.contract,
1529
- generated: fragment._generated,
1530
- guidelines: {
1531
- when: filterPlaceholders(fragment.usage?.when),
1532
- whenNot: filterPlaceholders(fragment.usage?.whenNot),
1533
- guidelines: fragment.usage?.guidelines ?? [],
1534
- accessibility: fragment.usage?.accessibility ?? [],
1535
- propConstraints,
1536
- alternatives: fragment.relations?.filter((r) => r.relationship === "alternative").map((r) => ({
1537
- component: r.component,
1538
- note: r.note
1539
- })) ?? []
1540
- },
1541
- examples: {
1542
- import: `import { ${fragment.meta.name} } from '${pkgName}';`,
1543
- code: examples,
1544
- propsReference
1545
- }
1546
- };
1547
- const aliasMap = { "usage": "guidelines" };
1548
- const resolvedFields = fields?.map((f) => {
1549
- const parts = f.split(".");
1550
- if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
1551
- return parts.join(".");
1552
- });
1553
- let result;
1554
- if (verbosity === "compact" && !resolvedFields?.length) {
1555
- result = {
1556
- meta: fullResult.meta,
1557
- propNames: Object.keys(fragment.props ?? {}),
1558
- variantNames: fragment.variants.map((v) => v.name)
1559
- };
1560
- } else {
1561
- result = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
1562
- }
1563
- return {
1564
- content: [{ type: "text", text: JSON.stringify(result) }]
1565
- };
1566
- }
1567
- // ================================================================
1568
- // BLOCKS
1569
- // ================================================================
1570
- case TOOL_NAMES.blocks: {
1571
- const data = await loadFragments();
1572
- const blockName = args?.name;
1573
- const search2 = args?.search?.toLowerCase() ?? void 0;
1574
- const component = args?.component?.toLowerCase() ?? void 0;
1575
- const category = args?.category?.toLowerCase() ?? void 0;
1576
- const blocksLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 50) : search2 ? 10 : void 0;
1577
- const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1578
- if (allBlocks.length === 0) {
1579
- return {
1580
- content: [{
1581
- type: "text",
1582
- text: JSON.stringify({
1583
- total: 0,
1584
- blocks: [],
1585
- hint: `No composition blocks found. Blocks are reusable patterns showing how components wire together (e.g., "Login Form", "Settings Page"). Create .block.ts files and run \`${BRAND.cliCommand} build\`.`
1586
- })
1587
- }]
1588
- };
1589
- }
1590
- let filtered = allBlocks;
1591
- if (blockName) {
1592
- filtered = filtered.filter(
1593
- (b) => b.name.toLowerCase() === blockName.toLowerCase()
1594
- );
1595
- if (filtered.length === 0) {
1596
- const allBlockNames = allBlocks.map((b) => b.name);
1597
- const closest = findClosestMatch(blockName, allBlockNames);
1598
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1599
- throw new Error(`Block "${blockName}" not found.${suggestion} Use ${TOOL_NAMES.blocks} to see available blocks.`);
1600
- }
1601
- }
1602
- if (search2) {
1603
- if (blockIndex) {
1604
- const ranked = searchBlocks(search2, blockIndex, 50);
1605
- const rankedNames = new Set(ranked.map((r) => r.name.toLowerCase()));
1606
- filtered = filtered.filter((b) => rankedNames.has(b.name.toLowerCase()));
1607
- filtered.sort((a, b) => {
1608
- const aIdx = ranked.findIndex((r) => r.name.toLowerCase() === a.name.toLowerCase());
1609
- const bIdx = ranked.findIndex((r) => r.name.toLowerCase() === b.name.toLowerCase());
1610
- return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
1611
- });
1612
- } else {
1613
- filtered = filtered.filter((b) => {
1614
- const haystack = [
1615
- b.name,
1616
- b.description,
1617
- ...b.tags ?? [],
1618
- ...b.components,
1619
- b.category
1620
- ].join(" ").toLowerCase();
1621
- return haystack.includes(search2);
1622
- });
1623
- }
1624
- }
1625
- if (component) {
1626
- filtered = filtered.filter(
1627
- (b) => b.components.some((c) => c.toLowerCase() === component)
1628
- );
1629
- }
1630
- if (category) {
1631
- filtered = filtered.filter(
1632
- (b) => b.category.toLowerCase() === category
1633
- );
1634
- }
1635
- const blocksUseIcons = filtered.some(
1636
- (b) => b.components.some((c) => c === "Icon") || b.code && /\bIcon\b/.test(b.code)
1637
- );
1638
- const iconHint = blocksUseIcons ? "Icon components in block code are from @phosphor-icons/react. Import them as: import { IconName } from '@phosphor-icons/react';" : void 0;
1639
- if (blocksLimit !== void 0) {
1640
- filtered = filtered.slice(0, blocksLimit);
1641
- }
1642
- const verbosity = args?.verbosity ?? "standard";
1643
- const blocksWithImports = await Promise.all(filtered.map(async (b) => {
1644
- const imports = await buildImportStatements(
1645
- b.components,
1646
- async (componentName) => getPackageName(componentName)
1647
- );
1648
- const base = {
1649
- name: b.name,
1650
- description: b.description,
1651
- category: b.category,
1652
- components: b.components,
1653
- tags: b.tags,
1654
- import: imports.join("\n"),
1655
- imports
1656
- };
1657
- if (verbosity === "compact") return base;
1658
- if (verbosity === "full") return { ...base, code: b.code };
1659
- const codeLines = b.code.split("\n");
1660
- const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : b.code;
1661
- return { ...base, code };
1662
- }));
1663
- return {
1664
- content: [{
1665
- type: "text",
1666
- text: JSON.stringify({
1667
- total: blocksWithImports.length,
1668
- blocks: blocksWithImports,
1669
- ...iconHint && { iconHint },
1670
- ...blocksWithImports.length === 0 && allBlocks.length > 0 && {
1671
- hint: "No blocks matching your query. Try broader search terms."
1672
- }
1673
- })
1674
- }]
1675
- };
1676
- }
1677
- // ================================================================
1678
- // TOKENS
1679
- // ================================================================
1680
- case TOOL_NAMES.tokens: {
1681
- const data = await loadFragments();
1682
- const category = args?.category?.toLowerCase() ?? void 0;
1683
- const search2 = args?.search?.toLowerCase() ?? void 0;
1684
- const tokensLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 100) : search2 ? 25 : void 0;
1685
- const tokenData = data.tokens;
1686
- if (!tokenData || tokenData.total === 0) {
1687
- return {
1688
- content: [{
1689
- type: "text",
1690
- text: JSON.stringify({
1691
- total: 0,
1692
- categories: {},
1693
- hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
1694
- })
1695
- }]
1696
- };
1697
- }
1698
- let filteredCategories = {};
1699
- let filteredTotal = 0;
1700
- const searchMatchesCategory = search2 ? Object.keys(tokenData.categories).find((cat) => cat.toLowerCase() === search2) : void 0;
1701
- for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1702
- if (category && cat !== category) continue;
1703
- let filtered = tokens;
1704
- if (search2) {
1705
- if (searchMatchesCategory && cat.toLowerCase() === search2) {
1706
- filtered = tokens;
1707
- } else {
1708
- filtered = tokens.filter(
1709
- (t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || cat.toLowerCase().includes(search2)
1710
- );
1711
- }
1712
- }
1713
- if (filtered.length > 0) {
1714
- filteredCategories[cat] = filtered;
1715
- filteredTotal += filtered.length;
1716
- }
1717
- }
1718
- if (tokensLimit !== void 0) {
1719
- const limited = limitTokensPerCategory(filteredCategories, tokensLimit);
1720
- filteredCategories = limited.categories;
1721
- filteredTotal = limited.total;
1722
- }
1723
- let hint;
1724
- if (filteredTotal === 0) {
1725
- const availableCategories = Object.keys(tokenData.categories);
1726
- if (category && search2) {
1727
- const categoryTotal = tokenData.categories[category]?.length ?? 0;
1728
- hint = categoryTotal > 0 ? `No tokens matching "${search2}" in category "${category}" (${categoryTotal} tokens in this category). Try a broader search or remove the search term.` : `Category "${category}" not found. Available: ${availableCategories.join(", ")}`;
1729
- } else if (search2) {
1730
- hint = `No tokens matching "${search2}". Available categories: ${availableCategories.join(", ")}`;
1731
- } else if (category) {
1732
- hint = `Category "${category}" not found. Available: ${availableCategories.join(", ")}`;
1733
- }
1734
- } else if (!category && !search2) {
1735
- hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
1736
- }
1737
- return {
1738
- content: [{
1739
- type: "text",
1740
- text: JSON.stringify({
1741
- prefix: tokenData.prefix,
1742
- total: filteredTotal,
1743
- totalAvailable: tokenData.total,
1744
- categories: filteredCategories,
1745
- ...hint && { hint },
1746
- ...!category && !search2 && {
1747
- availableCategories: Object.entries(tokenData.categories).map(
1748
- ([cat, tokens]) => ({ category: cat, count: tokens.length })
1749
- )
1750
- }
1751
- })
1752
- }]
1753
- };
1754
- }
1755
- // ================================================================
1756
- // IMPLEMENT — one-shot discover + inspect + blocks + tokens
1757
- // ================================================================
1758
- case TOOL_NAMES.implement: {
1759
- const data = await loadFragments();
1760
- const useCase = args?.useCase;
1761
- if (!useCase) {
1762
- throw new Error("useCase is required");
1763
- }
1764
- const verbosity = args?.verbosity ?? "standard";
1765
- const { allFragments, allBlocks, localData } = buildLocalSearchData(
1766
- data,
1767
- {
1768
- componentIndex,
1769
- blockIndex,
1770
- tokenIndex
1771
- }
1772
- );
1773
- const tokenData = data.tokens;
1774
- const implLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 15) : 5;
1775
- const [componentResults, blockResults, tokenResults] = await Promise.all([
1776
- hybridSearch(useCase, localData, implLimit * 3, "component", config.apiKey),
1777
- hybridSearch(useCase, localData, implLimit, "block", config.apiKey),
1778
- hybridSearch(useCase, localData, implLimit, "token", config.apiKey)
1779
- ]);
1780
- const topBlockScore = blockResults.length > 0 ? blockResults[0].score : 0;
1781
- const filteredBlockResults = blockResults.filter((r) => r.score >= topBlockScore * 0.3);
1782
- if (filteredBlockResults.length > 0) {
1783
- const matchedBlocks = filteredBlockResults.map((r) => allBlocks.find((b) => b.name.toLowerCase() === r.name.toLowerCase())).filter(Boolean);
1784
- const blockFreq = buildBlockComponentFrequency(matchedBlocks);
1785
- boostByBlockFrequency(componentResults, blockFreq);
1786
- }
1787
- const topComponentResults = componentResults.slice(0, implLimit);
1788
- const maxCompScore = topComponentResults.length > 0 ? topComponentResults[0].score : 0;
1789
- const topMatches = topComponentResults.map((result) => {
1790
- const fragment = allFragments.find(
1791
- (s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
1792
- );
1793
- return fragment ? { fragment, score: result.score } : null;
1794
- }).filter(Boolean);
1795
- const components = await Promise.all(
1796
- topMatches.map(async ({ fragment: s, score }) => {
1797
- const pkgName = await getPackageName(s.meta.name);
1798
- if (verbosity === "compact") {
1799
- return {
1800
- name: s.meta.name,
1801
- description: s.meta.description,
1802
- confidence: assignConfidence(score, maxCompScore),
1803
- import: `import { ${s.meta.name} } from '${pkgName}';`
1804
- };
1805
- }
1806
- const exampleLimit = verbosity === "full" ? s.variants.length : 2;
1807
- const propsLimit = verbosity === "full" ? Object.keys(s.props ?? {}).length : 5;
1808
- const examples = s.variants.slice(0, exampleLimit).map((v) => ({
1809
- variant: v.name,
1810
- code: v.code ?? `<${s.meta.name} />`
1811
- }));
1812
- const propsSummary = Object.entries(s.props ?? {}).slice(0, propsLimit).map(
1813
- ([pName, p]) => `${pName}${p.required ? " (required)" : ""}: ${p.type}${p.values ? ` = ${p.values.join("|")}` : ""}`
1814
- );
1815
- return {
1816
- name: s.meta.name,
1817
- category: s.meta.category,
1818
- description: s.meta.description,
1819
- confidence: assignConfidence(score, maxCompScore),
1820
- import: `import { ${s.meta.name} } from '${pkgName}';`,
1821
- props: propsSummary,
1822
- examples,
1823
- guidelines: filterPlaceholders(s.usage?.when).slice(0, 3),
1824
- accessibility: s.usage?.accessibility?.slice(0, 2) ?? []
1825
- };
1826
- })
1827
- );
1828
- const matchingBlocks = (await Promise.all(filteredBlockResults.slice(0, 5).map(async (result) => {
1829
- const block = allBlocks.find(
1830
- (b) => b.name.toLowerCase() === result.name.toLowerCase()
1831
- );
1832
- if (!block) return null;
1833
- const imports = await buildImportStatements(
1834
- block.components,
1835
- async (componentName) => getPackageName(componentName)
1836
- );
1837
- const codeLines = block.code.split("\n");
1838
- const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : block.code;
1839
- return {
1840
- name: block.name,
1841
- description: block.description,
1842
- components: block.components,
1843
- code,
1844
- import: imports.join("\n"),
1845
- imports
1846
- };
1847
- }))).filter(Boolean);
1848
- let relevantTokens;
1849
- if (tokenResults.length > 0 && tokenData) {
1850
- relevantTokens = {};
1851
- for (const result of tokenResults) {
1852
- for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1853
- if (tokens.some((t) => t.name === result.name)) {
1854
- if (!relevantTokens[cat]) relevantTokens[cat] = [];
1855
- relevantTokens[cat].push(result.name);
1856
- break;
1857
- }
1858
- }
1859
- }
1860
- if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
1861
- }
1862
- if (!relevantTokens && tokenData) {
1863
- const categories = extractTokenCategories(useCase);
1864
- relevantTokens = {};
1865
- for (const cat of categories) {
1866
- const tokens = tokenData.categories[cat];
1867
- if (tokens && tokens.length > 0) {
1868
- relevantTokens[cat] = tokens.slice(0, 5).map((t) => t.name);
1869
- }
1870
- }
1871
- if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
1872
- }
1873
- return {
1874
- content: [{
1875
- type: "text",
1876
- text: JSON.stringify({
1877
- useCase,
1878
- components,
1879
- blocks: verbosity !== "compact" && matchingBlocks.length > 0 ? matchingBlocks : void 0,
1880
- tokens: verbosity !== "compact" ? relevantTokens : void 0,
1881
- noMatch: components.length === 0,
1882
- summary: components.length > 0 ? `Found ${components.length} component(s) for "${useCase}". ${matchingBlocks.length > 0 ? `Plus ${matchingBlocks.length} ready-to-use block(s).` : ""}` : `No components match "${useCase}". Try ${TOOL_NAMES.discover} with different terms${tokenData ? ` or ${TOOL_NAMES.tokens} for CSS variables` : ""}.`
1883
- })
1884
- }]
1885
- };
1886
- }
1887
- // ================================================================
1888
- // RENDER — HTTP-only (no Playwright)
1889
- // ================================================================
1890
- case TOOL_NAMES.render: {
1891
- const componentName = args?.component;
1892
- const variantName = args?.variant;
1893
- const props = args?.props ?? {};
1894
- const viewport = args?.viewport;
1895
- const figmaUrl = args?.figmaUrl;
1896
- const threshold = args?.threshold ?? (figmaUrl ? 1 : config.threshold ?? DEFAULTS.diffThreshold);
1897
- if (!componentName) {
1898
- return {
1899
- content: [{ type: "text", text: "Error: component name is required" }],
1900
- isError: true
1901
- };
1902
- }
1903
- {
1904
- const data = await loadFragments();
1905
- const fragment = Object.values(data.fragments).find(
1906
- (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1907
- );
1908
- if (!fragment) {
1909
- const allNames = Object.values(data.fragments).map((s) => s.meta.name);
1910
- const closest = findClosestMatch(componentName, allNames);
1911
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1912
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
1913
- }
1914
- }
1915
- const viewerUrl = config.viewerUrl;
1916
- if (!viewerUrl) {
1917
- return {
1918
- content: [{
1919
- type: "text",
1920
- text: NO_VIEWER_MSG
1921
- }],
1922
- isError: true
1923
- };
1924
- }
1925
- if (figmaUrl) {
1926
- try {
1927
- const result = await compareComponent(viewerUrl, {
1928
- component: componentName,
1929
- variant: variantName,
1930
- props,
1931
- figmaUrl,
1932
- threshold
1933
- });
1934
- if (result.error) {
1935
- return {
1936
- content: [{
1937
- type: "text",
1938
- text: `Compare error: ${result.error}${result.suggestion ? `
1939
- Suggestion: ${result.suggestion}` : ""}`
1940
- }],
1941
- isError: true
1942
- };
1943
- }
1944
- const content = [];
1945
- const summaryText = result.match ? `MATCH: ${componentName} matches Figma design (${result.diffPercentage}% diff, threshold: ${result.threshold}%)` : `MISMATCH: ${componentName} differs from Figma design by ${result.diffPercentage}% (threshold: ${result.threshold}%)`;
1946
- content.push({ type: "text", text: summaryText });
1947
- if (result.diff && !result.match) {
1948
- content.push({
1949
- type: "image",
1950
- data: result.diff.replace("data:image/png;base64,", ""),
1951
- mimeType: "image/png"
1952
- });
1953
- content.push({
1954
- type: "text",
1955
- text: `Diff image above shows visual differences (red highlights). Changed regions: ${result.changedRegions?.length ?? 0}`
1956
- });
1957
- }
1958
- content.push({
1959
- type: "text",
1960
- text: JSON.stringify({
1961
- match: result.match,
1962
- diffPercentage: result.diffPercentage,
1963
- threshold: result.threshold,
1964
- figmaUrl: result.figmaUrl,
1965
- changedRegions: result.changedRegions
1966
- })
1967
- });
1968
- return { content };
1969
- } catch (error) {
1970
- return {
1971
- content: [{
1972
- type: "text",
1973
- text: `Failed to compare component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running and FIGMA_ACCESS_TOKEN is set.`
1974
- }],
1975
- isError: true
1976
- };
1977
- }
1978
- }
1979
- try {
1980
- const result = await renderComponent(viewerUrl, {
1981
- component: componentName,
1982
- props,
1983
- variant: variantName,
1984
- viewport: viewport ?? { width: 800, height: 600 }
1985
- });
1986
- if (result.error) {
1987
- return {
1988
- content: [{ type: "text", text: `Render error: ${result.error}` }],
1989
- isError: true
1990
- };
1991
- }
1992
- return {
1993
- content: [
1994
- {
1995
- type: "image",
1996
- data: result.screenshot.replace("data:image/png;base64,", ""),
1997
- mimeType: "image/png"
1998
- },
1999
- {
2000
- type: "text",
2001
- text: `Successfully rendered ${componentName}${variantName ? ` (variant: "${variantName}")` : ""}${Object.keys(props).length > 0 ? ` with props: ${JSON.stringify(props)}` : ""}`
2002
- }
2003
- ]
2004
- };
2005
- } catch (error) {
2006
- return {
2007
- content: [{
2008
- type: "text",
2009
- text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
2010
- }],
2011
- isError: true
2012
- };
2013
- }
2014
- }
2015
- // ================================================================
2016
- // FIX — HTTP-only (no Playwright)
2017
- // ================================================================
2018
- case TOOL_NAMES.fix: {
2019
- const data = await loadFragments();
2020
- const componentName = args?.component;
2021
- const variantName = args?.variant ?? void 0;
2022
- const fixType = args?.fixType ?? "all";
2023
- if (!componentName) {
2024
- throw new Error("component is required");
2025
- }
2026
- const fragment = Object.values(data.fragments).find(
2027
- (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
2028
- );
2029
- if (!fragment) {
2030
- const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2031
- const closest = findClosestMatch(componentName, allNames);
2032
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2033
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
2034
- }
2035
- const viewerUrl = config.viewerUrl;
2036
- if (!viewerUrl) {
2037
- return {
2038
- content: [{
2039
- type: "text",
2040
- text: NO_VIEWER_MSG
2041
- }],
2042
- isError: true
2043
- };
2044
- }
2045
- try {
2046
- const result = await fixComponent(viewerUrl, {
2047
- component: componentName,
2048
- variant: variantName,
2049
- fixType
2050
- });
2051
- if (result.error) {
2052
- return {
2053
- content: [{
2054
- type: "text",
2055
- text: `Fix generation error: ${result.error}`
2056
- }],
2057
- isError: true
2058
- };
2059
- }
2060
- return {
2061
- content: [{
2062
- type: "text",
2063
- text: JSON.stringify({
2064
- component: componentName,
2065
- variant: variantName ?? "all",
2066
- fixType,
2067
- patches: result.patches,
2068
- summary: result.summary,
2069
- patchCount: result.patches.length,
2070
- nextStep: result.patches.length > 0 ? "Apply patches using your editor or `patch` command, then run fragments_render to confirm fixes." : void 0
2071
- })
2072
- }]
2073
- };
2074
- } catch (error) {
2075
- return {
2076
- content: [{
2077
- type: "text",
2078
- text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
2079
- }],
2080
- isError: true
2081
- };
2082
- }
2083
- }
2084
- // ================================================================
2085
- // A11Y — accessibility audit
2086
- // ================================================================
2087
- case TOOL_NAMES.a11y: {
2088
- const componentName = args?.component;
2089
- const variantName = args?.variant ?? void 0;
2090
- const standard = args?.standard ?? "AA";
2091
- const includeFixPatches = args?.includeFixPatches ?? false;
2092
- if (!componentName) {
2093
- throw new Error("component is required");
2094
- }
2095
- {
2096
- const data = await loadFragments();
2097
- const fragment = Object.values(data.fragments).find(
2098
- (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
2099
- );
2100
- if (!fragment) {
2101
- const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2102
- const closest = findClosestMatch(componentName, allNames);
2103
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2104
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
2105
- }
2106
- }
2107
- const viewerUrl = config.viewerUrl;
2108
- if (!viewerUrl) {
2109
- return {
2110
- content: [{
2111
- type: "text",
2112
- text: NO_VIEWER_MSG
2113
- }],
2114
- isError: true
2115
- };
2116
- }
2117
- try {
2118
- const result = await auditComponent(viewerUrl, {
2119
- component: componentName,
2120
- variant: variantName,
2121
- standard,
2122
- includeFixPatches
2123
- });
2124
- if (result.error) {
2125
- return {
2126
- content: [{
2127
- type: "text",
2128
- text: `A11y audit error: ${result.error}`
2129
- }],
2130
- isError: true
2131
- };
2132
- }
2133
- let nextStep;
2134
- if (result.emptyAudit) {
2135
- nextStep = `No testable elements found for ${variantName ? `variant "${variantName}"` : componentName}. The variant may not exist or renders no accessible content. Check available variants with ${TOOL_NAMES.inspect}("${componentName}").`;
2136
- } else if (result.passed) {
2137
- nextStep = 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.';
2138
- } else {
2139
- nextStep = `Fix the violations above, then re-run ${TOOL_NAMES.a11y} to verify. Use ${TOOL_NAMES.fix} for automated fixes.`;
2140
- }
2141
- return {
2142
- content: [{
2143
- type: "text",
2144
- text: JSON.stringify({
2145
- component: componentName,
2146
- variant: variantName ?? "all",
2147
- standard,
2148
- totalViolations: result.results.reduce((sum, r) => sum + r.summary.total, 0),
2149
- variantsPassingAA: `${result.aaPercent}%`,
2150
- variantsPassingAAA: `${result.aaaPercent}%`,
2151
- passed: result.passed,
2152
- ...result.emptyAudit && { emptyAudit: true },
2153
- results: result.results,
2154
- nextStep
2155
- })
2156
- }]
2157
- };
2158
- } catch (error) {
2159
- return {
2160
- content: [{
2161
- type: "text",
2162
- text: `Failed to audit component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
2163
- }],
2164
- isError: true
2165
- };
2166
- }
2167
- }
2168
- // ================================================================
2169
- // GRAPH — component relationship queries
2170
- // ================================================================
2171
- case TOOL_NAMES.graph: {
2172
- const data = await loadFragments();
2173
- const graphArgs = {
2174
- mode: args?.mode ?? "health",
2175
- component: args?.component,
2176
- target: args?.target,
2177
- edgeTypes: args?.edgeTypes,
2178
- maxDepth: args?.maxDepth
2179
- };
2180
- const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2181
- const result = handleGraphTool(
2182
- graphArgs,
2183
- data.graph,
2184
- data.blocks ?? data.recipes,
2185
- allNames
2186
- );
2187
- if (result.isError) {
2188
- return {
2189
- content: [{ type: "text", text: result.text }],
2190
- isError: true
2191
- };
2192
- }
2193
- return {
2194
- content: [{ type: "text", text: result.text }]
2195
- };
2196
- }
2197
- // ================================================================
2198
- // GENERATE_UI — proxy to playground API
2199
- // ================================================================
2200
- case TOOL_NAMES.generate_ui: {
2201
- const prompt = args?.prompt;
2202
- if (!prompt) {
2203
- throw new Error("prompt is required");
2204
- }
2205
- const currentTree = args?.currentTree;
2206
- const playgroundUrl = config.playgroundUrl ?? "https://usefragments.com";
2207
- const response = await fetch(`${playgroundUrl}/api/playground/generate`, {
2208
- method: "POST",
2209
- headers: { "Content-Type": "application/json" },
2210
- body: JSON.stringify({
2211
- prompt,
2212
- ...currentTree && { currentSpec: currentTree }
2213
- })
2214
- });
2215
- if (!response.ok) {
2216
- const errorBody = await response.text();
2217
- throw new Error(`Playground API error (${response.status}): ${errorBody}`);
2218
- }
2219
- const text = await response.text();
2220
- return {
2221
- content: [{
2222
- type: "text",
2223
- text
2224
- }]
2225
- };
2226
- }
2227
- default:
2228
- throw new Error(`Unknown tool: ${name}`);
2229
- }
2230
- } catch (error) {
2231
- return {
2232
- content: [{
2233
- type: "text",
2234
- text: JSON.stringify({
2235
- error: error instanceof Error ? error.message : String(error)
2236
- })
2237
- }],
2238
- isError: true
2239
- };
2240
- }
2241
- });
2242
- return server;
2243
- }
2244
- async function startMcpServer(config) {
2245
- const server = createMcpServer(config);
2246
- const transport = new StdioServerTransport();
2247
- await server.connect(transport);
2248
- }
2249
- function createSandboxServer() {
2250
- return createMcpServer({ projectRoot: process.cwd() });
2251
- }
2252
-
2253
- export {
2254
- createMcpServer,
2255
- startMcpServer,
2256
- createSandboxServer
2257
- };
2258
- //# sourceMappingURL=chunk-ZOMUPGBG.js.map