@fragments-sdk/mcp 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,28 +1,19 @@
1
- import {
2
- BRAND,
3
- DEFAULTS
4
- } from "./chunk-4SVS3AA3.js";
5
- import {
6
- componentNames,
7
- findComponent,
8
- findComponentByName,
9
- getGuidanceWhen,
10
- getGuidanceWhenNot,
11
- listBlocks,
12
- listComponents
13
- } from "./chunk-YSRGQDEB.js";
14
-
15
1
  // src/server.ts
16
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
4
  import {
19
5
  CallToolRequestSchema,
20
- ListToolsRequestSchema
6
+ ErrorCode,
7
+ ListResourcesRequestSchema,
8
+ ListToolsRequestSchema,
9
+ McpError,
10
+ ReadResourceRequestSchema
21
11
  } from "@modelcontextprotocol/sdk/types.js";
22
- import { existsSync as existsSync9 } from "fs";
12
+ import { existsSync as existsSync8 } from "fs";
23
13
  import { readFileSync as readFileSync6 } from "fs";
24
- import { join as join8 } from "path";
14
+ import { join as join7 } from "path";
25
15
  import { fileURLToPath } from "url";
16
+ import { BRAND as BRAND4 } from "@fragments-sdk/core";
26
17
 
27
18
  // src/config.ts
28
19
  import { readFileSync, existsSync } from "fs";
@@ -30,2372 +21,294 @@ import { join } from "path";
30
21
  function loadConfigFile(projectRoot) {
31
22
  const configPath = join(projectRoot, "ds-mcp.config.json");
32
23
  if (existsSync(configPath)) {
33
- try {
34
- const content = readFileSync(configPath, "utf-8");
35
- return JSON.parse(content);
36
- } catch (e) {
37
- throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
38
- }
39
- }
40
- const pkgPath = join(projectRoot, "package.json");
41
- if (existsSync(pkgPath)) {
42
- try {
43
- const content = readFileSync(pkgPath, "utf-8");
44
- const pkg = JSON.parse(content);
45
- if (pkg.dsMcp) return pkg.dsMcp;
46
- } catch {
47
- }
48
- }
49
- return null;
50
- }
51
-
52
- // src/server.ts
53
- import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
54
-
55
- // src/orama-index.ts
56
- import { create, insertMultiple, search } from "@orama/orama";
57
- var SYNONYM_MAP = {
58
- "form": ["input", "field", "submit", "validation"],
59
- "input": ["form", "field", "text", "entry"],
60
- "button": ["action", "click", "submit", "trigger"],
61
- "action": ["button", "click", "trigger"],
62
- "submit": ["button", "form", "action", "send"],
63
- "alert": ["notification", "message", "warning", "error", "feedback"],
64
- "notification": ["alert", "message", "toast"],
65
- "feedback": ["form", "comment", "review", "rating"],
66
- "card": ["container", "panel", "box", "content"],
67
- "toggle": ["switch", "checkbox", "boolean", "on/off"],
68
- "switch": ["toggle", "checkbox", "boolean"],
69
- "badge": ["tag", "label", "status", "indicator"],
70
- "status": ["badge", "indicator", "state"],
71
- "login": ["auth", "signin", "authentication", "form"],
72
- "auth": ["login", "signin", "authentication"],
73
- "chat": ["message", "conversation", "ai"],
74
- "table": ["data", "grid", "list", "rows"],
75
- "textarea": ["text", "input", "multiline", "area", "comment"],
76
- "area": ["textarea", "multiline", "text"],
77
- "landing": ["page", "hero", "marketing", "section", "layout"],
78
- "hero": ["landing", "marketing", "banner", "headline", "section"],
79
- "marketing": ["landing", "hero", "pricing", "testimonial", "cta"],
80
- "cta": ["marketing", "banner", "action", "button"],
81
- "testimonial": ["marketing", "review", "quote", "feedback"],
82
- "layout": ["stack", "grid", "box", "container", "page"],
83
- "page": ["layout", "landing", "section", "container"],
84
- "section": ["hero", "feature", "testimonial", "cta", "faq"],
85
- "pricing": ["card", "plan", "tier", "marketing"],
86
- "plan": ["pricing", "card", "tier", "subscription"],
87
- "dashboard": ["metrics", "stats", "chart", "card", "grid"],
88
- "metrics": ["dashboard", "stats", "progress", "number"],
89
- "stats": ["metrics", "dashboard", "progress", "badge"],
90
- "chart": ["dashboard", "metrics", "data", "graph"]
91
- };
92
- function expandQuery(query) {
93
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
94
- const expanded = new Set(terms);
95
- for (const term of terms) {
96
- const synonyms = SYNONYM_MAP[term];
97
- if (synonyms) {
98
- for (const syn of synonyms) expanded.add(syn);
99
- }
100
- }
101
- return Array.from(expanded).join(" ");
102
- }
103
- function twoPassSearch(config) {
104
- const { index, query, properties, boost, limit, kind } = config;
105
- const baseConfig = {
106
- mode: "fulltext",
107
- properties,
108
- boost,
109
- limit
110
- };
111
- const originalTermsQuery = query.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
112
- const expandedQuery = expandQuery(query);
113
- const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });
114
- const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });
115
- const origHits = originalResults.hits;
116
- const expHits = expandedResults.hits;
117
- const scoreMap = /* @__PURE__ */ new Map();
118
- for (const hit of origHits) {
119
- scoreMap.set(hit.document.name, (hit.score || 0) * 2);
120
- }
121
- for (const hit of expHits) {
122
- const name = hit.document.name;
123
- const existing = scoreMap.get(name) ?? 0;
124
- scoreMap.set(name, existing + (hit.score || 0));
125
- }
126
- const scored = [];
127
- for (const [name, score] of scoreMap) {
128
- if (score > 0) {
129
- scored.push({ name, kind, rank: scored.length, score });
130
- }
131
- }
132
- scored.sort((a, b) => b.score - a.score);
133
- scored.forEach((s, i) => {
134
- s.rank = i;
135
- });
136
- return scored;
137
- }
138
- var componentSchema = {
139
- name: "string",
140
- description: "string",
141
- category: "string",
142
- tags: "string",
143
- whenUsed: "string",
144
- patterns: "string",
145
- variants: "string",
146
- status: "string"
147
- };
148
- function isCompiledFragment(value) {
149
- return "meta" in value;
150
- }
151
- function normalizeComponent(value) {
152
- if (!isCompiledFragment(value)) return value;
153
- return {
154
- id: value.filePath ?? value.meta.name,
155
- name: value.meta.name,
156
- description: value.meta.description ?? "",
157
- category: value.meta.category ?? "uncategorized",
158
- status: value.meta.status ?? "stable",
159
- tags: value.meta.tags ?? [],
160
- props: {},
161
- propsSummary: value.propsSummary ?? value.contract?.propsSummary ?? Object.entries(value.props ?? {}).map(
162
- ([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
163
- ),
164
- examples: (value.variants ?? []).map((variant) => ({
165
- name: variant.name,
166
- description: variant.description,
167
- code: variant.code
168
- })),
169
- relations: (value.relations ?? []).map((relation) => ({
170
- componentName: relation.component,
171
- relationship: relation.relationship,
172
- note: relation.note
173
- })),
174
- compoundChildren: [],
175
- guidance: {
176
- when: value.usage?.when ?? [],
177
- whenNot: value.usage?.whenNot ?? [],
178
- guidelines: value.usage?.guidelines ?? [],
179
- accessibility: value.usage?.accessibility ?? [],
180
- dos: value.usage?.when ?? [],
181
- donts: value.usage?.whenNot ?? [],
182
- patterns: []
183
- },
184
- sourceType: "fragments-json",
185
- sourcePath: value.sourcePath ?? value.filePath,
186
- metadata: {
187
- a11yRules: value.contract?.a11yRules ?? [],
188
- scenarioTags: value.contract?.scenarioTags ?? []
189
- }
190
- };
191
- }
192
- function buildComponentIndex(fragments) {
193
- const db = create({ schema: componentSchema, language: "english" });
194
- const normalized = fragments.map(normalizeComponent);
195
- const docs = normalized.map((f) => ({
196
- name: f.name,
197
- description: f.description ?? "",
198
- category: f.category ?? "",
199
- tags: (f.tags ?? []).join(" "),
200
- whenUsed: (f.guidance.when ?? []).join(" "),
201
- patterns: (f.guidance.patterns ?? []).map(
202
- (pattern) => `${pattern.name} ${pattern.description || ""}`
203
- ).join(" "),
204
- variants: f.examples.map(
205
- (example) => `${example.name} ${example.description || ""}`
206
- ).join(" "),
207
- status: f.status ?? "stable"
208
- }));
209
- insertMultiple(db, docs);
210
- return db;
211
- }
212
- function searchComponents(query, index, fragments, limit = 50) {
213
- const normalized = fragments.map(normalizeComponent);
214
- const boostConfig = {
215
- mode: "fulltext",
216
- properties: ["name", "whenUsed", "description", "patterns", "category", "tags", "variants"],
217
- boost: {
218
- name: 3,
219
- whenUsed: 2.5,
220
- description: 2,
221
- patterns: 1.5,
222
- category: 1.5,
223
- tags: 1.5,
224
- variants: 1
225
- },
226
- limit
227
- };
228
- const originalTermsList = query.toLowerCase().split(/\s+/).filter(Boolean);
229
- const originalTermsQuery = originalTermsList.join(" ");
230
- const expandedQuery = expandQuery(query);
231
- const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });
232
- const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });
233
- const origHits = originalResults.hits;
234
- const expHits = expandedResults.hits;
235
- const scoreMap = /* @__PURE__ */ new Map();
236
- for (const hit of origHits) {
237
- scoreMap.set(hit.document.name, (hit.score || 0) * 2);
238
- }
239
- for (const hit of expHits) {
240
- const name = hit.document.name;
241
- const existing = scoreMap.get(name) ?? 0;
242
- scoreMap.set(name, existing + (hit.score || 0));
243
- }
244
- const fragmentMap = /* @__PURE__ */ new Map();
245
- for (const f of normalized) {
246
- fragmentMap.set(f.name.toLowerCase(), f);
247
- }
248
- const originalTermsSet = new Set(originalTermsList);
249
- const scored = [];
250
- for (const [name, rawScore] of scoreMap) {
251
- let score = rawScore;
252
- const nameLower = name.toLowerCase();
253
- const fragment = fragmentMap.get(nameLower);
254
- if (originalTermsSet.has(nameLower)) {
255
- score += 25;
256
- }
257
- if (fragment) {
258
- if (fragment.status === "stable") score += 5;
259
- else if (fragment.status === "beta") score += 2;
260
- if (fragment.status === "deprecated") score -= 25;
261
- }
262
- if (score > 0) {
263
- scored.push({ name, kind: "component", rank: scored.length, score });
264
- }
265
- }
266
- scored.sort((a, b) => b.score - a.score);
267
- scored.forEach((s, i) => {
268
- s.rank = i;
269
- });
270
- return scored;
271
- }
272
- var blockSchema = {
273
- name: "string",
274
- description: "string",
275
- category: "string",
276
- tags: "string",
277
- components: "string"
278
- };
279
- function normalizeBlock(value) {
280
- if ("id" in value) return value;
281
- return {
282
- id: value.filePath ?? value.name,
283
- name: value.name,
284
- description: value.description ?? "",
285
- category: value.category ?? "uncategorized",
286
- components: value.components ?? [],
287
- tags: value.tags ?? [],
288
- code: value.code ?? ""
289
- };
290
- }
291
- function buildBlockIndex(blocks) {
292
- const db = create({ schema: blockSchema, language: "english" });
293
- const normalized = blocks.map(normalizeBlock);
294
- const docs = normalized.map((b) => ({
295
- name: b.name,
296
- description: b.description ?? "",
297
- category: b.category ?? "",
298
- tags: (b.tags ?? []).join(" "),
299
- components: b.components.join(" ")
300
- }));
301
- insertMultiple(db, docs);
302
- return db;
303
- }
304
- function searchBlocks(query, index, limit = 50) {
305
- return twoPassSearch({
306
- index,
307
- query,
308
- properties: ["name", "description", "components", "tags", "category"],
309
- boost: {
310
- name: 3,
311
- description: 2,
312
- components: 1.5,
313
- tags: 1.5,
314
- category: 1.5
315
- },
316
- limit,
317
- kind: "block"
318
- });
319
- }
320
- var tokenSchema = {
321
- name: "string",
322
- category: "string",
323
- description: "string"
324
- };
325
- function normalizeTokenData(tokenData) {
326
- if ("flat" in tokenData) return tokenData;
327
- const categories = Object.fromEntries(
328
- Object.entries(tokenData.categories).map(([category, entries]) => [
329
- category,
330
- entries.map((entry) => ({
331
- name: entry.name,
332
- category,
333
- value: typeof entry.value === "string" ? entry.value : void 0,
334
- description: entry.description
335
- }))
336
- ])
337
- );
338
- return {
339
- prefix: tokenData.prefix,
340
- total: tokenData.total,
341
- categories,
342
- flat: Object.values(categories).flat()
343
- };
344
- }
345
- function buildTokenIndex(tokenData) {
346
- const db = create({ schema: tokenSchema, language: "english" });
347
- const normalizedData = normalizeTokenData(tokenData);
348
- const docs = [];
349
- for (const [cat, tokens] of Object.entries(normalizedData.categories)) {
350
- for (const token of tokens) {
351
- docs.push({
352
- name: token.name,
353
- category: cat,
354
- description: token.description ?? ""
355
- });
356
- }
357
- }
358
- insertMultiple(db, docs);
359
- return db;
360
- }
361
- function searchTokens(query, index, limit = 50) {
362
- return twoPassSearch({
363
- index,
364
- query,
365
- properties: ["name", "category", "description"],
366
- boost: {
367
- name: 2.5,
368
- category: 2,
369
- description: 1.5
370
- },
371
- limit,
372
- kind: "token"
373
- });
374
- }
375
- var USE_CASE_TOKEN_CATEGORIES = {
376
- "table": ["spacing", "borders", "surfaces", "text"],
377
- "data": ["spacing", "borders", "surfaces"],
378
- "grid": ["spacing", "layout"],
379
- "form": ["spacing", "borders", "radius", "focus"],
380
- "input": ["spacing", "borders", "radius", "focus"],
381
- "card": ["surfaces", "shadows", "radius", "borders", "spacing"],
382
- "button": ["colors", "radius", "spacing", "focus"],
383
- "layout": ["spacing", "layout", "surfaces"],
384
- "dashboard": ["spacing", "surfaces", "borders", "shadows"],
385
- "chat": ["spacing", "surfaces", "radius", "shadows"],
386
- "modal": ["shadows", "surfaces", "radius", "spacing"],
387
- "dialog": ["shadows", "surfaces", "radius", "spacing"],
388
- "navigation": ["spacing", "surfaces", "borders"],
389
- "sidebar": ["spacing", "surfaces", "borders"],
390
- "hero": ["spacing", "typography", "colors"],
391
- "landing": ["spacing", "typography", "colors"],
392
- "pricing": ["spacing", "surfaces", "borders", "radius"],
393
- "auth": ["spacing", "borders", "radius", "focus"],
394
- "login": ["spacing", "borders", "radius", "focus"],
395
- "dark": ["colors", "surfaces"],
396
- "theme": ["colors", "surfaces", "text"]
397
- };
398
- function extractTokenCategories(query) {
399
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
400
- const categories = /* @__PURE__ */ new Set();
401
- for (const term of terms) {
402
- const cats = USE_CASE_TOKEN_CATEGORIES[term];
403
- if (cats) {
404
- for (const cat of cats) categories.add(cat);
405
- }
406
- }
407
- if (categories.size === 0) {
408
- return ["spacing", "colors", "surfaces"];
409
- }
410
- return Array.from(categories);
411
- }
412
-
413
- // src/version.ts
414
- import { readFileSync as readFileSync2 } from "fs";
415
- function readPackageVersion() {
416
- try {
417
- const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
418
- const pkg = JSON.parse(raw);
419
- return pkg.version ?? "0.0.0";
420
- } catch {
421
- return "0.0.0";
422
- }
423
- }
424
- var MCP_SERVER_VERSION = readPackageVersion();
425
-
426
- // src/search.ts
427
- var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
428
- var CONVEX_TIMEOUT_MS = 3e3;
429
- async function searchConvex(query, apiKey, limit = 10, kind) {
430
- try {
431
- const controller = new AbortController();
432
- const timeout = setTimeout(() => controller.abort(), CONVEX_TIMEOUT_MS);
433
- const response = await fetch(CONVEX_SEARCH_URL, {
434
- method: "POST",
435
- headers: {
436
- "Content-Type": "application/json",
437
- "Authorization": `Bearer ${apiKey}`
438
- },
439
- body: JSON.stringify({ query, limit, ...kind && { kind } }),
440
- signal: controller.signal
441
- });
442
- clearTimeout(timeout);
443
- if (!response.ok) {
444
- return [];
445
- }
446
- const data = await response.json();
447
- return data.results.map((r, i) => ({
448
- name: r.name,
449
- kind: r.kind ?? "component",
450
- rank: i,
451
- score: r.score
452
- }));
453
- } catch {
454
- return [];
455
- }
456
- }
457
- function keywordScoreComponents(query, fragments, componentIndex) {
458
- const index = componentIndex ?? buildComponentIndex(fragments);
459
- return searchComponents(query, index, fragments);
460
- }
461
- function keywordScoreBlocks(query, blocks, blockIndex) {
462
- const index = blockIndex ?? buildBlockIndex(blocks);
463
- return searchBlocks(query, index);
464
- }
465
- function keywordScoreTokens(query, tokenData, tokenIndex) {
466
- const index = tokenIndex ?? buildTokenIndex(tokenData);
467
- return searchTokens(query, index);
468
- }
469
- function reciprocalRankFusion(resultSets, k = 60) {
470
- const scoreMap = /* @__PURE__ */ new Map();
471
- for (const { results } of resultSets) {
472
- for (let rank = 0; rank < results.length; rank++) {
473
- const result2 = results[rank];
474
- const key = `${result2.kind}:${result2.name}`;
475
- const rrfScore = 1 / (k + rank + 1);
476
- const existing = scoreMap.get(key);
477
- if (existing) {
478
- existing.score += rrfScore;
479
- } else {
480
- scoreMap.set(key, { score: rrfScore, kind: result2.kind, name: result2.name });
481
- }
482
- }
483
- }
484
- const fused = [];
485
- for (const [, { score, kind, name }] of scoreMap) {
486
- fused.push({ name, kind, rank: 0, score });
487
- }
488
- fused.sort((a, b) => b.score - a.score);
489
- fused.forEach((r, i) => {
490
- r.rank = i;
491
- });
492
- return fused;
493
- }
494
- async function hybridSearch(query, data, limit = 10, kind, apiKey) {
495
- const keywordResults = [];
496
- if (!kind || kind === "component") {
497
- keywordResults.push(...keywordScoreComponents(query, data.fragments, data.componentIndex));
498
- }
499
- if ((!kind || kind === "block") && data.blocks) {
500
- keywordResults.push(...keywordScoreBlocks(query, data.blocks, data.blockIndex));
501
- }
502
- if ((!kind || kind === "token") && data.tokenData) {
503
- keywordResults.push(...keywordScoreTokens(query, data.tokenData, data.tokenIndex));
504
- }
505
- keywordResults.sort((a, b) => b.score - a.score);
506
- keywordResults.forEach((r, i) => {
507
- r.rank = i;
508
- });
509
- if (!apiKey) {
510
- return keywordResults.slice(0, limit);
511
- }
512
- const vectorResults = await searchConvex(query, apiKey, limit, kind);
513
- if (vectorResults.length === 0) {
514
- return keywordResults.slice(0, limit);
515
- }
516
- const graphBoostResults = [];
517
- if (data.graph) {
518
- try {
519
- const { ComponentGraphEngine: ComponentGraphEngine2, deserializeGraph: deserializeGraph2 } = await import("@fragments-sdk/context/graph");
520
- const graph = deserializeGraph2(data.graph);
521
- const engine = new ComponentGraphEngine2(graph);
522
- const topComponents = [...keywordResults, ...vectorResults].filter((r) => r.kind === "component").slice(0, 5);
523
- const neighborSet = /* @__PURE__ */ new Set();
524
- for (const result2 of topComponents) {
525
- const neighbors = engine.neighbors(result2.name, 1);
526
- for (const n of neighbors.neighbors) {
527
- if (!neighborSet.has(n.component)) {
528
- neighborSet.add(n.component);
529
- graphBoostResults.push({
530
- name: n.component,
531
- kind: "component",
532
- rank: graphBoostResults.length,
533
- score: 1
534
- // Will be normalized through RRF
535
- });
536
- }
537
- }
538
- }
539
- } catch {
540
- }
541
- }
542
- const resultSets = [
543
- { label: "vector", results: vectorResults },
544
- { label: "keyword", results: keywordResults }
545
- ];
546
- if (graphBoostResults.length > 0) {
547
- resultSets.push({ label: "graph", results: graphBoostResults });
548
- }
549
- const fused = reciprocalRankFusion(resultSets);
550
- return fused.slice(0, limit);
551
- }
552
-
553
- // src/scoring.ts
554
- var MINIMUM_SCORE_THRESHOLD = 5;
555
- function assignConfidence(score, maxScore) {
556
- if (maxScore <= 0) return "low";
557
- const ratio = score / maxScore;
558
- if (ratio >= 0.7) return "high";
559
- if (ratio >= 0.4) return "medium";
560
- return "low";
561
- }
562
- function meetsMinimumThreshold(maxScore) {
563
- return maxScore >= MINIMUM_SCORE_THRESHOLD;
564
- }
565
- function levenshtein(a, b) {
566
- const la = a.length;
567
- const lb = b.length;
568
- const dp = Array.from({ length: lb + 1 }, (_, i) => i);
569
- for (let i = 1; i <= la; i++) {
570
- let prev = i - 1;
571
- dp[0] = i;
572
- for (let j = 1; j <= lb; j++) {
573
- const temp = dp[j];
574
- dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
575
- prev = temp;
576
- }
577
- }
578
- return dp[lb];
579
- }
580
- function findClosestMatch(input, candidates, maxDistance = 3) {
581
- const inputLower = input.toLowerCase();
582
- let bestMatch = null;
583
- let bestDist = maxDistance + 1;
584
- for (const candidate of candidates) {
585
- const candidateLower = candidate.toLowerCase();
586
- const dist = levenshtein(inputLower, candidateLower);
587
- if (dist < bestDist) {
588
- bestDist = dist;
589
- bestMatch = candidate;
590
- } else if (dist === bestDist && bestMatch) {
591
- const currentLenDiff = Math.abs(bestMatch.length - input.length);
592
- const newLenDiff = Math.abs(candidate.length - input.length);
593
- if (newLenDiff < currentLenDiff) {
594
- bestMatch = candidate;
595
- }
596
- }
597
- }
598
- return bestDist <= maxDistance ? bestMatch : null;
599
- }
600
- var BLOCK_BOOST_PER_OCCURRENCE = 5;
601
- function buildBlockComponentFrequency(blocks) {
602
- const freq = /* @__PURE__ */ new Map();
603
- for (const block of blocks) {
604
- for (const comp of block.components) {
605
- const key = comp.toLowerCase();
606
- freq.set(key, (freq.get(key) ?? 0) + 1);
607
- }
608
- }
609
- return freq;
610
- }
611
- function boostByBlockFrequency(results, freq) {
612
- for (const result2 of results) {
613
- const count = freq.get(result2.name.toLowerCase()) ?? 0;
614
- if (count > 0) {
615
- result2.score += count * BLOCK_BOOST_PER_OCCURRENCE;
616
- }
617
- }
618
- results.sort((a, b) => b.score - a.score);
619
- results.forEach((r, i) => {
620
- r.rank = i;
621
- });
622
- return results;
623
- }
624
-
625
- // src/server-helpers.ts
626
- function normalizeFilter(value) {
627
- const normalized = value?.trim().toLowerCase();
628
- return normalized && normalized.length > 0 ? normalized : void 0;
629
- }
630
- function categoryMatches(category, categoryFilter) {
631
- if (!categoryFilter) return true;
632
- return normalizeFilter(category) === categoryFilter;
633
- }
634
- function buildLocalSearchData(data, indexes) {
635
- const isLegacy = "fragments" in data;
636
- const allFragments = Object.values(
637
- isLegacy ? data.fragments : data.components
638
- );
639
- const allBlocks = Object.values(
640
- isLegacy ? data.blocks ?? data.recipes ?? {} : data.blocks ?? {}
641
- );
642
- const tokens = isLegacy ? data.tokens : data.tokens;
643
- const graph = isLegacy ? data.graph : data.graph;
644
- const localData = {
645
- fragments: allFragments,
646
- blocks: allBlocks,
647
- tokenData: tokens,
648
- graph,
649
- componentIndex: indexes.componentIndex ?? void 0,
650
- blockIndex: indexes.blockIndex ?? void 0,
651
- tokenIndex: indexes.tokenIndex ?? void 0
652
- };
653
- return { allFragments, allBlocks, localData };
654
- }
655
- async function buildImportStatements(components, resolvePackageName) {
656
- const grouped = /* @__PURE__ */ new Map();
657
- const uniqueComponents = [...new Set(components.filter(Boolean))];
658
- const resolvedPackages = await Promise.all(
659
- uniqueComponents.map(async (component) => ({
660
- component,
661
- packageName: await resolvePackageName(component)
662
- }))
663
- );
664
- for (const { component, packageName } of resolvedPackages) {
665
- const existing = grouped.get(packageName);
666
- if (!existing) {
667
- grouped.set(packageName, [component]);
668
- continue;
669
- }
670
- existing.push(component);
671
- }
672
- return Array.from(grouped.entries()).map(
673
- ([packageName, componentNames2]) => `import { ${componentNames2.join(", ")} } from '${packageName}';`
674
- );
675
- }
676
- function limitTokensPerCategory(categories, limit) {
677
- if (limit === void 0) {
678
- return {
679
- categories,
680
- total: Object.values(categories).reduce((sum, tokens) => sum + tokens.length, 0)
681
- };
682
- }
683
- const limited = {};
684
- let total = 0;
685
- for (const [category, tokens] of Object.entries(categories)) {
686
- const sliced = tokens.slice(0, limit);
687
- if (sliced.length === 0) continue;
688
- limited[category] = sliced;
689
- total += sliced.length;
690
- }
691
- return { categories: limited, total };
692
- }
693
-
694
- // src/search-helpers.ts
695
- var STOP_WORDS = /* @__PURE__ */ new Set([
696
- "a",
697
- "an",
698
- "and",
699
- "bar",
700
- "build",
701
- "button",
702
- "for",
703
- "form",
704
- "i",
705
- "login",
706
- "me",
707
- "need",
708
- "of",
709
- "or",
710
- "the",
711
- "to",
712
- "use",
713
- "with"
714
- ]);
715
- function normalizeTerms(value) {
716
- return value.toLowerCase().split(/[^a-z0-9]+/g).filter((term) => term.length > 1 && !STOP_WORDS.has(term));
717
- }
718
- function hasDirectQueryOverlap(query, component) {
719
- const queryTerms = normalizeTerms(query);
720
- if (queryTerms.length === 0) return true;
721
- const haystack = new Set(
722
- normalizeTerms(
723
- [
724
- component.name,
725
- component.description,
726
- component.category,
727
- component.tags.join(" "),
728
- getGuidanceWhen(component).join(" "),
729
- getGuidanceWhenNot(component).join(" "),
730
- component.propsSummary.join(" ")
731
- ].join(" ")
732
- )
733
- );
734
- return queryTerms.some((term) => haystack.has(term));
735
- }
736
- function getRankingBonus(component) {
737
- let bonus = 0;
738
- if (component.isCanonical) bonus += 30;
739
- if (component.tier === "core") bonus += 20;
740
- if (component.status === "stable") bonus += 5;
741
- return bonus;
742
- }
743
-
744
- // src/tools/discover.ts
745
- function renderContextMarkdown(args) {
746
- const lines = ["# Design System Context", ""];
747
- for (const component of args.components) {
748
- lines.push(`## ${component.name}`);
749
- if (component.description) {
750
- lines.push(component.description);
751
- }
752
- lines.push(
753
- `- Category: ${component.category}`,
754
- `- Status: ${component.status}`
755
- );
756
- if (component.propsSummary.length > 0) {
757
- lines.push(`- Props: ${component.propsSummary.join(", ")}`);
758
- }
759
- const when = getGuidanceWhen(component);
760
- if (when.length > 0) {
761
- lines.push(`- Use when: ${when.slice(0, args.compact ? 1 : 3).join("; ")}`);
762
- }
763
- const whenNot = getGuidanceWhenNot(component);
764
- if (whenNot.length > 0) {
765
- lines.push(
766
- `- Avoid when: ${whenNot.slice(0, args.compact ? 1 : 2).join("; ")}`
767
- );
768
- }
769
- if (args.includeRelations && component.relations.length > 0) {
770
- lines.push(
771
- `- Related: ${component.relations.slice(0, 5).map(
772
- (relation) => `${relation.componentName} (${relation.relationship})`
773
- ).join(", ")}`
774
- );
775
- }
776
- if (args.includeCode && component.examples[0]?.code) {
777
- lines.push("", "```tsx", component.examples[0].code, "```");
778
- }
779
- lines.push("");
780
- }
781
- if (args.blocks.length > 0) {
782
- lines.push("## Blocks", "");
783
- for (const block of args.blocks) {
784
- lines.push(`- ${block.name}: ${block.description}`);
785
- }
786
- lines.push("");
787
- }
788
- return lines.join("\n").trim();
789
- }
790
- function renderContextJson(args) {
791
- return JSON.stringify({
792
- components: args.components.map((component) => ({
793
- name: component.name,
794
- description: component.description,
795
- category: component.category,
796
- status: component.status,
797
- propsSummary: component.propsSummary,
798
- when: getGuidanceWhen(component),
799
- whenNot: getGuidanceWhenNot(component),
800
- ...args.includeRelations && { relations: component.relations },
801
- ...args.includeCode && {
802
- examples: component.examples.filter((example) => Boolean(example.code)).slice(0, 2)
803
- }
804
- })),
805
- blocks: args.blocks
806
- });
807
- }
808
- var discoverHandler = async (args, ctx) => {
809
- const data = ctx.data;
810
- const snapshotComponents = listComponents(data.snapshot);
811
- const componentsByName = new Map(
812
- snapshotComponents.map((component) => [
813
- component.name.toLowerCase(),
814
- component
815
- ])
816
- );
817
- const useCase = args?.useCase ?? void 0;
818
- const componentForAlts = args?.component ?? void 0;
819
- const category = normalizeFilter(args?.category);
820
- const search2 = args?.search?.toLowerCase() ?? void 0;
821
- const status = args?.status ?? void 0;
822
- const format = args?.format ?? "markdown";
823
- const compact = args?.compact ?? false;
824
- const includeCode = args?.includeCode ?? false;
825
- const includeRelations = args?.includeRelations ?? false;
826
- const depth = args?.depth ?? "quick";
827
- const suggestLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : 10;
828
- const listLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : void 0;
829
- const verbosity = args?.verbosity ?? (compact ? "compact" : "standard");
830
- const allSnapshotComponents = listComponents(data.snapshot);
831
- if (!useCase && !componentForAlts && (args?.format || includeCode || includeRelations)) {
832
- let components2 = allSnapshotComponents;
833
- const allBlocks = listBlocks(data.snapshot);
834
- if (category) {
835
- components2 = components2.filter(
836
- (component) => categoryMatches(component.category, category)
837
- );
838
- }
839
- if (search2) {
840
- const scored = keywordScoreComponents(
841
- search2,
842
- components2,
843
- ctx.indexes.componentIndex ?? void 0
844
- );
845
- const allowedNames = new Set(components2.map((component) => component.name));
846
- const sortedNames = scored.filter((result2) => allowedNames.has(result2.name)).map((result2) => result2.name.toLowerCase());
847
- components2 = components2.filter((component) => sortedNames.includes(component.name.toLowerCase())).sort(
848
- (a, b) => sortedNames.indexOf(a.name.toLowerCase()) - sortedNames.indexOf(b.name.toLowerCase())
849
- );
850
- }
851
- if (status) {
852
- components2 = components2.filter((component) => component.status === status);
853
- }
854
- const blocks = allBlocks.map((block) => ({
855
- name: block.name,
856
- description: block.description
857
- }));
858
- const ctxContent = format === "json" ? renderContextJson({
859
- components: components2,
860
- blocks,
861
- includeCode: includeCode || verbosity === "full",
862
- includeRelations
863
- }) : renderContextMarkdown({
864
- components: components2,
865
- blocks,
866
- includeCode: includeCode || verbosity === "full",
867
- includeRelations,
868
- compact: verbosity === "compact"
869
- });
870
- return {
871
- content: [{ type: "text", text: ctxContent }],
872
- _meta: {
873
- componentCount: components2.length,
874
- blockCount: blocks.length
875
- }
876
- };
877
- }
878
- if (useCase) {
879
- const { allFragments, allBlocks, localData } = buildLocalSearchData(
880
- {
881
- components: data.components,
882
- blocks: data.blocks,
883
- tokens: data.tokens,
884
- graph: data.graph
885
- },
886
- {
887
- componentIndex: ctx.indexes.componentIndex,
888
- blockIndex: ctx.indexes.blockIndex,
889
- tokenIndex: ctx.indexes.tokenIndex
890
- }
891
- );
892
- const context = args?.context?.toLowerCase() ?? "";
893
- const fullQuery = context ? `${useCase} ${context}` : useCase;
894
- const searchResults = await hybridSearch(
895
- fullQuery,
896
- localData,
897
- suggestLimit,
898
- "component",
899
- ctx.config.searchApiKey
900
- );
901
- const filteredSearchResults = searchResults.filter((result2) => {
902
- const component = componentsByName.get(result2.name.toLowerCase());
903
- if (!component) return false;
904
- if (category && !categoryMatches(component.category, category)) return false;
905
- if (status && component.status !== status) return false;
906
- result2.score += getRankingBonus(component);
907
- return true;
908
- });
909
- const blockMatches = keywordScoreBlocks(
910
- fullQuery,
911
- allBlocks,
912
- ctx.indexes.blockIndex ?? void 0
913
- ).slice(0, 5);
914
- if (blockMatches.length > 0) {
915
- const matchedBlocks = blockMatches.map(
916
- (match) => allBlocks.find((block) => block.name.toLowerCase() === match.name.toLowerCase())
917
- ).filter(Boolean);
918
- const blockFreq = buildBlockComponentFrequency(matchedBlocks);
919
- boostByBlockFrequency(filteredSearchResults, blockFreq);
920
- }
921
- const maxScore = filteredSearchResults.length > 0 ? filteredSearchResults[0].score : 0;
922
- const scored = filteredSearchResults.map((result2) => {
923
- const component = componentsByName.get(result2.name.toLowerCase());
924
- if (!component) return null;
925
- return {
926
- component: component.name,
927
- category: component.category,
928
- description: component.description,
929
- confidence: assignConfidence(result2.score, maxScore),
930
- reasons: [`Matched via hybrid search (score: ${result2.score.toFixed(4)})`],
931
- usage: {
932
- when: getGuidanceWhen(component).slice(0, 3),
933
- whenNot: getGuidanceWhenNot(component).slice(0, 2)
934
- },
935
- publicRef: component.publicRef,
936
- componentKey: component.id,
937
- tier: component.tier,
938
- isCanonical: component.isCanonical ?? false,
939
- sourcePath: component.sourcePath,
940
- exampleCount: component.examples.length,
941
- status: component.status
942
- };
943
- }).filter(Boolean);
944
- const suggestions = [];
945
- const categoryCount = {};
946
- for (const item of scored) {
947
- if (!item) continue;
948
- const cat = item.category || "uncategorized";
949
- const count = categoryCount[cat] || 0;
950
- if (count < 2 || suggestions.length < 3) {
951
- suggestions.push(item);
952
- categoryCount[cat] = count + 1;
953
- if (suggestions.length >= suggestLimit) break;
954
- }
955
- }
956
- const compositionHint = suggestions.length >= 2 ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map((item) => item.component).join(" and ")}.` : void 0;
957
- const useCaseLower = useCase.toLowerCase();
958
- const styleKeywords = [
959
- "color",
960
- "spacing",
961
- "padding",
962
- "margin",
963
- "font",
964
- "border",
965
- "radius",
966
- "shadow",
967
- "variable",
968
- "token",
969
- "css",
970
- "theme",
971
- "dark mode",
972
- "background",
973
- "hover"
974
- ];
975
- const isStyleQuery = styleKeywords.some(
976
- (keyword) => useCaseLower.includes(keyword)
977
- );
978
- const noMatch = suggestions.length === 0 || !suggestions.some((item) => {
979
- const component = componentsByName.get(item.component.toLowerCase());
980
- return component ? hasDirectQueryOverlap(useCase, component) : false;
981
- });
982
- const belowThreshold = !noMatch && maxScore > 1 && !meetsMinimumThreshold(maxScore);
983
- const weakMatch = !noMatch && (belowThreshold || suggestions.every((item) => item.confidence === "low"));
984
- let recommendation;
985
- let nextStep;
986
- if (noMatch) {
987
- recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${ctx.toolNames.tokens} to find tokens.` : `No matching components found. Try different keywords or browse all components with ${ctx.toolNames.discover}.`;
988
- nextStep = isStyleQuery ? `Use ${ctx.toolNames.tokens}(search: "${useCaseLower.split(/\s+/)[0]}") to find tokens.` : void 0;
989
- } else if (weakMatch) {
990
- recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need tokens, try ${ctx.toolNames.tokens}.` : ""}`;
991
- nextStep = `Use ${ctx.toolNames.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
992
- } else {
993
- recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
994
- nextStep = `Use ${ctx.toolNames.inspect}("${suggestions[0].component}") for full details.`;
995
- }
996
- const tokenHint = isStyleQuery && !noMatch ? `Your query includes styling terms. For tokens, also try ${ctx.toolNames.tokens}(search: "${useCaseLower.split(/\s+/)[0]}").` : void 0;
997
- const blockNames = blockMatches.map(
998
- (match) => allBlocks.find((block) => block.name.toLowerCase() === match.name.toLowerCase())
999
- ).filter(Boolean).slice(0, 3).map((block) => block.name);
1000
- const blockHint = blockNames.length > 0 ? `Related blocks: ${blockNames.join(", ")}. Use ${ctx.toolNames.blocks}(search: "${useCase}") for ready-to-use patterns.` : void 0;
1001
- let fullBlocks;
1002
- let fullTokens;
1003
- let fullImports;
1004
- if (depth === "full" && !noMatch) {
1005
- const tokenData = ctx.data.tokens;
1006
- const [blockSearchResults, tokenSearchResults] = await Promise.all([
1007
- hybridSearch(fullQuery, localData, 5, "block", ctx.config.searchApiKey),
1008
- tokenData ? hybridSearch(fullQuery, localData, 10, "token", ctx.config.searchApiKey) : Promise.resolve([])
1009
- ]);
1010
- const topBlockScore = blockSearchResults.length > 0 ? blockSearchResults[0].score : 0;
1011
- const relevantBlockResults = blockSearchResults.filter(
1012
- (result2) => result2.score >= topBlockScore * 0.3
1013
- );
1014
- if (relevantBlockResults.length > 0) {
1015
- fullBlocks = (await Promise.all(
1016
- relevantBlockResults.slice(0, 5).map(async (result2) => {
1017
- const block = allBlocks.find(
1018
- (entry) => entry.name.toLowerCase() === result2.name.toLowerCase()
1019
- );
1020
- if (!block) return null;
1021
- const imports = await buildImportStatements(
1022
- block.components,
1023
- async (componentName) => ctx.resolvePackageName(componentName)
1024
- );
1025
- const codeLines = block.code.split("\n");
1026
- const code = codeLines.length > 30 ? `${codeLines.slice(0, 20).join("\n")}
1027
- // ... truncated (${codeLines.length} lines total)` : block.code;
1028
- return {
1029
- name: block.name,
1030
- description: block.description,
1031
- components: block.components,
1032
- code,
1033
- imports
1034
- };
1035
- })
1036
- )).filter(Boolean);
1037
- }
1038
- if (tokenSearchResults.length > 0 && tokenData) {
1039
- fullTokens = {};
1040
- const tokensByName = /* @__PURE__ */ new Map();
1041
- for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1042
- for (const token of tokens) {
1043
- tokensByName.set(token.name, cat);
1044
- }
1045
- }
1046
- for (const result2 of tokenSearchResults) {
1047
- const cat = tokensByName.get(result2.name);
1048
- if (cat) {
1049
- if (!fullTokens[cat]) fullTokens[cat] = [];
1050
- fullTokens[cat].push(result2.name);
1051
- }
1052
- }
1053
- if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
1054
- }
1055
- if (!fullTokens && tokenData) {
1056
- const categories = extractTokenCategories(fullQuery);
1057
- fullTokens = {};
1058
- for (const cat of categories) {
1059
- const tokens = tokenData.categories[cat];
1060
- if (tokens && tokens.length > 0) {
1061
- fullTokens[cat] = tokens.slice(0, 5).map((token) => token.name);
1062
- }
1063
- }
1064
- if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
1065
- }
1066
- if (suggestions.length > 0) {
1067
- fullImports = {};
1068
- for (const item of suggestions) {
1069
- if (!item) continue;
1070
- const pkgName = ctx.resolvePackageName(item.component);
1071
- fullImports[item.component] = `import { ${item.component} } from '${pkgName}';`;
1072
- }
1073
- }
1074
- }
1075
- const suggestResponse = verbosity === "compact" ? {
1076
- useCase,
1077
- suggestions: suggestions.map((item) => ({
1078
- component: item.component,
1079
- description: item.description,
1080
- confidence: item.confidence
1081
- })),
1082
- recommendation
1083
- } : {
1084
- useCase,
1085
- context: context || void 0,
1086
- suggestions: depth === "full" ? suggestions.map((item) => ({
1087
- ...item,
1088
- import: fullImports?.[item.component]
1089
- })) : suggestions,
1090
- noMatch,
1091
- weakMatch,
1092
- recommendation,
1093
- compositionHint,
1094
- ...tokenHint && { tokenHint },
1095
- ...blockHint && { blockHint },
1096
- nextStep,
1097
- ...fullBlocks && fullBlocks.length > 0 && { blocks: fullBlocks },
1098
- ...fullTokens && { tokens: fullTokens }
1099
- };
1100
- return {
1101
- content: [
1102
- {
1103
- type: "text",
1104
- text: JSON.stringify(suggestResponse)
1105
- }
1106
- ]
1107
- };
1108
- }
1109
- if (componentForAlts) {
1110
- const component = findComponent(data.snapshot, componentForAlts);
1111
- if (!component) {
1112
- const closest = findClosestMatch(
1113
- componentForAlts,
1114
- componentNames(data.snapshot)
1115
- );
1116
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1117
- return {
1118
- content: [
1119
- {
1120
- type: "text",
1121
- text: JSON.stringify({
1122
- error: `Component "${componentForAlts}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
1123
- })
1124
- }
1125
- ],
1126
- isError: true
1127
- };
1128
- }
1129
- const relations = component.relations;
1130
- const referencedBy = allSnapshotComponents.map((entry) => {
1131
- const relation = entry.relations.find(
1132
- (candidate) => candidate.componentName.toLowerCase() === component.name.toLowerCase()
1133
- );
1134
- if (!relation) return null;
1135
- return {
1136
- component: entry.name,
1137
- relationship: relation.relationship,
1138
- note: relation.note
1139
- };
1140
- }).filter(Boolean);
1141
- const sameCategory = allSnapshotComponents.filter(
1142
- (entry) => entry.category === component.category && entry.name.toLowerCase() !== component.name.toLowerCase()
1143
- ).map((entry) => ({
1144
- component: entry.name,
1145
- description: entry.description
1146
- }));
1147
- return {
1148
- content: [
1149
- {
1150
- type: "text",
1151
- text: JSON.stringify({
1152
- component: component.name,
1153
- category: component.category,
1154
- directRelations: relations,
1155
- referencedBy,
1156
- sameCategory,
1157
- suggestion: relations.find(
1158
- (relation) => relation.relationship === "alternative"
1159
- ) ? `Consider ${relations.find((relation) => relation.relationship === "alternative")?.componentName}: ${relations.find((relation) => relation.relationship === "alternative")?.note}` : void 0
1160
- })
1161
- }
1162
- ]
1163
- };
1164
- }
1165
- let filteredComponents = allSnapshotComponents.filter((component) => {
1166
- if (category && !categoryMatches(component.category, category)) return false;
1167
- if (status && (component.status ?? "stable") !== status) return false;
1168
- return true;
1169
- });
1170
- if (search2) {
1171
- const scored = keywordScoreComponents(
1172
- search2,
1173
- filteredComponents,
1174
- ctx.indexes.componentIndex ?? void 0
1175
- ).map((result2) => {
1176
- const component = filteredComponents.find(
1177
- (entry) => entry.name.toLowerCase() === result2.name.toLowerCase()
1178
- );
1179
- if (!component) return null;
1180
- return {
1181
- component,
1182
- score: result2.score + getRankingBonus(component)
1183
- };
1184
- }).filter(Boolean);
1185
- filteredComponents = scored.sort((a, b) => b.score - a.score).map((entry) => entry.component);
1186
- } else {
1187
- filteredComponents = filteredComponents.sort((a, b) => {
1188
- const bonusDiff = getRankingBonus(b) - getRankingBonus(a);
1189
- if (bonusDiff !== 0) return bonusDiff;
1190
- return a.name.localeCompare(b.name);
1191
- });
1192
- }
1193
- const limitedComponents = listLimit === void 0 ? filteredComponents : filteredComponents.slice(0, listLimit);
1194
- const components = limitedComponents.map((component) => {
1195
- if (verbosity === "compact") {
1196
- return {
1197
- name: component.name,
1198
- category: component.category,
1199
- publicRef: component.publicRef,
1200
- componentKey: component.id,
1201
- tier: component.tier,
1202
- isCanonical: component.isCanonical ?? false,
1203
- ...component.propsSummary.length > 0 && {
1204
- propsSummary: component.propsSummary
1205
- }
1206
- };
1207
- }
1208
- return {
1209
- name: component.name,
1210
- category: component.category,
1211
- description: component.description,
1212
- status: component.status ?? "stable",
1213
- publicRef: component.publicRef,
1214
- componentKey: component.id,
1215
- tier: component.tier,
1216
- isCanonical: component.isCanonical ?? false,
1217
- sourcePath: component.sourcePath,
1218
- exampleCount: component.examples.length,
1219
- tags: component.tags,
1220
- ...(includeCode || verbosity === "full") && component.examples[0]?.code ? {
1221
- example: component.examples[0].code
1222
- } : {}
1223
- };
1224
- });
1225
- return {
1226
- content: [
1227
- {
1228
- type: "text",
1229
- text: JSON.stringify({
1230
- total: filteredComponents.length,
1231
- returned: components.length,
1232
- components,
1233
- categories: [...new Set(components.map((component) => component.category))],
1234
- hint: components.length === 0 ? "No components found. Try broader search terms or check available categories." : components.length > 5 ? `Use ${ctx.toolNames.discover} with useCase for recommendations, or ${ctx.toolNames.inspect} for details on a specific component.` : void 0
1235
- })
1236
- }
1237
- ]
1238
- };
1239
- };
1240
-
1241
- // src/tools/inspect.ts
1242
- import { promises as fs } from "fs";
1243
- import { existsSync as existsSync2 } from "fs";
1244
- import { join as join2 } from "path";
1245
-
1246
- // src/utils.ts
1247
- function projectFields(obj, fields) {
1248
- if (!fields || fields.length === 0) {
1249
- return obj;
1250
- }
1251
- const result2 = {};
1252
- for (const field of fields) {
1253
- const parts = field.split(".");
1254
- let source = obj;
1255
- let target = result2;
1256
- for (let i = 0; i < parts.length; i++) {
1257
- const part = parts[i];
1258
- const isLast = i === parts.length - 1;
1259
- if (source === null || source === void 0 || typeof source !== "object") {
1260
- break;
1261
- }
1262
- const sourceObj = source;
1263
- const value = sourceObj[part];
1264
- if (isLast) {
1265
- target[part] = value;
1266
- } else {
1267
- if (!(part in target)) {
1268
- target[part] = {};
1269
- }
1270
- target = target[part];
1271
- source = value;
1272
- }
1273
- }
1274
- }
1275
- return result2;
1276
- }
1277
-
1278
- // src/tools/inspect.ts
1279
- async function getSourceCode(component, projectRoot) {
1280
- const sourcePath = component.sourcePath;
1281
- if (!sourcePath) return void 0;
1282
- const fullPath = join2(projectRoot, sourcePath);
1283
- if (!existsSync2(fullPath)) return { path: sourcePath, code: null };
1284
- try {
1285
- const code = await fs.readFile(fullPath, "utf-8");
1286
- return { path: sourcePath, code };
1287
- } catch {
1288
- return { path: sourcePath, code: null };
1289
- }
1290
- }
1291
- var inspectHandler = async (args, ctx) => {
1292
- const componentName = args?.component;
1293
- const fields = args?.fields;
1294
- const exampleName = args?.variant ?? void 0;
1295
- const maxExamples = args?.maxExamples;
1296
- const maxLines = args?.maxLines;
1297
- const verbosity = args?.verbosity ?? "standard";
1298
- if (!componentName) {
1299
- return {
1300
- content: [
1301
- {
1302
- type: "text",
1303
- text: JSON.stringify({ error: "component is required" })
1304
- }
1305
- ],
1306
- isError: true
1307
- };
1308
- }
1309
- const component = findComponent(ctx.data.snapshot, componentName);
1310
- if (!component) {
1311
- const closest = findClosestMatch(
1312
- componentName,
1313
- componentNames(ctx.data.snapshot)
1314
- );
1315
- const suggestion = closest ? ` Did you mean "${closest}"? Use ${ctx.toolNames.inspect}("${closest}") to inspect it.` : "";
1316
- return {
1317
- content: [
1318
- {
1319
- type: "text",
1320
- text: JSON.stringify({
1321
- error: `Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
1322
- })
1323
- }
1324
- ],
1325
- isError: true
1326
- };
1327
- }
1328
- const pkgName = ctx.resolvePackageName(component.name);
1329
- let examples = component.examples;
1330
- if (exampleName) {
1331
- const query = exampleName.toLowerCase();
1332
- let filtered = examples.filter((example) => example.name.toLowerCase() === query);
1333
- if (filtered.length === 0) {
1334
- filtered = examples.filter(
1335
- (example) => example.name.toLowerCase().startsWith(query)
1336
- );
1337
- }
1338
- if (filtered.length === 0) {
1339
- filtered = examples.filter(
1340
- (example) => example.name.toLowerCase().includes(query)
1341
- );
1342
- }
1343
- if (filtered.length > 0) {
1344
- examples = filtered;
1345
- } else {
1346
- return {
1347
- content: [
1348
- {
1349
- type: "text",
1350
- text: JSON.stringify({
1351
- error: `Example "${exampleName}" not found for ${componentName}. Available: ${component.examples.map((example) => example.name).join(", ")}`
1352
- })
1353
- }
1354
- ],
1355
- isError: true
1356
- };
1357
- }
1358
- }
1359
- if (maxExamples && maxExamples > 0) {
1360
- examples = examples.slice(0, maxExamples);
1361
- }
1362
- const truncateCode = (code) => {
1363
- if (!maxLines || maxLines <= 0) {
1364
- return { code, truncated: false, remainingLines: 0 };
1365
- }
1366
- const lines = code.split("\n");
1367
- if (lines.length <= maxLines) {
1368
- return { code, truncated: false, remainingLines: 0 };
1369
- }
1370
- return {
1371
- code: lines.slice(0, maxLines).join("\n"),
1372
- truncated: true,
1373
- remainingLines: lines.length - maxLines
1374
- };
1375
- };
1376
- const renderedExamples = examples.map((example) => ({
1377
- ...example.code ? truncateCode(example.code) : { truncated: false, remainingLines: 0 },
1378
- variant: example.name,
1379
- description: example.description,
1380
- code: example.code ? truncateCode(example.code).code : `<${component.name} />`,
1381
- ...example.code ? {} : {
1382
- note: "No code example provided. Refer to props for customization."
1383
- }
1384
- }));
1385
- const propsReference = Object.entries(component.props ?? {}).map(
1386
- ([propName, prop]) => ({
1387
- name: propName,
1388
- type: prop.type,
1389
- required: prop.required,
1390
- default: prop.default,
1391
- description: prop.description,
1392
- values: prop.values
1393
- })
1394
- );
1395
- const propConstraints = Object.entries(component.props ?? {}).filter(
1396
- ([, prop]) => Boolean(prop.constraints && prop.constraints.length > 0)
1397
- ).map(([propName, prop]) => ({
1398
- prop: propName,
1399
- constraints: prop.constraints
1400
- }));
1401
- const fullResult = {
1402
- meta: {
1403
- id: component.id,
1404
- name: component.name,
1405
- description: component.description,
1406
- category: component.category,
1407
- status: component.status,
1408
- publicRef: component.publicRef,
1409
- publicSlug: component.publicSlug,
1410
- isCanonical: component.isCanonical ?? false,
1411
- tier: component.tier
1412
- },
1413
- props: propsReference,
1414
- examples: {
1415
- import: `import { ${component.name} } from '${pkgName}';`,
1416
- code: renderedExamples
1417
- },
1418
- relations: component.relations,
1419
- compoundChildren: component.compoundChildren,
1420
- guidance: {
1421
- when: getGuidanceWhen(component),
1422
- whenNot: getGuidanceWhenNot(component),
1423
- guidelines: component.guidance.guidelines,
1424
- accessibility: component.guidance.accessibility,
1425
- usageGuidance: component.guidance.usageGuidance,
1426
- dos: component.guidance.dos,
1427
- donts: component.guidance.donts,
1428
- patterns: component.guidance.patterns,
1429
- propConstraints,
1430
- alternatives: component.relations?.filter((relation) => relation.relationship === "alternative").map((relation) => ({
1431
- component: relation.componentName,
1432
- note: relation.note
1433
- })) ?? []
1434
- },
1435
- metadata: component.metadata,
1436
- source: await getSourceCode(component, ctx.config.projectRoot)
1437
- };
1438
- const aliasMap = { usage: "guidance" };
1439
- const resolvedFields = fields?.map((field) => {
1440
- const parts = field.split(".");
1441
- if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
1442
- return parts.join(".");
1443
- });
1444
- let result2;
1445
- if (verbosity === "compact" && !resolvedFields?.length) {
1446
- result2 = {
1447
- meta: fullResult.meta,
1448
- propsSummary: component.propsSummary,
1449
- metadata: component.metadata
1450
- };
1451
- } else if (verbosity === "full") {
1452
- result2 = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
1453
- } else if (resolvedFields && resolvedFields.length > 0) {
1454
- result2 = projectFields(fullResult, resolvedFields);
1455
- } else {
1456
- const { source: _source, ...withoutSource } = fullResult;
1457
- result2 = withoutSource;
1458
- }
1459
- return {
1460
- content: [{ type: "text", text: JSON.stringify(result2) }]
1461
- };
1462
- };
1463
-
1464
- // src/tools/blocks.ts
1465
- var blocksHandler = async (args, ctx) => {
1466
- const blockName = args?.name;
1467
- const search2 = args?.search?.toLowerCase() ?? void 0;
1468
- const component = args?.component?.toLowerCase() ?? void 0;
1469
- const category = args?.category?.toLowerCase() ?? void 0;
1470
- const blocksLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 50) : void 0;
1471
- const allBlocks = listBlocks(ctx.data.snapshot);
1472
- if (allBlocks.length === 0) {
1473
- return {
1474
- content: [{
1475
- type: "text",
1476
- text: JSON.stringify({
1477
- total: 0,
1478
- blocks: [],
1479
- 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\`.`
1480
- })
1481
- }]
1482
- };
1483
- }
1484
- let filtered = allBlocks;
1485
- if (blockName) {
1486
- filtered = filtered.filter(
1487
- (b) => b.name.toLowerCase() === blockName.toLowerCase()
1488
- );
1489
- if (filtered.length === 0) {
1490
- const allBlockNames = allBlocks.map((b) => b.name);
1491
- const closest = findClosestMatch(blockName, allBlockNames);
1492
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1493
- throw new Error(`Block "${blockName}" not found.${suggestion} Use ${ctx.toolNames.blocks} to see available blocks.`);
1494
- }
1495
- }
1496
- if (search2) {
1497
- if (ctx.indexes.blockIndex) {
1498
- const ranked = searchBlocks(search2, ctx.indexes.blockIndex, 50);
1499
- const rankedNames = new Set(ranked.map((r) => r.name.toLowerCase()));
1500
- filtered = filtered.filter((b) => rankedNames.has(b.name.toLowerCase()));
1501
- filtered.sort((a, b) => {
1502
- const aIdx = ranked.findIndex((r) => r.name.toLowerCase() === a.name.toLowerCase());
1503
- const bIdx = ranked.findIndex((r) => r.name.toLowerCase() === b.name.toLowerCase());
1504
- return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
1505
- });
1506
- } else {
1507
- filtered = filtered.filter((b) => {
1508
- const haystack = [
1509
- b.name,
1510
- b.description,
1511
- ...b.tags ?? [],
1512
- ...b.components,
1513
- b.category
1514
- ].join(" ").toLowerCase();
1515
- return haystack.includes(search2);
1516
- });
1517
- }
1518
- }
1519
- if (component) {
1520
- filtered = filtered.filter(
1521
- (b) => b.components.some((c) => c.toLowerCase() === component)
1522
- );
1523
- }
1524
- if (category) {
1525
- filtered = filtered.filter(
1526
- (b) => b.category.toLowerCase() === category
1527
- );
1528
- }
1529
- const blocksUseIcons = filtered.some(
1530
- (b) => b.components.some((c) => c === "Icon") || b.code && /\bIcon\b/.test(b.code)
1531
- );
1532
- const iconHint = blocksUseIcons ? "Icon components in block code are from @phosphor-icons/react. Import them as: import { IconName } from '@phosphor-icons/react';" : void 0;
1533
- if (blocksLimit !== void 0) {
1534
- filtered = filtered.slice(0, blocksLimit);
1535
- }
1536
- const verbosity = args?.verbosity ?? "standard";
1537
- const blocksWithImports = await Promise.all(filtered.map(async (b) => {
1538
- const imports = await buildImportStatements(
1539
- b.components,
1540
- async (componentName) => ctx.resolvePackageName(componentName)
1541
- );
1542
- const base = {
1543
- name: b.name,
1544
- description: b.description,
1545
- category: b.category,
1546
- components: b.components,
1547
- tags: b.tags,
1548
- import: imports.join("\n"),
1549
- imports
1550
- };
1551
- if (verbosity === "compact") return base;
1552
- if (verbosity === "full") return { ...base, code: b.code };
1553
- const codeLines = b.code.split("\n");
1554
- const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : b.code;
1555
- return { ...base, code };
1556
- }));
1557
- return {
1558
- content: [{
1559
- type: "text",
1560
- text: JSON.stringify({
1561
- total: blocksWithImports.length,
1562
- blocks: blocksWithImports,
1563
- ...iconHint && { iconHint },
1564
- ...blocksWithImports.length === 0 && allBlocks.length > 0 && {
1565
- hint: "No blocks matching your query. Try broader search terms."
1566
- }
1567
- })
1568
- }]
1569
- };
1570
- };
1571
-
1572
- // src/tools/tokens.ts
1573
- var TOKEN_CATEGORY_ALIASES = {
1574
- colors: ["color", "colors", "accent", "background", "foreground", "theme"],
1575
- spacing: ["spacing", "space", "spaces", "padding", "margin", "gap", "inset"],
1576
- typography: ["typography", "type", "font", "fonts", "letter", "line-height"],
1577
- surfaces: ["surface", "surfaces", "canvas"],
1578
- shadows: ["shadow", "shadows", "elevation"],
1579
- radius: ["radius", "radii", "corner", "corners", "rounded", "rounding"],
1580
- borders: ["border", "borders", "stroke", "outline"],
1581
- text: ["text", "copy", "content"],
1582
- focus: ["focus", "ring", "focus-ring"],
1583
- layout: ["layout", "container", "grid", "breakpoint"],
1584
- code: ["code"],
1585
- "component-sizing": ["component-sizing", "sizing", "size", "sizes"]
1586
- };
1587
- function normalizeCategoryValue(value) {
1588
- const normalized = value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1589
- return normalized && normalized.length > 0 ? normalized : void 0;
1590
- }
1591
- function resolveCategoryKeys(categories, requestedCategory) {
1592
- const normalized = normalizeCategoryValue(requestedCategory);
1593
- if (!normalized) {
1594
- return Object.keys(categories);
1595
- }
1596
- const keys = Object.keys(categories);
1597
- const exactMatches = keys.filter((key) => normalizeCategoryValue(key) === normalized);
1598
- if (exactMatches.length > 0) {
1599
- return exactMatches;
1600
- }
1601
- const canonical = Object.entries(TOKEN_CATEGORY_ALIASES).find(
1602
- ([categoryName, aliases]) => categoryName === normalized || aliases.includes(normalized)
1603
- );
1604
- if (canonical) {
1605
- const aliases = [canonical[0], ...canonical[1]];
1606
- const aliasMatches = keys.filter((key) => {
1607
- const normalizedKey = normalizeCategoryValue(key) ?? "";
1608
- return aliases.some((alias) => normalizedKey.includes(alias));
1609
- });
1610
- if (aliasMatches.length > 0) {
1611
- return aliasMatches;
1612
- }
1613
- }
1614
- return keys.filter((key) => (normalizeCategoryValue(key) ?? "").includes(normalized));
1615
- }
1616
- function canonicalizeCategory(category, tokens) {
1617
- const normalizedCategory = normalizeCategoryValue(category);
1618
- const candidates = [
1619
- normalizedCategory,
1620
- ...tokens.flatMap((token) => [
1621
- normalizeCategoryValue(token.category),
1622
- normalizeCategoryValue(token.path?.[0]),
1623
- normalizeCategoryValue(token.name.split(/[.:/-]/)[0])
1624
- ])
1625
- ].filter(Boolean);
1626
- for (const candidate of candidates) {
1627
- for (const [canonical, aliases] of Object.entries(TOKEN_CATEGORY_ALIASES)) {
1628
- if (candidate === canonical || aliases.some(
1629
- (alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
1630
- )) {
1631
- return canonical;
1632
- }
1633
- }
1634
- }
1635
- return normalizedCategory || "other";
1636
- }
1637
- function normalizeTokenCategories(categories) {
1638
- const normalized = {};
1639
- for (const [category, tokens] of Object.entries(categories)) {
1640
- const canonical = canonicalizeCategory(category, tokens);
1641
- if (!normalized[canonical]) normalized[canonical] = [];
1642
- normalized[canonical].push(
1643
- ...tokens.map((token) => ({
1644
- ...token,
1645
- category: canonical
1646
- }))
1647
- );
1648
- }
1649
- for (const tokens of Object.values(normalized)) {
1650
- tokens.sort((a, b) => a.name.localeCompare(b.name));
1651
- }
1652
- return normalized;
1653
- }
1654
- var tokensHandler = async (args, ctx) => {
1655
- const category = args?.category;
1656
- const search2 = args?.search?.toLowerCase() ?? void 0;
1657
- const tokensLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 100) : search2 ? 25 : void 0;
1658
- const tokenData = ctx.data.tokens;
1659
- if (!tokenData || tokenData.total === 0) {
1660
- return {
1661
- content: [{
1662
- type: "text",
1663
- text: JSON.stringify({
1664
- total: 0,
1665
- categories: {},
1666
- hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
1667
- })
1668
- }]
1669
- };
1670
- }
1671
- const normalizedCategories = normalizeTokenCategories(tokenData.categories);
1672
- let filteredCategories = {};
1673
- let filteredTotal = 0;
1674
- const resolvedCategoryKeys = resolveCategoryKeys(normalizedCategories, category);
1675
- const friendlyCategories = Object.keys(TOKEN_CATEGORY_ALIASES);
1676
- const searchMatchesCategory = search2 ? resolveCategoryKeys(normalizedCategories, search2) : [];
1677
- for (const [cat, tokens] of Object.entries(normalizedCategories)) {
1678
- if (category && !resolvedCategoryKeys.includes(cat)) continue;
1679
- let filtered = tokens;
1680
- if (search2) {
1681
- if (searchMatchesCategory.includes(cat)) {
1682
- filtered = tokens;
1683
- } else {
1684
- filtered = tokens.filter(
1685
- (t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || (normalizeCategoryValue(cat) ?? "").includes(search2) || t.value && t.value.toLowerCase().includes(search2) || t.path && t.path.join(" ").toLowerCase().includes(search2)
1686
- );
1687
- }
1688
- }
1689
- if (filtered.length > 0) {
1690
- filteredCategories[cat] = filtered;
1691
- filteredTotal += filtered.length;
1692
- }
1693
- }
1694
- if (tokensLimit !== void 0) {
1695
- const limited = limitTokensPerCategory(filteredCategories, tokensLimit);
1696
- filteredCategories = limited.categories;
1697
- filteredTotal = limited.total;
1698
- }
1699
- let hint;
1700
- if (filteredTotal === 0) {
1701
- if (category && search2) {
1702
- const categoryTotal = resolvedCategoryKeys.reduce(
1703
- (sum, key) => sum + (normalizedCategories[key]?.length ?? 0),
1704
- 0
1705
- );
1706
- 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. Try categories like: ${friendlyCategories.join(", ")}.`;
1707
- } else if (search2) {
1708
- hint = `No tokens matching "${search2}". Try categories like: ${friendlyCategories.join(", ")}.`;
1709
- } else if (category) {
1710
- hint = `Category "${category}" not found. Try categories like: ${friendlyCategories.join(", ")}.`;
1711
- }
1712
- } else if (!category && !search2) {
1713
- hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
1714
- }
1715
- return {
1716
- content: [{
1717
- type: "text",
1718
- text: JSON.stringify({
1719
- prefix: tokenData.prefix,
1720
- total: filteredTotal,
1721
- totalAvailable: tokenData.total,
1722
- categories: filteredCategories,
1723
- ...hint && { hint },
1724
- ...!category && !search2 && {
1725
- availableCategories: Object.entries(normalizedCategories).map(([cat, tokens]) => ({
1726
- category: cat,
1727
- count: tokens.length
1728
- }))
1729
- }
1730
- })
1731
- }]
1732
- };
1733
- };
1734
-
1735
- // src/service.ts
1736
- var DEFAULT_ENDPOINTS = {
1737
- render: "/fragments/render",
1738
- compare: "/fragments/compare",
1739
- fix: "/fragments/fix",
1740
- a11y: "/fragments/a11y"
1741
- };
1742
- async function renderComponent(viewerUrl, request, endpoints) {
1743
- const renderUrl = `${viewerUrl}${endpoints?.render ?? DEFAULT_ENDPOINTS.render}`;
1744
- const response = await fetch(renderUrl, {
1745
- method: "POST",
1746
- headers: { "Content-Type": "application/json" },
1747
- body: JSON.stringify({
1748
- component: request.component,
1749
- props: request.props ?? {},
1750
- variant: request.variant,
1751
- viewport: request.viewport ?? { width: 800, height: 600 }
1752
- })
1753
- });
1754
- return await response.json();
1755
- }
1756
- async function compareComponent(viewerUrl, request, endpoints) {
1757
- const compareUrl = `${viewerUrl}${endpoints?.compare ?? DEFAULT_ENDPOINTS.compare}`;
1758
- const response = await fetch(compareUrl, {
1759
- method: "POST",
1760
- headers: { "Content-Type": "application/json" },
1761
- body: JSON.stringify(request)
1762
- });
1763
- return await response.json();
1764
- }
1765
- async function fixComponent(viewerUrl, request, endpoints) {
1766
- const fixUrl = `${viewerUrl}${endpoints?.fix ?? DEFAULT_ENDPOINTS.fix}`;
1767
- const response = await fetch(fixUrl, {
1768
- method: "POST",
1769
- headers: { "Content-Type": "application/json" },
1770
- body: JSON.stringify(request)
1771
- });
1772
- return await response.json();
1773
- }
1774
- async function auditComponent(viewerUrl, request, endpoints) {
1775
- const a11yUrl = `${viewerUrl}${endpoints?.a11y ?? DEFAULT_ENDPOINTS.a11y}`;
1776
- const response = await fetch(a11yUrl, {
1777
- method: "POST",
1778
- headers: { "Content-Type": "application/json" },
1779
- body: JSON.stringify({
1780
- component: request.component,
1781
- variant: request.variant,
1782
- standard: request.standard,
1783
- includeFixPatches: request.includeFixPatches
1784
- })
1785
- });
1786
- const raw = await response.json();
1787
- if (raw.error) {
1788
- return {
1789
- component: request.component,
1790
- results: [],
1791
- score: 0,
1792
- aaPercent: 0,
1793
- aaaPercent: 0,
1794
- passed: false,
1795
- standard: request.standard ?? "AA",
1796
- error: raw.error
1797
- };
1798
- }
1799
- const results = raw.results ?? [];
1800
- const standard = request.standard ?? "AA";
1801
- let totalCritical = 0;
1802
- let totalSerious = 0;
1803
- let totalModerate = 0;
1804
- let totalMinor = 0;
1805
- for (const r of results) {
1806
- totalCritical += r.summary.critical;
1807
- totalSerious += r.summary.serious;
1808
- totalModerate += r.summary.moderate;
1809
- totalMinor += r.summary.minor;
1810
- }
1811
- const deductions = totalCritical * 10 + totalSerious * 5 + totalModerate * 2 + totalMinor * 1;
1812
- const score = Math.max(0, 100 - deductions);
1813
- const variantCount = results.length;
1814
- const aaPassCount = results.filter((r) => {
1815
- const critical = r.summary.critical;
1816
- const serious = r.summary.serious;
1817
- return critical === 0 && serious === 0;
1818
- }).length;
1819
- const aaaPassCount = results.filter((r) => {
1820
- const total = r.summary.critical + r.summary.serious + r.summary.moderate + r.summary.minor;
1821
- return total === 0;
1822
- }).length;
1823
- const totalPasses = results.reduce((sum, r) => sum + r.passes, 0);
1824
- const totalViolations = totalCritical + totalSerious + totalModerate + totalMinor;
1825
- const emptyAudit = results.length > 0 && totalPasses === 0 && totalViolations === 0;
1826
- const aaPercent = variantCount > 0 ? Math.round(aaPassCount / variantCount * 100) : 100;
1827
- const aaaPercent = variantCount > 0 ? Math.round(aaaPassCount / variantCount * 100) : 100;
1828
- const aaPass = !emptyAudit && totalCritical === 0 && totalSerious === 0;
1829
- const aaaPass = !emptyAudit && totalViolations === 0;
1830
- const passed = standard === "AAA" ? aaaPass : aaPass;
1831
- return {
1832
- component: request.component,
1833
- results,
1834
- score: emptyAudit ? 0 : score,
1835
- aaPercent: emptyAudit ? 0 : aaPercent,
1836
- aaaPercent: emptyAudit ? 0 : aaaPercent,
1837
- ...emptyAudit && { emptyAudit },
1838
- passed,
1839
- standard
1840
- };
1841
- }
1842
-
1843
- // src/tools/render.ts
1844
- 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.";
1845
- var renderHandler = async (args, ctx) => {
1846
- const componentName = args?.component;
1847
- const variantName = args?.variant;
1848
- const props = args?.props ?? {};
1849
- const viewport = args?.viewport;
1850
- const figmaUrl = args?.figmaUrl;
1851
- const threshold = args?.threshold ?? (figmaUrl ? 1 : ctx.config.threshold ?? DEFAULTS.diffThreshold);
1852
- if (!componentName) {
1853
- return {
1854
- content: [{ type: "text", text: "Error: component name is required" }],
1855
- isError: true
1856
- };
1857
- }
1858
- {
1859
- const component = findComponentByName(ctx.data.snapshot, componentName);
1860
- if (!component) {
1861
- const allNames = componentNames(ctx.data.snapshot);
1862
- const closest = findClosestMatch(componentName, allNames);
1863
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1864
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
24
+ try {
25
+ const content = readFileSync(configPath, "utf-8");
26
+ return JSON.parse(content);
27
+ } catch (e) {
28
+ throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
1865
29
  }
1866
30
  }
1867
- const viewerUrl = ctx.config.viewerUrl;
1868
- if (!viewerUrl) {
1869
- return {
1870
- content: [{
1871
- type: "text",
1872
- text: NO_VIEWER_MSG
1873
- }],
1874
- isError: true
1875
- };
1876
- }
1877
- if (figmaUrl) {
31
+ const pkgPath = join(projectRoot, "package.json");
32
+ if (existsSync(pkgPath)) {
1878
33
  try {
1879
- const result2 = await compareComponent(viewerUrl, {
1880
- component: componentName,
1881
- variant: variantName,
1882
- props,
1883
- figmaUrl,
1884
- threshold
1885
- }, ctx.config.fileConfig?.endpoints);
1886
- if (result2.error) {
1887
- return {
1888
- content: [{
1889
- type: "text",
1890
- text: `Compare error: ${result2.error}${result2.suggestion ? `
1891
- Suggestion: ${result2.suggestion}` : ""}`
1892
- }],
1893
- isError: true
1894
- };
1895
- }
1896
- const content = [];
1897
- const summaryText = result2.match ? `MATCH: ${componentName} matches Figma design (${result2.diffPercentage}% diff, threshold: ${result2.threshold}%)` : `MISMATCH: ${componentName} differs from Figma design by ${result2.diffPercentage}% (threshold: ${result2.threshold}%)`;
1898
- content.push({ type: "text", text: summaryText });
1899
- if (result2.diff && !result2.match) {
1900
- content.push({
1901
- type: "image",
1902
- data: result2.diff.replace("data:image/png;base64,", ""),
1903
- mimeType: "image/png"
1904
- });
1905
- content.push({
1906
- type: "text",
1907
- text: `Diff image above shows visual differences (red highlights). Changed regions: ${result2.changedRegions?.length ?? 0}`
1908
- });
1909
- }
1910
- content.push({
1911
- type: "text",
1912
- text: JSON.stringify({
1913
- match: result2.match,
1914
- diffPercentage: result2.diffPercentage,
1915
- threshold: result2.threshold,
1916
- figmaUrl: result2.figmaUrl,
1917
- changedRegions: result2.changedRegions
1918
- })
1919
- });
1920
- return { content };
1921
- } catch (error) {
1922
- return {
1923
- content: [{
1924
- type: "text",
1925
- 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.`
1926
- }],
1927
- isError: true
1928
- };
34
+ const content = readFileSync(pkgPath, "utf-8");
35
+ const pkg = JSON.parse(content);
36
+ if (pkg.dsMcp) return pkg.dsMcp;
37
+ } catch {
1929
38
  }
1930
39
  }
40
+ return null;
41
+ }
42
+
43
+ // src/server.ts
44
+ import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
45
+
46
+ // src/version.ts
47
+ import { readFileSync as readFileSync2 } from "fs";
48
+ function readPackageVersion() {
1931
49
  try {
1932
- const result2 = await renderComponent(viewerUrl, {
1933
- component: componentName,
1934
- props,
1935
- variant: variantName,
1936
- viewport: viewport ?? { width: 800, height: 600 }
1937
- }, ctx.config.fileConfig?.endpoints);
1938
- if (result2.error) {
1939
- return {
1940
- content: [{ type: "text", text: `Render error: ${result2.error}` }],
1941
- isError: true
1942
- };
1943
- }
1944
- return {
1945
- content: [
1946
- {
1947
- type: "image",
1948
- data: result2.screenshot.replace("data:image/png;base64,", ""),
1949
- mimeType: "image/png"
1950
- },
1951
- {
1952
- type: "text",
1953
- text: `Successfully rendered ${componentName}${variantName ? ` (variant: "${variantName}")` : ""}${Object.keys(props).length > 0 ? ` with props: ${JSON.stringify(props)}` : ""}`
1954
- }
1955
- ]
1956
- };
1957
- } catch (error) {
1958
- return {
1959
- content: [{
1960
- type: "text",
1961
- text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
1962
- }],
1963
- isError: true
1964
- };
50
+ const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
51
+ const pkg = JSON.parse(raw);
52
+ return pkg.version ?? "0.0.0";
53
+ } catch {
54
+ return "0.0.0";
1965
55
  }
1966
- };
56
+ }
57
+ var MCP_SERVER_VERSION = readPackageVersion();
1967
58
 
1968
- // src/tools/fix.ts
1969
- 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.";
1970
- var fixHandler = async (args, ctx) => {
1971
- const componentName = args?.component;
1972
- const variantName = args?.variant ?? void 0;
1973
- const fixType = args?.fixType ?? "all";
1974
- if (!componentName) {
1975
- throw new Error("component is required");
1976
- }
1977
- const fragment = findComponentByName(ctx.data.snapshot, componentName);
1978
- if (!fragment) {
1979
- const allNames = componentNames(ctx.data.snapshot);
1980
- const closest = findClosestMatch(componentName, allNames);
1981
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1982
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
1983
- }
1984
- const viewerUrl = ctx.config.viewerUrl;
1985
- if (!viewerUrl) {
1986
- return {
1987
- content: [{
1988
- type: "text",
1989
- text: NO_VIEWER_MSG2
1990
- }],
1991
- isError: true
1992
- };
59
+ // src/catalog-meta.ts
60
+ function getCatalogMeta(data) {
61
+ const rawUpdatedAt = data.validateFixContext?.updatedAt ?? data.snapshot.metadata.updatedAt;
62
+ const updatedAt = typeof rawUpdatedAt === "number" ? new Date(rawUpdatedAt).toISOString() : rawUpdatedAt;
63
+ return {
64
+ catalogRevision: data.validateFixContext?.catalogRevision ?? data.snapshot.metadata.revision,
65
+ updatedAt
66
+ };
67
+ }
68
+
69
+ // src/token-suggestions.ts
70
+ function propertyFamilyFor(property, value) {
71
+ const prop = property.toLowerCase().trim();
72
+ const normalizedValue = value?.toLowerCase().trim();
73
+ if (prop.includes("shadow")) return "shadow";
74
+ if (prop.includes("z-index")) return "z-index";
75
+ if (prop.includes("transition") || prop.includes("duration") || prop.includes("animation")) {
76
+ return "duration";
77
+ }
78
+ if (prop.includes("font") || prop.includes("line-height") || prop === "letter-spacing") {
79
+ return "typography";
1993
80
  }
1994
- try {
1995
- const result2 = await fixComponent(viewerUrl, {
1996
- component: componentName,
1997
- variant: variantName,
1998
- fixType
1999
- }, ctx.config.fileConfig?.endpoints);
2000
- if (result2.error) {
2001
- return {
2002
- content: [{
2003
- type: "text",
2004
- text: `Fix generation error: ${result2.error}`
2005
- }],
2006
- isError: true
2007
- };
2008
- }
2009
- return {
2010
- content: [{
2011
- type: "text",
2012
- text: JSON.stringify({
2013
- component: componentName,
2014
- variant: variantName ?? "all",
2015
- fixType,
2016
- patches: result2.patches,
2017
- summary: result2.summary,
2018
- patchCount: result2.patches.length,
2019
- nextStep: result2.patches.length > 0 ? `Apply patches using your editor or \`patch\` command, then run ${ctx.toolNames.render} to confirm fixes.` : void 0
2020
- })
2021
- }]
2022
- };
2023
- } catch (error) {
2024
- return {
2025
- content: [{
2026
- type: "text",
2027
- text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
2028
- }],
2029
- isError: true
2030
- };
81
+ if (prop.includes("radius")) return "radius";
82
+ if (prop.endsWith("border-width") || prop === "border-width" || prop === "outline-width" || prop === "stroke-width") {
83
+ return "border-width";
2031
84
  }
2032
- };
2033
-
2034
- // src/tools/a11y.ts
2035
- 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.";
2036
- var a11yHandler = async (args, ctx) => {
2037
- const componentName = args?.component;
2038
- const variantName = args?.variant ?? void 0;
2039
- const standard = args?.standard ?? "AA";
2040
- const includeFixPatches = args?.includeFixPatches ?? false;
2041
- if (!componentName) {
2042
- throw new Error("component is required");
2043
- }
2044
- {
2045
- const fragment = findComponentByName(ctx.data.snapshot, componentName);
2046
- if (!fragment) {
2047
- const allNames = componentNames(ctx.data.snapshot);
2048
- const closest = findClosestMatch(componentName, allNames);
2049
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2050
- throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
2051
- }
85
+ if (prop.includes("color") || prop === "background" || prop.startsWith("background-") || prop === "fill" || prop === "stroke" || prop === "caret-color" || prop === "accent-color" || (prop === "border" || prop === "outline") && normalizedValue && looksLikeColor(normalizedValue)) {
86
+ return "color";
2052
87
  }
2053
- const viewerUrl = ctx.config.viewerUrl;
2054
- if (!viewerUrl) {
2055
- return {
2056
- content: [{
2057
- type: "text",
2058
- text: NO_VIEWER_MSG3
2059
- }],
2060
- isError: true
2061
- };
88
+ if (prop === "margin" || prop.startsWith("margin-") || prop === "padding" || prop.startsWith("padding-") || prop === "gap" || prop === "row-gap" || prop === "column-gap" || prop === "inset" || prop === "top" || prop === "right" || prop === "bottom" || prop === "left" || prop.endsWith("width") || prop.endsWith("height")) {
89
+ return "spacing";
2062
90
  }
2063
- try {
2064
- const result2 = await auditComponent(viewerUrl, {
2065
- component: componentName,
2066
- variant: variantName,
2067
- standard,
2068
- includeFixPatches
2069
- }, ctx.config.fileConfig?.endpoints);
2070
- if (result2.error) {
2071
- return {
2072
- content: [{
2073
- type: "text",
2074
- text: `A11y audit error: ${result2.error}`
2075
- }],
2076
- isError: true
2077
- };
2078
- }
2079
- let nextStep;
2080
- if (result2.emptyAudit) {
2081
- 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}").`;
2082
- } else if (result2.passed) {
2083
- nextStep = 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.';
2084
- } else {
2085
- nextStep = `Fix the violations above, then re-run ${ctx.toolNames.a11y} to verify. Use ${ctx.toolNames.fix} for automated fixes.`;
2086
- }
91
+ return "other";
92
+ }
93
+ function suggestToken(input) {
94
+ const family = propertyFamilyFor(input.property, input.value);
95
+ const limit = Math.min(Math.max(input.limit ?? 5, 1), 10);
96
+ const candidates = input.tokens ? scoreCandidates(input.tokens, family, input) : [];
97
+ const top = candidates.slice(0, limit).map(
98
+ (candidate) => presentCandidate(candidate, input.tokens)
99
+ );
100
+ const meta = {
101
+ propertyFamily: family,
102
+ catalogRevision: input.catalogRevision,
103
+ updatedAt: input.updatedAt,
104
+ candidateCount: candidates.length
105
+ };
106
+ if (family === "other") {
2087
107
  return {
2088
- content: [{
2089
- type: "text",
2090
- text: JSON.stringify({
2091
- component: componentName,
2092
- variant: variantName ?? "all",
2093
- standard,
2094
- totalViolations: result2.results.reduce((sum, r) => sum + r.summary.total, 0),
2095
- variantsPassingAA: `${result2.aaPercent}%`,
2096
- variantsPassingAAA: `${result2.aaaPercent}%`,
2097
- passed: result2.passed,
2098
- ...result2.emptyAudit && { emptyAudit: true },
2099
- results: result2.results,
2100
- nextStep
2101
- })
2102
- }]
108
+ alternatives: [],
109
+ noSuggestion: true,
110
+ noSuggestionReason: `No token family is known for CSS property "${input.property}".`,
111
+ _meta: meta
2103
112
  };
2104
- } catch (error) {
113
+ }
114
+ if (!input.tokens || input.tokens.total === 0) {
2105
115
  return {
2106
- content: [{
2107
- type: "text",
2108
- text: `Failed to audit component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
2109
- }],
2110
- isError: true
116
+ alternatives: [],
117
+ noSuggestion: true,
118
+ noSuggestionReason: "No design tokens are available in the active catalog.",
119
+ _meta: meta
2111
120
  };
2112
121
  }
2113
- };
2114
-
2115
- // src/graph-handler.ts
2116
- import {
2117
- ComponentGraphEngine,
2118
- deserializeGraph
2119
- } from "@fragments-sdk/context/graph";
2120
- function handleGraphTool(args, serializedGraph, blocks, componentNames2) {
2121
- if (!serializedGraph) {
122
+ if (top.length === 0) {
2122
123
  return {
2123
- text: JSON.stringify({
2124
- error: "No graph data available. Run `fragments build` to generate the component graph.",
2125
- hint: "The graph is built automatically during `fragments build` and embedded in fragments.json."
2126
- }),
2127
- isError: true
124
+ alternatives: [],
125
+ noSuggestion: true,
126
+ noSuggestionReason: `No ${family} tokens are available for CSS property "${input.property}".`,
127
+ _meta: meta
2128
128
  };
2129
129
  }
2130
- const graph = deserializeGraph(serializedGraph);
2131
- const blockData = blocks ? Object.fromEntries(
2132
- Object.entries(blocks).map(([k, v]) => [k, { components: v.components }])
2133
- ) : void 0;
2134
- const engine = new ComponentGraphEngine(graph, blockData);
2135
- const edgeTypes = args.edgeTypes;
2136
- switch (args.mode) {
2137
- case "health": {
2138
- const health = engine.getHealth();
2139
- const blockCount = blocks ? Object.keys(blocks).length : 0;
2140
- return {
2141
- text: JSON.stringify({
2142
- mode: "health",
2143
- ...health,
2144
- ...health.compositionCoverage === 0 && blockCount === 0 && {
2145
- compositionNote: "No composition blocks defined yet \u2014 compositionCoverage will increase as blocks are added"
2146
- },
2147
- summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
2148
- })
2149
- };
2150
- }
2151
- case "dependencies": {
2152
- if (!args.component) {
2153
- return { text: JSON.stringify({ error: "component is required for dependencies mode" }), isError: true };
2154
- }
2155
- if (!engine.hasNode(args.component)) {
2156
- const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
2157
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2158
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
2159
- }
2160
- const deps = engine.dependencies(args.component, edgeTypes);
2161
- return {
2162
- text: JSON.stringify({
2163
- mode: "dependencies",
2164
- component: args.component,
2165
- count: deps.length,
2166
- dependencies: deps.map((e) => ({
2167
- component: e.target,
2168
- type: e.type,
2169
- weight: e.weight,
2170
- note: e.note,
2171
- provenance: e.provenance
2172
- }))
2173
- })
2174
- };
2175
- }
2176
- case "dependents": {
2177
- if (!args.component) {
2178
- return { text: JSON.stringify({ error: "component is required for dependents mode" }), isError: true };
2179
- }
2180
- if (!engine.hasNode(args.component)) {
2181
- const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
2182
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2183
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
2184
- }
2185
- const deps = engine.dependents(args.component, edgeTypes);
2186
- return {
2187
- text: JSON.stringify({
2188
- mode: "dependents",
2189
- component: args.component,
2190
- count: deps.length,
2191
- dependents: deps.map((e) => ({
2192
- component: e.source,
2193
- type: e.type,
2194
- weight: e.weight,
2195
- note: e.note,
2196
- provenance: e.provenance
2197
- }))
2198
- })
2199
- };
2200
- }
2201
- case "impact": {
2202
- if (!args.component) {
2203
- return { text: JSON.stringify({ error: "component is required for impact mode" }), isError: true };
2204
- }
2205
- if (!engine.hasNode(args.component)) {
2206
- const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
2207
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2208
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
2209
- }
2210
- const result2 = engine.impact(args.component, args.maxDepth ?? 3);
2211
- return {
2212
- text: JSON.stringify({
2213
- mode: "impact",
2214
- ...result2,
2215
- summary: `Changing ${args.component} affects ${result2.totalAffected} component(s) and ${result2.affectedBlocks.length} block(s)`
2216
- })
2217
- };
2218
- }
2219
- case "path": {
2220
- if (!args.component || !args.target) {
2221
- return { text: JSON.stringify({ error: "component and target are required for path mode" }), isError: true };
2222
- }
2223
- const result2 = engine.path(args.component, args.target);
2224
- return {
2225
- text: JSON.stringify({
2226
- mode: "path",
2227
- from: args.component,
2228
- to: args.target,
2229
- ...result2,
2230
- edges: result2.edges.map((e) => ({
2231
- source: e.source,
2232
- target: e.target,
2233
- type: e.type
2234
- }))
2235
- })
2236
- };
2237
- }
2238
- case "composition": {
2239
- if (!args.component) {
2240
- return { text: JSON.stringify({ error: "component is required for composition mode" }), isError: true };
2241
- }
2242
- if (!engine.hasNode(args.component)) {
2243
- const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
2244
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2245
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
2246
- }
2247
- const tree = engine.composition(args.component);
2248
- return {
2249
- text: JSON.stringify({
2250
- mode: "composition",
2251
- ...tree
2252
- })
2253
- };
2254
- }
2255
- case "alternatives": {
2256
- if (!args.component) {
2257
- return { text: JSON.stringify({ error: "component is required for alternatives mode" }), isError: true };
2258
- }
2259
- if (!engine.hasNode(args.component)) {
2260
- const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
2261
- const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2262
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
130
+ const [recommended, ...alternatives] = top;
131
+ return {
132
+ recommended,
133
+ alternatives,
134
+ _meta: meta
135
+ };
136
+ }
137
+ function scoreCandidates(tokenData, family, input) {
138
+ if (family === "other") return [];
139
+ const value = normalizeComparableValue(input.value);
140
+ const flatTokens = tokenData.flat.length > 0 ? tokenData.flat : Object.values(tokenData.categories).flat();
141
+ const familyTokens = flatTokens.filter((token) => !isGarbageToken(token)).map((token) => ({ token, family: tokenFamily(token) })).filter((entry) => entry.family === family);
142
+ const scored = familyTokens.map(({ token }) => {
143
+ const tokenValue = normalizeComparableValue(token.value);
144
+ let score = 50;
145
+ let reason = "family-match";
146
+ if (value && tokenValue && value === tokenValue) {
147
+ score += 60;
148
+ reason = "exact-value-match";
149
+ } else if (value && tokenValue && family !== "color") {
150
+ const distanceScore = lengthDistanceScore(value, tokenValue);
151
+ if (distanceScore > 0) {
152
+ score += distanceScore;
153
+ reason = "nearest-neighbor";
2263
154
  }
2264
- const alts = engine.alternatives(args.component);
2265
- return {
2266
- text: JSON.stringify({
2267
- mode: "alternatives",
2268
- component: args.component,
2269
- count: alts.length,
2270
- alternatives: alts
2271
- })
2272
- };
2273
- }
2274
- case "islands": {
2275
- const islands = engine.islands();
2276
- return {
2277
- text: JSON.stringify({
2278
- mode: "islands",
2279
- count: islands.length,
2280
- islands: islands.map((island, i) => ({
2281
- id: i + 1,
2282
- size: island.length,
2283
- components: island
2284
- }))
2285
- })
2286
- };
2287
155
  }
2288
- default:
2289
- return {
2290
- text: JSON.stringify({
2291
- error: `Unknown mode: "${args.mode}". Valid modes: dependencies, dependents, impact, path, composition, alternatives, islands, health`
2292
- }),
2293
- isError: true
2294
- };
2295
- }
156
+ score += nameRelevanceScore(token, input.property, input.context);
157
+ return { token, family, score, reason };
158
+ });
159
+ return scored.filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.token.name.localeCompare(b.token.name));
2296
160
  }
2297
-
2298
- // src/tools/graph.ts
2299
- var graphHandler = async (args, ctx) => {
2300
- const graphArgs = {
2301
- mode: args?.mode ?? "health",
2302
- component: args?.component,
2303
- target: args?.target,
2304
- edgeTypes: args?.edgeTypes,
2305
- maxDepth: args?.maxDepth
2306
- };
2307
- const data = ctx.data;
2308
- const allNames = componentNames(data.snapshot);
2309
- const result2 = handleGraphTool(
2310
- graphArgs,
2311
- data.graph,
2312
- data.blocks,
2313
- allNames
2314
- );
2315
- if (result2.isError) {
2316
- return {
2317
- content: [{ type: "text", text: result2.text }],
2318
- isError: true
2319
- };
2320
- }
161
+ function presentCandidate(candidate, tokenData) {
162
+ const cssVar = cssVarForToken(candidate.token);
163
+ const resolvedValue = resolvedValueForToken(candidate.token);
164
+ const confidence = candidate.score >= 105 ? "high" : candidate.score >= 65 ? "medium" : "low";
2321
165
  return {
2322
- content: [{ type: "text", text: result2.text }]
166
+ name: dottedNameForToken(candidate.token, tokenData),
167
+ ...cssVar && { cssVar },
168
+ ...cssVar && {
169
+ cssValue: resolvedValue ? `var(${cssVar}, ${resolvedValue})` : `var(${cssVar})`
170
+ },
171
+ ...resolvedValue && { resolvedValue },
172
+ confidence,
173
+ reason: candidate.reason
2323
174
  };
2324
- };
175
+ }
176
+ function isGarbageToken(token) {
177
+ const value = token.value?.trim();
178
+ if (!value) return false;
179
+ if (value.includes("#{") || value.includes("$")) return true;
180
+ if (/^\$[\w-]+/.test(token.name)) return true;
181
+ return false;
182
+ }
183
+ function tokenFamily(token) {
184
+ const haystack = [
185
+ token.type,
186
+ token.category,
187
+ ...token.path ?? [],
188
+ token.name
189
+ ].filter(Boolean).join(" ").toLowerCase();
190
+ if (/\b(color|colour|background|foreground|surface|palette)\b/.test(haystack)) {
191
+ return "color";
192
+ }
193
+ if (/\b(radius|radii|rounded|corner)\b/.test(haystack)) return "radius";
194
+ if (/\b(border-width|border width|stroke-width|stroke width)\b/.test(haystack)) {
195
+ return "border-width";
196
+ }
197
+ if (/\b(shadow|elevation)\b/.test(haystack)) return "shadow";
198
+ if (/\b(font|type|typography|line-height|letter-spacing)\b/.test(haystack)) {
199
+ return "typography";
200
+ }
201
+ if (/\b(duration|transition|animation)\b/.test(haystack)) return "duration";
202
+ if (/\b(z-index|zindex)\b/.test(haystack)) return "z-index";
203
+ if (/\b(space|spacing|size|sizing|width|height|gap|padding|margin|inset)\b/.test(haystack)) {
204
+ return "spacing";
205
+ }
206
+ if (/\bborder\b/.test(haystack)) return "border-width";
207
+ return "other";
208
+ }
209
+ function cssVarForToken(token) {
210
+ if (token.name.startsWith("--")) return token.name;
211
+ const match = token.value?.match(/var\((--[\w-]+)/);
212
+ return match?.[1];
213
+ }
214
+ function dottedNameForToken(token, tokenData) {
215
+ if (!token.name.startsWith("--")) return token.name;
216
+ let name = token.name.slice(2);
217
+ const prefix = tokenData?.prefix?.replace(/^--/, "").replace(/-$/, "");
218
+ if (prefix && name.startsWith(`${prefix}-`)) {
219
+ name = name.slice(prefix.length + 1);
220
+ }
221
+ return name.replace(/-/g, ".");
222
+ }
223
+ function resolvedValueForToken(token) {
224
+ const value = token.value?.trim();
225
+ if (!value) return void 0;
226
+ const fallback = value.match(/var\(--[\w-]+,\s*([^)]+)\)/)?.[1]?.trim();
227
+ if (fallback) return fallback;
228
+ if (value.startsWith("var(")) return void 0;
229
+ return value;
230
+ }
231
+ function normalizeComparableValue(value) {
232
+ if (!value) return void 0;
233
+ const trimmed = value.trim().toLowerCase();
234
+ const color = normalizeColor(trimmed);
235
+ if (color) return color;
236
+ const length = parseLength(trimmed);
237
+ if (length) return `${length.value}${length.unit}`;
238
+ return trimmed.replace(/\s+/g, " ");
239
+ }
240
+ function looksLikeColor(value) {
241
+ return /^#([\da-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
242
+ }
243
+ function normalizeColor(value) {
244
+ const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i);
245
+ if (!hex) return void 0;
246
+ const body = hex[1].toLowerCase();
247
+ if (body.length === 3) {
248
+ return `#${body[0]}${body[0]}${body[1]}${body[1]}${body[2]}${body[2]}`;
249
+ }
250
+ return `#${body}`;
251
+ }
252
+ function parseLength(value) {
253
+ const match = value.match(/^(-?\d+(?:\.\d+)?)(px|rem|em|%)$/);
254
+ if (!match) return void 0;
255
+ return { value: Number(match[1]), unit: match[2] };
256
+ }
257
+ function lengthDistanceScore(inputValue, tokenValue) {
258
+ const input = parseLength(inputValue);
259
+ const token = parseLength(tokenValue);
260
+ if (!input || !token || input.unit !== token.unit) return 0;
261
+ const distance = Math.abs(input.value - token.value);
262
+ if (distance === 0) return 60;
263
+ if (distance <= 2) return 35;
264
+ if (distance <= 4) return 20;
265
+ if (distance <= 8) return 10;
266
+ return 0;
267
+ }
268
+ function nameRelevanceScore(token, property, context) {
269
+ const haystack = [token.name, token.category, ...token.path ?? []].join(" ").toLowerCase();
270
+ const prop = property.toLowerCase();
271
+ let score = 0;
272
+ for (const part of prop.split(/[^a-z0-9]+/).filter((part2) => part2.length > 2)) {
273
+ if (haystack.includes(part)) score += 4;
274
+ }
275
+ if (context === "component" && haystack.includes("component")) score += 3;
276
+ if (context === "global" && haystack.includes("global")) score += 3;
277
+ return score;
278
+ }
2325
279
 
2326
- // src/tools/perf.ts
2327
- var perfHandler = async (args, ctx) => {
2328
- const componentName = args?.component ?? void 0;
2329
- const sort = args?.sort ?? "size";
2330
- const filter = args?.filter ?? void 0;
2331
- let entries = Object.values(ctx.data.components).filter((component) => component.performance).map((component) => ({
2332
- name: component.name,
2333
- ...component.performance
2334
- }));
2335
- if (entries.length === 0) {
280
+ // src/tools/tokens-suggest.ts
281
+ var tokensSuggestHandler = async (args, ctx) => {
282
+ const property = args.property;
283
+ if (!property || typeof property !== "string") {
2336
284
  return {
2337
285
  content: [
2338
286
  {
2339
287
  type: "text",
2340
- text: JSON.stringify({
2341
- total: 0,
2342
- components: [],
2343
- hint: `No performance data found. Run \`${BRAND.cliCommand} perf\` first to measure bundle sizes.`
2344
- })
288
+ text: JSON.stringify({ error: "property is required." })
2345
289
  }
2346
- ]
290
+ ],
291
+ isError: true
2347
292
  };
2348
293
  }
2349
- if (componentName) {
2350
- entries = entries.filter(
2351
- (entry) => entry.name.toLowerCase() === componentName.toLowerCase()
2352
- );
2353
- if (entries.length === 0) {
2354
- throw new Error(
2355
- `No performance data for "${componentName}". Run \`${BRAND.cliCommand} perf --component ${componentName}\` first.`
2356
- );
2357
- }
2358
- }
2359
- if (filter) {
2360
- if (filter === "over-budget") {
2361
- entries = entries.filter((entry) => entry.overBudget);
2362
- } else {
2363
- entries = entries.filter((entry) => entry.complexity === filter);
2364
- }
2365
- }
2366
- switch (sort) {
2367
- case "name":
2368
- entries.sort((a, b) => a.name.localeCompare(b.name));
2369
- break;
2370
- case "budget":
2371
- entries.sort((a, b) => b.budgetPercent - a.budgetPercent);
2372
- break;
2373
- case "size":
2374
- default:
2375
- entries.sort((a, b) => b.bundleSize - a.bundleSize);
2376
- break;
2377
- }
294
+ const context = args.context;
295
+ const catalogMeta = getCatalogMeta(ctx.data);
296
+ const result2 = suggestToken({
297
+ tokens: ctx.data.tokens,
298
+ property,
299
+ value: typeof args.value === "string" ? args.value : void 0,
300
+ context: context === "component" || context === "block" || context === "global" ? context : void 0,
301
+ catalogRevision: catalogMeta.catalogRevision,
302
+ updatedAt: catalogMeta.updatedAt
303
+ });
2378
304
  return {
2379
- content: [
2380
- {
2381
- type: "text",
2382
- text: JSON.stringify({
2383
- total: entries.length,
2384
- summary: ctx.data.performanceSummary ?? void 0,
2385
- components: entries
2386
- })
2387
- }
2388
- ]
305
+ content: [{ type: "text", text: JSON.stringify(result2) }],
306
+ _meta: result2._meta
2389
307
  };
2390
308
  };
2391
309
 
2392
310
  // src/tools/spec-govern.ts
2393
- var SEVERITY_WEIGHTS = {
2394
- critical: 10,
2395
- serious: 5,
2396
- moderate: 2,
2397
- minor: 1
2398
- };
311
+ import { SEVERITY_WEIGHTS, maxSeverity } from "@fragments-sdk/core/severity";
2399
312
  function isRuleEnabled(rule, defaultEnabled = true) {
2400
313
  if (rule === void 0) return defaultEnabled;
2401
314
  if (typeof rule === "boolean") return rule;
@@ -2434,9 +347,8 @@ function flattenNodes(spec) {
2434
347
  return flattened;
2435
348
  }
2436
349
  function worstSeverity(violations) {
2437
- const order = ["critical", "serious", "moderate", "minor"];
2438
350
  return violations.reduce(
2439
- (worst, violation) => order.indexOf(violation.severity) < order.indexOf(worst) ? violation.severity : worst,
351
+ (worst, violation) => maxSeverity(worst, violation.severity),
2440
352
  "minor"
2441
353
  );
2442
354
  }
@@ -2448,12 +360,25 @@ function result(validator, violations) {
2448
360
  violations
2449
361
  };
2450
362
  }
363
+ var SEVERITY_SCORE_CAPS = {
364
+ critical: 25,
365
+ serious: 50,
366
+ moderate: 80,
367
+ minor: 95
368
+ };
369
+ function verdictFor(violations) {
370
+ if (violations.length === 0) return "pass";
371
+ const worst = worstSeverity(violations);
372
+ return worst === "critical" || worst === "serious" ? "fail" : "warn";
373
+ }
2451
374
  function computeScore(violations) {
375
+ if (violations.length === 0) return 100;
2452
376
  const penalty = violations.reduce(
2453
377
  (sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
2454
378
  0
2455
379
  );
2456
- return Math.max(0, 100 - penalty);
380
+ const cap = SEVERITY_SCORE_CAPS[worstSeverity(violations)];
381
+ return Math.min(cap, Math.max(0, 100 - penalty));
2457
382
  }
2458
383
  function validateComponents(nodes, options) {
2459
384
  const rules = options.policy?.rules ?? {};
@@ -2524,7 +449,76 @@ function validateSafety(nodes) {
2524
449
  }
2525
450
  }
2526
451
  }
2527
- return result("safety", violations);
452
+ return result("safety", violations);
453
+ }
454
+ function editDistance(a, b) {
455
+ const rows = a.length + 1;
456
+ const cols = b.length + 1;
457
+ const distances = Array.from({ length: rows }, () => Array(cols).fill(0));
458
+ for (let i = 0; i < rows; i++) distances[i][0] = i;
459
+ for (let j = 0; j < cols; j++) distances[0][j] = j;
460
+ for (let i = 1; i < rows; i++) {
461
+ for (let j = 1; j < cols; j++) {
462
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
463
+ distances[i][j] = Math.min(
464
+ distances[i - 1][j] + 1,
465
+ distances[i][j - 1] + 1,
466
+ distances[i - 1][j - 1] + cost
467
+ );
468
+ }
469
+ }
470
+ return distances[a.length][b.length];
471
+ }
472
+ function normalizePropValue(value) {
473
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
474
+ }
475
+ function closestAllowedValue(rawValue, allowedValues) {
476
+ const normalizedRaw = normalizePropValue(rawValue);
477
+ const ranked = allowedValues.map((value) => {
478
+ const normalizedValue = normalizePropValue(value);
479
+ const distance = editDistance(normalizedRaw, normalizedValue);
480
+ const prefixMatch = normalizedRaw.length >= 4 && (normalizedValue.startsWith(normalizedRaw) || normalizedRaw.startsWith(normalizedValue));
481
+ return { value, distance, prefixMatch };
482
+ }).sort((a, b) => {
483
+ if (a.prefixMatch !== b.prefixMatch) return a.prefixMatch ? -1 : 1;
484
+ return a.distance - b.distance || a.value.localeCompare(b.value);
485
+ });
486
+ const best = ranked[0];
487
+ if (!best) return void 0;
488
+ if (best.prefixMatch || best.distance <= 2) return best.value;
489
+ return void 0;
490
+ }
491
+ function validateProps(nodes, options) {
492
+ const rules = options.policy?.rules ?? {};
493
+ const propRule = rules["props/valid-values"];
494
+ const componentProps = options.componentProps ?? {};
495
+ const violations = [];
496
+ if (!isRuleEnabled(propRule)) {
497
+ return result("props", violations);
498
+ }
499
+ for (const [index, node] of nodes.entries()) {
500
+ const type = nodeType(node);
501
+ const propSchema = componentProps[type] ?? componentProps[parentType(type)];
502
+ if (!propSchema) continue;
503
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
504
+ const allowedValues = propSchema[prop]?.values?.filter(Boolean) ?? [];
505
+ if (allowedValues.length === 0) continue;
506
+ const rawValue = typeof value === "string" ? value : void 0;
507
+ if (!rawValue || allowedValues.includes(rawValue)) continue;
508
+ const closest = closestAllowedValue(rawValue, allowedValues);
509
+ violations.push({
510
+ nodeId: nodeId(node, index),
511
+ nodeType: type,
512
+ rule: "props/invalid-value",
513
+ severity: ruleSeverity(propRule, "moderate"),
514
+ message: `Prop "${prop}" on ${type} has invalid value "${rawValue}"`,
515
+ suggestion: closest ? `Use "${closest}" for prop "${prop}" on ${type}.` : `Use one of: ${allowedValues.join(", ")}`,
516
+ prop,
517
+ rawValue
518
+ });
519
+ }
520
+ }
521
+ return result("props", violations);
2528
522
  }
2529
523
  function hasHardcodedCssValue(value) {
2530
524
  if (value.includes("var(")) return false;
@@ -2576,14 +570,29 @@ function validateTokens(nodes, options) {
2576
570
  }
2577
571
  return result("tokens", violations);
2578
572
  }
573
+ function textFromUnknown(value) {
574
+ if (typeof value === "string") return value.trim();
575
+ if (Array.isArray(value)) return textFromChildren(value);
576
+ if (typeof value === "object" && value !== null) {
577
+ return textFromNode(value);
578
+ }
579
+ return "";
580
+ }
581
+ function textFromNode(node) {
582
+ const type = nodeType(node).toLowerCase();
583
+ const props = node.props ?? {};
584
+ const propText = [
585
+ textFromUnknown(props.children),
586
+ type === "text" ? textFromUnknown(props.value) : ""
587
+ ].filter(Boolean);
588
+ const childText = textFromChildren(node.children);
589
+ return [...propText, childText].join(" ").trim();
590
+ }
2579
591
  function textFromChildren(children) {
592
+ if (typeof children === "string") return children.trim();
2580
593
  if (!Array.isArray(children)) return "";
2581
594
  return children.map((child) => {
2582
- if (typeof child === "string") return child;
2583
- if (typeof child === "object" && child !== null && !Array.isArray(child)) {
2584
- return textFromChildren(child.children);
2585
- }
2586
- return "";
595
+ return textFromUnknown(child);
2587
596
  }).join(" ").trim();
2588
597
  }
2589
598
  function validateA11y(nodes) {
@@ -2593,7 +602,7 @@ function validateA11y(nodes) {
2593
602
  if (!/button/i.test(type)) continue;
2594
603
  const props = node.props ?? {};
2595
604
  const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
2596
- const childText = textFromChildren(node.children);
605
+ const childText = textFromNode(node);
2597
606
  if (typeof label === "string" && label.trim().length > 0) continue;
2598
607
  if (childText.length > 0) continue;
2599
608
  violations.push({
@@ -2613,6 +622,7 @@ function runSpecGovern(spec, options) {
2613
622
  const results = [
2614
623
  validateSafety(nodes),
2615
624
  validateComponents(nodes, options),
625
+ validateProps(nodes, options),
2616
626
  validateTokens(nodes, options),
2617
627
  validateA11y(nodes)
2618
628
  ];
@@ -2621,6 +631,7 @@ function runSpecGovern(spec, options) {
2621
631
  new Set(nodes.map((node) => nodeType(node)))
2622
632
  );
2623
633
  return {
634
+ verdict: verdictFor(violations),
2624
635
  passed: results.every((entry) => entry.passed),
2625
636
  score: computeScore(violations),
2626
637
  results,
@@ -2638,7 +649,7 @@ function formatVerdict(verdict, format = "summary") {
2638
649
  }
2639
650
  const lines = [];
2640
651
  const icon = verdict.passed ? "ok" : "fail";
2641
- lines.push(`${icon} Governance check: score ${verdict.score}/100`);
652
+ lines.push(`${icon} Governance check: verdict ${verdict.verdict}, score ${verdict.score}/100`);
2642
653
  lines.push("");
2643
654
  for (const entry of verdict.results) {
2644
655
  const resultIcon = entry.passed ? "ok" : "fail";
@@ -2657,6 +668,14 @@ function formatVerdict(verdict, format = "summary") {
2657
668
  }
2658
669
 
2659
670
  // src/tools/govern.ts
671
+ function buildComponentProps(ctx) {
672
+ return Object.fromEntries(
673
+ Object.values(ctx.data.components).map((component) => [
674
+ component.name,
675
+ component.props
676
+ ])
677
+ );
678
+ }
2660
679
  var governHandler = async (args, ctx) => {
2661
680
  const spec = args?.spec;
2662
681
  if (!spec || typeof spec !== "object") {
@@ -2665,7 +684,7 @@ var governHandler = async (args, ctx) => {
2665
684
  {
2666
685
  type: "text",
2667
686
  text: JSON.stringify({
2668
- error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
687
+ error: "spec is required and must be an object with { nodes: [{ id, type, props, children }], root?, metadata? }. See the govern.schema MCP resource for the full schema."
2669
688
  })
2670
689
  }
2671
690
  ],
@@ -2681,12 +700,15 @@ var governHandler = async (args, ctx) => {
2681
700
  const verdict = runSpecGovern(spec, {
2682
701
  allowedComponents,
2683
702
  tokenPrefix: ctx.data.tokens?.prefix,
2684
- policy: policyOverrides
703
+ policy: policyOverrides,
704
+ componentProps: buildComponentProps(ctx)
2685
705
  });
2686
706
  const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
2687
707
  return {
2688
708
  content: [{ type: "text", text }],
2689
709
  _meta: {
710
+ ...getCatalogMeta(ctx.data),
711
+ verdict: verdict.verdict,
2690
712
  score: verdict.score,
2691
713
  passed: verdict.passed,
2692
714
  violationCount: verdict.results.reduce(
@@ -2703,7 +725,7 @@ var governHandler = async (args, ctx) => {
2703
725
  {
2704
726
  type: "text",
2705
727
  text: JSON.stringify({
2706
- error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id: string, type: string, props: object, children?: string[] }] }` : message
728
+ error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id?: string, type: string, props?: object, children?: array|string }], root?: string, metadata?: object }. See the govern.schema MCP resource for examples.` : message
2707
729
  })
2708
730
  }
2709
731
  ],
@@ -2752,8 +774,11 @@ function validateNodeShape(node, path) {
2752
774
  return `${path}.props must be an object`;
2753
775
  }
2754
776
  if ("children" in node && node.children !== void 0) {
777
+ if (typeof node.children === "string") {
778
+ return null;
779
+ }
2755
780
  if (!Array.isArray(node.children)) {
2756
- return `${path}.children must be an array`;
781
+ return `${path}.children must be an array or string`;
2757
782
  }
2758
783
  for (const [index, child] of node.children.entries()) {
2759
784
  if (!isPlainObject(child)) continue;
@@ -2898,6 +923,59 @@ function getNextAction(status, replacements, unresolvedAmbiguityCount) {
2898
923
  if (replacements.length > 0) return "review_partial_fix";
2899
924
  return "revise_input";
2900
925
  }
926
+ function authorizationForAmbiguity(args) {
927
+ const required = [];
928
+ if (!args.applyFixes) {
929
+ required.push("applyFixes");
930
+ }
931
+ if (!args.allowElicitation) {
932
+ required.push("allowElicitation");
933
+ } else if (!args.supportsElicitation) {
934
+ required.push("clientCapabilities.elicitation.form");
935
+ }
936
+ if (!args.allowSampling) {
937
+ required.push("allowSampling");
938
+ } else if (!args.supportsSampling) {
939
+ required.push("clientCapabilities.sampling");
940
+ }
941
+ return required;
942
+ }
943
+ function buildWouldFixIfAuthorized(args) {
944
+ const entries = [];
945
+ if (!args.applyFixes) {
946
+ entries.push(
947
+ ...args.deterministicPreview.map((replacement) => ({
948
+ action: "replace_component",
949
+ nodeId: replacement.nodeId,
950
+ from: replacement.from,
951
+ to: replacement.to,
952
+ reason: replacement.reason,
953
+ requiredAuthorization: ["applyFixes"]
954
+ }))
955
+ );
956
+ }
957
+ const requiredAuthorization = authorizationForAmbiguity({
958
+ applyFixes: args.applyFixes,
959
+ allowElicitation: args.allowElicitation,
960
+ allowSampling: args.allowSampling,
961
+ supportsElicitation: args.supportsElicitation,
962
+ supportsSampling: args.supportsSampling
963
+ });
964
+ if (requiredAuthorization.length > 0) {
965
+ entries.push(
966
+ ...args.unresolvedAmbiguities.map((ambiguity) => ({
967
+ action: "resolve_ambiguous_component",
968
+ nodeId: ambiguity.nodeId,
969
+ from: ambiguity.from,
970
+ candidates: ambiguity.candidates,
971
+ rankedCandidates: ambiguity.rankedCandidates,
972
+ reason: ambiguity.reason,
973
+ requiredAuthorization
974
+ }))
975
+ );
976
+ }
977
+ return entries;
978
+ }
2901
979
  async function emitValidateAndFixTelemetry(args) {
2902
980
  if (!args.ctx.mcp?.server) return;
2903
981
  try {
@@ -2919,11 +997,20 @@ async function emitValidateAndFixTelemetry(args) {
2919
997
  function buildAllowedComponents(ctx) {
2920
998
  return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
2921
999
  }
1000
+ function buildComponentProps2(ctx) {
1001
+ return Object.fromEntries(
1002
+ Object.values(ctx.data.components).map((component) => [
1003
+ component.name,
1004
+ component.props
1005
+ ])
1006
+ );
1007
+ }
2922
1008
  function runGovern(spec, ctx, policyOverrides) {
2923
1009
  return runSpecGovern(spec, {
2924
1010
  allowedComponents: buildAllowedComponents(ctx),
2925
1011
  tokenPrefix: ctx.data.tokens?.prefix,
2926
- policy: policyOverrides
1012
+ policy: policyOverrides,
1013
+ componentProps: buildComponentProps2(ctx)
2927
1014
  });
2928
1015
  }
2929
1016
  function walkNodes2(nodes, visitor, path = "nodes") {
@@ -3237,11 +1324,14 @@ var validateAndFixHandler = async (args, ctx) => {
3237
1324
  let replacements = [];
3238
1325
  let ambiguities = [];
3239
1326
  const resolutionPath = [];
1327
+ const supportsElicitation = supportsFormElicitation(ctx);
1328
+ const supportsModelSampling = supportsSampling(ctx);
1329
+ const preview = !originalVerdict.passed ? applyDeterministicReplacements(spec, ctx) : void 0;
1330
+ if (!applyFixes && preview) {
1331
+ ambiguities = preview.ambiguities;
1332
+ }
3240
1333
  if (!originalVerdict.passed && applyFixes) {
3241
- const result2 = applyDeterministicReplacements(
3242
- spec,
3243
- ctx
3244
- );
1334
+ const result2 = preview ?? applyDeterministicReplacements(spec, ctx);
3245
1335
  replacements = result2.replacements;
3246
1336
  ambiguities = result2.ambiguities;
3247
1337
  if (replacements.length > 0) {
@@ -3251,7 +1341,7 @@ var validateAndFixHandler = async (args, ctx) => {
3251
1341
  const unresolvedAmbiguities2 = ambiguities.filter(
3252
1342
  (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
3253
1343
  );
3254
- if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsFormElicitation(ctx)) {
1344
+ if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsElicitation) {
3255
1345
  workingSpec ??= cloneSpec(spec);
3256
1346
  const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
3257
1347
  unresolvedAmbiguities2,
@@ -3266,7 +1356,7 @@ var validateAndFixHandler = async (args, ctx) => {
3266
1356
  const remainingAmbiguities = ambiguities.filter(
3267
1357
  (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
3268
1358
  );
3269
- if (remainingAmbiguities.length > 0 && allowSampling && supportsSampling(ctx)) {
1359
+ if (remainingAmbiguities.length > 0 && allowSampling && supportsModelSampling) {
3270
1360
  workingSpec ??= cloneSpec(spec);
3271
1361
  const sampledReplacements = await resolveAmbiguitiesWithSampling(
3272
1362
  remainingAmbiguities,
@@ -3287,6 +1377,18 @@ var validateAndFixHandler = async (args, ctx) => {
3287
1377
  const unresolvedAmbiguities = ambiguities.filter(
3288
1378
  (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
3289
1379
  ).map(({ nodeRef: _nodeRef, ...ambiguity }) => ambiguity);
1380
+ const unresolvedAmbiguitiesWithRefs = ambiguities.filter(
1381
+ (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
1382
+ );
1383
+ const wouldFixIfAuthorized = buildWouldFixIfAuthorized({
1384
+ deterministicPreview: preview?.replacements ?? [],
1385
+ unresolvedAmbiguities: unresolvedAmbiguitiesWithRefs,
1386
+ applyFixes,
1387
+ allowElicitation,
1388
+ allowSampling,
1389
+ supportsElicitation,
1390
+ supportsSampling: supportsModelSampling
1391
+ });
3290
1392
  const attestation = {
3291
1393
  sourceType: ctx.data.snapshot.sourceType,
3292
1394
  sourceLabel: ctx.data.snapshot.sourceLabel,
@@ -3301,9 +1403,9 @@ var validateAndFixHandler = async (args, ctx) => {
3301
1403
  overrideApplied: Boolean(policyOverrides)
3302
1404
  },
3303
1405
  clientCapabilities: {
3304
- sampling: supportsSampling(ctx),
1406
+ sampling: supportsModelSampling,
3305
1407
  samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
3306
- elicitationForm: supportsFormElicitation(ctx),
1408
+ elicitationForm: supportsElicitation,
3307
1409
  roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
3308
1410
  },
3309
1411
  capabilitiesUsed: {
@@ -3333,6 +1435,7 @@ var validateAndFixHandler = async (args, ctx) => {
3333
1435
  replacements,
3334
1436
  unresolvedAmbiguities.length
3335
1437
  );
1438
+ const catalogMeta = getCatalogMeta(ctx.data);
3336
1439
  const payload = {
3337
1440
  status,
3338
1441
  nextAction,
@@ -3358,7 +1461,8 @@ var validateAndFixHandler = async (args, ctx) => {
3358
1461
  effectiveComponents,
3359
1462
  "forbidden"
3360
1463
  ),
3361
- unresolvedAmbiguities
1464
+ unresolvedAmbiguities,
1465
+ wouldFixIfAuthorized
3362
1466
  };
3363
1467
  await emitValidateAndFixTelemetry({
3364
1468
  ctx,
@@ -3381,13 +1485,14 @@ var validateAndFixHandler = async (args, ctx) => {
3381
1485
  }
3382
1486
  ],
3383
1487
  _meta: {
1488
+ ...catalogMeta,
3384
1489
  status,
3385
1490
  nextAction,
3386
1491
  replacementCount: replacements.length,
3387
1492
  passed: finalVerdict.passed,
3388
1493
  unresolvedAmbiguityCount: unresolvedAmbiguities.length,
3389
- resolutionPath,
3390
- catalogRevision: attestation.catalogRevision
1494
+ wouldFixIfAuthorizedCount: wouldFixIfAuthorized.length,
1495
+ resolutionPath
3391
1496
  }
3392
1497
  };
3393
1498
  } catch (error) {
@@ -3406,52 +1511,22 @@ var validateAndFixHandler = async (args, ctx) => {
3406
1511
  }
3407
1512
  };
3408
1513
 
3409
- // src/tools/generate-ui.ts
3410
- var generateUiHandler = async (args, ctx) => {
3411
- const prompt = args?.prompt;
3412
- if (!prompt) {
3413
- throw new Error("prompt is required");
3414
- }
3415
- const currentTree = args?.currentTree;
3416
- const playgroundUrl = ctx.config.playgroundUrl ?? "https://usefragments.com";
3417
- const response = await fetch(`${playgroundUrl}/api/playground/generate`, {
3418
- method: "POST",
3419
- headers: { "Content-Type": "application/json" },
3420
- body: JSON.stringify({
3421
- prompt,
3422
- ...currentTree && { currentSpec: currentTree }
3423
- })
3424
- });
3425
- if (!response.ok) {
3426
- const errorBody = await response.text();
3427
- throw new Error(`Playground API error (${response.status}): ${errorBody}`);
3428
- }
3429
- const text = await response.text();
3430
- return {
3431
- content: [{
3432
- type: "text",
3433
- text
3434
- }]
3435
- };
3436
- };
3437
-
3438
- // src/findings-service.ts
1514
+ // src/cloud-http.ts
3439
1515
  var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
3440
1516
  function normalizeCloudUrl(url) {
3441
1517
  if (!url) return DEFAULT_CLOUD_URL;
3442
1518
  return url.replace(/\/+$/, "");
3443
1519
  }
3444
- async function fetchFindings(apiKey, params, cloudUrl) {
3445
- const base = normalizeCloudUrl(cloudUrl);
3446
- const url = new URL(`${base}/api/findings`);
3447
- if (params.status) url.searchParams.set("status", params.status);
3448
- if (params.severity) url.searchParams.set("severity", params.severity);
3449
- if (params.category) url.searchParams.set("category", params.category);
3450
- if (params.ruleId) url.searchParams.set("ruleId", params.ruleId);
3451
- if (params.filePath) url.searchParams.set("filePath", params.filePath);
3452
- if (params.limit != null) url.searchParams.set("limit", String(params.limit));
1520
+ async function cloudFetchJson(args) {
1521
+ const base = normalizeCloudUrl(args.cloudUrl);
1522
+ const url = new URL(`${base}${args.path}`);
1523
+ if (args.query) {
1524
+ for (const [key, value] of Object.entries(args.query)) {
1525
+ if (value !== void 0) url.searchParams.set(key, String(value));
1526
+ }
1527
+ }
3453
1528
  const response = await fetch(url.toString(), {
3454
- headers: { "X-API-Key": apiKey }
1529
+ headers: { "X-API-Key": args.apiKey }
3455
1530
  });
3456
1531
  if (!response.ok) {
3457
1532
  const body = await response.text();
@@ -3463,11 +1538,29 @@ async function fetchFindings(apiKey, params, cloudUrl) {
3463
1538
  message = body;
3464
1539
  }
3465
1540
  throw new Error(
3466
- `Cloud findings API error (${response.status}): ${message}`
1541
+ `Cloud ${args.resource} API error (${response.status}): ${message}`
3467
1542
  );
3468
1543
  }
3469
1544
  return await response.json();
3470
1545
  }
1546
+
1547
+ // src/findings-service.ts
1548
+ async function fetchFindings(apiKey, params, cloudUrl) {
1549
+ return cloudFetchJson({
1550
+ apiKey,
1551
+ cloudUrl,
1552
+ path: "/api/findings",
1553
+ resource: "findings",
1554
+ query: {
1555
+ status: params.status,
1556
+ severity: params.severity,
1557
+ category: params.category,
1558
+ ruleId: params.ruleId,
1559
+ filePath: params.filePath,
1560
+ limit: params.limit
1561
+ }
1562
+ });
1563
+ }
3471
1564
  async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
3472
1565
  return fetchFindings(
3473
1566
  apiKey,
@@ -3475,8 +1568,59 @@ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
3475
1568
  cloudUrl
3476
1569
  );
3477
1570
  }
1571
+ function buildFindingSourceUrl(finding) {
1572
+ const { repoFullName, commitSha, filePath, line } = finding;
1573
+ if (!repoFullName || !commitSha || !filePath) return void 0;
1574
+ if (!/^[-\w.]+\/[-\w.]+$/.test(repoFullName)) return void 0;
1575
+ if (!/^[a-fA-F0-9]{7,64}$/.test(commitSha)) return void 0;
1576
+ const normalizedPath = filePath.replace(/^\/+/, "");
1577
+ if (!normalizedPath) return void 0;
1578
+ const encodedPath = normalizedPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1579
+ const lineSuffix = line != null && line > 0 ? `#L${Math.floor(line)}` : "";
1580
+ return `https://github.com/${repoFullName}/blob/${commitSha}/${encodedPath}${lineSuffix}`;
1581
+ }
1582
+ async function fetchFindingsSummary(apiKey, params, cloudUrl) {
1583
+ const { findings } = await fetchFindings(
1584
+ apiKey,
1585
+ { ...params, limit: params.limit ?? 200 },
1586
+ cloudUrl
1587
+ );
1588
+ return summarizeFindings(findings, params);
1589
+ }
1590
+ function summarizeFindings(findings, params) {
1591
+ const bySeverity = { error: 0, warning: 0, info: 0 };
1592
+ const byStatus = { open: 0, resolved: 0, ignored: 0 };
1593
+ const byCategory = {};
1594
+ const byRuleId = {};
1595
+ const byFilePath = {};
1596
+ for (const finding of findings) {
1597
+ bySeverity[finding.severity] += 1;
1598
+ byStatus[finding.status] += 1;
1599
+ increment(byRuleId, finding.ruleId);
1600
+ if (finding.category) increment(byCategory, finding.category);
1601
+ if (finding.filePath) increment(byFilePath, finding.filePath);
1602
+ }
1603
+ const { limit: _limit, ...filters } = params;
1604
+ return {
1605
+ total: findings.length,
1606
+ filters,
1607
+ bySeverity,
1608
+ byStatus,
1609
+ byCategory,
1610
+ byRuleId,
1611
+ byFilePath,
1612
+ topFiles: topEntries(byFilePath, "filePath"),
1613
+ topRules: topEntries(byRuleId, "ruleId")
1614
+ };
1615
+ }
1616
+ function increment(counts, key) {
1617
+ counts[key] = (counts[key] ?? 0) + 1;
1618
+ }
1619
+ function topEntries(counts, keyName) {
1620
+ return Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10).map(([key, count]) => ({ [keyName]: key, count }));
1621
+ }
3478
1622
 
3479
- // src/tools/findings.ts
1623
+ // src/tools/cloud-auth.ts
3480
1624
  function resolveCloudApiKey(ctx) {
3481
1625
  return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
3482
1626
  }
@@ -3496,6 +1640,38 @@ function missingKeyError() {
3496
1640
  isError: true
3497
1641
  };
3498
1642
  }
1643
+
1644
+ // src/tools/findings.ts
1645
+ function enrichFindings(findings, ctx) {
1646
+ return findings.map((finding) => {
1647
+ const sourceUrl = finding.sourceUrl ?? buildFindingSourceUrl(finding);
1648
+ const withSourceUrl = sourceUrl ? { ...finding, sourceUrl } : finding;
1649
+ if (!finding.prop || !finding.rawValue || !ctx.data.tokens) {
1650
+ return withSourceUrl;
1651
+ }
1652
+ const catalogMeta = getCatalogMeta(ctx.data);
1653
+ const suggestion = suggestToken({
1654
+ tokens: ctx.data.tokens,
1655
+ property: finding.prop,
1656
+ value: finding.rawValue,
1657
+ catalogRevision: catalogMeta.catalogRevision,
1658
+ updatedAt: catalogMeta.updatedAt
1659
+ });
1660
+ if (!suggestion.recommended) {
1661
+ const {
1662
+ suggestedToken: _discarded,
1663
+ suggestedTokenDetails: _details,
1664
+ ...rest
1665
+ } = withSourceUrl;
1666
+ return rest;
1667
+ }
1668
+ return {
1669
+ ...withSourceUrl,
1670
+ suggestedToken: suggestion.recommended.cssVar ?? suggestion.recommended.name,
1671
+ suggestedTokenDetails: suggestion.recommended
1672
+ };
1673
+ });
1674
+ }
3499
1675
  var findingsListHandler = async (args, ctx) => {
3500
1676
  const apiKey = resolveCloudApiKey(ctx);
3501
1677
  if (!apiKey) return missingKeyError();
@@ -3510,9 +1686,65 @@ var findingsListHandler = async (args, ctx) => {
3510
1686
  if (args.limit != null) params.limit = Number(args.limit);
3511
1687
  try {
3512
1688
  const result2 = await fetchFindings(apiKey, params, cloudUrl);
1689
+ const findings = enrichFindings(result2.findings, ctx);
1690
+ const catalogMeta = getCatalogMeta(ctx.data);
1691
+ return {
1692
+ content: [
1693
+ {
1694
+ type: "text",
1695
+ text: JSON.stringify({ ...result2, findings })
1696
+ }
1697
+ ],
1698
+ _meta: {
1699
+ ...catalogMeta,
1700
+ count: findings.length,
1701
+ tokenSuggestionCount: findings.filter(
1702
+ (finding) => finding.suggestedTokenDetails
1703
+ ).length
1704
+ }
1705
+ };
1706
+ } catch (error) {
1707
+ return {
1708
+ content: [
1709
+ {
1710
+ type: "text",
1711
+ text: JSON.stringify({
1712
+ error: error instanceof Error ? error.message : String(error)
1713
+ })
1714
+ }
1715
+ ],
1716
+ isError: true
1717
+ };
1718
+ }
1719
+ };
1720
+ var findingsSummaryHandler = async (args, ctx) => {
1721
+ const apiKey = resolveCloudApiKey(ctx);
1722
+ if (!apiKey) return missingKeyError();
1723
+ const cloudUrl = resolveCloudUrl(ctx);
1724
+ const params = {};
1725
+ if (args.status) params.status = args.status;
1726
+ if (args.severity)
1727
+ params.severity = args.severity;
1728
+ if (args.category) params.category = String(args.category);
1729
+ if (args.ruleId) params.ruleId = String(args.ruleId);
1730
+ if (args.filePath) params.filePath = String(args.filePath);
1731
+ if (args.limit != null) params.limit = Number(args.limit);
1732
+ try {
1733
+ const summary = await fetchFindingsSummary(apiKey, params, cloudUrl);
1734
+ const catalogMeta = getCatalogMeta(ctx.data);
3513
1735
  return {
3514
- content: [{ type: "text", text: JSON.stringify(result2) }],
3515
- _meta: { count: result2.findings.length }
1736
+ content: [
1737
+ {
1738
+ type: "text",
1739
+ text: JSON.stringify(summary)
1740
+ }
1741
+ ],
1742
+ _meta: {
1743
+ ...catalogMeta,
1744
+ total: summary.total,
1745
+ topFileCount: summary.topFiles.length,
1746
+ topRuleCount: summary.topRules.length
1747
+ }
3516
1748
  };
3517
1749
  } catch (error) {
3518
1750
  return {
@@ -3546,7 +1778,11 @@ var findingsForFileHandler = async (args, ctx) => {
3546
1778
  const cloudUrl = resolveCloudUrl(ctx);
3547
1779
  try {
3548
1780
  const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
3549
- const findings = result2.findings.filter((f) => f.filePath === filePath);
1781
+ const findings = enrichFindings(
1782
+ result2.findings.filter((f) => f.filePath === filePath),
1783
+ ctx
1784
+ );
1785
+ const catalogMeta = getCatalogMeta(ctx.data);
3550
1786
  return {
3551
1787
  content: [
3552
1788
  {
@@ -3554,7 +1790,14 @@ var findingsForFileHandler = async (args, ctx) => {
3554
1790
  text: JSON.stringify({ findings, filePath })
3555
1791
  }
3556
1792
  ],
3557
- _meta: { count: findings.length, filePath }
1793
+ _meta: {
1794
+ ...catalogMeta,
1795
+ count: findings.length,
1796
+ filePath,
1797
+ tokenSuggestionCount: findings.filter(
1798
+ (finding) => finding.suggestedTokenDetails
1799
+ ).length
1800
+ }
3558
1801
  };
3559
1802
  } catch (error) {
3560
1803
  return {
@@ -3571,43 +1814,208 @@ var findingsForFileHandler = async (args, ctx) => {
3571
1814
  }
3572
1815
  };
3573
1816
 
1817
+ // src/tools/swap-to-canonical.ts
1818
+ import * as ts from "typescript";
1819
+ import { resolveCanonicalForHtmlElement, formatRawHtmlElement } from "@fragments-sdk/classifier";
1820
+
1821
+ // src/canonical-mappings-service.ts
1822
+ async function fetchCanonicalMappings(apiKey, cloudUrl) {
1823
+ const catalog = await cloudFetchJson({
1824
+ apiKey,
1825
+ cloudUrl,
1826
+ path: "/api/catalog",
1827
+ resource: "catalog"
1828
+ });
1829
+ return catalog.canonicalMappings ?? [];
1830
+ }
1831
+
1832
+ // src/tools/swap-to-canonical.ts
1833
+ function errorResult(message) {
1834
+ return {
1835
+ content: [
1836
+ {
1837
+ type: "text",
1838
+ text: JSON.stringify({ error: message })
1839
+ }
1840
+ ],
1841
+ isError: true
1842
+ };
1843
+ }
1844
+ var resolveCanonicalForElement = resolveCanonicalForHtmlElement;
1845
+ function walkRawJsxElements(source) {
1846
+ const results = [];
1847
+ const visit = (node) => {
1848
+ let opening;
1849
+ if (ts.isJsxSelfClosingElement(node)) {
1850
+ opening = node;
1851
+ } else if (ts.isJsxElement(node)) {
1852
+ opening = node.openingElement;
1853
+ }
1854
+ if (opening) {
1855
+ const tagNameNode = opening.tagName;
1856
+ if (ts.isIdentifier(tagNameNode)) {
1857
+ const tagName = tagNameNode.text;
1858
+ if (/^[a-z]/.test(tagName)) {
1859
+ const attrs = /* @__PURE__ */ new Map();
1860
+ for (const attr of opening.attributes.properties) {
1861
+ if (!ts.isJsxAttribute(attr)) continue;
1862
+ if (!attr.name || !ts.isIdentifier(attr.name)) continue;
1863
+ const name = attr.name.text;
1864
+ if (!attr.initializer) {
1865
+ attrs.set(name, { value: "", dynamic: false });
1866
+ continue;
1867
+ }
1868
+ if (ts.isStringLiteral(attr.initializer)) {
1869
+ attrs.set(name, {
1870
+ value: attr.initializer.text,
1871
+ dynamic: false
1872
+ });
1873
+ } else if (ts.isJsxExpression(attr.initializer)) {
1874
+ attrs.set(name, { value: "", dynamic: true });
1875
+ }
1876
+ }
1877
+ const start = opening.getStart(source);
1878
+ const { line } = source.getLineAndCharacterOfPosition(start);
1879
+ results.push({ tagName, attrs, line: line + 1 });
1880
+ }
1881
+ }
1882
+ }
1883
+ ts.forEachChild(node, visit);
1884
+ };
1885
+ visit(source);
1886
+ return results;
1887
+ }
1888
+ var formatRawElement = formatRawHtmlElement;
1889
+ function buildPropMapping(attrs, rowMapping) {
1890
+ const transforms = new Map(rowMapping.map((m) => [m.rawProp, m]));
1891
+ const mapping = [];
1892
+ for (const rawProp of attrs.keys()) {
1893
+ if (rawProp === "type") continue;
1894
+ mapping.push(transforms.get(rawProp) ?? { rawProp, canonicalProp: rawProp });
1895
+ }
1896
+ return mapping;
1897
+ }
1898
+ function groupMappingsByCanonical(mappings) {
1899
+ const out = /* @__PURE__ */ new Map();
1900
+ for (const m of mappings) {
1901
+ if (!m.importPath) continue;
1902
+ const list = out.get(m.canonical) ?? [];
1903
+ list.push(m);
1904
+ out.set(m.canonical, list);
1905
+ }
1906
+ for (const list of out.values()) {
1907
+ list.sort((a, b) => {
1908
+ const ac = a.confidence ?? 0;
1909
+ const bc = b.confidence ?? 0;
1910
+ if (ac !== bc) return bc - ac;
1911
+ return a.name.localeCompare(b.name);
1912
+ });
1913
+ }
1914
+ return out;
1915
+ }
1916
+ function buildSwapSuggestions(args) {
1917
+ const eligible = args.mappings.filter(
1918
+ (m) => (m.canonicalStatus ?? legacyStatusForMapping(m)) === "confirmed"
1919
+ );
1920
+ if (eligible.length === 0) return [];
1921
+ const byCanonical = groupMappingsByCanonical(eligible);
1922
+ if (byCanonical.size === 0) return [];
1923
+ const source = ts.createSourceFile(
1924
+ args.filePath,
1925
+ args.fileContent,
1926
+ ts.ScriptTarget.Latest,
1927
+ /* setParentNodes */
1928
+ true,
1929
+ ts.ScriptKind.TSX
1930
+ );
1931
+ const elements = walkRawJsxElements(source);
1932
+ const suggestions = [];
1933
+ for (const el of elements) {
1934
+ const canonical = resolveCanonicalForElement(el.tagName, el.attrs);
1935
+ if (!canonical) continue;
1936
+ const candidates = byCanonical.get(canonical);
1937
+ if (!candidates || candidates.length === 0) continue;
1938
+ const primary = candidates[0];
1939
+ const importPath = primary.importPath;
1940
+ if (!importPath) continue;
1941
+ const suggestion = {
1942
+ rawElement: formatRawElement(el.tagName, el.attrs),
1943
+ canonical,
1944
+ componentName: primary.name,
1945
+ importPath,
1946
+ propMapping: buildPropMapping(el.attrs, primary.propMapping),
1947
+ line: el.line
1948
+ };
1949
+ if (candidates.length > 1) {
1950
+ suggestion.alternates = candidates.slice(1).filter((c) => c.importPath).map((c) => ({
1951
+ name: c.name,
1952
+ importPath: c.importPath,
1953
+ confidence: c.confidence ?? 0
1954
+ }));
1955
+ }
1956
+ suggestions.push(suggestion);
1957
+ }
1958
+ return suggestions;
1959
+ }
1960
+ function legacyStatusForMapping(mapping) {
1961
+ return mapping.canonicalConfidence === "overridden" || mapping.canonicalConfidence === "auto" ? "confirmed" : "proposed";
1962
+ }
1963
+ var swapToCanonicalHandler = async (args, ctx) => {
1964
+ const apiKey = resolveCloudApiKey(ctx);
1965
+ if (!apiKey) return missingKeyError();
1966
+ const filePath = typeof args.filePath === "string" ? args.filePath : "";
1967
+ const fileContent = typeof args.fileContent === "string" ? args.fileContent : "";
1968
+ if (!filePath || !fileContent) {
1969
+ return errorResult("filePath and fileContent are required.");
1970
+ }
1971
+ const cloudUrl = resolveCloudUrl(ctx);
1972
+ let mappings;
1973
+ try {
1974
+ mappings = await fetchCanonicalMappings(apiKey, cloudUrl);
1975
+ } catch (error) {
1976
+ return errorResult(error instanceof Error ? error.message : String(error));
1977
+ }
1978
+ const suggestions = buildSwapSuggestions({
1979
+ filePath,
1980
+ fileContent,
1981
+ mappings
1982
+ });
1983
+ const message = mappings.length === 0 ? "No confirmed canonical primitives are configured for this project. Visit the Components page to confirm primitives." : void 0;
1984
+ return {
1985
+ content: [
1986
+ {
1987
+ type: "text",
1988
+ text: JSON.stringify({ suggestions, filePath, message })
1989
+ }
1990
+ ],
1991
+ _meta: {
1992
+ count: suggestions.length,
1993
+ mappingCount: mappings.length,
1994
+ filePath
1995
+ }
1996
+ };
1997
+ };
1998
+
3574
1999
  // src/tools/index.ts
3575
2000
  var CORE_TOOLS = {
3576
- discover: discoverHandler,
3577
- inspect: inspectHandler,
3578
- blocks: blocksHandler,
3579
- tokens: tokensHandler,
3580
- graph: graphHandler,
3581
- perf: perfHandler,
2001
+ "tokens.suggest": tokensSuggestHandler,
3582
2002
  govern: governHandler,
3583
2003
  validate_and_fix: validateAndFixHandler,
3584
2004
  findings_list: findingsListHandler,
3585
- findings_for_file: findingsForFileHandler
3586
- };
3587
- var VIEWER_TOOLS = {
3588
- render: renderHandler,
3589
- fix: fixHandler,
3590
- a11y: a11yHandler
3591
- };
3592
- var INFRA_TOOLS = {
3593
- generate_ui: generateUiHandler
2005
+ findings_summary: findingsSummaryHandler,
2006
+ findings_for_file: findingsForFileHandler,
2007
+ swap_to_canonical: swapToCanonicalHandler
3594
2008
  };
2009
+ var VIEWER_TOOLS = {};
2010
+ var INFRA_TOOLS = {};
3595
2011
  var BUILTIN_TOOLS = {
3596
2012
  ...CORE_TOOLS,
3597
2013
  ...VIEWER_TOOLS,
3598
2014
  ...INFRA_TOOLS
3599
2015
  };
3600
2016
  var TOOL_CAPABILITIES = {
3601
- discover: ["components"],
3602
- inspect: ["components"],
3603
- blocks: ["blocks"],
3604
- tokens: ["tokens"],
3605
- graph: ["graph"],
3606
- perf: ["performance"],
3607
- validate_and_fix: ["components"],
3608
- render: ["components"],
3609
- fix: ["components"],
3610
- a11y: ["components"]
2017
+ "tokens.suggest": ["tokens"],
2018
+ validate_and_fix: ["components"]
3611
2019
  };
3612
2020
 
3613
2021
  // src/registry.ts
@@ -3747,16 +2155,17 @@ function telemetryMiddleware(logger) {
3747
2155
  }
3748
2156
 
3749
2157
  // src/source-selection.ts
3750
- import { existsSync as existsSync8 } from "fs";
3751
- import { join as join7 } from "path";
2158
+ import { existsSync as existsSync7 } from "fs";
2159
+ import { join as join6 } from "path";
3752
2160
 
3753
2161
  // src/adapters/fragments-json.ts
3754
2162
  import { readFile } from "fs/promises";
3755
2163
 
3756
2164
  // src/discovery.ts
3757
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
3758
- import { join as join3, dirname, resolve } from "path";
2165
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
2166
+ import { join as join2, dirname, resolve } from "path";
3759
2167
  import { createRequire } from "module";
2168
+ import { BRAND } from "@fragments-sdk/core";
3760
2169
  function resolveWorkspaceGlob(baseDir, pattern) {
3761
2170
  const parts = pattern.split("/");
3762
2171
  let dirs = [baseDir];
@@ -3768,14 +2177,14 @@ function resolveWorkspaceGlob(baseDir, pattern) {
3768
2177
  try {
3769
2178
  for (const entry of readdirSync(d, { withFileTypes: true })) {
3770
2179
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
3771
- next.push(join3(d, entry.name));
2180
+ next.push(join2(d, entry.name));
3772
2181
  }
3773
2182
  }
3774
2183
  } catch {
3775
2184
  }
3776
2185
  } else {
3777
- const candidate = join3(d, part);
3778
- if (existsSync3(candidate)) next.push(candidate);
2186
+ const candidate = join2(d, part);
2187
+ if (existsSync2(candidate)) next.push(candidate);
3779
2188
  }
3780
2189
  }
3781
2190
  dirs = next;
@@ -3784,8 +2193,8 @@ function resolveWorkspaceGlob(baseDir, pattern) {
3784
2193
  }
3785
2194
  function getWorkspaceDirs(rootDir) {
3786
2195
  const dirs = [];
3787
- const rootPkgPath = join3(rootDir, "package.json");
3788
- if (existsSync3(rootPkgPath)) {
2196
+ const rootPkgPath = join2(rootDir, "package.json");
2197
+ if (existsSync2(rootPkgPath)) {
3789
2198
  try {
3790
2199
  const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
3791
2200
  const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
@@ -3798,8 +2207,8 @@ function getWorkspaceDirs(rootDir) {
3798
2207
  } catch {
3799
2208
  }
3800
2209
  }
3801
- const pnpmWsPath = join3(rootDir, "pnpm-workspace.yaml");
3802
- if (existsSync3(pnpmWsPath)) {
2210
+ const pnpmWsPath = join2(rootDir, "pnpm-workspace.yaml");
2211
+ if (existsSync2(pnpmWsPath)) {
3803
2212
  try {
3804
2213
  const content = readFileSync3(pnpmWsPath, "utf-8");
3805
2214
  const lines = content.split("\n");
@@ -3832,8 +2241,8 @@ function resolveDepPackageJson(localRequire, depName) {
3832
2241
  const mainPath = localRequire.resolve(depName);
3833
2242
  let dir = dirname(mainPath);
3834
2243
  while (true) {
3835
- const candidate = join3(dir, "package.json");
3836
- if (existsSync3(candidate)) {
2244
+ const candidate = join2(dir, "package.json");
2245
+ if (existsSync2(candidate)) {
3837
2246
  const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
3838
2247
  if (pkg.name === depName) return candidate;
3839
2248
  }
@@ -3846,23 +2255,23 @@ function resolveDepPackageJson(localRequire, depName) {
3846
2255
  return null;
3847
2256
  }
3848
2257
  function findFragmentsInDeps(dir, found, depField) {
3849
- const pkgJsonPath = join3(dir, "package.json");
3850
- if (!existsSync3(pkgJsonPath)) return;
2258
+ const pkgJsonPath = join2(dir, "package.json");
2259
+ if (!existsSync2(pkgJsonPath)) return;
3851
2260
  try {
3852
2261
  const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
3853
2262
  const allDeps = {
3854
2263
  ...pkgJson.dependencies,
3855
2264
  ...pkgJson.devDependencies
3856
2265
  };
3857
- const localRequire = createRequire(join3(dir, "noop.js"));
2266
+ const localRequire = createRequire(join2(dir, "noop.js"));
3858
2267
  for (const depName of Object.keys(allDeps)) {
3859
2268
  try {
3860
2269
  const depPkgPath = resolveDepPackageJson(localRequire, depName);
3861
2270
  if (!depPkgPath) continue;
3862
2271
  const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
3863
2272
  if (depPkg[depField]) {
3864
- const fragmentsPath = join3(dirname(depPkgPath), depPkg[depField]);
3865
- if (existsSync3(fragmentsPath) && !found.includes(fragmentsPath)) {
2273
+ const fragmentsPath = join2(dirname(depPkgPath), depPkg[depField]);
2274
+ if (existsSync2(fragmentsPath) && !found.includes(fragmentsPath)) {
3866
2275
  found.push(fragmentsPath);
3867
2276
  }
3868
2277
  }
@@ -3877,8 +2286,8 @@ function findDesignSystemJson(startDir, outFile, depField) {
3877
2286
  const resolvedStart = resolve(startDir);
3878
2287
  let dir = resolvedStart;
3879
2288
  while (true) {
3880
- const candidate = join3(dir, outFile);
3881
- if (existsSync3(candidate)) {
2289
+ const candidate = join2(dir, outFile);
2290
+ if (existsSync2(candidate)) {
3882
2291
  found.push(candidate);
3883
2292
  break;
3884
2293
  }
@@ -3887,7 +2296,7 @@ function findDesignSystemJson(startDir, outFile, depField) {
3887
2296
  dir = parent;
3888
2297
  }
3889
2298
  findFragmentsInDeps(resolvedStart, found, depField);
3890
- if (found.length === 0 || existsSync3(join3(resolvedStart, "pnpm-workspace.yaml"))) {
2299
+ if (found.length === 0 || existsSync2(join2(resolvedStart, "pnpm-workspace.yaml"))) {
3891
2300
  const workspaceDirs = getWorkspaceDirs(resolvedStart);
3892
2301
  for (const wsDir of workspaceDirs) {
3893
2302
  findFragmentsInDeps(wsDir, found, depField);
@@ -3902,8 +2311,8 @@ function findBundleManifest(startDir) {
3902
2311
  const found = [];
3903
2312
  let dir = resolve(startDir);
3904
2313
  while (true) {
3905
- const candidate = join3(dir, BRAND.dataDir, BRAND.manifestFile);
3906
- if (existsSync3(candidate)) {
2314
+ const candidate = join2(dir, BRAND.dataDir, BRAND.manifestFile);
2315
+ if (existsSync2(candidate)) {
3907
2316
  found.push(candidate);
3908
2317
  break;
3909
2318
  }
@@ -3914,6 +2323,9 @@ function findBundleManifest(startDir) {
3914
2323
  return found;
3915
2324
  }
3916
2325
 
2326
+ // src/adapters/fragments-json.ts
2327
+ import { BRAND as BRAND2 } from "@fragments-sdk/core";
2328
+
3917
2329
  // src/adapters/snapshot-converters.ts
3918
2330
  import { mcpSnapshotSchema } from "@fragments-sdk/core";
3919
2331
  function slugify(value) {
@@ -4059,13 +2471,15 @@ function tokensFromCompiledTokenData(tokens) {
4059
2471
  category,
4060
2472
  value: valueToString(entry.value),
4061
2473
  description: entry.description
4062
- }));
4063
- categories[category] = normalized;
4064
- flat.push(...normalized);
2474
+ })).filter((token) => !isGarbageToken(token));
2475
+ if (normalized.length > 0) {
2476
+ categories[category] = normalized;
2477
+ flat.push(...normalized);
2478
+ }
4065
2479
  }
4066
2480
  return {
4067
2481
  prefix: tokens.prefix,
4068
- total: tokens.total,
2482
+ total: flat.length,
4069
2483
  categories,
4070
2484
  flat
4071
2485
  };
@@ -4104,18 +2518,18 @@ var FragmentsJsonAdapter = class {
4104
2518
  const paths = this.discover(projectRoot);
4105
2519
  if (paths.length === 0) {
4106
2520
  throw new Error(
4107
- `No ${BRAND.outFile} found. Searched ${projectRoot} and package.json dependencies.
2521
+ `No ${BRAND2.outFile} found. Searched ${projectRoot} and package.json dependencies.
4108
2522
 
4109
2523
  Fix: Add a project-level MCP config so the server runs from your workspace root:
4110
2524
 
4111
2525
  Cursor: .cursor/mcp.json
4112
2526
  VS Code: .vscode/mcp.json
4113
- Claude: claude mcp add ${BRAND.nameLower} -- npx @fragments-sdk/mcp
2527
+ Claude: claude mcp add ${BRAND2.nameLower} -- npx @fragments-sdk/mcp
4114
2528
  Windsurf: .windsurf/mcp.json
4115
2529
 
4116
2530
  Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
4117
2531
 
4118
- If you're a library author, run \`${BRAND.cliCommand} build\` first.`
2532
+ If you're a library author, run \`${BRAND2.cliCommand} build\` first.`
4119
2533
  );
4120
2534
  }
4121
2535
  const content = await readFile(paths[0], "utf-8");
@@ -4164,7 +2578,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
4164
2578
  const snapshot = validateSnapshot({
4165
2579
  schemaVersion: 1,
4166
2580
  sourceType: "fragments-json",
4167
- sourceLabel: BRAND.outFile,
2581
+ sourceLabel: BRAND2.outFile,
4168
2582
  capabilities: buildCapabilities({
4169
2583
  components,
4170
2584
  blocks,
@@ -4200,12 +2614,12 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
4200
2614
  };
4201
2615
 
4202
2616
  // src/adapters/auto-extract.ts
4203
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
4204
- import { join as join6, relative, sep } from "path";
2617
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2618
+ import { join as join5, relative, sep } from "path";
4205
2619
 
4206
2620
  // src/adapters/discover-components.ts
4207
- import { readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
4208
- import { join as join4, extname, basename } from "path";
2621
+ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
2622
+ import { join as join3, extname, basename } from "path";
4209
2623
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
4210
2624
  "node_modules",
4211
2625
  "dist",
@@ -4241,10 +2655,10 @@ function discoverComponentFiles(projectRoot) {
4241
2655
  "src/ui",
4242
2656
  "lib/ui",
4243
2657
  "packages"
4244
- ].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
2658
+ ].map((d) => join3(projectRoot, d)).filter((d) => existsSync3(d));
4245
2659
  if (scanDirs.length === 0) {
4246
- const srcDir = join4(projectRoot, "src");
4247
- if (existsSync4(srcDir)) scanDirs.push(srcDir);
2660
+ const srcDir = join3(projectRoot, "src");
2661
+ if (existsSync3(srcDir)) scanDirs.push(srcDir);
4248
2662
  }
4249
2663
  for (const dir of scanDirs) {
4250
2664
  walkDir(dir, results, seen);
@@ -4263,14 +2677,14 @@ function walkDir(dir, results, seen, depth = 0) {
4263
2677
  if (entry.name.startsWith(".")) continue;
4264
2678
  if (entry.isDirectory()) {
4265
2679
  if (EXCLUDED_DIRS.has(entry.name)) continue;
4266
- walkDir(join4(dir, entry.name), results, seen, depth + 1);
2680
+ walkDir(join3(dir, entry.name), results, seen, depth + 1);
4267
2681
  continue;
4268
2682
  }
4269
2683
  if (!entry.isFile()) continue;
4270
2684
  const ext = extname(entry.name);
4271
2685
  if (ext !== ".tsx" && ext !== ".jsx") continue;
4272
2686
  if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
4273
- const filePath = join4(dir, entry.name);
2687
+ const filePath = join3(dir, entry.name);
4274
2688
  if (seen.has(filePath)) continue;
4275
2689
  seen.add(filePath);
4276
2690
  const name = inferComponentName(entry.name, dir);
@@ -4288,8 +2702,8 @@ function inferComponentName(fileName, dirPath) {
4288
2702
  }
4289
2703
 
4290
2704
  // src/adapters/scan-tokens.ts
4291
- import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
4292
- import { join as join5, extname as extname2 } from "path";
2705
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
2706
+ import { join as join4, extname as extname2 } from "path";
4293
2707
  function scanTokens(projectRoot) {
4294
2708
  const cssFiles = discoverCssFiles(projectRoot);
4295
2709
  if (cssFiles.length === 0) return void 0;
@@ -4327,7 +2741,7 @@ function discoverCssFiles(projectRoot) {
4327
2741
  "styles",
4328
2742
  "css",
4329
2743
  "app"
4330
- ].map((d) => join5(projectRoot, d)).filter((d) => existsSync5(d));
2744
+ ].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
4331
2745
  searchDirs.push(projectRoot);
4332
2746
  for (const dir of searchDirs) {
4333
2747
  try {
@@ -4336,22 +2750,22 @@ function discoverCssFiles(projectRoot) {
4336
2750
  if (!entry.isFile()) continue;
4337
2751
  const ext = extname2(entry.name);
4338
2752
  if (ext === ".css" || ext === ".scss") {
4339
- files.push(join5(dir, entry.name));
2753
+ files.push(join4(dir, entry.name));
4340
2754
  }
4341
2755
  }
4342
2756
  } catch {
4343
2757
  continue;
4344
2758
  }
4345
2759
  }
4346
- const srcDir = join5(projectRoot, "src");
4347
- if (existsSync5(srcDir)) {
2760
+ const srcDir = join4(projectRoot, "src");
2761
+ if (existsSync4(srcDir)) {
4348
2762
  try {
4349
2763
  for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
4350
2764
  if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
4351
- const subDir = join5(srcDir, subEntry.name);
2765
+ const subDir = join4(srcDir, subEntry.name);
4352
2766
  for (const file of readdirSync3(subDir, { withFileTypes: true })) {
4353
2767
  if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
4354
- files.push(join5(subDir, file.name));
2768
+ files.push(join4(subDir, file.name));
4355
2769
  }
4356
2770
  }
4357
2771
  }
@@ -4583,7 +2997,7 @@ Check that your tsconfig.json includes the component directories.`
4583
2997
  var extractorModulePromise = null;
4584
2998
  async function loadExtractorModule() {
4585
2999
  if (!extractorModulePromise) {
4586
- extractorModulePromise = import("./dist-BDWAHJ4K.js");
3000
+ extractorModulePromise = import("./dist-LVC53MY5.js");
4587
3001
  }
4588
3002
  return extractorModulePromise;
4589
3003
  }
@@ -4849,15 +3263,15 @@ function inferCategory(relativePath) {
4849
3263
  function findTsConfig(projectRoot) {
4850
3264
  const candidates = ["tsconfig.json", "tsconfig.app.json"];
4851
3265
  for (const name of candidates) {
4852
- const p = join6(projectRoot, name);
4853
- if (existsSync6(p)) return p;
3266
+ const p = join5(projectRoot, name);
3267
+ if (existsSync5(p)) return p;
4854
3268
  }
4855
3269
  return null;
4856
3270
  }
4857
3271
  function readPackageName(projectRoot) {
4858
3272
  try {
4859
- const pkgPath = join6(projectRoot, "package.json");
4860
- if (!existsSync6(pkgPath)) return void 0;
3273
+ const pkgPath = join5(projectRoot, "package.json");
3274
+ if (!existsSync5(pkgPath)) return void 0;
4861
3275
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
4862
3276
  return pkg.name;
4863
3277
  } catch {
@@ -4880,7 +3294,36 @@ function chooseComponentSource(args) {
4880
3294
  }
4881
3295
  return args.catalogComponents;
4882
3296
  }
4883
- var TOKEN_CATEGORY_ALIASES2 = {
3297
+ function dominantString(values) {
3298
+ const counts = /* @__PURE__ */ new Map();
3299
+ for (const value of values) {
3300
+ const trimmed = typeof value === "string" ? value.trim() : "";
3301
+ if (!trimmed) continue;
3302
+ counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
3303
+ }
3304
+ const ranked = [...counts.entries()].sort(
3305
+ (a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
3306
+ );
3307
+ if (ranked.length === 0) return void 0;
3308
+ if (ranked[1] && ranked[1][1] === ranked[0][1]) return void 0;
3309
+ return ranked[0][0];
3310
+ }
3311
+ function inferDesignSystemMetadataFromComponents(components) {
3312
+ const packageName = dominantString(
3313
+ components.map((component) => component.packageName)
3314
+ );
3315
+ const packageComponents = packageName ? components.filter((component) => component.packageName === packageName) : components;
3316
+ const importPath = dominantString(
3317
+ packageComponents.map(
3318
+ (component) => component.importPath ?? component.packageName
3319
+ )
3320
+ );
3321
+ return {
3322
+ packageName: packageName ?? null,
3323
+ importPath: importPath ?? packageName ?? null
3324
+ };
3325
+ }
3326
+ var TOKEN_CATEGORY_ALIASES = {
4884
3327
  color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
4885
3328
  spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
4886
3329
  typography: ["typography", "font", "text", "copy", "line-height", "letter"],
@@ -4911,7 +3354,7 @@ function canonicalizeTokenCategory(token) {
4911
3354
  ].map(normalizeValue).filter(Boolean);
4912
3355
  for (const candidate of candidates) {
4913
3356
  for (const [canonical, aliases] of Object.entries(
4914
- TOKEN_CATEGORY_ALIASES2
3357
+ TOKEN_CATEGORY_ALIASES
4915
3358
  )) {
4916
3359
  if (candidate === canonical || aliases.some(
4917
3360
  (alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
@@ -4924,7 +3367,7 @@ function canonicalizeTokenCategory(token) {
4924
3367
  }
4925
3368
  function groupTokens(flat) {
4926
3369
  const categories = {};
4927
- const normalizedFlat = (flat ?? []).map((token) => {
3370
+ const normalizedFlat = (flat ?? []).filter((token) => !isGarbageToken(token)).map((token) => {
4928
3371
  const category = canonicalizeTokenCategory(token);
4929
3372
  const normalized = {
4930
3373
  name: token.name,
@@ -5055,6 +3498,7 @@ var CloudCatalogAdapter = class {
5055
3498
  constructor(options) {
5056
3499
  this.options = options;
5057
3500
  }
3501
+ options;
5058
3502
  name = "cloud";
5059
3503
  async load(_projectRoot) {
5060
3504
  const headers = {
@@ -5073,14 +3517,19 @@ var CloudCatalogAdapter = class {
5073
3517
  }
5074
3518
  const raw = await response.json();
5075
3519
  const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
5076
- const designSystem = mergeDesignSystemMetadata(
5077
- raw.designSystem,
5078
- validateFixRaw?.content?.designSystem
5079
- );
5080
3520
  const sourceComponents = chooseComponentSource({
5081
3521
  catalogComponents: raw.components ?? [],
5082
3522
  contextComponents: validateFixRaw?.content?.components ?? []
5083
3523
  });
3524
+ const inferredDesignSystem = inferDesignSystemMetadataFromComponents(sourceComponents);
3525
+ const designSystem = mergeDesignSystemMetadata(
3526
+ {
3527
+ ...raw.designSystem,
3528
+ packageName: raw.designSystem?.packageName ?? inferredDesignSystem.packageName,
3529
+ importPath: raw.designSystem?.importPath ?? inferredDesignSystem.importPath
3530
+ },
3531
+ validateFixRaw?.content?.designSystem
3532
+ );
5084
3533
  const components = Object.fromEntries(
5085
3534
  sourceComponents.map((component) => [
5086
3535
  component.componentKey,
@@ -5147,7 +3596,7 @@ var CloudCatalogAdapter = class {
5147
3596
  };
5148
3597
 
5149
3598
  // src/adapters/bundle.ts
5150
- import { existsSync as existsSync7 } from "fs";
3599
+ import { existsSync as existsSync6 } from "fs";
5151
3600
  import { readFile as readFile2 } from "fs/promises";
5152
3601
  import { dirname as dirname3, resolve as resolve2 } from "path";
5153
3602
  import {
@@ -5155,6 +3604,7 @@ import {
5155
3604
  bundleManifestSchema,
5156
3605
  bundleTokenFileSchema
5157
3606
  } from "@fragments-sdk/core";
3607
+ import { BRAND as BRAND3 } from "@fragments-sdk/core";
5158
3608
  async function readJsonFile(path, parser, label) {
5159
3609
  const content = await readFile2(path, "utf-8");
5160
3610
  try {
@@ -5273,7 +3723,7 @@ var BundleAdapter = class {
5273
3723
  const manifests = this.discover(projectRoot);
5274
3724
  if (manifests.length === 0) {
5275
3725
  throw new Error(
5276
- `No ${BRAND.dataDir}/${BRAND.manifestFile} found. Run \`${BRAND.cliCommand} context install --cloud\` or commit a Fragments bundle into your workspace.`
3726
+ `No ${BRAND3.dataDir}/${BRAND3.manifestFile} found. Run \`${BRAND3.cliCommand} context install --cloud\` or commit a Fragments bundle into your workspace.`
5277
3727
  );
5278
3728
  }
5279
3729
  const manifestPath = manifests[0];
@@ -5285,7 +3735,7 @@ var BundleAdapter = class {
5285
3735
  const bundleDir = dirname3(manifestPath);
5286
3736
  const repoRoot = dirname3(bundleDir);
5287
3737
  const tokensPath = resolve2(bundleDir, "tokens.json");
5288
- const tokensFile = existsSync7(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
3738
+ const tokensFile = existsSync6(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
5289
3739
  const components = Object.fromEntries(
5290
3740
  await Promise.all(
5291
3741
  Object.values(manifest.components).map(async (entry) => {
@@ -5331,7 +3781,7 @@ var BundleAdapter = class {
5331
3781
  const snapshot = validateSnapshot({
5332
3782
  schemaVersion: 1,
5333
3783
  sourceType: "bundle",
5334
- sourceLabel: `${BRAND.dataDir}/${BRAND.manifestFile}`,
3784
+ sourceLabel: `${BRAND3.dataDir}/${BRAND3.manifestFile}`,
5335
3785
  capabilities: buildCapabilities({
5336
3786
  components,
5337
3787
  tokens
@@ -5382,7 +3832,7 @@ function resolveCloudUrl2(fileConfig) {
5382
3832
  return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
5383
3833
  }
5384
3834
  function hasTsProject(projectRoot) {
5385
- return existsSync8(join7(projectRoot, "tsconfig.json")) || existsSync8(join7(projectRoot, "tsconfig.app.json"));
3835
+ return existsSync7(join6(projectRoot, "tsconfig.json")) || existsSync7(join6(projectRoot, "tsconfig.app.json"));
5386
3836
  }
5387
3837
  function resolveDataAdapter(config, fileConfig) {
5388
3838
  const source = config.source ?? fileConfig?.source ?? "auto";
@@ -5396,7 +3846,7 @@ function resolveDataAdapter(config, fileConfig) {
5396
3846
  case "cloud":
5397
3847
  if (!cloudApiKey) {
5398
3848
  throw new Error(
5399
- "Cloud source requires a Cloud API key. Set FRAGMENTS_API_KEY or pass cloudApiKey."
3849
+ "Cloud source requires a Cloud API key. Pass --cloud-api-key, set FRAGMENTS_API_KEY, or configure cloud.apiKey in ds-mcp.config.json."
5400
3850
  );
5401
3851
  }
5402
3852
  return {
@@ -5440,7 +3890,142 @@ function resolveDataAdapter(config, fileConfig) {
5440
3890
  }
5441
3891
  }
5442
3892
  function resolveSearchApiKey(config, fileConfig) {
5443
- return config.searchApiKey ?? config.apiKey ?? fileConfig?.vectorSearch?.apiKey;
3893
+ return config.searchApiKey ?? fileConfig?.vectorSearch?.apiKey;
3894
+ }
3895
+
3896
+ // src/spec-schema.ts
3897
+ var UI_SPEC_SCHEMA_URI = "fragments://schemas/govern.schema";
3898
+ var UI_SPEC_SCHEMA_NAME = "govern.schema";
3899
+ var UI_SPEC_SCHEMA_MIME_TYPE = "application/schema+json";
3900
+ var UI_SPEC_SCHEMA_RESOURCE = {
3901
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3902
+ $id: UI_SPEC_SCHEMA_URI,
3903
+ title: "Fragments UI Spec",
3904
+ description: "Input shape accepted by the Fragments govern and validate_and_fix MCP tools.",
3905
+ type: "object",
3906
+ required: ["nodes"],
3907
+ additionalProperties: false,
3908
+ properties: {
3909
+ root: {
3910
+ type: "string",
3911
+ description: "Optional node id to treat as the root. When omitted, top-level nodes are evaluated in order."
3912
+ },
3913
+ metadata: {
3914
+ type: "object",
3915
+ description: "Optional caller metadata. The validator stores and echoes only fields that downstream tools understand.",
3916
+ additionalProperties: true
3917
+ },
3918
+ nodes: {
3919
+ type: "array",
3920
+ description: "Top-level UI nodes. Each node names a component type and may carry props plus nested children.",
3921
+ items: { $ref: "#/$defs/node" }
3922
+ }
3923
+ },
3924
+ $defs: {
3925
+ node: {
3926
+ type: "object",
3927
+ required: ["type"],
3928
+ additionalProperties: false,
3929
+ properties: {
3930
+ id: {
3931
+ type: "string",
3932
+ description: "Stable caller-provided id. Used in violations and fixedSpec patches."
3933
+ },
3934
+ type: {
3935
+ type: "string",
3936
+ description: 'Design-system component name, including compound names such as "DatePicker.Trigger".'
3937
+ },
3938
+ props: {
3939
+ type: "object",
3940
+ description: 'Component props. Put simple text labels in props.children, e.g. { "children": "Save" }.',
3941
+ additionalProperties: true
3942
+ },
3943
+ children: {
3944
+ description: "Nested child nodes, or a string text child for leaf content. Prefer props.children for simple button labels.",
3945
+ oneOf: [
3946
+ { type: "string" },
3947
+ {
3948
+ type: "array",
3949
+ items: {
3950
+ oneOf: [{ type: "string" }, { $ref: "#/$defs/node" }]
3951
+ }
3952
+ }
3953
+ ]
3954
+ }
3955
+ }
3956
+ }
3957
+ },
3958
+ examples: {
3959
+ valid: [
3960
+ {
3961
+ nodes: [
3962
+ {
3963
+ id: "save",
3964
+ type: "Button",
3965
+ props: { variant: "primary", children: "Save" }
3966
+ }
3967
+ ]
3968
+ },
3969
+ {
3970
+ root: "card",
3971
+ metadata: { source: "agent-draft" },
3972
+ nodes: [
3973
+ {
3974
+ id: "card",
3975
+ type: "Card",
3976
+ props: {},
3977
+ children: [
3978
+ {
3979
+ id: "title",
3980
+ type: "Heading",
3981
+ props: { level: 2, children: "Billing" }
3982
+ },
3983
+ {
3984
+ id: "submit",
3985
+ type: "Button",
3986
+ props: { children: "Update plan" }
3987
+ }
3988
+ ]
3989
+ }
3990
+ ]
3991
+ }
3992
+ ],
3993
+ invalid: [
3994
+ {
3995
+ reason: "String event handlers are blocked by safety/no-string-handlers.",
3996
+ spec: {
3997
+ nodes: [
3998
+ {
3999
+ id: "danger",
4000
+ type: "Button",
4001
+ props: { children: "Save", onClick: 'alert("saved")' }
4002
+ }
4003
+ ]
4004
+ }
4005
+ },
4006
+ {
4007
+ reason: "Raw CSS values should use design tokens; call tokens.suggest before writing them.",
4008
+ spec: {
4009
+ nodes: [
4010
+ {
4011
+ id: "panel",
4012
+ type: "Box",
4013
+ props: { style: { padding: "6px" } }
4014
+ }
4015
+ ]
4016
+ }
4017
+ }
4018
+ ]
4019
+ },
4020
+ notes: [
4021
+ "Use component names from the active catalog; unknown components fail components/allow.",
4022
+ "Use props.children for simple text labels, especially Button text.",
4023
+ "Do not put JavaScript source strings in event handler props.",
4024
+ "Run validate_and_fix after govern when the verdict asks for revision or deterministic repair."
4025
+ ]
4026
+ };
4027
+ function serializeUiSpecSchema() {
4028
+ return JSON.stringify(UI_SPEC_SCHEMA_RESOURCE, null, 2);
5444
4029
  }
5445
4030
 
5446
4031
  // src/server.ts
@@ -5452,12 +4037,13 @@ var TOOL_DEFINITION_BY_KEY = new Map(
5452
4037
  function createMcpServer(config) {
5453
4038
  const server = new Server(
5454
4039
  {
5455
- name: `${BRAND.nameLower}-mcp`,
4040
+ name: `${BRAND4.nameLower}-mcp`,
5456
4041
  version: MCP_SERVER_VERSION
5457
4042
  },
5458
4043
  {
5459
4044
  capabilities: {
5460
4045
  tools: { listChanged: true },
4046
+ resources: { listChanged: false },
5461
4047
  logging: {}
5462
4048
  }
5463
4049
  }
@@ -5488,21 +4074,20 @@ function createMcpServer(config) {
5488
4074
  let loadDataPromise = null;
5489
4075
  let resolvedRoot = null;
5490
4076
  let resolveProjectRootPromise = null;
5491
- let componentIndex = null;
5492
- let blockIndex = null;
5493
- let tokenIndex = null;
5494
4077
  async function resolveProjectRoot() {
5495
4078
  if (resolvedRoot) return resolvedRoot;
5496
4079
  if (resolveProjectRootPromise) return resolveProjectRootPromise;
5497
4080
  resolveProjectRootPromise = (async () => {
5498
- try {
5499
- const result2 = await server.listRoots();
5500
- if (result2.roots?.length > 0) {
5501
- const rootUri = result2.roots[0].uri;
5502
- resolvedRoot = fileURLToPath(rootUri);
5503
- return resolvedRoot;
4081
+ if (server.getClientCapabilities()?.roots) {
4082
+ try {
4083
+ const result2 = await server.listRoots();
4084
+ if (result2.roots?.length > 0) {
4085
+ const rootUri = result2.roots[0].uri;
4086
+ resolvedRoot = fileURLToPath(rootUri);
4087
+ return resolvedRoot;
4088
+ }
4089
+ } catch {
5504
4090
  }
5505
- } catch {
5506
4091
  }
5507
4092
  resolvedRoot = config.projectRoot;
5508
4093
  return resolvedRoot;
@@ -5519,11 +4104,6 @@ function createMcpServer(config) {
5519
4104
  loadDataPromise = (async () => {
5520
4105
  const projectRoot = await resolveProjectRoot();
5521
4106
  const loaded = await adapter.load(projectRoot);
5522
- const allFragments = Object.values(loaded.components);
5523
- const allBlocks = Object.values(loaded.blocks ?? {});
5524
- componentIndex = buildComponentIndex(allFragments);
5525
- blockIndex = allBlocks.length > 0 ? buildBlockIndex(allBlocks) : null;
5526
- tokenIndex = loaded.tokens && loaded.tokens.total > 0 ? buildTokenIndex(loaded.tokens) : null;
5527
4107
  cachedData = loaded;
5528
4108
  return loaded;
5529
4109
  })();
@@ -5538,21 +4118,49 @@ function createMcpServer(config) {
5538
4118
  return {
5539
4119
  tools: registry.listTools(
5540
4120
  {
5541
- hasViewer: !!config.viewerUrl,
5542
- hasPlayground: !!(config.playgroundUrl ?? fileConfig?.playgroundUrl),
4121
+ hasViewer: false,
4122
+ hasPlayground: false,
5543
4123
  capabilities: data.capabilities
5544
4124
  },
5545
4125
  TOOLS
5546
4126
  )
5547
4127
  };
5548
4128
  });
4129
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
4130
+ resources: [
4131
+ {
4132
+ uri: UI_SPEC_SCHEMA_URI,
4133
+ name: UI_SPEC_SCHEMA_NAME,
4134
+ title: "Fragments govern UI spec schema",
4135
+ description: "JSON schema and examples for the spec argument accepted by govern and validate_and_fix.",
4136
+ mimeType: UI_SPEC_SCHEMA_MIME_TYPE
4137
+ }
4138
+ ]
4139
+ }));
4140
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4141
+ if (request.params.uri !== UI_SPEC_SCHEMA_URI) {
4142
+ throw new McpError(
4143
+ ErrorCode.InvalidParams,
4144
+ `Unknown resource URI: ${request.params.uri}`
4145
+ );
4146
+ }
4147
+ return {
4148
+ contents: [
4149
+ {
4150
+ uri: UI_SPEC_SCHEMA_URI,
4151
+ mimeType: UI_SPEC_SCHEMA_MIME_TYPE,
4152
+ text: serializeUiSpecSchema()
4153
+ }
4154
+ ]
4155
+ };
4156
+ });
5549
4157
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
5550
4158
  const { name, arguments: args } = request.params;
5551
4159
  const data = await loadData();
5552
4160
  const toolContext = {
5553
4161
  data,
5554
4162
  config: mergedConfig,
5555
- indexes: { componentIndex, blockIndex, tokenIndex },
4163
+ indexes: { componentIndex: null, blockIndex: null, tokenIndex: null },
5556
4164
  mcp: {
5557
4165
  server,
5558
4166
  clientCapabilities: server.getClientCapabilities()
@@ -5567,8 +4175,8 @@ function createMcpServer(config) {
5567
4175
  return "your-component-library";
5568
4176
  }
5569
4177
  const root = resolvedRoot ?? config.projectRoot;
5570
- const packageJsonPath = join8(root, "package.json");
5571
- if (existsSync9(packageJsonPath)) {
4178
+ const packageJsonPath = join7(root, "package.json");
4179
+ if (existsSync8(packageJsonPath)) {
5572
4180
  try {
5573
4181
  const content = readFileSync6(packageJsonPath, "utf-8");
5574
4182
  const pkg = JSON.parse(content);
@@ -5632,11 +4240,6 @@ function createSandboxServer() {
5632
4240
 
5633
4241
  export {
5634
4242
  loadConfigFile,
5635
- SYNONYM_MAP,
5636
- USE_CASE_TOKEN_CATEGORIES,
5637
- MINIMUM_SCORE_THRESHOLD,
5638
- BLOCK_BOOST_PER_OCCURRENCE,
5639
- DEFAULT_ENDPOINTS,
5640
4243
  CORE_TOOLS,
5641
4244
  VIEWER_TOOLS,
5642
4245
  INFRA_TOOLS,
@@ -5657,4 +4260,4 @@ export {
5657
4260
  startMcpServer,
5658
4261
  createSandboxServer
5659
4262
  };
5660
- //# sourceMappingURL=chunk-YSNIGHNU.js.map
4263
+ //# sourceMappingURL=chunk-KGFM5SCE.js.map