@fragments-sdk/mcp 0.9.0 → 0.10.0

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,27 +1,21 @@
1
1
  import {
2
- BRAND,
3
- DEFAULTS
2
+ BRAND
4
3
  } 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
4
 
15
5
  // src/server.ts
16
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
8
  import {
19
9
  CallToolRequestSchema,
20
- ListToolsRequestSchema
10
+ ErrorCode,
11
+ ListResourcesRequestSchema,
12
+ ListToolsRequestSchema,
13
+ McpError,
14
+ ReadResourceRequestSchema
21
15
  } from "@modelcontextprotocol/sdk/types.js";
22
- import { existsSync as existsSync9 } from "fs";
16
+ import { existsSync as existsSync8 } from "fs";
23
17
  import { readFileSync as readFileSync6 } from "fs";
24
- import { join as join8 } from "path";
18
+ import { join as join7 } from "path";
25
19
  import { fileURLToPath } from "url";
26
20
 
27
21
  // src/config.ts
@@ -30,2362 +24,289 @@ import { join } from "path";
30
24
  function loadConfigFile(projectRoot) {
31
25
  const configPath = join(projectRoot, "ds-mcp.config.json");
32
26
  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.`);
27
+ try {
28
+ const content = readFileSync(configPath, "utf-8");
29
+ return JSON.parse(content);
30
+ } catch (e) {
31
+ throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
1865
32
  }
1866
33
  }
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) {
34
+ const pkgPath = join(projectRoot, "package.json");
35
+ if (existsSync(pkgPath)) {
1878
36
  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
- };
37
+ const content = readFileSync(pkgPath, "utf-8");
38
+ const pkg = JSON.parse(content);
39
+ if (pkg.dsMcp) return pkg.dsMcp;
40
+ } catch {
1929
41
  }
1930
42
  }
43
+ return null;
44
+ }
45
+
46
+ // src/server.ts
47
+ import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
48
+
49
+ // src/version.ts
50
+ import { readFileSync as readFileSync2 } from "fs";
51
+ function readPackageVersion() {
1931
52
  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
- };
53
+ const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
54
+ const pkg = JSON.parse(raw);
55
+ return pkg.version ?? "0.0.0";
56
+ } catch {
57
+ return "0.0.0";
1965
58
  }
1966
- };
59
+ }
60
+ var MCP_SERVER_VERSION = readPackageVersion();
1967
61
 
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
- };
62
+ // src/catalog-meta.ts
63
+ function getCatalogMeta(data) {
64
+ const rawUpdatedAt = data.validateFixContext?.updatedAt ?? data.snapshot.metadata.updatedAt;
65
+ const updatedAt = typeof rawUpdatedAt === "number" ? new Date(rawUpdatedAt).toISOString() : rawUpdatedAt;
66
+ return {
67
+ catalogRevision: data.validateFixContext?.catalogRevision ?? data.snapshot.metadata.revision,
68
+ updatedAt
69
+ };
70
+ }
71
+
72
+ // src/token-suggestions.ts
73
+ function propertyFamilyFor(property, value) {
74
+ const prop = property.toLowerCase().trim();
75
+ const normalizedValue = value?.toLowerCase().trim();
76
+ if (prop.includes("shadow")) return "shadow";
77
+ if (prop.includes("z-index")) return "z-index";
78
+ if (prop.includes("transition") || prop.includes("duration") || prop.includes("animation")) {
79
+ return "duration";
80
+ }
81
+ if (prop.includes("font") || prop.includes("line-height") || prop === "letter-spacing") {
82
+ return "typography";
1993
83
  }
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
- };
84
+ if (prop.includes("radius")) return "radius";
85
+ if (prop.endsWith("border-width") || prop === "border-width" || prop === "outline-width" || prop === "stroke-width") {
86
+ return "border-width";
2031
87
  }
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
- }
88
+ 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)) {
89
+ return "color";
2052
90
  }
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
- };
91
+ 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")) {
92
+ return "spacing";
2062
93
  }
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
- }
94
+ return "other";
95
+ }
96
+ function suggestToken(input) {
97
+ const family = propertyFamilyFor(input.property, input.value);
98
+ const limit = Math.min(Math.max(input.limit ?? 5, 1), 10);
99
+ const candidates = input.tokens ? scoreCandidates(input.tokens, family, input) : [];
100
+ const top = candidates.slice(0, limit).map(
101
+ (candidate) => presentCandidate(candidate, input.tokens)
102
+ );
103
+ const meta = {
104
+ propertyFamily: family,
105
+ catalogRevision: input.catalogRevision,
106
+ updatedAt: input.updatedAt,
107
+ candidateCount: candidates.length
108
+ };
109
+ if (family === "other") {
2087
110
  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
- }]
111
+ alternatives: [],
112
+ noSuggestion: true,
113
+ noSuggestionReason: `No token family is known for CSS property "${input.property}".`,
114
+ _meta: meta
2103
115
  };
2104
- } catch (error) {
116
+ }
117
+ if (!input.tokens || input.tokens.total === 0) {
2105
118
  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
119
+ alternatives: [],
120
+ noSuggestion: true,
121
+ noSuggestionReason: "No design tokens are available in the active catalog.",
122
+ _meta: meta
2111
123
  };
2112
124
  }
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) {
125
+ if (top.length === 0) {
2122
126
  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
127
+ alternatives: [],
128
+ noSuggestion: true,
129
+ noSuggestionReason: `No ${family} tokens are available for CSS property "${input.property}".`,
130
+ _meta: meta
2128
131
  };
2129
132
  }
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 };
133
+ const [recommended, ...alternatives] = top;
134
+ return {
135
+ recommended,
136
+ alternatives,
137
+ _meta: meta
138
+ };
139
+ }
140
+ function scoreCandidates(tokenData, family, input) {
141
+ if (family === "other") return [];
142
+ const value = normalizeComparableValue(input.value);
143
+ const flatTokens = tokenData.flat.length > 0 ? tokenData.flat : Object.values(tokenData.categories).flat();
144
+ const familyTokens = flatTokens.filter((token) => !isGarbageToken(token)).map((token) => ({ token, family: tokenFamily(token) })).filter((entry) => entry.family === family);
145
+ const scored = familyTokens.map(({ token }) => {
146
+ const tokenValue = normalizeComparableValue(token.value);
147
+ let score = 50;
148
+ let reason = "family-match";
149
+ if (value && tokenValue && value === tokenValue) {
150
+ score += 60;
151
+ reason = "exact-value-match";
152
+ } else if (value && tokenValue && family !== "color") {
153
+ const distanceScore = lengthDistanceScore(value, tokenValue);
154
+ if (distanceScore > 0) {
155
+ score += distanceScore;
156
+ reason = "nearest-neighbor";
2263
157
  }
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
158
  }
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
- }
159
+ score += nameRelevanceScore(token, input.property, input.context);
160
+ return { token, family, score, reason };
161
+ });
162
+ return scored.filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.token.name.localeCompare(b.token.name));
2296
163
  }
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
- }
164
+ function presentCandidate(candidate, tokenData) {
165
+ const cssVar = cssVarForToken(candidate.token);
166
+ const resolvedValue = resolvedValueForToken(candidate.token);
167
+ const confidence = candidate.score >= 105 ? "high" : candidate.score >= 65 ? "medium" : "low";
2321
168
  return {
2322
- content: [{ type: "text", text: result2.text }]
169
+ name: dottedNameForToken(candidate.token, tokenData),
170
+ ...cssVar && { cssVar },
171
+ ...cssVar && {
172
+ cssValue: resolvedValue ? `var(${cssVar}, ${resolvedValue})` : `var(${cssVar})`
173
+ },
174
+ ...resolvedValue && { resolvedValue },
175
+ confidence,
176
+ reason: candidate.reason
2323
177
  };
2324
- };
178
+ }
179
+ function isGarbageToken(token) {
180
+ const value = token.value?.trim();
181
+ if (!value) return false;
182
+ if (value.includes("#{") || value.includes("$")) return true;
183
+ if (/^\$[\w-]+/.test(token.name)) return true;
184
+ return false;
185
+ }
186
+ function tokenFamily(token) {
187
+ const haystack = [
188
+ token.type,
189
+ token.category,
190
+ ...token.path ?? [],
191
+ token.name
192
+ ].filter(Boolean).join(" ").toLowerCase();
193
+ if (/\b(color|colour|background|foreground|surface|palette)\b/.test(haystack)) {
194
+ return "color";
195
+ }
196
+ if (/\b(radius|radii|rounded|corner)\b/.test(haystack)) return "radius";
197
+ if (/\b(border-width|border width|stroke-width|stroke width)\b/.test(haystack)) {
198
+ return "border-width";
199
+ }
200
+ if (/\b(shadow|elevation)\b/.test(haystack)) return "shadow";
201
+ if (/\b(font|type|typography|line-height|letter-spacing)\b/.test(haystack)) {
202
+ return "typography";
203
+ }
204
+ if (/\b(duration|transition|animation)\b/.test(haystack)) return "duration";
205
+ if (/\b(z-index|zindex)\b/.test(haystack)) return "z-index";
206
+ if (/\b(space|spacing|size|sizing|width|height|gap|padding|margin|inset)\b/.test(haystack)) {
207
+ return "spacing";
208
+ }
209
+ if (/\bborder\b/.test(haystack)) return "border-width";
210
+ return "other";
211
+ }
212
+ function cssVarForToken(token) {
213
+ if (token.name.startsWith("--")) return token.name;
214
+ const match = token.value?.match(/var\((--[\w-]+)/);
215
+ return match?.[1];
216
+ }
217
+ function dottedNameForToken(token, tokenData) {
218
+ if (!token.name.startsWith("--")) return token.name;
219
+ let name = token.name.slice(2);
220
+ const prefix = tokenData?.prefix?.replace(/^--/, "").replace(/-$/, "");
221
+ if (prefix && name.startsWith(`${prefix}-`)) {
222
+ name = name.slice(prefix.length + 1);
223
+ }
224
+ return name.replace(/-/g, ".");
225
+ }
226
+ function resolvedValueForToken(token) {
227
+ const value = token.value?.trim();
228
+ if (!value) return void 0;
229
+ const fallback = value.match(/var\(--[\w-]+,\s*([^)]+)\)/)?.[1]?.trim();
230
+ if (fallback) return fallback;
231
+ if (value.startsWith("var(")) return void 0;
232
+ return value;
233
+ }
234
+ function normalizeComparableValue(value) {
235
+ if (!value) return void 0;
236
+ const trimmed = value.trim().toLowerCase();
237
+ const color = normalizeColor(trimmed);
238
+ if (color) return color;
239
+ const length = parseLength(trimmed);
240
+ if (length) return `${length.value}${length.unit}`;
241
+ return trimmed.replace(/\s+/g, " ");
242
+ }
243
+ function looksLikeColor(value) {
244
+ return /^#([\da-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
245
+ }
246
+ function normalizeColor(value) {
247
+ const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i);
248
+ if (!hex) return void 0;
249
+ const body = hex[1].toLowerCase();
250
+ if (body.length === 3) {
251
+ return `#${body[0]}${body[0]}${body[1]}${body[1]}${body[2]}${body[2]}`;
252
+ }
253
+ return `#${body}`;
254
+ }
255
+ function parseLength(value) {
256
+ const match = value.match(/^(-?\d+(?:\.\d+)?)(px|rem|em|%)$/);
257
+ if (!match) return void 0;
258
+ return { value: Number(match[1]), unit: match[2] };
259
+ }
260
+ function lengthDistanceScore(inputValue, tokenValue) {
261
+ const input = parseLength(inputValue);
262
+ const token = parseLength(tokenValue);
263
+ if (!input || !token || input.unit !== token.unit) return 0;
264
+ const distance = Math.abs(input.value - token.value);
265
+ if (distance === 0) return 60;
266
+ if (distance <= 2) return 35;
267
+ if (distance <= 4) return 20;
268
+ if (distance <= 8) return 10;
269
+ return 0;
270
+ }
271
+ function nameRelevanceScore(token, property, context) {
272
+ const haystack = [token.name, token.category, ...token.path ?? []].join(" ").toLowerCase();
273
+ const prop = property.toLowerCase();
274
+ let score = 0;
275
+ for (const part of prop.split(/[^a-z0-9]+/).filter((part2) => part2.length > 2)) {
276
+ if (haystack.includes(part)) score += 4;
277
+ }
278
+ if (context === "component" && haystack.includes("component")) score += 3;
279
+ if (context === "global" && haystack.includes("global")) score += 3;
280
+ return score;
281
+ }
2325
282
 
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) {
283
+ // src/tools/tokens-suggest.ts
284
+ var tokensSuggestHandler = async (args, ctx) => {
285
+ const property = args.property;
286
+ if (!property || typeof property !== "string") {
2336
287
  return {
2337
288
  content: [
2338
289
  {
2339
290
  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
- })
291
+ text: JSON.stringify({ error: "property is required." })
2345
292
  }
2346
- ]
293
+ ],
294
+ isError: true
2347
295
  };
2348
296
  }
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
- }
297
+ const context = args.context;
298
+ const catalogMeta = getCatalogMeta(ctx.data);
299
+ const result2 = suggestToken({
300
+ tokens: ctx.data.tokens,
301
+ property,
302
+ value: typeof args.value === "string" ? args.value : void 0,
303
+ context: context === "component" || context === "block" || context === "global" ? context : void 0,
304
+ catalogRevision: catalogMeta.catalogRevision,
305
+ updatedAt: catalogMeta.updatedAt
306
+ });
2378
307
  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
- ]
308
+ content: [{ type: "text", text: JSON.stringify(result2) }],
309
+ _meta: result2._meta
2389
310
  };
2390
311
  };
2391
312
 
@@ -2448,12 +369,25 @@ function result(validator, violations) {
2448
369
  violations
2449
370
  };
2450
371
  }
372
+ var SEVERITY_SCORE_CAPS = {
373
+ critical: 25,
374
+ serious: 50,
375
+ moderate: 80,
376
+ minor: 95
377
+ };
378
+ function verdictFor(violations) {
379
+ if (violations.length === 0) return "pass";
380
+ const worst = worstSeverity(violations);
381
+ return worst === "critical" || worst === "serious" ? "fail" : "warn";
382
+ }
2451
383
  function computeScore(violations) {
384
+ if (violations.length === 0) return 100;
2452
385
  const penalty = violations.reduce(
2453
386
  (sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
2454
387
  0
2455
388
  );
2456
- return Math.max(0, 100 - penalty);
389
+ const cap = SEVERITY_SCORE_CAPS[worstSeverity(violations)];
390
+ return Math.min(cap, Math.max(0, 100 - penalty));
2457
391
  }
2458
392
  function validateComponents(nodes, options) {
2459
393
  const rules = options.policy?.rules ?? {};
@@ -2524,7 +458,76 @@ function validateSafety(nodes) {
2524
458
  }
2525
459
  }
2526
460
  }
2527
- return result("safety", violations);
461
+ return result("safety", violations);
462
+ }
463
+ function editDistance(a, b) {
464
+ const rows = a.length + 1;
465
+ const cols = b.length + 1;
466
+ const distances = Array.from({ length: rows }, () => Array(cols).fill(0));
467
+ for (let i = 0; i < rows; i++) distances[i][0] = i;
468
+ for (let j = 0; j < cols; j++) distances[0][j] = j;
469
+ for (let i = 1; i < rows; i++) {
470
+ for (let j = 1; j < cols; j++) {
471
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
472
+ distances[i][j] = Math.min(
473
+ distances[i - 1][j] + 1,
474
+ distances[i][j - 1] + 1,
475
+ distances[i - 1][j - 1] + cost
476
+ );
477
+ }
478
+ }
479
+ return distances[a.length][b.length];
480
+ }
481
+ function normalizePropValue(value) {
482
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
483
+ }
484
+ function closestAllowedValue(rawValue, allowedValues) {
485
+ const normalizedRaw = normalizePropValue(rawValue);
486
+ const ranked = allowedValues.map((value) => {
487
+ const normalizedValue = normalizePropValue(value);
488
+ const distance = editDistance(normalizedRaw, normalizedValue);
489
+ const prefixMatch = normalizedRaw.length >= 4 && (normalizedValue.startsWith(normalizedRaw) || normalizedRaw.startsWith(normalizedValue));
490
+ return { value, distance, prefixMatch };
491
+ }).sort((a, b) => {
492
+ if (a.prefixMatch !== b.prefixMatch) return a.prefixMatch ? -1 : 1;
493
+ return a.distance - b.distance || a.value.localeCompare(b.value);
494
+ });
495
+ const best = ranked[0];
496
+ if (!best) return void 0;
497
+ if (best.prefixMatch || best.distance <= 2) return best.value;
498
+ return void 0;
499
+ }
500
+ function validateProps(nodes, options) {
501
+ const rules = options.policy?.rules ?? {};
502
+ const propRule = rules["props/valid-values"];
503
+ const componentProps = options.componentProps ?? {};
504
+ const violations = [];
505
+ if (!isRuleEnabled(propRule)) {
506
+ return result("props", violations);
507
+ }
508
+ for (const [index, node] of nodes.entries()) {
509
+ const type = nodeType(node);
510
+ const propSchema = componentProps[type] ?? componentProps[parentType(type)];
511
+ if (!propSchema) continue;
512
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
513
+ const allowedValues = propSchema[prop]?.values?.filter(Boolean) ?? [];
514
+ if (allowedValues.length === 0) continue;
515
+ const rawValue = typeof value === "string" ? value : void 0;
516
+ if (!rawValue || allowedValues.includes(rawValue)) continue;
517
+ const closest = closestAllowedValue(rawValue, allowedValues);
518
+ violations.push({
519
+ nodeId: nodeId(node, index),
520
+ nodeType: type,
521
+ rule: "props/invalid-value",
522
+ severity: ruleSeverity(propRule, "moderate"),
523
+ message: `Prop "${prop}" on ${type} has invalid value "${rawValue}"`,
524
+ suggestion: closest ? `Use "${closest}" for prop "${prop}" on ${type}.` : `Use one of: ${allowedValues.join(", ")}`,
525
+ prop,
526
+ rawValue
527
+ });
528
+ }
529
+ }
530
+ return result("props", violations);
2528
531
  }
2529
532
  function hasHardcodedCssValue(value) {
2530
533
  if (value.includes("var(")) return false;
@@ -2576,14 +579,29 @@ function validateTokens(nodes, options) {
2576
579
  }
2577
580
  return result("tokens", violations);
2578
581
  }
582
+ function textFromUnknown(value) {
583
+ if (typeof value === "string") return value.trim();
584
+ if (Array.isArray(value)) return textFromChildren(value);
585
+ if (typeof value === "object" && value !== null) {
586
+ return textFromNode(value);
587
+ }
588
+ return "";
589
+ }
590
+ function textFromNode(node) {
591
+ const type = nodeType(node).toLowerCase();
592
+ const props = node.props ?? {};
593
+ const propText = [
594
+ textFromUnknown(props.children),
595
+ type === "text" ? textFromUnknown(props.value) : ""
596
+ ].filter(Boolean);
597
+ const childText = textFromChildren(node.children);
598
+ return [...propText, childText].join(" ").trim();
599
+ }
2579
600
  function textFromChildren(children) {
601
+ if (typeof children === "string") return children.trim();
2580
602
  if (!Array.isArray(children)) return "";
2581
603
  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 "";
604
+ return textFromUnknown(child);
2587
605
  }).join(" ").trim();
2588
606
  }
2589
607
  function validateA11y(nodes) {
@@ -2593,7 +611,7 @@ function validateA11y(nodes) {
2593
611
  if (!/button/i.test(type)) continue;
2594
612
  const props = node.props ?? {};
2595
613
  const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
2596
- const childText = textFromChildren(node.children);
614
+ const childText = textFromNode(node);
2597
615
  if (typeof label === "string" && label.trim().length > 0) continue;
2598
616
  if (childText.length > 0) continue;
2599
617
  violations.push({
@@ -2613,6 +631,7 @@ function runSpecGovern(spec, options) {
2613
631
  const results = [
2614
632
  validateSafety(nodes),
2615
633
  validateComponents(nodes, options),
634
+ validateProps(nodes, options),
2616
635
  validateTokens(nodes, options),
2617
636
  validateA11y(nodes)
2618
637
  ];
@@ -2621,6 +640,7 @@ function runSpecGovern(spec, options) {
2621
640
  new Set(nodes.map((node) => nodeType(node)))
2622
641
  );
2623
642
  return {
643
+ verdict: verdictFor(violations),
2624
644
  passed: results.every((entry) => entry.passed),
2625
645
  score: computeScore(violations),
2626
646
  results,
@@ -2638,7 +658,7 @@ function formatVerdict(verdict, format = "summary") {
2638
658
  }
2639
659
  const lines = [];
2640
660
  const icon = verdict.passed ? "ok" : "fail";
2641
- lines.push(`${icon} Governance check: score ${verdict.score}/100`);
661
+ lines.push(`${icon} Governance check: verdict ${verdict.verdict}, score ${verdict.score}/100`);
2642
662
  lines.push("");
2643
663
  for (const entry of verdict.results) {
2644
664
  const resultIcon = entry.passed ? "ok" : "fail";
@@ -2657,6 +677,14 @@ function formatVerdict(verdict, format = "summary") {
2657
677
  }
2658
678
 
2659
679
  // src/tools/govern.ts
680
+ function buildComponentProps(ctx) {
681
+ return Object.fromEntries(
682
+ Object.values(ctx.data.components).map((component) => [
683
+ component.name,
684
+ component.props
685
+ ])
686
+ );
687
+ }
2660
688
  var governHandler = async (args, ctx) => {
2661
689
  const spec = args?.spec;
2662
690
  if (!spec || typeof spec !== "object") {
@@ -2665,7 +693,7 @@ var governHandler = async (args, ctx) => {
2665
693
  {
2666
694
  type: "text",
2667
695
  text: JSON.stringify({
2668
- error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
696
+ 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
697
  })
2670
698
  }
2671
699
  ],
@@ -2681,12 +709,15 @@ var governHandler = async (args, ctx) => {
2681
709
  const verdict = runSpecGovern(spec, {
2682
710
  allowedComponents,
2683
711
  tokenPrefix: ctx.data.tokens?.prefix,
2684
- policy: policyOverrides
712
+ policy: policyOverrides,
713
+ componentProps: buildComponentProps(ctx)
2685
714
  });
2686
715
  const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
2687
716
  return {
2688
717
  content: [{ type: "text", text }],
2689
718
  _meta: {
719
+ ...getCatalogMeta(ctx.data),
720
+ verdict: verdict.verdict,
2690
721
  score: verdict.score,
2691
722
  passed: verdict.passed,
2692
723
  violationCount: verdict.results.reduce(
@@ -2703,7 +734,7 @@ var governHandler = async (args, ctx) => {
2703
734
  {
2704
735
  type: "text",
2705
736
  text: JSON.stringify({
2706
- error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id: string, type: string, props: object, children?: string[] }] }` : message
737
+ 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
738
  })
2708
739
  }
2709
740
  ],
@@ -2752,8 +783,11 @@ function validateNodeShape(node, path) {
2752
783
  return `${path}.props must be an object`;
2753
784
  }
2754
785
  if ("children" in node && node.children !== void 0) {
786
+ if (typeof node.children === "string") {
787
+ return null;
788
+ }
2755
789
  if (!Array.isArray(node.children)) {
2756
- return `${path}.children must be an array`;
790
+ return `${path}.children must be an array or string`;
2757
791
  }
2758
792
  for (const [index, child] of node.children.entries()) {
2759
793
  if (!isPlainObject(child)) continue;
@@ -2898,6 +932,59 @@ function getNextAction(status, replacements, unresolvedAmbiguityCount) {
2898
932
  if (replacements.length > 0) return "review_partial_fix";
2899
933
  return "revise_input";
2900
934
  }
935
+ function authorizationForAmbiguity(args) {
936
+ const required = [];
937
+ if (!args.applyFixes) {
938
+ required.push("applyFixes");
939
+ }
940
+ if (!args.allowElicitation) {
941
+ required.push("allowElicitation");
942
+ } else if (!args.supportsElicitation) {
943
+ required.push("clientCapabilities.elicitation.form");
944
+ }
945
+ if (!args.allowSampling) {
946
+ required.push("allowSampling");
947
+ } else if (!args.supportsSampling) {
948
+ required.push("clientCapabilities.sampling");
949
+ }
950
+ return required;
951
+ }
952
+ function buildWouldFixIfAuthorized(args) {
953
+ const entries = [];
954
+ if (!args.applyFixes) {
955
+ entries.push(
956
+ ...args.deterministicPreview.map((replacement) => ({
957
+ action: "replace_component",
958
+ nodeId: replacement.nodeId,
959
+ from: replacement.from,
960
+ to: replacement.to,
961
+ reason: replacement.reason,
962
+ requiredAuthorization: ["applyFixes"]
963
+ }))
964
+ );
965
+ }
966
+ const requiredAuthorization = authorizationForAmbiguity({
967
+ applyFixes: args.applyFixes,
968
+ allowElicitation: args.allowElicitation,
969
+ allowSampling: args.allowSampling,
970
+ supportsElicitation: args.supportsElicitation,
971
+ supportsSampling: args.supportsSampling
972
+ });
973
+ if (requiredAuthorization.length > 0) {
974
+ entries.push(
975
+ ...args.unresolvedAmbiguities.map((ambiguity) => ({
976
+ action: "resolve_ambiguous_component",
977
+ nodeId: ambiguity.nodeId,
978
+ from: ambiguity.from,
979
+ candidates: ambiguity.candidates,
980
+ rankedCandidates: ambiguity.rankedCandidates,
981
+ reason: ambiguity.reason,
982
+ requiredAuthorization
983
+ }))
984
+ );
985
+ }
986
+ return entries;
987
+ }
2901
988
  async function emitValidateAndFixTelemetry(args) {
2902
989
  if (!args.ctx.mcp?.server) return;
2903
990
  try {
@@ -2919,11 +1006,20 @@ async function emitValidateAndFixTelemetry(args) {
2919
1006
  function buildAllowedComponents(ctx) {
2920
1007
  return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
2921
1008
  }
1009
+ function buildComponentProps2(ctx) {
1010
+ return Object.fromEntries(
1011
+ Object.values(ctx.data.components).map((component) => [
1012
+ component.name,
1013
+ component.props
1014
+ ])
1015
+ );
1016
+ }
2922
1017
  function runGovern(spec, ctx, policyOverrides) {
2923
1018
  return runSpecGovern(spec, {
2924
1019
  allowedComponents: buildAllowedComponents(ctx),
2925
1020
  tokenPrefix: ctx.data.tokens?.prefix,
2926
- policy: policyOverrides
1021
+ policy: policyOverrides,
1022
+ componentProps: buildComponentProps2(ctx)
2927
1023
  });
2928
1024
  }
2929
1025
  function walkNodes2(nodes, visitor, path = "nodes") {
@@ -3237,11 +1333,14 @@ var validateAndFixHandler = async (args, ctx) => {
3237
1333
  let replacements = [];
3238
1334
  let ambiguities = [];
3239
1335
  const resolutionPath = [];
1336
+ const supportsElicitation = supportsFormElicitation(ctx);
1337
+ const supportsModelSampling = supportsSampling(ctx);
1338
+ const preview = !originalVerdict.passed ? applyDeterministicReplacements(spec, ctx) : void 0;
1339
+ if (!applyFixes && preview) {
1340
+ ambiguities = preview.ambiguities;
1341
+ }
3240
1342
  if (!originalVerdict.passed && applyFixes) {
3241
- const result2 = applyDeterministicReplacements(
3242
- spec,
3243
- ctx
3244
- );
1343
+ const result2 = preview ?? applyDeterministicReplacements(spec, ctx);
3245
1344
  replacements = result2.replacements;
3246
1345
  ambiguities = result2.ambiguities;
3247
1346
  if (replacements.length > 0) {
@@ -3251,7 +1350,7 @@ var validateAndFixHandler = async (args, ctx) => {
3251
1350
  const unresolvedAmbiguities2 = ambiguities.filter(
3252
1351
  (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
3253
1352
  );
3254
- if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsFormElicitation(ctx)) {
1353
+ if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsElicitation) {
3255
1354
  workingSpec ??= cloneSpec(spec);
3256
1355
  const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
3257
1356
  unresolvedAmbiguities2,
@@ -3266,7 +1365,7 @@ var validateAndFixHandler = async (args, ctx) => {
3266
1365
  const remainingAmbiguities = ambiguities.filter(
3267
1366
  (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
3268
1367
  );
3269
- if (remainingAmbiguities.length > 0 && allowSampling && supportsSampling(ctx)) {
1368
+ if (remainingAmbiguities.length > 0 && allowSampling && supportsModelSampling) {
3270
1369
  workingSpec ??= cloneSpec(spec);
3271
1370
  const sampledReplacements = await resolveAmbiguitiesWithSampling(
3272
1371
  remainingAmbiguities,
@@ -3287,6 +1386,18 @@ var validateAndFixHandler = async (args, ctx) => {
3287
1386
  const unresolvedAmbiguities = ambiguities.filter(
3288
1387
  (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
3289
1388
  ).map(({ nodeRef: _nodeRef, ...ambiguity }) => ambiguity);
1389
+ const unresolvedAmbiguitiesWithRefs = ambiguities.filter(
1390
+ (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
1391
+ );
1392
+ const wouldFixIfAuthorized = buildWouldFixIfAuthorized({
1393
+ deterministicPreview: preview?.replacements ?? [],
1394
+ unresolvedAmbiguities: unresolvedAmbiguitiesWithRefs,
1395
+ applyFixes,
1396
+ allowElicitation,
1397
+ allowSampling,
1398
+ supportsElicitation,
1399
+ supportsSampling: supportsModelSampling
1400
+ });
3290
1401
  const attestation = {
3291
1402
  sourceType: ctx.data.snapshot.sourceType,
3292
1403
  sourceLabel: ctx.data.snapshot.sourceLabel,
@@ -3301,9 +1412,9 @@ var validateAndFixHandler = async (args, ctx) => {
3301
1412
  overrideApplied: Boolean(policyOverrides)
3302
1413
  },
3303
1414
  clientCapabilities: {
3304
- sampling: supportsSampling(ctx),
1415
+ sampling: supportsModelSampling,
3305
1416
  samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
3306
- elicitationForm: supportsFormElicitation(ctx),
1417
+ elicitationForm: supportsElicitation,
3307
1418
  roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
3308
1419
  },
3309
1420
  capabilitiesUsed: {
@@ -3333,6 +1444,7 @@ var validateAndFixHandler = async (args, ctx) => {
3333
1444
  replacements,
3334
1445
  unresolvedAmbiguities.length
3335
1446
  );
1447
+ const catalogMeta = getCatalogMeta(ctx.data);
3336
1448
  const payload = {
3337
1449
  status,
3338
1450
  nextAction,
@@ -3358,7 +1470,8 @@ var validateAndFixHandler = async (args, ctx) => {
3358
1470
  effectiveComponents,
3359
1471
  "forbidden"
3360
1472
  ),
3361
- unresolvedAmbiguities
1473
+ unresolvedAmbiguities,
1474
+ wouldFixIfAuthorized
3362
1475
  };
3363
1476
  await emitValidateAndFixTelemetry({
3364
1477
  ctx,
@@ -3381,13 +1494,14 @@ var validateAndFixHandler = async (args, ctx) => {
3381
1494
  }
3382
1495
  ],
3383
1496
  _meta: {
1497
+ ...catalogMeta,
3384
1498
  status,
3385
1499
  nextAction,
3386
1500
  replacementCount: replacements.length,
3387
1501
  passed: finalVerdict.passed,
3388
1502
  unresolvedAmbiguityCount: unresolvedAmbiguities.length,
3389
- resolutionPath,
3390
- catalogRevision: attestation.catalogRevision
1503
+ wouldFixIfAuthorizedCount: wouldFixIfAuthorized.length,
1504
+ resolutionPath
3391
1505
  }
3392
1506
  };
3393
1507
  } catch (error) {
@@ -3406,52 +1520,22 @@ var validateAndFixHandler = async (args, ctx) => {
3406
1520
  }
3407
1521
  };
3408
1522
 
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
1523
+ // src/cloud-http.ts
3439
1524
  var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
3440
1525
  function normalizeCloudUrl(url) {
3441
1526
  if (!url) return DEFAULT_CLOUD_URL;
3442
1527
  return url.replace(/\/+$/, "");
3443
1528
  }
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));
1529
+ async function cloudFetchJson(args) {
1530
+ const base = normalizeCloudUrl(args.cloudUrl);
1531
+ const url = new URL(`${base}${args.path}`);
1532
+ if (args.query) {
1533
+ for (const [key, value] of Object.entries(args.query)) {
1534
+ if (value !== void 0) url.searchParams.set(key, String(value));
1535
+ }
1536
+ }
3453
1537
  const response = await fetch(url.toString(), {
3454
- headers: { "X-API-Key": apiKey }
1538
+ headers: { "X-API-Key": args.apiKey }
3455
1539
  });
3456
1540
  if (!response.ok) {
3457
1541
  const body = await response.text();
@@ -3463,11 +1547,29 @@ async function fetchFindings(apiKey, params, cloudUrl) {
3463
1547
  message = body;
3464
1548
  }
3465
1549
  throw new Error(
3466
- `Cloud findings API error (${response.status}): ${message}`
1550
+ `Cloud ${args.resource} API error (${response.status}): ${message}`
3467
1551
  );
3468
1552
  }
3469
1553
  return await response.json();
3470
1554
  }
1555
+
1556
+ // src/findings-service.ts
1557
+ async function fetchFindings(apiKey, params, cloudUrl) {
1558
+ return cloudFetchJson({
1559
+ apiKey,
1560
+ cloudUrl,
1561
+ path: "/api/findings",
1562
+ resource: "findings",
1563
+ query: {
1564
+ status: params.status,
1565
+ severity: params.severity,
1566
+ category: params.category,
1567
+ ruleId: params.ruleId,
1568
+ filePath: params.filePath,
1569
+ limit: params.limit
1570
+ }
1571
+ });
1572
+ }
3471
1573
  async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
3472
1574
  return fetchFindings(
3473
1575
  apiKey,
@@ -3475,8 +1577,59 @@ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
3475
1577
  cloudUrl
3476
1578
  );
3477
1579
  }
1580
+ function buildFindingSourceUrl(finding) {
1581
+ const { repoFullName, commitSha, filePath, line } = finding;
1582
+ if (!repoFullName || !commitSha || !filePath) return void 0;
1583
+ if (!/^[-\w.]+\/[-\w.]+$/.test(repoFullName)) return void 0;
1584
+ if (!/^[a-fA-F0-9]{7,64}$/.test(commitSha)) return void 0;
1585
+ const normalizedPath = filePath.replace(/^\/+/, "");
1586
+ if (!normalizedPath) return void 0;
1587
+ const encodedPath = normalizedPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1588
+ const lineSuffix = line != null && line > 0 ? `#L${Math.floor(line)}` : "";
1589
+ return `https://github.com/${repoFullName}/blob/${commitSha}/${encodedPath}${lineSuffix}`;
1590
+ }
1591
+ async function fetchFindingsSummary(apiKey, params, cloudUrl) {
1592
+ const { findings } = await fetchFindings(
1593
+ apiKey,
1594
+ { ...params, limit: params.limit ?? 200 },
1595
+ cloudUrl
1596
+ );
1597
+ return summarizeFindings(findings, params);
1598
+ }
1599
+ function summarizeFindings(findings, params) {
1600
+ const bySeverity = { error: 0, warning: 0, info: 0 };
1601
+ const byStatus = { open: 0, resolved: 0, ignored: 0 };
1602
+ const byCategory = {};
1603
+ const byRuleId = {};
1604
+ const byFilePath = {};
1605
+ for (const finding of findings) {
1606
+ bySeverity[finding.severity] += 1;
1607
+ byStatus[finding.status] += 1;
1608
+ increment(byRuleId, finding.ruleId);
1609
+ if (finding.category) increment(byCategory, finding.category);
1610
+ if (finding.filePath) increment(byFilePath, finding.filePath);
1611
+ }
1612
+ const { limit: _limit, ...filters } = params;
1613
+ return {
1614
+ total: findings.length,
1615
+ filters,
1616
+ bySeverity,
1617
+ byStatus,
1618
+ byCategory,
1619
+ byRuleId,
1620
+ byFilePath,
1621
+ topFiles: topEntries(byFilePath, "filePath"),
1622
+ topRules: topEntries(byRuleId, "ruleId")
1623
+ };
1624
+ }
1625
+ function increment(counts, key) {
1626
+ counts[key] = (counts[key] ?? 0) + 1;
1627
+ }
1628
+ function topEntries(counts, keyName) {
1629
+ 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 }));
1630
+ }
3478
1631
 
3479
- // src/tools/findings.ts
1632
+ // src/tools/cloud-auth.ts
3480
1633
  function resolveCloudApiKey(ctx) {
3481
1634
  return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
3482
1635
  }
@@ -3496,6 +1649,38 @@ function missingKeyError() {
3496
1649
  isError: true
3497
1650
  };
3498
1651
  }
1652
+
1653
+ // src/tools/findings.ts
1654
+ function enrichFindings(findings, ctx) {
1655
+ return findings.map((finding) => {
1656
+ const sourceUrl = finding.sourceUrl ?? buildFindingSourceUrl(finding);
1657
+ const withSourceUrl = sourceUrl ? { ...finding, sourceUrl } : finding;
1658
+ if (!finding.prop || !finding.rawValue || !ctx.data.tokens) {
1659
+ return withSourceUrl;
1660
+ }
1661
+ const catalogMeta = getCatalogMeta(ctx.data);
1662
+ const suggestion = suggestToken({
1663
+ tokens: ctx.data.tokens,
1664
+ property: finding.prop,
1665
+ value: finding.rawValue,
1666
+ catalogRevision: catalogMeta.catalogRevision,
1667
+ updatedAt: catalogMeta.updatedAt
1668
+ });
1669
+ if (!suggestion.recommended) {
1670
+ const {
1671
+ suggestedToken: _discarded,
1672
+ suggestedTokenDetails: _details,
1673
+ ...rest
1674
+ } = withSourceUrl;
1675
+ return rest;
1676
+ }
1677
+ return {
1678
+ ...withSourceUrl,
1679
+ suggestedToken: suggestion.recommended.cssVar ?? suggestion.recommended.name,
1680
+ suggestedTokenDetails: suggestion.recommended
1681
+ };
1682
+ });
1683
+ }
3499
1684
  var findingsListHandler = async (args, ctx) => {
3500
1685
  const apiKey = resolveCloudApiKey(ctx);
3501
1686
  if (!apiKey) return missingKeyError();
@@ -3510,9 +1695,65 @@ var findingsListHandler = async (args, ctx) => {
3510
1695
  if (args.limit != null) params.limit = Number(args.limit);
3511
1696
  try {
3512
1697
  const result2 = await fetchFindings(apiKey, params, cloudUrl);
1698
+ const findings = enrichFindings(result2.findings, ctx);
1699
+ const catalogMeta = getCatalogMeta(ctx.data);
1700
+ return {
1701
+ content: [
1702
+ {
1703
+ type: "text",
1704
+ text: JSON.stringify({ ...result2, findings })
1705
+ }
1706
+ ],
1707
+ _meta: {
1708
+ ...catalogMeta,
1709
+ count: findings.length,
1710
+ tokenSuggestionCount: findings.filter(
1711
+ (finding) => finding.suggestedTokenDetails
1712
+ ).length
1713
+ }
1714
+ };
1715
+ } catch (error) {
1716
+ return {
1717
+ content: [
1718
+ {
1719
+ type: "text",
1720
+ text: JSON.stringify({
1721
+ error: error instanceof Error ? error.message : String(error)
1722
+ })
1723
+ }
1724
+ ],
1725
+ isError: true
1726
+ };
1727
+ }
1728
+ };
1729
+ var findingsSummaryHandler = async (args, ctx) => {
1730
+ const apiKey = resolveCloudApiKey(ctx);
1731
+ if (!apiKey) return missingKeyError();
1732
+ const cloudUrl = resolveCloudUrl(ctx);
1733
+ const params = {};
1734
+ if (args.status) params.status = args.status;
1735
+ if (args.severity)
1736
+ params.severity = args.severity;
1737
+ if (args.category) params.category = String(args.category);
1738
+ if (args.ruleId) params.ruleId = String(args.ruleId);
1739
+ if (args.filePath) params.filePath = String(args.filePath);
1740
+ if (args.limit != null) params.limit = Number(args.limit);
1741
+ try {
1742
+ const summary = await fetchFindingsSummary(apiKey, params, cloudUrl);
1743
+ const catalogMeta = getCatalogMeta(ctx.data);
3513
1744
  return {
3514
- content: [{ type: "text", text: JSON.stringify(result2) }],
3515
- _meta: { count: result2.findings.length }
1745
+ content: [
1746
+ {
1747
+ type: "text",
1748
+ text: JSON.stringify(summary)
1749
+ }
1750
+ ],
1751
+ _meta: {
1752
+ ...catalogMeta,
1753
+ total: summary.total,
1754
+ topFileCount: summary.topFiles.length,
1755
+ topRuleCount: summary.topRules.length
1756
+ }
3516
1757
  };
3517
1758
  } catch (error) {
3518
1759
  return {
@@ -3546,7 +1787,11 @@ var findingsForFileHandler = async (args, ctx) => {
3546
1787
  const cloudUrl = resolveCloudUrl(ctx);
3547
1788
  try {
3548
1789
  const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
3549
- const findings = result2.findings.filter((f) => f.filePath === filePath);
1790
+ const findings = enrichFindings(
1791
+ result2.findings.filter((f) => f.filePath === filePath),
1792
+ ctx
1793
+ );
1794
+ const catalogMeta = getCatalogMeta(ctx.data);
3550
1795
  return {
3551
1796
  content: [
3552
1797
  {
@@ -3554,7 +1799,14 @@ var findingsForFileHandler = async (args, ctx) => {
3554
1799
  text: JSON.stringify({ findings, filePath })
3555
1800
  }
3556
1801
  ],
3557
- _meta: { count: findings.length, filePath }
1802
+ _meta: {
1803
+ ...catalogMeta,
1804
+ count: findings.length,
1805
+ filePath,
1806
+ tokenSuggestionCount: findings.filter(
1807
+ (finding) => finding.suggestedTokenDetails
1808
+ ).length
1809
+ }
3558
1810
  };
3559
1811
  } catch (error) {
3560
1812
  return {
@@ -3571,43 +1823,211 @@ var findingsForFileHandler = async (args, ctx) => {
3571
1823
  }
3572
1824
  };
3573
1825
 
1826
+ // src/tools/swap-to-canonical.ts
1827
+ import * as ts from "typescript";
1828
+ import {
1829
+ resolveCanonicalForHtmlElement,
1830
+ formatRawHtmlElement
1831
+ } from "@fragments-sdk/classifier";
1832
+
1833
+ // src/canonical-mappings-service.ts
1834
+ async function fetchCanonicalMappings(apiKey, cloudUrl) {
1835
+ const catalog = await cloudFetchJson({
1836
+ apiKey,
1837
+ cloudUrl,
1838
+ path: "/api/catalog",
1839
+ resource: "catalog"
1840
+ });
1841
+ return catalog.canonicalMappings ?? [];
1842
+ }
1843
+
1844
+ // src/tools/swap-to-canonical.ts
1845
+ function errorResult(message) {
1846
+ return {
1847
+ content: [
1848
+ {
1849
+ type: "text",
1850
+ text: JSON.stringify({ error: message })
1851
+ }
1852
+ ],
1853
+ isError: true
1854
+ };
1855
+ }
1856
+ var resolveCanonicalForElement = resolveCanonicalForHtmlElement;
1857
+ function walkRawJsxElements(source) {
1858
+ const results = [];
1859
+ const visit = (node) => {
1860
+ let opening;
1861
+ if (ts.isJsxSelfClosingElement(node)) {
1862
+ opening = node;
1863
+ } else if (ts.isJsxElement(node)) {
1864
+ opening = node.openingElement;
1865
+ }
1866
+ if (opening) {
1867
+ const tagNameNode = opening.tagName;
1868
+ if (ts.isIdentifier(tagNameNode)) {
1869
+ const tagName = tagNameNode.text;
1870
+ if (/^[a-z]/.test(tagName)) {
1871
+ const attrs = /* @__PURE__ */ new Map();
1872
+ for (const attr of opening.attributes.properties) {
1873
+ if (!ts.isJsxAttribute(attr)) continue;
1874
+ if (!attr.name || !ts.isIdentifier(attr.name)) continue;
1875
+ const name = attr.name.text;
1876
+ if (!attr.initializer) {
1877
+ attrs.set(name, { value: "", dynamic: false });
1878
+ continue;
1879
+ }
1880
+ if (ts.isStringLiteral(attr.initializer)) {
1881
+ attrs.set(name, {
1882
+ value: attr.initializer.text,
1883
+ dynamic: false
1884
+ });
1885
+ } else if (ts.isJsxExpression(attr.initializer)) {
1886
+ attrs.set(name, { value: "", dynamic: true });
1887
+ }
1888
+ }
1889
+ const start = opening.getStart(source);
1890
+ const { line } = source.getLineAndCharacterOfPosition(start);
1891
+ results.push({ tagName, attrs, line: line + 1 });
1892
+ }
1893
+ }
1894
+ }
1895
+ ts.forEachChild(node, visit);
1896
+ };
1897
+ visit(source);
1898
+ return results;
1899
+ }
1900
+ var formatRawElement = formatRawHtmlElement;
1901
+ function buildPropMapping(attrs, rowMapping) {
1902
+ const transforms = new Map(
1903
+ rowMapping.map((m) => [m.rawProp, m])
1904
+ );
1905
+ const mapping = [];
1906
+ for (const rawProp of attrs.keys()) {
1907
+ if (rawProp === "type") continue;
1908
+ mapping.push(
1909
+ transforms.get(rawProp) ?? { rawProp, canonicalProp: rawProp }
1910
+ );
1911
+ }
1912
+ return mapping;
1913
+ }
1914
+ function groupMappingsByCanonical(mappings) {
1915
+ const out = /* @__PURE__ */ new Map();
1916
+ for (const m of mappings) {
1917
+ if (!m.importPath) continue;
1918
+ const list = out.get(m.canonical) ?? [];
1919
+ list.push(m);
1920
+ out.set(m.canonical, list);
1921
+ }
1922
+ for (const list of out.values()) {
1923
+ list.sort((a, b) => {
1924
+ const ac = a.confidence ?? 0;
1925
+ const bc = b.confidence ?? 0;
1926
+ if (ac !== bc) return bc - ac;
1927
+ return a.name.localeCompare(b.name);
1928
+ });
1929
+ }
1930
+ return out;
1931
+ }
1932
+ function buildSwapSuggestions(args) {
1933
+ const eligible = args.mappings.filter(
1934
+ (m) => m.canonicalConfidence === "auto" || m.canonicalConfidence === "overridden"
1935
+ );
1936
+ if (eligible.length === 0) return [];
1937
+ const byCanonical = groupMappingsByCanonical(eligible);
1938
+ if (byCanonical.size === 0) return [];
1939
+ const source = ts.createSourceFile(
1940
+ args.filePath,
1941
+ args.fileContent,
1942
+ ts.ScriptTarget.Latest,
1943
+ /* setParentNodes */
1944
+ true,
1945
+ ts.ScriptKind.TSX
1946
+ );
1947
+ const elements = walkRawJsxElements(source);
1948
+ const suggestions = [];
1949
+ for (const el of elements) {
1950
+ const canonical = resolveCanonicalForElement(el.tagName, el.attrs);
1951
+ if (!canonical) continue;
1952
+ const candidates = byCanonical.get(canonical);
1953
+ if (!candidates || candidates.length === 0) continue;
1954
+ const primary = candidates[0];
1955
+ const importPath = primary.importPath;
1956
+ if (!importPath) continue;
1957
+ const suggestion = {
1958
+ rawElement: formatRawElement(el.tagName, el.attrs),
1959
+ canonical,
1960
+ componentName: primary.name,
1961
+ importPath,
1962
+ propMapping: buildPropMapping(el.attrs, primary.propMapping),
1963
+ line: el.line
1964
+ };
1965
+ if (candidates.length > 1) {
1966
+ suggestion.alternates = candidates.slice(1).filter((c) => c.importPath).map((c) => ({
1967
+ name: c.name,
1968
+ importPath: c.importPath,
1969
+ confidence: c.confidence ?? 0
1970
+ }));
1971
+ }
1972
+ suggestions.push(suggestion);
1973
+ }
1974
+ return suggestions;
1975
+ }
1976
+ var swapToCanonicalHandler = async (args, ctx) => {
1977
+ const apiKey = resolveCloudApiKey(ctx);
1978
+ if (!apiKey) return missingKeyError();
1979
+ const filePath = typeof args.filePath === "string" ? args.filePath : "";
1980
+ const fileContent = typeof args.fileContent === "string" ? args.fileContent : "";
1981
+ if (!filePath || !fileContent) {
1982
+ return errorResult("filePath and fileContent are required.");
1983
+ }
1984
+ const cloudUrl = resolveCloudUrl(ctx);
1985
+ let mappings;
1986
+ try {
1987
+ mappings = await fetchCanonicalMappings(apiKey, cloudUrl);
1988
+ } catch (error) {
1989
+ return errorResult(error instanceof Error ? error.message : String(error));
1990
+ }
1991
+ const suggestions = buildSwapSuggestions({
1992
+ filePath,
1993
+ fileContent,
1994
+ mappings
1995
+ });
1996
+ return {
1997
+ content: [
1998
+ {
1999
+ type: "text",
2000
+ text: JSON.stringify({ suggestions, filePath })
2001
+ }
2002
+ ],
2003
+ _meta: {
2004
+ count: suggestions.length,
2005
+ mappingCount: mappings.length,
2006
+ filePath
2007
+ }
2008
+ };
2009
+ };
2010
+
3574
2011
  // src/tools/index.ts
3575
2012
  var CORE_TOOLS = {
3576
- discover: discoverHandler,
3577
- inspect: inspectHandler,
3578
- blocks: blocksHandler,
3579
- tokens: tokensHandler,
3580
- graph: graphHandler,
3581
- perf: perfHandler,
2013
+ "tokens.suggest": tokensSuggestHandler,
3582
2014
  govern: governHandler,
3583
2015
  validate_and_fix: validateAndFixHandler,
3584
2016
  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
2017
+ findings_summary: findingsSummaryHandler,
2018
+ findings_for_file: findingsForFileHandler,
2019
+ swap_to_canonical: swapToCanonicalHandler
3594
2020
  };
2021
+ var VIEWER_TOOLS = {};
2022
+ var INFRA_TOOLS = {};
3595
2023
  var BUILTIN_TOOLS = {
3596
2024
  ...CORE_TOOLS,
3597
2025
  ...VIEWER_TOOLS,
3598
2026
  ...INFRA_TOOLS
3599
2027
  };
3600
2028
  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"]
2029
+ "tokens.suggest": ["tokens"],
2030
+ validate_and_fix: ["components"]
3611
2031
  };
3612
2032
 
3613
2033
  // src/registry.ts
@@ -3747,15 +2167,15 @@ function telemetryMiddleware(logger) {
3747
2167
  }
3748
2168
 
3749
2169
  // src/source-selection.ts
3750
- import { existsSync as existsSync8 } from "fs";
3751
- import { join as join7 } from "path";
2170
+ import { existsSync as existsSync7 } from "fs";
2171
+ import { join as join6 } from "path";
3752
2172
 
3753
2173
  // src/adapters/fragments-json.ts
3754
2174
  import { readFile } from "fs/promises";
3755
2175
 
3756
2176
  // src/discovery.ts
3757
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
3758
- import { join as join3, dirname, resolve } from "path";
2177
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
2178
+ import { join as join2, dirname, resolve } from "path";
3759
2179
  import { createRequire } from "module";
3760
2180
  function resolveWorkspaceGlob(baseDir, pattern) {
3761
2181
  const parts = pattern.split("/");
@@ -3768,14 +2188,14 @@ function resolveWorkspaceGlob(baseDir, pattern) {
3768
2188
  try {
3769
2189
  for (const entry of readdirSync(d, { withFileTypes: true })) {
3770
2190
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
3771
- next.push(join3(d, entry.name));
2191
+ next.push(join2(d, entry.name));
3772
2192
  }
3773
2193
  }
3774
2194
  } catch {
3775
2195
  }
3776
2196
  } else {
3777
- const candidate = join3(d, part);
3778
- if (existsSync3(candidate)) next.push(candidate);
2197
+ const candidate = join2(d, part);
2198
+ if (existsSync2(candidate)) next.push(candidate);
3779
2199
  }
3780
2200
  }
3781
2201
  dirs = next;
@@ -3784,8 +2204,8 @@ function resolveWorkspaceGlob(baseDir, pattern) {
3784
2204
  }
3785
2205
  function getWorkspaceDirs(rootDir) {
3786
2206
  const dirs = [];
3787
- const rootPkgPath = join3(rootDir, "package.json");
3788
- if (existsSync3(rootPkgPath)) {
2207
+ const rootPkgPath = join2(rootDir, "package.json");
2208
+ if (existsSync2(rootPkgPath)) {
3789
2209
  try {
3790
2210
  const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
3791
2211
  const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
@@ -3798,8 +2218,8 @@ function getWorkspaceDirs(rootDir) {
3798
2218
  } catch {
3799
2219
  }
3800
2220
  }
3801
- const pnpmWsPath = join3(rootDir, "pnpm-workspace.yaml");
3802
- if (existsSync3(pnpmWsPath)) {
2221
+ const pnpmWsPath = join2(rootDir, "pnpm-workspace.yaml");
2222
+ if (existsSync2(pnpmWsPath)) {
3803
2223
  try {
3804
2224
  const content = readFileSync3(pnpmWsPath, "utf-8");
3805
2225
  const lines = content.split("\n");
@@ -3832,8 +2252,8 @@ function resolveDepPackageJson(localRequire, depName) {
3832
2252
  const mainPath = localRequire.resolve(depName);
3833
2253
  let dir = dirname(mainPath);
3834
2254
  while (true) {
3835
- const candidate = join3(dir, "package.json");
3836
- if (existsSync3(candidate)) {
2255
+ const candidate = join2(dir, "package.json");
2256
+ if (existsSync2(candidate)) {
3837
2257
  const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
3838
2258
  if (pkg.name === depName) return candidate;
3839
2259
  }
@@ -3846,23 +2266,23 @@ function resolveDepPackageJson(localRequire, depName) {
3846
2266
  return null;
3847
2267
  }
3848
2268
  function findFragmentsInDeps(dir, found, depField) {
3849
- const pkgJsonPath = join3(dir, "package.json");
3850
- if (!existsSync3(pkgJsonPath)) return;
2269
+ const pkgJsonPath = join2(dir, "package.json");
2270
+ if (!existsSync2(pkgJsonPath)) return;
3851
2271
  try {
3852
2272
  const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
3853
2273
  const allDeps = {
3854
2274
  ...pkgJson.dependencies,
3855
2275
  ...pkgJson.devDependencies
3856
2276
  };
3857
- const localRequire = createRequire(join3(dir, "noop.js"));
2277
+ const localRequire = createRequire(join2(dir, "noop.js"));
3858
2278
  for (const depName of Object.keys(allDeps)) {
3859
2279
  try {
3860
2280
  const depPkgPath = resolveDepPackageJson(localRequire, depName);
3861
2281
  if (!depPkgPath) continue;
3862
2282
  const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
3863
2283
  if (depPkg[depField]) {
3864
- const fragmentsPath = join3(dirname(depPkgPath), depPkg[depField]);
3865
- if (existsSync3(fragmentsPath) && !found.includes(fragmentsPath)) {
2284
+ const fragmentsPath = join2(dirname(depPkgPath), depPkg[depField]);
2285
+ if (existsSync2(fragmentsPath) && !found.includes(fragmentsPath)) {
3866
2286
  found.push(fragmentsPath);
3867
2287
  }
3868
2288
  }
@@ -3877,8 +2297,8 @@ function findDesignSystemJson(startDir, outFile, depField) {
3877
2297
  const resolvedStart = resolve(startDir);
3878
2298
  let dir = resolvedStart;
3879
2299
  while (true) {
3880
- const candidate = join3(dir, outFile);
3881
- if (existsSync3(candidate)) {
2300
+ const candidate = join2(dir, outFile);
2301
+ if (existsSync2(candidate)) {
3882
2302
  found.push(candidate);
3883
2303
  break;
3884
2304
  }
@@ -3887,7 +2307,7 @@ function findDesignSystemJson(startDir, outFile, depField) {
3887
2307
  dir = parent;
3888
2308
  }
3889
2309
  findFragmentsInDeps(resolvedStart, found, depField);
3890
- if (found.length === 0 || existsSync3(join3(resolvedStart, "pnpm-workspace.yaml"))) {
2310
+ if (found.length === 0 || existsSync2(join2(resolvedStart, "pnpm-workspace.yaml"))) {
3891
2311
  const workspaceDirs = getWorkspaceDirs(resolvedStart);
3892
2312
  for (const wsDir of workspaceDirs) {
3893
2313
  findFragmentsInDeps(wsDir, found, depField);
@@ -3902,8 +2322,8 @@ function findBundleManifest(startDir) {
3902
2322
  const found = [];
3903
2323
  let dir = resolve(startDir);
3904
2324
  while (true) {
3905
- const candidate = join3(dir, BRAND.dataDir, BRAND.manifestFile);
3906
- if (existsSync3(candidate)) {
2325
+ const candidate = join2(dir, BRAND.dataDir, BRAND.manifestFile);
2326
+ if (existsSync2(candidate)) {
3907
2327
  found.push(candidate);
3908
2328
  break;
3909
2329
  }
@@ -4059,13 +2479,15 @@ function tokensFromCompiledTokenData(tokens) {
4059
2479
  category,
4060
2480
  value: valueToString(entry.value),
4061
2481
  description: entry.description
4062
- }));
4063
- categories[category] = normalized;
4064
- flat.push(...normalized);
2482
+ })).filter((token) => !isGarbageToken(token));
2483
+ if (normalized.length > 0) {
2484
+ categories[category] = normalized;
2485
+ flat.push(...normalized);
2486
+ }
4065
2487
  }
4066
2488
  return {
4067
2489
  prefix: tokens.prefix,
4068
- total: tokens.total,
2490
+ total: flat.length,
4069
2491
  categories,
4070
2492
  flat
4071
2493
  };
@@ -4200,12 +2622,12 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
4200
2622
  };
4201
2623
 
4202
2624
  // src/adapters/auto-extract.ts
4203
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
4204
- import { join as join6, relative, sep } from "path";
2625
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2626
+ import { join as join5, relative, sep } from "path";
4205
2627
 
4206
2628
  // src/adapters/discover-components.ts
4207
- import { readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
4208
- import { join as join4, extname, basename } from "path";
2629
+ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
2630
+ import { join as join3, extname, basename } from "path";
4209
2631
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
4210
2632
  "node_modules",
4211
2633
  "dist",
@@ -4241,10 +2663,10 @@ function discoverComponentFiles(projectRoot) {
4241
2663
  "src/ui",
4242
2664
  "lib/ui",
4243
2665
  "packages"
4244
- ].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
2666
+ ].map((d) => join3(projectRoot, d)).filter((d) => existsSync3(d));
4245
2667
  if (scanDirs.length === 0) {
4246
- const srcDir = join4(projectRoot, "src");
4247
- if (existsSync4(srcDir)) scanDirs.push(srcDir);
2668
+ const srcDir = join3(projectRoot, "src");
2669
+ if (existsSync3(srcDir)) scanDirs.push(srcDir);
4248
2670
  }
4249
2671
  for (const dir of scanDirs) {
4250
2672
  walkDir(dir, results, seen);
@@ -4263,14 +2685,14 @@ function walkDir(dir, results, seen, depth = 0) {
4263
2685
  if (entry.name.startsWith(".")) continue;
4264
2686
  if (entry.isDirectory()) {
4265
2687
  if (EXCLUDED_DIRS.has(entry.name)) continue;
4266
- walkDir(join4(dir, entry.name), results, seen, depth + 1);
2688
+ walkDir(join3(dir, entry.name), results, seen, depth + 1);
4267
2689
  continue;
4268
2690
  }
4269
2691
  if (!entry.isFile()) continue;
4270
2692
  const ext = extname(entry.name);
4271
2693
  if (ext !== ".tsx" && ext !== ".jsx") continue;
4272
2694
  if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
4273
- const filePath = join4(dir, entry.name);
2695
+ const filePath = join3(dir, entry.name);
4274
2696
  if (seen.has(filePath)) continue;
4275
2697
  seen.add(filePath);
4276
2698
  const name = inferComponentName(entry.name, dir);
@@ -4288,8 +2710,8 @@ function inferComponentName(fileName, dirPath) {
4288
2710
  }
4289
2711
 
4290
2712
  // 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";
2713
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
2714
+ import { join as join4, extname as extname2 } from "path";
4293
2715
  function scanTokens(projectRoot) {
4294
2716
  const cssFiles = discoverCssFiles(projectRoot);
4295
2717
  if (cssFiles.length === 0) return void 0;
@@ -4327,7 +2749,7 @@ function discoverCssFiles(projectRoot) {
4327
2749
  "styles",
4328
2750
  "css",
4329
2751
  "app"
4330
- ].map((d) => join5(projectRoot, d)).filter((d) => existsSync5(d));
2752
+ ].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
4331
2753
  searchDirs.push(projectRoot);
4332
2754
  for (const dir of searchDirs) {
4333
2755
  try {
@@ -4336,22 +2758,22 @@ function discoverCssFiles(projectRoot) {
4336
2758
  if (!entry.isFile()) continue;
4337
2759
  const ext = extname2(entry.name);
4338
2760
  if (ext === ".css" || ext === ".scss") {
4339
- files.push(join5(dir, entry.name));
2761
+ files.push(join4(dir, entry.name));
4340
2762
  }
4341
2763
  }
4342
2764
  } catch {
4343
2765
  continue;
4344
2766
  }
4345
2767
  }
4346
- const srcDir = join5(projectRoot, "src");
4347
- if (existsSync5(srcDir)) {
2768
+ const srcDir = join4(projectRoot, "src");
2769
+ if (existsSync4(srcDir)) {
4348
2770
  try {
4349
2771
  for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
4350
2772
  if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
4351
- const subDir = join5(srcDir, subEntry.name);
2773
+ const subDir = join4(srcDir, subEntry.name);
4352
2774
  for (const file of readdirSync3(subDir, { withFileTypes: true })) {
4353
2775
  if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
4354
- files.push(join5(subDir, file.name));
2776
+ files.push(join4(subDir, file.name));
4355
2777
  }
4356
2778
  }
4357
2779
  }
@@ -4583,7 +3005,7 @@ Check that your tsconfig.json includes the component directories.`
4583
3005
  var extractorModulePromise = null;
4584
3006
  async function loadExtractorModule() {
4585
3007
  if (!extractorModulePromise) {
4586
- extractorModulePromise = import("./dist-BDWAHJ4K.js");
3008
+ extractorModulePromise = import("./dist-TTCI6TME.js");
4587
3009
  }
4588
3010
  return extractorModulePromise;
4589
3011
  }
@@ -4849,15 +3271,15 @@ function inferCategory(relativePath) {
4849
3271
  function findTsConfig(projectRoot) {
4850
3272
  const candidates = ["tsconfig.json", "tsconfig.app.json"];
4851
3273
  for (const name of candidates) {
4852
- const p = join6(projectRoot, name);
4853
- if (existsSync6(p)) return p;
3274
+ const p = join5(projectRoot, name);
3275
+ if (existsSync5(p)) return p;
4854
3276
  }
4855
3277
  return null;
4856
3278
  }
4857
3279
  function readPackageName(projectRoot) {
4858
3280
  try {
4859
- const pkgPath = join6(projectRoot, "package.json");
4860
- if (!existsSync6(pkgPath)) return void 0;
3281
+ const pkgPath = join5(projectRoot, "package.json");
3282
+ if (!existsSync5(pkgPath)) return void 0;
4861
3283
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
4862
3284
  return pkg.name;
4863
3285
  } catch {
@@ -4880,7 +3302,36 @@ function chooseComponentSource(args) {
4880
3302
  }
4881
3303
  return args.catalogComponents;
4882
3304
  }
4883
- var TOKEN_CATEGORY_ALIASES2 = {
3305
+ function dominantString(values) {
3306
+ const counts = /* @__PURE__ */ new Map();
3307
+ for (const value of values) {
3308
+ const trimmed = typeof value === "string" ? value.trim() : "";
3309
+ if (!trimmed) continue;
3310
+ counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
3311
+ }
3312
+ const ranked = [...counts.entries()].sort(
3313
+ (a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
3314
+ );
3315
+ if (ranked.length === 0) return void 0;
3316
+ if (ranked[1] && ranked[1][1] === ranked[0][1]) return void 0;
3317
+ return ranked[0][0];
3318
+ }
3319
+ function inferDesignSystemMetadataFromComponents(components) {
3320
+ const packageName = dominantString(
3321
+ components.map((component) => component.packageName)
3322
+ );
3323
+ const packageComponents = packageName ? components.filter((component) => component.packageName === packageName) : components;
3324
+ const importPath = dominantString(
3325
+ packageComponents.map(
3326
+ (component) => component.importPath ?? component.packageName
3327
+ )
3328
+ );
3329
+ return {
3330
+ packageName: packageName ?? null,
3331
+ importPath: importPath ?? packageName ?? null
3332
+ };
3333
+ }
3334
+ var TOKEN_CATEGORY_ALIASES = {
4884
3335
  color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
4885
3336
  spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
4886
3337
  typography: ["typography", "font", "text", "copy", "line-height", "letter"],
@@ -4911,7 +3362,7 @@ function canonicalizeTokenCategory(token) {
4911
3362
  ].map(normalizeValue).filter(Boolean);
4912
3363
  for (const candidate of candidates) {
4913
3364
  for (const [canonical, aliases] of Object.entries(
4914
- TOKEN_CATEGORY_ALIASES2
3365
+ TOKEN_CATEGORY_ALIASES
4915
3366
  )) {
4916
3367
  if (candidate === canonical || aliases.some(
4917
3368
  (alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
@@ -4924,7 +3375,7 @@ function canonicalizeTokenCategory(token) {
4924
3375
  }
4925
3376
  function groupTokens(flat) {
4926
3377
  const categories = {};
4927
- const normalizedFlat = (flat ?? []).map((token) => {
3378
+ const normalizedFlat = (flat ?? []).filter((token) => !isGarbageToken(token)).map((token) => {
4928
3379
  const category = canonicalizeTokenCategory(token);
4929
3380
  const normalized = {
4930
3381
  name: token.name,
@@ -5055,6 +3506,7 @@ var CloudCatalogAdapter = class {
5055
3506
  constructor(options) {
5056
3507
  this.options = options;
5057
3508
  }
3509
+ options;
5058
3510
  name = "cloud";
5059
3511
  async load(_projectRoot) {
5060
3512
  const headers = {
@@ -5073,14 +3525,19 @@ var CloudCatalogAdapter = class {
5073
3525
  }
5074
3526
  const raw = await response.json();
5075
3527
  const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
5076
- const designSystem = mergeDesignSystemMetadata(
5077
- raw.designSystem,
5078
- validateFixRaw?.content?.designSystem
5079
- );
5080
3528
  const sourceComponents = chooseComponentSource({
5081
3529
  catalogComponents: raw.components ?? [],
5082
3530
  contextComponents: validateFixRaw?.content?.components ?? []
5083
3531
  });
3532
+ const inferredDesignSystem = inferDesignSystemMetadataFromComponents(sourceComponents);
3533
+ const designSystem = mergeDesignSystemMetadata(
3534
+ {
3535
+ ...raw.designSystem,
3536
+ packageName: raw.designSystem?.packageName ?? inferredDesignSystem.packageName,
3537
+ importPath: raw.designSystem?.importPath ?? inferredDesignSystem.importPath
3538
+ },
3539
+ validateFixRaw?.content?.designSystem
3540
+ );
5084
3541
  const components = Object.fromEntries(
5085
3542
  sourceComponents.map((component) => [
5086
3543
  component.componentKey,
@@ -5147,7 +3604,7 @@ var CloudCatalogAdapter = class {
5147
3604
  };
5148
3605
 
5149
3606
  // src/adapters/bundle.ts
5150
- import { existsSync as existsSync7 } from "fs";
3607
+ import { existsSync as existsSync6 } from "fs";
5151
3608
  import { readFile as readFile2 } from "fs/promises";
5152
3609
  import { dirname as dirname3, resolve as resolve2 } from "path";
5153
3610
  import {
@@ -5285,7 +3742,7 @@ var BundleAdapter = class {
5285
3742
  const bundleDir = dirname3(manifestPath);
5286
3743
  const repoRoot = dirname3(bundleDir);
5287
3744
  const tokensPath = resolve2(bundleDir, "tokens.json");
5288
- const tokensFile = existsSync7(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
3745
+ const tokensFile = existsSync6(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
5289
3746
  const components = Object.fromEntries(
5290
3747
  await Promise.all(
5291
3748
  Object.values(manifest.components).map(async (entry) => {
@@ -5382,7 +3839,7 @@ function resolveCloudUrl2(fileConfig) {
5382
3839
  return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
5383
3840
  }
5384
3841
  function hasTsProject(projectRoot) {
5385
- return existsSync8(join7(projectRoot, "tsconfig.json")) || existsSync8(join7(projectRoot, "tsconfig.app.json"));
3842
+ return existsSync7(join6(projectRoot, "tsconfig.json")) || existsSync7(join6(projectRoot, "tsconfig.app.json"));
5386
3843
  }
5387
3844
  function resolveDataAdapter(config, fileConfig) {
5388
3845
  const source = config.source ?? fileConfig?.source ?? "auto";
@@ -5396,7 +3853,7 @@ function resolveDataAdapter(config, fileConfig) {
5396
3853
  case "cloud":
5397
3854
  if (!cloudApiKey) {
5398
3855
  throw new Error(
5399
- "Cloud source requires a Cloud API key. Set FRAGMENTS_API_KEY or pass cloudApiKey."
3856
+ "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
3857
  );
5401
3858
  }
5402
3859
  return {
@@ -5440,7 +3897,142 @@ function resolveDataAdapter(config, fileConfig) {
5440
3897
  }
5441
3898
  }
5442
3899
  function resolveSearchApiKey(config, fileConfig) {
5443
- return config.searchApiKey ?? config.apiKey ?? fileConfig?.vectorSearch?.apiKey;
3900
+ return config.searchApiKey ?? fileConfig?.vectorSearch?.apiKey;
3901
+ }
3902
+
3903
+ // src/spec-schema.ts
3904
+ var UI_SPEC_SCHEMA_URI = "fragments://schemas/govern.schema";
3905
+ var UI_SPEC_SCHEMA_NAME = "govern.schema";
3906
+ var UI_SPEC_SCHEMA_MIME_TYPE = "application/schema+json";
3907
+ var UI_SPEC_SCHEMA_RESOURCE = {
3908
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3909
+ $id: UI_SPEC_SCHEMA_URI,
3910
+ title: "Fragments UI Spec",
3911
+ description: "Input shape accepted by the Fragments govern and validate_and_fix MCP tools.",
3912
+ type: "object",
3913
+ required: ["nodes"],
3914
+ additionalProperties: false,
3915
+ properties: {
3916
+ root: {
3917
+ type: "string",
3918
+ description: "Optional node id to treat as the root. When omitted, top-level nodes are evaluated in order."
3919
+ },
3920
+ metadata: {
3921
+ type: "object",
3922
+ description: "Optional caller metadata. The validator stores and echoes only fields that downstream tools understand.",
3923
+ additionalProperties: true
3924
+ },
3925
+ nodes: {
3926
+ type: "array",
3927
+ description: "Top-level UI nodes. Each node names a component type and may carry props plus nested children.",
3928
+ items: { $ref: "#/$defs/node" }
3929
+ }
3930
+ },
3931
+ $defs: {
3932
+ node: {
3933
+ type: "object",
3934
+ required: ["type"],
3935
+ additionalProperties: false,
3936
+ properties: {
3937
+ id: {
3938
+ type: "string",
3939
+ description: "Stable caller-provided id. Used in violations and fixedSpec patches."
3940
+ },
3941
+ type: {
3942
+ type: "string",
3943
+ description: 'Design-system component name, including compound names such as "DatePicker.Trigger".'
3944
+ },
3945
+ props: {
3946
+ type: "object",
3947
+ description: 'Component props. Put simple text labels in props.children, e.g. { "children": "Save" }.',
3948
+ additionalProperties: true
3949
+ },
3950
+ children: {
3951
+ description: "Nested child nodes, or a string text child for leaf content. Prefer props.children for simple button labels.",
3952
+ oneOf: [
3953
+ { type: "string" },
3954
+ {
3955
+ type: "array",
3956
+ items: {
3957
+ oneOf: [{ type: "string" }, { $ref: "#/$defs/node" }]
3958
+ }
3959
+ }
3960
+ ]
3961
+ }
3962
+ }
3963
+ }
3964
+ },
3965
+ examples: {
3966
+ valid: [
3967
+ {
3968
+ nodes: [
3969
+ {
3970
+ id: "save",
3971
+ type: "Button",
3972
+ props: { variant: "primary", children: "Save" }
3973
+ }
3974
+ ]
3975
+ },
3976
+ {
3977
+ root: "card",
3978
+ metadata: { source: "agent-draft" },
3979
+ nodes: [
3980
+ {
3981
+ id: "card",
3982
+ type: "Card",
3983
+ props: {},
3984
+ children: [
3985
+ {
3986
+ id: "title",
3987
+ type: "Heading",
3988
+ props: { level: 2, children: "Billing" }
3989
+ },
3990
+ {
3991
+ id: "submit",
3992
+ type: "Button",
3993
+ props: { children: "Update plan" }
3994
+ }
3995
+ ]
3996
+ }
3997
+ ]
3998
+ }
3999
+ ],
4000
+ invalid: [
4001
+ {
4002
+ reason: "String event handlers are blocked by safety/no-string-handlers.",
4003
+ spec: {
4004
+ nodes: [
4005
+ {
4006
+ id: "danger",
4007
+ type: "Button",
4008
+ props: { children: "Save", onClick: 'alert("saved")' }
4009
+ }
4010
+ ]
4011
+ }
4012
+ },
4013
+ {
4014
+ reason: "Raw CSS values should use design tokens; call tokens.suggest before writing them.",
4015
+ spec: {
4016
+ nodes: [
4017
+ {
4018
+ id: "panel",
4019
+ type: "Box",
4020
+ props: { style: { padding: "6px" } }
4021
+ }
4022
+ ]
4023
+ }
4024
+ }
4025
+ ]
4026
+ },
4027
+ notes: [
4028
+ "Use component names from the active catalog; unknown components fail components/allow.",
4029
+ "Use props.children for simple text labels, especially Button text.",
4030
+ "Do not put JavaScript source strings in event handler props.",
4031
+ "Run validate_and_fix after govern when the verdict asks for revision or deterministic repair."
4032
+ ]
4033
+ };
4034
+ function serializeUiSpecSchema() {
4035
+ return JSON.stringify(UI_SPEC_SCHEMA_RESOURCE, null, 2);
5444
4036
  }
5445
4037
 
5446
4038
  // src/server.ts
@@ -5458,6 +4050,7 @@ function createMcpServer(config) {
5458
4050
  {
5459
4051
  capabilities: {
5460
4052
  tools: { listChanged: true },
4053
+ resources: { listChanged: false },
5461
4054
  logging: {}
5462
4055
  }
5463
4056
  }
@@ -5488,21 +4081,20 @@ function createMcpServer(config) {
5488
4081
  let loadDataPromise = null;
5489
4082
  let resolvedRoot = null;
5490
4083
  let resolveProjectRootPromise = null;
5491
- let componentIndex = null;
5492
- let blockIndex = null;
5493
- let tokenIndex = null;
5494
4084
  async function resolveProjectRoot() {
5495
4085
  if (resolvedRoot) return resolvedRoot;
5496
4086
  if (resolveProjectRootPromise) return resolveProjectRootPromise;
5497
4087
  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;
4088
+ if (server.getClientCapabilities()?.roots) {
4089
+ try {
4090
+ const result2 = await server.listRoots();
4091
+ if (result2.roots?.length > 0) {
4092
+ const rootUri = result2.roots[0].uri;
4093
+ resolvedRoot = fileURLToPath(rootUri);
4094
+ return resolvedRoot;
4095
+ }
4096
+ } catch {
5504
4097
  }
5505
- } catch {
5506
4098
  }
5507
4099
  resolvedRoot = config.projectRoot;
5508
4100
  return resolvedRoot;
@@ -5519,11 +4111,6 @@ function createMcpServer(config) {
5519
4111
  loadDataPromise = (async () => {
5520
4112
  const projectRoot = await resolveProjectRoot();
5521
4113
  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
4114
  cachedData = loaded;
5528
4115
  return loaded;
5529
4116
  })();
@@ -5538,21 +4125,49 @@ function createMcpServer(config) {
5538
4125
  return {
5539
4126
  tools: registry.listTools(
5540
4127
  {
5541
- hasViewer: !!config.viewerUrl,
5542
- hasPlayground: !!(config.playgroundUrl ?? fileConfig?.playgroundUrl),
4128
+ hasViewer: false,
4129
+ hasPlayground: false,
5543
4130
  capabilities: data.capabilities
5544
4131
  },
5545
4132
  TOOLS
5546
4133
  )
5547
4134
  };
5548
4135
  });
4136
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
4137
+ resources: [
4138
+ {
4139
+ uri: UI_SPEC_SCHEMA_URI,
4140
+ name: UI_SPEC_SCHEMA_NAME,
4141
+ title: "Fragments govern UI spec schema",
4142
+ description: "JSON schema and examples for the spec argument accepted by govern and validate_and_fix.",
4143
+ mimeType: UI_SPEC_SCHEMA_MIME_TYPE
4144
+ }
4145
+ ]
4146
+ }));
4147
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4148
+ if (request.params.uri !== UI_SPEC_SCHEMA_URI) {
4149
+ throw new McpError(
4150
+ ErrorCode.InvalidParams,
4151
+ `Unknown resource URI: ${request.params.uri}`
4152
+ );
4153
+ }
4154
+ return {
4155
+ contents: [
4156
+ {
4157
+ uri: UI_SPEC_SCHEMA_URI,
4158
+ mimeType: UI_SPEC_SCHEMA_MIME_TYPE,
4159
+ text: serializeUiSpecSchema()
4160
+ }
4161
+ ]
4162
+ };
4163
+ });
5549
4164
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
5550
4165
  const { name, arguments: args } = request.params;
5551
4166
  const data = await loadData();
5552
4167
  const toolContext = {
5553
4168
  data,
5554
4169
  config: mergedConfig,
5555
- indexes: { componentIndex, blockIndex, tokenIndex },
4170
+ indexes: { componentIndex: null, blockIndex: null, tokenIndex: null },
5556
4171
  mcp: {
5557
4172
  server,
5558
4173
  clientCapabilities: server.getClientCapabilities()
@@ -5567,8 +4182,8 @@ function createMcpServer(config) {
5567
4182
  return "your-component-library";
5568
4183
  }
5569
4184
  const root = resolvedRoot ?? config.projectRoot;
5570
- const packageJsonPath = join8(root, "package.json");
5571
- if (existsSync9(packageJsonPath)) {
4185
+ const packageJsonPath = join7(root, "package.json");
4186
+ if (existsSync8(packageJsonPath)) {
5572
4187
  try {
5573
4188
  const content = readFileSync6(packageJsonPath, "utf-8");
5574
4189
  const pkg = JSON.parse(content);
@@ -5632,11 +4247,6 @@ function createSandboxServer() {
5632
4247
 
5633
4248
  export {
5634
4249
  loadConfigFile,
5635
- SYNONYM_MAP,
5636
- USE_CASE_TOKEN_CATEGORIES,
5637
- MINIMUM_SCORE_THRESHOLD,
5638
- BLOCK_BOOST_PER_OCCURRENCE,
5639
- DEFAULT_ENDPOINTS,
5640
4250
  CORE_TOOLS,
5641
4251
  VIEWER_TOOLS,
5642
4252
  INFRA_TOOLS,
@@ -5657,4 +4267,4 @@ export {
5657
4267
  startMcpServer,
5658
4268
  createSandboxServer
5659
4269
  };
5660
- //# sourceMappingURL=chunk-YSNIGHNU.js.map
4270
+ //# sourceMappingURL=chunk-YJTMK4JY.js.map