@4da/mcp-server 5.0.3 → 5.0.4

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.js CHANGED
@@ -836,7 +836,17 @@ export class FourDADatabase {
836
836
  query += ` AND source_type = ?`;
837
837
  params.push(sourceType);
838
838
  }
839
- query += ` ORDER BY relevance_score DESC LIMIT ?`;
839
+ // Ranked read (desktop audit items 12+26, schema 110): order by the
840
+ // batch-relative rank when the analysis cycle has ranked the item, falling
841
+ // back to the evidence score. This mirrors the Rust source of truth —
842
+ // `RANKED_ORDER_EXPR` in `src-tauri/src/db/scoring_queries.rs` — and must
843
+ // stay textually in sync with it. The `relevance_score >= ?` membership
844
+ // filter above deliberately stays on evidence: evidence decides membership,
845
+ // rank decides order. Guarded on column existence for pre-110 DBs.
846
+ const rankedOrder = this.hasColumn("source_items", "rank_score")
847
+ ? `COALESCE(rank_score, relevance_score) DESC`
848
+ : `relevance_score DESC`;
849
+ query += ` ORDER BY ${rankedOrder} LIMIT ?`;
840
850
  // Over-fetch so near-duplicate collapsing below can still fill `limit`
841
851
  // (the same story arrives via reddit + rss + mastodon: live feeds showed
842
852
  // three copies of "Rust 1.98.0" in one 20-item response).
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { LiveCache } from "./cache.js";
10
10
  import type { RateLimiter } from "./rate-limiter.js";
11
- import type { ResolvedDependency, VulnerabilityScanResult } from "./types.js";
11
+ import type { ResolvedDependency, OsvVulnerability, VulnerabilityScanResult } from "./types.js";
12
12
  export declare class OsvScanner {
13
13
  private cache;
14
14
  private rateLimiter;
@@ -28,3 +28,22 @@ export declare function extractCvssScore(severity: Array<{
28
28
  type: string;
29
29
  score: string;
30
30
  }> | undefined): number | null;
31
+ /**
32
+ * Pick the fix version for the OSV range that actually CONTAINS the installed
33
+ * version — never another release line's fix. The old code returned the first
34
+ * range's first `fixed` event, so a multi-range advisory (e.g. undici fixed at
35
+ * 6.x on the 6 line and 7.x on the 7 line) recommended a DOWNGRADE to 6.x for
36
+ * an installed 7.x.
37
+ *
38
+ * Selection order:
39
+ * 1. The `fixed` of the interval containing installed (introduced <= installed
40
+ * < fixed).
41
+ * 2. If that interval has no fix (or none contains installed), the smallest
42
+ * published fix strictly ABOVE installed across all ranges.
43
+ * 3. If the installed version is unparseable, the highest published fix (it
44
+ * resolves the advisory regardless of release line).
45
+ * 4. Otherwise null — omitting the recommendation beats emitting a downgrade.
46
+ *
47
+ * Hard guard: the returned version is never below the installed version.
48
+ */
49
+ export declare function extractFixedVersion(affected: OsvVulnerability["affected"] | undefined, packageName: string, ecosystem: string, installedVersion: string | null): string | null;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { fetchWithTimeout, fetchJson } from "./http-utils.js";
11
11
  import { cvssBaseScore } from "./cvss.js";
12
+ import { compareSemver, maxSemver, parseSemver } from "./semver-utils.js";
12
13
  const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
13
14
  const OSV_VULN_URL = "https://api.osv.dev/v1/vulns/";
14
15
  const OSV_TIMEOUT_MS = 15_000;
@@ -174,7 +175,7 @@ export class OsvScanner {
174
175
  }
175
176
  function mapVulnerability(vuln, dep) {
176
177
  const { severity, cvssScore } = deriveSeverity(vuln);
177
- const fixedVersion = extractFixedVersion(vuln.affected, dep.name, dep.ecosystem);
178
+ const fixedVersion = extractFixedVersion(vuln.affected, dep.name, dep.ecosystem, dep.version);
178
179
  return {
179
180
  package: dep.name,
180
181
  currentVersion: dep.version || "unknown",
@@ -260,18 +261,91 @@ function cvssToSeverity(score) {
260
261
  return "low";
261
262
  return "unknown";
262
263
  }
263
- function extractFixedVersion(affected, packageName, ecosystem) {
264
+ /**
265
+ * Pick the fix version for the OSV range that actually CONTAINS the installed
266
+ * version — never another release line's fix. The old code returned the first
267
+ * range's first `fixed` event, so a multi-range advisory (e.g. undici fixed at
268
+ * 6.x on the 6 line and 7.x on the 7 line) recommended a DOWNGRADE to 6.x for
269
+ * an installed 7.x.
270
+ *
271
+ * Selection order:
272
+ * 1. The `fixed` of the interval containing installed (introduced <= installed
273
+ * < fixed).
274
+ * 2. If that interval has no fix (or none contains installed), the smallest
275
+ * published fix strictly ABOVE installed across all ranges.
276
+ * 3. If the installed version is unparseable, the highest published fix (it
277
+ * resolves the advisory regardless of release line).
278
+ * 4. Otherwise null — omitting the recommendation beats emitting a downgrade.
279
+ *
280
+ * Hard guard: the returned version is never below the installed version.
281
+ */
282
+ export function extractFixedVersion(affected, packageName, ecosystem, installedVersion) {
264
283
  if (!affected)
265
284
  return null;
285
+ const intervals = collectVulnerableIntervals(affected, packageName, ecosystem);
286
+ const fixes = intervals
287
+ .map((i) => i.fixed)
288
+ .filter((f) => f !== null);
289
+ if (fixes.length === 0)
290
+ return null;
291
+ if (installedVersion === null || parseSemver(installedVersion) === null) {
292
+ // Can't place the installed version on a release line — recommend the
293
+ // highest published fix so the upgrade resolves the advisory either way.
294
+ return maxSemver(fixes);
295
+ }
296
+ // The interval whose release line the installed version sits on. `compareSemver`
297
+ // returns 0 for unparseable bounds (e.g. introduced "0" = since inception),
298
+ // which keeps those intervals eligible on the introduced side only.
299
+ const containing = intervals.find((i) => compareSemver(installedVersion, i.introduced) >= 0 &&
300
+ (i.fixed === null || compareSemver(installedVersion, i.fixed) < 0));
301
+ if (containing?.fixed &&
302
+ compareSemver(containing.fixed, installedVersion) > 0) {
303
+ return containing.fixed;
304
+ }
305
+ // No fix on the installed line (open interval, or installed already past
306
+ // every containing range): smallest fix strictly above installed, or nothing.
307
+ const above = fixes.filter((f) => compareSemver(f, installedVersion) > 0);
308
+ if (above.length === 0)
309
+ return null;
310
+ return above.reduce((min, v) => (compareSemver(v, min) < 0 ? v : min), above[0]);
311
+ }
312
+ /**
313
+ * Flatten the matching package's version ranges into [introduced, fixed)
314
+ * intervals. OSV sorts events within a range; each `introduced` opens an
315
+ * interval and the next `fixed` closes it. GIT ranges hold commit hashes,
316
+ * not versions, and are skipped.
317
+ */
318
+ function collectVulnerableIntervals(affected, packageName, ecosystem) {
319
+ const intervals = [];
266
320
  for (const a of affected) {
267
- if (a.package.name === packageName && a.package.ecosystem === ecosystem) {
268
- for (const range of a.ranges || []) {
269
- for (const event of range.events || []) {
270
- if (event.fixed)
271
- return event.fixed;
321
+ if (a.package.name !== packageName || a.package.ecosystem !== ecosystem)
322
+ continue;
323
+ for (const range of a.ranges || []) {
324
+ if (range.type === "GIT")
325
+ continue;
326
+ let open = null;
327
+ for (const event of range.events || []) {
328
+ if (event.introduced !== undefined) {
329
+ if (open)
330
+ intervals.push(open);
331
+ open = { introduced: event.introduced, fixed: null };
332
+ }
333
+ else if (event.fixed !== undefined) {
334
+ if (open) {
335
+ open.fixed = event.fixed;
336
+ intervals.push(open);
337
+ open = null;
338
+ }
339
+ else {
340
+ // `fixed` with no preceding `introduced` — treat as vulnerable
341
+ // since inception ("0" = OSV's since-the-beginning sentinel).
342
+ intervals.push({ introduced: "0", fixed: event.fixed });
343
+ }
272
344
  }
273
345
  }
346
+ if (open)
347
+ intervals.push(open);
274
348
  }
275
349
  }
276
- return null;
350
+ return intervals;
277
351
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4da/mcp-server",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "mcpName": "io.github.4DA-Systems/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",