@4da/mcp-server 4.6.1 → 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.
Files changed (139) hide show
  1. package/README.md +3 -1
  2. package/dist/db.d.ts +21 -2
  3. package/dist/db.js +66 -16
  4. package/dist/doctor.js +5 -3
  5. package/dist/embeddings.d.ts +88 -0
  6. package/dist/embeddings.js +292 -0
  7. package/dist/index.d.ts +9 -9
  8. package/dist/index.js +21 -15
  9. package/dist/live/cvss.d.ts +16 -0
  10. package/dist/live/cvss.js +62 -0
  11. package/dist/live/index.d.ts +9 -1
  12. package/dist/live/index.js +40 -3
  13. package/dist/live/npm-registry.js +29 -6
  14. package/dist/live/osv-scanner.d.ts +8 -0
  15. package/dist/live/osv-scanner.js +115 -30
  16. package/dist/live/platform.d.ts +32 -0
  17. package/dist/live/platform.js +156 -0
  18. package/dist/live/semver-utils.d.ts +8 -0
  19. package/dist/live/semver-utils.js +24 -0
  20. package/dist/live/types.d.ts +31 -9
  21. package/dist/live/version-resolver.d.ts +12 -2
  22. package/dist/live/version-resolver.js +63 -20
  23. package/dist/llm.js +1 -1
  24. package/dist/project-scanner.d.ts +2 -0
  25. package/dist/project-scanner.js +34 -0
  26. package/dist/scheduler/core.d.ts +25 -0
  27. package/dist/scheduler/core.js +61 -0
  28. package/dist/scheduler/freshness.d.ts +17 -0
  29. package/dist/scheduler/freshness.js +41 -0
  30. package/dist/scheduler/index.d.ts +21 -0
  31. package/dist/scheduler/index.js +32 -0
  32. package/dist/scheduler/refreshers.d.ts +21 -0
  33. package/dist/scheduler/refreshers.js +21 -0
  34. package/dist/schemas/get-relevant-content.json +1 -1
  35. package/dist/schemas/upgrade-planner.json +1 -1
  36. package/dist/schemas/what-should-i-know.json +1 -1
  37. package/dist/tools/agent-memory.d.ts +11 -1
  38. package/dist/tools/agent-memory.js +120 -14
  39. package/dist/tools/decision-enforcement.d.ts +17 -1
  40. package/dist/tools/decision-enforcement.js +109 -81
  41. package/dist/tools/decision-memory.d.ts +1 -1
  42. package/dist/tools/decision-memory.js +58 -28
  43. package/dist/tools/decision-recall.d.ts +59 -0
  44. package/dist/tools/decision-recall.js +87 -0
  45. package/dist/tools/dependency-health.js +6 -4
  46. package/dist/tools/developer-dna.js +4 -2
  47. package/dist/tools/knowledge-gaps.js +16 -4
  48. package/dist/tools/recall.d.ts +33 -0
  49. package/dist/tools/recall.js +134 -0
  50. package/dist/tools/upgrade-planner.d.ts +25 -1
  51. package/dist/tools/upgrade-planner.js +111 -30
  52. package/dist/tools/vulnerability-scan.d.ts +46 -0
  53. package/dist/tools/vulnerability-scan.js +64 -24
  54. package/dist/tools/what-should-i-know.d.ts +3 -1
  55. package/dist/tools/what-should-i-know.js +210 -106
  56. package/dist/types.d.ts +9 -0
  57. package/package.json +8 -3
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js.map +0 -1
  60. package/dist/db.d.ts.map +0 -1
  61. package/dist/db.js.map +0 -1
  62. package/dist/doctor.d.ts.map +0 -1
  63. package/dist/doctor.js.map +0 -1
  64. package/dist/http-transport.d.ts.map +0 -1
  65. package/dist/http-transport.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/live/cache.d.ts.map +0 -1
  69. package/dist/live/cache.js.map +0 -1
  70. package/dist/live/crates-registry.d.ts.map +0 -1
  71. package/dist/live/crates-registry.js.map +0 -1
  72. package/dist/live/go-registry.d.ts.map +0 -1
  73. package/dist/live/go-registry.js.map +0 -1
  74. package/dist/live/hn-fetcher.d.ts.map +0 -1
  75. package/dist/live/hn-fetcher.js.map +0 -1
  76. package/dist/live/http-utils.d.ts.map +0 -1
  77. package/dist/live/http-utils.js.map +0 -1
  78. package/dist/live/index.d.ts.map +0 -1
  79. package/dist/live/index.js.map +0 -1
  80. package/dist/live/npm-registry.d.ts.map +0 -1
  81. package/dist/live/npm-registry.js.map +0 -1
  82. package/dist/live/osv-scanner.d.ts.map +0 -1
  83. package/dist/live/osv-scanner.js.map +0 -1
  84. package/dist/live/pypi-registry.d.ts.map +0 -1
  85. package/dist/live/pypi-registry.js.map +0 -1
  86. package/dist/live/rate-limiter.d.ts.map +0 -1
  87. package/dist/live/rate-limiter.js.map +0 -1
  88. package/dist/live/semver-utils.d.ts.map +0 -1
  89. package/dist/live/semver-utils.js.map +0 -1
  90. package/dist/live/types.d.ts.map +0 -1
  91. package/dist/live/types.js.map +0 -1
  92. package/dist/live/version-resolver.d.ts.map +0 -1
  93. package/dist/live/version-resolver.js.map +0 -1
  94. package/dist/live-singleton.d.ts.map +0 -1
  95. package/dist/live-singleton.js.map +0 -1
  96. package/dist/llm.d.ts.map +0 -1
  97. package/dist/llm.js.map +0 -1
  98. package/dist/output-manager.d.ts.map +0 -1
  99. package/dist/output-manager.js.map +0 -1
  100. package/dist/project-scanner.d.ts.map +0 -1
  101. package/dist/project-scanner.js.map +0 -1
  102. package/dist/schema-registry.d.ts.map +0 -1
  103. package/dist/schema-registry.js.map +0 -1
  104. package/dist/setup.d.ts.map +0 -1
  105. package/dist/setup.js.map +0 -1
  106. package/dist/tool-dispatch.d.ts.map +0 -1
  107. package/dist/tool-dispatch.js.map +0 -1
  108. package/dist/tools/agent-memory.d.ts.map +0 -1
  109. package/dist/tools/agent-memory.js.map +0 -1
  110. package/dist/tools/decision-enforcement.d.ts.map +0 -1
  111. package/dist/tools/decision-enforcement.js.map +0 -1
  112. package/dist/tools/decision-memory.d.ts.map +0 -1
  113. package/dist/tools/decision-memory.js.map +0 -1
  114. package/dist/tools/dependency-health.d.ts.map +0 -1
  115. package/dist/tools/dependency-health.js.map +0 -1
  116. package/dist/tools/developer-dna.d.ts.map +0 -1
  117. package/dist/tools/developer-dna.js.map +0 -1
  118. package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
  119. package/dist/tools/ecosystem-pulse.js.map +0 -1
  120. package/dist/tools/get-actionable-signals.d.ts.map +0 -1
  121. package/dist/tools/get-actionable-signals.js.map +0 -1
  122. package/dist/tools/get-context.d.ts.map +0 -1
  123. package/dist/tools/get-context.js.map +0 -1
  124. package/dist/tools/get-relevant-content.d.ts.map +0 -1
  125. package/dist/tools/get-relevant-content.js.map +0 -1
  126. package/dist/tools/index.d.ts.map +0 -1
  127. package/dist/tools/index.js.map +0 -1
  128. package/dist/tools/knowledge-gaps.d.ts.map +0 -1
  129. package/dist/tools/knowledge-gaps.js.map +0 -1
  130. package/dist/tools/record-feedback.d.ts.map +0 -1
  131. package/dist/tools/record-feedback.js.map +0 -1
  132. package/dist/tools/upgrade-planner.d.ts.map +0 -1
  133. package/dist/tools/upgrade-planner.js.map +0 -1
  134. package/dist/tools/vulnerability-scan.d.ts.map +0 -1
  135. package/dist/tools/vulnerability-scan.js.map +0 -1
  136. package/dist/tools/what-should-i-know.d.ts.map +0 -1
  137. package/dist/tools/what-should-i-know.js.map +0 -1
  138. package/dist/types.d.ts.map +0 -1
  139. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -100,6 +100,8 @@ Results are cached (24h for registry data, 1h for vulnerabilities, 30min for new
100
100
 
101
101
  **What's sent over the network:** package names + versions, generic tech keywords. The same data visible in your `package.json`. No source code, no file paths, no personal data. Set `FOURDA_OFFLINE=true` to disable all network calls.
102
102
 
103
+ > The one exception: if you *explicitly* configure an OpenAI embedding provider (`FOURDA_EMBED_PROVIDER=openai`) for semantic recall, the decision/memory text you store is sent to OpenAI to be embedded. The default — no embedding provider, or a local Ollama one — keeps everything on your machine, and `FOURDA_OFFLINE=true` overrides it regardless.
104
+
103
105
  **Ecosystems supported:** npm, crates.io (Rust), PyPI (Python), Go.
104
106
 
105
107
  ## What You Can Ask
@@ -206,7 +208,7 @@ npx @4da/mcp-server --version # Print version
206
208
  ## FAQ
207
209
 
208
210
  **Does this send my code anywhere?**
209
- No. The server sends package names and versions to public APIs ([OSV.dev](https://osv.dev), npm registry, crates.io, PyPI, Go proxy) and generic tech keywords to [HN Algolia](https://hn.algolia.com/api). The same public data visible in your `package.json`. No source code, no file paths, no personal data. Set `FOURDA_OFFLINE=true` to disable all network calls.
211
+ No. The server sends package names and versions to public APIs ([OSV.dev](https://osv.dev), npm registry, crates.io, PyPI, Go proxy) and generic tech keywords to [HN Algolia](https://hn.algolia.com/api). The same public data visible in your `package.json`. No source code, no file paths, no personal data. Set `FOURDA_OFFLINE=true` to disable all network calls. (The sole exception is opt-in OpenAI embeddings — see the network note above.)
210
212
 
211
213
  **Do I need the 4DA desktop app?**
212
214
  No. 9 tools work standalone: vulnerability scanning, dependency health, upgrade planning, ecosystem news, pre-task briefings, project context, decision memory, alignment checking, and agent memory. The desktop app adds scored content from 20+ sources that compounds over time.
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
  */
@@ -105,6 +117,13 @@ export declare class FourDADatabase {
105
117
  /** `MAX(sources.last_fetch)`, tolerant of a missing `sources` table (returns null). */
106
118
  private safeMaxLastFetch;
107
119
  private hasColumn;
120
+ /**
121
+ * Idempotently add a column to a table if it does not already exist.
122
+ * Used to add the optional `embedding` / `embedding_model` columns to
123
+ * pre-existing databases created before semantic recall shipped. Column/type
124
+ * are server-controlled constants (never user input), so the inline SQL is safe.
125
+ */
126
+ ensureColumn(table: string, column: string, type: string): void;
108
127
  /**
109
128
  * Use Rust-computed relevance_score from the desktop app's scoring pipeline.
110
129
  * These scores are from the full 5-axis PASIFA engine — much more accurate.
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();
@@ -356,6 +369,8 @@ export class FourDADatabase {
356
369
  CREATE INDEX IF NOT EXISTS idx_deps_project ON project_dependencies(project_path);
357
370
 
358
371
  -- Developer decisions — queried by tech_radar, decision_memory
372
+ -- embedding/embedding_model are populated lazily ONLY when an embedding
373
+ -- provider is configured (optional semantic recall); null otherwise.
359
374
  CREATE TABLE IF NOT EXISTS developer_decisions (
360
375
  id INTEGER PRIMARY KEY AUTOINCREMENT,
361
376
  decision_type TEXT NOT NULL,
@@ -368,7 +383,9 @@ export class FourDADatabase {
368
383
  status TEXT NOT NULL DEFAULT 'active',
369
384
  superseded_by INTEGER,
370
385
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
371
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
386
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
387
+ embedding BLOB,
388
+ embedding_model TEXT
372
389
  );
373
390
  CREATE INDEX IF NOT EXISTS idx_decisions_type ON developer_decisions(decision_type);
374
391
  CREATE INDEX IF NOT EXISTS idx_decisions_subject ON developer_decisions(subject);
@@ -399,6 +416,8 @@ export class FourDADatabase {
399
416
  );
400
417
 
401
418
  -- Agent memory — queried by agent_memory, agent_session_brief
419
+ -- embedding/embedding_model are populated lazily ONLY when an embedding
420
+ -- provider is configured (optional semantic recall); null otherwise.
402
421
  CREATE TABLE IF NOT EXISTS agent_memory (
403
422
  id INTEGER PRIMARY KEY AUTOINCREMENT,
404
423
  session_id TEXT NOT NULL,
@@ -409,7 +428,9 @@ export class FourDADatabase {
409
428
  context_tags TEXT DEFAULT '[]',
410
429
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
411
430
  expires_at TEXT,
412
- promoted_to_decision_id INTEGER
431
+ promoted_to_decision_id INTEGER,
432
+ embedding BLOB,
433
+ embedding_model TEXT
413
434
  );
414
435
  CREATE INDEX IF NOT EXISTS idx_agent_memory_type ON agent_memory(memory_type);
415
436
  CREATE INDEX IF NOT EXISTS idx_agent_memory_session ON agent_memory(session_id);
@@ -686,15 +707,33 @@ export class FourDADatabase {
686
707
  return false;
687
708
  }
688
709
  }
710
+ /**
711
+ * Idempotently add a column to a table if it does not already exist.
712
+ * Used to add the optional `embedding` / `embedding_model` columns to
713
+ * pre-existing databases created before semantic recall shipped. Column/type
714
+ * are server-controlled constants (never user input), so the inline SQL is safe.
715
+ */
716
+ ensureColumn(table, column, type) {
717
+ if (this.hasColumn(table, column))
718
+ return;
719
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
720
+ }
689
721
  /**
690
722
  * Use Rust-computed relevance_score from the desktop app's scoring pipeline.
691
723
  * These scores are from the full 5-axis PASIFA engine — much more accurate.
692
724
  */
693
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`;
694
732
  let query = `
695
733
  SELECT id, source_type, source_id, url, title, content, content_hash,
696
734
  created_at, last_seen, relevance_score, content_type,
697
- signal_type, signal_priority
735
+ signal_type, signal_priority,
736
+ ${depCount} AS dep_match_count
698
737
  FROM source_items
699
738
  WHERE relevance_score >= ?
700
739
  AND datetime(created_at) >= datetime(?)
@@ -718,6 +757,14 @@ export class FourDADatabase {
718
757
  ? `${hoursAgo} hours ago`
719
758
  : `${Math.round(hoursAgo / 24)} days ago`;
720
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";
721
768
  return {
722
769
  id: item.id,
723
770
  source_type: item.source_type,
@@ -734,6 +781,7 @@ export class FourDADatabase {
734
781
  necessity_urgency: necessity.urgency,
735
782
  signal_type: item.signal_type ?? null,
736
783
  signal_priority: item.signal_priority ?? null,
784
+ evidence_class,
737
785
  };
738
786
  });
739
787
  }
@@ -785,6 +833,8 @@ export class FourDADatabase {
785
833
  necessity_urgency: necessity.urgency,
786
834
  signal_type: null,
787
835
  signal_priority: null,
836
+ // Standalone keyword scorer — no Rust pipeline, no dependency grounding.
837
+ evidence_class: "keyword_heuristic",
788
838
  });
789
839
  }
790
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",
@@ -103,7 +105,7 @@ export function runDoctor() {
103
105
  const dbCheck = checks.find((c) => c.name === "4DA database");
104
106
  if (dbCheck && dbCheck.status === "warn") {
105
107
  console.log(" The MCP server can work standalone or with the 4DA desktop app.");
106
- console.log(" Standalone: scans your project, provides tech radar and context.");
108
+ console.log(" Standalone: scans your dependencies for known CVEs and provides project context.");
107
109
  console.log(" Full app: adds content scoring, source monitoring, and more.");
108
110
  console.log("");
109
111
  console.log(" Desktop app: https://github.com/runyourempire/4DA/releases/latest");
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Optional, provider-backed semantic embeddings for local knowledge recall.
3
+ *
4
+ * The MCP server ships ZERO embedding dependencies — that is the whole point of
5
+ * the `npx` zero-install promise. Semantic recall therefore lights up ONLY when
6
+ * the user already has an embedding provider reachable:
7
+ * - Ollama (local, private, free) — set settings.embedding.provider = "ollama"
8
+ * - OpenAI (cloud, explicit opt-in) — set settings.embedding.provider = "openai"
9
+ *
10
+ * When no provider is configured, the provider is unreachable, the embed model is
11
+ * missing, or FOURDA_OFFLINE is set, every caller falls back to ranked lexical
12
+ * recall and the response says so via `recall_mode`. Critically, the server embeds
13
+ * BOTH the query and the stored rows with the SAME provider/model, so the vector
14
+ * space is always self-consistent — we never mix our vectors with the desktop
15
+ * app's fastembed vectors.
16
+ *
17
+ * No sqlite-vec: similarity is a plain JS cosine over Float32 BLOBs, which is
18
+ * trivially fast for the few-hundred-row local knowledge tables this serves.
19
+ */
20
+ import type { FourDADatabase } from "./db.js";
21
+ export interface EmbeddingConfig {
22
+ provider: "ollama" | "openai";
23
+ model: string;
24
+ /** Ollama base URL (ignored for openai). */
25
+ url?: string;
26
+ /** OpenAI API key (ignored for ollama). */
27
+ apiKey?: string;
28
+ }
29
+ /** True when network calls are disabled by the FOURDA_OFFLINE escape hatch. */
30
+ export declare function isOfflineMode(): boolean;
31
+ /**
32
+ * Resolve the embedding provider. Order of precedence:
33
+ * 1. FOURDA_OFFLINE -> always null (no network)
34
+ * 2. environment variables -> FOURDA_EMBED_PROVIDER / FOURDA_EMBED_MODEL / ...
35
+ * 3. settings.json -> the `embedding` block
36
+ * 4. (nothing) -> null, callers stay lexical
37
+ *
38
+ * Deliberately does NOT auto-derive from the chat `llm` block: enabling semantic
39
+ * recall is an explicit choice so a recall never makes a surprise network call.
40
+ */
41
+ export declare function getEmbeddingConfig(): EmbeddingConfig | null;
42
+ /** A short, stable identity for the active model, stored alongside each vector. */
43
+ export declare function modelTag(config: EmbeddingConfig): string;
44
+ /**
45
+ * Embed a single text. Returns null on ANY failure (provider down, model
46
+ * missing, bad response, empty input) so callers degrade to lexical silently.
47
+ */
48
+ export declare function embedText(text: string, config: EmbeddingConfig, signal?: AbortSignal): Promise<Float32Array | null>;
49
+ /** Pack a vector into a little-endian Float32 BLOB for SQLite storage. */
50
+ export declare function vectorToBlob(vec: Float32Array): Buffer;
51
+ /**
52
+ * Unpack a stored BLOB back into a Float32Array. Returns null for empty/
53
+ * misaligned blobs so a corrupt cell never throws mid-recall. Copies the bytes
54
+ * to guarantee 4-byte alignment regardless of the source buffer's offset.
55
+ */
56
+ export declare function blobToVector(blob: Buffer | Uint8Array | null | undefined): Float32Array | null;
57
+ /** Cosine similarity in [-1, 1]; 0 for length mismatch or a zero vector. */
58
+ export declare function cosineSimilarity(a: Float32Array, b: Float32Array): number;
59
+ export interface SemanticScoreResult {
60
+ /** id -> cosine similarity to the query (only for rows that have a vector). */
61
+ semanticById: Map<number, number>;
62
+ /** False when the query itself could not be embedded — caller stays lexical. */
63
+ queryEmbedded: boolean;
64
+ /** How many candidate rows contributed a semantic score this call. */
65
+ embeddedCount: number;
66
+ /** How many rows were embedded fresh this call (lazy backfill). */
67
+ newlyEmbedded: number;
68
+ /** The provider:model tag the scores were produced with. */
69
+ model: string;
70
+ }
71
+ /**
72
+ * Score `items` (id + the text to embed) against a query using cached, lazily
73
+ * backfilled embeddings stored in `<table>.embedding` / `<table>.embedding_model`.
74
+ *
75
+ * Shared by every semantic-recall surface (agent_memory, developer_decisions) so
76
+ * the embed/cache/cosine logic lives in exactly one place. `table` is always a
77
+ * server-controlled constant (never user input), so the interpolated SQL is safe.
78
+ *
79
+ * Returns `queryEmbedded: false` (and an empty map) when the provider is
80
+ * unreachable, so callers fall back to pure lexical recall.
81
+ */
82
+ export declare function semanticScores(db: FourDADatabase, table: string, query: string, items: Array<{
83
+ id: number;
84
+ text: string;
85
+ }>, config: EmbeddingConfig, opts?: {
86
+ maxNewEmbeds?: number;
87
+ }): Promise<SemanticScoreResult>;
88
+ //# sourceMappingURL=embeddings.d.ts.map
@@ -0,0 +1,292 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Optional, provider-backed semantic embeddings for local knowledge recall.
4
+ *
5
+ * The MCP server ships ZERO embedding dependencies — that is the whole point of
6
+ * the `npx` zero-install promise. Semantic recall therefore lights up ONLY when
7
+ * the user already has an embedding provider reachable:
8
+ * - Ollama (local, private, free) — set settings.embedding.provider = "ollama"
9
+ * - OpenAI (cloud, explicit opt-in) — set settings.embedding.provider = "openai"
10
+ *
11
+ * When no provider is configured, the provider is unreachable, the embed model is
12
+ * missing, or FOURDA_OFFLINE is set, every caller falls back to ranked lexical
13
+ * recall and the response says so via `recall_mode`. Critically, the server embeds
14
+ * BOTH the query and the stored rows with the SAME provider/model, so the vector
15
+ * space is always self-consistent — we never mix our vectors with the desktop
16
+ * app's fastembed vectors.
17
+ *
18
+ * No sqlite-vec: similarity is a plain JS cosine over Float32 BLOBs, which is
19
+ * trivially fast for the few-hundred-row local knowledge tables this serves.
20
+ */
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));
25
+ const DEFAULT_OLLAMA_EMBED_MODEL = "nomic-embed-text";
26
+ const DEFAULT_OPENAI_EMBED_MODEL = "text-embedding-3-small";
27
+ const DEFAULT_OLLAMA_URL = "http://localhost:11434";
28
+ /** True when network calls are disabled by the FOURDA_OFFLINE escape hatch. */
29
+ export function isOfflineMode() {
30
+ return String(process.env.FOURDA_OFFLINE || "").toLowerCase() === "true";
31
+ }
32
+ /**
33
+ * Locate settings.json the same way llm.ts does, so embedding config lives
34
+ * alongside the rest of the user's local configuration.
35
+ */
36
+ function findSettingsFile() {
37
+ const envPath = process.env.FOURDA_SETTINGS_PATH;
38
+ if (envPath && existsSync(envPath))
39
+ return envPath;
40
+ const dbPath = process.env.FOURDA_DB_PATH;
41
+ if (dbPath) {
42
+ const sibling = join(dirname(dbPath), "settings.json");
43
+ if (existsSync(sibling))
44
+ return sibling;
45
+ }
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
+ if (existsSync(candidate))
53
+ return candidate;
54
+ }
55
+ return null;
56
+ }
57
+ let _cached = null;
58
+ /**
59
+ * Resolve the embedding provider. Order of precedence:
60
+ * 1. FOURDA_OFFLINE -> always null (no network)
61
+ * 2. environment variables -> FOURDA_EMBED_PROVIDER / FOURDA_EMBED_MODEL / ...
62
+ * 3. settings.json -> the `embedding` block
63
+ * 4. (nothing) -> null, callers stay lexical
64
+ *
65
+ * Deliberately does NOT auto-derive from the chat `llm` block: enabling semantic
66
+ * recall is an explicit choice so a recall never makes a surprise network call.
67
+ */
68
+ export function getEmbeddingConfig() {
69
+ if (isOfflineMode())
70
+ return null;
71
+ // 2. Environment override
72
+ const envProvider = (process.env.FOURDA_EMBED_PROVIDER || "").toLowerCase();
73
+ if (envProvider === "ollama" || envProvider === "openai") {
74
+ return normalizeConfig({
75
+ provider: envProvider,
76
+ model: process.env.FOURDA_EMBED_MODEL,
77
+ url: process.env.OLLAMA_URL,
78
+ api_key: process.env.OPENAI_API_KEY,
79
+ });
80
+ }
81
+ // 3. settings.json `embedding` block (mtime-cached)
82
+ const settingsPath = findSettingsFile();
83
+ if (!settingsPath)
84
+ return null;
85
+ try {
86
+ const mtimeMs = statSync(settingsPath).mtimeMs;
87
+ if (_cached && _cached.path === settingsPath && _cached.mtimeMs === mtimeMs) {
88
+ return _cached.config;
89
+ }
90
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
91
+ const raw = settings.embedding;
92
+ const config = raw
93
+ ? normalizeConfig({
94
+ provider: (raw.provider || "").toLowerCase(),
95
+ model: raw.model,
96
+ url: raw.url || raw.base_url,
97
+ api_key: raw.api_key,
98
+ })
99
+ : null;
100
+ _cached = { config, path: settingsPath, mtimeMs };
101
+ return config;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ function normalizeConfig(raw) {
108
+ if (raw.provider === "ollama") {
109
+ return {
110
+ provider: "ollama",
111
+ model: raw.model || DEFAULT_OLLAMA_EMBED_MODEL,
112
+ url: raw.url || DEFAULT_OLLAMA_URL,
113
+ };
114
+ }
115
+ if (raw.provider === "openai") {
116
+ if (!raw.api_key)
117
+ return null; // openai needs a key — no key, no semantic
118
+ return {
119
+ provider: "openai",
120
+ model: raw.model || DEFAULT_OPENAI_EMBED_MODEL,
121
+ apiKey: raw.api_key,
122
+ };
123
+ }
124
+ return null;
125
+ }
126
+ /** A short, stable identity for the active model, stored alongside each vector. */
127
+ export function modelTag(config) {
128
+ return `${config.provider}:${config.model}`;
129
+ }
130
+ // =============================================================================
131
+ // Embedding calls
132
+ // =============================================================================
133
+ /** Cap on the text length sent to the embedder, to bound payload size. */
134
+ const MAX_EMBED_CHARS = 8000;
135
+ /**
136
+ * Embed a single text. Returns null on ANY failure (provider down, model
137
+ * missing, bad response, empty input) so callers degrade to lexical silently.
138
+ */
139
+ export async function embedText(text, config, signal) {
140
+ const input = (text || "").slice(0, MAX_EMBED_CHARS).trim();
141
+ if (!input)
142
+ return null;
143
+ try {
144
+ if (config.provider === "ollama") {
145
+ const base = config.url || DEFAULT_OLLAMA_URL;
146
+ const res = await fetch(`${base}/api/embeddings`, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({ model: config.model, prompt: input }),
150
+ signal,
151
+ });
152
+ if (!res.ok)
153
+ return null;
154
+ const data = (await res.json());
155
+ return data.embedding && data.embedding.length
156
+ ? Float32Array.from(data.embedding)
157
+ : null;
158
+ }
159
+ // openai
160
+ const res = await fetch("https://api.openai.com/v1/embeddings", {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ Authorization: `Bearer ${config.apiKey}`,
165
+ },
166
+ body: JSON.stringify({ model: config.model, input }),
167
+ signal,
168
+ });
169
+ if (!res.ok)
170
+ return null;
171
+ const data = (await res.json());
172
+ const emb = data.data?.[0]?.embedding;
173
+ return emb && emb.length ? Float32Array.from(emb) : null;
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ // =============================================================================
180
+ // Vector helpers (BLOB <-> Float32Array, cosine)
181
+ // =============================================================================
182
+ /** Pack a vector into a little-endian Float32 BLOB for SQLite storage. */
183
+ export function vectorToBlob(vec) {
184
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
185
+ }
186
+ /**
187
+ * Unpack a stored BLOB back into a Float32Array. Returns null for empty/
188
+ * misaligned blobs so a corrupt cell never throws mid-recall. Copies the bytes
189
+ * to guarantee 4-byte alignment regardless of the source buffer's offset.
190
+ */
191
+ export function blobToVector(blob) {
192
+ if (!blob || blob.byteLength === 0 || blob.byteLength % 4 !== 0)
193
+ return null;
194
+ const copy = Buffer.from(blob);
195
+ return new Float32Array(copy.buffer, copy.byteOffset, copy.byteLength / 4);
196
+ }
197
+ /** Cosine similarity in [-1, 1]; 0 for length mismatch or a zero vector. */
198
+ export function cosineSimilarity(a, b) {
199
+ if (a.length !== b.length || a.length === 0)
200
+ return 0;
201
+ let dot = 0;
202
+ let na = 0;
203
+ let nb = 0;
204
+ for (let i = 0; i < a.length; i++) {
205
+ dot += a[i] * b[i];
206
+ na += a[i] * a[i];
207
+ nb += b[i] * b[i];
208
+ }
209
+ if (na === 0 || nb === 0)
210
+ return 0;
211
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
212
+ }
213
+ // =============================================================================
214
+ // Generic semantic scoring over a local knowledge table
215
+ // =============================================================================
216
+ /** Default cap on rows embedded per call, so a cold cache cannot stall a response. */
217
+ const DEFAULT_MAX_NEW_EMBEDS = 48;
218
+ /**
219
+ * Score `items` (id + the text to embed) against a query using cached, lazily
220
+ * backfilled embeddings stored in `<table>.embedding` / `<table>.embedding_model`.
221
+ *
222
+ * Shared by every semantic-recall surface (agent_memory, developer_decisions) so
223
+ * the embed/cache/cosine logic lives in exactly one place. `table` is always a
224
+ * server-controlled constant (never user input), so the interpolated SQL is safe.
225
+ *
226
+ * Returns `queryEmbedded: false` (and an empty map) when the provider is
227
+ * unreachable, so callers fall back to pure lexical recall.
228
+ */
229
+ export async function semanticScores(db, table, query, items, config, opts = {}) {
230
+ const tag = modelTag(config);
231
+ const queryVec = await embedText(query, config);
232
+ if (!queryVec) {
233
+ return { semanticById: new Map(), queryEmbedded: false, embeddedCount: 0, newlyEmbedded: 0, model: tag };
234
+ }
235
+ // Cache columns are optional and added lazily for databases that predate them.
236
+ db.ensureColumn(table, "embedding", "BLOB");
237
+ db.ensureColumn(table, "embedding_model", "TEXT");
238
+ const rawDb = db.getRawDb();
239
+ const stored = loadStoredEmbeddings(rawDb, table, items.map((i) => i.id), tag);
240
+ const updateStmt = rawDb.prepare(`UPDATE ${table} SET embedding = ?, embedding_model = ? WHERE id = ?`);
241
+ const maxNew = opts.maxNewEmbeds ?? DEFAULT_MAX_NEW_EMBEDS;
242
+ const semanticById = new Map();
243
+ let embeddedCount = 0;
244
+ let newlyEmbedded = 0;
245
+ for (const item of items) {
246
+ let vec = stored.get(item.id) ?? null;
247
+ if (!vec && newlyEmbedded < maxNew) {
248
+ vec = await embedText(item.text, config);
249
+ if (vec) {
250
+ try {
251
+ updateStmt.run(vectorToBlob(vec), tag, item.id);
252
+ }
253
+ catch {
254
+ // Persisting the cache is best-effort; scoring still proceeds.
255
+ }
256
+ newlyEmbedded++;
257
+ }
258
+ }
259
+ if (vec && vec.length === queryVec.length) {
260
+ embeddedCount++;
261
+ semanticById.set(item.id, cosineSimilarity(queryVec, vec));
262
+ }
263
+ }
264
+ return { semanticById, queryEmbedded: true, embeddedCount, newlyEmbedded, model: tag };
265
+ }
266
+ /**
267
+ * Load cached embeddings for the given ids that were produced by the CURRENT model
268
+ * (a model change invalidates old vectors). Returns a map of id -> vector.
269
+ */
270
+ function loadStoredEmbeddings(rawDb, table, ids, modelTagValue) {
271
+ const out = new Map();
272
+ if (ids.length === 0)
273
+ return out;
274
+ try {
275
+ const placeholders = ids.map(() => "?").join(",");
276
+ const rows = rawDb
277
+ .prepare(`SELECT id, embedding, embedding_model FROM ${table} WHERE id IN (${placeholders})`)
278
+ .all(...ids);
279
+ for (const row of rows) {
280
+ if (row.embedding && row.embedding_model === modelTagValue) {
281
+ const vec = blobToVector(row.embedding);
282
+ if (vec)
283
+ out.set(row.id, vec);
284
+ }
285
+ }
286
+ }
287
+ catch {
288
+ // Defensive: a missing column simply means no cache, so everything re-embeds.
289
+ }
290
+ return out;
291
+ }
292
+ //# sourceMappingURL=embeddings.js.map
package/dist/index.d.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * 4DA MCP Server - Internal Use Only
3
+ * 4DA MCP Server
4
4
  *
5
- * This MCP server provides tool and resource access to 4DA's local database
6
- * for use with Claude Desktop and other MCP-compatible hosts.
5
+ * Provides dependency-intelligence tools (CVE scanning, dependency health,
6
+ * upgrade planning, ecosystem news, decision/agent memory) to Claude Code,
7
+ * Claude Desktop, and other MCP hosts. Runs locally; the only data leaving the
8
+ * machine is public package names/versions sent to registries (OSV, npm, etc.).
7
9
  *
8
- * IMPORTANT: This server is designed for LOCAL use only. It reads from the
9
- * user's local 4DA SQLite database. It does NOT implement authentication
10
- * or authorization - it trusts the local environment.
11
- *
12
- * Do NOT expose this server over a network without adding proper auth.
10
+ * SECURITY: the server trusts its local environment and does NOT implement
11
+ * authentication. The optional --http transport is intended for localhost use;
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.0 — 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.