@fragments-sdk/mcp 0.7.1 → 0.8.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.
package/dist/bin.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  resolveDataAdapter,
5
5
  resolveSearchApiKey,
6
6
  startMcpServer
7
- } from "./chunk-HGGAXLRO.js";
7
+ } from "./chunk-6JMX4AMO.js";
8
8
  import "./chunk-4SVS3AA3.js";
9
9
  import "./chunk-YSRGQDEB.js";
10
10
 
@@ -2462,6 +2462,205 @@ var governHandler = async (args, ctx) => {
2462
2462
  }
2463
2463
  };
2464
2464
 
2465
+ // src/tools/validate-and-fix.ts
2466
+ import {
2467
+ formatVerdict as formatVerdict2,
2468
+ fragments as fragmentsPreset2,
2469
+ handleGovernTool as handleGovernTool2,
2470
+ universal as universal2
2471
+ } from "@fragments-sdk/govern";
2472
+ function classifyComponent(component) {
2473
+ if (component.status === "deprecated") return "discouraged";
2474
+ if (component.isCanonical && component.tier === "core") return "preferred";
2475
+ return "allowed";
2476
+ }
2477
+ function buildEffectiveComponents(ctx) {
2478
+ const selectionByKey = new Map(
2479
+ (ctx.data.validateFixContext?.components ?? []).map((component) => [
2480
+ component.componentKey,
2481
+ component.selection
2482
+ ])
2483
+ );
2484
+ return Object.entries(ctx.data.components).map(([componentKey, component]) => ({
2485
+ component,
2486
+ selection: selectionByKey.get(componentKey) ?? classifyComponent(component)
2487
+ }));
2488
+ }
2489
+ function cloneSpec(spec) {
2490
+ return structuredClone(spec);
2491
+ }
2492
+ function getSelectionNames(effectiveComponents, selection) {
2493
+ const selections = Array.isArray(selection) ? selection : [selection];
2494
+ return Array.from(
2495
+ new Set(
2496
+ effectiveComponents.filter((entry) => selections.includes(entry.selection)).map((entry) => entry.component.name)
2497
+ )
2498
+ );
2499
+ }
2500
+ function buildBasePolicy(ctx) {
2501
+ const tokenPrefix = ctx.data.tokens?.prefix;
2502
+ const basePolicy = tokenPrefix === "fui-" ? { rules: fragmentsPreset2().rules } : { rules: universal2().rules };
2503
+ const allowedComponents = buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
2504
+ basePolicy.rules["components/allow"] = {
2505
+ enabled: true,
2506
+ severity: "serious",
2507
+ options: {
2508
+ components: allowedComponents
2509
+ }
2510
+ };
2511
+ return basePolicy;
2512
+ }
2513
+ async function runGovern(spec, ctx, policyOverrides) {
2514
+ const input = {
2515
+ spec,
2516
+ policy: policyOverrides,
2517
+ format: "json"
2518
+ };
2519
+ const engineOptions = ctx.data.tokens ? { tokenData: ctx.data.tokens } : void 0;
2520
+ return handleGovernTool2(input, buildBasePolicy(ctx), engineOptions);
2521
+ }
2522
+ function applyDeterministicReplacements(spec, ctx) {
2523
+ const effectiveComponents = buildEffectiveComponents(ctx);
2524
+ const byName = /* @__PURE__ */ new Map();
2525
+ const preferredByCategory = /* @__PURE__ */ new Map();
2526
+ for (const entry of effectiveComponents) {
2527
+ const list = byName.get(entry.component.name) ?? [];
2528
+ list.push(entry);
2529
+ byName.set(entry.component.name, list);
2530
+ if (entry.selection !== "preferred") continue;
2531
+ const category = entry.component.category ?? "uncategorized";
2532
+ const names = preferredByCategory.get(category) ?? [];
2533
+ if (!names.includes(entry.component.name)) {
2534
+ names.push(entry.component.name);
2535
+ }
2536
+ preferredByCategory.set(category, names);
2537
+ }
2538
+ const fixedSpec = cloneSpec(spec);
2539
+ const nodes = Array.isArray(fixedSpec.nodes) ? fixedSpec.nodes : [];
2540
+ const replacements = [];
2541
+ for (const node of nodes) {
2542
+ if (!node.type) continue;
2543
+ const componentEntries = byName.get(node.type) ?? [];
2544
+ const component = componentEntries[0];
2545
+ if (!component || component.selection !== "discouraged" && component.selection !== "forbidden") {
2546
+ continue;
2547
+ }
2548
+ const candidates = (preferredByCategory.get(component.component.category ?? "uncategorized") ?? []).filter((candidate) => candidate !== component.component.name);
2549
+ if (candidates.length !== 1) continue;
2550
+ const replacement = candidates[0];
2551
+ replacements.push({
2552
+ nodeId: node.id ?? node.type,
2553
+ from: component.component.name,
2554
+ to: replacement,
2555
+ reason: "deprecated_component"
2556
+ });
2557
+ node.type = replacement;
2558
+ }
2559
+ return {
2560
+ fixedSpec,
2561
+ replacements
2562
+ };
2563
+ }
2564
+ function buildSummary(args) {
2565
+ const lines = [
2566
+ `status: ${args.status}`,
2567
+ `original: ${formatVerdict2(args.originalVerdict, "summary")}`
2568
+ ];
2569
+ if (args.replacements.length > 0) {
2570
+ lines.push(
2571
+ `replacements: ${args.replacements.map((item) => `${item.from} -> ${item.to}`).join(", ")}`
2572
+ );
2573
+ lines.push(`final: ${formatVerdict2(args.finalVerdict, "summary")}`);
2574
+ }
2575
+ return lines.join("\n");
2576
+ }
2577
+ var validateAndFixHandler = async (args, ctx) => {
2578
+ const spec = args?.spec;
2579
+ if (!spec || typeof spec !== "object") {
2580
+ return {
2581
+ content: [
2582
+ {
2583
+ type: "text",
2584
+ text: JSON.stringify({
2585
+ error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
2586
+ })
2587
+ }
2588
+ ],
2589
+ isError: true
2590
+ };
2591
+ }
2592
+ const policyOverrides = args?.policy;
2593
+ const applyFixes = args?.applyFixes !== false;
2594
+ const format = args?.format ?? "json";
2595
+ try {
2596
+ const effectiveComponents = buildEffectiveComponents(ctx);
2597
+ const originalVerdict = await runGovern(spec, ctx, policyOverrides);
2598
+ let finalVerdict = originalVerdict;
2599
+ let fixedSpec;
2600
+ let replacements = [];
2601
+ if (!originalVerdict.passed && applyFixes) {
2602
+ const result = applyDeterministicReplacements(
2603
+ spec,
2604
+ ctx
2605
+ );
2606
+ replacements = result.replacements;
2607
+ fixedSpec = result.fixedSpec;
2608
+ if (replacements.length > 0) {
2609
+ finalVerdict = await runGovern(fixedSpec, ctx, policyOverrides);
2610
+ }
2611
+ }
2612
+ const status = originalVerdict.passed ? "pass" : replacements.length > 0 ? "fixed" : "fail";
2613
+ const payload = {
2614
+ status,
2615
+ applyFixes,
2616
+ replacements,
2617
+ originalVerdict,
2618
+ finalVerdict,
2619
+ ...fixedSpec ? { fixedSpec } : {},
2620
+ preferredComponents: getSelectionNames(
2621
+ effectiveComponents,
2622
+ "preferred"
2623
+ ),
2624
+ discouragedComponents: getSelectionNames(effectiveComponents, [
2625
+ "discouraged",
2626
+ "forbidden"
2627
+ ])
2628
+ };
2629
+ return {
2630
+ content: [
2631
+ {
2632
+ type: "text",
2633
+ text: format === "summary" ? buildSummary({
2634
+ status,
2635
+ originalVerdict,
2636
+ finalVerdict,
2637
+ replacements
2638
+ }) : JSON.stringify(payload)
2639
+ }
2640
+ ],
2641
+ _meta: {
2642
+ status,
2643
+ replacementCount: replacements.length,
2644
+ passed: finalVerdict.passed
2645
+ }
2646
+ };
2647
+ } catch (error) {
2648
+ const message = error instanceof Error ? error.message : String(error);
2649
+ const isSpecError = message.includes("Expected") || message.includes("Required");
2650
+ return {
2651
+ content: [
2652
+ {
2653
+ type: "text",
2654
+ text: JSON.stringify({
2655
+ error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id: string, type: string, props: object, children?: string[] }] }` : message
2656
+ })
2657
+ }
2658
+ ],
2659
+ isError: true
2660
+ };
2661
+ }
2662
+ };
2663
+
2465
2664
  // src/tools/generate-ui.ts
2466
2665
  var generateUiHandler = async (args, ctx) => {
2467
2666
  const prompt = args?.prompt;
@@ -2491,6 +2690,142 @@ var generateUiHandler = async (args, ctx) => {
2491
2690
  };
2492
2691
  };
2493
2692
 
2693
+ // src/findings-service.ts
2694
+ var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
2695
+ function normalizeCloudUrl(url) {
2696
+ if (!url) return DEFAULT_CLOUD_URL;
2697
+ return url.replace(/\/+$/, "");
2698
+ }
2699
+ async function fetchFindings(apiKey, params, cloudUrl) {
2700
+ const base = normalizeCloudUrl(cloudUrl);
2701
+ const url = new URL(`${base}/api/findings`);
2702
+ if (params.status) url.searchParams.set("status", params.status);
2703
+ if (params.severity) url.searchParams.set("severity", params.severity);
2704
+ if (params.category) url.searchParams.set("category", params.category);
2705
+ if (params.ruleId) url.searchParams.set("ruleId", params.ruleId);
2706
+ if (params.filePath) url.searchParams.set("filePath", params.filePath);
2707
+ if (params.limit != null) url.searchParams.set("limit", String(params.limit));
2708
+ const response = await fetch(url.toString(), {
2709
+ headers: { "X-API-Key": apiKey }
2710
+ });
2711
+ if (!response.ok) {
2712
+ const body = await response.text();
2713
+ let message;
2714
+ try {
2715
+ const parsed = JSON.parse(body);
2716
+ message = parsed.error ?? body;
2717
+ } catch {
2718
+ message = body;
2719
+ }
2720
+ throw new Error(
2721
+ `Cloud findings API error (${response.status}): ${message}`
2722
+ );
2723
+ }
2724
+ return await response.json();
2725
+ }
2726
+ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
2727
+ return fetchFindings(
2728
+ apiKey,
2729
+ { status: "open", filePath, limit: 200 },
2730
+ cloudUrl
2731
+ );
2732
+ }
2733
+
2734
+ // src/tools/findings.ts
2735
+ function resolveCloudApiKey(ctx) {
2736
+ return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
2737
+ }
2738
+ function resolveCloudUrl(ctx) {
2739
+ return ctx.config.fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
2740
+ }
2741
+ function missingKeyError() {
2742
+ return {
2743
+ content: [
2744
+ {
2745
+ type: "text",
2746
+ text: JSON.stringify({
2747
+ error: "Cloud API key required. Set FRAGMENTS_API_KEY env var, pass --cloud-api-key, or configure cloud.apiKey in ds-mcp.config.json."
2748
+ })
2749
+ }
2750
+ ],
2751
+ isError: true
2752
+ };
2753
+ }
2754
+ var findingsListHandler = async (args, ctx) => {
2755
+ const apiKey = resolveCloudApiKey(ctx);
2756
+ if (!apiKey) return missingKeyError();
2757
+ const cloudUrl = resolveCloudUrl(ctx);
2758
+ const params = {};
2759
+ if (args.status) params.status = args.status;
2760
+ if (args.severity)
2761
+ params.severity = args.severity;
2762
+ if (args.category) params.category = String(args.category);
2763
+ if (args.ruleId) params.ruleId = String(args.ruleId);
2764
+ if (args.filePath) params.filePath = String(args.filePath);
2765
+ if (args.limit != null) params.limit = Number(args.limit);
2766
+ try {
2767
+ const result = await fetchFindings(apiKey, params, cloudUrl);
2768
+ return {
2769
+ content: [{ type: "text", text: JSON.stringify(result) }],
2770
+ _meta: { count: result.findings.length }
2771
+ };
2772
+ } catch (error) {
2773
+ return {
2774
+ content: [
2775
+ {
2776
+ type: "text",
2777
+ text: JSON.stringify({
2778
+ error: error instanceof Error ? error.message : String(error)
2779
+ })
2780
+ }
2781
+ ],
2782
+ isError: true
2783
+ };
2784
+ }
2785
+ };
2786
+ var findingsForFileHandler = async (args, ctx) => {
2787
+ const apiKey = resolveCloudApiKey(ctx);
2788
+ if (!apiKey) return missingKeyError();
2789
+ const filePath = args.filePath;
2790
+ if (!filePath || typeof filePath !== "string") {
2791
+ return {
2792
+ content: [
2793
+ {
2794
+ type: "text",
2795
+ text: JSON.stringify({ error: "filePath is required." })
2796
+ }
2797
+ ],
2798
+ isError: true
2799
+ };
2800
+ }
2801
+ const cloudUrl = resolveCloudUrl(ctx);
2802
+ try {
2803
+ const result = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
2804
+ const findings = result.findings.filter((f) => f.filePath === filePath);
2805
+ return {
2806
+ content: [
2807
+ {
2808
+ type: "text",
2809
+ text: JSON.stringify({ findings, filePath })
2810
+ }
2811
+ ],
2812
+ _meta: { count: findings.length, filePath }
2813
+ };
2814
+ } catch (error) {
2815
+ return {
2816
+ content: [
2817
+ {
2818
+ type: "text",
2819
+ text: JSON.stringify({
2820
+ error: error instanceof Error ? error.message : String(error)
2821
+ })
2822
+ }
2823
+ ],
2824
+ isError: true
2825
+ };
2826
+ }
2827
+ };
2828
+
2494
2829
  // src/tools/index.ts
2495
2830
  var CORE_TOOLS = {
2496
2831
  discover: discoverHandler,
@@ -2499,7 +2834,10 @@ var CORE_TOOLS = {
2499
2834
  tokens: tokensHandler,
2500
2835
  graph: graphHandler,
2501
2836
  perf: perfHandler,
2502
- govern: governHandler
2837
+ govern: governHandler,
2838
+ validate_and_fix: validateAndFixHandler,
2839
+ findings_list: findingsListHandler,
2840
+ findings_for_file: findingsForFileHandler
2503
2841
  };
2504
2842
  var VIEWER_TOOLS = {
2505
2843
  render: renderHandler,
@@ -2521,6 +2859,7 @@ var TOOL_CAPABILITIES = {
2521
2859
  tokens: ["tokens"],
2522
2860
  graph: ["graph"],
2523
2861
  perf: ["performance"],
2862
+ validate_and_fix: ["components"],
2524
2863
  render: ["components"],
2525
2864
  fix: ["components"],
2526
2865
  a11y: ["components"]
@@ -3782,7 +4121,7 @@ function readPackageName(projectRoot) {
3782
4121
  }
3783
4122
 
3784
4123
  // src/adapters/cloud-catalog.ts
3785
- var DEFAULT_CLOUD_URL = "https://app.usefragments.com/api/catalog";
4124
+ var DEFAULT_CLOUD_URL2 = "https://app.usefragments.com/api/catalog";
3786
4125
  var TOKEN_CATEGORY_ALIASES2 = {
3787
4126
  color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
3788
4127
  spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
@@ -3795,10 +4134,14 @@ var TOKEN_CATEGORY_ALIASES2 = {
3795
4134
  surface: ["surface", "surfaces", "canvas", "card", "background"]
3796
4135
  };
3797
4136
  function normalizeCatalogUrl(url) {
3798
- if (!url) return DEFAULT_CLOUD_URL;
4137
+ if (!url) return DEFAULT_CLOUD_URL2;
3799
4138
  if (url.endsWith("/api/catalog")) return url;
3800
4139
  return `${url.replace(/\/+$/, "")}/api/catalog`;
3801
4140
  }
4141
+ function normalizeValidateFixUrl(url) {
4142
+ const base = normalizeCatalogUrl(url).replace(/\/api\/catalog$/, "");
4143
+ return `${base}/api/context?target=validate-fix`;
4144
+ }
3802
4145
  function normalizeValue(value) {
3803
4146
  return value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() ?? "";
3804
4147
  }
@@ -3917,17 +4260,54 @@ function mapComponent(component, designSystem) {
3917
4260
  }
3918
4261
  };
3919
4262
  }
4263
+ function normalizeValidateFixContext(raw) {
4264
+ const content = raw.content;
4265
+ if (!content || !Array.isArray(content.components)) {
4266
+ return void 0;
4267
+ }
4268
+ const components = content.components.filter(
4269
+ (component) => Boolean(component?.componentKey) && Boolean(component?.publicRef) && Boolean(component?.name) && Boolean(component?.selection)
4270
+ ).map((component) => ({
4271
+ componentKey: component.componentKey,
4272
+ publicRef: component.publicRef,
4273
+ name: component.name,
4274
+ category: component.category ?? "uncategorized",
4275
+ status: component.status ?? "stable",
4276
+ tier: component.tier ?? "composition",
4277
+ isCanonical: component.isCanonical ?? false,
4278
+ isActive: component.isActive ?? true,
4279
+ selection: component.selection ?? "allowed",
4280
+ reasons: component.reasons ?? [],
4281
+ usageGuidance: component.usageGuidance,
4282
+ dos: component.dos ?? [],
4283
+ donts: component.donts ?? []
4284
+ }));
4285
+ return {
4286
+ version: 1,
4287
+ catalogRevision: content.catalogRevision,
4288
+ updatedAt: content.updatedAt,
4289
+ policy: {
4290
+ mode: "cloud",
4291
+ endpoint: content.policy?.endpoint ?? "/api/govern/policy"
4292
+ },
4293
+ components
4294
+ };
4295
+ }
3920
4296
  var CloudCatalogAdapter = class {
3921
4297
  constructor(options) {
3922
4298
  this.options = options;
3923
4299
  }
3924
4300
  name = "cloud";
3925
4301
  async load(_projectRoot) {
3926
- const response = await fetch(normalizeCatalogUrl(this.options.url), {
3927
- headers: {
3928
- "X-API-Key": this.options.apiKey
3929
- }
3930
- });
4302
+ const headers = {
4303
+ "X-API-Key": this.options.apiKey
4304
+ };
4305
+ const [response, validateFixResponse] = await Promise.all([
4306
+ fetch(normalizeCatalogUrl(this.options.url), { headers }),
4307
+ fetch(normalizeValidateFixUrl(this.options.url), { headers }).catch(
4308
+ () => null
4309
+ )
4310
+ ]);
3931
4311
  if (!response.ok) {
3932
4312
  throw new Error(
3933
4313
  `Failed to load Cloud catalog (${response.status} ${response.statusText}).`
@@ -3969,6 +4349,9 @@ var CloudCatalogAdapter = class {
3969
4349
  packageMap,
3970
4350
  defaultPackageName: packageName
3971
4351
  });
4352
+ const validateFixContext = validateFixResponse && validateFixResponse.ok ? normalizeValidateFixContext(
4353
+ await validateFixResponse.json()
4354
+ ) : void 0;
3972
4355
  const hydratedComponents = Object.fromEntries(
3973
4356
  Object.entries(snapshot.components).map(([componentId, component]) => [
3974
4357
  componentId,
@@ -3990,6 +4373,7 @@ var CloudCatalogAdapter = class {
3990
4373
  performanceSummary: void 0,
3991
4374
  packageMap: snapshot.packageMap,
3992
4375
  defaultPackageName: snapshot.defaultPackageName,
4376
+ validateFixContext,
3993
4377
  capabilities: new Set(snapshot.capabilities)
3994
4378
  };
3995
4379
  }
@@ -4224,10 +4608,10 @@ var BundleAdapter = class {
4224
4608
  };
4225
4609
 
4226
4610
  // src/source-selection.ts
4227
- function resolveCloudApiKey(config, fileConfig) {
4611
+ function resolveCloudApiKey2(config, fileConfig) {
4228
4612
  return config.cloudApiKey ?? fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
4229
4613
  }
4230
- function resolveCloudUrl(fileConfig) {
4614
+ function resolveCloudUrl2(fileConfig) {
4231
4615
  return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
4232
4616
  }
4233
4617
  function hasTsProject(projectRoot) {
@@ -4237,8 +4621,8 @@ function resolveDataAdapter(config, fileConfig) {
4237
4621
  const source = config.source ?? fileConfig?.source ?? "auto";
4238
4622
  const fragmentsJsonPaths = findFragmentsJson(config.projectRoot);
4239
4623
  const bundleManifestPaths = findBundleManifest(config.projectRoot);
4240
- const cloudApiKey = resolveCloudApiKey(config, fileConfig);
4241
- const cloudUrl = resolveCloudUrl(fileConfig);
4624
+ const cloudApiKey = resolveCloudApiKey2(config, fileConfig);
4625
+ const cloudUrl = resolveCloudUrl2(fileConfig);
4242
4626
  switch (source) {
4243
4627
  case "fragments-json":
4244
4628
  return { adapter: new FragmentsJsonAdapter(), mode: "fragments-json" };
@@ -4498,4 +4882,4 @@ export {
4498
4882
  startMcpServer,
4499
4883
  createSandboxServer
4500
4884
  };
4501
- //# sourceMappingURL=chunk-HGGAXLRO.js.map
4885
+ //# sourceMappingURL=chunk-6JMX4AMO.js.map