@fragments-sdk/mcp 0.6.0 → 0.6.2

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