@4da/mcp-server 4.6.2 → 4.6.3

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/dist/db.d.ts CHANGED
@@ -53,14 +53,26 @@ export declare class FourDADatabase {
53
53
  */
54
54
  get isStandalone(): boolean;
55
55
  /**
56
- * Validate that a database file exists, is readable, and passes integrity checks.
56
+ * Validate that a database file exists and is a readable SQLite database.
57
57
  *
58
58
  * Use this before accepting tool calls to ensure the database is in a good state.
59
59
  *
60
+ * By default this runs a CHEAP probe (open + list tables, ~0.1s) which surfaces
61
+ * a missing, locked, or corrupt-header file. It deliberately does NOT run a full
62
+ * `PRAGMA integrity_check`: that is an O(database-size) scan (~11s on a 1.7 GB DB,
63
+ * and growing) and, because better-sqlite3 is synchronous, it blocks the event
64
+ * loop — running it on the startup path stalls the MCP stdio handshake until it
65
+ * finishes, so the host's connect timeout can fire and the server looks like it
66
+ * "failed to start". Pass `{ deep: true }` for the full integrity_check on the
67
+ * explicit `--doctor` diagnostic path, where a slow, thorough scan is expected.
68
+ *
60
69
  * @param dbPath - Path to the database file. If omitted, uses the default resolution.
70
+ * @param opts.deep - Run the full `PRAGMA integrity_check` (slow, blocking). Default false.
61
71
  * @returns Validation result with table list on success, or error details on failure.
62
72
  */
63
- static validateDatabase(dbPath?: string): DatabaseValidationResult;
73
+ static validateDatabase(dbPath?: string, opts?: {
74
+ deep?: boolean;
75
+ }): DatabaseValidationResult;
64
76
  /**
65
77
  * Get the raw better-sqlite3 database instance for custom queries
66
78
  */
package/dist/db.js CHANGED
@@ -121,14 +121,24 @@ export class FourDADatabase {
121
121
  return this._isStandalone;
122
122
  }
123
123
  /**
124
- * Validate that a database file exists, is readable, and passes integrity checks.
124
+ * Validate that a database file exists and is a readable SQLite database.
125
125
  *
126
126
  * Use this before accepting tool calls to ensure the database is in a good state.
127
127
  *
128
+ * By default this runs a CHEAP probe (open + list tables, ~0.1s) which surfaces
129
+ * a missing, locked, or corrupt-header file. It deliberately does NOT run a full
130
+ * `PRAGMA integrity_check`: that is an O(database-size) scan (~11s on a 1.7 GB DB,
131
+ * and growing) and, because better-sqlite3 is synchronous, it blocks the event
132
+ * loop — running it on the startup path stalls the MCP stdio handshake until it
133
+ * finishes, so the host's connect timeout can fire and the server looks like it
134
+ * "failed to start". Pass `{ deep: true }` for the full integrity_check on the
135
+ * explicit `--doctor` diagnostic path, where a slow, thorough scan is expected.
136
+ *
128
137
  * @param dbPath - Path to the database file. If omitted, uses the default resolution.
138
+ * @param opts.deep - Run the full `PRAGMA integrity_check` (slow, blocking). Default false.
129
139
  * @returns Validation result with table list on success, or error details on failure.
130
140
  */
131
- static validateDatabase(dbPath) {
141
+ static validateDatabase(dbPath, opts) {
132
142
  const resolvedPath = dbPath || getDefaultDbPath();
133
143
  const absolutePath = path.isAbsolute(resolvedPath)
134
144
  ? resolvedPath
@@ -141,21 +151,24 @@ export class FourDADatabase {
141
151
  error: `No existing database at ${absolutePath}. Standalone mode will create one on startup.`,
142
152
  };
143
153
  }
144
- // Try to open and run integrity check
145
154
  let testDb = null;
146
155
  try {
147
156
  testDb = new Database(absolutePath, { readonly: true });
148
- // Run integrity check
149
- const integrityResult = testDb.pragma("integrity_check");
150
- const integrityStatus = integrityResult[0]?.integrity_check;
151
- if (integrityStatus !== "ok") {
152
- return {
153
- valid: false,
154
- error: `Database integrity check failed: ${integrityStatus}. `
155
- + "The database file may be corrupt. Try deleting data/4da.db and restarting 4DA.",
156
- };
157
+ // Deep, opt-in integrity check (slow: full-database scan). Only on --doctor.
158
+ if (opts?.deep) {
159
+ const integrityResult = testDb.pragma("integrity_check");
160
+ const integrityStatus = integrityResult[0]?.integrity_check;
161
+ if (integrityStatus !== "ok") {
162
+ return {
163
+ valid: false,
164
+ error: `Database integrity check failed: ${integrityStatus}. `
165
+ + "The database file may be corrupt. Try deleting data/4da.db and restarting 4DA.",
166
+ };
167
+ }
157
168
  }
158
- // List tables
169
+ // List tables. This reads the schema, so a truncated/corrupt-header file
170
+ // (not a valid SQLite DB) throws here and is reported below — giving the
171
+ // cheap path meaningful corruption detection without a full scan.
159
172
  const tables = testDb
160
173
  .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
161
174
  .all();
@@ -710,10 +723,17 @@ export class FourDADatabase {
710
723
  * These scores are from the full 5-axis PASIFA engine — much more accurate.
711
724
  */
712
725
  getRelevantContentFromRust(minScore, sourceType, limit, sinceDate) {
726
+ // Grounding subquery — guarded: older/partial 4DA DBs may have relevance_score
727
+ // (so the Rust path runs) but lack source_item_dependencies. Fall back to 0
728
+ // (→ evidence_class "semantic_only") rather than throwing.
729
+ const depCount = this.hasColumn("source_item_dependencies", "source_item_id")
730
+ ? `(SELECT COUNT(*) FROM source_item_dependencies d WHERE d.source_item_id = source_items.id)`
731
+ : `0`;
713
732
  let query = `
714
733
  SELECT id, source_type, source_id, url, title, content, content_hash,
715
734
  created_at, last_seen, relevance_score, content_type,
716
- signal_type, signal_priority
735
+ signal_type, signal_priority,
736
+ ${depCount} AS dep_match_count
717
737
  FROM source_items
718
738
  WHERE relevance_score >= ?
719
739
  AND datetime(created_at) >= datetime(?)
@@ -737,6 +757,14 @@ export class FourDADatabase {
737
757
  ? `${hoursAgo} hours ago`
738
758
  : `${Math.round(hoursAgo / 24)} days ago`;
739
759
  const necessity = this.getNecessityForItem(item.id);
760
+ // Provenance: grounded (matched one of the user's installed deps) vs
761
+ // semantic-only. A grounded security advisory is the OSV-verified gold tier.
762
+ const grounded = (item.dep_match_count ?? 0) > 0;
763
+ const evidence_class = grounded
764
+ ? item.content_type === "security_advisory"
765
+ ? "osv_verified"
766
+ : "dependency_grounded"
767
+ : "semantic_only";
740
768
  return {
741
769
  id: item.id,
742
770
  source_type: item.source_type,
@@ -753,6 +781,7 @@ export class FourDADatabase {
753
781
  necessity_urgency: necessity.urgency,
754
782
  signal_type: item.signal_type ?? null,
755
783
  signal_priority: item.signal_priority ?? null,
784
+ evidence_class,
756
785
  };
757
786
  });
758
787
  }
@@ -804,6 +833,8 @@ export class FourDADatabase {
804
833
  necessity_urgency: necessity.urgency,
805
834
  signal_type: null,
806
835
  signal_priority: null,
836
+ // Standalone keyword scorer — no Rust pipeline, no dependency grounding.
837
+ evidence_class: "keyword_heuristic",
807
838
  });
808
839
  }
809
840
  }
package/dist/doctor.js CHANGED
@@ -34,8 +34,10 @@ export function runDoctor() {
34
34
  status: "pass",
35
35
  detail: "better-sqlite3 loaded successfully",
36
36
  });
37
- // 3. Database discovery
38
- const validation = FourDADatabase.validateDatabase();
37
+ // 3. Database discovery — deep integrity_check is appropriate here: --doctor is
38
+ // an explicit, user-invoked diagnostic where a slow full scan is expected. The
39
+ // startup path uses the cheap default to keep the MCP handshake responsive.
40
+ const validation = FourDADatabase.validateDatabase(undefined, { deep: true });
39
41
  if (validation.valid) {
40
42
  checks.push({
41
43
  name: "4DA database",
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * do NOT expose it over a network without putting your own auth in front.
13
13
  */
14
14
  /**
15
- * 4DA MCP Server v4.6.2 — Dependency Intelligence for AI Coding Agents
15
+ * 4DA MCP Server v4.6.3 — Dependency Intelligence for AI Coding Agents
16
16
  *
17
17
  * 14 tools across 5 categories. Live vulnerability scanning (OSV.dev),
18
18
  * ecosystem news, persistent memory, and tech stack awareness for any MCP host.
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * do NOT expose it over a network without putting your own auth in front.
14
14
  */
15
15
  /**
16
- * 4DA MCP Server v4.6.2 — Dependency Intelligence for AI Coding Agents
16
+ * 4DA MCP Server v4.6.3 — Dependency Intelligence for AI Coding Agents
17
17
  *
18
18
  * 14 tools across 5 categories. Live vulnerability scanning (OSV.dev),
19
19
  * ecosystem news, persistent memory, and tech stack awareness for any MCP host.
@@ -51,7 +51,7 @@ import { createDatabase, FourDADatabase } from "./db.js";
51
51
  // =============================================================================
52
52
  const server = new Server({
53
53
  name: "4da-server",
54
- version: "4.6.2",
54
+ version: "4.6.3",
55
55
  }, {
56
56
  capabilities: {
57
57
  tools: { listChanged: true },
@@ -293,7 +293,7 @@ async function main() {
293
293
  const args = process.argv.slice(2);
294
294
  // Version
295
295
  if (args.includes("--version") || args.includes("-v")) {
296
- console.log("@4da/mcp-server 4.6.2");
296
+ console.log("@4da/mcp-server 4.6.3");
297
297
  return;
298
298
  }
299
299
  // Help
@@ -382,7 +382,7 @@ async function main() {
382
382
  process.exit(0);
383
383
  });
384
384
  const toolCount = getSlimToolList().length;
385
- console.error(`4DA MCP Server v4.6.2 started — ${toolCount} tools, stdio transport`);
385
+ console.error(`4DA MCP Server v4.6.3 started — ${toolCount} tools, stdio transport`);
386
386
  console.error(" Use --http for Streamable HTTP, --setup to configure editors, --doctor to check health");
387
387
  }
388
388
  main().catch((error) => {
@@ -25,6 +25,7 @@ export declare class LiveIntelligence {
25
25
  private resolvedDeps;
26
26
  private auditDeps;
27
27
  private initialized;
28
+ private projectRoot;
28
29
  constructor(db: Database.Database);
29
30
  /**
30
31
  * Initialize with project data. Call once after project scan.
@@ -56,6 +57,13 @@ export declare class LiveIntelligence {
56
57
  deps: string[];
57
58
  devDeps: string[];
58
59
  }>): void;
60
+ /**
61
+ * Root directory the dependency set was resolved from, or null before init.
62
+ * For multi-manifest (grouped) init this is the deepest common ancestor of
63
+ * the group directories — honest scope reporting for tool output, instead
64
+ * of whatever `process.cwd()` happens to be.
65
+ */
66
+ getProjectRoot(): string | null;
59
67
  /**
60
68
  * Run vulnerability scan (returns cached if fresh, fetches otherwise).
61
69
  */
@@ -32,6 +32,7 @@ export class LiveIntelligence {
32
32
  resolvedDeps = [];
33
33
  auditDeps = [];
34
34
  initialized = false;
35
+ projectRoot = null;
35
36
  constructor(db) {
36
37
  this.enabled = process.env.FOURDA_OFFLINE !== "true";
37
38
  this.cache = new LiveCache(db);
@@ -50,6 +51,7 @@ export class LiveIntelligence {
50
51
  initFromProject(projectPath, deps, devDeps, language) {
51
52
  this.resolvedDeps = resolveVersions(projectPath, deps, devDeps, language);
52
53
  this.auditDeps = resolveAuditVersions(projectPath, deps, devDeps, language);
54
+ this.projectRoot = projectPath;
53
55
  this.initialized = true;
54
56
  }
55
57
  /**
@@ -66,6 +68,7 @@ export class LiveIntelligence {
66
68
  }
67
69
  this.resolvedDeps = dedupeDependencies(allResolved);
68
70
  this.auditDeps = dedupeDependencies(allAudit);
71
+ this.projectRoot = projectPath;
69
72
  this.initialized = true;
70
73
  }
71
74
  /**
@@ -87,8 +90,18 @@ export class LiveIntelligence {
87
90
  }
88
91
  this.resolvedDeps = dedupeDependencies(allResolved);
89
92
  this.auditDeps = dedupeDependencies(allAudit);
93
+ this.projectRoot = commonPathRoot(groups.map((g) => g.dir));
90
94
  this.initialized = true;
91
95
  }
96
+ /**
97
+ * Root directory the dependency set was resolved from, or null before init.
98
+ * For multi-manifest (grouped) init this is the deepest common ancestor of
99
+ * the group directories — honest scope reporting for tool output, instead
100
+ * of whatever `process.cwd()` happens to be.
101
+ */
102
+ getProjectRoot() {
103
+ return this.projectRoot;
104
+ }
92
105
  /**
93
106
  * Run vulnerability scan (returns cached if fresh, fetches otherwise).
94
107
  */
@@ -227,6 +240,26 @@ export class LiveIntelligence {
227
240
  };
228
241
  }
229
242
  }
243
+ /**
244
+ * Deepest common ancestor of a set of directories (segment-wise, both slash
245
+ * styles). Null for an empty set; a single dir is its own root.
246
+ */
247
+ function commonPathRoot(dirs) {
248
+ if (dirs.length === 0)
249
+ return null;
250
+ const split = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
251
+ let common = split(dirs[0]);
252
+ for (const dir of dirs.slice(1)) {
253
+ const parts = split(dir);
254
+ let i = 0;
255
+ while (i < common.length && i < parts.length && common[i].toLowerCase() === parts[i].toLowerCase())
256
+ i++;
257
+ common = common.slice(0, i);
258
+ if (common.length === 0)
259
+ break;
260
+ }
261
+ return common.length > 0 ? common.join("/") : null;
262
+ }
230
263
  function dedupeDependencies(deps) {
231
264
  const unique = new Map();
232
265
  for (const dep of deps) {
@@ -1,4 +1,4 @@
1
- export type OsvEcosystem = "npm" | "crates.io" | "PyPI" | "Go" | "Maven" | "NuGet" | "RubyGems" | "Packagist";
1
+ export type OsvEcosystem = "npm" | "crates.io" | "PyPI" | "Go" | "Maven" | "NuGet" | "RubyGems" | "Packagist" | "Pub";
2
2
  export interface ResolvedDependency {
3
3
  name: string;
4
4
  version: string | null;
@@ -6,6 +6,16 @@
6
6
  * Uses only Node.js built-ins — no external parsers.
7
7
  */
8
8
  import type { OsvEcosystem, ResolvedDependency } from "./types.js";
9
+ /**
10
+ * Map a language/ecosystem alias to its OSV ecosystem id (case-insensitive).
11
+ *
12
+ * Falls back to "npm" ONLY for genuinely unrecognized input — a last resort, not
13
+ * a real default. OSV-unindexed languages (Swift, C/C++) and unknowns produce no
14
+ * OSV matches anyway (their package names don't collide with the npm namespace),
15
+ * so this never fabricates a vulnerability. Ecosystems that lack a version
16
+ * resolver here (NuGet/Maven/Packagist/RubyGems/Pub) still get the CORRECT label,
17
+ * so the moment a resolver is added the scan is right by construction.
18
+ */
9
19
  export declare function mapEcosystem(language: string): OsvEcosystem;
10
20
  export declare function resolveVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
11
21
  /**
@@ -9,18 +9,58 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { targetActiveOnHost } from "./platform.js";
12
+ // Alias -> OSV ecosystem. Mirrors the desktop app's canonical
13
+ // `Ecosystem::parse` (src-tauri/src/ecosystem.rs) so both sides recognize the
14
+ // SAME aliases — they previously drifted (the Rust side knew csharp/dart/php
15
+ // while this map keyed C# as "dotnet" and omitted Dart entirely, mislabeling
16
+ // those deps). Keep this in sync with that enum.
12
17
  const ECOSYSTEM_MAP = {
13
18
  npm: "npm",
19
+ javascript: "npm",
20
+ typescript: "npm",
21
+ node: "npm",
22
+ js: "npm",
23
+ ts: "npm",
14
24
  rust: "crates.io",
25
+ cargo: "crates.io",
26
+ "crates.io": "crates.io",
27
+ crates: "crates.io",
15
28
  python: "PyPI",
29
+ pypi: "PyPI",
30
+ pip: "PyPI",
31
+ py: "PyPI",
16
32
  go: "Go",
33
+ golang: "Go",
17
34
  java: "Maven",
35
+ maven: "Maven",
36
+ kotlin: "Maven",
37
+ gradle: "Maven",
38
+ csharp: "NuGet",
39
+ "c#": "NuGet",
18
40
  dotnet: "NuGet",
19
- ruby: "RubyGems",
41
+ nuget: "NuGet",
20
42
  php: "Packagist",
43
+ composer: "Packagist",
44
+ packagist: "Packagist",
45
+ ruby: "RubyGems",
46
+ rubygems: "RubyGems",
47
+ gem: "RubyGems",
48
+ dart: "Pub",
49
+ flutter: "Pub",
50
+ pub: "Pub",
21
51
  };
52
+ /**
53
+ * Map a language/ecosystem alias to its OSV ecosystem id (case-insensitive).
54
+ *
55
+ * Falls back to "npm" ONLY for genuinely unrecognized input — a last resort, not
56
+ * a real default. OSV-unindexed languages (Swift, C/C++) and unknowns produce no
57
+ * OSV matches anyway (their package names don't collide with the npm namespace),
58
+ * so this never fabricates a vulnerability. Ecosystems that lack a version
59
+ * resolver here (NuGet/Maven/Packagist/RubyGems/Pub) still get the CORRECT label,
60
+ * so the moment a resolver is added the scan is right by construction.
61
+ */
22
62
  export function mapEcosystem(language) {
23
- return ECOSYSTEM_MAP[language] || "npm";
63
+ return ECOSYSTEM_MAP[language.trim().toLowerCase()] || "npm";
24
64
  }
25
65
  export function resolveVersions(cwd, deps, devDeps, language, targets = {}) {
26
66
  const ecosystem = mapEcosystem(language);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get_relevant_content",
3
- "description": "Get filtered relevant content from 4DA's personalized feed.\n\nReturns content items that match the user's interests, tech stack, and learned preferences.\nItems are scored based on:\n- Explicit interests declared by the user\n- Tech stack and domains\n- ACE-detected context (recent files, projects, git activity)\n- Learned preferences from past interactions\n\nCall when the user asks what to read, what is relevant to them, or for content on a topic.",
3
+ "description": "Get filtered relevant content from 4DA's personalized feed.\n\nReturns content items that match the user's interests, tech stack, and learned preferences.\nItems are scored based on:\n- Explicit interests declared by the user\n- Tech stack and domains\n- ACE-detected context (recent files, projects, git activity)\n- Learned preferences from past interactions\n\nIMPORTANT for autonomous use: each item carries an `evidence_class` describing how it is grounded. The relevance_score float is NOT comparable across classes — gate actions on evidence_class, not the number:\n- \"osv_verified\": a security advisory matching one of the user's INSTALLED dependencies — safe to act on\n- \"dependency_grounded\": matched one of the user's declared dependencies — trustworthy\n- \"semantic_only\": topical/embedding relevance with NO dependency edge — surface to the human, verify before acting\n- \"keyword_heuristic\": standalone-mode keyword score (no Rust pipeline) — weakest signal\n\nCall when the user asks what to read, what is relevant to them, or for content on a topic.",
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upgrade_planner",
3
- "description": "Ranked upgrade recommendations for project dependencies. Combines vulnerability severity, deprecation status, and version distance to produce a prioritized upgrade plan.\n\nShows quick wins (safe patch/minor upgrades) separately from breaking changes (major version bumps). Call after dependency_health to plan your upgrade strategy.\n\nPrivacy: uses cached registry data. No additional network calls beyond what dependency_health already fetched.",
3
+ "description": "Ranked standalone upgrade recommendations for project dependencies — call before upgrading, adding, or auditing any dependency to pick the safest order. Combines vulnerability severity, deprecation status, and version distance into a prioritized plan.\n\nSplits recommendations three ways: quick wins (safe patch/minor upgrades), breaking changes (major version bumps), and transitive CVEs marked waiting_on_upstream (not declared in your manifest — the fix arrives via a parent package update or a lockfile refresh). Call dependency_health first for registry context and vulnerability_scan first for a CVE-aware plan; without a scan the output says it is CVE-blind.\n\nProvenance: standalone heuristic — a point-in-time plan from local lockfiles plus live registry/OSV lookups, not the 4DA desktop app's cross-project, version-confirmed plan.\n\nPrivacy: live mode may query public registries and OSV with package names and versions. It never sends source code.",
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what_should_i_know",
3
- "description": "Pre-task intelligence briefing. Given a task description and optional file paths, returns filtered advisories (security alerts, breaking changes), active decision windows, signal chains, relevant past decisions/memories, and a delegation assessment. Call this before starting any non-trivial task to get caught up on what matters.",
3
+ "description": "Pre-task intelligence briefing. Given a task description and optional file paths, returns filtered advisories (security alerts, breaking changes), active decision windows, signal chains, relevant past decisions/memories, and a delegation assessment. Call this before starting any non-trivial task to get caught up on what matters. If the task involves upgrading, adding, or auditing dependencies, follow with upgrade_planner for the ranked plan.",
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
@@ -137,7 +137,7 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
137
137
  parts.push("all healthy");
138
138
  return {
139
139
  scannedAt: new Date().toISOString(),
140
- projectPath: process.cwd(),
140
+ projectPath: liveIntel.getProjectRoot() ?? process.cwd(),
141
141
  ecosystemsScanned: ecosystems,
142
142
  totalDeps: total,
143
143
  outdatedCount: outdated,
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * upgrade_planner tool
3
3
  *
4
- * Ranked upgrade recommendations for project dependencies.
4
+ * Ranked standalone upgrade recommendations for project dependencies.
5
5
  * Combines version freshness, vulnerability data, and deprecation status
6
6
  * to produce a prioritized upgrade plan.
7
+ *
8
+ * Honesty contract (Phase 0 hardening):
9
+ * - Transitive CVEs are surfaced as `waiting_on_upstream` steps instead of
10
+ * being counted-but-never-recommended. No dependency-graph parsing happens
11
+ * here — parent mapping is the desktop core's job; the honest label is ours.
12
+ * - Output carries explicit heuristic provenance: this is a point-in-time
13
+ * standalone plan, not the 4DA app's cross-project, version-confirmed plan.
14
+ * - `projectPath` reports the directory set the dependencies were actually
15
+ * resolved from, not whatever `process.cwd()` happens to be.
16
+ * - When no vulnerability scan has run, the plan says so instead of silently
17
+ * producing a CVE-blind ranking.
7
18
  */
8
19
  import type { FourDADatabase } from "../db.js";
9
20
  import type { LiveIntelligence } from "../live/index.js";
@@ -22,6 +33,12 @@ interface UpgradeRecommendation {
22
33
  reasons: string[];
23
34
  breaking: boolean;
24
35
  isDev: boolean;
36
+ /** Whether this package is declared in a manifest (direct) or only present via the lockfile (transitive). */
37
+ scope: "direct" | "transitive";
38
+ /** Direct deps you can bump yourself; transitive fixes arrive via a parent update or lockfile refresh. */
39
+ action: "upgrade_direct" | "waiting_on_upstream";
40
+ /** False when the dependency is gated behind a target spec not active on this host (label, never suppressed). */
41
+ platformActive: boolean;
25
42
  }
26
43
  interface UpgradePlanResult {
27
44
  generatedAt: string;
@@ -31,6 +48,13 @@ interface UpgradePlanResult {
31
48
  summary: string;
32
49
  quickWins: number;
33
50
  breakingChanges: number;
51
+ waitingOnUpstream: number;
52
+ /** False when no vulnerability scan has run this session — the ranking is then CVE-blind and says so. */
53
+ vulnerabilityDataAvailable: boolean;
54
+ provenance: {
55
+ mode: "standalone_heuristic";
56
+ note: string;
57
+ };
34
58
  }
35
59
  export declare const upgradePlannerTool: {
36
60
  name: string;
@@ -2,13 +2,27 @@
2
2
  /**
3
3
  * upgrade_planner tool
4
4
  *
5
- * Ranked upgrade recommendations for project dependencies.
5
+ * Ranked standalone upgrade recommendations for project dependencies.
6
6
  * Combines version freshness, vulnerability data, and deprecation status
7
7
  * to produce a prioritized upgrade plan.
8
+ *
9
+ * Honesty contract (Phase 0 hardening):
10
+ * - Transitive CVEs are surfaced as `waiting_on_upstream` steps instead of
11
+ * being counted-but-never-recommended. No dependency-graph parsing happens
12
+ * here — parent mapping is the desktop core's job; the honest label is ours.
13
+ * - Output carries explicit heuristic provenance: this is a point-in-time
14
+ * standalone plan, not the 4DA app's cross-project, version-confirmed plan.
15
+ * - `projectPath` reports the directory set the dependencies were actually
16
+ * resolved from, not whatever `process.cwd()` happens to be.
17
+ * - When no vulnerability scan has run, the plan says so instead of silently
18
+ * producing a CVE-blind ranking.
8
19
  */
20
+ import { maxSemver } from "../live/semver-utils.js";
21
+ const PROVENANCE_NOTE = "Point-in-time heuristic from local lockfiles plus live registry/OSV lookups. " +
22
+ "Not the 4DA desktop app's cross-project, version-confirmed plan.";
9
23
  export const upgradePlannerTool = {
10
24
  name: "upgrade_planner",
11
- description: "Ranked upgrade recommendations for dependencies. Prioritizes by vulnerability severity, deprecation, and version distance. Shows quick wins (patch/minor) vs breaking changes (major).",
25
+ description: "Ranked standalone upgrade recommendations for dependencies — call before upgrading, adding, or auditing any dependency to pick the safest order. Prioritizes by vulnerability severity (direct AND transitive), deprecation, and version distance. Splits quick wins (patch/minor) from breaking changes (major) and from transitive CVEs waiting on upstream. Run vulnerability_scan first for a CVE-aware plan. Privacy: live mode may query public registries and OSV with package names and versions; it never sends source code.",
12
26
  inputSchema: {
13
27
  type: "object",
14
28
  properties: {
@@ -28,6 +42,14 @@ export const upgradePlannerTool = {
28
42
  },
29
43
  },
30
44
  };
45
+ const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
46
+ function severityToRisk(severities) {
47
+ if (severities.includes("critical"))
48
+ return "critical";
49
+ if (severities.includes("high"))
50
+ return "high";
51
+ return "medium";
52
+ }
31
53
  export async function executeUpgradePlanner(_db, params, liveIntel) {
32
54
  if (!liveIntel || !liveIntel.isInitialized()) {
33
55
  return {
@@ -38,16 +60,20 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
38
60
  summary: "No project detected. Run from a directory with package.json, Cargo.toml, pyproject.toml, or go.mod.",
39
61
  quickWins: 0,
40
62
  breakingChanges: 0,
63
+ waitingOnUpstream: 0,
64
+ vulnerabilityDataAvailable: false,
65
+ provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
41
66
  };
42
67
  }
43
68
  const includeDev = params.include_dev ?? false;
44
69
  let deps = liveIntel.getResolvedDeps();
45
70
  if (!includeDev)
46
71
  deps = deps.filter((d) => !d.isDev);
47
- // Fetch registry data
72
+ // Fetch registry data (live lookups for direct deps; cached where fresh)
48
73
  const registryData = await liveIntel.fetchRegistryHealth(deps);
49
- // Get vulnerability data
74
+ // Vulnerability data from the last scan this session (no network here).
50
75
  const vulnResult = liveIntel.getVulnerabilities();
76
+ const vulnerabilityDataAvailable = vulnResult !== null;
51
77
  const vulnsByPackage = new Map();
52
78
  if (vulnResult) {
53
79
  for (const v of vulnResult.vulnerabilities) {
@@ -58,31 +84,32 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
58
84
  vulnId: v.vulnId,
59
85
  summary: v.summary,
60
86
  fixedVersion: v.fixedVersion,
87
+ platformActive: v.platformActive,
61
88
  });
62
89
  }
63
90
  }
64
- // Build recommendations
91
+ // Build recommendations — direct deps (registry-informed)
65
92
  const recommendations = [];
93
+ const directPackages = new Set();
66
94
  for (const dep of registryData) {
95
+ directPackages.add(`${dep.ecosystem}\0${dep.name}`);
67
96
  const reasons = [];
68
97
  let risk = "low";
69
98
  let targetVersion = dep.latestStableVersion || dep.latestVersion;
99
+ let platformActive = true;
70
100
  // Check vulnerabilities
71
101
  const vulns = vulnsByPackage.get(dep.name);
72
102
  if (vulns && vulns.length > 0) {
73
- const hasCritical = vulns.some((v) => v.severity === "critical");
74
- const hasHigh = vulns.some((v) => v.severity === "high");
75
- if (hasCritical)
76
- risk = "critical";
77
- else if (hasHigh)
78
- risk = "high";
79
- else
80
- risk = "medium";
103
+ risk = severityToRisk(vulns.map((v) => v.severity));
81
104
  reasons.push(`${vulns.length} known CVE${vulns.length !== 1 ? "s" : ""} (${vulns.map((v) => v.vulnId).join(", ")})`);
105
+ if (vulns.every((v) => !v.platformActive)) {
106
+ platformActive = false;
107
+ reasons.push("Affected code is gated behind a target not active on this platform");
108
+ }
82
109
  // Use fixed version as target if available
83
- const fixedVersions = vulns.map((v) => v.fixedVersion).filter(Boolean);
110
+ const fixedVersions = vulns.map((v) => v.fixedVersion).filter((f) => Boolean(f));
84
111
  if (fixedVersions.length > 0 && !targetVersion) {
85
- targetVersion = fixedVersions[0];
112
+ targetVersion = maxSemver(fixedVersions);
86
113
  }
87
114
  }
88
115
  // Check deprecation
@@ -121,45 +148,101 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
121
148
  reasons,
122
149
  breaking: dep.versionsBehind?.label === "major",
123
150
  isDev: dep.isDev,
151
+ scope: "direct",
152
+ action: "upgrade_direct",
153
+ platformActive,
124
154
  });
125
155
  }
156
+ // Transitive vulnerable packages — previously counted in scan totals but
157
+ // never recommended. Surfaced as waiting_on_upstream with honest labels.
158
+ if (vulnResult) {
159
+ const transitive = new Map();
160
+ for (const v of vulnResult.vulnerabilities) {
161
+ if (v.isDirect)
162
+ continue;
163
+ if (directPackages.has(`${v.ecosystem}\0${v.package}`))
164
+ continue;
165
+ if (!includeDev && v.isDev && v.devScopeKnown)
166
+ continue;
167
+ const key = `${v.ecosystem}\0${v.package}`;
168
+ if (!transitive.has(key))
169
+ transitive.set(key, []);
170
+ transitive.get(key).push(v);
171
+ }
172
+ for (const entries of transitive.values()) {
173
+ const first = entries[0];
174
+ const fixedVersions = entries.map((e) => e.fixedVersion).filter((f) => Boolean(f));
175
+ const platformActive = !entries.every((e) => !e.platformActive);
176
+ const reasons = [
177
+ `${entries.length} known CVE${entries.length !== 1 ? "s" : ""} (${entries.map((e) => e.vulnId).join(", ")})`,
178
+ "Transitive dependency — not declared in your manifest; the fix arrives via a parent package update or a lockfile refresh",
179
+ ];
180
+ if (!platformActive) {
181
+ reasons.push("Affected code is gated behind a target not active on this platform");
182
+ }
183
+ recommendations.push({
184
+ package: first.package,
185
+ ecosystem: first.ecosystem,
186
+ currentVersion: first.currentVersion,
187
+ targetVersion: fixedVersions.length > 0 ? maxSemver(fixedVersions) : null,
188
+ upgradeType: "unknown",
189
+ risk: severityToRisk(entries.map((e) => e.severity)),
190
+ reasons,
191
+ breaking: false,
192
+ isDev: entries.every((e) => e.isDev),
193
+ scope: "transitive",
194
+ action: "waiting_on_upstream",
195
+ platformActive,
196
+ });
197
+ }
198
+ }
126
199
  // Filter by risk threshold
127
- const riskLevels = { low: 0, medium: 1, high: 2, critical: 3 };
128
200
  const threshold = params.risk_threshold ?? "all";
129
201
  const filtered = threshold === "all"
130
202
  ? recommendations
131
- : recommendations.filter((r) => riskLevels[r.risk] >= riskLevels[threshold]);
132
- // Sort: critical > high > medium > low, then by breaking (non-breaking first for quick wins)
203
+ : recommendations.filter((r) => RISK_LEVELS[r.risk] >= RISK_LEVELS[threshold]);
204
+ // Sort: critical > high > medium > low, then fixable-now (direct) before
205
+ // waiting-on-upstream (transitive), then non-breaking first (quick wins).
133
206
  filtered.sort((a, b) => {
134
- const riskDiff = riskLevels[b.risk] - riskLevels[a.risk];
207
+ const riskDiff = RISK_LEVELS[b.risk] - RISK_LEVELS[a.risk];
135
208
  if (riskDiff !== 0)
136
209
  return riskDiff;
210
+ if (a.scope !== b.scope)
211
+ return a.scope === "direct" ? -1 : 1;
137
212
  if (a.breaking !== b.breaking)
138
213
  return a.breaking ? 1 : -1;
139
214
  return 0;
140
215
  });
141
216
  const maxRecs = params.max_recommendations ?? 20;
142
217
  const limited = filtered.slice(0, maxRecs);
143
- const quickWins = limited.filter((r) => !r.breaking).length;
144
- const breakingChanges = limited.filter((r) => r.breaking).length;
218
+ const direct = limited.filter((r) => r.scope === "direct");
219
+ const quickWins = direct.filter((r) => !r.breaking).length;
220
+ const breakingChanges = direct.filter((r) => r.breaking).length;
221
+ const waitingOnUpstream = limited.filter((r) => r.scope === "transitive").length;
145
222
  // Summary
146
223
  const parts = [];
147
- parts.push(`${limited.length} upgrade${limited.length !== 1 ? "s" : ""} recommended`);
148
- if (quickWins > 0)
149
- parts.push(`${quickWins} quick win${quickWins !== 1 ? "s" : ""} (patch/minor)`);
150
- if (breakingChanges > 0)
151
- parts.push(`${breakingChanges} breaking change${breakingChanges !== 1 ? "s" : ""} (major)`);
224
+ parts.push(`${limited.length} recommendation${limited.length !== 1 ? "s" : ""}`);
225
+ if (direct.length > 0)
226
+ parts.push(`${direct.length} fixable directly (${quickWins} quick win${quickWins !== 1 ? "s" : ""}, ${breakingChanges} breaking)`);
227
+ if (waitingOnUpstream > 0)
228
+ parts.push(`${waitingOnUpstream} transitive, waiting on upstream`);
152
229
  const criticalCount = limited.filter((r) => r.risk === "critical").length;
153
230
  if (criticalCount > 0)
154
231
  parts.push(`${criticalCount} critical`);
232
+ if (!vulnerabilityDataAvailable) {
233
+ parts.push("CVE data not loaded — run vulnerability_scan first for a security-aware plan");
234
+ }
155
235
  return {
156
236
  generatedAt: new Date().toISOString(),
157
- projectPath: process.cwd(),
237
+ projectPath: liveIntel.getProjectRoot() ?? vulnResult?.projectPath ?? process.cwd(),
158
238
  totalDeps: deps.length,
159
239
  recommendations: limited,
160
240
  summary: parts.join(". ") + ".",
161
241
  quickWins,
162
242
  breakingChanges,
243
+ waitingOnUpstream,
244
+ vulnerabilityDataAvailable,
245
+ provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
163
246
  };
164
247
  }
165
248
  //# sourceMappingURL=upgrade-planner.js.map
@@ -20,7 +20,7 @@ import { memoryEmbedText } from "./agent-memory.js";
20
20
  // ============================================================================
21
21
  export const whatShouldIKnowTool = {
22
22
  name: "what_should_i_know",
23
- description: "Pre-task intelligence briefing. Given a task description and optional file paths, returns filtered advisories, decision windows, signal chains, relevant wisdom, and a delegation assessment. Call before starting any non-trivial task.",
23
+ description: "Pre-task intelligence briefing. Given a task description and optional file paths, returns filtered advisories, decision windows, signal chains, relevant wisdom, and a delegation assessment. Call before starting any non-trivial task. If the task involves upgrading, adding, or auditing dependencies, follow with upgrade_planner for the ranked plan.",
24
24
  inputSchema: {
25
25
  type: "object",
26
26
  properties: {
package/dist/types.d.ts CHANGED
@@ -42,6 +42,15 @@ export interface RelevantItem {
42
42
  signal_type: string | null;
43
43
  /** Pipeline-computed signal priority (critical, high, medium, low) */
44
44
  signal_priority: string | null;
45
+ /**
46
+ * Provenance of this item's relevance — how an autonomous agent should weight it.
47
+ * Scores alone are NOT comparable across classes; gate actions on this, not the float:
48
+ * - "osv_verified": a security advisory matching one of YOUR installed dependencies — safe to act on
49
+ * - "dependency_grounded": matched one of your declared dependencies — trustworthy
50
+ * - "semantic_only": topical/embedding relevance with NO dependency edge — verify before acting
51
+ * - "keyword_heuristic": standalone-mode keyword score (no Rust pipeline) — weakest signal
52
+ */
53
+ evidence_class: "osv_verified" | "dependency_grounded" | "semantic_only" | "keyword_heuristic";
45
54
  }
46
55
  /**
47
56
  * Explicit user interest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4da/mcp-server",
3
- "version": "4.6.2",
3
+ "version": "4.6.3",
4
4
  "mcpName": "io.github.runyourempire/4da-mcp-server",
5
5
  "description": "Dependency intelligence for AI coding agents. Live CVE scanning, dependency health, upgrade planning, ecosystem news, decision memory — zero config, privacy-first. 9 tools standalone, 14 total with desktop app.",
6
6
  "type": "module",
@@ -85,8 +85,9 @@
85
85
  },
86
86
  "devDependencies": {
87
87
  "@types/better-sqlite3": "^7.6.11",
88
- "@types/node": "^25.6.0",
88
+ "@types/node": "^26.0.1",
89
89
  "typescript": "^5.3.0",
90
+ "vite": "~7.3.5",
90
91
  "vitest": "^4.1.5"
91
92
  },
92
93
  "engines": {
@@ -99,7 +100,9 @@
99
100
  ],
100
101
  "overrides": {
101
102
  "fast-uri": ">=3.1.2",
102
- "ip-address": "^10.1.1"
103
+ "ip-address": "^10.1.1",
104
+ "vite": ">=7.3.5 <8",
105
+ "esbuild": ">=0.28.1"
103
106
  }
104
107
  }
105
108
  }