@4da/mcp-server 6.2.3 → 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 (123) hide show
  1. package/CHANGELOG.md +520 -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 +289 -16
  18. package/dist/live/fix-paths.js +876 -93
  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/range-satisfies.d.ts +4 -2
  56. package/dist/live/range-satisfies.js +7 -5
  57. package/dist/live/rate-limiter.d.ts +7 -0
  58. package/dist/live/rate-limiter.js +22 -1
  59. package/dist/live/refresh-rule.d.ts +36 -0
  60. package/dist/live/refresh-rule.js +49 -0
  61. package/dist/live/release-metadata.d.ts +5 -0
  62. package/dist/live/resolution.d.ts +3 -0
  63. package/dist/live/resolution.js +6 -3
  64. package/dist/live/scan-coverage.d.ts +26 -0
  65. package/dist/live/scan-coverage.js +70 -0
  66. package/dist/live/semver-utils.d.ts +8 -0
  67. package/dist/live/semver-utils.js +26 -3
  68. package/dist/live/severity-scope.d.ts +26 -1
  69. package/dist/live/severity-scope.js +32 -0
  70. package/dist/live/shell-safe.d.ts +19 -0
  71. package/dist/live/shell-safe.js +36 -0
  72. package/dist/live/types.d.ts +134 -8
  73. package/dist/live/version-compare.d.ts +51 -0
  74. package/dist/live/version-compare.js +152 -1
  75. package/dist/live/withdrawn-releases.d.ts +50 -0
  76. package/dist/live/withdrawn-releases.js +125 -0
  77. package/dist/project-manifests.d.ts +7 -0
  78. package/dist/project-manifests.js +45 -5
  79. package/dist/project-scanner.js +6 -4
  80. package/dist/project-tree.d.ts +56 -0
  81. package/dist/project-tree.js +174 -5
  82. package/dist/schema-registry.d.ts +47 -4
  83. package/dist/schema-registry.js +76 -9
  84. package/dist/server-instructions.js +4 -2
  85. package/dist/setup.d.ts +8 -0
  86. package/dist/setup.js +21 -2
  87. package/dist/tool-dispatch.js +3 -3
  88. package/dist/tools/agent-memory.d.ts +5 -0
  89. package/dist/tools/agent-memory.js +9 -0
  90. package/dist/tools/briefing-task-scope.js +5 -2
  91. package/dist/tools/decision-memory.js +2 -0
  92. package/dist/tools/dependency-check-held.d.ts +44 -0
  93. package/dist/tools/dependency-check-held.js +98 -0
  94. package/dist/tools/dependency-check-signals.d.ts +19 -2
  95. package/dist/tools/dependency-check-signals.js +84 -13
  96. package/dist/tools/dependency-check-versions.d.ts +37 -4
  97. package/dist/tools/dependency-check-versions.js +48 -13
  98. package/dist/tools/dependency-check.d.ts +11 -1
  99. package/dist/tools/dependency-check.js +81 -29
  100. package/dist/tools/dependency-health.js +209 -45
  101. package/dist/tools/get-context.d.ts +3 -0
  102. package/dist/tools/get-context.js +4 -0
  103. package/dist/tools/project-scope.d.ts +8 -0
  104. package/dist/tools/project-scope.js +21 -0
  105. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  106. package/dist/tools/upgrade-impact-features.js +176 -0
  107. package/dist/tools/upgrade-impact-literals.js +26 -4
  108. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  109. package/dist/tools/upgrade-impact-report.js +26 -5
  110. package/dist/tools/upgrade-impact.js +10 -3
  111. package/dist/tools/upgrade-planner-fixpaths.d.ts +164 -5
  112. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  113. package/dist/tools/upgrade-planner.d.ts +70 -7
  114. package/dist/tools/upgrade-planner.js +237 -61
  115. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  116. package/dist/tools/vulnerability-scan-concise.js +37 -3
  117. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  118. package/dist/tools/vulnerability-scan-format.js +193 -26
  119. package/dist/tools/vulnerability-scan.d.ts +224 -63
  120. package/dist/tools/vulnerability-scan.js +50 -15
  121. package/dist/tools/what-should-i-know.d.ts +6 -1
  122. package/dist/tools/what-should-i-know.js +72 -15
  123. package/package.json +2 -2
package/dist/db.d.ts CHANGED
@@ -27,6 +27,40 @@ import { type SqliteDatabase } from "./sqlite-driver.js";
27
27
  export declare function nativeBindingProblem(): string | null;
28
28
  import type { RelevantItem, UserContext, FeedbackAction, FeedbackResult } from "./types.js";
29
29
  import type { ProjectScanResult } from "./project-scanner.js";
30
+ /**
31
+ * Resolve the database path, in priority order:
32
+ * 1. FOURDA_DB_PATH (relative paths resolve against cwd). A relative path
33
+ * must name an existing file; an absolute one may name a new file in an
34
+ * existing directory, which becomes a standalone database (see
35
+ * explicitDbPathProblem()).
36
+ * 2. The deployed desktop app's database (desktopAppDbPaths)
37
+ * 3. Otherwise the standalone database (standaloneDbPath), created on first use
38
+ *
39
+ * Nothing is discovered from the working directory. Until 6.2.5 a
40
+ * `data/4da.db` under cwd was adopted (meant for the app repository's
41
+ * development database): any cloned repository could plant one, its rows came
42
+ * back as the user's own decisions, and a standalone-looking file was reopened
43
+ * writable (audit 2026-10-12, B-4/B-5). The app repository's `.mcp.json` sets
44
+ * FOURDA_DB_PATH explicitly instead.
45
+ */
46
+ export declare function getDefaultDbPath(): string;
47
+ /**
48
+ * Why the explicit FOURDA_DB_PATH cannot be used, or null.
49
+ *
50
+ * - An existing file: used.
51
+ * - A RELATIVE path naming no file is refused. It resolves against whatever
52
+ * directory the host started the server in: `./data/4da.db` from 4DA's
53
+ * `.mcp.json` silently created a fresh standalone database named like the
54
+ * app's inside any repository, and every decision and memory written there
55
+ * was lost to the real one (audit B-5).
56
+ * - An ABSOLUTE path naming no file, in a directory that exists, is a
57
+ * deliberate choice of where the standalone database lives (the G7 agent
58
+ * eval sets `<run>/server-home/standalone.db`): it is created, and the
59
+ * first tool answer says so (FourDADatabase.takeCreationNote()).
60
+ * - An absolute path whose directory does not exist is refused: a typo there
61
+ * would otherwise create directories.
62
+ */
63
+ export declare function explicitDbPathProblem(env?: NodeJS.ProcessEnv): string | null;
30
64
  /**
31
65
  * Where the deployed desktop app keeps its database, mirroring
32
66
  * `src-tauri/src/state.rs::get_db_path` / `get_platform_data_dir`:
@@ -154,6 +188,10 @@ export declare class FourDADatabase {
154
188
  private _isStandalone;
155
189
  /** Absolute path of the open database file. */
156
190
  readonly dbPath: string;
191
+ /** Set when this open created the database at an explicit FOURDA_DB_PATH; reported once. */
192
+ private creationNote;
193
+ /** "Created a new standalone database at ..." the first time it is asked after the database was created there; null afterwards. */
194
+ takeCreationNote(): string | null;
157
195
  constructor(dbPath?: string);
158
196
  /**
159
197
  * Whether this database was freshly created in standalone mode
package/dist/db.js CHANGED
@@ -34,37 +34,66 @@ export function nativeBindingProblem() {
34
34
  }
35
35
  import { dbRecoveryNote, readDbRecoveredMarker, readEngineBlockMarker } from "./freshness-markers.js";
36
36
  /**
37
- * Resolve the database path by checking multiple locations in priority order:
38
- * 1. FOURDA_DB_PATH env var
39
- * 2. data/4da.db relative to cwd: the desktop app's development database,
40
- * when the server is started in a checkout of the app's repository
41
- * 3. The deployed desktop app's database (desktopAppDbPaths)
42
- * 4. Otherwise the standalone database (standaloneDbPath), created on first use
37
+ * Resolve the database path, in priority order:
38
+ * 1. FOURDA_DB_PATH (relative paths resolve against cwd). A relative path
39
+ * must name an existing file; an absolute one may name a new file in an
40
+ * existing directory, which becomes a standalone database (see
41
+ * explicitDbPathProblem()).
42
+ * 2. The deployed desktop app's database (desktopAppDbPaths)
43
+ * 3. Otherwise the standalone database (standaloneDbPath), created on first use
43
44
  *
44
- * Until 6.0.2 a step between 2 and 3 looked for data/4da.db two directories
45
- * above this file, which found the app's database while this server was the
46
- * app repository's mcp-4da-server/ folder. Since the server moved to its own
47
- * repository (6.0.1) that resolves to node_modules/@4da/ or to the folder
48
- * holding a clone, never to an app database, so it was removed.
45
+ * Nothing is discovered from the working directory. Until 6.2.5 a
46
+ * `data/4da.db` under cwd was adopted (meant for the app repository's
47
+ * development database): any cloned repository could plant one, its rows came
48
+ * back as the user's own decisions, and a standalone-looking file was reopened
49
+ * writable (audit 2026-10-12, B-4/B-5). The app repository's `.mcp.json` sets
50
+ * FOURDA_DB_PATH explicitly instead.
49
51
  */
50
- function getDefaultDbPath() {
51
- // 1. Environment variable (highest priority)
52
- if (process.env.FOURDA_DB_PATH) {
53
- return process.env.FOURDA_DB_PATH;
54
- }
55
- // 2. Relative to cwd (the app repository's development database)
56
- const cwdPath = path.resolve(process.cwd(), "data", "4da.db");
57
- if (fs.existsSync(cwdPath)) {
58
- return cwdPath;
59
- }
60
- // 3. The desktop app's own database, where the deployed app keeps it.
52
+ export function getDefaultDbPath() {
53
+ const explicit = process.env.FOURDA_DB_PATH?.trim();
54
+ if (explicit)
55
+ return path.resolve(process.cwd(), explicit);
61
56
  for (const appDataPath of desktopAppDbPaths()) {
62
57
  if (fs.existsSync(appDataPath))
63
58
  return appDataPath;
64
59
  }
65
- // 4. No desktop database: the standalone database, in the user's data dir.
66
60
  return standaloneDbPath();
67
61
  }
62
+ /**
63
+ * Why the explicit FOURDA_DB_PATH cannot be used, or null.
64
+ *
65
+ * - An existing file: used.
66
+ * - A RELATIVE path naming no file is refused. It resolves against whatever
67
+ * directory the host started the server in: `./data/4da.db` from 4DA's
68
+ * `.mcp.json` silently created a fresh standalone database named like the
69
+ * app's inside any repository, and every decision and memory written there
70
+ * was lost to the real one (audit B-5).
71
+ * - An ABSOLUTE path naming no file, in a directory that exists, is a
72
+ * deliberate choice of where the standalone database lives (the G7 agent
73
+ * eval sets `<run>/server-home/standalone.db`): it is created, and the
74
+ * first tool answer says so (FourDADatabase.takeCreationNote()).
75
+ * - An absolute path whose directory does not exist is refused: a typo there
76
+ * would otherwise create directories.
77
+ */
78
+ export function explicitDbPathProblem(env = process.env) {
79
+ const explicit = env.FOURDA_DB_PATH?.trim();
80
+ if (!explicit)
81
+ return null;
82
+ const absolute = path.resolve(process.cwd(), explicit);
83
+ if (fs.existsSync(absolute))
84
+ return null;
85
+ if (path.isAbsolute(explicit)) {
86
+ if (fs.existsSync(path.dirname(absolute)))
87
+ return null;
88
+ return (`FOURDA_DB_PATH is set to "${explicit}", but its directory ${path.dirname(absolute)} does not exist. ` +
89
+ `Nothing was created. Create the directory to have a new standalone database made there, point ` +
90
+ `FOURDA_DB_PATH at an existing 4DA database, or unset it to use the desktop app's database or this server's own.`);
91
+ }
92
+ return (`FOURDA_DB_PATH is set to the relative path "${explicit}", but there is no database at ${absolute} ` +
93
+ `(relative paths resolve against the server's working directory). Nothing was created there. Point it at an ` +
94
+ `existing 4DA database, use an absolute path to have a new standalone database created, or unset it to use ` +
95
+ `the desktop app's database or this server's own.`);
96
+ }
68
97
  /**
69
98
  * Where the deployed desktop app keeps its database, mirroring
70
99
  * `src-tauri/src/state.rs::get_db_path` / `get_platform_data_dir`:
@@ -266,13 +295,32 @@ export class FourDADatabase {
266
295
  _isStandalone = false;
267
296
  /** Absolute path of the open database file. */
268
297
  dbPath;
298
+ /** Set when this open created the database at an explicit FOURDA_DB_PATH; reported once. */
299
+ creationNote = null;
300
+ /** "Created a new standalone database at ..." the first time it is asked after the database was created there; null afterwards. */
301
+ takeCreationNote() {
302
+ const note = this.creationNote;
303
+ this.creationNote = null;
304
+ return note;
305
+ }
269
306
  constructor(dbPath) {
307
+ // An explicit argument (tests, tools) may create a new file; the env var may not.
308
+ if (!dbPath) {
309
+ const problem = explicitDbPathProblem();
310
+ if (problem)
311
+ throw new Error(problem);
312
+ }
270
313
  const resolvedPath = dbPath || getDefaultDbPath();
271
314
  // Resolve path - if relative, resolve from cwd
272
315
  const absolutePath = path.isAbsolute(resolvedPath)
273
316
  ? resolvedPath
274
317
  : path.resolve(process.cwd(), resolvedPath);
275
318
  const isNew = !fs.existsSync(absolutePath);
319
+ const envPath = process.env.FOURDA_DB_PATH?.trim();
320
+ if (isNew && !dbPath && envPath && path.resolve(process.cwd(), envPath) === path.resolve(absolutePath)) {
321
+ this.creationNote = `Created a new standalone database at ${absolutePath}: FOURDA_DB_PATH named no existing file.`;
322
+ console.error(`[4DA] ${this.creationNote}`);
323
+ }
276
324
  // Ensure parent directory exists (standalone mode may need to create it)
277
325
  if (isNew) {
278
326
  const dir = path.dirname(absolutePath);
@@ -441,6 +489,13 @@ export class FourDADatabase {
441
489
  const absolutePath = path.isAbsolute(resolvedPath)
442
490
  ? resolvedPath
443
491
  : path.resolve(process.cwd(), resolvedPath);
492
+ // The path FOURDA_DB_PATH names is never created: report it, not standalone.
493
+ const envPath = process.env.FOURDA_DB_PATH?.trim();
494
+ if (envPath && path.resolve(process.cwd(), envPath) === path.resolve(absolutePath)) {
495
+ const problem = explicitDbPathProblem();
496
+ if (problem)
497
+ return { valid: false, standalone: false, error: problem };
498
+ }
444
499
  // Check if file exists — if not, standalone mode will create it
445
500
  if (!fs.existsSync(absolutePath)) {
446
501
  return {
package/dist/doctor.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * 4da-mcp --doctor
14
14
  */
15
15
  import { existsSync } from "node:fs";
16
- import { FourDADatabase } from "./db.js";
16
+ import { explicitDbPathProblem, FourDADatabase } from "./db.js";
17
17
  import { NODE_SQLITE_MIN, sqliteDriverStatus } from "./sqlite-driver.js";
18
18
  import { checkBuildStaleness } from "./build-staleness.js";
19
19
  export function runDoctor() {
@@ -76,10 +76,13 @@ export function runDoctor() {
76
76
  const envDb = process.env.FOURDA_DB_PATH;
77
77
  if (envDb) {
78
78
  const envExists = existsSync(envDb);
79
+ const problem = explicitDbPathProblem();
79
80
  checks.push({
80
81
  name: "FOURDA_DB_PATH",
81
- status: envExists ? "pass" : "fail",
82
- detail: envExists ? envDb : `Set to ${envDb} but file does not exist`,
82
+ status: envExists ? "pass" : problem ? "fail" : "warn",
83
+ detail: envExists
84
+ ? envDb
85
+ : problem ?? `Set to ${envDb}, which does not exist yet: the server creates a new standalone database there on first use.`,
83
86
  });
84
87
  }
85
88
  // 5. Build freshness — repo checkouts only. `.mcp.json` runs dist/, and
@@ -17,7 +17,7 @@
17
17
  * No sqlite-vec: similarity is a plain JS cosine over Float32 BLOBs, which is
18
18
  * trivially fast for the few-hundred-row local knowledge tables this serves.
19
19
  */
20
- import type { FourDADatabase } from "./db.js";
20
+ import { type FourDADatabase } from "./db.js";
21
21
  export interface EmbeddingConfig {
22
22
  provider: "ollama" | "openai";
23
23
  model: string;
@@ -19,9 +19,8 @@
19
19
  * trivially fast for the few-hundred-row local knowledge tables this serves.
20
20
  */
21
21
  import { readFileSync, existsSync, statSync } from "node:fs";
22
- import { join, dirname } from "node:path";
23
- import { fileURLToPath } from "node:url";
24
- const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ import { join, dirname, resolve } from "node:path";
23
+ import { desktopAppDbPaths } from "./db.js";
25
24
  const DEFAULT_OLLAMA_EMBED_MODEL = "nomic-embed-text";
26
25
  const DEFAULT_OPENAI_EMBED_MODEL = "text-embedding-3-small";
27
26
  const DEFAULT_OLLAMA_URL = "http://localhost:11434";
@@ -30,25 +29,28 @@ export function isOfflineMode() {
30
29
  return String(process.env.FOURDA_OFFLINE || "").toLowerCase() === "true";
31
30
  }
32
31
  /**
33
- * Locate settings.json the same way llm.ts does, so embedding config lives
34
- * alongside the rest of the user's local configuration.
32
+ * Locate settings.json: FOURDA_SETTINGS_PATH, else the one beside an explicit
33
+ * FOURDA_DB_PATH (relative paths resolve against cwd, as the database's do),
34
+ * else the desktop app's own data directory.
35
+ *
36
+ * Never the working directory or its parent. Until 6.2.5 `cwd/data`,
37
+ * `cwd/../data` and the package's own `../data` were searched, so a cloned
38
+ * repository with `data/settings.json` could point semantic recall at any
39
+ * host, and stored agent memory and decisions were sent there to be embedded
40
+ * (audit 2026-10-12, B-3).
35
41
  */
36
42
  function findSettingsFile() {
37
- const envPath = process.env.FOURDA_SETTINGS_PATH;
43
+ const envPath = process.env.FOURDA_SETTINGS_PATH?.trim();
38
44
  if (envPath && existsSync(envPath))
39
45
  return envPath;
40
- const dbPath = process.env.FOURDA_DB_PATH;
46
+ const dbPath = process.env.FOURDA_DB_PATH?.trim();
41
47
  if (dbPath) {
42
- const sibling = join(dirname(dbPath), "settings.json");
48
+ const sibling = join(dirname(resolve(process.cwd(), dbPath)), "settings.json");
43
49
  if (existsSync(sibling))
44
50
  return sibling;
45
51
  }
46
- const candidates = [
47
- join(process.cwd(), "data", "settings.json"),
48
- join(__dirname, "..", "data", "settings.json"),
49
- join(process.cwd(), "..", "data", "settings.json"),
50
- ];
51
- for (const candidate of candidates) {
52
+ for (const appDb of desktopAppDbPaths()) {
53
+ const candidate = join(dirname(appDb), "settings.json");
52
54
  if (existsSync(candidate))
53
55
  return candidate;
54
56
  }
package/dist/index.js CHANGED
@@ -35,21 +35,20 @@
35
35
  */
36
36
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
37
37
  import { Server } from "@modelcontextprotocol/server";
38
- import { readFileSync, existsSync } from "node:fs";
38
+ import { readFileSync } from "node:fs";
39
39
  import { join, dirname } from "node:path";
40
40
  import { fileURLToPath } from "node:url";
41
- import { homedir } from "node:os";
42
41
  import { startHttpServer } from "./http-transport.js";
43
42
  import { runSetup } from "./setup.js";
44
43
  import { runDoctor } from "./doctor.js";
45
- import { scanProjectTree, treeResolutionGroups } from "./project-tree.js";
44
+ import { exclusionNotes, scanProjectTree, treeResolutionGroups } from "./project-tree.js";
46
45
  import { IgnoreRules } from "./gitignore.js";
47
46
  import { LiveIntelligence } from "./live/index.js";
48
47
  import { setLiveIntelligence } from "./live-singleton.js";
49
48
  import { SERVER_INSTRUCTIONS } from "./server-instructions.js";
50
49
  import { validateToolArgs } from "./tool-args.js";
51
50
  // Tool registry: listing, schema resources, category metadata
52
- import { getCategoryManifest, getSchemaResources, getSlimToolList, getToolSchemaDocument, hasToolSchema, TOOL_REGISTRY, } from "./schema-registry.js";
51
+ import { getSlimToolList, listResources, readResource, startupToolsLine, toolEntry, unknownPromptError, unknownToolError, } from "./schema-registry.js";
53
52
  // Map-based tool dispatch (replaces per-tool imports + switch statement)
54
53
  import { DATABASE_FREE_TOOLS, dispatchTool } from "./tool-dispatch.js";
55
54
  import { SqliteUnavailableError, sqliteDriverStatus } from "./sqlite-driver.js";
@@ -95,8 +94,10 @@ let liveIntel = null;
95
94
  */
96
95
  function getDatabase() {
97
96
  if (!db) {
98
- const dbPath = process.env.FOURDA_DB_PATH;
99
- db = createDatabase(dbPath);
97
+ // No argument: the constructor resolves FOURDA_DB_PATH itself and applies
98
+ // its rules (a relative path must exist). Passing the env value as an
99
+ // explicit path bypassed them.
100
+ db = createDatabase();
100
101
  // Initialize live intelligence layer
101
102
  liveIntel = new LiveIntelligence(db.getRawDb());
102
103
  setLiveIntelligence(liveIntel);
@@ -117,12 +118,16 @@ function getDatabase() {
117
118
  ...scan.frameworks,
118
119
  ].filter(Boolean);
119
120
  console.error(`[4DA] Standalone mode: scanned ${scan.projectPath} (${tree.length} project${tree.length === 1 ? "" : "s"})`);
121
+ // Initialize live intelligence with per-ecosystem resolved versions
122
+ // (targets carry platform-gated dep info so advisories can be flagged
123
+ // platform-relevant for the host). Also with NO groups: the walk's
124
+ // report of what it found but does not read (pom.xml, build.gradle,
125
+ // test-fixture lockfiles) is what every tool's coverage note is built
126
+ // from. Started inside spring-petclinic, 6.2.5 kept nothing and said
127
+ // "none of an unsupported one either" (readiness audit H3).
128
+ liveIntel.initFromProjectTree(cwd, groups, discovery);
120
129
  if (groups.length > 0) {
121
130
  console.error(`[4DA] Detected: ${detected.join(", ") || "lockfiles"} | ${scan.dependencies.length} deps, ${scan.devDependencies.length} dev deps at the root`);
122
- // Initialize live intelligence with per-ecosystem resolved versions
123
- // (targets carry platform-gated dep info so advisories can be flagged
124
- // platform-relevant for the host).
125
- liveIntel.initFromProjectTree(cwd, groups, discovery);
126
131
  if (liveIntel.isEnabled()) {
127
132
  console.error(`[4DA] Live intelligence: enabled (OSV.dev)`);
128
133
  // The vulnerability scan goes through the warmup so a briefing that
@@ -133,7 +138,7 @@ function getDatabase() {
133
138
  }
134
139
  }
135
140
  else {
136
- console.error(`[4DA] No project manifests found in ${cwd} — tools will return empty results`);
141
+ console.error(`[4DA] ${noSupportedProjectNote(cwd, discovery)}`);
137
142
  }
138
143
  }
139
144
  else {
@@ -180,6 +185,18 @@ function getDatabase() {
180
185
  if (groups.size > 0) {
181
186
  liveIntel.initFromDependencyGroups([...groups.values()]);
182
187
  }
188
+ else {
189
+ // The app tracks nothing under this directory (it indexes the
190
+ // projects it was pointed at, not every directory a host starts the
191
+ // server in). Its tables say nothing about this project, so read it
192
+ // from disk as standalone mode does; never write the scan into the
193
+ // app's database. Without this every tool read the project as empty
194
+ // and the planner answered "0 steps" (audit 6.2.5 D-1).
195
+ const root = resolveProjectDir();
196
+ const { entries: tree, discovery } = scanProjectTree(root);
197
+ liveIntel.initFromProjectTree(root, treeResolutionGroups(tree), discovery);
198
+ console.error(`[4DA] The 4DA app tracks no project at ${root}; reading its lockfiles directly.`);
199
+ }
183
200
  // Headlines are fetched only when ecosystem_pulse is called: the
184
201
  // search terms come from the user's projects.
185
202
  if (liveIntel.isEnabled()) {
@@ -222,12 +239,22 @@ function initWithoutDatabase() {
222
239
  const { entries, discovery } = scanProjectTree(cwd);
223
240
  const groups = treeResolutionGroups(entries);
224
241
  console.error(`[4DA] No SQLite driver: the dependency tools run without a database (${cwd}).`);
225
- if (groups.length === 0)
226
- return;
242
+ // Kept with no groups too: the coverage notes come from the walk (H3).
227
243
  liveIntel.initFromProjectTree(cwd, groups, discovery);
244
+ if (groups.length === 0) {
245
+ console.error(`[4DA] ${noSupportedProjectNote(cwd, discovery)}`);
246
+ return;
247
+ }
228
248
  if (liveIntel.isEnabled())
229
249
  liveIntel.startVulnerabilityWarmup(cwd);
230
250
  }
251
+ /** The startup line for a directory with no lockfile this server reads: what it did find, never just "nothing". */
252
+ function noSupportedProjectNote(cwd, discovery) {
253
+ const notes = exclusionNotes(discovery);
254
+ return notes.length > 0
255
+ ? `No lockfile of a supported ecosystem in ${cwd}. ${notes.join(" ")}`
256
+ : `No project manifests found in ${cwd} — tools will return empty results`;
257
+ }
231
258
  /** The database for a tool call; null only for a database-free tool when no SQLite driver is usable. */
232
259
  function databaseFor(tool) {
233
260
  try {
@@ -306,108 +333,43 @@ export function buildServer() {
306
333
  tools: getSlimToolList(isStandaloneMode() ? true : undefined),
307
334
  };
308
335
  });
309
- // List schema resources: full tool schemas as MCP Resources for lazy
310
- // loading, plus the skill and category manifests.
311
- server.setRequestHandler("resources/list", async () => {
312
- const resources = [
313
- ...getSchemaResources(),
314
- // Skill manifest for agent dispatch
315
- {
316
- uri: "4da://skills",
317
- name: "Skill manifest",
318
- description: "Registry of 4DA skills for Claude Code agent dispatch",
319
- mimeType: "application/json",
320
- },
321
- // Category manifest for tool discovery
322
- {
323
- uri: "4da://categories",
324
- name: "Tool categories",
325
- description: "Tool groupings by category with tag metadata",
326
- mimeType: "application/json",
327
- },
328
- ];
329
- return { resources };
330
- });
331
- // Read a resource (schema or skill manifest)
332
- server.setRequestHandler("resources/read", async (request) => {
333
- const uri = request.params.uri;
334
- // Handle category manifest
335
- if (uri === "4da://categories") {
336
- return {
337
- contents: [
338
- {
339
- uri,
340
- mimeType: "application/json",
341
- text: JSON.stringify(getCategoryManifest(), null, 2),
342
- },
343
- ],
344
- };
345
- }
346
- // Handle skill manifest
347
- if (uri === "4da://skills") {
348
- const skillsPath = join(homedir(), ".local", "share", "4da", "skills", "registry.json");
349
- if (!existsSync(skillsPath)) {
350
- throw new Error("Skill manifest not found. Run 4DA setup to create it.");
351
- }
352
- const skillsContent = readFileSync(skillsPath, "utf-8");
353
- return {
354
- contents: [
355
- {
356
- uri,
357
- mimeType: "application/json",
358
- text: skillsContent,
359
- },
360
- ],
361
- };
362
- }
363
- // Parse tool name from URI: 4da://schema/{tool_name}
364
- const match = uri.match(/^4da:\/\/schema\/(.+)$/);
365
- if (!match) {
366
- throw new Error(`Invalid resource URI: ${uri}`);
367
- }
368
- const toolName = match[1];
369
- const document = hasToolSchema(toolName) ? getToolSchemaDocument(toolName) : null;
370
- if (!document) {
371
- throw new Error(`Unknown tool: ${toolName}`);
372
- }
373
- return {
374
- contents: [
375
- {
376
- uri,
377
- mimeType: "application/json",
378
- text: JSON.stringify(document, null, 2),
379
- },
380
- ],
381
- };
382
- });
336
+ // Resources: every tool's full schema (lazy loading), and the category
337
+ // manifest. An unknown URI is a resource-not-found protocol error.
338
+ // Filtered as tools/list is: standalone mode advertised the 5 desktop-only schemas (audit L6).
339
+ server.setRequestHandler("resources/list", async () => ({ resources: listResources(isStandaloneMode() ? true : undefined) }));
340
+ server.setRequestHandler("resources/read", async (request) => readResource(request.params.uri, isStandaloneMode() ? true : undefined));
383
341
  // Prompts: user-invoked workflows (surfaced as slash commands by some hosts).
384
342
  server.setRequestHandler("prompts/list", async () => ({ prompts: listPrompts() }));
385
343
  server.setRequestHandler("prompts/get", async (request) => {
386
344
  const prompt = getPrompt(request.params.name, request.params.arguments);
387
- if (!prompt) {
388
- throw new Error(`Unknown prompt: ${request.params.name}`);
389
- }
345
+ if (!prompt)
346
+ throw unknownPromptError(request.params.name);
390
347
  return prompt;
391
348
  });
392
349
  // Execute a tool
393
350
  server.setRequestHandler("tools/call", async (request) => {
394
351
  const { name, arguments: args } = request.params;
352
+ // An unknown tool NAME is a JSON-RPC Invalid Params error (MCP spec), not
353
+ // a tool result: it used to come back as `isError` content like a tool
354
+ // that ran and failed.
355
+ const entry = toolEntry(name);
356
+ if (!entry)
357
+ throw unknownToolError(name);
395
358
  // Arguments are checked against the published schema before anything
396
359
  // runs, so a wrong type or a misspelled parameter comes back as a
397
360
  // correctable error rather than a Node exception or a silent no-op.
398
- const entry = TOOL_REGISTRY[name];
399
- if (entry) {
400
- const problem = validateToolArgs(name, entry.definition.inputSchema, (args ?? {}));
401
- if (problem) {
402
- return {
403
- content: [{ type: "text", text: JSON.stringify({ error: problem }) }],
404
- isError: true,
405
- };
406
- }
361
+ const problem = validateToolArgs(name, entry.definition.inputSchema, (args ?? {}));
362
+ if (problem) {
363
+ return {
364
+ content: [{ type: "text", text: JSON.stringify({ error: problem }) }],
365
+ isError: true,
366
+ };
407
367
  }
408
368
  try {
409
369
  const database = databaseFor(name);
410
- return await dispatchTool(name, database, args);
370
+ const result = await dispatchTool(name, database, args);
371
+ const note = database?.takeCreationNote() ?? null;
372
+ return note ? withDatabaseNote(result, note) : result;
411
373
  }
412
374
  catch (error) {
413
375
  // A database that turns out damaged mid-session (SQLITE_CORRUPT on a
@@ -431,6 +393,15 @@ export function buildServer() {
431
393
  });
432
394
  return server;
433
395
  }
396
+ function withDatabaseNote(result, note) {
397
+ const r = result;
398
+ if (r.structuredContent) {
399
+ const payload = r.structuredContent;
400
+ const structuredContent = { ...payload, _meta: { ...(payload._meta ?? {}), database_note: note } };
401
+ return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
402
+ }
403
+ return { ...result, content: [...r.content, { type: "text", text: JSON.stringify({ _meta: { database_note: note } }) }] };
404
+ }
434
405
  // =============================================================================
435
406
  // Server Lifecycle
436
407
  // =============================================================================
@@ -593,9 +564,8 @@ async function main() {
593
564
  db.close();
594
565
  process.exit(0);
595
566
  });
596
- const toolCount = getSlimToolList(validation.standalone ? true : undefined).length;
597
- const toolLabel = validation.standalone ? "standalone tools" : "tools";
598
- console.error(`4DA MCP Server v${SERVER_VERSION} started — ${toolCount} ${toolLabel}, stdio transport`);
567
+ // What this start will serve: with the database refused, every call returns that error (audit L6).
568
+ console.error(`4DA MCP Server v${SERVER_VERSION} started — ${startupToolsLine(validation, Boolean(sqlite.driver), DATABASE_FREE_TOOLS.size)}, stdio transport`);
599
569
  console.error(" Use --http for Streamable HTTP, --setup to configure editors, --doctor to check health");
600
570
  }
601
571
  main().catch((error) => {
@@ -0,0 +1,68 @@
1
+ /**
2
+ * One advisory, many records. OSV publishes the same issue as a GHSA record
3
+ * and as an ecosystem record (PYSEC, RUSTSEC, GO), each naming the other in
4
+ * `aliases`. Counting records counted one issue two or three times:
5
+ * - tokio 1.18.0 showed "6 advisories" for 3 (audit A9);
6
+ * - pygoat's django "96 known vulnerabilities" for 51 (readiness audit M1, 2026-10-16).
7
+ * Every count of advisories goes through here.
8
+ */
9
+ import type { VulnerabilityEntry } from "./types.js";
10
+ export interface AdvisoryGroup {
11
+ /** Every id the group's records carry, aliases included. */
12
+ ids: Set<string>;
13
+ /** The most severe record, so merging never softens a verdict. */
14
+ best: VulnerabilityEntry;
15
+ members: VulnerabilityEntry[];
16
+ }
17
+ /**
18
+ * Group the records of ONE installed copy by shared ids. The grouping is transitive: a record
19
+ * naming ids of two groups joins them.
20
+ */
21
+ export declare function groupAdvisories(entries: readonly VulnerabilityEntry[] | undefined): AdvisoryGroup[];
22
+ /** One record of an alias group, as the group grade reads it. */
23
+ export interface GradedRecord {
24
+ id: string;
25
+ /** Its own grade: the curated label first, the CVSS band second (osv-scanner.ts deriveSeverity); unknown when it has none. */
26
+ own: VulnerabilityEntry["severity"];
27
+ aliases: readonly string[];
28
+ maintenance: boolean;
29
+ /** The record's CWE ids (`database_specific.cwe_ids`) and summary, for the malware predicate. */
30
+ cweIds?: readonly string[];
31
+ summary?: string;
32
+ }
33
+ /** The grade of one alias group, and where it came from. */
34
+ export interface GroupGrade {
35
+ /** Null when nothing grades it, even through its aliases. */
36
+ severity: Exclude<VulnerabilityEntry["severity"], "unknown"> | null;
37
+ /** The GHSA alias an inherited grade came from; null for a grade a record of the group has itself. */
38
+ inheritedFrom: string | null;
39
+ /** The group record whose own grade it is, when it is one. */
40
+ ownFrom: string | null;
41
+ /**
42
+ * The base urgency the Upgrade Plan and the alerts rank by, before the scope rule: the tier
43
+ * (`low` reading `watch`); a group nothing grades is `medium`; a group of maintenance notices only
44
+ * `watch`; a malicious-package record (MAL-) `high`.
45
+ */
46
+ urgency: "critical" | "high" | "medium" | "watch";
47
+ }
48
+ /**
49
+ * THE grade of one alias group, shared with the 4DA app (`fourda_depengine::severity`, vectors
50
+ * severity.jsonl sv-001..sv-019): the MOST severe OWN grade among the group's records; only when no
51
+ * record has one, the highest grade among their GHSA aliases that can be read (`aliasGrades`, null =
52
+ * held but ungraded), the smallest id on a tie. An unmaintained-package notice is never graded
53
+ * from an alias.
54
+ */
55
+ export declare function gradeGroup(records: readonly GradedRecord[], aliasGrades: ReadonlyMap<string, VulnerabilityEntry["severity"] | null>): GroupGrade;
56
+ /**
57
+ * One severity per advisory, in every tool (the rule shared with the 4DA app, gradeGroup): within
58
+ * one alias group of one installed copy, every record carries the group's grade. vulnerability_scan
59
+ * kept the "richest" record's grade while dependency_check, dependency_health and upgrade_planner
60
+ * kept the most severe, so CVE-2026-4277 (GHSA-pwjp-ccjc-ghwg rated LOW, PYSEC-2026-52 CVSS 9.8) was
61
+ * low in one tool and critical in the others (final release audit of 6.2.5, 2026-10-18). A record
62
+ * that does not carry the grade itself names where it came from (`severityFrom`: the group record,
63
+ * or the GHSA alias of an inherited grade). A grade a record took from its alias
64
+ * (osv-scanner.ts rateFromAliases) never overrides an own grade in its group.
65
+ */
66
+ export declare function gradeAliasGroups(entries: readonly VulnerabilityEntry[]): VulnerabilityEntry[];
67
+ /** Distinct advisories among the records of one installed copy. */
68
+ export declare function distinctAdvisoryCount(entries: readonly VulnerabilityEntry[] | undefined): number;