@fragments-sdk/mcp 0.8.1 → 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.
@@ -0,0 +1,4270 @@
1
+ import {
2
+ BRAND
3
+ } from "./chunk-4SVS3AA3.js";
4
+
5
+ // src/server.ts
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import {
9
+ CallToolRequestSchema,
10
+ ErrorCode,
11
+ ListResourcesRequestSchema,
12
+ ListToolsRequestSchema,
13
+ McpError,
14
+ ReadResourceRequestSchema
15
+ } from "@modelcontextprotocol/sdk/types.js";
16
+ import { existsSync as existsSync8 } from "fs";
17
+ import { readFileSync as readFileSync6 } from "fs";
18
+ import { join as join7 } from "path";
19
+ import { fileURLToPath } from "url";
20
+
21
+ // src/config.ts
22
+ import { readFileSync, existsSync } from "fs";
23
+ import { join } from "path";
24
+ function loadConfigFile(projectRoot) {
25
+ const configPath = join(projectRoot, "ds-mcp.config.json");
26
+ if (existsSync(configPath)) {
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)}`);
32
+ }
33
+ }
34
+ const pkgPath = join(projectRoot, "package.json");
35
+ if (existsSync(pkgPath)) {
36
+ try {
37
+ const content = readFileSync(pkgPath, "utf-8");
38
+ const pkg = JSON.parse(content);
39
+ if (pkg.dsMcp) return pkg.dsMcp;
40
+ } catch {
41
+ }
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() {
52
+ try {
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";
58
+ }
59
+ }
60
+ var MCP_SERVER_VERSION = readPackageVersion();
61
+
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";
83
+ }
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";
87
+ }
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";
90
+ }
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";
93
+ }
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") {
110
+ return {
111
+ alternatives: [],
112
+ noSuggestion: true,
113
+ noSuggestionReason: `No token family is known for CSS property "${input.property}".`,
114
+ _meta: meta
115
+ };
116
+ }
117
+ if (!input.tokens || input.tokens.total === 0) {
118
+ return {
119
+ alternatives: [],
120
+ noSuggestion: true,
121
+ noSuggestionReason: "No design tokens are available in the active catalog.",
122
+ _meta: meta
123
+ };
124
+ }
125
+ if (top.length === 0) {
126
+ return {
127
+ alternatives: [],
128
+ noSuggestion: true,
129
+ noSuggestionReason: `No ${family} tokens are available for CSS property "${input.property}".`,
130
+ _meta: meta
131
+ };
132
+ }
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";
157
+ }
158
+ }
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));
163
+ }
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";
168
+ return {
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
177
+ };
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
+ }
282
+
283
+ // src/tools/tokens-suggest.ts
284
+ var tokensSuggestHandler = async (args, ctx) => {
285
+ const property = args.property;
286
+ if (!property || typeof property !== "string") {
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: JSON.stringify({ error: "property is required." })
292
+ }
293
+ ],
294
+ isError: true
295
+ };
296
+ }
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
+ });
307
+ return {
308
+ content: [{ type: "text", text: JSON.stringify(result2) }],
309
+ _meta: result2._meta
310
+ };
311
+ };
312
+
313
+ // src/tools/spec-govern.ts
314
+ var SEVERITY_WEIGHTS = {
315
+ critical: 10,
316
+ serious: 5,
317
+ moderate: 2,
318
+ minor: 1
319
+ };
320
+ function isRuleEnabled(rule, defaultEnabled = true) {
321
+ if (rule === void 0) return defaultEnabled;
322
+ if (typeof rule === "boolean") return rule;
323
+ return rule.enabled ?? defaultEnabled;
324
+ }
325
+ function ruleSeverity(rule, fallback) {
326
+ return typeof rule === "object" && rule?.severity ? rule.severity : fallback;
327
+ }
328
+ function ruleOptions(rule) {
329
+ return typeof rule === "object" && rule?.options ? rule.options : {};
330
+ }
331
+ function nodeId(node, fallback) {
332
+ return typeof node.id === "string" ? node.id : `node-${fallback}`;
333
+ }
334
+ function nodeType(node) {
335
+ return typeof node.type === "string" ? node.type : "_unknown";
336
+ }
337
+ function parentType(type) {
338
+ const dotIndex = type.indexOf(".");
339
+ return dotIndex > 0 ? type.slice(0, dotIndex) : type;
340
+ }
341
+ function walkNodes(nodes, visitor) {
342
+ if (!Array.isArray(nodes)) return;
343
+ for (const [index, node] of nodes.entries()) {
344
+ if (typeof node !== "object" || node === null || Array.isArray(node)) {
345
+ continue;
346
+ }
347
+ const specNode = node;
348
+ visitor(specNode, index);
349
+ walkNodes(specNode.children, visitor);
350
+ }
351
+ }
352
+ function flattenNodes(spec) {
353
+ const flattened = [];
354
+ walkNodes(spec.nodes, (node) => flattened.push(node));
355
+ return flattened;
356
+ }
357
+ function worstSeverity(violations) {
358
+ const order = ["critical", "serious", "moderate", "minor"];
359
+ return violations.reduce(
360
+ (worst, violation) => order.indexOf(violation.severity) < order.indexOf(worst) ? violation.severity : worst,
361
+ "minor"
362
+ );
363
+ }
364
+ function result(validator, violations) {
365
+ return {
366
+ validator,
367
+ severity: violations.length > 0 ? worstSeverity(violations) : "minor",
368
+ passed: violations.length === 0,
369
+ violations
370
+ };
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
+ }
383
+ function computeScore(violations) {
384
+ if (violations.length === 0) return 100;
385
+ const penalty = violations.reduce(
386
+ (sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
387
+ 0
388
+ );
389
+ const cap = SEVERITY_SCORE_CAPS[worstSeverity(violations)];
390
+ return Math.min(cap, Math.max(0, 100 - penalty));
391
+ }
392
+ function validateComponents(nodes, options) {
393
+ const rules = options.policy?.rules ?? {};
394
+ const allowRule = rules["components/allow"];
395
+ const denyRule = rules["components/deny"];
396
+ const allowOptions = ruleOptions(allowRule);
397
+ const denyOptions = ruleOptions(denyRule);
398
+ const allowed = new Set(
399
+ (allowOptions.components ?? options.allowedComponents).filter(Boolean)
400
+ );
401
+ const denied = new Set(denyOptions.components ?? []);
402
+ const violations = [];
403
+ for (const [index, node] of nodes.entries()) {
404
+ const type = nodeType(node);
405
+ const parent = parentType(type);
406
+ if (allowed.size > 0 && isRuleEnabled(allowRule) && !allowed.has(type) && !allowed.has(parent)) {
407
+ violations.push({
408
+ nodeId: nodeId(node, index),
409
+ nodeType: type,
410
+ rule: "components/allow",
411
+ severity: ruleSeverity(allowRule, "serious"),
412
+ message: `Component "${type}" is not in the allowed list`,
413
+ suggestion: `Use one of the allowed components: ${[...allowed].slice(0, 10).join(", ")}${allowed.size > 10 ? "..." : ""}`
414
+ });
415
+ }
416
+ if (denied.size > 0 && isRuleEnabled(denyRule) && (denied.has(type) || denied.has(parent))) {
417
+ violations.push({
418
+ nodeId: nodeId(node, index),
419
+ nodeType: type,
420
+ rule: "components/deny",
421
+ severity: ruleSeverity(denyRule, "serious"),
422
+ message: `Component "${type}" is blocked by the deny list`,
423
+ suggestion: `Remove "${type}" or replace it with an allowed alternative`
424
+ });
425
+ }
426
+ }
427
+ return result("components", violations);
428
+ }
429
+ function isEventHandlerProp(prop) {
430
+ return /^on[A-Z]/.test(prop);
431
+ }
432
+ function validateSafety(nodes) {
433
+ const violations = [];
434
+ const dangerousProps = /* @__PURE__ */ new Set(["dangerouslySetInnerHTML", "innerHTML"]);
435
+ for (const [index, node] of nodes.entries()) {
436
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
437
+ if (dangerousProps.has(prop)) {
438
+ violations.push({
439
+ nodeId: nodeId(node, index),
440
+ nodeType: nodeType(node),
441
+ rule: "safety/no-dangerous-props",
442
+ severity: "critical",
443
+ message: `Dangerous prop "${prop}" is not allowed`,
444
+ suggestion: `Remove "${prop}" or use a safe rendering pattern`,
445
+ prop
446
+ });
447
+ }
448
+ if (isEventHandlerProp(prop) && typeof value === "string") {
449
+ violations.push({
450
+ nodeId: nodeId(node, index),
451
+ nodeType: nodeType(node),
452
+ rule: "safety/no-string-handlers",
453
+ severity: "serious",
454
+ message: `Event handler prop "${prop}" must not be a string`,
455
+ suggestion: `Remove "${prop}" from generated specs`,
456
+ prop
457
+ });
458
+ }
459
+ }
460
+ }
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);
531
+ }
532
+ function hasHardcodedCssValue(value) {
533
+ if (value.includes("var(")) return false;
534
+ return /#[0-9a-f]{3,8}\b/i.test(value) || /\b\d+(?:px|rem|em)\b/.test(value);
535
+ }
536
+ function validateTokens(nodes, options) {
537
+ const rules = options.policy?.rules ?? {};
538
+ const requireRule = rules["tokens/require-design-tokens"];
539
+ const prefixRule = rules["tokens/allowed-prefixes"];
540
+ const requireTokens = isRuleEnabled(requireRule);
541
+ const defaultTokenPrefix = options.tokenPrefix?.replace(/^--/, "");
542
+ const allowedPrefixes = ruleOptions(prefixRule).prefixes ?? (defaultTokenPrefix ? [defaultTokenPrefix] : []);
543
+ const violations = [];
544
+ for (const [index, node] of nodes.entries()) {
545
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
546
+ if (typeof value !== "string") continue;
547
+ if (requireTokens && hasHardcodedCssValue(value)) {
548
+ violations.push({
549
+ nodeId: nodeId(node, index),
550
+ nodeType: nodeType(node),
551
+ rule: "tokens/require-design-tokens",
552
+ severity: ruleSeverity(requireRule, "moderate"),
553
+ message: `Hardcoded CSS value "${value}" in prop "${prop}" - use a design token instead`,
554
+ suggestion: "Use a design token instead of a hardcoded CSS value",
555
+ prop,
556
+ rawValue: value
557
+ });
558
+ }
559
+ if (allowedPrefixes.length > 0 && isRuleEnabled(prefixRule, Boolean(options.tokenPrefix))) {
560
+ const matches = value.matchAll(/var\(--([^)]+)\)/g);
561
+ for (const match of matches) {
562
+ const tokenName = match[1];
563
+ if (allowedPrefixes.some((prefix) => tokenName.startsWith(prefix))) {
564
+ continue;
565
+ }
566
+ violations.push({
567
+ nodeId: nodeId(node, index),
568
+ nodeType: nodeType(node),
569
+ rule: "tokens/allowed-prefixes",
570
+ severity: ruleSeverity(prefixRule, "moderate"),
571
+ message: `Token "--${tokenName}" does not use an allowed prefix (${allowedPrefixes.join(", ")})`,
572
+ suggestion: `Use a token with one of the allowed prefixes: ${allowedPrefixes.map((prefix) => `--${prefix}*`).join(", ")}`,
573
+ prop,
574
+ rawValue: value
575
+ });
576
+ }
577
+ }
578
+ }
579
+ }
580
+ return result("tokens", violations);
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
+ }
600
+ function textFromChildren(children) {
601
+ if (typeof children === "string") return children.trim();
602
+ if (!Array.isArray(children)) return "";
603
+ return children.map((child) => {
604
+ return textFromUnknown(child);
605
+ }).join(" ").trim();
606
+ }
607
+ function validateA11y(nodes) {
608
+ const violations = [];
609
+ for (const [index, node] of nodes.entries()) {
610
+ const type = nodeType(node);
611
+ if (!/button/i.test(type)) continue;
612
+ const props = node.props ?? {};
613
+ const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
614
+ const childText = textFromNode(node);
615
+ if (typeof label === "string" && label.trim().length > 0) continue;
616
+ if (childText.length > 0) continue;
617
+ violations.push({
618
+ nodeId: nodeId(node, index),
619
+ nodeType: type,
620
+ rule: "button-name",
621
+ severity: "serious",
622
+ message: "Buttons must have discernible text (button-name)",
623
+ suggestion: "Add visible text or an aria-label"
624
+ });
625
+ }
626
+ return result("a11y", violations);
627
+ }
628
+ function runSpecGovern(spec, options) {
629
+ const startedAt = Date.now();
630
+ const nodes = flattenNodes(spec);
631
+ const results = [
632
+ validateSafety(nodes),
633
+ validateComponents(nodes, options),
634
+ validateProps(nodes, options),
635
+ validateTokens(nodes, options),
636
+ validateA11y(nodes)
637
+ ];
638
+ const violations = results.flatMap((entry) => entry.violations);
639
+ const componentTypes = Array.from(
640
+ new Set(nodes.map((node) => nodeType(node)))
641
+ );
642
+ return {
643
+ verdict: verdictFor(violations),
644
+ passed: results.every((entry) => entry.passed),
645
+ score: computeScore(violations),
646
+ results,
647
+ metadata: {
648
+ runner: "mcp",
649
+ duration: Date.now() - startedAt,
650
+ nodeCount: nodes.length,
651
+ componentTypes
652
+ }
653
+ };
654
+ }
655
+ function formatVerdict(verdict, format = "summary") {
656
+ if (format === "json") {
657
+ return JSON.stringify(verdict, null, 2);
658
+ }
659
+ const lines = [];
660
+ const icon = verdict.passed ? "ok" : "fail";
661
+ lines.push(`${icon} Governance check: verdict ${verdict.verdict}, score ${verdict.score}/100`);
662
+ lines.push("");
663
+ for (const entry of verdict.results) {
664
+ const resultIcon = entry.passed ? "ok" : "fail";
665
+ const count = entry.violations.length;
666
+ lines.push(` ${resultIcon} ${entry.validator}: ${count === 0 ? "passed" : `${count} violation(s)`}`);
667
+ for (const violation of entry.violations) {
668
+ lines.push(` - [${violation.severity}] ${violation.nodeType}#${violation.nodeId}: ${violation.message}`);
669
+ if (violation.suggestion) {
670
+ lines.push(` -> ${violation.suggestion}`);
671
+ }
672
+ }
673
+ }
674
+ lines.push("");
675
+ lines.push(`Duration: ${verdict.metadata.duration.toFixed(0)}ms | Nodes: ${verdict.metadata.nodeCount}`);
676
+ return lines.join("\n");
677
+ }
678
+
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
+ }
688
+ var governHandler = async (args, ctx) => {
689
+ const spec = args?.spec;
690
+ if (!spec || typeof spec !== "object") {
691
+ return {
692
+ content: [
693
+ {
694
+ type: "text",
695
+ text: JSON.stringify({
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."
697
+ })
698
+ }
699
+ ],
700
+ isError: true
701
+ };
702
+ }
703
+ const policyOverrides = args?.policy;
704
+ const format = args?.format ?? "json";
705
+ const allowedComponents = Object.values(ctx.data.components).map(
706
+ (component) => component.name
707
+ );
708
+ try {
709
+ const verdict = runSpecGovern(spec, {
710
+ allowedComponents,
711
+ tokenPrefix: ctx.data.tokens?.prefix,
712
+ policy: policyOverrides,
713
+ componentProps: buildComponentProps(ctx)
714
+ });
715
+ const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
716
+ return {
717
+ content: [{ type: "text", text }],
718
+ _meta: {
719
+ ...getCatalogMeta(ctx.data),
720
+ verdict: verdict.verdict,
721
+ score: verdict.score,
722
+ passed: verdict.passed,
723
+ violationCount: verdict.results.reduce(
724
+ (sum, r) => sum + r.violations.length,
725
+ 0
726
+ )
727
+ }
728
+ };
729
+ } catch (error) {
730
+ const message = error instanceof Error ? error.message : String(error);
731
+ const isSpecError = message.includes("Expected") || message.includes("Required");
732
+ return {
733
+ content: [
734
+ {
735
+ type: "text",
736
+ text: JSON.stringify({
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
738
+ })
739
+ }
740
+ ],
741
+ isError: true
742
+ };
743
+ }
744
+ };
745
+
746
+ // src/tools/validate-and-fix.ts
747
+ function classifyComponent(component) {
748
+ if (component.status === "deprecated") return "discouraged";
749
+ if (component.isCanonical && component.tier === "core") return "preferred";
750
+ return "allowed";
751
+ }
752
+ function buildEffectiveComponents(ctx) {
753
+ const validateFixByKey = new Map(
754
+ (ctx.data.validateFixContext?.components ?? []).map((component) => [
755
+ component.componentKey,
756
+ component
757
+ ])
758
+ );
759
+ return Object.entries(ctx.data.components).map(([componentKey, component]) => ({
760
+ component,
761
+ selection: validateFixByKey.get(componentKey)?.selection ?? classifyComponent(component),
762
+ isActive: validateFixByKey.get(componentKey)?.isActive ?? true,
763
+ reasons: validateFixByKey.get(componentKey)?.reasons ?? []
764
+ }));
765
+ }
766
+ function cloneSpec(spec) {
767
+ return structuredClone(spec);
768
+ }
769
+ function isPlainObject(value) {
770
+ return typeof value === "object" && value !== null && !Array.isArray(value);
771
+ }
772
+ function validateNodeShape(node, path) {
773
+ if (!isPlainObject(node)) {
774
+ return `${path} must be an object`;
775
+ }
776
+ if ("id" in node && node.id !== void 0 && typeof node.id !== "string") {
777
+ return `${path}.id must be a string`;
778
+ }
779
+ if ("type" in node && node.type !== void 0 && typeof node.type !== "string") {
780
+ return `${path}.type must be a string`;
781
+ }
782
+ if ("props" in node && node.props !== void 0 && !isPlainObject(node.props)) {
783
+ return `${path}.props must be an object`;
784
+ }
785
+ if ("children" in node && node.children !== void 0) {
786
+ if (typeof node.children === "string") {
787
+ return null;
788
+ }
789
+ if (!Array.isArray(node.children)) {
790
+ return `${path}.children must be an array or string`;
791
+ }
792
+ for (const [index, child] of node.children.entries()) {
793
+ if (!isPlainObject(child)) continue;
794
+ const childError = validateNodeShape(
795
+ child,
796
+ `${path}.children[${index}]`
797
+ );
798
+ if (childError) return childError;
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+ function validateSpecShape(spec) {
804
+ if (!isPlainObject(spec)) {
805
+ return "spec must be an object";
806
+ }
807
+ if ("root" in spec && spec.root !== void 0 && typeof spec.root !== "string") {
808
+ return "spec.root must be a string";
809
+ }
810
+ if ("metadata" in spec && spec.metadata !== void 0 && !isPlainObject(spec.metadata)) {
811
+ return "spec.metadata must be an object";
812
+ }
813
+ if ("nodes" in spec && spec.nodes !== void 0) {
814
+ if (!Array.isArray(spec.nodes)) {
815
+ return "spec.nodes must be an array";
816
+ }
817
+ for (const [index, node] of spec.nodes.entries()) {
818
+ const error = validateNodeShape(node, `spec.nodes[${index}]`);
819
+ if (error) return error;
820
+ }
821
+ }
822
+ return null;
823
+ }
824
+ function getSelectionNames(effectiveComponents, selection) {
825
+ const selections = Array.isArray(selection) ? selection : [selection];
826
+ return Array.from(
827
+ new Set(
828
+ effectiveComponents.filter((entry) => selections.includes(entry.selection)).map((entry) => entry.component.name)
829
+ )
830
+ );
831
+ }
832
+ function normalizeTokens(value) {
833
+ return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
834
+ }
835
+ function buildComponentText(component) {
836
+ return [
837
+ component.name,
838
+ component.category,
839
+ component.description,
840
+ component.guidance.usageGuidance,
841
+ ...component.guidance.when,
842
+ ...component.guidance.whenNot,
843
+ ...component.guidance.dos,
844
+ ...component.guidance.donts
845
+ ].filter(Boolean).join(" ");
846
+ }
847
+ function rankReplacementCandidates(args) {
848
+ const nodePropKeys = Object.keys(args.node.props ?? {});
849
+ const sourcePropKeys = new Set(Object.keys(args.source.component.props ?? {}));
850
+ const sourceTokens = new Set(normalizeTokens(buildComponentText(args.source.component)));
851
+ return args.candidates.map((candidate) => {
852
+ let score = 0;
853
+ const reasons = [];
854
+ const candidatePropKeys = new Set(Object.keys(candidate.component.props ?? {}));
855
+ if (candidate.selection === "preferred") {
856
+ score += 12;
857
+ reasons.push("preferred selection");
858
+ }
859
+ if (candidate.component.isCanonical) {
860
+ score += 6;
861
+ reasons.push("canonical component");
862
+ }
863
+ if (candidate.component.tier === "core") {
864
+ score += 4;
865
+ reasons.push("core tier");
866
+ }
867
+ if (candidate.isActive) {
868
+ score += 3;
869
+ reasons.push("active component");
870
+ }
871
+ const supportedNodeProps = nodePropKeys.filter(
872
+ (key) => candidatePropKeys.has(key)
873
+ );
874
+ if (supportedNodeProps.length > 0) {
875
+ score += supportedNodeProps.length * 5;
876
+ reasons.push(`supports node props: ${supportedNodeProps.join(", ")}`);
877
+ }
878
+ const missingNodeProps = nodePropKeys.filter(
879
+ (key) => !candidatePropKeys.has(key)
880
+ );
881
+ if (missingNodeProps.length > 0) {
882
+ score -= missingNodeProps.length * 6;
883
+ reasons.push(`missing node props: ${missingNodeProps.join(", ")}`);
884
+ }
885
+ const sharedContractProps = [...sourcePropKeys].filter(
886
+ (key) => candidatePropKeys.has(key)
887
+ );
888
+ if (sharedContractProps.length > 0) {
889
+ score += sharedContractProps.length * 2;
890
+ reasons.push(`shares source props: ${sharedContractProps.join(", ")}`);
891
+ }
892
+ const candidateTokens = new Set(
893
+ normalizeTokens(buildComponentText(candidate.component))
894
+ );
895
+ const tokenOverlap = [...candidateTokens].filter(
896
+ (token) => sourceTokens.has(token)
897
+ );
898
+ if (tokenOverlap.length > 0) {
899
+ score += Math.min(tokenOverlap.length, 4);
900
+ reasons.push(`guidance overlap: ${tokenOverlap.join(", ")}`);
901
+ }
902
+ if (candidate.component.parentComponentId && candidate.component.parentComponentId === args.source.component.parentComponentId) {
903
+ score += 2;
904
+ reasons.push("matches parent component");
905
+ }
906
+ return {
907
+ name: candidate.component.name,
908
+ score,
909
+ reasons
910
+ };
911
+ }).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
912
+ }
913
+ function chooseDeterministicCandidate(rankedCandidates) {
914
+ if (rankedCandidates.length === 0) return null;
915
+ if (rankedCandidates.length === 1) return rankedCandidates[0].name;
916
+ const [first, second] = rankedCandidates;
917
+ if (first.score <= 0) return null;
918
+ if (first.score - second.score < 4) return null;
919
+ return first.name;
920
+ }
921
+ function countViolations(verdict) {
922
+ return verdict.results.reduce(
923
+ (sum, result2) => sum + result2.violations.length,
924
+ 0
925
+ );
926
+ }
927
+ function getNextAction(status, replacements, unresolvedAmbiguityCount) {
928
+ if (status === "pass") return "none";
929
+ if (status === "fixed") return "apply_fixed_spec";
930
+ if (status === "partial_fix") return "review_partial_fix";
931
+ if (unresolvedAmbiguityCount > 0) return "resolve_ambiguities";
932
+ if (replacements.length > 0) return "review_partial_fix";
933
+ return "revise_input";
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
+ }
988
+ async function emitValidateAndFixTelemetry(args) {
989
+ if (!args.ctx.mcp?.server) return;
990
+ try {
991
+ await args.ctx.mcp.server.sendLoggingMessage({
992
+ level: "info",
993
+ data: JSON.stringify({
994
+ tool: "validate_and_fix",
995
+ status: args.status,
996
+ nextAction: args.nextAction,
997
+ resolutionPath: args.resolutionPath,
998
+ evaluation: args.evaluation,
999
+ attestation: args.attestation
1000
+ }),
1001
+ logger: "fragments-mcp"
1002
+ });
1003
+ } catch {
1004
+ }
1005
+ }
1006
+ function buildAllowedComponents(ctx) {
1007
+ return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
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
+ }
1017
+ function runGovern(spec, ctx, policyOverrides) {
1018
+ return runSpecGovern(spec, {
1019
+ allowedComponents: buildAllowedComponents(ctx),
1020
+ tokenPrefix: ctx.data.tokens?.prefix,
1021
+ policy: policyOverrides,
1022
+ componentProps: buildComponentProps2(ctx)
1023
+ });
1024
+ }
1025
+ function walkNodes2(nodes, visitor, path = "nodes") {
1026
+ for (const [index, node] of nodes.entries()) {
1027
+ if (!isPlainObject(node)) continue;
1028
+ const nodeRef = `${path}[${index}]`;
1029
+ visitor(node, nodeRef);
1030
+ if (Array.isArray(node.children)) {
1031
+ walkNodes2(node.children, visitor, `${nodeRef}.children`);
1032
+ }
1033
+ }
1034
+ }
1035
+ function applyDeterministicReplacements(spec, ctx) {
1036
+ const effectiveComponents = buildEffectiveComponents(ctx);
1037
+ const byName = /* @__PURE__ */ new Map();
1038
+ const preferredByCategory = /* @__PURE__ */ new Map();
1039
+ for (const entry of effectiveComponents) {
1040
+ const list = byName.get(entry.component.name) ?? [];
1041
+ list.push(entry);
1042
+ byName.set(entry.component.name, list);
1043
+ if (entry.selection !== "preferred") continue;
1044
+ const category = entry.component.category ?? "uncategorized";
1045
+ const names = preferredByCategory.get(category) ?? [];
1046
+ if (!names.includes(entry.component.name)) {
1047
+ names.push(entry.component.name);
1048
+ }
1049
+ preferredByCategory.set(category, names);
1050
+ }
1051
+ const fixedSpec = cloneSpec(spec);
1052
+ const nodes = Array.isArray(fixedSpec.nodes) ? fixedSpec.nodes : [];
1053
+ const replacements = [];
1054
+ const ambiguities = [];
1055
+ walkNodes2(nodes, (node, nodeRef) => {
1056
+ if (!node.type) return;
1057
+ const componentEntries = byName.get(node.type) ?? [];
1058
+ const component = componentEntries[0];
1059
+ if (!component || component.selection !== "discouraged" && component.selection !== "forbidden") {
1060
+ return;
1061
+ }
1062
+ const candidateEntries = (preferredByCategory.get(component.component.category ?? "uncategorized") ?? []).filter((candidate) => candidate !== component.component.name).map(
1063
+ (candidateName) => effectiveComponents.find(
1064
+ (entry) => entry.component.name === candidateName
1065
+ )
1066
+ ).filter((entry) => Boolean(entry));
1067
+ const rankedCandidates = rankReplacementCandidates({
1068
+ source: component,
1069
+ candidates: candidateEntries,
1070
+ node
1071
+ });
1072
+ const replacement = chooseDeterministicCandidate(rankedCandidates);
1073
+ if (!replacement) {
1074
+ if (rankedCandidates.length > 1) {
1075
+ ambiguities.push({
1076
+ nodeId: node.id ?? node.type,
1077
+ nodeRef,
1078
+ from: component.component.name,
1079
+ candidates: rankedCandidates.map((candidate) => candidate.name),
1080
+ rankedCandidates,
1081
+ reason: "ambiguous_preferred_components"
1082
+ });
1083
+ }
1084
+ return;
1085
+ }
1086
+ replacements.push({
1087
+ nodeId: node.id ?? node.type,
1088
+ from: component.component.name,
1089
+ to: replacement,
1090
+ reason: "deprecated_component",
1091
+ mode: "deterministic"
1092
+ });
1093
+ node.type = replacement;
1094
+ });
1095
+ return {
1096
+ fixedSpec,
1097
+ replacements,
1098
+ ambiguities
1099
+ };
1100
+ }
1101
+ function applyReplacementAtRef(spec, nodeRef, replacement) {
1102
+ const nodes = Array.isArray(spec.nodes) ? spec.nodes : [];
1103
+ let applied = false;
1104
+ walkNodes2(nodes, (node, currentRef) => {
1105
+ if (applied || currentRef !== nodeRef) return;
1106
+ node.type = replacement;
1107
+ applied = true;
1108
+ });
1109
+ return applied;
1110
+ }
1111
+ function supportsFormElicitation(ctx) {
1112
+ const capabilities = ctx.mcp?.clientCapabilities?.elicitation;
1113
+ if (!capabilities) return false;
1114
+ return capabilities.form !== void 0 || capabilities.url === void 0;
1115
+ }
1116
+ function supportsSampling(ctx) {
1117
+ return Boolean(ctx.mcp?.clientCapabilities?.sampling);
1118
+ }
1119
+ function getSamplingText(response) {
1120
+ if (Array.isArray(response.content)) {
1121
+ return response.content.find((item) => item.type === "text")?.text;
1122
+ }
1123
+ return response.content.type === "text" ? response.content.text : void 0;
1124
+ }
1125
+ function buildComponentGuidanceMap(ctx) {
1126
+ return new Map(
1127
+ Object.values(ctx.data.components).map((component) => {
1128
+ const guidance = [];
1129
+ if (component.description) {
1130
+ guidance.push(`description: ${component.description}`);
1131
+ }
1132
+ if (component.guidance.usageGuidance) {
1133
+ guidance.push(`usage: ${component.guidance.usageGuidance}`);
1134
+ }
1135
+ if (component.guidance.dos.length > 0) {
1136
+ guidance.push(`dos: ${component.guidance.dos.join("; ")}`);
1137
+ }
1138
+ if (component.guidance.donts.length > 0) {
1139
+ guidance.push(`donts: ${component.guidance.donts.join("; ")}`);
1140
+ }
1141
+ return [component.name, guidance];
1142
+ })
1143
+ );
1144
+ }
1145
+ function buildAmbiguityGuidanceLines(candidates, guidanceByName) {
1146
+ return candidates.map((candidate) => {
1147
+ const guidance = guidanceByName.get(candidate) ?? [];
1148
+ return guidance.length > 0 ? `${candidate}: ${guidance.join(" | ")}` : `${candidate}: no additional guidance available`;
1149
+ });
1150
+ }
1151
+ async function resolveAmbiguitiesWithElicitation(ambiguities, spec, ctx) {
1152
+ if (!ctx.mcp?.server || ambiguities.length === 0) return [];
1153
+ const replacements = [];
1154
+ const guidanceByName = buildComponentGuidanceMap(ctx);
1155
+ for (const ambiguity of ambiguities) {
1156
+ const guidanceLines = buildAmbiguityGuidanceLines(
1157
+ ambiguity.candidates,
1158
+ guidanceByName
1159
+ );
1160
+ const result2 = await ctx.mcp.server.elicitInput({
1161
+ mode: "form",
1162
+ message: [
1163
+ `Fragments found multiple safe replacements for ${ambiguity.from} on node ${ambiguity.nodeId}.`,
1164
+ "Choose the best replacement, or skip this change.",
1165
+ "Candidate guidance:",
1166
+ ...guidanceLines
1167
+ ].join("\n"),
1168
+ requestedSchema: {
1169
+ type: "object",
1170
+ properties: {
1171
+ replacement: {
1172
+ type: "string",
1173
+ title: "Replacement",
1174
+ description: [
1175
+ "Select the best replacement for this node.",
1176
+ ...guidanceLines,
1177
+ "Choose Skip to leave this component unchanged."
1178
+ ].join("\n"),
1179
+ oneOf: [
1180
+ ...ambiguity.candidates.map((candidate) => ({
1181
+ const: candidate,
1182
+ title: candidate
1183
+ })),
1184
+ {
1185
+ const: "__skip__",
1186
+ title: "Skip"
1187
+ }
1188
+ ],
1189
+ default: "__skip__"
1190
+ }
1191
+ },
1192
+ required: ["replacement"]
1193
+ }
1194
+ });
1195
+ if (result2.action !== "accept" || !result2.content || typeof result2.content.replacement !== "string" || result2.content.replacement === "__skip__") {
1196
+ continue;
1197
+ }
1198
+ if (!ambiguity.candidates.includes(result2.content.replacement)) {
1199
+ continue;
1200
+ }
1201
+ if (!applyReplacementAtRef(spec, ambiguity.nodeRef, result2.content.replacement)) {
1202
+ continue;
1203
+ }
1204
+ replacements.push({
1205
+ nodeId: ambiguity.nodeId,
1206
+ from: ambiguity.from,
1207
+ to: result2.content.replacement,
1208
+ reason: "deprecated_component",
1209
+ mode: "elicitation"
1210
+ });
1211
+ }
1212
+ return replacements;
1213
+ }
1214
+ async function resolveAmbiguitiesWithSampling(ambiguities, spec, ctx) {
1215
+ if (!ctx.mcp?.server || ambiguities.length === 0) return [];
1216
+ const guidanceByName = buildComponentGuidanceMap(ctx);
1217
+ const prompt = [
1218
+ "You are choosing design-system component replacements.",
1219
+ 'Return strict JSON only in the form {"choices":[{"nodeId":"...","replacement":"...|__skip__"}]}.',
1220
+ "Only choose from the provided candidates.",
1221
+ "Prefer the option whose guidance best matches the original component intent.",
1222
+ JSON.stringify(
1223
+ {
1224
+ ambiguities: ambiguities.map((ambiguity) => ({
1225
+ nodeId: ambiguity.nodeId,
1226
+ from: ambiguity.from,
1227
+ candidates: ambiguity.candidates.map((candidate) => ({
1228
+ name: candidate,
1229
+ guidance: guidanceByName.get(candidate) ?? []
1230
+ }))
1231
+ }))
1232
+ },
1233
+ null,
1234
+ 2
1235
+ )
1236
+ ].join("\n\n");
1237
+ const response = await ctx.mcp.server.createMessage({
1238
+ messages: [
1239
+ {
1240
+ role: "user",
1241
+ content: {
1242
+ type: "text",
1243
+ text: prompt
1244
+ }
1245
+ }
1246
+ ],
1247
+ maxTokens: 600
1248
+ });
1249
+ const text = getSamplingText(response);
1250
+ if (!text) return [];
1251
+ let parsed;
1252
+ try {
1253
+ parsed = JSON.parse(text);
1254
+ } catch {
1255
+ return [];
1256
+ }
1257
+ const replacements = [];
1258
+ for (const choice of parsed.choices ?? []) {
1259
+ if (!choice.nodeId || !choice.replacement || choice.replacement === "__skip__") {
1260
+ continue;
1261
+ }
1262
+ const ambiguity = ambiguities.find((entry) => entry.nodeId === choice.nodeId);
1263
+ if (!ambiguity || !ambiguity.candidates.includes(choice.replacement)) {
1264
+ continue;
1265
+ }
1266
+ if (!applyReplacementAtRef(spec, ambiguity.nodeRef, choice.replacement)) {
1267
+ continue;
1268
+ }
1269
+ replacements.push({
1270
+ nodeId: ambiguity.nodeId,
1271
+ from: ambiguity.from,
1272
+ to: choice.replacement,
1273
+ reason: "deprecated_component",
1274
+ mode: "sampling"
1275
+ });
1276
+ }
1277
+ return replacements;
1278
+ }
1279
+ function buildSummary(args) {
1280
+ const lines = [
1281
+ `status: ${args.status}`,
1282
+ `original: ${formatVerdict(args.originalVerdict, "summary")}`
1283
+ ];
1284
+ if (args.replacements.length > 0) {
1285
+ lines.push(
1286
+ `replacements: ${args.replacements.map((item) => `${item.from} -> ${item.to}`).join(", ")}`
1287
+ );
1288
+ lines.push(`final: ${formatVerdict(args.finalVerdict, "summary")}`);
1289
+ }
1290
+ return lines.join("\n");
1291
+ }
1292
+ var validateAndFixHandler = async (args, ctx) => {
1293
+ const spec = args?.spec;
1294
+ if (!spec || typeof spec !== "object") {
1295
+ return {
1296
+ content: [
1297
+ {
1298
+ type: "text",
1299
+ text: JSON.stringify({
1300
+ error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
1301
+ })
1302
+ }
1303
+ ],
1304
+ isError: true
1305
+ };
1306
+ }
1307
+ const specError = validateSpecShape(spec);
1308
+ if (specError) {
1309
+ return {
1310
+ content: [
1311
+ {
1312
+ type: "text",
1313
+ text: JSON.stringify({
1314
+ error: `Invalid spec format: ${specError}. Expected: { nodes: [{ id?: string, type?: string, props?: object, children?: object[] }] }`
1315
+ })
1316
+ }
1317
+ ],
1318
+ isError: true
1319
+ };
1320
+ }
1321
+ const policyOverrides = args?.policy;
1322
+ const applyFixes = args?.applyFixes !== false;
1323
+ const allowElicitation = args?.allowElicitation !== false;
1324
+ const allowSampling = args?.allowSampling !== false;
1325
+ const format = args?.format ?? "json";
1326
+ const startedAt = Date.now();
1327
+ try {
1328
+ const effectiveComponents = buildEffectiveComponents(ctx);
1329
+ const originalVerdict = await runGovern(spec, ctx, policyOverrides);
1330
+ let finalVerdict = originalVerdict;
1331
+ let workingSpec;
1332
+ let fixedSpec;
1333
+ let replacements = [];
1334
+ let ambiguities = [];
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
+ }
1342
+ if (!originalVerdict.passed && applyFixes) {
1343
+ const result2 = preview ?? applyDeterministicReplacements(spec, ctx);
1344
+ replacements = result2.replacements;
1345
+ ambiguities = result2.ambiguities;
1346
+ if (replacements.length > 0) {
1347
+ workingSpec = result2.fixedSpec;
1348
+ resolutionPath.push("deterministic");
1349
+ }
1350
+ const unresolvedAmbiguities2 = ambiguities.filter(
1351
+ (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
1352
+ );
1353
+ if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsElicitation) {
1354
+ workingSpec ??= cloneSpec(spec);
1355
+ const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
1356
+ unresolvedAmbiguities2,
1357
+ workingSpec,
1358
+ ctx
1359
+ );
1360
+ if (elicitedReplacements.length > 0) {
1361
+ resolutionPath.push("elicitation");
1362
+ }
1363
+ replacements.push(...elicitedReplacements);
1364
+ }
1365
+ const remainingAmbiguities = ambiguities.filter(
1366
+ (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
1367
+ );
1368
+ if (remainingAmbiguities.length > 0 && allowSampling && supportsModelSampling) {
1369
+ workingSpec ??= cloneSpec(spec);
1370
+ const sampledReplacements = await resolveAmbiguitiesWithSampling(
1371
+ remainingAmbiguities,
1372
+ workingSpec,
1373
+ ctx
1374
+ );
1375
+ if (sampledReplacements.length > 0) {
1376
+ resolutionPath.push("sampling");
1377
+ }
1378
+ replacements.push(...sampledReplacements);
1379
+ }
1380
+ fixedSpec = replacements.length > 0 ? workingSpec : void 0;
1381
+ if (fixedSpec) {
1382
+ finalVerdict = await runGovern(fixedSpec, ctx, policyOverrides);
1383
+ }
1384
+ }
1385
+ const status = originalVerdict.passed ? "pass" : replacements.length > 0 ? finalVerdict.passed ? "fixed" : "partial_fix" : "fail";
1386
+ const unresolvedAmbiguities = ambiguities.filter(
1387
+ (ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
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
+ });
1401
+ const attestation = {
1402
+ sourceType: ctx.data.snapshot.sourceType,
1403
+ sourceLabel: ctx.data.snapshot.sourceLabel,
1404
+ designSystemName: ctx.data.snapshot.metadata.designSystemName,
1405
+ packageName: ctx.data.snapshot.metadata.packageName,
1406
+ importPath: ctx.data.snapshot.metadata.importPath,
1407
+ catalogRevision: ctx.data.validateFixContext?.catalogRevision ?? ctx.data.snapshot.metadata.revision,
1408
+ catalogUpdatedAt: ctx.data.validateFixContext?.updatedAt ?? ctx.data.snapshot.metadata.updatedAt,
1409
+ policy: {
1410
+ mode: ctx.data.validateFixContext?.policy.mode ?? "embedded",
1411
+ endpoint: ctx.data.validateFixContext?.policy.endpoint,
1412
+ overrideApplied: Boolean(policyOverrides)
1413
+ },
1414
+ clientCapabilities: {
1415
+ sampling: supportsModelSampling,
1416
+ samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
1417
+ elicitationForm: supportsElicitation,
1418
+ roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
1419
+ },
1420
+ capabilitiesUsed: {
1421
+ deterministic: replacements.some((replacement) => replacement.mode === "deterministic"),
1422
+ elicitation: replacements.some((replacement) => replacement.mode === "elicitation"),
1423
+ sampling: replacements.some((replacement) => replacement.mode === "sampling")
1424
+ }
1425
+ };
1426
+ const evaluation = {
1427
+ durationMs: Date.now() - startedAt,
1428
+ originalViolationCount: countViolations(originalVerdict),
1429
+ finalViolationCount: countViolations(finalVerdict),
1430
+ originalPassed: originalVerdict.passed,
1431
+ finalPassed: finalVerdict.passed,
1432
+ replacementCount: replacements.length,
1433
+ replacementsByMode: {
1434
+ deterministic: replacements.filter((replacement) => replacement.mode === "deterministic").length,
1435
+ elicitation: replacements.filter((replacement) => replacement.mode === "elicitation").length,
1436
+ sampling: replacements.filter((replacement) => replacement.mode === "sampling").length
1437
+ },
1438
+ ambiguityCount: ambiguities.length,
1439
+ unresolvedAmbiguityCount: unresolvedAmbiguities.length,
1440
+ candidateRankingUsed: true
1441
+ };
1442
+ const nextAction = getNextAction(
1443
+ status,
1444
+ replacements,
1445
+ unresolvedAmbiguities.length
1446
+ );
1447
+ const catalogMeta = getCatalogMeta(ctx.data);
1448
+ const payload = {
1449
+ status,
1450
+ nextAction,
1451
+ applyFixes,
1452
+ allowElicitation,
1453
+ allowSampling,
1454
+ resolutionPath,
1455
+ replacements,
1456
+ originalVerdict,
1457
+ finalVerdict,
1458
+ ...fixedSpec ? { fixedSpec } : {},
1459
+ attestation,
1460
+ evaluation,
1461
+ preferredComponents: getSelectionNames(
1462
+ effectiveComponents,
1463
+ "preferred"
1464
+ ),
1465
+ discouragedComponents: getSelectionNames(
1466
+ effectiveComponents,
1467
+ "discouraged"
1468
+ ),
1469
+ forbiddenComponents: getSelectionNames(
1470
+ effectiveComponents,
1471
+ "forbidden"
1472
+ ),
1473
+ unresolvedAmbiguities,
1474
+ wouldFixIfAuthorized
1475
+ };
1476
+ await emitValidateAndFixTelemetry({
1477
+ ctx,
1478
+ status,
1479
+ resolutionPath,
1480
+ evaluation,
1481
+ attestation,
1482
+ nextAction
1483
+ });
1484
+ return {
1485
+ content: [
1486
+ {
1487
+ type: "text",
1488
+ text: format === "summary" ? buildSummary({
1489
+ status,
1490
+ originalVerdict,
1491
+ finalVerdict,
1492
+ replacements
1493
+ }) : JSON.stringify(payload)
1494
+ }
1495
+ ],
1496
+ _meta: {
1497
+ ...catalogMeta,
1498
+ status,
1499
+ nextAction,
1500
+ replacementCount: replacements.length,
1501
+ passed: finalVerdict.passed,
1502
+ unresolvedAmbiguityCount: unresolvedAmbiguities.length,
1503
+ wouldFixIfAuthorizedCount: wouldFixIfAuthorized.length,
1504
+ resolutionPath
1505
+ }
1506
+ };
1507
+ } catch (error) {
1508
+ const message = error instanceof Error ? error.message : String(error);
1509
+ return {
1510
+ content: [
1511
+ {
1512
+ type: "text",
1513
+ text: JSON.stringify({
1514
+ error: message
1515
+ })
1516
+ }
1517
+ ],
1518
+ isError: true
1519
+ };
1520
+ }
1521
+ };
1522
+
1523
+ // src/cloud-http.ts
1524
+ var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
1525
+ function normalizeCloudUrl(url) {
1526
+ if (!url) return DEFAULT_CLOUD_URL;
1527
+ return url.replace(/\/+$/, "");
1528
+ }
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
+ }
1537
+ const response = await fetch(url.toString(), {
1538
+ headers: { "X-API-Key": args.apiKey }
1539
+ });
1540
+ if (!response.ok) {
1541
+ const body = await response.text();
1542
+ let message;
1543
+ try {
1544
+ const parsed = JSON.parse(body);
1545
+ message = parsed.error ?? body;
1546
+ } catch {
1547
+ message = body;
1548
+ }
1549
+ throw new Error(
1550
+ `Cloud ${args.resource} API error (${response.status}): ${message}`
1551
+ );
1552
+ }
1553
+ return await response.json();
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
+ }
1573
+ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
1574
+ return fetchFindings(
1575
+ apiKey,
1576
+ { status: "open", filePath, limit: 200 },
1577
+ cloudUrl
1578
+ );
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
+ }
1631
+
1632
+ // src/tools/cloud-auth.ts
1633
+ function resolveCloudApiKey(ctx) {
1634
+ return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
1635
+ }
1636
+ function resolveCloudUrl(ctx) {
1637
+ return ctx.config.fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
1638
+ }
1639
+ function missingKeyError() {
1640
+ return {
1641
+ content: [
1642
+ {
1643
+ type: "text",
1644
+ text: JSON.stringify({
1645
+ error: "Cloud API key required. Set FRAGMENTS_API_KEY env var, pass --cloud-api-key, or configure cloud.apiKey in ds-mcp.config.json."
1646
+ })
1647
+ }
1648
+ ],
1649
+ isError: true
1650
+ };
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
+ }
1684
+ var findingsListHandler = async (args, ctx) => {
1685
+ const apiKey = resolveCloudApiKey(ctx);
1686
+ if (!apiKey) return missingKeyError();
1687
+ const cloudUrl = resolveCloudUrl(ctx);
1688
+ const params = {};
1689
+ if (args.status) params.status = args.status;
1690
+ if (args.severity)
1691
+ params.severity = args.severity;
1692
+ if (args.category) params.category = String(args.category);
1693
+ if (args.ruleId) params.ruleId = String(args.ruleId);
1694
+ if (args.filePath) params.filePath = String(args.filePath);
1695
+ if (args.limit != null) params.limit = Number(args.limit);
1696
+ try {
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);
1744
+ return {
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
+ }
1757
+ };
1758
+ } catch (error) {
1759
+ return {
1760
+ content: [
1761
+ {
1762
+ type: "text",
1763
+ text: JSON.stringify({
1764
+ error: error instanceof Error ? error.message : String(error)
1765
+ })
1766
+ }
1767
+ ],
1768
+ isError: true
1769
+ };
1770
+ }
1771
+ };
1772
+ var findingsForFileHandler = async (args, ctx) => {
1773
+ const apiKey = resolveCloudApiKey(ctx);
1774
+ if (!apiKey) return missingKeyError();
1775
+ const filePath = args.filePath;
1776
+ if (!filePath || typeof filePath !== "string") {
1777
+ return {
1778
+ content: [
1779
+ {
1780
+ type: "text",
1781
+ text: JSON.stringify({ error: "filePath is required." })
1782
+ }
1783
+ ],
1784
+ isError: true
1785
+ };
1786
+ }
1787
+ const cloudUrl = resolveCloudUrl(ctx);
1788
+ try {
1789
+ const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
1790
+ const findings = enrichFindings(
1791
+ result2.findings.filter((f) => f.filePath === filePath),
1792
+ ctx
1793
+ );
1794
+ const catalogMeta = getCatalogMeta(ctx.data);
1795
+ return {
1796
+ content: [
1797
+ {
1798
+ type: "text",
1799
+ text: JSON.stringify({ findings, filePath })
1800
+ }
1801
+ ],
1802
+ _meta: {
1803
+ ...catalogMeta,
1804
+ count: findings.length,
1805
+ filePath,
1806
+ tokenSuggestionCount: findings.filter(
1807
+ (finding) => finding.suggestedTokenDetails
1808
+ ).length
1809
+ }
1810
+ };
1811
+ } catch (error) {
1812
+ return {
1813
+ content: [
1814
+ {
1815
+ type: "text",
1816
+ text: JSON.stringify({
1817
+ error: error instanceof Error ? error.message : String(error)
1818
+ })
1819
+ }
1820
+ ],
1821
+ isError: true
1822
+ };
1823
+ }
1824
+ };
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
+
2011
+ // src/tools/index.ts
2012
+ var CORE_TOOLS = {
2013
+ "tokens.suggest": tokensSuggestHandler,
2014
+ govern: governHandler,
2015
+ validate_and_fix: validateAndFixHandler,
2016
+ findings_list: findingsListHandler,
2017
+ findings_summary: findingsSummaryHandler,
2018
+ findings_for_file: findingsForFileHandler,
2019
+ swap_to_canonical: swapToCanonicalHandler
2020
+ };
2021
+ var VIEWER_TOOLS = {};
2022
+ var INFRA_TOOLS = {};
2023
+ var BUILTIN_TOOLS = {
2024
+ ...CORE_TOOLS,
2025
+ ...VIEWER_TOOLS,
2026
+ ...INFRA_TOOLS
2027
+ };
2028
+ var TOOL_CAPABILITIES = {
2029
+ "tokens.suggest": ["tokens"],
2030
+ validate_and_fix: ["components"]
2031
+ };
2032
+
2033
+ // src/registry.ts
2034
+ var ToolRegistry = class {
2035
+ tools = /* @__PURE__ */ new Map();
2036
+ prefix;
2037
+ onChanged;
2038
+ constructor(prefix, opts) {
2039
+ this.prefix = prefix;
2040
+ this.onChanged = opts?.onChanged;
2041
+ }
2042
+ /** Register a single tool */
2043
+ register(key, handler, definition, availability = "always", requiredCapabilities = []) {
2044
+ this.tools.set(key, {
2045
+ key,
2046
+ handler,
2047
+ definition,
2048
+ availability,
2049
+ requiredCapabilities
2050
+ });
2051
+ this.onChanged?.();
2052
+ }
2053
+ /** Unregister a tool by key */
2054
+ unregister(key) {
2055
+ if (this.tools.delete(key)) {
2056
+ this.onChanged?.();
2057
+ }
2058
+ }
2059
+ /** Bulk register built-in tools with availability metadata */
2060
+ registerBuiltins(toolSets, definitions, capabilityByKey = {}) {
2061
+ const defMap = new Map(definitions.map((d) => [d.key, d]));
2062
+ for (const [key, handler] of Object.entries(toolSets.core)) {
2063
+ this.tools.set(key, {
2064
+ key,
2065
+ handler,
2066
+ definition: defMap.get(key),
2067
+ availability: "always",
2068
+ requiredCapabilities: capabilityByKey[key] ?? []
2069
+ });
2070
+ }
2071
+ for (const [key, handler] of Object.entries(toolSets.viewer)) {
2072
+ this.tools.set(key, {
2073
+ key,
2074
+ handler,
2075
+ definition: defMap.get(key),
2076
+ availability: "viewer",
2077
+ requiredCapabilities: capabilityByKey[key] ?? []
2078
+ });
2079
+ }
2080
+ for (const [key, handler] of Object.entries(toolSets.infra)) {
2081
+ this.tools.set(key, {
2082
+ key,
2083
+ handler,
2084
+ definition: defMap.get(key),
2085
+ availability: "playground",
2086
+ requiredCapabilities: capabilityByKey[key] ?? []
2087
+ });
2088
+ }
2089
+ }
2090
+ /** Get handler for a tool (by unprefixed key or prefixed name) */
2091
+ getHandler(nameOrKey) {
2092
+ const direct = this.tools.get(nameOrKey);
2093
+ if (direct) return direct.handler;
2094
+ if (!this.prefix) return void 0;
2095
+ const prefixStr = this.prefix + "_";
2096
+ if (nameOrKey.startsWith(prefixStr)) {
2097
+ const key = nameOrKey.slice(prefixStr.length);
2098
+ return this.tools.get(key)?.handler;
2099
+ }
2100
+ return void 0;
2101
+ }
2102
+ /** Resolve unprefixed key from a potentially prefixed tool name */
2103
+ resolveKey(name) {
2104
+ if (!this.prefix) return name;
2105
+ const prefixStr = this.prefix + "_";
2106
+ return name.startsWith(prefixStr) ? name.slice(prefixStr.length) : name;
2107
+ }
2108
+ /** List available tools as MCP Tool[] based on current availability context */
2109
+ listTools(ctx, allToolSchemas) {
2110
+ const availableKeys = /* @__PURE__ */ new Set();
2111
+ for (const [key, tool] of this.tools) {
2112
+ const hasCapabilities = tool.requiredCapabilities.every(
2113
+ (capability) => ctx.capabilities.has(capability)
2114
+ );
2115
+ if (!hasCapabilities) continue;
2116
+ if (tool.availability === "always") {
2117
+ availableKeys.add(key);
2118
+ } else if (tool.availability === "viewer" && ctx.hasViewer) {
2119
+ availableKeys.add(key);
2120
+ } else if (tool.availability === "playground" && ctx.hasPlayground) {
2121
+ availableKeys.add(key);
2122
+ }
2123
+ }
2124
+ if (!this.prefix) {
2125
+ return allToolSchemas.filter((t) => availableKeys.has(t.name));
2126
+ }
2127
+ const prefixStr = this.prefix + "_";
2128
+ return allToolSchemas.filter((t) => {
2129
+ const key = t.name.startsWith(prefixStr) ? t.name.slice(prefixStr.length) : t.name;
2130
+ return availableKeys.has(key);
2131
+ });
2132
+ }
2133
+ /** Execute a tool by name, dispatching to its handler */
2134
+ async execute(name, args, ctx) {
2135
+ const key = this.resolveKey(name);
2136
+ const registered = this.tools.get(key);
2137
+ if (!registered) {
2138
+ throw new Error(`Unknown tool: ${name}`);
2139
+ }
2140
+ return registered.handler(args, ctx);
2141
+ }
2142
+ /** Get the number of registered tools */
2143
+ get size() {
2144
+ return this.tools.size;
2145
+ }
2146
+ /** Get all registered keys */
2147
+ keys() {
2148
+ return Array.from(this.tools.keys());
2149
+ }
2150
+ };
2151
+
2152
+ // src/middleware.ts
2153
+ function executeWithMiddleware(middlewares, mCtx, handler) {
2154
+ const chain = [...middlewares].reverse().reduce(
2155
+ (next, mw) => () => mw(mCtx, next),
2156
+ handler
2157
+ );
2158
+ return chain();
2159
+ }
2160
+ function telemetryMiddleware(logger) {
2161
+ return async (mCtx, next) => {
2162
+ const start = Date.now();
2163
+ const result2 = await next();
2164
+ logger(`${mCtx.toolKey}: ${Date.now() - start}ms`);
2165
+ return result2;
2166
+ };
2167
+ }
2168
+
2169
+ // src/source-selection.ts
2170
+ import { existsSync as existsSync7 } from "fs";
2171
+ import { join as join6 } from "path";
2172
+
2173
+ // src/adapters/fragments-json.ts
2174
+ import { readFile } from "fs/promises";
2175
+
2176
+ // src/discovery.ts
2177
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
2178
+ import { join as join2, dirname, resolve } from "path";
2179
+ import { createRequire } from "module";
2180
+ function resolveWorkspaceGlob(baseDir, pattern) {
2181
+ const parts = pattern.split("/");
2182
+ let dirs = [baseDir];
2183
+ for (const part of parts) {
2184
+ if (part === "**") continue;
2185
+ const next = [];
2186
+ for (const d of dirs) {
2187
+ if (part === "*") {
2188
+ try {
2189
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
2190
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
2191
+ next.push(join2(d, entry.name));
2192
+ }
2193
+ }
2194
+ } catch {
2195
+ }
2196
+ } else {
2197
+ const candidate = join2(d, part);
2198
+ if (existsSync2(candidate)) next.push(candidate);
2199
+ }
2200
+ }
2201
+ dirs = next;
2202
+ }
2203
+ return dirs;
2204
+ }
2205
+ function getWorkspaceDirs(rootDir) {
2206
+ const dirs = [];
2207
+ const rootPkgPath = join2(rootDir, "package.json");
2208
+ if (existsSync2(rootPkgPath)) {
2209
+ try {
2210
+ const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
2211
+ const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
2212
+ if (Array.isArray(workspaces)) {
2213
+ for (const pattern of workspaces) {
2214
+ dirs.push(...resolveWorkspaceGlob(rootDir, pattern));
2215
+ }
2216
+ return dirs;
2217
+ }
2218
+ } catch {
2219
+ }
2220
+ }
2221
+ const pnpmWsPath = join2(rootDir, "pnpm-workspace.yaml");
2222
+ if (existsSync2(pnpmWsPath)) {
2223
+ try {
2224
+ const content = readFileSync3(pnpmWsPath, "utf-8");
2225
+ const lines = content.split("\n");
2226
+ let inPackages = false;
2227
+ for (const line of lines) {
2228
+ if (/^packages\s*:/.test(line)) {
2229
+ inPackages = true;
2230
+ continue;
2231
+ }
2232
+ if (inPackages) {
2233
+ const match = line.match(/^\s+-\s+['"]?([^'"#\n]+)['"]?/);
2234
+ if (match) {
2235
+ dirs.push(...resolveWorkspaceGlob(rootDir, match[1].trim()));
2236
+ } else if (/^\S/.test(line) && line.trim()) {
2237
+ break;
2238
+ }
2239
+ }
2240
+ }
2241
+ } catch {
2242
+ }
2243
+ }
2244
+ return dirs;
2245
+ }
2246
+ function resolveDepPackageJson(localRequire, depName) {
2247
+ try {
2248
+ return localRequire.resolve(`${depName}/package.json`);
2249
+ } catch {
2250
+ }
2251
+ try {
2252
+ const mainPath = localRequire.resolve(depName);
2253
+ let dir = dirname(mainPath);
2254
+ while (true) {
2255
+ const candidate = join2(dir, "package.json");
2256
+ if (existsSync2(candidate)) {
2257
+ const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
2258
+ if (pkg.name === depName) return candidate;
2259
+ }
2260
+ const parent = dirname(dir);
2261
+ if (parent === dir) break;
2262
+ dir = parent;
2263
+ }
2264
+ } catch {
2265
+ }
2266
+ return null;
2267
+ }
2268
+ function findFragmentsInDeps(dir, found, depField) {
2269
+ const pkgJsonPath = join2(dir, "package.json");
2270
+ if (!existsSync2(pkgJsonPath)) return;
2271
+ try {
2272
+ const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
2273
+ const allDeps = {
2274
+ ...pkgJson.dependencies,
2275
+ ...pkgJson.devDependencies
2276
+ };
2277
+ const localRequire = createRequire(join2(dir, "noop.js"));
2278
+ for (const depName of Object.keys(allDeps)) {
2279
+ try {
2280
+ const depPkgPath = resolveDepPackageJson(localRequire, depName);
2281
+ if (!depPkgPath) continue;
2282
+ const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
2283
+ if (depPkg[depField]) {
2284
+ const fragmentsPath = join2(dirname(depPkgPath), depPkg[depField]);
2285
+ if (existsSync2(fragmentsPath) && !found.includes(fragmentsPath)) {
2286
+ found.push(fragmentsPath);
2287
+ }
2288
+ }
2289
+ } catch {
2290
+ }
2291
+ }
2292
+ } catch {
2293
+ }
2294
+ }
2295
+ function findDesignSystemJson(startDir, outFile, depField) {
2296
+ const found = [];
2297
+ const resolvedStart = resolve(startDir);
2298
+ let dir = resolvedStart;
2299
+ while (true) {
2300
+ const candidate = join2(dir, outFile);
2301
+ if (existsSync2(candidate)) {
2302
+ found.push(candidate);
2303
+ break;
2304
+ }
2305
+ const parent = dirname(dir);
2306
+ if (parent === dir) break;
2307
+ dir = parent;
2308
+ }
2309
+ findFragmentsInDeps(resolvedStart, found, depField);
2310
+ if (found.length === 0 || existsSync2(join2(resolvedStart, "pnpm-workspace.yaml"))) {
2311
+ const workspaceDirs = getWorkspaceDirs(resolvedStart);
2312
+ for (const wsDir of workspaceDirs) {
2313
+ findFragmentsInDeps(wsDir, found, depField);
2314
+ }
2315
+ }
2316
+ return found;
2317
+ }
2318
+ function findFragmentsJson(startDir) {
2319
+ return findDesignSystemJson(startDir, BRAND.outFile, "fragments");
2320
+ }
2321
+ function findBundleManifest(startDir) {
2322
+ const found = [];
2323
+ let dir = resolve(startDir);
2324
+ while (true) {
2325
+ const candidate = join2(dir, BRAND.dataDir, BRAND.manifestFile);
2326
+ if (existsSync2(candidate)) {
2327
+ found.push(candidate);
2328
+ break;
2329
+ }
2330
+ const parent = dirname(dir);
2331
+ if (parent === dir) break;
2332
+ dir = parent;
2333
+ }
2334
+ return found;
2335
+ }
2336
+
2337
+ // src/adapters/snapshot-converters.ts
2338
+ import { mcpSnapshotSchema } from "@fragments-sdk/core";
2339
+ function slugify(value) {
2340
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
2341
+ }
2342
+ function buildComponentId(args) {
2343
+ const base = args.filePath ?? args.packageName ?? args.name;
2344
+ return `${slugify(args.name)}:${base}`;
2345
+ }
2346
+ function valueToString(value) {
2347
+ if (value === void 0) return void 0;
2348
+ if (typeof value === "string") return value;
2349
+ if (typeof value === "number" || typeof value === "boolean" || value === null) {
2350
+ return String(value);
2351
+ }
2352
+ try {
2353
+ return JSON.stringify(value);
2354
+ } catch {
2355
+ return void 0;
2356
+ }
2357
+ }
2358
+ function buildCompoundChildren(contract, ai) {
2359
+ const contractChildren = Object.entries(contract?.compoundChildren ?? {});
2360
+ if (contractChildren.length > 0) {
2361
+ return contractChildren.map(([childName, child]) => ({
2362
+ name: childName,
2363
+ description: child.description,
2364
+ required: child.required,
2365
+ accepts: child.accepts,
2366
+ visibility: "public"
2367
+ }));
2368
+ }
2369
+ const subs = ai?.subComponents;
2370
+ if (!subs || subs.length === 0) return [];
2371
+ const requiredSet = new Set(ai?.requiredChildren ?? []);
2372
+ return subs.map((name) => ({
2373
+ name,
2374
+ required: requiredSet.has(name) || void 0,
2375
+ visibility: "public"
2376
+ }));
2377
+ }
2378
+ function componentFromCompiledFragment(args) {
2379
+ const { fragment, sourceType } = args;
2380
+ const name = fragment.meta.name;
2381
+ const description = fragment.meta.description ?? "";
2382
+ const usage = fragment.usage ?? { when: [], whenNot: [] };
2383
+ const contract = fragment.contract;
2384
+ const ai = fragment.ai;
2385
+ const guidancePatterns = (fragment._cloudPatterns ?? []).map((pattern) => ({
2386
+ name: pattern.name ?? "Pattern",
2387
+ description: pattern.description
2388
+ }));
2389
+ return {
2390
+ id: args.id ?? buildComponentId({
2391
+ name,
2392
+ filePath: fragment.sourcePath ?? fragment.filePath,
2393
+ packageName: args.packageName
2394
+ }),
2395
+ name,
2396
+ description,
2397
+ category: fragment.meta.category ?? "uncategorized",
2398
+ status: fragment.meta.status ?? "stable",
2399
+ tags: fragment.meta.tags ?? [],
2400
+ props: Object.fromEntries(
2401
+ Object.entries(fragment.props ?? {}).map(([propName, prop]) => [
2402
+ propName,
2403
+ {
2404
+ type: prop.type,
2405
+ description: prop.description ?? "",
2406
+ required: prop.required ?? false,
2407
+ default: prop.default,
2408
+ values: prop.values ? [...prop.values] : void 0,
2409
+ constraints: prop.constraints ? [...prop.constraints] : void 0
2410
+ }
2411
+ ])
2412
+ ),
2413
+ propsSummary: fragment.propsSummary ?? contract?.propsSummary ?? Object.entries(fragment.props ?? {}).map(
2414
+ ([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
2415
+ ),
2416
+ examples: (fragment.variants ?? []).map((variant) => ({
2417
+ name: variant.name,
2418
+ description: variant.description,
2419
+ code: variant.code,
2420
+ kind: "example"
2421
+ })),
2422
+ relations: (fragment.relations ?? []).map((relation) => ({
2423
+ componentName: relation.component,
2424
+ relationship: relation.relationship,
2425
+ note: relation.note
2426
+ })),
2427
+ compoundChildren: buildCompoundChildren(contract, ai),
2428
+ guidance: {
2429
+ when: usage.when ?? [],
2430
+ whenNot: usage.whenNot ?? [],
2431
+ guidelines: usage.guidelines ?? [],
2432
+ accessibility: usage.accessibility ?? [],
2433
+ dos: usage.when ?? [],
2434
+ donts: usage.whenNot ?? [],
2435
+ patterns: guidancePatterns.length > 0 ? guidancePatterns : (ai?.commonPatterns ?? []).map((pattern) => ({
2436
+ name: pattern
2437
+ }))
2438
+ },
2439
+ sourceType,
2440
+ sourcePath: fragment.sourcePath ?? fragment.filePath,
2441
+ packageName: args.packageName,
2442
+ importPath: args.importPath ?? args.packageName,
2443
+ performance: fragment.performance ? {
2444
+ bundleSize: fragment.performance.bundleSize,
2445
+ rawSize: fragment.performance.rawSize,
2446
+ complexity: fragment.performance.complexity,
2447
+ budgetPercent: fragment.performance.budgetPercent,
2448
+ overBudget: fragment.performance.overBudget,
2449
+ measuredAt: fragment.performance.measuredAt,
2450
+ imports: fragment.performance.imports?.map((entry) => ({
2451
+ path: entry.path,
2452
+ bytes: entry.bytes,
2453
+ percent: entry.percent
2454
+ }))
2455
+ } : void 0,
2456
+ metadata: {
2457
+ a11yRules: contract?.a11yRules ?? [],
2458
+ scenarioTags: contract?.scenarioTags ?? []
2459
+ }
2460
+ };
2461
+ }
2462
+ function blockFromCompiledBlock(key, block) {
2463
+ return {
2464
+ id: key,
2465
+ name: block.name,
2466
+ description: block.description ?? "",
2467
+ category: block.category ?? "uncategorized",
2468
+ components: block.components ?? [],
2469
+ tags: block.tags ?? [],
2470
+ code: block.code ?? ""
2471
+ };
2472
+ }
2473
+ function tokensFromCompiledTokenData(tokens) {
2474
+ const flat = [];
2475
+ const categories = {};
2476
+ for (const [category, entries] of Object.entries(tokens.categories)) {
2477
+ const normalized = entries.map((entry) => ({
2478
+ name: entry.name,
2479
+ category,
2480
+ value: valueToString(entry.value),
2481
+ description: entry.description
2482
+ })).filter((token) => !isGarbageToken(token));
2483
+ if (normalized.length > 0) {
2484
+ categories[category] = normalized;
2485
+ flat.push(...normalized);
2486
+ }
2487
+ }
2488
+ return {
2489
+ prefix: tokens.prefix,
2490
+ total: flat.length,
2491
+ categories,
2492
+ flat
2493
+ };
2494
+ }
2495
+ function buildCapabilities(args) {
2496
+ const capabilities = /* @__PURE__ */ new Set();
2497
+ if (Object.keys(args.components).length > 0) {
2498
+ capabilities.add("components");
2499
+ }
2500
+ if (args.tokens && args.tokens.total > 0) {
2501
+ capabilities.add("tokens");
2502
+ }
2503
+ if (args.blocks && Object.keys(args.blocks).length > 0) {
2504
+ capabilities.add("blocks");
2505
+ }
2506
+ if (args.graph) {
2507
+ capabilities.add("graph");
2508
+ }
2509
+ const hasPerformance = Boolean(args.performanceSummary) || Object.values(args.components).some((component) => Boolean(component.performance));
2510
+ if (hasPerformance) {
2511
+ capabilities.add("performance");
2512
+ }
2513
+ return Array.from(capabilities);
2514
+ }
2515
+ function validateSnapshot(snapshot) {
2516
+ return mcpSnapshotSchema.parse(snapshot);
2517
+ }
2518
+
2519
+ // src/adapters/fragments-json.ts
2520
+ var FragmentsJsonAdapter = class {
2521
+ name = "fragments-json";
2522
+ discover(startDir) {
2523
+ return findFragmentsJson(startDir);
2524
+ }
2525
+ async load(projectRoot) {
2526
+ const paths = this.discover(projectRoot);
2527
+ if (paths.length === 0) {
2528
+ throw new Error(
2529
+ `No ${BRAND.outFile} found. Searched ${projectRoot} and package.json dependencies.
2530
+
2531
+ Fix: Add a project-level MCP config so the server runs from your workspace root:
2532
+
2533
+ Cursor: .cursor/mcp.json
2534
+ VS Code: .vscode/mcp.json
2535
+ Claude: claude mcp add ${BRAND.nameLower} -- npx @fragments-sdk/mcp
2536
+ Windsurf: .windsurf/mcp.json
2537
+
2538
+ Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
2539
+
2540
+ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
2541
+ );
2542
+ }
2543
+ const content = await readFile(paths[0], "utf-8");
2544
+ const primary = JSON.parse(content);
2545
+ if (!primary.blocks && primary.recipes) {
2546
+ primary.blocks = primary.recipes;
2547
+ }
2548
+ const packageMap = {};
2549
+ if (primary.packageName) {
2550
+ for (const name of Object.keys(primary.fragments)) {
2551
+ packageMap[name] = primary.packageName;
2552
+ }
2553
+ }
2554
+ for (let i = 1; i < paths.length; i++) {
2555
+ const extra = JSON.parse(await readFile(paths[i], "utf-8"));
2556
+ if (extra.packageName) {
2557
+ for (const name of Object.keys(extra.fragments)) {
2558
+ packageMap[name] = extra.packageName;
2559
+ }
2560
+ }
2561
+ Object.assign(primary.fragments, extra.fragments);
2562
+ const extraBlocks = extra.blocks ?? extra.recipes;
2563
+ if (extraBlocks) {
2564
+ primary.blocks = { ...primary.blocks, ...extraBlocks };
2565
+ }
2566
+ }
2567
+ const components = Object.fromEntries(
2568
+ Object.entries(primary.fragments).map(([key, fragment]) => [
2569
+ key,
2570
+ componentFromCompiledFragment({
2571
+ id: key,
2572
+ fragment,
2573
+ sourceType: "fragments-json",
2574
+ packageName: packageMap[fragment.meta.name] ?? primary.packageName,
2575
+ importPath: packageMap[fragment.meta.name] ?? primary.packageName
2576
+ })
2577
+ ])
2578
+ );
2579
+ const blocks = primary.blocks ? Object.fromEntries(
2580
+ Object.entries(primary.blocks).map(([key, block]) => [
2581
+ key,
2582
+ blockFromCompiledBlock(key, block)
2583
+ ])
2584
+ ) : void 0;
2585
+ const tokens = primary.tokens ? tokensFromCompiledTokenData(primary.tokens) : void 0;
2586
+ const snapshot = validateSnapshot({
2587
+ schemaVersion: 1,
2588
+ sourceType: "fragments-json",
2589
+ sourceLabel: BRAND.outFile,
2590
+ capabilities: buildCapabilities({
2591
+ components,
2592
+ blocks,
2593
+ tokens,
2594
+ graph: primary.graph,
2595
+ performanceSummary: primary.performanceSummary
2596
+ }),
2597
+ metadata: {
2598
+ designSystemName: primary.packageName,
2599
+ packageName: primary.packageName,
2600
+ importPath: primary.packageName
2601
+ },
2602
+ components,
2603
+ blocks,
2604
+ tokens,
2605
+ graph: primary.graph,
2606
+ performanceSummary: primary.performanceSummary,
2607
+ packageMap,
2608
+ defaultPackageName: primary.packageName
2609
+ });
2610
+ return {
2611
+ snapshot,
2612
+ components: snapshot.components,
2613
+ blocks: snapshot.blocks,
2614
+ tokens: snapshot.tokens,
2615
+ graph: primary.graph,
2616
+ performanceSummary: primary.performanceSummary,
2617
+ packageMap: snapshot.packageMap,
2618
+ defaultPackageName: snapshot.defaultPackageName,
2619
+ capabilities: new Set(snapshot.capabilities)
2620
+ };
2621
+ }
2622
+ };
2623
+
2624
+ // src/adapters/auto-extract.ts
2625
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2626
+ import { join as join5, relative, sep } from "path";
2627
+
2628
+ // src/adapters/discover-components.ts
2629
+ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
2630
+ import { join as join3, extname, basename } from "path";
2631
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
2632
+ "node_modules",
2633
+ "dist",
2634
+ "build",
2635
+ ".next",
2636
+ ".nuxt",
2637
+ "coverage",
2638
+ "__tests__",
2639
+ "__mocks__",
2640
+ ".git",
2641
+ ".cache",
2642
+ ".turbo",
2643
+ "out"
2644
+ ]);
2645
+ var EXCLUDED_PATTERNS = [
2646
+ /\.test\./,
2647
+ /\.spec\./,
2648
+ /\.stories\./,
2649
+ /\.story\./,
2650
+ /\.fragment\./,
2651
+ /\.d\.ts$/,
2652
+ /\.config\./,
2653
+ /\.mock\./,
2654
+ /\.fixture\./
2655
+ ];
2656
+ function discoverComponentFiles(projectRoot) {
2657
+ const results = [];
2658
+ const seen = /* @__PURE__ */ new Set();
2659
+ const scanDirs = [
2660
+ "src/components",
2661
+ "components",
2662
+ "lib/components",
2663
+ "src/ui",
2664
+ "lib/ui",
2665
+ "packages"
2666
+ ].map((d) => join3(projectRoot, d)).filter((d) => existsSync3(d));
2667
+ if (scanDirs.length === 0) {
2668
+ const srcDir = join3(projectRoot, "src");
2669
+ if (existsSync3(srcDir)) scanDirs.push(srcDir);
2670
+ }
2671
+ for (const dir of scanDirs) {
2672
+ walkDir(dir, results, seen);
2673
+ }
2674
+ return results;
2675
+ }
2676
+ function walkDir(dir, results, seen, depth = 0) {
2677
+ if (depth > 6) return;
2678
+ let entries;
2679
+ try {
2680
+ entries = readdirSync2(dir, { withFileTypes: true });
2681
+ } catch {
2682
+ return;
2683
+ }
2684
+ for (const entry of entries) {
2685
+ if (entry.name.startsWith(".")) continue;
2686
+ if (entry.isDirectory()) {
2687
+ if (EXCLUDED_DIRS.has(entry.name)) continue;
2688
+ walkDir(join3(dir, entry.name), results, seen, depth + 1);
2689
+ continue;
2690
+ }
2691
+ if (!entry.isFile()) continue;
2692
+ const ext = extname(entry.name);
2693
+ if (ext !== ".tsx" && ext !== ".jsx") continue;
2694
+ if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
2695
+ const filePath = join3(dir, entry.name);
2696
+ if (seen.has(filePath)) continue;
2697
+ seen.add(filePath);
2698
+ const name = inferComponentName(entry.name, dir);
2699
+ if (name) {
2700
+ results.push({ filePath, componentName: name });
2701
+ }
2702
+ }
2703
+ }
2704
+ function inferComponentName(fileName, dirPath) {
2705
+ const withoutExt = fileName.replace(/\.(tsx|jsx)$/, "");
2706
+ if (withoutExt === "index") {
2707
+ return basename(dirPath);
2708
+ }
2709
+ return withoutExt;
2710
+ }
2711
+
2712
+ // src/adapters/scan-tokens.ts
2713
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
2714
+ import { join as join4, extname as extname2 } from "path";
2715
+ function scanTokens(projectRoot) {
2716
+ const cssFiles = discoverCssFiles(projectRoot);
2717
+ if (cssFiles.length === 0) return void 0;
2718
+ const allTokens = [];
2719
+ let prefix = "";
2720
+ for (const filePath of cssFiles) {
2721
+ try {
2722
+ const content = readFileSync4(filePath, "utf-8");
2723
+ const tokens = extractCustomProperties(content);
2724
+ allTokens.push(...tokens);
2725
+ } catch {
2726
+ continue;
2727
+ }
2728
+ }
2729
+ if (allTokens.length === 0) return void 0;
2730
+ prefix = detectPrefix(allTokens.map((t) => t.name));
2731
+ const categories = {};
2732
+ for (const token of allTokens) {
2733
+ const category = inferTokenCategory(token.name);
2734
+ if (!categories[category]) categories[category] = [];
2735
+ if (!categories[category].some((t) => t.name === token.name)) {
2736
+ categories[category].push(token);
2737
+ }
2738
+ }
2739
+ return {
2740
+ prefix,
2741
+ total: Object.values(categories).reduce((sum, arr) => sum + arr.length, 0),
2742
+ categories
2743
+ };
2744
+ }
2745
+ function discoverCssFiles(projectRoot) {
2746
+ const files = [];
2747
+ const searchDirs = [
2748
+ "src",
2749
+ "styles",
2750
+ "css",
2751
+ "app"
2752
+ ].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
2753
+ searchDirs.push(projectRoot);
2754
+ for (const dir of searchDirs) {
2755
+ try {
2756
+ const entries = readdirSync3(dir, { withFileTypes: true });
2757
+ for (const entry of entries) {
2758
+ if (!entry.isFile()) continue;
2759
+ const ext = extname2(entry.name);
2760
+ if (ext === ".css" || ext === ".scss") {
2761
+ files.push(join4(dir, entry.name));
2762
+ }
2763
+ }
2764
+ } catch {
2765
+ continue;
2766
+ }
2767
+ }
2768
+ const srcDir = join4(projectRoot, "src");
2769
+ if (existsSync4(srcDir)) {
2770
+ try {
2771
+ for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
2772
+ if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
2773
+ const subDir = join4(srcDir, subEntry.name);
2774
+ for (const file of readdirSync3(subDir, { withFileTypes: true })) {
2775
+ if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
2776
+ files.push(join4(subDir, file.name));
2777
+ }
2778
+ }
2779
+ }
2780
+ }
2781
+ } catch {
2782
+ }
2783
+ }
2784
+ return [...new Set(files)];
2785
+ }
2786
+ function extractCustomProperties(content) {
2787
+ const tokens = [];
2788
+ let inRelevantBlock = false;
2789
+ let braceDepth = 0;
2790
+ const relevantSelectors = [":root", ".dark", "[data-theme", "@theme"];
2791
+ const lines = content.split("\n");
2792
+ for (const line of lines) {
2793
+ const trimmed = line.trim();
2794
+ if (!inRelevantBlock && braceDepth === 0) {
2795
+ if (relevantSelectors.some((sel) => trimmed.startsWith(sel) || trimmed.includes(sel))) {
2796
+ if (trimmed.includes("{")) {
2797
+ inRelevantBlock = true;
2798
+ braceDepth = 1;
2799
+ }
2800
+ }
2801
+ }
2802
+ if (inRelevantBlock) {
2803
+ for (const ch of trimmed) {
2804
+ if (ch === "{") braceDepth++;
2805
+ else if (ch === "}") {
2806
+ braceDepth--;
2807
+ if (braceDepth <= 0) {
2808
+ inRelevantBlock = false;
2809
+ braceDepth = 0;
2810
+ }
2811
+ }
2812
+ }
2813
+ }
2814
+ if (!inRelevantBlock && braceDepth === 0) continue;
2815
+ const match = trimmed.match(/^(--[\w-]+)\s*:\s*(.+?)\s*;/);
2816
+ if (match) {
2817
+ const [, name, value] = match;
2818
+ const commentMatch = value.match(/\/\*\s*(.+?)\s*\*\//);
2819
+ const cleanValue = value.replace(/\/\*.*?\*\//, "").trim();
2820
+ tokens.push({
2821
+ name,
2822
+ value: cleanValue,
2823
+ description: commentMatch?.[1]
2824
+ });
2825
+ }
2826
+ }
2827
+ return tokens;
2828
+ }
2829
+ function detectPrefix(names) {
2830
+ if (names.length === 0) return "--";
2831
+ const stripped = names.map((n) => n.slice(2));
2832
+ let prefix = "";
2833
+ const first = stripped[0];
2834
+ for (let i = 0; i < first.length; i++) {
2835
+ const ch = first[i];
2836
+ if (stripped.every((n) => n[i] === ch)) {
2837
+ prefix += ch;
2838
+ } else {
2839
+ break;
2840
+ }
2841
+ }
2842
+ const lastDash = prefix.lastIndexOf("-");
2843
+ if (lastDash > 0) {
2844
+ return "--" + prefix.slice(0, lastDash + 1);
2845
+ }
2846
+ return "--";
2847
+ }
2848
+ function inferTokenCategory(name) {
2849
+ const n = name.toLowerCase();
2850
+ if (n.includes("color") || n.includes("background") || n.includes("foreground") || n.includes("primary") || n.includes("secondary") || n.includes("accent") || n.includes("muted") || n.includes("destructive") || n.includes("popover") || n.includes("card") && !n.includes("card-") || n.includes("chart")) {
2851
+ return "colors";
2852
+ }
2853
+ if (n.includes("font") || n.includes("text") || n.includes("letter") || n.includes("line-height")) {
2854
+ return "typography";
2855
+ }
2856
+ if (n.includes("space") || n.includes("gap") || n.includes("padding") || n.includes("margin")) {
2857
+ return "spacing";
2858
+ }
2859
+ if (n.includes("radius")) {
2860
+ return "radius";
2861
+ }
2862
+ if (n.includes("shadow")) {
2863
+ return "shadows";
2864
+ }
2865
+ if (n.includes("border")) {
2866
+ return "borders";
2867
+ }
2868
+ if (n.includes("ring")) {
2869
+ return "focus";
2870
+ }
2871
+ if (n.includes("sidebar")) {
2872
+ return "sidebar";
2873
+ }
2874
+ if (n.includes("input")) {
2875
+ return "forms";
2876
+ }
2877
+ return "other";
2878
+ }
2879
+
2880
+ // src/adapters/auto-extract.ts
2881
+ var AutoExtractionAdapter = class {
2882
+ name = "auto-extract";
2883
+ discover(startDir) {
2884
+ return discoverComponentFiles(startDir).map((f) => f.filePath);
2885
+ }
2886
+ async load(projectRoot) {
2887
+ let extractMod;
2888
+ try {
2889
+ extractMod = await loadExtractorModule();
2890
+ } catch (e) {
2891
+ throw new Error(
2892
+ "Auto-extraction requires @fragments-sdk/extract and TypeScript.\n\nIf you see this error, the MCP server may not be installed correctly.\nAlternative: pre-build your design system with `npx fragments build`"
2893
+ );
2894
+ }
2895
+ const tsconfigPath = findTsConfig(projectRoot);
2896
+ const extractor = extractMod.createComponentExtractor(tsconfigPath ?? void 0);
2897
+ try {
2898
+ const discovered = discoverComponentFiles(projectRoot);
2899
+ if (discovered.length === 0) {
2900
+ throw new Error(
2901
+ `No component files found in ${projectRoot}.
2902
+ Searched: src/components/, components/, lib/components/, src/ui/
2903
+
2904
+ If your components are elsewhere, create a fragments.json with:
2905
+ npx fragments build`
2906
+ );
2907
+ }
2908
+ const components = {};
2909
+ const packageMap = /* @__PURE__ */ new Map();
2910
+ const defaultPackageName = readPackageName(projectRoot);
2911
+ let extractedCount = 0;
2912
+ const fileToComponents = /* @__PURE__ */ new Map();
2913
+ for (const { filePath, componentName } of discovered) {
2914
+ try {
2915
+ const metas = extractor.extractAll(filePath);
2916
+ for (const meta of metas) {
2917
+ const fragment = mapToCompiledFragment(meta, filePath, projectRoot);
2918
+ components[meta.name] = fragment;
2919
+ if (defaultPackageName) {
2920
+ packageMap.set(meta.name, defaultPackageName);
2921
+ }
2922
+ extractedCount++;
2923
+ const relPath = relative(projectRoot, filePath);
2924
+ if (!fileToComponents.has(relPath)) fileToComponents.set(relPath, []);
2925
+ fileToComponents.get(relPath).push(meta.name);
2926
+ }
2927
+ if (metas.length === 0) {
2928
+ const meta = extractor.extract(filePath, componentName);
2929
+ if (meta) {
2930
+ const fragment = mapToCompiledFragment(meta, filePath, projectRoot);
2931
+ components[meta.name] = fragment;
2932
+ if (defaultPackageName) {
2933
+ packageMap.set(meta.name, defaultPackageName);
2934
+ }
2935
+ extractedCount++;
2936
+ const relPath = relative(projectRoot, filePath);
2937
+ if (!fileToComponents.has(relPath)) fileToComponents.set(relPath, []);
2938
+ fileToComponents.get(relPath).push(meta.name);
2939
+ }
2940
+ }
2941
+ } catch {
2942
+ continue;
2943
+ }
2944
+ }
2945
+ inferRelations(components, fileToComponents);
2946
+ console.error(`[fragments-mcp] Extracted ${extractedCount} components from ${discovered.length} files.`);
2947
+ if (extractedCount === 0) {
2948
+ throw new Error(
2949
+ `Found ${discovered.length} component files but could not extract any props.
2950
+ This usually means TypeScript cannot parse the files.
2951
+ Check that your tsconfig.json includes the component directories.`
2952
+ );
2953
+ }
2954
+ const tokens = scanTokens(projectRoot);
2955
+ if (tokens) {
2956
+ console.error(`[fragments-mcp] Found ${tokens.total} design tokens across ${Object.keys(tokens.categories).length} categories.`);
2957
+ }
2958
+ const snapshotComponents = Object.fromEntries(
2959
+ Object.entries(components).map(([key, fragment]) => [
2960
+ key,
2961
+ componentFromCompiledFragment({
2962
+ id: key,
2963
+ fragment,
2964
+ sourceType: "auto-extract",
2965
+ packageName: packageMap.get(fragment.meta.name) ?? defaultPackageName,
2966
+ importPath: packageMap.get(fragment.meta.name) ?? defaultPackageName
2967
+ })
2968
+ ])
2969
+ );
2970
+ const snapshotTokens = tokens ? tokensFromCompiledTokenData(tokens) : void 0;
2971
+ const packageMapRecord = Object.fromEntries(packageMap.entries());
2972
+ const snapshot = validateSnapshot({
2973
+ schemaVersion: 1,
2974
+ sourceType: "auto-extract",
2975
+ sourceLabel: "Auto-extracted source files",
2976
+ capabilities: buildCapabilities({
2977
+ components: snapshotComponents,
2978
+ tokens: snapshotTokens
2979
+ }),
2980
+ metadata: {
2981
+ packageName: defaultPackageName,
2982
+ importPath: defaultPackageName
2983
+ },
2984
+ components: snapshotComponents,
2985
+ tokens: snapshotTokens,
2986
+ packageMap: packageMapRecord,
2987
+ defaultPackageName
2988
+ });
2989
+ return {
2990
+ snapshot,
2991
+ components: snapshot.components,
2992
+ blocks: snapshot.blocks,
2993
+ tokens: snapshot.tokens,
2994
+ graph: void 0,
2995
+ performanceSummary: void 0,
2996
+ packageMap: snapshot.packageMap,
2997
+ defaultPackageName: snapshot.defaultPackageName,
2998
+ capabilities: new Set(snapshot.capabilities)
2999
+ };
3000
+ } finally {
3001
+ extractor.dispose();
3002
+ }
3003
+ }
3004
+ };
3005
+ var extractorModulePromise = null;
3006
+ async function loadExtractorModule() {
3007
+ if (!extractorModulePromise) {
3008
+ extractorModulePromise = import("./dist-TTCI6TME.js");
3009
+ }
3010
+ return extractorModulePromise;
3011
+ }
3012
+ var UNIVERSAL_INHERITED = /* @__PURE__ */ new Set(["children", "className", "id", "disabled"]);
3013
+ var FORM_INPUT_INHERITED = /* @__PURE__ */ new Set([
3014
+ "placeholder",
3015
+ "value",
3016
+ "defaultValue",
3017
+ "onChange",
3018
+ "name",
3019
+ "required",
3020
+ "autoFocus",
3021
+ "autoComplete",
3022
+ "checked",
3023
+ "defaultChecked",
3024
+ "type"
3025
+ ]);
3026
+ var LABEL_INHERITED = /* @__PURE__ */ new Set(["htmlFor"]);
3027
+ var INPUT_LIKE_NAMES = /input|select|textarea|checkbox|radio|switch|slider|combobox/i;
3028
+ var LABEL_LIKE_NAMES = /label/i;
3029
+ function getRelevantInheritedProps(componentName) {
3030
+ const allowed = new Set(UNIVERSAL_INHERITED);
3031
+ if (INPUT_LIKE_NAMES.test(componentName)) {
3032
+ for (const p of FORM_INPUT_INHERITED) allowed.add(p);
3033
+ }
3034
+ if (LABEL_LIKE_NAMES.test(componentName)) {
3035
+ for (const p of LABEL_INHERITED) allowed.add(p);
3036
+ }
3037
+ return allowed;
3038
+ }
3039
+ function mapToCompiledFragment(meta, filePath, projectRoot) {
3040
+ const relevantInherited = getRelevantInheritedProps(meta.name);
3041
+ const props = {};
3042
+ for (const [name, propMeta] of Object.entries(meta.props)) {
3043
+ if (propMeta.source === "inherited" && !relevantInherited.has(name)) continue;
3044
+ props[name] = {
3045
+ type: propMeta.type,
3046
+ description: propMeta.description ?? "",
3047
+ required: propMeta.required,
3048
+ ...propMeta.values && { values: propMeta.values },
3049
+ ...propMeta.default !== void 0 && { default: propMeta.default }
3050
+ };
3051
+ }
3052
+ const relativePath = relative(projectRoot, filePath);
3053
+ const category = inferCategory(relativePath);
3054
+ const importPath = buildImportPath(relativePath);
3055
+ const description = buildDescription(meta, props);
3056
+ const propsSummary = buildPropsSummary(props);
3057
+ const codeExample = buildCodeExample(meta.name, props);
3058
+ const fragmentMeta = {
3059
+ name: meta.name,
3060
+ description,
3061
+ category
3062
+ };
3063
+ return {
3064
+ filePath: relativePath,
3065
+ meta: fragmentMeta,
3066
+ usage: {
3067
+ when: [`Use ${meta.name} when you need a ${category} ${meta.name.toLowerCase()} element.`],
3068
+ whenNot: [],
3069
+ guidelines: importPath ? [`import { ${meta.name} } from "${importPath}"`] : []
3070
+ },
3071
+ props,
3072
+ propsSummary,
3073
+ variants: codeExample ? [{ name: "Default", description: `Basic ${meta.name} usage`, code: codeExample }] : [],
3074
+ sourcePath: relativePath,
3075
+ exportName: meta.name,
3076
+ _generated: {
3077
+ source: "extracted",
3078
+ verified: false,
3079
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3080
+ },
3081
+ ...meta.composition && {
3082
+ ai: {
3083
+ compositionPattern: meta.composition.pattern
3084
+ }
3085
+ }
3086
+ };
3087
+ }
3088
+ function buildImportPath(relativePath) {
3089
+ const withoutExt = relativePath.replace(/\.(tsx|jsx|ts|js)$/, "");
3090
+ if (withoutExt.startsWith(`src${sep}`)) {
3091
+ return "@/" + withoutExt.slice(4).split(sep).join("/");
3092
+ }
3093
+ return "./" + withoutExt.split(sep).join("/");
3094
+ }
3095
+ function buildDescription(meta, localProps) {
3096
+ if (meta.description && meta.description !== meta.name && !meta.description.endsWith(" component")) {
3097
+ return meta.description;
3098
+ }
3099
+ const propEntries = Object.entries(localProps);
3100
+ if (propEntries.length === 0) {
3101
+ return `${meta.name} component`;
3102
+ }
3103
+ const details = [];
3104
+ for (const [name, prop] of propEntries) {
3105
+ if (prop.values && prop.values.length > 0) {
3106
+ details.push(`${prop.values.length} ${name} options`);
3107
+ }
3108
+ }
3109
+ if (details.length > 0) {
3110
+ return `${meta.name} component with ${details.join(" and ")}.`;
3111
+ }
3112
+ return `${meta.name} component with ${propEntries.length} configurable prop${propEntries.length === 1 ? "" : "s"}.`;
3113
+ }
3114
+ function buildPropsSummary(props) {
3115
+ const summaries = [];
3116
+ for (const [name, prop] of Object.entries(props)) {
3117
+ let summary = `${name}`;
3118
+ if (prop.values && prop.values.length > 0) {
3119
+ summary += `: ${prop.values.map((v) => `"${v}"`).join(" | ")}`;
3120
+ } else {
3121
+ summary += `: ${prop.type}`;
3122
+ }
3123
+ if (prop.default !== void 0) {
3124
+ summary += ` (default: ${JSON.stringify(prop.default)})`;
3125
+ }
3126
+ if (prop.required) {
3127
+ summary += " (required)";
3128
+ }
3129
+ summaries.push(summary);
3130
+ }
3131
+ return summaries;
3132
+ }
3133
+ function buildCodeExample(name, props) {
3134
+ const propsStr = [];
3135
+ for (const [pName, prop] of Object.entries(props)) {
3136
+ if (pName === "children" || pName === "asChild" || pName === "className") continue;
3137
+ if (pName === "onChange" || pName === "onSubmit" || pName === "value" || pName === "defaultValue" || pName === "checked" || pName === "defaultChecked" || pName === "autoFocus" || pName === "autoComplete" || pName === "required" || pName === "disabled" || pName === "name") continue;
3138
+ if (prop.default !== void 0) {
3139
+ if (prop.values && prop.values.length > 1) {
3140
+ const nonDefault = prop.values.find((v) => v !== String(prop.default));
3141
+ if (nonDefault) {
3142
+ propsStr.push(`${pName}="${nonDefault}"`);
3143
+ continue;
3144
+ }
3145
+ }
3146
+ }
3147
+ if (pName === "placeholder" && INPUT_LIKE_NAMES.test(name)) {
3148
+ propsStr.push(`placeholder="Enter ${name.toLowerCase()}..."`);
3149
+ continue;
3150
+ }
3151
+ if (pName === "htmlFor" && LABEL_LIKE_NAMES.test(name)) {
3152
+ propsStr.push(`htmlFor="field-id"`);
3153
+ continue;
3154
+ }
3155
+ if (pName === "id" || pName === "className" || pName === "children") continue;
3156
+ if (prop.required) {
3157
+ if (prop.values && prop.values.length > 0) {
3158
+ propsStr.push(`${pName}="${prop.values[0]}"`);
3159
+ } else if (prop.type === "string") {
3160
+ propsStr.push(`${pName}="..."`);
3161
+ }
3162
+ }
3163
+ }
3164
+ const propsJsx = propsStr.length > 0 ? " " + propsStr.join(" ") : "";
3165
+ const nameLower = name.toLowerCase();
3166
+ const selfClosing = nameLower.includes("input") || nameLower.includes("separator") || nameLower.includes("slider") || nameLower.includes("switch");
3167
+ if (selfClosing) {
3168
+ return `<${name}${propsJsx} />`;
3169
+ }
3170
+ return `<${name}${propsJsx}>${name}</${name}>`;
3171
+ }
3172
+ function buildCompositionExample(root, subs) {
3173
+ const header = subs.filter((s) => s.includes("Header"));
3174
+ const title = subs.filter((s) => s.includes("Title"));
3175
+ const description = subs.filter((s) => s.includes("Description"));
3176
+ const content = subs.filter((s) => s.includes("Content") || s.includes("Body"));
3177
+ const footer = subs.filter((s) => s.includes("Footer"));
3178
+ const action = subs.filter((s) => s.includes("Action"));
3179
+ const other = subs.filter(
3180
+ (s) => !header.includes(s) && !title.includes(s) && !description.includes(s) && !content.includes(s) && !footer.includes(s) && !action.includes(s)
3181
+ );
3182
+ const lines = [`<${root}>`];
3183
+ if (header.length > 0) {
3184
+ lines.push(` <${header[0]}>`);
3185
+ for (const t of title) lines.push(` <${t}>Title</${t}>`);
3186
+ for (const d of description) lines.push(` <${d}>Description</${d}>`);
3187
+ lines.push(` </${header[0]}>`);
3188
+ } else {
3189
+ for (const t of title) lines.push(` <${t}>Title</${t}>`);
3190
+ for (const d of description) lines.push(` <${d}>Description</${d}>`);
3191
+ }
3192
+ for (const c of content) lines.push(` <${c}>Content</${c}>`);
3193
+ for (const o of other) lines.push(` <${o}>...</${o}>`);
3194
+ if (footer.length > 0) {
3195
+ lines.push(` <${footer[0]}>`);
3196
+ for (const a of action) lines.push(` <${a}>Action</${a}>`);
3197
+ lines.push(` </${footer[0]}>`);
3198
+ } else {
3199
+ for (const a of action) lines.push(` <${a}>Action</${a}>`);
3200
+ }
3201
+ lines.push(`</${root}>`);
3202
+ return lines.join("\n");
3203
+ }
3204
+ function inferRelations(components, fileToComponents) {
3205
+ for (const [_file, names] of fileToComponents) {
3206
+ if (names.length <= 1) continue;
3207
+ const sorted = [...names].sort((a, b) => a.length - b.length);
3208
+ const root = sorted[0];
3209
+ const subs = sorted.slice(1).filter((n) => n.startsWith(root));
3210
+ if (subs.length === 0) continue;
3211
+ const rootComp = components[root];
3212
+ if (rootComp) {
3213
+ rootComp.ai = {
3214
+ ...rootComp.ai,
3215
+ compositionPattern: "compound",
3216
+ subComponents: subs
3217
+ };
3218
+ rootComp.relations = subs.map((sub) => ({
3219
+ component: sub,
3220
+ relationship: "parent-of",
3221
+ note: `${sub} is a sub-component of ${root}`
3222
+ }));
3223
+ const compositionCode = buildCompositionExample(root, subs);
3224
+ rootComp.variants = [
3225
+ ...rootComp.variants ?? [],
3226
+ { name: "Composition", description: `${root} with all sub-components`, code: compositionCode }
3227
+ ];
3228
+ }
3229
+ const rootImportPath = rootComp?.usage?.guidelines?.[0];
3230
+ for (const sub of subs) {
3231
+ const subComp = components[sub];
3232
+ if (subComp) {
3233
+ subComp.relations = [{
3234
+ component: root,
3235
+ relationship: "child-of",
3236
+ note: `Use inside <${root}>`
3237
+ }];
3238
+ if (rootImportPath) {
3239
+ const allNames = [root, ...subs].join(", ");
3240
+ const fromPath = rootImportPath.match(/from\s+"([^"]+)"/)?.[1] ?? "";
3241
+ if (fromPath) {
3242
+ subComp.usage = {
3243
+ ...subComp.usage,
3244
+ guidelines: [`import { ${allNames} } from "${fromPath}"`]
3245
+ };
3246
+ }
3247
+ }
3248
+ }
3249
+ }
3250
+ if (rootComp && rootComp.usage?.guidelines?.[0]) {
3251
+ const fromPath = rootComp.usage.guidelines[0].match(/from\s+"([^"]+)"/)?.[1] ?? "";
3252
+ if (fromPath) {
3253
+ const allNames = [root, ...subs].join(", ");
3254
+ rootComp.usage.guidelines = [`import { ${allNames} } from "${fromPath}"`];
3255
+ }
3256
+ }
3257
+ }
3258
+ }
3259
+ function inferCategory(relativePath) {
3260
+ const parts = relativePath.split(sep);
3261
+ const componentsIdx = parts.findIndex(
3262
+ (p) => p === "components" || p === "ui"
3263
+ );
3264
+ if (componentsIdx >= 0 && componentsIdx + 1 < parts.length - 1) {
3265
+ const nextPart = parts[componentsIdx + 1];
3266
+ if (/^[A-Z]/.test(nextPart)) return parts[componentsIdx] || "uncategorized";
3267
+ return nextPart;
3268
+ }
3269
+ return "uncategorized";
3270
+ }
3271
+ function findTsConfig(projectRoot) {
3272
+ const candidates = ["tsconfig.json", "tsconfig.app.json"];
3273
+ for (const name of candidates) {
3274
+ const p = join5(projectRoot, name);
3275
+ if (existsSync5(p)) return p;
3276
+ }
3277
+ return null;
3278
+ }
3279
+ function readPackageName(projectRoot) {
3280
+ try {
3281
+ const pkgPath = join5(projectRoot, "package.json");
3282
+ if (!existsSync5(pkgPath)) return void 0;
3283
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
3284
+ return pkg.name;
3285
+ } catch {
3286
+ return void 0;
3287
+ }
3288
+ }
3289
+
3290
+ // src/adapters/cloud-catalog.ts
3291
+ var DEFAULT_CLOUD_URL2 = "https://app.usefragments.com/api/catalog";
3292
+ function mergeDesignSystemMetadata(catalogDesignSystem, contextDesignSystem) {
3293
+ return {
3294
+ name: catalogDesignSystem?.name ?? contextDesignSystem?.name,
3295
+ packageName: catalogDesignSystem?.packageName ?? contextDesignSystem?.packageName ?? null,
3296
+ importPath: catalogDesignSystem?.importPath ?? contextDesignSystem?.importPath ?? catalogDesignSystem?.packageName ?? contextDesignSystem?.packageName ?? null
3297
+ };
3298
+ }
3299
+ function chooseComponentSource(args) {
3300
+ if (args.contextComponents.length > args.catalogComponents.length) {
3301
+ return args.contextComponents;
3302
+ }
3303
+ return args.catalogComponents;
3304
+ }
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 = {
3335
+ color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
3336
+ spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
3337
+ typography: ["typography", "font", "text", "copy", "line-height", "letter"],
3338
+ border: ["border", "borders", "stroke", "outline"],
3339
+ radius: ["radius", "radii", "corner", "corners", "rounded"],
3340
+ shadow: ["shadow", "shadows", "elevation"],
3341
+ layout: ["layout", "grid", "container", "breakpoint"],
3342
+ focus: ["focus", "ring", "focus-ring"],
3343
+ surface: ["surface", "surfaces", "canvas", "card", "background"]
3344
+ };
3345
+ function normalizeCatalogUrl(url) {
3346
+ if (!url) return DEFAULT_CLOUD_URL2;
3347
+ if (url.endsWith("/api/catalog")) return url;
3348
+ return `${url.replace(/\/+$/, "")}/api/catalog`;
3349
+ }
3350
+ function normalizeValidateFixUrl(url) {
3351
+ const base = normalizeCatalogUrl(url).replace(/\/api\/catalog$/, "");
3352
+ return `${base}/api/context?target=validate-fix`;
3353
+ }
3354
+ function normalizeValue(value) {
3355
+ return value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() ?? "";
3356
+ }
3357
+ function canonicalizeTokenCategory(token) {
3358
+ const candidates = [
3359
+ token.category,
3360
+ token.path?.[0],
3361
+ token.name.split(/[.:/-]/)[0]
3362
+ ].map(normalizeValue).filter(Boolean);
3363
+ for (const candidate of candidates) {
3364
+ for (const [canonical, aliases] of Object.entries(
3365
+ TOKEN_CATEGORY_ALIASES
3366
+ )) {
3367
+ if (candidate === canonical || aliases.some(
3368
+ (alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
3369
+ )) {
3370
+ return canonical;
3371
+ }
3372
+ }
3373
+ }
3374
+ return candidates[0] || "other";
3375
+ }
3376
+ function groupTokens(flat) {
3377
+ const categories = {};
3378
+ const normalizedFlat = (flat ?? []).filter((token) => !isGarbageToken(token)).map((token) => {
3379
+ const category = canonicalizeTokenCategory(token);
3380
+ const normalized = {
3381
+ name: token.name,
3382
+ category,
3383
+ value: token.value,
3384
+ description: token.description,
3385
+ path: token.path,
3386
+ type: token.type
3387
+ };
3388
+ if (!categories[category]) {
3389
+ categories[category] = [];
3390
+ }
3391
+ categories[category].push(normalized);
3392
+ return normalized;
3393
+ });
3394
+ return {
3395
+ prefix: "",
3396
+ total: normalizedFlat.length,
3397
+ categories,
3398
+ flat: normalizedFlat
3399
+ };
3400
+ }
3401
+ function mapComponent(component, designSystem) {
3402
+ return {
3403
+ id: component.componentKey,
3404
+ name: component.name,
3405
+ description: component.description ?? "",
3406
+ category: component.category ?? "uncategorized",
3407
+ status: component.status ?? "stable",
3408
+ tags: [],
3409
+ props: Object.fromEntries(
3410
+ Object.entries(component.props ?? {}).map(([propName, prop]) => [
3411
+ propName,
3412
+ {
3413
+ type: prop.type ?? "unknown",
3414
+ description: prop.description ?? "",
3415
+ required: prop.required ?? false,
3416
+ default: prop.default,
3417
+ values: prop.values
3418
+ }
3419
+ ])
3420
+ ),
3421
+ propsSummary: Object.entries(component.props ?? {}).map(
3422
+ ([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type ?? "unknown"}`
3423
+ ),
3424
+ examples: (component.examples ?? []).map((example) => ({
3425
+ name: example.title ?? "Example",
3426
+ description: example.kind,
3427
+ code: example.code,
3428
+ kind: example.kind
3429
+ })),
3430
+ relations: (component.relations ?? []).map((relation) => ({
3431
+ componentName: relation.component ?? relation.componentKey ?? "Unknown",
3432
+ componentId: relation.componentKey,
3433
+ relationship: relation.relationship ?? relation.type ?? "related",
3434
+ note: relation.note,
3435
+ description: relation.description
3436
+ })),
3437
+ compoundChildren: (component.compoundChildren ?? []).filter((child) => child.subcomponentVisibility !== "internal").map((child) => ({
3438
+ name: child.name,
3439
+ componentId: child.componentKey,
3440
+ description: child.description,
3441
+ required: child.required,
3442
+ accepts: child.accepts,
3443
+ visibility: child.subcomponentVisibility === "internal" ? "internal" : "public"
3444
+ })),
3445
+ guidance: {
3446
+ when: component.usageGuidance ? [component.usageGuidance] : [],
3447
+ whenNot: component.donts ?? [],
3448
+ guidelines: component.usageGuidance ? [component.usageGuidance] : [],
3449
+ accessibility: [],
3450
+ usageGuidance: component.usageGuidance,
3451
+ dos: component.dos ?? [],
3452
+ donts: component.donts ?? [],
3453
+ patterns: []
3454
+ },
3455
+ sourceType: "cloud",
3456
+ sourcePath: component.sourcePath,
3457
+ sourceRepoFullName: component.sourceRepoFullName ?? void 0,
3458
+ packageName: component.packageName ?? designSystem?.packageName ?? void 0,
3459
+ importPath: component.importPath ?? component.packageName ?? designSystem?.importPath ?? designSystem?.packageName ?? void 0,
3460
+ publicRef: component.publicRef,
3461
+ publicSlug: component.publicSlug ?? null,
3462
+ isCanonical: component.isCanonical ?? false,
3463
+ tier: component.tier,
3464
+ parentComponentId: component.parentComponentKey,
3465
+ parentComponentName: component.parentComponentName,
3466
+ metadata: {
3467
+ a11yRules: [],
3468
+ scenarioTags: []
3469
+ }
3470
+ };
3471
+ }
3472
+ function normalizeValidateFixContext(raw) {
3473
+ const content = raw.content;
3474
+ if (!content || !Array.isArray(content.components)) {
3475
+ return void 0;
3476
+ }
3477
+ const components = content.components.filter(
3478
+ (component) => Boolean(component?.componentKey) && Boolean(component?.publicRef) && Boolean(component?.name) && Boolean(component?.selection)
3479
+ ).map((component) => ({
3480
+ componentKey: component.componentKey,
3481
+ publicRef: component.publicRef,
3482
+ name: component.name,
3483
+ category: component.category ?? "uncategorized",
3484
+ status: component.status ?? "stable",
3485
+ tier: component.tier ?? "composition",
3486
+ isCanonical: component.isCanonical ?? false,
3487
+ isActive: component.isActive ?? true,
3488
+ selection: component.selection ?? "allowed",
3489
+ reasons: component.reasons ?? [],
3490
+ usageGuidance: component.usageGuidance,
3491
+ dos: component.dos ?? [],
3492
+ donts: component.donts ?? []
3493
+ }));
3494
+ return {
3495
+ version: 1,
3496
+ catalogRevision: content.catalogRevision,
3497
+ updatedAt: content.updatedAt,
3498
+ policy: {
3499
+ mode: "cloud",
3500
+ endpoint: content.policy?.endpoint ?? "/api/govern/policy"
3501
+ },
3502
+ components
3503
+ };
3504
+ }
3505
+ var CloudCatalogAdapter = class {
3506
+ constructor(options) {
3507
+ this.options = options;
3508
+ }
3509
+ options;
3510
+ name = "cloud";
3511
+ async load(_projectRoot) {
3512
+ const headers = {
3513
+ "X-API-Key": this.options.apiKey
3514
+ };
3515
+ const [response, validateFixResponse] = await Promise.all([
3516
+ fetch(normalizeCatalogUrl(this.options.url), { headers }),
3517
+ fetch(normalizeValidateFixUrl(this.options.url), { headers }).catch(
3518
+ () => null
3519
+ )
3520
+ ]);
3521
+ if (!response.ok) {
3522
+ throw new Error(
3523
+ `Failed to load Cloud catalog (${response.status} ${response.statusText}).`
3524
+ );
3525
+ }
3526
+ const raw = await response.json();
3527
+ const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
3528
+ const sourceComponents = chooseComponentSource({
3529
+ catalogComponents: raw.components ?? [],
3530
+ contextComponents: validateFixRaw?.content?.components ?? []
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
+ );
3541
+ const components = Object.fromEntries(
3542
+ sourceComponents.map((component) => [
3543
+ component.componentKey,
3544
+ mapComponent(component, designSystem)
3545
+ ])
3546
+ );
3547
+ const tokens = raw.tokens?.flat ? groupTokens(raw.tokens.flat) : void 0;
3548
+ const packageName = designSystem?.packageName ?? void 0;
3549
+ const importPath = designSystem?.importPath ?? designSystem?.packageName ?? void 0;
3550
+ const packageMap = Object.fromEntries(
3551
+ Object.values(components).map((component) => [
3552
+ component.name,
3553
+ component.importPath ?? component.packageName ?? packageName
3554
+ ]).filter(
3555
+ (entry) => typeof entry[1] === "string" && entry[1].length > 0
3556
+ )
3557
+ );
3558
+ const snapshot = validateSnapshot({
3559
+ schemaVersion: 1,
3560
+ sourceType: "cloud",
3561
+ sourceLabel: "Fragments Cloud catalog",
3562
+ capabilities: buildCapabilities({
3563
+ components,
3564
+ tokens
3565
+ }),
3566
+ metadata: {
3567
+ designSystemName: designSystem?.name ?? raw.org.name,
3568
+ packageName,
3569
+ importPath,
3570
+ revision: raw.revision,
3571
+ updatedAt: typeof raw.updatedAt === "number" ? new Date(raw.updatedAt).toISOString() : void 0
3572
+ },
3573
+ components,
3574
+ tokens,
3575
+ packageMap,
3576
+ defaultPackageName: packageName
3577
+ });
3578
+ const validateFixContext = validateFixRaw ? normalizeValidateFixContext(validateFixRaw) : void 0;
3579
+ const hydratedComponents = Object.fromEntries(
3580
+ Object.entries(snapshot.components).map(([componentId, component]) => [
3581
+ componentId,
3582
+ {
3583
+ ...component,
3584
+ isCanonical: components[componentId]?.isCanonical ?? false
3585
+ }
3586
+ ])
3587
+ );
3588
+ return {
3589
+ snapshot: {
3590
+ ...snapshot,
3591
+ components: hydratedComponents
3592
+ },
3593
+ components: hydratedComponents,
3594
+ blocks: snapshot.blocks,
3595
+ tokens: snapshot.tokens,
3596
+ graph: void 0,
3597
+ performanceSummary: void 0,
3598
+ packageMap: snapshot.packageMap,
3599
+ defaultPackageName: snapshot.defaultPackageName,
3600
+ validateFixContext,
3601
+ capabilities: new Set(snapshot.capabilities)
3602
+ };
3603
+ }
3604
+ };
3605
+
3606
+ // src/adapters/bundle.ts
3607
+ import { existsSync as existsSync6 } from "fs";
3608
+ import { readFile as readFile2 } from "fs/promises";
3609
+ import { dirname as dirname3, resolve as resolve2 } from "path";
3610
+ import {
3611
+ bundleComponentShardSchema,
3612
+ bundleManifestSchema,
3613
+ bundleTokenFileSchema
3614
+ } from "@fragments-sdk/core";
3615
+ async function readJsonFile(path, parser, label) {
3616
+ const content = await readFile2(path, "utf-8");
3617
+ try {
3618
+ return parser.parse(JSON.parse(content));
3619
+ } catch (error) {
3620
+ throw new Error(
3621
+ `Invalid ${label} at ${path}: ${error instanceof Error ? error.message : String(error)}`
3622
+ );
3623
+ }
3624
+ }
3625
+ function normalizeProps(props) {
3626
+ return Object.fromEntries(
3627
+ Object.entries(props ?? {}).map(([propName, value]) => {
3628
+ const prop = value && typeof value === "object" ? value : {};
3629
+ return [
3630
+ propName,
3631
+ {
3632
+ type: typeof prop.type === "string" && prop.type.length > 0 ? prop.type : "unknown",
3633
+ description: typeof prop.description === "string" ? prop.description : "",
3634
+ required: Boolean(prop.required),
3635
+ default: prop.default,
3636
+ values: Array.isArray(prop.values) ? prop.values.filter((entry) => typeof entry === "string") : void 0,
3637
+ constraints: Array.isArray(prop.constraints) ? prop.constraints.filter(
3638
+ (entry) => typeof entry === "string"
3639
+ ) : void 0
3640
+ }
3641
+ ];
3642
+ })
3643
+ );
3644
+ }
3645
+ function normalizeExamples(examples) {
3646
+ return examples.map((example) => {
3647
+ const record = example && typeof example === "object" ? example : {};
3648
+ return {
3649
+ name: typeof record.title === "string" && record.title || typeof record.name === "string" && record.name || "Example",
3650
+ description: typeof record.description === "string" ? record.description : void 0,
3651
+ code: typeof record.code === "string" ? record.code : void 0,
3652
+ kind: typeof record.kind === "string" ? record.kind : void 0
3653
+ };
3654
+ });
3655
+ }
3656
+ function normalizeRelations(relations, fallback) {
3657
+ if (relations.length > 0) {
3658
+ return relations.map((relation) => {
3659
+ const record = relation && typeof relation === "object" ? relation : {};
3660
+ return {
3661
+ componentName: typeof record.component === "string" && record.component || typeof record.name === "string" && record.name || typeof record.componentName === "string" && record.componentName || typeof record.componentId === "string" && record.componentId || "Unknown",
3662
+ componentId: typeof record.componentId === "string" ? record.componentId : void 0,
3663
+ relationship: typeof record.relationship === "string" && record.relationship || typeof record.type === "string" && record.type || "related",
3664
+ note: typeof record.note === "string" ? record.note : void 0,
3665
+ description: typeof record.description === "string" ? record.description : void 0
3666
+ };
3667
+ });
3668
+ }
3669
+ return fallback.map((relation) => ({
3670
+ componentName: relation.name,
3671
+ componentId: relation.componentId,
3672
+ relationship: relation.type
3673
+ }));
3674
+ }
3675
+ function buildComponent(manifest, entry, shard) {
3676
+ const sourcePackage = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
3677
+ const component = shard.component;
3678
+ const componentRecord = component;
3679
+ const props = normalizeProps(component.props);
3680
+ return {
3681
+ id: shard.componentId,
3682
+ name: component.name,
3683
+ description: component.description,
3684
+ category: component.category,
3685
+ status: component.status,
3686
+ tags: [],
3687
+ props,
3688
+ propsSummary: Object.entries(props).map(
3689
+ ([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
3690
+ ),
3691
+ examples: normalizeExamples(component.examples),
3692
+ relations: normalizeRelations(component.relations, entry.relations),
3693
+ compoundChildren: entry.compoundChildren.map((child) => ({
3694
+ name: child.name,
3695
+ componentId: child.componentId,
3696
+ visibility: "public"
3697
+ })),
3698
+ guidance: {
3699
+ when: component.usageGuidance ? [component.usageGuidance] : [],
3700
+ whenNot: component.donts,
3701
+ guidelines: component.usageGuidance ? [component.usageGuidance] : [],
3702
+ accessibility: [],
3703
+ usageGuidance: component.usageGuidance || void 0,
3704
+ dos: component.dos,
3705
+ donts: component.donts,
3706
+ patterns: []
3707
+ },
3708
+ sourceType: "bundle",
3709
+ sourcePath: component.sourcePath,
3710
+ sourceRepoFullName: component.sourceRepoFullName,
3711
+ packageName: manifest.designSystem.packageName ?? void 0,
3712
+ importPath: sourcePackage,
3713
+ publicRef: component.publicRef,
3714
+ publicSlug: component.publicSlug,
3715
+ isCanonical: componentRecord.isCanonical ?? component.tier === "core",
3716
+ tier: component.tier,
3717
+ parentComponentName: component.parentComponentName,
3718
+ metadata: {
3719
+ a11yRules: [],
3720
+ scenarioTags: []
3721
+ }
3722
+ };
3723
+ }
3724
+ var BundleAdapter = class {
3725
+ name = "bundle";
3726
+ discover(startDir) {
3727
+ return findBundleManifest(startDir);
3728
+ }
3729
+ async load(projectRoot) {
3730
+ const manifests = this.discover(projectRoot);
3731
+ if (manifests.length === 0) {
3732
+ throw new Error(
3733
+ `No ${BRAND.dataDir}/${BRAND.manifestFile} found. Run \`${BRAND.cliCommand} context install --cloud\` or commit a Fragments bundle into your workspace.`
3734
+ );
3735
+ }
3736
+ const manifestPath = manifests[0];
3737
+ const manifest = await readJsonFile(
3738
+ manifestPath,
3739
+ bundleManifestSchema,
3740
+ "bundle manifest"
3741
+ );
3742
+ const bundleDir = dirname3(manifestPath);
3743
+ const repoRoot = dirname3(bundleDir);
3744
+ const tokensPath = resolve2(bundleDir, "tokens.json");
3745
+ const tokensFile = existsSync6(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
3746
+ const components = Object.fromEntries(
3747
+ await Promise.all(
3748
+ Object.values(manifest.components).map(async (entry) => {
3749
+ const shardPath = resolve2(repoRoot, entry.file);
3750
+ const shard = await readJsonFile(
3751
+ shardPath,
3752
+ bundleComponentShardSchema,
3753
+ `bundle component shard (${entry.name})`
3754
+ );
3755
+ return [entry.componentId, buildComponent(manifest, entry, shard)];
3756
+ })
3757
+ )
3758
+ );
3759
+ const packageName = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
3760
+ const packageMap = packageName ? Object.fromEntries(
3761
+ Object.values(components).map((component) => [component.name, packageName])
3762
+ ) : {};
3763
+ const tokens = tokensFile ? {
3764
+ prefix: "",
3765
+ total: tokensFile.flat.length,
3766
+ categories: Object.fromEntries(
3767
+ Object.entries(tokensFile.categories).map(([category, value]) => [
3768
+ category,
3769
+ value.tokens.map((token) => ({
3770
+ name: token.name,
3771
+ category: token.category,
3772
+ value: token.value,
3773
+ description: token.description,
3774
+ path: token.path,
3775
+ type: token.type
3776
+ }))
3777
+ ])
3778
+ ),
3779
+ flat: tokensFile.flat.map((token) => ({
3780
+ name: token.name,
3781
+ category: token.category,
3782
+ value: token.value,
3783
+ description: token.description,
3784
+ path: token.path,
3785
+ type: token.type
3786
+ }))
3787
+ } : void 0;
3788
+ const snapshot = validateSnapshot({
3789
+ schemaVersion: 1,
3790
+ sourceType: "bundle",
3791
+ sourceLabel: `${BRAND.dataDir}/${BRAND.manifestFile}`,
3792
+ capabilities: buildCapabilities({
3793
+ components,
3794
+ tokens
3795
+ }),
3796
+ metadata: {
3797
+ designSystemName: manifest.designSystem.name,
3798
+ packageName: manifest.designSystem.packageName ?? void 0,
3799
+ importPath: manifest.designSystem.importPath ?? void 0,
3800
+ revision: manifest.catalogRevision,
3801
+ updatedAt: manifest.catalogUpdatedAt
3802
+ },
3803
+ components,
3804
+ tokens,
3805
+ packageMap,
3806
+ defaultPackageName: packageName
3807
+ });
3808
+ const hydratedComponents = Object.fromEntries(
3809
+ Object.entries(snapshot.components).map(([componentId, component]) => [
3810
+ componentId,
3811
+ {
3812
+ ...component,
3813
+ isCanonical: components[componentId]?.isCanonical ?? component.tier === "core"
3814
+ }
3815
+ ])
3816
+ );
3817
+ return {
3818
+ snapshot: {
3819
+ ...snapshot,
3820
+ components: hydratedComponents
3821
+ },
3822
+ components: hydratedComponents,
3823
+ blocks: snapshot.blocks,
3824
+ tokens: snapshot.tokens,
3825
+ graph: snapshot.graph,
3826
+ performanceSummary: snapshot.performanceSummary,
3827
+ packageMap: snapshot.packageMap,
3828
+ defaultPackageName: snapshot.defaultPackageName,
3829
+ capabilities: new Set(snapshot.capabilities)
3830
+ };
3831
+ }
3832
+ };
3833
+
3834
+ // src/source-selection.ts
3835
+ function resolveCloudApiKey2(config, fileConfig) {
3836
+ return config.cloudApiKey ?? fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
3837
+ }
3838
+ function resolveCloudUrl2(fileConfig) {
3839
+ return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
3840
+ }
3841
+ function hasTsProject(projectRoot) {
3842
+ return existsSync7(join6(projectRoot, "tsconfig.json")) || existsSync7(join6(projectRoot, "tsconfig.app.json"));
3843
+ }
3844
+ function resolveDataAdapter(config, fileConfig) {
3845
+ const source = config.source ?? fileConfig?.source ?? "auto";
3846
+ const fragmentsJsonPaths = findFragmentsJson(config.projectRoot);
3847
+ const bundleManifestPaths = findBundleManifest(config.projectRoot);
3848
+ const cloudApiKey = resolveCloudApiKey2(config, fileConfig);
3849
+ const cloudUrl = resolveCloudUrl2(fileConfig);
3850
+ switch (source) {
3851
+ case "fragments-json":
3852
+ return { adapter: new FragmentsJsonAdapter(), mode: "fragments-json" };
3853
+ case "cloud":
3854
+ if (!cloudApiKey) {
3855
+ throw new Error(
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."
3857
+ );
3858
+ }
3859
+ return {
3860
+ adapter: new CloudCatalogAdapter({ apiKey: cloudApiKey, url: cloudUrl }),
3861
+ mode: "cloud"
3862
+ };
3863
+ case "bundle":
3864
+ return { adapter: new BundleAdapter(), mode: "bundle" };
3865
+ case "extract":
3866
+ return { adapter: new AutoExtractionAdapter(), mode: "extract" };
3867
+ case "auto":
3868
+ default:
3869
+ if (fragmentsJsonPaths.length > 0) {
3870
+ return {
3871
+ adapter: new FragmentsJsonAdapter(),
3872
+ mode: "fragments-json"
3873
+ };
3874
+ }
3875
+ if (cloudApiKey) {
3876
+ return {
3877
+ adapter: new CloudCatalogAdapter({
3878
+ apiKey: cloudApiKey,
3879
+ url: cloudUrl
3880
+ }),
3881
+ mode: "cloud"
3882
+ };
3883
+ }
3884
+ if (bundleManifestPaths.length > 0) {
3885
+ return {
3886
+ adapter: new BundleAdapter(),
3887
+ mode: "bundle"
3888
+ };
3889
+ }
3890
+ if (hasTsProject(config.projectRoot)) {
3891
+ return { adapter: new AutoExtractionAdapter(), mode: "extract" };
3892
+ }
3893
+ return {
3894
+ adapter: new FragmentsJsonAdapter(),
3895
+ mode: "fragments-json"
3896
+ };
3897
+ }
3898
+ }
3899
+ function resolveSearchApiKey(config, fileConfig) {
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);
4036
+ }
4037
+
4038
+ // src/server.ts
4039
+ var TOOL_NAMES = buildToolNames();
4040
+ var TOOLS = buildMcpTools();
4041
+ var TOOL_DEFINITION_BY_KEY = new Map(
4042
+ MCP_TOOL_DEFINITIONS.map((definition) => [definition.key, definition])
4043
+ );
4044
+ function createMcpServer(config) {
4045
+ const server = new Server(
4046
+ {
4047
+ name: `${BRAND.nameLower}-mcp`,
4048
+ version: MCP_SERVER_VERSION
4049
+ },
4050
+ {
4051
+ capabilities: {
4052
+ tools: { listChanged: true },
4053
+ resources: { listChanged: false },
4054
+ logging: {}
4055
+ }
4056
+ }
4057
+ );
4058
+ const registry = new ToolRegistry("", {
4059
+ onChanged: () => {
4060
+ server.notification({ method: "notifications/tools/list_changed", params: {} });
4061
+ }
4062
+ });
4063
+ registry.registerBuiltins(
4064
+ { core: CORE_TOOLS, viewer: VIEWER_TOOLS, infra: INFRA_TOOLS },
4065
+ MCP_TOOL_DEFINITIONS,
4066
+ TOOL_CAPABILITIES
4067
+ );
4068
+ const fileConfig = config.fileConfig ?? loadConfigFile(config.projectRoot) ?? void 0;
4069
+ const mergedConfig = {
4070
+ ...fileConfig ? { ...config, fileConfig } : config,
4071
+ searchApiKey: resolveSearchApiKey(config, fileConfig)
4072
+ };
4073
+ const adapter = config.adapter ?? resolveDataAdapter(mergedConfig, fileConfig).adapter;
4074
+ config.onRegistry?.(registry);
4075
+ if (fileConfig?.tools?.exclude) {
4076
+ for (const key of fileConfig.tools.exclude) {
4077
+ registry.unregister(key);
4078
+ }
4079
+ }
4080
+ let cachedData = null;
4081
+ let loadDataPromise = null;
4082
+ let resolvedRoot = null;
4083
+ let resolveProjectRootPromise = null;
4084
+ async function resolveProjectRoot() {
4085
+ if (resolvedRoot) return resolvedRoot;
4086
+ if (resolveProjectRootPromise) return resolveProjectRootPromise;
4087
+ resolveProjectRootPromise = (async () => {
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 {
4097
+ }
4098
+ }
4099
+ resolvedRoot = config.projectRoot;
4100
+ return resolvedRoot;
4101
+ })();
4102
+ try {
4103
+ return await resolveProjectRootPromise;
4104
+ } finally {
4105
+ resolveProjectRootPromise = null;
4106
+ }
4107
+ }
4108
+ async function loadData() {
4109
+ if (cachedData) return cachedData;
4110
+ if (loadDataPromise) return loadDataPromise;
4111
+ loadDataPromise = (async () => {
4112
+ const projectRoot = await resolveProjectRoot();
4113
+ const loaded = await adapter.load(projectRoot);
4114
+ cachedData = loaded;
4115
+ return loaded;
4116
+ })();
4117
+ try {
4118
+ return await loadDataPromise;
4119
+ } finally {
4120
+ loadDataPromise = null;
4121
+ }
4122
+ }
4123
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
4124
+ const data = await loadData();
4125
+ return {
4126
+ tools: registry.listTools(
4127
+ {
4128
+ hasViewer: false,
4129
+ hasPlayground: false,
4130
+ capabilities: data.capabilities
4131
+ },
4132
+ TOOLS
4133
+ )
4134
+ };
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
+ });
4164
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4165
+ const { name, arguments: args } = request.params;
4166
+ const data = await loadData();
4167
+ const toolContext = {
4168
+ data,
4169
+ config: mergedConfig,
4170
+ indexes: { componentIndex: null, blockIndex: null, tokenIndex: null },
4171
+ mcp: {
4172
+ server,
4173
+ clientCapabilities: server.getClientCapabilities()
4174
+ },
4175
+ resolvePackageName: (name2) => {
4176
+ if (name2) {
4177
+ const pkg = data.packageMap[name2];
4178
+ if (pkg) return pkg;
4179
+ }
4180
+ if (data.defaultPackageName) return data.defaultPackageName;
4181
+ if (data.snapshot.sourceType === "cloud") {
4182
+ return "your-component-library";
4183
+ }
4184
+ const root = resolvedRoot ?? config.projectRoot;
4185
+ const packageJsonPath = join7(root, "package.json");
4186
+ if (existsSync8(packageJsonPath)) {
4187
+ try {
4188
+ const content = readFileSync6(packageJsonPath, "utf-8");
4189
+ const pkg = JSON.parse(content);
4190
+ if (pkg.name) {
4191
+ return pkg.name;
4192
+ }
4193
+ } catch {
4194
+ }
4195
+ }
4196
+ return "your-component-library";
4197
+ },
4198
+ toolNames: TOOL_NAMES
4199
+ };
4200
+ try {
4201
+ const toolKey = registry.resolveKey(name);
4202
+ const definition = TOOL_DEFINITION_BY_KEY.get(toolKey);
4203
+ const argumentKeys = Object.keys(args ?? {});
4204
+ const allowedKeys = new Set(Object.keys(definition?.params ?? {}));
4205
+ const unknownKeys = definition ? argumentKeys.filter((key) => !allowedKeys.has(key)) : [];
4206
+ if (unknownKeys.length > 0) {
4207
+ return {
4208
+ content: [
4209
+ {
4210
+ type: "text",
4211
+ text: JSON.stringify({
4212
+ error: `Unknown argument(s) for ${toolKey}: ${unknownKeys.join(", ")}`
4213
+ })
4214
+ }
4215
+ ],
4216
+ isError: true
4217
+ };
4218
+ }
4219
+ const mCtx = {
4220
+ toolName: name,
4221
+ toolKey,
4222
+ args: args ?? {},
4223
+ ctx: toolContext
4224
+ };
4225
+ return await executeWithMiddleware(
4226
+ config.middleware ?? [],
4227
+ mCtx,
4228
+ () => registry.execute(name, args ?? {}, toolContext)
4229
+ );
4230
+ } catch (error) {
4231
+ return {
4232
+ content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) }],
4233
+ isError: true
4234
+ };
4235
+ }
4236
+ });
4237
+ return server;
4238
+ }
4239
+ async function startMcpServer(config) {
4240
+ const server = createMcpServer(config);
4241
+ const transport = new StdioServerTransport();
4242
+ await server.connect(transport);
4243
+ }
4244
+ function createSandboxServer() {
4245
+ return createMcpServer({ projectRoot: process.cwd() });
4246
+ }
4247
+
4248
+ export {
4249
+ loadConfigFile,
4250
+ CORE_TOOLS,
4251
+ VIEWER_TOOLS,
4252
+ INFRA_TOOLS,
4253
+ BUILTIN_TOOLS,
4254
+ ToolRegistry,
4255
+ executeWithMiddleware,
4256
+ telemetryMiddleware,
4257
+ componentFromCompiledFragment,
4258
+ blockFromCompiledBlock,
4259
+ tokensFromCompiledTokenData,
4260
+ buildCapabilities,
4261
+ validateSnapshot,
4262
+ FragmentsJsonAdapter,
4263
+ AutoExtractionAdapter,
4264
+ resolveDataAdapter,
4265
+ resolveSearchApiKey,
4266
+ createMcpServer,
4267
+ startMcpServer,
4268
+ createSandboxServer
4269
+ };
4270
+ //# sourceMappingURL=chunk-YJTMK4JY.js.map