@decantr/mcp-server 1.0.6 → 2.1.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/README.md CHANGED
@@ -13,6 +13,7 @@ Design intelligence for AI-generated UI. Make Claude, Cursor, and Windsurf gener
13
13
  ![Decantr MCP demo](https://raw.githubusercontent.com/decantr-ai/decantr/main/packages/mcp-server/assets/decantr-demo.gif)
14
14
 
15
15
  - **Structured design context** -- gives your AI assistant patterns, layouts, and component specs instead of letting it guess
16
+ - **Evidence-backed repair loops** -- gives AI agents Project Health, Evidence Bundles, workspace health, and scoped repair prompts without uploading source
16
17
  - **Drift detection** -- catches when generated code deviates from your design intent
17
18
  - **Zero config** -- run with `npx`, no API keys or accounts required
18
19
 
@@ -138,13 +139,17 @@ The server exposes Decantr registry, context, benchmark, and verification tools.
138
139
  | `decantr_compile_execution_packs` | Compile a hosted execution-pack bundle from a local or inline essence document | `{ "path": "./decantr.essence.json", "namespace": "@official" }` |
139
140
  | `decantr_audit_project` | Run the schema-backed Decantr project audit against essence and compiled packs, with hosted fallback when local pack artifacts are missing | `{ "namespace": "@official" }` |
140
141
  | `decantr_critique` | Critique a file against the compiled review contract, with hosted fallback when local review packs are missing | `{ "file_path": "./src/pages/Overview.tsx", "namespace": "@official" }` |
142
+ | `decantr_get_evidence_bundle` | Generate the local privacy-redacted Evidence Bundle for a project | `{ "project_path": "apps/web" }` |
143
+ | `decantr_workspace_health` | Discover Decantr projects and return aggregate workspace health | `{ "workspace_root": ".", "max_projects": 100 }` |
144
+ | `decantr_get_repair_prompt` | Return the scoped repair prompt for a health finding | `{ "finding_id": "assertion-contract-context-pack-manifest" }` |
145
+ | `decantr_run_health_loop` | Run health, evidence, and next repair prompt in one local agent loop | `{ "project_path": "apps/web" }` |
141
146
  | `decantr_get_showcase_benchmarks` | Read the audited showcase corpus manifest, shortlist, or verification report | `{ "view": "verification" }` |
142
147
 
143
148
  For the broader product surface and support policy, see the root Decantr docs and package support matrix.
144
149
 
145
150
  ## Compatibility
146
151
 
147
- `@decantr/mcp-server` is stable in the `1.x` line for the documented MCP tool surface.
152
+ `@decantr/mcp-server` is stable in the `2.x` line for the documented MCP tool surface.
148
153
 
149
154
  - new tools may be added in compatible releases
150
155
  - existing documented tool names and envelopes should not break without a major version
@@ -169,8 +174,10 @@ The AI assistant calls these tools behind the scenes:
169
174
  7. `decantr_check_drift` -- validates the generated code against the Essence spec before presenting it
170
175
  8. `decantr_critique` -- critiques a specific file, falling back to the hosted verifier when the local review pack is missing
171
176
  9. `decantr_audit_project` -- runs the stronger project-level audit once the implementation is in place
177
+ 10. `decantr_get_evidence_bundle` -- returns the local evidence bundle for the AI repair loop
178
+ 11. `decantr_get_repair_prompt` -- gives the assistant exact finding evidence, constraints to preserve, and commands to rerun
172
179
 
173
- The AI now generates code with the right layout structure, correct components, and consistent styling -- not a generic guess.
180
+ The AI now generates code with the right layout structure, correct components, and consistent styling, then gets a scoped evidence-backed repair loop instead of a generic guess.
174
181
 
175
182
  ## License
176
183
 
package/dist/bin.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-A4ZCCVQR.js";
2
+ import "./chunk-FEXPLJKB.js";
@@ -7,14 +7,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
7
7
  import { existsSync, readdirSync, readFileSync } from "fs";
8
8
  import { readFile as readFile2 } from "fs/promises";
9
9
  import { basename as basename2, dirname as dirname2, join as join2, relative as relative2 } from "path";
10
- import { evaluateGuard, isV3 as isV32, validateEssence } from "@decantr/essence-spec";
10
+ import { evaluateGuard, isV4 as isV42, validateEssence } from "@decantr/essence-spec";
11
11
  import { isContentIntelligenceSource, resolvePatternPreset } from "@decantr/registry";
12
12
 
13
13
  // src/helpers.ts
14
14
  import { realpathSync } from "fs";
15
15
  import { mkdir, readFile, writeFile } from "fs/promises";
16
16
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
17
- import { isV3, migrateV2ToV3 } from "@decantr/essence-spec";
17
+ import { isV4 } from "@decantr/essence-spec";
18
18
  import { RegistryAPIClient } from "@decantr/registry";
19
19
  var MAX_INPUT_LENGTH = 1e3;
20
20
  function validateStringArg(args, field) {
@@ -95,8 +95,13 @@ async function writeEssenceFile(essencePath, essence) {
95
95
  }
96
96
  async function mutateEssenceFile(essencePath, mutate) {
97
97
  const { essence, path } = await readEssenceFile(essencePath);
98
- const v3 = isV3(essence) ? structuredClone(essence) : migrateV2ToV3(essence);
99
- const updated = mutate(v3);
98
+ if (!isV4(essence)) {
99
+ throw new Error(
100
+ "Active Decantr V2 workflows require Essence v4.0.0. Run `decantr migrate --to v4` for older essence files."
101
+ );
102
+ }
103
+ const v4 = structuredClone(essence);
104
+ const updated = mutate(v4);
100
105
  await writeEssenceFile(path, updated);
101
106
  return { essence: updated, path };
102
107
  }
@@ -464,12 +469,332 @@ var WRITE_TOOL = {
464
469
  idempotentHint: false,
465
470
  openWorldHint: false
466
471
  };
472
+ var MCP_PROJECT_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/project-health-report.v1.json";
473
+ var MCP_WORKSPACE_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/workspace-health-report.v1.json";
474
+ var MCP_WORKSPACE_IGNORES = /* @__PURE__ */ new Set([
475
+ ".git",
476
+ ".next",
477
+ ".turbo",
478
+ ".vercel",
479
+ "coverage",
480
+ "dist",
481
+ "node_modules",
482
+ "playwright-report"
483
+ ]);
484
+ function mcpSlug(value) {
485
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
486
+ }
487
+ function mcpStatusFromCounts(counts) {
488
+ if (counts.errorCount > 0) return "error";
489
+ if (counts.warnCount > 0) return "warning";
490
+ return "healthy";
491
+ }
492
+ function mcpScoreFromCounts(counts) {
493
+ return Math.max(0, Math.min(100, 100 - counts.errorCount * 15 - counts.warnCount * 5 - counts.infoCount));
494
+ }
495
+ function mcpCommandsForFinding(source) {
496
+ switch (source) {
497
+ case "assertion":
498
+ return ["decantr refresh", "decantr health --evidence"];
499
+ case "brownfield":
500
+ return ["decantr analyze", "decantr init --existing --merge-proposal", "decantr health"];
501
+ case "browser":
502
+ return ["decantr health --browser", "decantr health --evidence"];
503
+ case "check":
504
+ return ["decantr check", "decantr health"];
505
+ case "design-token":
506
+ return ["decantr export --to figma-tokens", "decantr health --evidence"];
507
+ case "interaction":
508
+ return ["decantr check --strict", "decantr health"];
509
+ case "pack":
510
+ return ["decantr refresh", "decantr registry get-pack review --write-context", "decantr health"];
511
+ case "runtime":
512
+ return ["npm run build", "decantr health"];
513
+ default:
514
+ return ["decantr audit", "decantr health"];
515
+ }
516
+ }
517
+ function mcpSourceFromFinding(finding) {
518
+ const category = finding.category.toLowerCase();
519
+ const id = finding.id.toLowerCase();
520
+ const rule = finding.rule?.toLowerCase() ?? "";
521
+ if (category.includes("runtime") || category.includes("document") || category.includes("performance")) {
522
+ return "runtime";
523
+ }
524
+ if (category.includes("pack") || category.includes("review contract")) {
525
+ return "pack";
526
+ }
527
+ if (category.includes("interaction") || id.includes("interaction") || rule.includes("interaction")) {
528
+ return "interaction";
529
+ }
530
+ return "audit";
531
+ }
532
+ function mcpBuildRepairPrompt(input) {
533
+ return [
534
+ "You are fixing one Decantr Project Health finding in this local workspace.",
535
+ "",
536
+ "Read `DECANTR.md`, `decantr.essence.json`, and `.decantr/context/scaffold-pack.md` if they exist. For route or page work, read the matching page/section packs before editing.",
537
+ "",
538
+ `Finding: ${input.id}`,
539
+ `Source: ${input.source}`,
540
+ `Severity: ${input.severity}`,
541
+ `Category: ${input.category}`,
542
+ `Message: ${input.message}`,
543
+ input.evidence.length > 0 ? `Evidence:
544
+ ${input.evidence.map((entry) => `- ${entry}`).join("\n")}` : null,
545
+ input.suggestedFix ? `Suggested fix: ${input.suggestedFix}` : null,
546
+ "",
547
+ "Make the smallest coherent code or contract change that resolves this finding. Preserve the existing framework, routing, styling system, and Decantr workflow mode unless the finding explicitly requires a contract update.",
548
+ "Do not rewrite unrelated routes, replace the styling system, remove existing product behavior, or regenerate Decantr artifacts unless the finding is about stale or missing generated context.",
549
+ "",
550
+ `After the fix, run:
551
+ ${input.commands.map((command) => `- ${command}`).join("\n")}`
552
+ ].filter((line) => Boolean(line)).join("\n");
553
+ }
554
+ function mcpHealthFinding(input) {
555
+ const id = `${input.source}-${mcpSlug(input.baseId || input.rule || `${input.category}-${input.message}`)}`;
556
+ const commands = mcpCommandsForFinding(input.source);
557
+ return {
558
+ id,
559
+ source: input.source,
560
+ category: input.category,
561
+ severity: input.severity,
562
+ message: input.message,
563
+ evidence: input.evidence ?? [],
564
+ target: input.target,
565
+ file: input.file,
566
+ rule: input.rule,
567
+ suggestedFix: input.suggestedFix,
568
+ remediation: {
569
+ summary: input.suggestedFix || `Resolve ${input.category.toLowerCase()} finding.`,
570
+ commands,
571
+ prompt: mcpBuildRepairPrompt({
572
+ id,
573
+ source: input.source,
574
+ category: input.category,
575
+ severity: input.severity,
576
+ message: input.message,
577
+ evidence: input.evidence ?? [],
578
+ suggestedFix: input.suggestedFix,
579
+ commands
580
+ })
581
+ }
582
+ };
583
+ }
584
+ function mcpCollectDeclaredRoutes(essence) {
585
+ if (!essence || !isV42(essence)) return [];
586
+ return Object.keys(essence.blueprint.routes ?? {}).sort();
587
+ }
588
+ function mcpReportFromAudit(projectRoot, audit, assertions) {
589
+ const findings = [];
590
+ const seen = /* @__PURE__ */ new Set();
591
+ const pushUnique = (finding) => {
592
+ const key = `${finding.rule ?? finding.id}|${finding.message}`;
593
+ if (seen.has(key)) return;
594
+ seen.add(key);
595
+ findings.push(finding);
596
+ };
597
+ for (const finding of audit.findings) {
598
+ pushUnique(
599
+ mcpHealthFinding({
600
+ source: mcpSourceFromFinding(finding),
601
+ category: finding.category,
602
+ severity: finding.severity,
603
+ message: finding.message,
604
+ evidence: finding.evidence,
605
+ target: finding.target,
606
+ file: finding.file,
607
+ rule: finding.rule,
608
+ suggestedFix: finding.suggestedFix,
609
+ baseId: finding.id
610
+ })
611
+ );
612
+ }
613
+ for (const assertion of assertions) {
614
+ if (assertion.status !== "failed") continue;
615
+ pushUnique(
616
+ mcpHealthFinding({
617
+ source: "assertion",
618
+ category: `Contract ${assertion.category}`,
619
+ severity: assertion.severity,
620
+ message: assertion.message,
621
+ evidence: assertion.evidence,
622
+ target: assertion.target,
623
+ rule: assertion.rule,
624
+ suggestedFix: assertion.suggestedFix,
625
+ baseId: assertion.id
626
+ })
627
+ );
628
+ }
629
+ if (!audit.valid && findings.every((finding) => finding.severity !== "error")) {
630
+ pushUnique(
631
+ mcpHealthFinding({
632
+ source: "audit",
633
+ category: "Project Contract",
634
+ severity: "error",
635
+ message: "Project audit is not valid.",
636
+ evidence: ["The verifier returned valid=false."],
637
+ rule: "project-audit-invalid",
638
+ suggestedFix: "Resolve blocking audit findings and rerun `decantr health`."
639
+ })
640
+ );
641
+ }
642
+ const counts = {
643
+ errorCount: findings.filter((finding) => finding.severity === "error").length,
644
+ warnCount: findings.filter((finding) => finding.severity === "warn").length,
645
+ infoCount: findings.filter((finding) => finding.severity === "info").length
646
+ };
647
+ const manifest = audit.packManifest;
648
+ return {
649
+ $schema: MCP_PROJECT_HEALTH_SCHEMA_URL,
650
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
651
+ projectRoot,
652
+ status: mcpStatusFromCounts(counts),
653
+ score: mcpScoreFromCounts(counts),
654
+ summary: {
655
+ ...counts,
656
+ findingCount: findings.length,
657
+ workflowMode: null,
658
+ adoptionMode: null,
659
+ essenceVersion: audit.summary.essenceVersion,
660
+ pageCount: audit.summary.pageCount,
661
+ runtimeAuditChecked: audit.summary.runtimeAuditChecked,
662
+ runtimePassed: audit.summary.runtimePassed,
663
+ packManifestPresent: audit.summary.packManifestPresent,
664
+ reviewPackPresent: audit.summary.reviewPackPresent
665
+ },
666
+ routes: {
667
+ declared: mcpCollectDeclaredRoutes(audit.essence),
668
+ runtimeChecked: audit.runtimeAudit.routeHintsChecked,
669
+ runtimeMatched: audit.runtimeAudit.routeHintsMatched,
670
+ runtimeCoverageOk: audit.summary.runtimeAuditChecked ? audit.runtimeAudit.routeHintsCoverageOk : null,
671
+ issues: findings.filter(
672
+ (finding) => finding.category.toLowerCase().includes("route") || finding.rule?.toLowerCase().includes("route") || finding.id.toLowerCase().includes("route")
673
+ ).map((finding) => finding.message)
674
+ },
675
+ packs: {
676
+ manifestPresent: Boolean(manifest),
677
+ reviewPackPresent: Boolean(manifest?.review ?? audit.reviewPack),
678
+ scaffoldPackPresent: Boolean(manifest?.scaffold),
679
+ sectionPackCount: manifest?.sections.length ?? 0,
680
+ pagePackCount: manifest?.pages.length ?? 0,
681
+ mutationPackCount: manifest?.mutations?.length ?? 0,
682
+ generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
683
+ },
684
+ ci: {
685
+ recommendedCommand: "decantr health --ci --fail-on error",
686
+ failOn: "error"
687
+ },
688
+ findings
689
+ };
690
+ }
691
+ function resolveMcpProjectRoot(value) {
692
+ if (value == null) return process.cwd();
693
+ if (typeof value !== "string") {
694
+ throw new Error("project_path must be a string when provided.");
695
+ }
696
+ return resolveWorkspacePath(value);
697
+ }
698
+ async function getMcpHealthState(projectRoot) {
699
+ const { auditProject, createContractAssertions, createEvidenceBundle } = await import("@decantr/verifier");
700
+ const audit = await auditProject(projectRoot);
701
+ const assertions = createContractAssertions(projectRoot, audit);
702
+ const report = mcpReportFromAudit(projectRoot, audit, assertions);
703
+ const evidence = createEvidenceBundle({
704
+ projectRoot,
705
+ audit,
706
+ assertions,
707
+ report,
708
+ workspaceConfigPath: existsSync(join2(projectRoot, ".decantr", "workspace.json")) ? join2(projectRoot, ".decantr", "workspace.json") : null
709
+ });
710
+ return { audit, assertions, report, evidence };
711
+ }
712
+ function discoverMcpWorkspaceProjects(root, maxProjects = 500) {
713
+ const projects = [];
714
+ function walk(dir, depth) {
715
+ if (projects.length >= maxProjects || depth > 6) return;
716
+ if (existsSync(join2(dir, "decantr.essence.json"))) {
717
+ const path = relative2(root, dir).replace(/\\/g, "/") || ".";
718
+ projects.push({
719
+ id: path.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project",
720
+ path,
721
+ absolutePath: dir
722
+ });
723
+ return;
724
+ }
725
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
726
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
727
+ if (MCP_WORKSPACE_IGNORES.has(entry.name)) continue;
728
+ walk(join2(dir, entry.name), depth + 1);
729
+ if (projects.length >= maxProjects) return;
730
+ }
731
+ }
732
+ walk(root, 0);
733
+ return projects.sort((a, b) => a.path.localeCompare(b.path));
734
+ }
735
+ async function getMcpWorkspaceHealth(args) {
736
+ const root = args.workspace_root == null ? process.cwd() : resolveMcpProjectRoot(args.workspace_root);
737
+ const maxProjects = typeof args.max_projects === "number" && Number.isFinite(args.max_projects) ? Math.max(1, Math.floor(args.max_projects)) : 500;
738
+ const discovered = discoverMcpWorkspaceProjects(root, maxProjects);
739
+ const projects = [];
740
+ for (const project of discovered) {
741
+ const startedAt = Date.now();
742
+ try {
743
+ const state = await getMcpHealthState(project.absolutePath);
744
+ projects.push({
745
+ id: project.id,
746
+ path: project.path,
747
+ status: state.report.status,
748
+ score: state.report.score,
749
+ errorCount: state.report.summary.errorCount,
750
+ warnCount: state.report.summary.warnCount,
751
+ infoCount: state.report.summary.infoCount,
752
+ findingCount: state.report.summary.findingCount,
753
+ durationMs: Date.now() - startedAt,
754
+ changed: false,
755
+ source: "auto",
756
+ error: null
757
+ });
758
+ } catch (error) {
759
+ projects.push({
760
+ id: project.id,
761
+ path: project.path,
762
+ status: "failed",
763
+ score: 0,
764
+ errorCount: 1,
765
+ warnCount: 0,
766
+ infoCount: 0,
767
+ findingCount: 1,
768
+ durationMs: Date.now() - startedAt,
769
+ changed: false,
770
+ source: "auto",
771
+ error: error.message
772
+ });
773
+ }
774
+ }
775
+ return {
776
+ $schema: MCP_WORKSPACE_HEALTH_SCHEMA_URL,
777
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
778
+ workspaceRoot: "<workspace>",
779
+ changedOnly: false,
780
+ since: null,
781
+ summary: {
782
+ projectCount: discovered.length,
783
+ checkedCount: projects.length,
784
+ healthyCount: projects.filter((project) => project.status === "healthy").length,
785
+ warningCount: projects.filter((project) => project.status === "warning").length,
786
+ errorCount: projects.filter((project) => project.status === "error").length,
787
+ failedCount: projects.filter((project) => project.status === "failed").length
788
+ },
789
+ projects
790
+ };
791
+ }
467
792
  var TOOLS = [
468
793
  // 1. decantr_read_essence — local read
469
794
  {
470
795
  name: "decantr_read_essence",
471
796
  title: "Read Essence",
472
- description: "Read and return the current decantr.essence.json file from the working directory. For v3 files, optionally filter by layer (dna, blueprint, or full).",
797
+ description: "Read and return the current Essence v4 decantr.essence.json file from the working directory. Optionally filter by layer (dna, blueprint, or full).",
473
798
  inputSchema: {
474
799
  type: "object",
475
800
  properties: {
@@ -480,7 +805,7 @@ var TOOLS = [
480
805
  layer: {
481
806
  type: "string",
482
807
  enum: ["dna", "blueprint", "full"],
483
- description: "For v3 essences: return only the specified layer. Defaults to full."
808
+ description: "For Essence v4 files: return only the specified layer. Defaults to full."
484
809
  }
485
810
  }
486
811
  },
@@ -490,7 +815,7 @@ var TOOLS = [
490
815
  {
491
816
  name: "decantr_validate",
492
817
  title: "Validate Essence",
493
- description: "Validate a decantr.essence.json file against the schema and guard rules. For v3, reports DNA vs Blueprint violations separately.",
818
+ description: "Validate an Essence v4 decantr.essence.json file against the schema and guard rules, reporting DNA vs Blueprint violations separately.",
494
819
  inputSchema: {
495
820
  type: "object",
496
821
  properties: {
@@ -593,7 +918,7 @@ var TOOLS = [
593
918
  {
594
919
  name: "decantr_check_drift",
595
920
  title: "Check Drift",
596
- description: "Check if code changes violate the design intent captured in the Essence spec. For v3, returns separate dna_violations and blueprint_drift with autoFixable flags.",
921
+ description: "Check if code changes violate the design intent captured in the Essence v4 spec. Returns separate dna_violations and blueprint_drift with autoFixable flags.",
597
922
  inputSchema: {
598
923
  type: "object",
599
924
  properties: {
@@ -619,7 +944,7 @@ var TOOLS = [
619
944
  {
620
945
  name: "decantr_create_essence",
621
946
  title: "Create Essence",
622
- description: "Generate a valid v3 Essence spec skeleton from a project description. Returns a structured essence.json template based on the closest matching archetype and blueprint.",
947
+ description: "Generate a valid Essence v4 skeleton from a project description. Returns a sectioned decantr.essence.json template based on the closest matching archetype and blueprint.",
623
948
  inputSchema: {
624
949
  type: "object",
625
950
  properties: {
@@ -680,7 +1005,7 @@ var TOOLS = [
680
1005
  {
681
1006
  name: "decantr_update_essence",
682
1007
  title: "Update Essence",
683
- description: "Mutate the essence file: add/remove/update pages, update DNA or blueprint fields, add/remove features. Operates on v3 format (auto-migrates v2).",
1008
+ description: "Mutate an Essence v4 file: add/remove/update pages, update DNA or blueprint fields, add/remove features. Older projects must run `decantr migrate --to v4` first.",
684
1009
  inputSchema: {
685
1010
  type: "object",
686
1011
  properties: {
@@ -935,6 +1260,82 @@ var TOOLS = [
935
1260
  required: ["file_path"]
936
1261
  },
937
1262
  annotations: READ_ONLY_NETWORK
1263
+ },
1264
+ // 22. decantr_get_evidence_bundle — local reliability artifact
1265
+ {
1266
+ name: "decantr_get_evidence_bundle",
1267
+ title: "Get Evidence Bundle",
1268
+ description: "Generate a local Evidence Bundle for the current Decantr project. The bundle redacts source, prompts, secrets, absolute paths, repo names, and screenshots by default.",
1269
+ inputSchema: {
1270
+ type: "object",
1271
+ properties: {
1272
+ project_path: {
1273
+ type: "string",
1274
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
1275
+ }
1276
+ }
1277
+ },
1278
+ annotations: READ_ONLY
1279
+ },
1280
+ // 23. decantr_workspace_health — local workspace reliability scan
1281
+ {
1282
+ name: "decantr_workspace_health",
1283
+ title: "Workspace Health",
1284
+ description: "Discover Decantr projects in the active workspace and return deterministic aggregate health for monorepos with many Decantr apps.",
1285
+ inputSchema: {
1286
+ type: "object",
1287
+ properties: {
1288
+ workspace_root: {
1289
+ type: "string",
1290
+ description: "Optional relative workspace root inside the active workspace. Defaults to the current working directory."
1291
+ },
1292
+ max_projects: {
1293
+ type: "number",
1294
+ description: "Optional cap on discovered projects. Defaults to 500."
1295
+ }
1296
+ }
1297
+ },
1298
+ annotations: READ_ONLY
1299
+ },
1300
+ // 24. decantr_get_repair_prompt — local AI repair loop
1301
+ {
1302
+ name: "decantr_get_repair_prompt",
1303
+ title: "Get Repair Prompt",
1304
+ description: "Return an AI-ready repair prompt for a Project Health finding, including exact finding evidence, preserved constraints, do-not-change guidance, and rerun commands.",
1305
+ inputSchema: {
1306
+ type: "object",
1307
+ properties: {
1308
+ project_path: {
1309
+ type: "string",
1310
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
1311
+ },
1312
+ finding_id: {
1313
+ type: "string",
1314
+ description: "Optional finding id. Defaults to the first error or warning, then the first finding."
1315
+ }
1316
+ }
1317
+ },
1318
+ annotations: READ_ONLY
1319
+ },
1320
+ // 25. decantr_run_health_loop — local evidence + repair loop
1321
+ {
1322
+ name: "decantr_run_health_loop",
1323
+ title: "Run Health Loop",
1324
+ description: "Run Project Health, produce evidence, and return the next repair prompt for AI agents without uploading project source.",
1325
+ inputSchema: {
1326
+ type: "object",
1327
+ properties: {
1328
+ project_path: {
1329
+ type: "string",
1330
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
1331
+ },
1332
+ finding_id: {
1333
+ type: "string",
1334
+ description: "Optional finding id to target. Defaults to the first error or warning, then the first finding."
1335
+ }
1336
+ }
1337
+ },
1338
+ annotations: READ_ONLY
938
1339
  }
939
1340
  ];
940
1341
  async function handleTool(name, args) {
@@ -946,7 +1347,12 @@ async function handleTool(name, args) {
946
1347
  const raw = await readFile2(essencePath, "utf-8");
947
1348
  const essence = JSON.parse(raw);
948
1349
  const layer = args.layer;
949
- if (layer && isV32(essence)) {
1350
+ if (!isV42(essence)) {
1351
+ return {
1352
+ error: "Active Decantr V2 workflows require Essence v4.0.0. Run `decantr migrate --to v4` for older essence files."
1353
+ };
1354
+ }
1355
+ if (layer) {
950
1356
  if (layer === "dna") return essence.dna;
951
1357
  if (layer === "blueprint") return essence.blueprint;
952
1358
  }
@@ -975,13 +1381,13 @@ async function handleTool(name, args) {
975
1381
  } catch {
976
1382
  }
977
1383
  }
978
- if (result.valid && typeof essence === "object" && essence !== null && isV32(essence)) {
1384
+ if (result.valid && typeof essence === "object" && essence !== null && isV42(essence)) {
979
1385
  const dnaViolations = guardViolations.filter((v) => v.layer === "dna");
980
1386
  const blueprintViolations = guardViolations.filter((v) => v.layer === "blueprint");
981
1387
  const otherViolations = guardViolations.filter((v) => !v.layer);
982
1388
  return {
983
1389
  ...result,
984
- format: "v3",
1390
+ format: "v4",
985
1391
  dna_violations: dnaViolations,
986
1392
  blueprint_violations: blueprintViolations,
987
1393
  guardViolations: otherViolations
@@ -1189,51 +1595,43 @@ async function handleTool(name, args) {
1189
1595
  if (!validation.valid) {
1190
1596
  return { drifted: true, reason: "invalid_essence", errors: validation.errors };
1191
1597
  }
1598
+ if (!isV42(essence)) {
1599
+ return {
1600
+ drifted: true,
1601
+ reason: "legacy_essence",
1602
+ errors: [
1603
+ "Active Decantr V2 workflows require Essence v4.0.0. Run `decantr migrate --to v4` for older essence files."
1604
+ ]
1605
+ };
1606
+ }
1192
1607
  const violations = [];
1193
1608
  if (args.theme_used && typeof args.theme_used === "string") {
1194
- let expectedThemeId;
1195
- if (isV32(essence)) {
1196
- expectedThemeId = essence.dna.theme.id;
1197
- } else {
1198
- const expectedTheme = essence.theme;
1199
- expectedThemeId = expectedTheme?.id ?? expectedTheme?.style;
1200
- }
1609
+ const expectedThemeId = essence.dna.theme.id;
1201
1610
  if (expectedThemeId && args.theme_used !== expectedThemeId) {
1202
1611
  violations.push({
1203
1612
  rule: "theme-match",
1204
1613
  severity: "critical",
1205
1614
  message: `Theme drift: code uses "${args.theme_used}" but Essence specifies "${expectedThemeId}". Do not switch themes.`,
1206
- ...isV32(essence) ? { layer: "dna", autoFixable: false } : {}
1615
+ layer: "dna",
1616
+ autoFixable: false
1207
1617
  });
1208
1618
  }
1209
1619
  }
1210
1620
  if (args.page_id && typeof args.page_id === "string") {
1211
- let pages;
1212
- if (isV32(essence)) {
1213
- pages = essence.blueprint.pages;
1214
- } else {
1215
- pages = essence.structure || [];
1216
- }
1621
+ const pages = listEssencePages(essence);
1217
1622
  if (!pages.find((p) => p.id === args.page_id)) {
1218
1623
  violations.push({
1219
1624
  rule: "page-exists",
1220
1625
  severity: "critical",
1221
1626
  message: `Page "${args.page_id}" not found in Essence structure. Add it to the Essence before generating code for it.`,
1222
- ...isV32(essence) ? {
1223
- layer: "blueprint",
1224
- autoFixable: true,
1225
- autoFix: { type: "add_page", patch: { id: args.page_id } }
1226
- } : {}
1627
+ layer: "blueprint",
1628
+ autoFixable: true,
1629
+ autoFix: { type: "add_page", patch: { id: args.page_id } }
1227
1630
  });
1228
1631
  }
1229
1632
  }
1230
1633
  if (args.components_used && Array.isArray(args.components_used) && args.page_id && typeof args.page_id === "string") {
1231
- let pages;
1232
- if (isV32(essence)) {
1233
- pages = essence.blueprint.pages;
1234
- } else {
1235
- pages = essence.structure || [];
1236
- }
1634
+ const pages = listEssencePages(essence);
1237
1635
  const page = pages.find((p) => p.id === args.page_id);
1238
1636
  if (page && page.layout) {
1239
1637
  const expectedPatterns = /* @__PURE__ */ new Set();
@@ -1265,7 +1663,8 @@ async function handleTool(name, args) {
1265
1663
  rule: "component-pattern-match",
1266
1664
  severity: "warning",
1267
1665
  message: `Components [${unmatchedComponents.join(", ")}] do not match any pattern in page "${args.page_id}" layout. Expected patterns: [${[...expectedPatterns].join(", ")}].`,
1268
- ...isV32(essence) ? { layer: "blueprint", autoFixable: false } : {}
1666
+ layer: "blueprint",
1667
+ autoFixable: false
1269
1668
  });
1270
1669
  }
1271
1670
  }
@@ -1286,21 +1685,11 @@ async function handleTool(name, args) {
1286
1685
  }
1287
1686
  } catch {
1288
1687
  }
1289
- if (isV32(essence)) {
1290
- const dnaViolations = violations.filter((v) => v.layer === "dna");
1291
- const blueprintDrift = violations.filter((v) => v.layer === "blueprint");
1292
- const other = violations.filter((v) => !v.layer);
1293
- return {
1294
- drifted: violations.length > 0,
1295
- dna_violations: dnaViolations,
1296
- blueprint_drift: blueprintDrift,
1297
- other_violations: other,
1298
- checkedAgainst: essencePath
1299
- };
1300
- }
1301
1688
  return {
1302
1689
  drifted: violations.length > 0,
1303
- violations,
1690
+ dna_violations: violations.filter((v) => v.layer === "dna"),
1691
+ blueprint_drift: violations.filter((v) => v.layer === "blueprint"),
1692
+ other_violations: violations.filter((v) => !v.layer),
1304
1693
  checkedAgainst: essencePath
1305
1694
  };
1306
1695
  }
@@ -1348,8 +1737,17 @@ async function handleTool(name, args) {
1348
1737
  }
1349
1738
  const rawPages = pages || [{ id: "home", shell: "full-bleed", default_layout: ["hero"] }];
1350
1739
  const defaultShell = rawPages[0]?.shell || "sidebar-main";
1740
+ const sectionPages = rawPages.map((p, index) => ({
1741
+ id: p.id,
1742
+ route: p.id === "home" || index === 0 ? "/" : `/${p.id}`,
1743
+ ...p.shell !== defaultShell ? { shell_override: p.shell } : {},
1744
+ layout: p.default_layout || []
1745
+ }));
1746
+ const routes = Object.fromEntries(
1747
+ sectionPages.map((page) => [page.route, { section: bestMatch, page: page.id }])
1748
+ );
1351
1749
  const essence = {
1352
- version: "3.0.0",
1750
+ version: "4.0.0",
1353
1751
  dna: {
1354
1752
  theme: {
1355
1753
  id: "auradecantism",
@@ -1394,12 +1792,18 @@ async function handleTool(name, args) {
1394
1792
  },
1395
1793
  blueprint: {
1396
1794
  shell: defaultShell,
1397
- pages: rawPages.map((p) => ({
1398
- id: p.id,
1399
- ...p.shell !== defaultShell ? { shell_override: p.shell } : {},
1400
- layout: p.default_layout || []
1401
- })),
1402
- features
1795
+ sections: [
1796
+ {
1797
+ id: bestMatch,
1798
+ role: "primary",
1799
+ shell: defaultShell,
1800
+ features,
1801
+ description: `${bestMatch} primary section`,
1802
+ pages: sectionPages
1803
+ }
1804
+ ],
1805
+ features,
1806
+ routes
1403
1807
  },
1404
1808
  meta: {
1405
1809
  archetype: bestMatch,
@@ -1411,7 +1815,7 @@ async function handleTool(name, args) {
1411
1815
  return {
1412
1816
  essence,
1413
1817
  archetype: bestMatch,
1414
- format: "v3",
1818
+ format: "v4",
1415
1819
  instructions: `Save this as decantr.essence.json in your project root. Review the dna (design tokens), blueprint (pages/features), and meta (project config) sections and adjust to match your needs. The guard rules will validate your code against this spec.`,
1416
1820
  _generated: {
1417
1821
  matched_archetype: bestMatch,
@@ -1477,11 +1881,11 @@ async function handleTool(name, args) {
1477
1881
  };
1478
1882
  }
1479
1883
  try {
1480
- const { essence, path } = await mutateEssenceFile(args.path, (v3) => {
1884
+ const { essence, path } = await mutateEssenceFile(args.path, (v4) => {
1481
1885
  for (const v of violations) {
1482
- applyDriftAcceptance(v3, v, resolution, args.scope);
1886
+ applyDriftAcceptance(v4, v, resolution, args.scope);
1483
1887
  }
1484
- return v3;
1888
+ return v4;
1485
1889
  });
1486
1890
  return {
1487
1891
  status: resolution === "accept_scoped" ? "accepted_scoped" : "accepted",
@@ -1517,8 +1921,8 @@ async function handleTool(name, args) {
1517
1921
  };
1518
1922
  }
1519
1923
  try {
1520
- const { essence, path } = await mutateEssenceFile(args.path, (v3) => {
1521
- return applyEssenceUpdate(v3, operation, payload);
1924
+ const { essence, path } = await mutateEssenceFile(args.path, (v4) => {
1925
+ return applyEssenceUpdate(v4, operation, payload);
1522
1926
  });
1523
1927
  return {
1524
1928
  status: "updated",
@@ -1647,8 +2051,10 @@ async function handleTool(name, args) {
1647
2051
  } catch {
1648
2052
  return { error: "No valid essence file found. Run decantr init first." };
1649
2053
  }
1650
- if (!isV32(essence)) {
1651
- return { error: "Section context requires a v3 essence file. Run decantr migrate first." };
2054
+ if (!isV42(essence)) {
2055
+ return {
2056
+ error: "Section context requires Essence v4.0.0. Run `decantr migrate --to v4` first."
2057
+ };
1652
2058
  }
1653
2059
  const sections = essence.blueprint.sections || [];
1654
2060
  const section = sections.find((s) => s.id === sectionId);
@@ -2092,10 +2498,122 @@ async function handleTool(name, args) {
2092
2498
  }
2093
2499
  return auditProject(projectRoot);
2094
2500
  }
2501
+ case "decantr_get_evidence_bundle": {
2502
+ try {
2503
+ const projectRoot = resolveMcpProjectRoot(args.project_path);
2504
+ const state = await getMcpHealthState(projectRoot);
2505
+ return state.evidence;
2506
+ } catch (error) {
2507
+ return { error: error.message };
2508
+ }
2509
+ }
2510
+ case "decantr_workspace_health": {
2511
+ if (args.workspace_root != null && typeof args.workspace_root !== "string") {
2512
+ return { error: "Invalid workspace_root. Must be a string when provided." };
2513
+ }
2514
+ if (args.max_projects != null && (typeof args.max_projects !== "number" || !Number.isFinite(args.max_projects))) {
2515
+ return { error: "Invalid max_projects. Must be a finite number when provided." };
2516
+ }
2517
+ try {
2518
+ return await getMcpWorkspaceHealth(args);
2519
+ } catch (error) {
2520
+ return { error: error.message };
2521
+ }
2522
+ }
2523
+ case "decantr_get_repair_prompt": {
2524
+ if (args.finding_id != null && typeof args.finding_id !== "string") {
2525
+ return { error: "Invalid finding_id. Must be a string when provided." };
2526
+ }
2527
+ try {
2528
+ const projectRoot = resolveMcpProjectRoot(args.project_path);
2529
+ const state = await getMcpHealthState(projectRoot);
2530
+ const finding = (typeof args.finding_id === "string" ? state.report.findings.find((entry) => entry.id === args.finding_id) : void 0) ?? state.report.findings.find((entry) => entry.severity === "error") ?? state.report.findings.find((entry) => entry.severity === "warn") ?? state.report.findings[0] ?? null;
2531
+ if (!finding) {
2532
+ return {
2533
+ project: state.evidence.project,
2534
+ health: state.evidence.health,
2535
+ prompt: null,
2536
+ message: "No Project Health findings require repair.",
2537
+ commands: ["decantr health --evidence"]
2538
+ };
2539
+ }
2540
+ return {
2541
+ project: state.evidence.project,
2542
+ health: state.evidence.health,
2543
+ finding: {
2544
+ id: finding.id,
2545
+ source: finding.source,
2546
+ severity: finding.severity,
2547
+ category: finding.category,
2548
+ message: finding.message
2549
+ },
2550
+ prompt: finding.remediation.prompt,
2551
+ commands: finding.remediation.commands
2552
+ };
2553
+ } catch (error) {
2554
+ return { error: error.message };
2555
+ }
2556
+ }
2557
+ case "decantr_run_health_loop": {
2558
+ if (args.finding_id != null && typeof args.finding_id !== "string") {
2559
+ return { error: "Invalid finding_id. Must be a string when provided." };
2560
+ }
2561
+ try {
2562
+ const projectRoot = resolveMcpProjectRoot(args.project_path);
2563
+ const state = await getMcpHealthState(projectRoot);
2564
+ const finding = (typeof args.finding_id === "string" ? state.report.findings.find((entry) => entry.id === args.finding_id) : void 0) ?? state.report.findings.find((entry) => entry.severity === "error") ?? state.report.findings.find((entry) => entry.severity === "warn") ?? state.report.findings[0] ?? null;
2565
+ return {
2566
+ project: state.evidence.project,
2567
+ health: state.evidence.health,
2568
+ report: state.report,
2569
+ evidence: state.evidence,
2570
+ repair: finding === null ? {
2571
+ finding: null,
2572
+ prompt: null,
2573
+ commands: ["decantr health --evidence"],
2574
+ message: "No Project Health findings require repair."
2575
+ } : {
2576
+ finding: {
2577
+ id: finding.id,
2578
+ source: finding.source,
2579
+ severity: finding.severity,
2580
+ category: finding.category,
2581
+ message: finding.message
2582
+ },
2583
+ prompt: finding.remediation.prompt,
2584
+ commands: finding.remediation.commands
2585
+ }
2586
+ };
2587
+ } catch (error) {
2588
+ return { error: error.message };
2589
+ }
2590
+ }
2095
2591
  default:
2096
2592
  return { error: `Unknown tool: ${name}` };
2097
2593
  }
2098
2594
  }
2595
+ function listEssencePages(essence) {
2596
+ return essence.blueprint.sections.flatMap(
2597
+ (section) => section.pages.map((page) => ({ ...page, sectionId: section.id }))
2598
+ );
2599
+ }
2600
+ function getMutablePage(essence, id, sectionId) {
2601
+ for (const section of essence.blueprint.sections) {
2602
+ if (sectionId && section.id !== sectionId) continue;
2603
+ const index = section.pages.findIndex((page) => page.id === id);
2604
+ if (index !== -1) {
2605
+ return { page: section.pages[index], section, index };
2606
+ }
2607
+ }
2608
+ return null;
2609
+ }
2610
+ function getDefaultSection(essence) {
2611
+ const section = essence.blueprint.sections.find((s) => s.role === "primary") ?? essence.blueprint.sections[0];
2612
+ if (!section) {
2613
+ throw new Error("Essence v4 requires at least one blueprint section.");
2614
+ }
2615
+ return section;
2616
+ }
2099
2617
  function applyDriftAcceptance(essence, violation, resolution, scope) {
2100
2618
  switch (violation.rule) {
2101
2619
  case "theme-match":
@@ -2109,9 +2627,10 @@ function applyDriftAcceptance(essence, violation, resolution, scope) {
2109
2627
  case "page-exists":
2110
2628
  case "structure": {
2111
2629
  if (violation.page_id) {
2112
- const existing = essence.blueprint.pages.find((p) => p.id === violation.page_id);
2630
+ const section = getDefaultSection(essence);
2631
+ const existing = getMutablePage(essence, violation.page_id);
2113
2632
  if (!existing) {
2114
- essence.blueprint.pages.push({
2633
+ section.pages.push({
2115
2634
  id: violation.page_id,
2116
2635
  layout: []
2117
2636
  });
@@ -2134,11 +2653,15 @@ function applyEssenceUpdate(essence, operation, payload) {
2134
2653
  case "add_page": {
2135
2654
  const id = payload.id;
2136
2655
  if (!id) throw new Error('Payload must include "id" for add_page.');
2137
- const existing = essence.blueprint.pages.find((p) => p.id === id);
2656
+ const sectionId = payload.section_id;
2657
+ const section = sectionId ? essence.blueprint.sections.find((candidate) => candidate.id === sectionId) : getDefaultSection(essence);
2658
+ if (!section) throw new Error(`Section "${sectionId}" not found.`);
2659
+ const existing = getMutablePage(essence, id, section.id);
2138
2660
  if (existing) throw new Error(`Page "${id}" already exists.`);
2139
- essence.blueprint.pages.push({
2661
+ section.pages.push({
2140
2662
  id,
2141
2663
  layout: payload.layout || [],
2664
+ ...payload.route ? { route: payload.route } : {},
2142
2665
  ...payload.shell_override ? { shell_override: payload.shell_override } : {},
2143
2666
  ...payload.surface ? { surface: payload.surface } : {}
2144
2667
  });
@@ -2147,9 +2670,9 @@ function applyEssenceUpdate(essence, operation, payload) {
2147
2670
  case "remove_page": {
2148
2671
  const id = payload.id;
2149
2672
  if (!id) throw new Error('Payload must include "id" for remove_page.');
2150
- const idx = essence.blueprint.pages.findIndex((p) => p.id === id);
2151
- if (idx === -1) throw new Error(`Page "${id}" not found.`);
2152
- essence.blueprint.pages.splice(idx, 1);
2673
+ const match = getMutablePage(essence, id, payload.section_id);
2674
+ if (!match) throw new Error(`Page "${id}" not found.`);
2675
+ match.section.pages.splice(match.index, 1);
2153
2676
  break;
2154
2677
  }
2155
2678
  case "update_page_layout": {
@@ -2158,9 +2681,9 @@ function applyEssenceUpdate(essence, operation, payload) {
2158
2681
  if (!id) throw new Error('Payload must include "id" for update_page_layout.');
2159
2682
  if (!layout || !Array.isArray(layout))
2160
2683
  throw new Error('Payload must include "layout" array for update_page_layout.');
2161
- const page = essence.blueprint.pages.find((p) => p.id === id);
2162
- if (!page) throw new Error(`Page "${id}" not found.`);
2163
- page.layout = layout;
2684
+ const match = getMutablePage(essence, id, payload.section_id);
2685
+ if (!match) throw new Error(`Page "${id}" not found.`);
2686
+ match.page.layout = layout;
2164
2687
  break;
2165
2688
  }
2166
2689
  case "update_dna": {
@@ -2178,7 +2701,7 @@ function applyEssenceUpdate(essence, operation, payload) {
2178
2701
  }
2179
2702
  case "update_blueprint": {
2180
2703
  for (const [key, value] of Object.entries(payload)) {
2181
- if (key === "pages") continue;
2704
+ if (key === "pages" || key === "sections") continue;
2182
2705
  essence.blueprint[key] = value;
2183
2706
  }
2184
2707
  break;
@@ -2224,7 +2747,7 @@ function describeUpdate(operation, payload) {
2224
2747
  }
2225
2748
 
2226
2749
  // src/index.ts
2227
- var VERSION = "0.2.0";
2750
+ var VERSION = "2.0.0";
2228
2751
  var server = new Server({ name: "decantr", version: VERSION }, { capabilities: { tools: {} } });
2229
2752
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2230
2753
  return { tools: TOOLS };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import "./chunk-A4ZCCVQR.js";
1
+ import "./chunk-FEXPLJKB.js";
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@decantr/mcp-server",
3
- "version": "1.0.6",
3
+ "version": "2.1.0",
4
4
  "mcpName": "io.github.decantr-ai/mcp-server",
5
5
  "description": "MCP server for Decantr — exposes design intelligence, packs, and verification to AI coding assistants",
6
6
  "keywords": [
7
+ "decantr",
8
+ "decantr-ai",
7
9
  "mcp",
8
10
  "mcp-server",
9
11
  "model-context-protocol",
@@ -15,6 +17,7 @@
15
17
  "windsurf",
16
18
  "design-system",
17
19
  "design-intelligence",
20
+ "project-health",
18
21
  "ui-generation",
19
22
  "drift-detection"
20
23
  ],
@@ -46,9 +49,9 @@
46
49
  },
47
50
  "dependencies": {
48
51
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@decantr/essence-spec": "1.0.6",
50
- "@decantr/registry": "1.0.4",
51
- "@decantr/verifier": "1.0.6"
52
+ "@decantr/essence-spec": "2.0.1",
53
+ "@decantr/verifier": "2.1.0",
54
+ "@decantr/registry": "2.0.0"
52
55
  },
53
56
  "scripts": {
54
57
  "build": "tsup",