@nifrajs/cli 2.11.0 → 2.12.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.
Files changed (177) hide show
  1. package/README.md +6 -0
  2. package/dist/assure-hydration.d.ts +14 -0
  3. package/dist/assure-hydration.d.ts.map +1 -0
  4. package/dist/assure-hydration.js +483 -0
  5. package/dist/assure-hydration.js.map +1 -0
  6. package/dist/assure.d.ts +35 -0
  7. package/dist/assure.d.ts.map +1 -1
  8. package/dist/assure.js +222 -1
  9. package/dist/assure.js.map +1 -1
  10. package/dist/capabilities-tool.d.ts +13 -0
  11. package/dist/capabilities-tool.d.ts.map +1 -1
  12. package/dist/capabilities-tool.js +64 -1
  13. package/dist/capabilities-tool.js.map +1 -1
  14. package/dist/check.d.ts +125 -10
  15. package/dist/check.d.ts.map +1 -1
  16. package/dist/check.js +706 -145
  17. package/dist/check.js.map +1 -1
  18. package/dist/cli.d.ts +23 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +194 -10
  21. package/dist/cli.js.map +1 -1
  22. package/dist/contract-proof.d.ts +49 -0
  23. package/dist/contract-proof.d.ts.map +1 -0
  24. package/dist/contract-proof.js +128 -0
  25. package/dist/contract-proof.js.map +1 -0
  26. package/dist/contracts.d.ts +22 -0
  27. package/dist/contracts.d.ts.map +1 -0
  28. package/dist/contracts.js +134 -0
  29. package/dist/contracts.js.map +1 -0
  30. package/dist/diagnostics.d.ts +23 -0
  31. package/dist/diagnostics.d.ts.map +1 -0
  32. package/dist/diagnostics.js +9 -0
  33. package/dist/diagnostics.js.map +1 -0
  34. package/dist/diff-tool.d.ts +2 -0
  35. package/dist/diff-tool.d.ts.map +1 -1
  36. package/dist/diff-tool.js +1 -1
  37. package/dist/diff-tool.js.map +1 -1
  38. package/dist/docs-search.d.ts +8 -3
  39. package/dist/docs-search.d.ts.map +1 -1
  40. package/dist/docs-search.js +36 -15
  41. package/dist/docs-search.js.map +1 -1
  42. package/dist/doctor.d.ts +28 -2
  43. package/dist/doctor.d.ts.map +1 -1
  44. package/dist/doctor.js +173 -7
  45. package/dist/doctor.js.map +1 -1
  46. package/dist/examples.d.ts +1 -2
  47. package/dist/examples.d.ts.map +1 -1
  48. package/dist/examples.js +37 -16
  49. package/dist/examples.js.map +1 -1
  50. package/dist/fix-recipes.d.ts +12 -0
  51. package/dist/fix-recipes.d.ts.map +1 -0
  52. package/dist/fix-recipes.js +106 -0
  53. package/dist/fix-recipes.js.map +1 -0
  54. package/dist/init-agents.d.ts.map +1 -1
  55. package/dist/init-agents.js +60 -4
  56. package/dist/init-agents.js.map +1 -1
  57. package/dist/internal/source-facts.d.ts +15 -0
  58. package/dist/internal/source-facts.d.ts.map +1 -0
  59. package/dist/internal/source-facts.js +82 -0
  60. package/dist/internal/source-facts.js.map +1 -0
  61. package/dist/internal/typescript-import.d.ts +9 -0
  62. package/dist/internal/typescript-import.d.ts.map +1 -1
  63. package/dist/internal/typescript-import.js +38 -0
  64. package/dist/internal/typescript-import.js.map +1 -1
  65. package/dist/introspect.d.ts +6 -2
  66. package/dist/introspect.d.ts.map +1 -1
  67. package/dist/introspect.js +53 -4
  68. package/dist/introspect.js.map +1 -1
  69. package/dist/load.d.ts +6 -0
  70. package/dist/load.d.ts.map +1 -1
  71. package/dist/load.js.map +1 -1
  72. package/dist/manifest-tool.d.ts.map +1 -1
  73. package/dist/manifest-tool.js +14 -9
  74. package/dist/manifest-tool.js.map +1 -1
  75. package/dist/mcp-docs-tools.d.ts.map +1 -1
  76. package/dist/mcp-docs-tools.js +3 -0
  77. package/dist/mcp-docs-tools.js.map +1 -1
  78. package/dist/mcp-http.js +1 -1
  79. package/dist/mcp-root.d.ts +83 -0
  80. package/dist/mcp-root.d.ts.map +1 -0
  81. package/dist/mcp-root.js +221 -0
  82. package/dist/mcp-root.js.map +1 -0
  83. package/dist/mcp-run.d.ts.map +1 -1
  84. package/dist/mcp-run.js +18 -4
  85. package/dist/mcp-run.js.map +1 -1
  86. package/dist/mcp.d.ts +27 -4
  87. package/dist/mcp.d.ts.map +1 -1
  88. package/dist/mcp.js +358 -25
  89. package/dist/mcp.js.map +1 -1
  90. package/dist/project-path.d.ts +3 -0
  91. package/dist/project-path.d.ts.map +1 -0
  92. package/dist/project-path.js +36 -0
  93. package/dist/project-path.js.map +1 -0
  94. package/dist/replay.d.ts +12 -0
  95. package/dist/replay.d.ts.map +1 -0
  96. package/dist/replay.js +36 -0
  97. package/dist/replay.js.map +1 -0
  98. package/dist/rules/codes.d.ts +42 -0
  99. package/dist/rules/codes.d.ts.map +1 -0
  100. package/dist/rules/codes.js +52 -0
  101. package/dist/rules/codes.js.map +1 -0
  102. package/dist/rules/comment-markers.d.ts +10 -0
  103. package/dist/rules/comment-markers.d.ts.map +1 -0
  104. package/dist/rules/comment-markers.js +24 -0
  105. package/dist/rules/comment-markers.js.map +1 -0
  106. package/dist/rules/index.d.ts +31 -0
  107. package/dist/rules/index.d.ts.map +1 -0
  108. package/dist/rules/index.js +133 -0
  109. package/dist/rules/index.js.map +1 -0
  110. package/dist/rules/legacy.d.ts +7 -0
  111. package/dist/rules/legacy.d.ts.map +1 -0
  112. package/dist/rules/legacy.js +85 -0
  113. package/dist/rules/legacy.js.map +1 -0
  114. package/dist/rules/routes.d.ts +12 -0
  115. package/dist/rules/routes.d.ts.map +1 -0
  116. package/dist/rules/routes.js +142 -0
  117. package/dist/rules/routes.js.map +1 -0
  118. package/dist/rules/security.d.ts +10 -0
  119. package/dist/rules/security.d.ts.map +1 -0
  120. package/dist/rules/security.js +471 -0
  121. package/dist/rules/security.js.map +1 -0
  122. package/dist/search-terms.d.ts +2 -0
  123. package/dist/search-terms.d.ts.map +1 -1
  124. package/dist/search-terms.js +5 -1
  125. package/dist/search-terms.js.map +1 -1
  126. package/dist/types-search.d.ts +1 -2
  127. package/dist/types-search.d.ts.map +1 -1
  128. package/dist/types-search.js +33 -16
  129. package/dist/types-search.js.map +1 -1
  130. package/dist/verification.d.ts +9 -0
  131. package/dist/verification.d.ts.map +1 -1
  132. package/dist/verification.js +17 -1
  133. package/dist/verification.js.map +1 -1
  134. package/dist/work-graph.d.ts +123 -0
  135. package/dist/work-graph.d.ts.map +1 -0
  136. package/dist/work-graph.js +465 -0
  137. package/dist/work-graph.js.map +1 -0
  138. package/docs/examples.json +32 -2
  139. package/docs/llms-full.txt +906 -15
  140. package/docs/types.json +1682 -130
  141. package/package.json +34 -9
  142. package/src/assure-hydration.ts +606 -0
  143. package/src/assure.ts +316 -2
  144. package/src/capabilities-tool.ts +80 -1
  145. package/src/check.ts +873 -168
  146. package/src/cli.ts +198 -11
  147. package/src/contract-proof.ts +194 -0
  148. package/src/contracts.ts +157 -0
  149. package/src/diagnostics.ts +32 -0
  150. package/src/diff-tool.ts +1 -1
  151. package/src/docs-search.ts +45 -15
  152. package/src/doctor.ts +259 -8
  153. package/src/examples.ts +43 -15
  154. package/src/fix-recipes.ts +126 -0
  155. package/src/init-agents.ts +61 -4
  156. package/src/internal/source-facts.ts +133 -0
  157. package/src/internal/typescript-import.ts +29 -0
  158. package/src/introspect.ts +75 -6
  159. package/src/load.ts +6 -0
  160. package/src/manifest-tool.ts +16 -10
  161. package/src/mcp-docs-tools.ts +3 -0
  162. package/src/mcp-http.ts +1 -1
  163. package/src/mcp-root.ts +271 -0
  164. package/src/mcp-run.ts +19 -4
  165. package/src/mcp.ts +438 -32
  166. package/src/project-path.ts +36 -0
  167. package/src/replay.ts +44 -0
  168. package/src/rules/codes.ts +57 -0
  169. package/src/rules/comment-markers.ts +26 -0
  170. package/src/rules/index.ts +175 -0
  171. package/src/rules/legacy.ts +93 -0
  172. package/src/rules/routes.ts +157 -0
  173. package/src/rules/security.ts +505 -0
  174. package/src/search-terms.ts +9 -4
  175. package/src/types-search.ts +39 -15
  176. package/src/verification.ts +18 -1
  177. package/src/work-graph.ts +613 -0
package/dist/mcp-run.js CHANGED
@@ -12,8 +12,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
12
12
  }
13
13
  return path;
14
14
  };
15
- import { existsSync } from "node:fs";
16
- import { resolve, sep } from "node:path";
15
+ import { existsSync, realpathSync } from "node:fs";
16
+ import { isAbsolute, relative, resolve, sep } from "node:path";
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { runApp } from "@nifrajs/runner";
19
19
  const ENTRY_CANDIDATES = ["backend.ts", "app.ts", "src/backend.ts", "src/app.ts"];
@@ -26,8 +26,22 @@ export async function loadBackend(cwd, entry) {
26
26
  }
27
27
  // The `entry` arg comes from MCP args; importing it executes it. Keep it inside the project root so a
28
28
  // crafted `entry` (`../../x`, an absolute path) can't run arbitrary modules outside the project.
29
- const root = resolve(cwd);
30
- if (found !== root && !found.startsWith(root + sep)) {
29
+ const root = realpathSync(resolve(cwd));
30
+ let canonicalFound;
31
+ try {
32
+ canonicalFound = realpathSync(found);
33
+ }
34
+ catch {
35
+ return { error: `refusing to load an unresolved backend entry: ${entry}` };
36
+ }
37
+ const rel = relative(root, canonicalFound);
38
+ // `isAbsolute` is not redundant with the `..` test: across two Windows drives `relative()` gives up
39
+ // and returns the absolute target, which starts with neither `..` nor the separator - and then
40
+ // `resolve(root, rel)` returns that same absolute path, so the round-trip check agrees with it too.
41
+ if (rel.startsWith("..") ||
42
+ rel === sep ||
43
+ isAbsolute(rel) ||
44
+ resolve(root, rel) !== canonicalFound) {
31
45
  return { error: `refusing to load a backend entry outside the project root: ${entry}` };
32
46
  }
33
47
  let mod;
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-run.js","sourceRoot":"","sources":["../src/mcp-run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-run.js","sourceRoot":"","sources":["../src/mcp-run.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts CHANGED
@@ -25,14 +25,18 @@
25
25
  * - `nifra_inspect` - read the running dev server's recent request traces (method/path/status/
26
26
  * duration/ISR) from the DevTools plugin: what your requests ACTUALLY did, not a guess.
27
27
  *
28
- * Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]`, run in the
29
- * project root. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
28
+ * Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]` (or
29
+ * `["mcp", "<dir>"]` to pin the project directory explicitly). The server does NOT silently trust its
30
+ * spawn directory: the root is resolved via `./mcp-root.ts` (marker walk-up, the client's MCP `roots`,
31
+ * fail-closed tools when no nifra project is found) and announced in `initialize` + on every project
32
+ * tool result. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
30
33
  * standard MCP progress notifications and request cancellation - no SDK dependency, the same
31
34
  * minimal-surface choice as the rest of nifra. The pure dispatch lives in `./mcp-protocol.ts`; this
32
35
  * module is the I/O shell (stdin loop, tool wiring, the run subprocess).
33
36
  */
34
37
  import type { LoadAppOptions, LoadedApp } from "./load.js";
35
38
  import { type McpPrompt, type McpResource, type McpServerFeatures, type McpTool } from "./mcp-protocol.js";
39
+ import { type McpRootVerdict } from "./mcp-root.js";
36
40
  /** Local dev-tool reads are intentionally bounded: MCP runs in an agent process and must not hang on or
37
41
  * buffer an unrelated loopback service just because a caller supplied its port. */
38
42
  export declare const LOCAL_TOOL_FETCH_TIMEOUT_MS = 2000;
@@ -89,7 +93,26 @@ export declare function extractBackendTools(backend: unknown): McpTool[];
89
93
  export declare function extractBackendResources(backend: unknown): McpResource[];
90
94
  /** Extract MCP prompts registered via `.prompt()` on the Nifra backend. Exported for the surfacing test. */
91
95
  export declare function extractBackendPrompts(backend: unknown): McpPrompt[];
92
- /** Run the stdio MCP server: read newline-delimited JSON-RPC from stdin, write responses to stdout. */
93
- export declare function runMcpServer(cwd: string, version: string): Promise<void>;
96
+ /**
97
+ * Wrap every project-scoped tool with the root guard: a bad root ({@link rootVerdict} `blocked`) makes
98
+ * the tool refuse with the remediation message instead of answering about whatever directory the
99
+ * server happened to start in; a good root stamps the result with `[nifra] project root: …` so the
100
+ * agent (and the human reading its transcript) can see WHICH project answered. Docs tools pass
101
+ * through untouched - they serve the bundled corpus, not the project.
102
+ */
103
+ export declare function guardTools(tools: McpTool[], verdict: McpRootVerdict): McpTool[];
104
+ /** Whether the client's `initialize` params declare the `roots` capability - the precondition for
105
+ * this server sending it a `roots/list` request. */
106
+ export declare function clientSupportsRoots(params: Record<string, unknown> | undefined): boolean;
107
+ /**
108
+ * Run the stdio MCP server: read newline-delimited JSON-RPC from stdin, write responses to stdout.
109
+ *
110
+ * The project root is resolved defensively rather than assumed (`./mcp-root.ts`): explicit
111
+ * `nifra mcp <dir>` beats a cwd guess, a cwd guess walks up to the nearest nifra marker, the client's
112
+ * MCP `roots` (requested after the handshake, re-requested on `roots/list_changed`) can correct an
113
+ * unambiguous wrong guess, and anything still unresolved fails closed per tool call. The effective
114
+ * root is announced in the `initialize` instructions and stamped on every project tool result.
115
+ */
116
+ export declare function runMcpServer(cwd: string, version: string, explicitDir?: string): Promise<void>;
94
117
  export {};
95
118
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
package/dist/mcp.js CHANGED
@@ -25,24 +25,31 @@
25
25
  * - `nifra_inspect` - read the running dev server's recent request traces (method/path/status/
26
26
  * duration/ISR) from the DevTools plugin: what your requests ACTUALLY did, not a guess.
27
27
  *
28
- * Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]`, run in the
29
- * project root. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
28
+ * Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]` (or
29
+ * `["mcp", "<dir>"]` to pin the project directory explicitly). The server does NOT silently trust its
30
+ * spawn directory: the root is resolved via `./mcp-root.ts` (marker walk-up, the client's MCP `roots`,
31
+ * fail-closed tools when no nifra project is found) and announced in `initialize` + on every project
32
+ * tool result. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
30
33
  * standard MCP progress notifications and request cancellation - no SDK dependency, the same
31
34
  * minimal-surface choice as the rest of nifra. The pure dispatch lives in `./mcp-protocol.ts`; this
32
35
  * module is the I/O shell (stdin loop, tool wiring, the run subprocess).
33
36
  */
37
+ import { realpathSync } from "node:fs";
34
38
  import { readFile, stat } from "node:fs/promises";
35
- import { basename, resolve, sep } from "node:path";
39
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
36
40
  import { fileURLToPath } from "node:url";
37
41
  import { reflectRoutes } from "@nifrajs/core/reflection";
38
42
  import { Glob } from "bun";
43
+ import { collectContractProof } from "./contract-proof.js";
39
44
  import { loadDocsCorpus } from "./docs-search.js";
40
45
  import { loadExamplesCorpus } from "./examples.js";
41
46
  import { describeProject } from "./introspect.js";
42
47
  import { detectMonorepo, loadMonorepoApps } from "./load.js";
43
48
  import { docsTools } from "./mcp-docs-tools.js";
44
49
  import { createMcpProtocolState, handleRpc, rpcError, } from "./mcp-protocol.js";
50
+ import { applyClientRoots, detectToolingDrift, driftNote, pathsFromRootsResult, resolveRootState, rootInstructions, rootVerdict, } from "./mcp-root.js";
45
51
  import { loadTypesCorpus } from "./types-search.js";
52
+ import { collectProjectWorkGraph } from "./work-graph.js";
46
53
  /** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
47
54
  * dev, `.js` once built). Each runs in a FRESH subprocess per call so the project's current code loads. */
48
55
  function childPath(name) {
@@ -552,7 +559,35 @@ export function resolveProjectDir(root, dir) {
552
559
  if (dir === undefined || dir === "")
553
560
  return root;
554
561
  const target = resolve(root, dir);
555
- return target === root || target.startsWith(root + sep) ? target : null;
562
+ let canonicalRoot;
563
+ try {
564
+ canonicalRoot = realpathSync(root);
565
+ }
566
+ catch {
567
+ const lexical = relative(root, target);
568
+ return lexical === "" || (!lexical.startsWith("..") && !isAbsolute(lexical)) ? target : null;
569
+ }
570
+ let probe = target;
571
+ const missing = [];
572
+ while (true) {
573
+ try {
574
+ const canonicalProbe = realpathSync(probe);
575
+ const rel = relative(canonicalRoot, canonicalProbe);
576
+ if (rel.startsWith("..") || isAbsolute(rel))
577
+ return null;
578
+ return missing.length === 0 ? canonicalProbe : resolve(canonicalProbe, ...missing.reverse());
579
+ }
580
+ catch {
581
+ const parent = resolve(probe, "..");
582
+ if (parent === probe)
583
+ return null;
584
+ // `basename`, not a slice past `parent`: when `parent` is the filesystem root its trailing
585
+ // separator is part of it, so `parent.length + 1` eats the first character of the segment and
586
+ // the path gets rebuilt wrong.
587
+ missing.push(basename(probe));
588
+ probe = parent;
589
+ }
590
+ }
556
591
  }
557
592
  /** Consistent error string for a `dir` that escapes the project root. */
558
593
  function dirError(dir) {
@@ -563,6 +598,100 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
563
598
  const warmRun = createWarmHandler("mcp-run", cwd, "run");
564
599
  const warmRender = createWarmHandler("mcp-render", cwd, "render");
565
600
  return [
601
+ {
602
+ name: "nifra_contract_proof",
603
+ description: "Compare the current route contract with its snapshot and join each changed route to its route-assurance and capability evidence. Returns hasBreaking as the gate. The typed-contract check is lazy and runs only when check is true.",
604
+ inputSchema: {
605
+ type: "object",
606
+ properties: {
607
+ baseline: {
608
+ type: "string",
609
+ description: "Snapshot path relative to the selected project directory. Defaults to api-snapshot.json.",
610
+ },
611
+ check: {
612
+ type: "boolean",
613
+ description: "Also run the typed-contract check. Default false to keep the diff lazy.",
614
+ },
615
+ dir: {
616
+ type: "string",
617
+ description: "Project subdirectory to inspect.",
618
+ },
619
+ },
620
+ additionalProperties: false,
621
+ },
622
+ handler: async (args) => {
623
+ const raw = args;
624
+ if (raw.dir !== undefined && typeof raw.dir !== "string") {
625
+ return JSON.stringify({ ok: false, error: "dir must be a string" }, null, 2);
626
+ }
627
+ if (raw.baseline !== undefined && typeof raw.baseline !== "string") {
628
+ return JSON.stringify({ ok: false, error: "baseline must be a string" }, null, 2);
629
+ }
630
+ const dir = raw.dir;
631
+ const baseline = raw.baseline;
632
+ const target = resolveProjectDir(cwd, dir);
633
+ if (target === null)
634
+ return dirError(dir);
635
+ if (baseline !== undefined && baseline.trim() === "") {
636
+ return JSON.stringify({ ok: false, error: "baseline must not be empty" }, null, 2);
637
+ }
638
+ try {
639
+ return JSON.stringify(await collectContractProof(target, {
640
+ ...(baseline === undefined ? {} : { baselinePath: baseline }),
641
+ ...(raw.check === undefined ? {} : { check: raw.check === true }),
642
+ }), null, 2);
643
+ }
644
+ catch (error) {
645
+ return JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }, null, 2);
646
+ }
647
+ },
648
+ },
649
+ {
650
+ name: "nifra_prove",
651
+ description: "Build the static verification work graph for this project. Pass changed files to get only impacted routes and the cheapest proof plan. The result includes a serializable evidence bundle and a machine-checkable stop condition. Requires a fresh build and never probes a running application.",
652
+ inputSchema: {
653
+ type: "object",
654
+ properties: {
655
+ files: {
656
+ type: "array",
657
+ items: { type: "string" },
658
+ description: "Project-relative changed files. Omit for a graph overview.",
659
+ },
660
+ minLevel: {
661
+ type: "integer",
662
+ minimum: 0,
663
+ maximum: 4,
664
+ description: "Required proof level for the stop condition. Defaults to 1.",
665
+ },
666
+ },
667
+ additionalProperties: false,
668
+ },
669
+ handler: async (args) => {
670
+ const files = args.files;
671
+ const minLevel = args.minLevel;
672
+ if ((files !== undefined &&
673
+ (!Array.isArray(files) || files.some((file) => typeof file !== "string"))) ||
674
+ (minLevel !== undefined &&
675
+ (typeof minLevel !== "number" ||
676
+ !Number.isInteger(minLevel) ||
677
+ minLevel < 0 ||
678
+ minLevel > 4))) {
679
+ return JSON.stringify({
680
+ ok: false,
681
+ error: "files must be strings and minLevel must be an integer from 0 to 4",
682
+ }, null, 2);
683
+ }
684
+ try {
685
+ return JSON.stringify(await collectProjectWorkGraph(cwd, {
686
+ ...(files === undefined ? {} : { changedFiles: files }),
687
+ ...(minLevel === undefined ? {} : { minLevel }),
688
+ }), null, 2);
689
+ }
690
+ catch (error) {
691
+ return JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "work graph failed" }, null, 2);
692
+ }
693
+ },
694
+ },
566
695
  {
567
696
  name: "nifra_context",
568
697
  description: "Get this nifra project's surface. Call it once UNFILTERED for a tight INDEX: the route list (API routes as `METHOD path`, page routes as `pattern → file`) + framework conventions + a pointer - cheap even on a big app, no per-route schema dump. Then pass `path` (a route prefix like /api/orders) and/or `kind` (api|pages) to fetch the FULL contract for that slice (body/query/response TS shapes + the exact typed-client call form).",
@@ -937,14 +1066,17 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
937
1066
  const target = resolveProjectDir(cwd, opts.dir);
938
1067
  if (target === null)
939
1068
  return dirError(opts.dir);
940
- return JSON.stringify(await collectCheckResult(target, {
1069
+ const result = await collectCheckResult(target, {
941
1070
  lintsOnly: opts.lintsOnly ?? false,
942
1071
  signal: context.signal,
943
1072
  // Bound the result so a large project can't emit an MCP message big enough to break the stdio
944
1073
  // transport (`-32000: Connection closed`). If `truncated` comes back, fix the shown diagnostics
945
1074
  // and re-run. The scan already skips gitignored trees (walkSource), so this is the safety net.
946
1075
  maxDiagnostics: 100,
947
- }), null, 2);
1076
+ });
1077
+ return JSON.stringify(result.structuredDiagnostics === undefined
1078
+ ? result
1079
+ : { ...result, diagnostics: result.structuredDiagnostics }, null, 2);
948
1080
  },
949
1081
  },
950
1082
  {
@@ -952,14 +1084,20 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
952
1084
  description: "Automatically fix diagnostic lints (such as rewriting hand-rolled fetch() calls to the typed nifra client, adding generic types to client factory calls, and resolving dependency drift in package.json). Runs diagnostics, applies all mechanical edit suggestions, applies doctor dependency fixes, and returns the remaining unresolved diagnostics.",
953
1085
  inputSchema: {
954
1086
  type: "object",
955
- properties: {},
1087
+ properties: {
1088
+ code: {
1089
+ type: "string",
1090
+ description: "Apply the registered recipe for one stable diagnostic code when available.",
1091
+ },
1092
+ },
956
1093
  additionalProperties: false,
957
1094
  },
958
- handler: async (_args, context) => {
1095
+ handler: async (args, context) => {
959
1096
  const { collectCheckResult } = await import("./check.js");
1097
+ const { applyDiagnosticRecipe } = await import("./fix-recipes.js");
960
1098
  const { applyDoctorAutoFix } = await import("./doctor.js");
1099
+ const { resolveInsideProject } = await import("./project-path.js");
961
1100
  const { writeFile, readFile } = await import("node:fs/promises");
962
- const { resolve } = await import("node:path");
963
1101
  let doctorResult = null;
964
1102
  try {
965
1103
  doctorResult = await applyDoctorAutoFix(cwd);
@@ -972,6 +1110,15 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
972
1110
  signal: context.signal,
973
1111
  maxDiagnostics: 100,
974
1112
  });
1113
+ const requestedCode = args.code;
1114
+ const recipeFixed = [];
1115
+ if (typeof requestedCode === "string") {
1116
+ for (const diagnostic of checkResult.structuredDiagnostics ?? []) {
1117
+ if (diagnostic.code !== requestedCode)
1118
+ continue;
1119
+ recipeFixed.push(...(await applyDiagnosticRecipe(cwd, diagnostic)));
1120
+ }
1121
+ }
975
1122
  const fixed = [];
976
1123
  for (const diag of checkResult.diagnostics) {
977
1124
  if (diag.file && diag.line && diag.suggestion?.kind === "edit" && diag.suggestion.diff) {
@@ -980,7 +1127,9 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
980
1127
  const beforeLine = diffLines.find((l) => l.startsWith("-"))?.slice(1);
981
1128
  const afterLine = diffLines.find((l) => l.startsWith("+"))?.slice(1);
982
1129
  if (beforeLine !== undefined && afterLine !== undefined) {
983
- const filePath = resolve(cwd, diag.file);
1130
+ const filePath = await resolveInsideProject(cwd, diag.file);
1131
+ if (filePath === undefined)
1132
+ continue;
984
1133
  const content = await readFile(filePath, "utf-8");
985
1134
  const lines = content.split("\n");
986
1135
  const idx = diag.line - 1;
@@ -1008,6 +1157,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
1008
1157
  return JSON.stringify({
1009
1158
  ok: finalResult.ok,
1010
1159
  fixed,
1160
+ recipeFixed,
1011
1161
  doctorFixed: doctorResult?.fixed ?? [],
1012
1162
  remainingDiagnostics: finalResult.diagnostics,
1013
1163
  }, null, 2);
@@ -1023,6 +1173,22 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
1023
1173
  type: "string",
1024
1174
  description: "Config path relative to the selected project directory. Default: nifra.assurance.ts.",
1025
1175
  },
1176
+ bundle: {
1177
+ type: "boolean",
1178
+ description: "Return the single gate bundle with verdict and explicit skipped gates.",
1179
+ },
1180
+ strict: {
1181
+ type: "boolean",
1182
+ description: "Treat warning diagnostics as failures in the bundle.",
1183
+ },
1184
+ hydration: {
1185
+ type: "boolean",
1186
+ description: "Run the hydration assurance gate.",
1187
+ },
1188
+ interact: {
1189
+ type: "boolean",
1190
+ description: "Run declared hydration probes after the client mounts.",
1191
+ },
1026
1192
  dir: {
1027
1193
  type: "string",
1028
1194
  description: 'Evaluate this project subdirectory (relative to the MCP root), e.g. "apps/api". Default: the project root.',
@@ -1039,10 +1205,62 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
1039
1205
  if (config !== undefined && config !== target && !config.startsWith(target + sep)) {
1040
1206
  return JSON.stringify({ ok: false, error: "config must stay inside the selected project directory" }, null, 2);
1041
1207
  }
1042
- const { collectAssuranceReport } = await import("./assure.js");
1208
+ const { collectAssuranceReport, collectAssureBundle } = await import("./assure.js");
1209
+ if (opts.bundle === true || opts.strict === true || opts.hydration === true) {
1210
+ return JSON.stringify(await collectAssureBundle(target, {
1211
+ ...(config === undefined ? {} : { config }),
1212
+ ...(opts.strict === undefined ? {} : { strict: opts.strict }),
1213
+ ...(opts.hydration === undefined ? {} : { hydration: opts.hydration }),
1214
+ ...(opts.interact === undefined ? {} : { interact: opts.interact }),
1215
+ }), null, 2);
1216
+ }
1043
1217
  return JSON.stringify(await collectAssuranceReport(target, config), null, 2);
1044
1218
  },
1045
1219
  },
1220
+ {
1221
+ name: "nifra_hydrate",
1222
+ description: "Run the hydration assurance gate and return stable NF-H diagnostics.",
1223
+ inputSchema: {
1224
+ type: "object",
1225
+ properties: {
1226
+ dir: { type: "string", description: "Project subdirectory to inspect." },
1227
+ interact: { type: "boolean", description: "Run declared hydration probes." },
1228
+ },
1229
+ additionalProperties: false,
1230
+ },
1231
+ handler: async (args) => {
1232
+ const opts = args;
1233
+ const target = resolveProjectDir(cwd, opts.dir);
1234
+ if (target === null)
1235
+ return dirError(opts.dir);
1236
+ const { runHydrationAssurance } = await import("./assure-hydration.js");
1237
+ return JSON.stringify(await runHydrationAssurance(target, { interact: opts.interact === true }), null, 2);
1238
+ },
1239
+ },
1240
+ {
1241
+ name: "nifra_replay",
1242
+ description: "Validate and dispatch a token-only replay metadata file.",
1243
+ inputSchema: {
1244
+ type: "object",
1245
+ properties: {
1246
+ file: { type: "string", description: "Replay file relative to the project root." },
1247
+ },
1248
+ required: ["file"],
1249
+ additionalProperties: false,
1250
+ },
1251
+ handler: async (args) => {
1252
+ const file = args.file;
1253
+ if (typeof file !== "string" || file.trim() === "")
1254
+ return JSON.stringify({ ok: false, error: "file is required" });
1255
+ const { runReplay } = await import("./replay.js");
1256
+ try {
1257
+ return JSON.stringify(await runReplay(cwd, file), null, 2);
1258
+ }
1259
+ catch (error) {
1260
+ return JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }, null, 2);
1261
+ }
1262
+ },
1263
+ },
1046
1264
  {
1047
1265
  name: "nifra_levels",
1048
1266
  description: "Compute this project's verification ladder and return { achieved, levels[] }, where each level carries { level, name, ok, reasons[] }. The ladder is cumulative: L0 typed contract (nifra check), L1 route assurance, L2 reviewed capability lockfile, L3 route trust manifest in sync, L4 contract invariants, so a level only holds when every level below it holds, and `achieved` is -1 when even L0 fails. This is the single gate that reports what a change actually proves: run it after finishing work, and treat any `reasons` on a level the project already claimed as a regression to fix.",
@@ -1261,12 +1479,17 @@ async function appDeclaredTools(loader) {
1261
1479
  return [];
1262
1480
  }
1263
1481
  }
1264
- /** Run the stdio MCP server: read newline-delimited JSON-RPC from stdin, write responses to stdout. */
1265
- export async function runMcpServer(cwd, version) {
1266
- let features;
1267
- const monorepo = await detectMonorepo(cwd);
1482
+ /** Tool names that never touch the project - they serve the bundled docs/examples/types corpus, so
1483
+ * they stay available (and unstamped) whatever the root state is. */
1484
+ const PROJECT_FREE_TOOLS = new Set(["nifra_docs", "nifra_example", "nifra_types", "nifra_learn"]);
1485
+ /** The id of this server's own `roots/list` request to the client. A fixed, namespaced string: the
1486
+ * server only ever has one roots request in flight, and no response to a CLIENT-initiated request can
1487
+ * carry it (clients pick their own ids; this one is recognisably ours). */
1488
+ const ROOTS_REQUEST_ID = "nifra:roots/list";
1489
+ async function createProjectContext(root) {
1490
+ const monorepo = await detectMonorepo(root);
1268
1491
  if (monorepo) {
1269
- const appEntries = await loadMonorepoApps(cwd, monorepo);
1492
+ const appEntries = await loadMonorepoApps(root, monorepo);
1270
1493
  const allResources = [];
1271
1494
  const allPrompts = [];
1272
1495
  for (const { name, cwd: appCwd } of appEntries) {
@@ -1275,22 +1498,118 @@ export async function runMcpServer(cwd, version) {
1275
1498
  allResources.push(...(ns.features.resources ?? []));
1276
1499
  allPrompts.push(...(ns.features.prompts ?? []));
1277
1500
  }
1278
- features = { resources: allResources, prompts: allPrompts };
1501
+ return {
1502
+ monorepo,
1503
+ features: { resources: allResources, prompts: allPrompts },
1504
+ loadAppCached: createCachedAppLoader(root),
1505
+ };
1279
1506
  }
1280
- else {
1281
- const loadAppCached = createCachedAppLoader(cwd);
1282
- features = await backendFeatures(loadAppCached, projectFeatures(cwd, loadAppCached));
1507
+ const loadAppCached = createCachedAppLoader(root);
1508
+ return {
1509
+ monorepo,
1510
+ features: await backendFeatures(loadAppCached, projectFeatures(root, loadAppCached)),
1511
+ loadAppCached,
1512
+ };
1513
+ }
1514
+ /**
1515
+ * Wrap every project-scoped tool with the root guard: a bad root ({@link rootVerdict} `blocked`) makes
1516
+ * the tool refuse with the remediation message instead of answering about whatever directory the
1517
+ * server happened to start in; a good root stamps the result with `[nifra] project root: …` so the
1518
+ * agent (and the human reading its transcript) can see WHICH project answered. Docs tools pass
1519
+ * through untouched - they serve the bundled corpus, not the project.
1520
+ */
1521
+ export function guardTools(tools, verdict) {
1522
+ return tools.map((tool) => {
1523
+ if (PROJECT_FREE_TOOLS.has(tool.name))
1524
+ return tool;
1525
+ return {
1526
+ ...tool,
1527
+ handler: async (args, context) => {
1528
+ if (verdict.blocked !== undefined) {
1529
+ return { content: [{ type: "text", text: verdict.blocked }], isError: true };
1530
+ }
1531
+ const result = await tool.handler(args, context);
1532
+ const note = { type: "text", text: verdict.note };
1533
+ if (typeof result === "string") {
1534
+ return { content: [{ type: "text", text: result }, note] };
1535
+ }
1536
+ return { ...result, content: [...(result.content ?? []), note] };
1537
+ },
1538
+ };
1539
+ });
1540
+ }
1541
+ /** Whether the client's `initialize` params declare the `roots` capability - the precondition for
1542
+ * this server sending it a `roots/list` request. */
1543
+ export function clientSupportsRoots(params) {
1544
+ const capabilities = params?.capabilities;
1545
+ if (typeof capabilities !== "object" || capabilities === null)
1546
+ return false;
1547
+ const roots = capabilities.roots;
1548
+ return typeof roots === "object" && roots !== null;
1549
+ }
1550
+ /**
1551
+ * Run the stdio MCP server: read newline-delimited JSON-RPC from stdin, write responses to stdout.
1552
+ *
1553
+ * The project root is resolved defensively rather than assumed (`./mcp-root.ts`): explicit
1554
+ * `nifra mcp <dir>` beats a cwd guess, a cwd guess walks up to the nearest nifra marker, the client's
1555
+ * MCP `roots` (requested after the handshake, re-requested on `roots/list_changed`) can correct an
1556
+ * unambiguous wrong guess, and anything still unresolved fails closed per tool call. The effective
1557
+ * root is announced in the `initialize` instructions and stamped on every project tool result.
1558
+ */
1559
+ export async function runMcpServer(cwd, version, explicitDir) {
1560
+ const requested = explicitDir !== undefined ? resolve(cwd, explicitDir) : cwd;
1561
+ if (explicitDir !== undefined && !(await stat(requested).catch(() => null))?.isDirectory()) {
1562
+ throw new Error(`nifra mcp: directory not found: ${requested}`);
1283
1563
  }
1284
- const loadAppCached = createCachedAppLoader(cwd);
1564
+ let rootState = await resolveRootState(requested, explicitDir !== undefined);
1565
+ let ctx = await createProjectContext(rootState.root);
1566
+ // The CLI answering may not be the nifra the project builds with (a globally installed server, a
1567
+ // stale agent config). Recomputed whenever the root moves, since a different project can install a
1568
+ // different version.
1569
+ let drift = await detectToolingDrift(rootState.root, version);
1285
1570
  const serverInfo = { name: "nifra", version };
1286
1571
  const state = createMcpProtocolState();
1287
1572
  const send = (message) => {
1288
1573
  process.stdout.write(`${JSON.stringify(message)}\n`);
1289
1574
  };
1575
+ let rootsSupported = false;
1576
+ const requestRoots = () => {
1577
+ if (!rootsSupported)
1578
+ return;
1579
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: ROOTS_REQUEST_ID, method: "roots/list" })}\n`);
1580
+ };
1581
+ const onRootsAnswer = async (message) => {
1582
+ const result = message.result;
1583
+ // An error response (or a malformed one) carries no roots: keep the current state - the absence
1584
+ // of workspace data is not a mismatch, and never a reason to move the root.
1585
+ if (result === undefined)
1586
+ return;
1587
+ const next = await applyClientRoots(rootState, pathsFromRootsResult(result));
1588
+ if (next.root !== rootState.root) {
1589
+ ctx = await createProjectContext(next.root);
1590
+ drift = await detectToolingDrift(next.root, version);
1591
+ }
1592
+ rootState = next;
1593
+ };
1594
+ // Root updates are serialized against tool dispatch: a message read AFTER the client's roots answer
1595
+ // must see the adopted root, even though dispatches themselves run concurrently.
1596
+ let rootsUpdate = Promise.resolve();
1290
1597
  const dispatch = async (message) => {
1598
+ // The client's answer to OUR `roots/list` request - a response (id, no method), which the pure
1599
+ // dispatch would reject as an unknown method. Intercept it before handleRpc ever sees it.
1600
+ if (message.method === undefined && message.id === ROOTS_REQUEST_ID) {
1601
+ // Swallow failures: a bad answer must not poison the chain and 500 every later dispatch.
1602
+ rootsUpdate = onRootsAnswer(message).catch(() => { });
1603
+ await rootsUpdate;
1604
+ return;
1605
+ }
1606
+ await rootsUpdate;
1607
+ if (message.method === "initialize") {
1608
+ rootsSupported = clientSupportsRoots(message.params);
1609
+ }
1291
1610
  let activeTools;
1292
- if (monorepo) {
1293
- const appEntries = await loadMonorepoApps(cwd, monorepo);
1611
+ if (ctx.monorepo) {
1612
+ const appEntries = await loadMonorepoApps(rootState.root, ctx.monorepo);
1294
1613
  const allTools = [];
1295
1614
  for (const { name, cwd: appCwd } of appEntries) {
1296
1615
  const loader = createCachedAppLoader(appCwd);
@@ -1302,16 +1621,30 @@ export async function runMcpServer(cwd, version) {
1302
1621
  }
1303
1622
  else {
1304
1623
  activeTools = [
1305
- ...projectTools(cwd, loadAppCached),
1306
- ...(await appDeclaredTools(loadAppCached)),
1624
+ ...projectTools(rootState.root, ctx.loadAppCached),
1625
+ ...(await appDeclaredTools(ctx.loadAppCached)),
1307
1626
  ];
1308
1627
  }
1628
+ const verdict = await rootVerdict(rootState);
1629
+ activeTools = guardTools(activeTools, drift === undefined || verdict.blocked !== undefined
1630
+ ? verdict
1631
+ : { ...verdict, note: `${verdict.note}\n${driftNote(drift)}` });
1632
+ const features = {
1633
+ ...ctx.features,
1634
+ instructions: rootInstructions(rootState, drift),
1635
+ };
1309
1636
  const response = await handleRpc(message, activeTools, serverInfo, features, {
1310
1637
  state,
1311
1638
  sendNotification: send,
1312
1639
  });
1313
1640
  if (response)
1314
1641
  send(response);
1642
+ // Ask for the client's workspace roots once the handshake completes, and again whenever the
1643
+ // client says they changed. The answer comes back through the interception above.
1644
+ if (message.method === "notifications/initialized" ||
1645
+ message.method === "notifications/roots/list_changed") {
1646
+ requestRoots();
1647
+ }
1315
1648
  };
1316
1649
  const decoder = new TextDecoder();
1317
1650
  let buffer = "";