@4da/mcp-server 6.2.4 → 6.2.5

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 (119) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/README.md +22 -6
  3. package/dist/db.d.ts +38 -0
  4. package/dist/db.js +78 -23
  5. package/dist/doctor.js +6 -3
  6. package/dist/embeddings.d.ts +1 -1
  7. package/dist/embeddings.js +16 -14
  8. package/dist/index.js +73 -103
  9. package/dist/live/advisory-groups.d.ts +68 -0
  10. package/dist/live/advisory-groups.js +131 -0
  11. package/dist/live/cache.js +11 -3
  12. package/dist/live/cargo-platform.d.ts +6 -0
  13. package/dist/live/cargo-platform.js +8 -2
  14. package/dist/live/changelog.js +38 -6
  15. package/dist/live/dependency-set.d.ts +9 -1
  16. package/dist/live/dependency-set.js +10 -2
  17. package/dist/live/fix-paths.d.ts +258 -9
  18. package/dist/live/fix-paths.js +790 -45
  19. package/dist/live/github-releases.js +3 -0
  20. package/dist/live/held-dependencies.d.ts +93 -0
  21. package/dist/live/held-dependencies.js +212 -0
  22. package/dist/live/held-targets.d.ts +30 -0
  23. package/dist/live/held-targets.js +107 -0
  24. package/dist/live/http-utils.js +13 -3
  25. package/dist/live/index.d.ts +7 -4
  26. package/dist/live/index.js +24 -19
  27. package/dist/live/install-state.js +6 -2
  28. package/dist/live/js-lockfile-readers.d.ts +8 -1
  29. package/dist/live/js-lockfile-readers.js +43 -6
  30. package/dist/live/lockfile-parents.d.ts +15 -0
  31. package/dist/live/lockfile-parents.js +85 -10
  32. package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
  33. package/dist/live/lockfile-parsers-pygo.js +230 -12
  34. package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
  35. package/dist/live/lockfile-parsers-rbphp.js +123 -0
  36. package/dist/live/lockfile-parsers.d.ts +6 -0
  37. package/dist/live/lockfile-parsers.js +116 -4
  38. package/dist/live/lockfile-types.d.ts +28 -1
  39. package/dist/live/maintenance.d.ts +20 -4
  40. package/dist/live/maintenance.js +31 -2
  41. package/dist/live/malware.d.ts +20 -0
  42. package/dist/live/malware.js +31 -0
  43. package/dist/live/npm-packument.d.ts +4 -1
  44. package/dist/live/npm-packument.js +5 -2
  45. package/dist/live/offered-release.d.ts +53 -0
  46. package/dist/live/offered-release.js +117 -0
  47. package/dist/live/osv-ranges.d.ts +111 -0
  48. package/dist/live/osv-ranges.js +173 -14
  49. package/dist/live/osv-scanner.d.ts +87 -5
  50. package/dist/live/osv-scanner.js +310 -59
  51. package/dist/live/pinned-advisories.d.ts +2 -2
  52. package/dist/live/pinned-advisories.js +3 -3
  53. package/dist/live/pypi-advice.d.ts +36 -0
  54. package/dist/live/pypi-advice.js +158 -0
  55. package/dist/live/rate-limiter.d.ts +7 -0
  56. package/dist/live/rate-limiter.js +22 -1
  57. package/dist/live/release-metadata.d.ts +5 -0
  58. package/dist/live/resolution.d.ts +3 -0
  59. package/dist/live/resolution.js +6 -3
  60. package/dist/live/scan-coverage.d.ts +26 -0
  61. package/dist/live/scan-coverage.js +70 -0
  62. package/dist/live/semver-utils.d.ts +8 -0
  63. package/dist/live/semver-utils.js +26 -3
  64. package/dist/live/severity-scope.d.ts +26 -1
  65. package/dist/live/severity-scope.js +32 -0
  66. package/dist/live/shell-safe.d.ts +19 -0
  67. package/dist/live/shell-safe.js +36 -0
  68. package/dist/live/types.d.ts +134 -8
  69. package/dist/live/version-compare.d.ts +49 -0
  70. package/dist/live/version-compare.js +131 -0
  71. package/dist/live/withdrawn-releases.d.ts +50 -0
  72. package/dist/live/withdrawn-releases.js +125 -0
  73. package/dist/project-manifests.d.ts +7 -0
  74. package/dist/project-manifests.js +45 -5
  75. package/dist/project-scanner.js +6 -4
  76. package/dist/project-tree.d.ts +56 -0
  77. package/dist/project-tree.js +174 -5
  78. package/dist/schema-registry.d.ts +47 -4
  79. package/dist/schema-registry.js +76 -9
  80. package/dist/server-instructions.js +4 -2
  81. package/dist/setup.d.ts +8 -0
  82. package/dist/setup.js +21 -2
  83. package/dist/tool-dispatch.js +3 -3
  84. package/dist/tools/agent-memory.d.ts +5 -0
  85. package/dist/tools/agent-memory.js +9 -0
  86. package/dist/tools/briefing-task-scope.js +5 -2
  87. package/dist/tools/decision-memory.js +2 -0
  88. package/dist/tools/dependency-check-held.d.ts +44 -0
  89. package/dist/tools/dependency-check-held.js +98 -0
  90. package/dist/tools/dependency-check-signals.d.ts +19 -2
  91. package/dist/tools/dependency-check-signals.js +84 -13
  92. package/dist/tools/dependency-check-versions.d.ts +37 -4
  93. package/dist/tools/dependency-check-versions.js +48 -13
  94. package/dist/tools/dependency-check.d.ts +11 -1
  95. package/dist/tools/dependency-check.js +81 -29
  96. package/dist/tools/dependency-health.js +209 -45
  97. package/dist/tools/get-context.d.ts +3 -0
  98. package/dist/tools/get-context.js +4 -0
  99. package/dist/tools/project-scope.d.ts +8 -0
  100. package/dist/tools/project-scope.js +21 -0
  101. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  102. package/dist/tools/upgrade-impact-features.js +176 -0
  103. package/dist/tools/upgrade-impact-literals.js +26 -4
  104. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  105. package/dist/tools/upgrade-impact-report.js +26 -5
  106. package/dist/tools/upgrade-impact.js +10 -3
  107. package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
  108. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  109. package/dist/tools/upgrade-planner.d.ts +59 -4
  110. package/dist/tools/upgrade-planner.js +237 -61
  111. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  112. package/dist/tools/vulnerability-scan-concise.js +37 -3
  113. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  114. package/dist/tools/vulnerability-scan-format.js +193 -26
  115. package/dist/tools/vulnerability-scan.d.ts +224 -63
  116. package/dist/tools/vulnerability-scan.js +50 -15
  117. package/dist/tools/what-should-i-know.d.ts +6 -1
  118. package/dist/tools/what-should-i-know.js +72 -15
  119. package/package.json +2 -2
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
- import { normalizePythonName, parseGoMod } from "./live/lockfile-parsers-pygo.js";
18
+ import { compiledIndirect, compiledRequirements, confinedInclude, normalizePythonName, parseGoMod, requirementOptions } from "./live/lockfile-parsers-pygo.js";
19
19
  /** The body of a TOML table (`[name]`) up to the next table header, or null. */
20
20
  export function tomlTable(content, name) {
21
21
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -130,6 +130,37 @@ export function parseRequirementNames(content) {
130
130
  }
131
131
  return [...new Set(names)];
132
132
  }
133
+ /**
134
+ * Requirement names from a requirements file and every file it includes with `-r` (confined to
135
+ * `root`, as the version reader confines them): an included file's pins are declared too. A pin
136
+ * in `-r sub/inner.txt` read as "Transitive dependency, not declared in your manifest" (final
137
+ * release audit of 6.2.5, 7g: jinja2==2.10).
138
+ */
139
+ export function requirementNamesWithIncludes(file, root = path.dirname(path.resolve(file)), visited = new Set()) {
140
+ const key = path.resolve(file).toLowerCase();
141
+ if (visited.has(key))
142
+ return [];
143
+ visited.add(key);
144
+ let content;
145
+ try {
146
+ content = fs.readFileSync(file, "utf-8");
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ // A compiled file pins its whole tree: a pin there only because other packages require it
152
+ // (`# via requests`) is transitive, as go.mod's `// indirect` is (audit 6.2.5 A-4).
153
+ const compiled = compiledRequirements(content);
154
+ const names = parseRequirementNames(content).filter((n) => !compiled || !compiledIndirect(compiled, n));
155
+ for (const { option, target } of requirementOptions(content)) {
156
+ if (option !== "r")
157
+ continue;
158
+ const included = confinedInclude(path.dirname(file), target, root);
159
+ if (included)
160
+ names.push(...requirementNamesWithIncludes(included, root, visited));
161
+ }
162
+ return [...new Set(names)];
163
+ }
133
164
  /** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
134
165
  export function parseGoModDirectDeps(content) {
135
166
  return parseGoMod(content)
@@ -162,12 +193,15 @@ export function cargoWorkspaceDeps(root, content) {
162
193
  const name = /^name\s*=\s*"([^"]+)"/m.exec(tomlTable(manifest, "package") ?? "")?.[1];
163
194
  if (name)
164
195
  memberNames.add(name);
165
- for (const dep of tomlKeys(tomlTable(manifest, "dependencies")))
166
- deps.add(dep);
167
- for (const section of ["dev-dependencies", "build-dependencies"]) {
196
+ // build-dependencies run at build time on every machine that builds the crate: they are not
197
+ // dev-only (the app reads them as normal), and discounting them hid memoffset with
198
+ // include_dev: false (audit 6.2.5 F-3).
199
+ for (const section of ["dependencies", "build-dependencies"]) {
168
200
  for (const dep of tomlKeys(tomlTable(manifest, section)))
169
- devDeps.add(dep);
201
+ deps.add(dep);
170
202
  }
203
+ for (const dep of tomlKeys(tomlTable(manifest, "dev-dependencies")))
204
+ devDeps.add(dep);
171
205
  }
172
206
  }
173
207
  const strip = (set) => [...set].filter((d) => !memberNames.has(d));
@@ -176,6 +210,12 @@ export function cargoWorkspaceDeps(root, content) {
176
210
  }
177
211
  /** Member directories for one `members` entry: a path, or a path whose last segment is `*`. */
178
212
  function expandMemberGlob(root, pattern) {
213
+ // `members = ["../outside/*"]` was followed out of the project: manifests outside it were read
214
+ // and their dependency names sent to the registries (audit 6.2.5 C-4). Members must resolve inside.
215
+ const inside = (dir) => confinedInclude(root, path.relative(root, dir), root) !== null;
216
+ return expandMemberGlobUnconfined(root, pattern).filter(inside);
217
+ }
218
+ function expandMemberGlobUnconfined(root, pattern) {
179
219
  if (!pattern.includes("*"))
180
220
  return [path.join(root, pattern)];
181
221
  const star = pattern.indexOf("*");
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies, parseRequirementNames, } from "./project-manifests.js";
16
+ import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies, requirementNamesWithIncludes, } from "./project-manifests.js";
17
17
  // =============================================================================
18
18
  // Framework Detection Maps
19
19
  // =============================================================================
@@ -190,7 +190,8 @@ export function scanCurrentProject(cwd) {
190
190
  const rustDevDeps = [];
191
191
  parseTOMLDependencies(content, "[dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
192
192
  parseTOMLDependencies(content, "[dev-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
193
- parseTOMLDependencies(content, "[build-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
193
+ // build-dependencies run at build time on every machine that builds the crate: normal, not dev (audit 6.2.5 F-3).
194
+ parseTOMLDependencies(content, "[build-dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
194
195
  // Platform-gated deps: [target.'cfg(...)'.dependencies] / [target.<triple>.dependencies].
195
196
  // Previously skipped entirely (e.g. windows-sys, winreg, libc were invisible).
196
197
  parseTargetDependencies(content, rustDeps, rustDevDeps, result.depTargets, fwSet, RUST_FRAMEWORK_MAP);
@@ -243,7 +244,8 @@ export function scanCurrentProject(cwd) {
243
244
  if (fs.existsSync(requirementsPath)) {
244
245
  langSet.add("python");
245
246
  try {
246
- parseRequirementNames(fs.readFileSync(requirementsPath, "utf-8")).forEach((d) => pyDeps.add(d));
247
+ // Included files (`-r sub/inner.txt`) declare their pins too (audit 7g).
248
+ requirementNamesWithIncludes(requirementsPath, cwd).forEach((d) => pyDeps.add(d));
247
249
  }
248
250
  catch {
249
251
  // Skip
@@ -364,7 +366,7 @@ function parseTargetDependencies(content, deps, devDeps, depTargets, frameworkSe
364
366
  let m;
365
367
  while ((m = headerRegex.exec(content)) !== null) {
366
368
  const spec = m[1].trim().replace(/^['"]|['"]$/g, ""); // strip surrounding quotes
367
- const isDev = m[2] !== "dependencies";
369
+ const isDev = m[2] === "dev-dependencies"; // build-dependencies are normal (audit 6.2.5 F-3)
368
370
  // Body runs from the end of this header line to the next section header.
369
371
  const rest = content.slice(m.index + m[0].length);
370
372
  const nextHeader = rest.search(/^\[/m);
@@ -22,6 +22,13 @@
22
22
  * lockfile that was left out and say why.
23
23
  */
24
24
  import { type ProjectScanResult } from "./project-scanner.js";
25
+ /**
26
+ * Manifests and lockfiles of ecosystems this server does not read, by file
27
+ * name: reported as `not_covered` so an empty or clean answer is never read
28
+ * as "no dependencies" (audit A2: a Maven or .NET project was told "No
29
+ * manifest files found... run from your project root").
30
+ */
31
+ export declare function unsupportedEcosystem(name: string, names: string[]): string | null;
25
32
  /**
26
33
  * Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
27
34
  * the walk plus every project scan takes about 2 s. Depth 4 covers it with a
@@ -48,6 +55,18 @@ export interface TreeDiscovery {
48
55
  skippedCount: number;
49
56
  /** True when the walk itself stopped at MAX_WALK_DIRS: lockfiles beyond it were not even counted. */
50
57
  walkTruncated: boolean;
58
+ /** Manifests and lockfiles of ecosystems this server does not read (the first MAX_LISTED_NOT_COVERED), relative to the root. */
59
+ notCovered?: Array<{
60
+ path: string;
61
+ ecosystem: string;
62
+ }>;
63
+ /** Every such file found (`notCovered` may list fewer). */
64
+ notCoveredCount?: number;
65
+ /** Lockfiles under test-fixture directories, left out unless include_test_fixtures was set. */
66
+ fixtures?: {
67
+ count: number;
68
+ dirs: string[];
69
+ };
51
70
  }
52
71
  /** Python requirement files: requirements.txt, requirements-dev.txt, dev_requirements.txt, _test_minimum_requirements.txt. */
53
72
  export declare function isRequirementsFileName(name: string): boolean;
@@ -62,10 +81,47 @@ export declare function lockfilesIn(dir: string, listing?: string[]): string[];
62
81
  export declare function scanProjectTree(root: string, limits?: {
63
82
  maxDepth: number;
64
83
  maxProjects: number;
84
+ }, options?: {
85
+ includeFixtures?: boolean;
65
86
  }): {
66
87
  entries: ProjectTreeEntry[];
67
88
  discovery: TreeDiscovery;
68
89
  };
90
+ /**
91
+ * The walk's report for `root`, taken once per process. For answers that
92
+ * had no walk recorded and must not claim "nothing here" unchecked (H3):
93
+ * vulnerability_scan's "nothing found" message and dependency_health's
94
+ * nothing-checked answer. Re-walking on every call cost a full tree walk per
95
+ * call (review of H3, 2026-10-18).
96
+ */
97
+ export declare function discoveryFor(root: string): TreeDiscovery;
98
+ /** A lockfile problem as the notes read it (lockfile-types.ts LockfileProblem). */
99
+ export type LockfileProblemLike = {
100
+ file: string;
101
+ error: string;
102
+ skipped?: number;
103
+ refused?: true;
104
+ };
105
+ /**
106
+ * The three kinds, kept apart: a file that could not be read (UNREADABLE),
107
+ * one read with entries skipped (`skipped`, PARTIAL: a yarn.lock or
108
+ * requirements line past the length cap, audit L5), and a requirements
109
+ * include refused because it points outside the project (`refused`, B-11).
110
+ */
111
+ export declare function splitLockfileProblems<P extends LockfileProblemLike>(problems: P[]): {
112
+ unreadable: P[];
113
+ partial: P[];
114
+ refused: P[];
115
+ };
116
+ /**
117
+ * One wording for every tool (vulnerability_scan, dependency_health,
118
+ * upgrade_planner). `show` renders a path (the scan shows it relative).
119
+ */
120
+ export declare function lockfileProblemNotes(problems: LockfileProblemLike[], show?: (file: string) => string): string[];
121
+ /** Kept for its importers (upgrade_planner): lockfileProblemNotes. */
122
+ export declare function unreadableNotes(problems: LockfileProblemLike[]): string[];
123
+ /** What a scan left out on purpose or cannot read: ecosystems it does not cover, and test fixtures. */
124
+ export declare function exclusionNotes(discovery: TreeDiscovery | null): string[];
69
125
  /** One sentence for an answer, or null when nothing was left out. */
70
126
  export declare function discoveryNote(discovery: TreeDiscovery | null): string | null;
71
127
  /**
@@ -38,12 +38,57 @@ const LOCK_MARKERS = [
38
38
  "pdm.lock",
39
39
  "Pipfile.lock",
40
40
  "go.mod",
41
+ "Gemfile.lock",
42
+ "composer.lock",
41
43
  ];
42
44
  const SKIP_DIRS = new Set([
43
45
  "node_modules", "target", ".git", "dist", "build", "out", "vendor", ".venv", "venv",
44
- "__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor", "fixtures",
45
- "__fixtures__", "testdata", "test-fixtures", "__tests__", ".turbo", ".cache", ".pnpm-store",
46
+ "__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor",
47
+ ".turbo", ".cache", ".pnpm-store",
46
48
  ]);
49
+ /**
50
+ * Directories whose lockfiles are test data, not the project's dependencies:
51
+ * a scan of D:\4DA\src-tauri read the 4DA conformance corpus
52
+ * (tests/conformance/cases, 10,000+ vendored packages) as the app's own
53
+ * (audit A3, 2026-10-12). Left out by default, counted and named in the
54
+ * answer (`not_scanned`), and scanned on request (include_test_fixtures).
55
+ */
56
+ const FIXTURE_DIRS = new Set([
57
+ "fixtures", "__fixtures__", "testdata", "test-data", "test-fixtures", "__tests__", "tests", "test", "conformance",
58
+ ]);
59
+ /**
60
+ * Manifests and lockfiles of ecosystems this server does not read, by file
61
+ * name: reported as `not_covered` so an empty or clean answer is never read
62
+ * as "no dependencies" (audit A2: a Maven or .NET project was told "No
63
+ * manifest files found... run from your project root").
64
+ */
65
+ export function unsupportedEcosystem(name, names) {
66
+ if (name === "pom.xml")
67
+ return "Maven";
68
+ if (/^build\.gradle(\.kts)?$/.test(name) || name === "gradle.lockfile")
69
+ return "Maven (Gradle)";
70
+ if (/\.(cs|fs|vb)proj$/i.test(name) || name === "packages.lock.json" || name === "packages.config" || name === "Directory.Packages.props")
71
+ return "NuGet";
72
+ if (name === "pubspec.lock")
73
+ return "Pub";
74
+ if (name === "mix.lock")
75
+ return "Hex";
76
+ if (name === "Package.resolved")
77
+ return "SwiftPM";
78
+ if (name === "Podfile.lock")
79
+ return "CocoaPods";
80
+ if (name === "conan.lock")
81
+ return "ConanCenter";
82
+ if (name === "renv.lock")
83
+ return "CRAN";
84
+ if (name === "Gemfile" && !names.includes("Gemfile.lock"))
85
+ return "RubyGems (Gemfile with no Gemfile.lock)";
86
+ if (name === "composer.json" && !names.includes("composer.lock"))
87
+ return "Packagist (composer.json with no composer.lock)";
88
+ return null;
89
+ }
90
+ /** Entries listed in `notCovered`; the count covers the rest. */
91
+ const MAX_LISTED_NOT_COVERED = 25;
47
92
  /**
48
93
  * Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
49
94
  * the walk plus every project scan takes about 2 s. Depth 4 covers it with a
@@ -88,13 +133,26 @@ export function lockfilesIn(dir, listing) {
88
133
  * repository's .gitignore excludes are not part of the project and are
89
134
  * skipped (gitignore.ts); they are not reported as left out either.
90
135
  */
91
- export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }) {
136
+ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }, options = {}) {
92
137
  const entries = [{ dir: root, scan: scanCurrentProject(root) }];
93
138
  const ignore = new IgnoreRules(root);
94
139
  const skipped = [];
95
140
  let skippedCount = 0;
96
141
  let visited = 0;
97
142
  let walkTruncated = false;
143
+ const notCovered = [];
144
+ let notCoveredCount = 0;
145
+ const fixtures = { count: 0, dirs: [] };
146
+ const noteUnsupported = (rel, names) => {
147
+ for (const name of names) {
148
+ const ecosystem = unsupportedEcosystem(name, names);
149
+ if (!ecosystem)
150
+ continue;
151
+ notCoveredCount++;
152
+ if (notCovered.length < MAX_LISTED_NOT_COVERED)
153
+ notCovered.push({ path: rel ? `${rel}/${name}` : name, ecosystem });
154
+ }
155
+ };
98
156
  // One directory listing per directory: it decides both whether the
99
157
  // directory is a project and which children to descend into.
100
158
  const listing = (dir) => {
@@ -105,6 +163,25 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
105
163
  return null;
106
164
  }
107
165
  };
166
+ // Test-fixture lockfiles: counted, never scanned (unless asked for).
167
+ const countFixtures = (dir, children) => {
168
+ for (const child of children) {
169
+ if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
170
+ continue;
171
+ if (++visited > MAX_WALK_DIRS) {
172
+ walkTruncated = true;
173
+ return;
174
+ }
175
+ const sub = path.join(dir, child.name);
176
+ const grandchildren = listing(sub);
177
+ if (!grandchildren)
178
+ continue;
179
+ fixtures.count += lockfilesIn(sub, grandchildren.map((g) => g.name)).length;
180
+ countFixtures(sub, grandchildren);
181
+ if (walkTruncated)
182
+ return;
183
+ }
184
+ };
108
185
  const walk = (dir, rel, depth, children) => {
109
186
  for (const child of children) {
110
187
  if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
@@ -120,10 +197,21 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
120
197
  const grandchildren = listing(sub);
121
198
  if (!grandchildren)
122
199
  continue;
200
+ if (!options.includeFixtures && FIXTURE_DIRS.has(child.name.toLowerCase())) {
201
+ const before = fixtures.count;
202
+ fixtures.count += lockfilesIn(sub, grandchildren.map((g) => g.name)).length;
203
+ countFixtures(sub, grandchildren);
204
+ if (fixtures.count > before && fixtures.dirs.length < 10)
205
+ fixtures.dirs.push(childRel);
206
+ if (walkTruncated)
207
+ return;
208
+ continue;
209
+ }
123
210
  // A .gitignore is only read where one exists.
124
211
  if (grandchildren.some((g) => g.name === ".gitignore"))
125
212
  ignore.load(childRel);
126
213
  const names = grandchildren.map((g) => g.name);
214
+ noteUnsupported(childRel, names.filter((n) => !ignore.ignores(`${childRel}/${n}`, false)));
127
215
  const locks = lockfilesIn(sub, names).filter((name) => !ignore.ignores(`${childRel}/${name}`, false));
128
216
  if (locks.length > 0) {
129
217
  const reason = depth > limits.maxDepth ? "depth" : entries.length >= limits.maxProjects ? "project_limit" : null;
@@ -142,12 +230,91 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
142
230
  return;
143
231
  }
144
232
  };
145
- walk(root, "", 1, listing(root) ?? []);
233
+ const rootListing = listing(root) ?? [];
234
+ noteUnsupported("", rootListing.filter((d) => d.isFile()).map((d) => d.name));
235
+ walk(root, "", 1, rootListing);
146
236
  return {
147
237
  entries,
148
- discovery: { projects: entries.length, maxDepth: limits.maxDepth, maxProjects: limits.maxProjects, skipped, skippedCount, walkTruncated },
238
+ discovery: {
239
+ projects: entries.length,
240
+ maxDepth: limits.maxDepth,
241
+ maxProjects: limits.maxProjects,
242
+ skipped,
243
+ skippedCount,
244
+ walkTruncated,
245
+ ...(notCoveredCount > 0 ? { notCovered, notCoveredCount } : {}),
246
+ ...(fixtures.count > 0 ? { fixtures } : {}),
247
+ },
149
248
  };
150
249
  }
250
+ const walkedDiscovery = new Map();
251
+ /**
252
+ * The walk's report for `root`, taken once per process. For answers that
253
+ * had no walk recorded and must not claim "nothing here" unchecked (H3):
254
+ * vulnerability_scan's "nothing found" message and dependency_health's
255
+ * nothing-checked answer. Re-walking on every call cost a full tree walk per
256
+ * call (review of H3, 2026-10-18).
257
+ */
258
+ export function discoveryFor(root) {
259
+ const key = path.resolve(root).toLowerCase();
260
+ let found = walkedDiscovery.get(key);
261
+ if (!found) {
262
+ found = scanProjectTree(root).discovery;
263
+ walkedDiscovery.set(key, found);
264
+ }
265
+ return found;
266
+ }
267
+ /**
268
+ * The three kinds, kept apart: a file that could not be read (UNREADABLE),
269
+ * one read with entries skipped (`skipped`, PARTIAL: a yarn.lock or
270
+ * requirements line past the length cap, audit L5), and a requirements
271
+ * include refused because it points outside the project (`refused`, B-11).
272
+ */
273
+ export function splitLockfileProblems(problems) {
274
+ return {
275
+ unreadable: problems.filter((p) => !p.skipped && !p.refused),
276
+ partial: problems.filter((p) => Boolean(p.skipped) && !p.refused),
277
+ refused: problems.filter((p) => p.refused === true),
278
+ };
279
+ }
280
+ /**
281
+ * One wording for every tool (vulnerability_scan, dependency_health,
282
+ * upgrade_planner). `show` renders a path (the scan shows it relative).
283
+ */
284
+ export function lockfileProblemNotes(problems, show = (f) => f) {
285
+ const listed = (ps) => `${ps.slice(0, 3).map((p) => `${show(p.file)} (${p.error})`).join("; ")}${ps.length > 3 ? "; ..." : ""}`;
286
+ const { unreadable, partial, refused } = splitLockfileProblems(problems);
287
+ const notes = [];
288
+ if (unreadable.length > 0) {
289
+ notes.push(`UNREADABLE: ${unreadable.length} lockfile${unreadable.length === 1 ? "" : "s"} could not be read, so ${unreadable.length === 1 ? "its" : "their"} dependencies were not checked: ${listed(unreadable)}.`);
290
+ }
291
+ if (partial.length > 0) {
292
+ notes.push(`PARTIAL: ${partial.length} lockfile${partial.length === 1 ? " was" : "s were"} read with entries skipped: ${listed(partial)}.`);
293
+ }
294
+ if (refused.length > 0) {
295
+ notes.push(`REFUSED: ${refused.length} requirements include${refused.length === 1 ? " was" : "s were"} not read: ${listed(refused)}.`);
296
+ }
297
+ return notes;
298
+ }
299
+ /** Kept for its importers (upgrade_planner): lockfileProblemNotes. */
300
+ export function unreadableNotes(problems) {
301
+ return lockfileProblemNotes(problems);
302
+ }
303
+ /** What a scan left out on purpose or cannot read: ecosystems it does not cover, and test fixtures. */
304
+ export function exclusionNotes(discovery) {
305
+ const notes = [];
306
+ const unsupported = discovery?.notCoveredCount ?? 0;
307
+ if (unsupported > 0) {
308
+ const ecosystems = [...new Set((discovery?.notCovered ?? []).map((n) => n.ecosystem.split(" ")[0]))].join(", ");
309
+ const listed = (discovery?.notCovered ?? []).slice(0, 3).map((n) => n.path).join(", ");
310
+ notes.push(`NOT COVERED: ${unsupported} manifest/lockfile${unsupported === 1 ? "" : "s"} of ecosystems this server does not read (${ecosystems}): ${listed}${unsupported > 3 ? ", ..." : ""}. Their dependencies were not checked at all; see not_covered.`);
311
+ }
312
+ const fixtures = discovery?.fixtures;
313
+ if (fixtures && fixtures.count > 0) {
314
+ notes.push(`Not scanned: ${fixtures.count} test-fixture lockfile${fixtures.count === 1 ? "" : "s"} under ${fixtures.dirs.slice(0, 3).join(", ")}${fixtures.dirs.length > 3 ? ", ..." : ""} (pass include_test_fixtures: true to scan them).`);
315
+ }
316
+ return notes;
317
+ }
151
318
  /** One sentence for an answer, or null when nothing was left out. */
152
319
  export function discoveryNote(discovery) {
153
320
  if (!discovery || (discovery.skippedCount === 0 && !discovery.walkTruncated))
@@ -173,6 +340,8 @@ const LOCKS_BY_LANGUAGE = {
173
340
  rust: ["Cargo.lock"],
174
341
  python: ["poetry.lock", "uv.lock", "pdm.lock", "Pipfile.lock"],
175
342
  go: ["go.mod"],
343
+ ruby: ["Gemfile.lock"],
344
+ php: ["composer.lock"],
176
345
  };
177
346
  /**
178
347
  * Resolution groups for the live layer: one per (project directory, ecosystem).
@@ -18,6 +18,7 @@
18
18
  * tool output told agents to "pass include_dev". The full list costs about
19
19
  * 4k tokens, inside the 3-5k band of well-regarded servers.
20
20
  */
21
+ import { ProtocolError } from "@modelcontextprotocol/server";
21
22
  /** Tool categories — maps to the functional groupings in the MCP server */
22
23
  export type ToolCategory = "security" | "intelligence" | "decisions" | "agent" | "identity";
23
24
  /**
@@ -73,12 +74,19 @@ export declare function getSlimToolList(standaloneOnly?: boolean): Array<{
73
74
  inputSchema: ToolInputSchema;
74
75
  annotations: ToolAnnotations;
75
76
  }>;
77
+ /**
78
+ * The registry entry for a tool NAME a client sent, or undefined. Own
79
+ * properties only: `TOOL_REGISTRY["constructor"]` is Object's constructor,
80
+ * and `"toString" in TOOL_REGISTRY` is true, so a read of
81
+ * `4da://schema/__proto__` used to throw a TypeError (audit 2026-10-12 B-14).
82
+ */
83
+ export declare function toolEntry(name: string): ToolRegistryEntry | undefined;
76
84
  /** The full schema document a `4da://schema/{tool}` resource serves. */
77
85
  export declare function getToolSchemaDocument(toolName: string): object | null;
78
86
  /**
79
87
  * Get list of schema resources for ListResources
80
88
  */
81
- export declare function getSchemaResources(): Array<{
89
+ export declare function getSchemaResources(standaloneOnly?: boolean): Array<{
82
90
  uri: string;
83
91
  name: string;
84
92
  description: string;
@@ -86,10 +94,35 @@ export declare function getSchemaResources(): Array<{
86
94
  }>;
87
95
  /** Check if a tool exists */
88
96
  export declare function hasToolSchema(toolName: string): boolean;
89
- /** Get tool names grouped by category */
90
- export declare function getToolsByCategory(): Record<ToolCategory, string[]>;
97
+ /**
98
+ * MCP Resources: every tool's full schema, and the category manifest.
99
+ * `4da://skills` was advertised until 6.2.5, but it read a file nothing ever
100
+ * wrote, so every read failed (audit 2026-10-12 B-15). `standaloneOnly` is
101
+ * tools/list's filter: in standalone mode the 5 desktop-only schemas were
102
+ * advertised beside a tool list that leaves them out (audit L6).
103
+ */
104
+ export declare function listResources(standaloneOnly?: boolean): Array<{
105
+ uri: string;
106
+ name: string;
107
+ description: string;
108
+ mimeType: string;
109
+ }>;
110
+ /** `resources/read`: the resource, or a resource-not-found protocol error naming the URI. */
111
+ export declare function readResource(uri: string, standaloneOnly?: boolean): {
112
+ contents: Array<{
113
+ uri: string;
114
+ mimeType: string;
115
+ text: string;
116
+ }>;
117
+ };
118
+ /** tools/call for a name no tool has: JSON-RPC Invalid Params (-32602), as the MCP spec says. */
119
+ export declare function unknownToolError(name: string): ProtocolError;
120
+ /** prompts/get for a name no prompt has: JSON-RPC Invalid Params (-32602). */
121
+ export declare function unknownPromptError(name: string): ProtocolError;
122
+ /** Get tool names grouped by category (only the standalone ones, or only the desktop ones, when asked). */
123
+ export declare function getToolsByCategory(standaloneOnly?: boolean): Record<ToolCategory, string[]>;
91
124
  /** Structured category manifest for the 4da://categories resource */
92
- export declare function getCategoryManifest(): {
125
+ export declare function getCategoryManifest(standaloneOnly?: boolean): {
93
126
  version: string;
94
127
  total_tools: number;
95
128
  categories: Record<ToolCategory, {
@@ -97,5 +130,15 @@ export declare function getCategoryManifest(): {
97
130
  count: number;
98
131
  }>;
99
132
  };
133
+ /**
134
+ * The tool count the startup line states, for what this start will serve.
135
+ * With FOURDA_DB_PATH refused or the database unreadable, every call returns
136
+ * that error, so "16 tools" (6.2.5) was not true (audit L6); with no SQLite
137
+ * driver only the dependency tools run.
138
+ */
139
+ export declare function startupToolsLine(validation: {
140
+ valid: boolean;
141
+ standalone?: boolean;
142
+ }, hasSqliteDriver: boolean, databaseFreeTools?: number): string;
100
143
  /** Find tools matching any of the given tags */
101
144
  export declare function getToolsByTags(tags: string[]): string[];
@@ -19,6 +19,7 @@
19
19
  * tool output told agents to "pass include_dev". The full list costs about
20
20
  * 4k tokens, inside the 3-5k band of well-regarded servers.
21
21
  */
22
+ import { ProtocolError, ProtocolErrorCode, ResourceNotFoundError } from "@modelcontextprotocol/server";
22
23
  import { agentMemoryTool, checkDecisionAlignmentTool, decisionMemoryTool, dependencyCheckTool, dependencyHealthTool, developerDnaTool, ecosystemPulseTool, getActionableSignalsTool, getContextTool, getRelevantContentTool, knowledgeGapsTool, recordFeedbackTool, upgradeImpactTool, upgradePlannerTool, vulnerabilityScanTool, whatShouldIKnowTool, } from "./tools/index.js";
23
24
  const READ_LOCAL = { readOnlyHint: true, openWorldHint: false };
24
25
  const READ_NETWORK = { readOnlyHint: true, openWorldHint: true };
@@ -178,9 +179,18 @@ export function getSlimToolList(standaloneOnly) {
178
179
  annotations: info.annotations,
179
180
  }));
180
181
  }
182
+ /**
183
+ * The registry entry for a tool NAME a client sent, or undefined. Own
184
+ * properties only: `TOOL_REGISTRY["constructor"]` is Object's constructor,
185
+ * and `"toString" in TOOL_REGISTRY` is true, so a read of
186
+ * `4da://schema/__proto__` used to throw a TypeError (audit 2026-10-12 B-14).
187
+ */
188
+ export function toolEntry(name) {
189
+ return Object.hasOwn(TOOL_REGISTRY, name) ? TOOL_REGISTRY[name] : undefined;
190
+ }
181
191
  /** The full schema document a `4da://schema/{tool}` resource serves. */
182
192
  export function getToolSchemaDocument(toolName) {
183
- const entry = TOOL_REGISTRY[toolName];
193
+ const entry = toolEntry(toolName);
184
194
  if (!entry)
185
195
  return null;
186
196
  return {
@@ -196,8 +206,10 @@ export function getToolSchemaDocument(toolName) {
196
206
  /**
197
207
  * Get list of schema resources for ListResources
198
208
  */
199
- export function getSchemaResources() {
200
- return Object.entries(TOOL_REGISTRY).map(([name]) => ({
209
+ export function getSchemaResources(standaloneOnly) {
210
+ return Object.entries(TOOL_REGISTRY)
211
+ .filter(([, info]) => standaloneOnly == null || info.standalone === standaloneOnly)
212
+ .map(([name]) => ({
201
213
  uri: `4da://schema/${name}`,
202
214
  name: `${name} schema`,
203
215
  description: `Full JSON Schema for the ${name} tool`,
@@ -206,12 +218,51 @@ export function getSchemaResources() {
206
218
  }
207
219
  /** Check if a tool exists */
208
220
  export function hasToolSchema(toolName) {
209
- return toolName in TOOL_REGISTRY;
221
+ return toolEntry(toolName) !== undefined;
222
+ }
223
+ /**
224
+ * MCP Resources: every tool's full schema, and the category manifest.
225
+ * `4da://skills` was advertised until 6.2.5, but it read a file nothing ever
226
+ * wrote, so every read failed (audit 2026-10-12 B-15). `standaloneOnly` is
227
+ * tools/list's filter: in standalone mode the 5 desktop-only schemas were
228
+ * advertised beside a tool list that leaves them out (audit L6).
229
+ */
230
+ export function listResources(standaloneOnly) {
231
+ return [
232
+ ...getSchemaResources(standaloneOnly),
233
+ {
234
+ uri: "4da://categories",
235
+ name: "Tool categories",
236
+ description: "Tool groupings by category with tag metadata",
237
+ mimeType: "application/json",
238
+ },
239
+ ];
210
240
  }
211
- /** Get tool names grouped by category */
212
- export function getToolsByCategory() {
241
+ /** `resources/read`: the resource, or a resource-not-found protocol error naming the URI. */
242
+ export function readResource(uri, standaloneOnly) {
243
+ const json = (value) => ({ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(value, null, 2) }] });
244
+ if (uri === "4da://categories")
245
+ return json(getCategoryManifest(standaloneOnly));
246
+ const match = /^4da:\/\/schema\/(.+)$/.exec(uri);
247
+ const document = match ? getToolSchemaDocument(match[1]) : null;
248
+ if (!document)
249
+ throw new ResourceNotFoundError(uri, `Resource not found: ${uri}`);
250
+ return json(document);
251
+ }
252
+ /** tools/call for a name no tool has: JSON-RPC Invalid Params (-32602), as the MCP spec says. */
253
+ export function unknownToolError(name) {
254
+ return new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${name}`);
255
+ }
256
+ /** prompts/get for a name no prompt has: JSON-RPC Invalid Params (-32602). */
257
+ export function unknownPromptError(name) {
258
+ return new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown prompt: ${name}`);
259
+ }
260
+ /** Get tool names grouped by category (only the standalone ones, or only the desktop ones, when asked). */
261
+ export function getToolsByCategory(standaloneOnly) {
213
262
  const result = {};
214
263
  for (const [name, entry] of Object.entries(TOOL_REGISTRY)) {
264
+ if (standaloneOnly != null && entry.standalone !== standaloneOnly)
265
+ continue;
215
266
  if (!result[entry.category]) {
216
267
  result[entry.category] = [];
217
268
  }
@@ -220,18 +271,34 @@ export function getToolsByCategory() {
220
271
  return result;
221
272
  }
222
273
  /** Structured category manifest for the 4da://categories resource */
223
- export function getCategoryManifest() {
224
- const grouped = getToolsByCategory();
274
+ export function getCategoryManifest(standaloneOnly) {
275
+ const grouped = getToolsByCategory(standaloneOnly);
225
276
  const categories = {};
226
277
  for (const [cat, tools] of Object.entries(grouped)) {
227
278
  categories[cat] = { tools, count: tools.length };
228
279
  }
229
280
  return {
230
281
  version: "1.0.0",
231
- total_tools: Object.keys(TOOL_REGISTRY).length,
282
+ total_tools: Object.values(grouped).reduce((n, tools) => n + tools.length, 0),
232
283
  categories,
233
284
  };
234
285
  }
286
+ /**
287
+ * The tool count the startup line states, for what this start will serve.
288
+ * With FOURDA_DB_PATH refused or the database unreadable, every call returns
289
+ * that error, so "16 tools" (6.2.5) was not true (audit L6); with no SQLite
290
+ * driver only the dependency tools run.
291
+ */
292
+ export function startupToolsLine(validation, hasSqliteDriver, databaseFreeTools = 0) {
293
+ const standalone = validation.standalone === true;
294
+ if (!hasSqliteDriver)
295
+ return `${databaseFreeTools} dependency tools run without a database (every other tool returns the driver problem above)`;
296
+ if (validation.valid || standalone) {
297
+ const count = getSlimToolList(standalone ? true : undefined).length;
298
+ return `${count} ${standalone ? "standalone tools" : "tools"}`;
299
+ }
300
+ return `no tool can run until the database problem above is fixed (${getSlimToolList().length} tools are listed; each call returns that error)`;
301
+ }
235
302
  /** Find tools matching any of the given tags */
236
303
  export function getToolsByTags(tags) {
237
304
  const tagSet = new Set(tags.map((t) => t.toLowerCase()));