@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.
- package/CHANGELOG.md +484 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +258 -9
- package/dist/live/fix-paths.js +790 -45
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +49 -0
- package/dist/live/version-compare.js +131 -0
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +59 -4
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- package/package.json +2 -2
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
|
|
38
|
-
* 1. FOURDA_DB_PATH
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
|
|
52
|
-
if (
|
|
53
|
-
return process.
|
|
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
|
|
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
|
package/dist/embeddings.d.ts
CHANGED
|
@@ -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
|
|
20
|
+
import { type FourDADatabase } from "./db.js";
|
|
21
21
|
export interface EmbeddingConfig {
|
|
22
22
|
provider: "ollama" | "openai";
|
|
23
23
|
model: string;
|
package/dist/embeddings.js
CHANGED
|
@@ -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 {
|
|
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
|
|
34
|
-
*
|
|
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
|
|
47
|
-
join(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
99
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
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
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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;
|