mcp-rag-memory 2.4.1 → 2.7.0
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/.env.example +3 -1
- package/LICENSE +21 -0
- package/README.md +151 -16
- package/dist/cli.js +102 -34
- package/dist/db/database.d.ts +10 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/database.js +119 -18
- package/dist/db/database.js.map +1 -1
- package/dist/env.js +32 -12
- package/dist/mcp/rag-server.js +140 -39
- package/dist/mcp/rag-server.js.map +1 -1
- package/dist/memory/memory.d.ts +5 -1
- package/dist/memory/memory.d.ts.map +1 -1
- package/dist/memory/memory.js +245 -101
- package/dist/memory/memory.js.map +1 -1
- package/dist/rag/chunker.d.ts +7 -0
- package/dist/rag/chunker.d.ts.map +1 -1
- package/dist/rag/chunker.js +119 -11
- package/dist/rag/chunker.js.map +1 -1
- package/dist/rag/embeddings.d.ts +25 -0
- package/dist/rag/embeddings.d.ts.map +1 -1
- package/dist/rag/embeddings.js +56 -4
- package/dist/rag/embeddings.js.map +1 -1
- package/dist/rag/pipeline.d.ts +19 -2
- package/dist/rag/pipeline.d.ts.map +1 -1
- package/dist/rag/pipeline.js +212 -39
- package/dist/rag/pipeline.js.map +1 -1
- package/dist/rag/reindex.d.ts +27 -0
- package/dist/rag/reindex.d.ts.map +1 -0
- package/dist/rag/reindex.js +138 -0
- package/dist/rag/reindex.js.map +1 -0
- package/dist/rag/vector-search.d.ts +26 -1
- package/dist/rag/vector-search.d.ts.map +1 -1
- package/dist/rag/vector-search.js +207 -65
- package/dist/rag/vector-search.js.map +1 -1
- package/dist/session/transcript.d.ts +15 -5
- package/dist/session/transcript.d.ts.map +1 -1
- package/dist/session/transcript.js +152 -25
- package/dist/session/transcript.js.map +1 -1
- package/package.json +6 -3
package/dist/db/database.js
CHANGED
|
@@ -17,18 +17,59 @@ export function getDB() {
|
|
|
17
17
|
return _db;
|
|
18
18
|
ensureDir();
|
|
19
19
|
const db = new DatabaseSync(DB_PATH);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
try {
|
|
21
|
+
// busy_timeout first: every later statement should inherit it.
|
|
22
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
23
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
24
|
+
db.exec("PRAGMA synchronous = NORMAL;");
|
|
25
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
26
|
+
// Migrate BEFORE caching the handle: a failed migration must surface and be
|
|
27
|
+
// retried on the next call, never leave a half-migrated connection cached.
|
|
28
|
+
migrate(db);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
// The handle is NOT cached on failure, so without this close every failed
|
|
32
|
+
// init leaked a file handle — reachable whenever a concurrent writer holds
|
|
33
|
+
// the lock, and it blocked later cleanup of the store directory on Windows.
|
|
34
|
+
try {
|
|
35
|
+
db.close();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* already closed */
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
23
42
|
_db = db;
|
|
24
|
-
migrate(db);
|
|
25
43
|
return db;
|
|
26
44
|
}
|
|
27
45
|
function columnExists(db, table, column) {
|
|
46
|
+
// PRAGMA cannot take a bound parameter, so the identifier is interpolated.
|
|
47
|
+
// Only ever called with literals; assert it so a future caller cannot inject.
|
|
48
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table))
|
|
49
|
+
throw new Error(`Unsafe table name: ${table}`);
|
|
28
50
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
29
51
|
return cols.some((c) => c.name === column);
|
|
30
52
|
}
|
|
31
53
|
function migrate(db) {
|
|
54
|
+
// One IMMEDIATE transaction for the whole schema step: two processes opening
|
|
55
|
+
// the same fresh store (CLI + MCP server + plugin) must not race and end up
|
|
56
|
+
// with half-applied DDL or a "duplicate column" failure.
|
|
57
|
+
db.exec("BEGIN IMMEDIATE;");
|
|
58
|
+
try {
|
|
59
|
+
createSchema(db);
|
|
60
|
+
db.exec("COMMIT;");
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
try {
|
|
64
|
+
db.exec("ROLLBACK;");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* transaction already closed */
|
|
68
|
+
}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function createSchema(db) {
|
|
32
73
|
db.exec(`
|
|
33
74
|
CREATE TABLE IF NOT EXISTS documents (
|
|
34
75
|
id TEXT PRIMARY KEY,
|
|
@@ -51,6 +92,10 @@ function migrate(db) {
|
|
|
51
92
|
created_at INTEGER NOT NULL
|
|
52
93
|
);
|
|
53
94
|
CREATE INDEX IF NOT EXISTS idx_chunks_doc_id ON chunks(doc_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_documents_updated ON documents(updated_at DESC);
|
|
96
|
+
-- Filtered searches resolve doc ids via source/doc_id first; without this
|
|
97
|
+
-- every rag_search(source=...) scanned the whole documents table.
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_documents_source ON documents(source);
|
|
54
99
|
|
|
55
100
|
CREATE TABLE IF NOT EXISTS memories (
|
|
56
101
|
id TEXT PRIMARY KEY,
|
|
@@ -66,6 +111,7 @@ function migrate(db) {
|
|
|
66
111
|
);
|
|
67
112
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
68
113
|
CREATE INDEX IF NOT EXISTS idx_memories_prune ON memories(importance, recall_count, created_at);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC);
|
|
69
115
|
|
|
70
116
|
CREATE TABLE IF NOT EXISTS meta (
|
|
71
117
|
key TEXT PRIMARY KEY,
|
|
@@ -84,17 +130,24 @@ function migrate(db) {
|
|
|
84
130
|
const ftsRow = db
|
|
85
131
|
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'chunks_fts'")
|
|
86
132
|
.get();
|
|
87
|
-
if (
|
|
88
|
-
db.exec("CREATE VIRTUAL TABLE chunks_fts USING fts5(content, tokenize = 'trigram');");
|
|
89
|
-
}
|
|
90
|
-
else if (!ftsRow.sql.includes("trigram")) {
|
|
133
|
+
if (ftsRow) {
|
|
91
134
|
// Existing store built with 'unicode61' — recreate with trigram and backfill.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
135
|
+
if (!(ftsRow.sql ?? "").includes("trigram"))
|
|
136
|
+
rebuildFts(db);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// First run with this schema (or a store whose FTS table was dropped):
|
|
140
|
+
// create AND backfill, otherwise existing chunks silently lose keyword search
|
|
141
|
+
// forever — the next boot sees "trigram" and would skip the rebuild.
|
|
142
|
+
rebuildFts(db);
|
|
96
143
|
}
|
|
97
144
|
}
|
|
145
|
+
/** Runs inside the migrate() transaction. */
|
|
146
|
+
function rebuildFts(db) {
|
|
147
|
+
db.exec("DROP TABLE IF EXISTS chunks_fts;");
|
|
148
|
+
db.exec("CREATE VIRTUAL TABLE chunks_fts USING fts5(content, tokenize = 'trigram');");
|
|
149
|
+
db.exec("INSERT INTO chunks_fts(rowid, content) SELECT rowid, content FROM chunks;");
|
|
150
|
+
}
|
|
98
151
|
/** SHA-256 hex of raw content — used for document dedup. */
|
|
99
152
|
export function contentHash(content) {
|
|
100
153
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
@@ -102,13 +155,13 @@ export function contentHash(content) {
|
|
|
102
155
|
export function newId() {
|
|
103
156
|
return randomUUID();
|
|
104
157
|
}
|
|
105
|
-
/** Serialize a Float64Array embedding to a SQLite BLOB. */
|
|
158
|
+
/** Serialize a Float64Array embedding to a SQLite BLOB (view-safe). */
|
|
106
159
|
export function packVector(v) {
|
|
107
|
-
return new Uint8Array(v.buffer.slice(
|
|
160
|
+
return new Uint8Array(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength));
|
|
108
161
|
}
|
|
109
|
-
/** Deserialize a BLOB back into a Float64Array. */
|
|
162
|
+
/** Deserialize a BLOB back into a Float64Array. Returns null on absent/partial data. */
|
|
110
163
|
export function unpackVector(blob) {
|
|
111
|
-
if (!blob || blob.byteLength === 0)
|
|
164
|
+
if (!blob || blob.byteLength === 0 || blob.byteLength % 8 !== 0)
|
|
112
165
|
return null;
|
|
113
166
|
return new Float64Array(blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength));
|
|
114
167
|
}
|
|
@@ -118,24 +171,41 @@ export function nowMs() {
|
|
|
118
171
|
export function closeDB() {
|
|
119
172
|
_db?.close();
|
|
120
173
|
_db = null;
|
|
174
|
+
cachedDim = null;
|
|
121
175
|
}
|
|
122
176
|
/* ------------------------------------------------------------------ */
|
|
123
177
|
/* Embedding dimension metadata */
|
|
124
178
|
/* ------------------------------------------------------------------ */
|
|
179
|
+
/** Cached embed_dim: locked on first write, so it is read once per process. */
|
|
180
|
+
let cachedDim = null;
|
|
125
181
|
export function getEmbedDim() {
|
|
126
|
-
|
|
127
|
-
|
|
182
|
+
if (cachedDim === null) {
|
|
183
|
+
const r = getDB().prepare("SELECT value FROM meta WHERE key = 'embed_dim'").get();
|
|
184
|
+
const n = Number(r?.value);
|
|
185
|
+
// A corrupt row must not cache NaN: it would wedge this process with a
|
|
186
|
+
// misleading "dimension mismatch" until restart.
|
|
187
|
+
cachedDim = Number.isInteger(n) && n > 0 ? n : 0;
|
|
188
|
+
}
|
|
189
|
+
return cachedDim;
|
|
128
190
|
}
|
|
129
191
|
export function setEmbedMeta(dim, model) {
|
|
130
192
|
const db = getDB();
|
|
131
193
|
db.prepare("INSERT INTO meta(key, value) VALUES ('embed_dim', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(dim));
|
|
132
194
|
db.prepare("INSERT INTO meta(key, value) VALUES ('embed_model', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(model);
|
|
195
|
+
cachedDim = dim;
|
|
196
|
+
// A completed migration restores the store to a coherent state.
|
|
197
|
+
setEmbedState("ready");
|
|
133
198
|
}
|
|
134
199
|
/**
|
|
135
200
|
* Lock the store to one embedding dimension. Call after producing a vector.
|
|
136
201
|
* Prevents silently mixing vectors of different models/providers.
|
|
137
202
|
*/
|
|
138
203
|
export function ensureDim(vec, model) {
|
|
204
|
+
// A half-migrated store mixes old and new vectors: cosine across two spaces
|
|
205
|
+
// is meaningless and every search silently returns noise. Blocking here stops
|
|
206
|
+
// it at the one chokepoint every search and ingest already passes through,
|
|
207
|
+
// rather than letting a partial reindex look like a working store.
|
|
208
|
+
assertEmbedStateReady();
|
|
139
209
|
const current = getEmbedDim();
|
|
140
210
|
if (current === 0) {
|
|
141
211
|
setEmbedMeta(vec.length, model);
|
|
@@ -146,9 +216,40 @@ export function ensureDim(vec, model) {
|
|
|
146
216
|
`new vectors are ${vec.length} (model: ${model}). ` +
|
|
147
217
|
`Use a consistent EMBEDDING_PROVIDER or wipe ${STORAGE_DIR}.`);
|
|
148
218
|
}
|
|
219
|
+
// Same-dimension model swaps are the dangerous case: blending two vector
|
|
220
|
+
// spaces degrades search to noise with no error anywhere, and the only clue
|
|
221
|
+
// was a stale model name inside a dimension-mismatch message that never fired.
|
|
222
|
+
const currentModel = getEmbedModel();
|
|
223
|
+
if (currentModel && currentModel !== model) {
|
|
224
|
+
throw new Error(`Embedding model mismatch: DB stores ${currentModel}, requested ${model} (both ${current}-dim). ` +
|
|
225
|
+
`Point RAG_DB_DIR at a fresh store or re-ingest with a consistent EMBEDDING_MODEL. ` +
|
|
226
|
+
`Current store: ${STORAGE_DIR}.`);
|
|
227
|
+
}
|
|
149
228
|
}
|
|
150
229
|
export function getEmbedModel() {
|
|
151
230
|
const r = getDB().prepare("SELECT value FROM meta WHERE key = 'embed_model'").get();
|
|
152
231
|
return r ? r.value : "";
|
|
153
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* `embed_state` is '' (or 'ready') for a coherent store and 'reindexing' while a
|
|
235
|
+
* vector-space migration is in flight.
|
|
236
|
+
*/
|
|
237
|
+
export function getEmbedState() {
|
|
238
|
+
const r = getDB().prepare("SELECT value FROM meta WHERE key = 'embed_state'").get();
|
|
239
|
+
return r ? r.value : "";
|
|
240
|
+
}
|
|
241
|
+
export function setEmbedState(state) {
|
|
242
|
+
getDB()
|
|
243
|
+
.prepare("INSERT INTO meta(key, value) VALUES('embed_state', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
244
|
+
.run(state);
|
|
245
|
+
}
|
|
246
|
+
/** Throws when the store is mid-reindex. Called from ensureDim, the shared gate. */
|
|
247
|
+
export function assertEmbedStateReady() {
|
|
248
|
+
const state = getEmbedState();
|
|
249
|
+
if (state !== "" && state !== "ready") {
|
|
250
|
+
throw new Error(`Store is mid-reindex (embed_state=${state}): vectors from two embedding models are ` +
|
|
251
|
+
`mixed and search results would be meaningless. Run 'reindex' again to finish the ` +
|
|
252
|
+
`migration. Current store: ${STORAGE_DIR}.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
154
255
|
//# sourceMappingURL=database.js.map
|
package/dist/db/database.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU;IAC/C,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACtC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC,CAAC;AAE7C,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AAE5D,IAAI,GAAG,GAAwB,IAAI,CAAC;AAEpC,SAAS,SAAS;IAChB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,KAAK;IACnB,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IACpB,SAAS,EAAE,CAAC;IACZ,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IACrC,EAAE,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
|
package/dist/env.js
CHANGED
|
@@ -6,38 +6,58 @@
|
|
|
6
6
|
* config (MCP clients, CI, shell) always wins. Runs synchronously at import —
|
|
7
7
|
* must be imported before any module that reads `process.env.*` at load time.
|
|
8
8
|
* Supports `#` comments, `export KEY=` prefixes, and single/double-quoted values.
|
|
9
|
+
*
|
|
10
|
+
* Only this package's own knobs are loadable: an untrusted repo's `.env` must
|
|
11
|
+
* not be able to inject arbitrary process.env entries into the host session.
|
|
9
12
|
*/
|
|
10
13
|
import * as fs from "fs";
|
|
11
14
|
import * as path from "path";
|
|
15
|
+
/** Prefixes/names this server actually reads. */
|
|
16
|
+
const LOADABLE = /^(RAG_[A-Z0-9_]+|OPENCODE_[A-Z0-9_]+|EMBEDDING_[A-Z0-9_]+|SEARCH_MODE)$/;
|
|
17
|
+
const VALID_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
18
|
+
/** Strip one layer of matching quotes plus any trailing `# comment`. */
|
|
19
|
+
function unquote(value) {
|
|
20
|
+
const q = value[0];
|
|
21
|
+
if (q === '"' || q === "'") {
|
|
22
|
+
const close = value.indexOf(q, 1);
|
|
23
|
+
if (close > 0) {
|
|
24
|
+
const rest = value.slice(close + 1).trim();
|
|
25
|
+
if (rest === "" || rest.startsWith("#"))
|
|
26
|
+
return value.slice(1, close);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const hash = value.indexOf(" #");
|
|
30
|
+
return hash >= 0 ? value.slice(0, hash).trimEnd() : value;
|
|
31
|
+
}
|
|
12
32
|
export function loadDotenv(file) {
|
|
13
33
|
let raw;
|
|
14
34
|
try {
|
|
15
35
|
raw = fs.readFileSync(file, "utf8");
|
|
16
36
|
}
|
|
17
|
-
catch {
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (process.env.RAG_ENV_FILE) {
|
|
39
|
+
process.stderr.write(`env: cannot read RAG_ENV_FILE ${file} (${e.message})\n`);
|
|
40
|
+
}
|
|
18
41
|
return;
|
|
19
42
|
}
|
|
20
43
|
for (const line of raw.split(/\r?\n/)) {
|
|
21
44
|
let t = line.trim();
|
|
22
45
|
if (!t || t.startsWith("#"))
|
|
23
46
|
continue;
|
|
24
|
-
if (t.startsWith("export "))
|
|
47
|
+
if (t.startsWith("export ") || t.startsWith("export\t"))
|
|
25
48
|
t = t.slice(7).trimStart();
|
|
26
49
|
const eq = t.indexOf("=");
|
|
27
50
|
if (eq <= 0)
|
|
28
51
|
continue;
|
|
29
52
|
const key = t.slice(0, eq).trim();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
value = value.slice(0, hash).trimEnd();
|
|
36
|
-
}
|
|
37
|
-
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
38
|
-
value = value.slice(1, -1);
|
|
53
|
+
if (!VALID_KEY.test(key))
|
|
54
|
+
continue;
|
|
55
|
+
if (!LOADABLE.test(key)) {
|
|
56
|
+
process.stderr.write(`env: ignoring ${key} (not an mcp-rag-memory variable)\n`);
|
|
57
|
+
continue;
|
|
39
58
|
}
|
|
40
|
-
|
|
59
|
+
const value = unquote(t.slice(eq + 1).trim());
|
|
60
|
+
if (process.env[key] === undefined)
|
|
41
61
|
process.env[key] = value;
|
|
42
62
|
}
|
|
43
63
|
}
|
package/dist/mcp/rag-server.js
CHANGED
|
@@ -4,13 +4,32 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { STORAGE_DIR } from "../db/database.js";
|
|
7
|
+
import { STORAGE_DIR, closeDB, getEmbedState } from "../db/database.js";
|
|
8
8
|
import { embeddingInfo } from "../rag/embeddings.js";
|
|
9
|
-
import {
|
|
9
|
+
import { reindex } from "../rag/reindex.js";
|
|
10
|
+
import { ingestText, ingestFile, ingestDirectory, searchDocs, retrieve, listDocuments, deleteDocument, documentStats, MAX_INGEST_CHARS, } from "../rag/pipeline.js";
|
|
10
11
|
import { ingestSession as syncSession, ingestLatest as syncLatest, } from "../session/transcript.js";
|
|
11
12
|
import { remember, recall, listMemories, getMemory, updateMemory, forget, consolidate, memoryStats, contextPrompt, MEMORY_TYPES, } from "../memory/memory.js";
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
13
14
|
const serverVersion = require("../../package.json").version;
|
|
15
|
+
/**
|
|
16
|
+
* Input bounds for every tool schema.
|
|
17
|
+
*
|
|
18
|
+
* These were all unbounded `z.string().min(1)`, which meant a single tool call
|
|
19
|
+
* could push a megabyte of text straight into the chunker and embedder (a
|
|
20
|
+
* synchronous CPU spin that blocks the whole stdio server), or a path long
|
|
21
|
+
* enough to be a denial of service. The caps sit above any realistic input and
|
|
22
|
+
* well below anything that would stall the process.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_ID = 200;
|
|
25
|
+
const MAX_QUERY = 2_000;
|
|
26
|
+
const MAX_PATH = 4_096;
|
|
27
|
+
const MAX_TITLE = 500;
|
|
28
|
+
const MAX_TAG = 64;
|
|
29
|
+
const MAX_TAGS = 64;
|
|
30
|
+
const MAX_TOPIC = 500;
|
|
31
|
+
/** A memory is a self-contained statement, not a document. */
|
|
32
|
+
const MAX_MEMORY_CONTENT = 20_000;
|
|
14
33
|
const server = new McpServer({
|
|
15
34
|
name: "rag-memory-server",
|
|
16
35
|
version: serverVersion,
|
|
@@ -24,25 +43,65 @@ server.registerTool("system_stats", {
|
|
|
24
43
|
}, async () => {
|
|
25
44
|
const docs = documentStats();
|
|
26
45
|
const mem = memoryStats();
|
|
46
|
+
// Surface the embed_state too: a store stuck mid-reindex refuses every search,
|
|
47
|
+
// and system_stats is the first thing anyone calls when a tool starts failing.
|
|
48
|
+
const state = getEmbedState();
|
|
27
49
|
return {
|
|
28
50
|
content: [
|
|
29
51
|
{
|
|
30
52
|
type: "text",
|
|
31
|
-
text: JSON.stringify({
|
|
53
|
+
text: JSON.stringify({
|
|
54
|
+
dbDir: STORAGE_DIR,
|
|
55
|
+
embedding: embeddingInfo(),
|
|
56
|
+
storeState: state === "" || state === "ready" ? "ready" : state,
|
|
57
|
+
...(state !== "" && state !== "ready"
|
|
58
|
+
? { remediation: "Run 'rag_reindex' to finish the interrupted migration." }
|
|
59
|
+
: {}),
|
|
60
|
+
documents: docs,
|
|
61
|
+
memories: mem,
|
|
62
|
+
}, null, 2),
|
|
32
63
|
},
|
|
33
64
|
],
|
|
34
65
|
};
|
|
35
66
|
});
|
|
36
67
|
/* ------------------------------------------------------------------ */
|
|
68
|
+
/* Maintenance */
|
|
69
|
+
/* ------------------------------------------------------------------ */
|
|
70
|
+
server.registerTool("rag_reindex", {
|
|
71
|
+
description: "Re-embed every stored vector for the currently configured EMBEDDING_PROVIDER/MODEL. " +
|
|
72
|
+
"Use after changing the embedding model or dimension to migrate the existing store in place " +
|
|
73
|
+
"instead of wiping and re-ingesting. Searches are blocked while it runs; re-run to resume " +
|
|
74
|
+
"after an interruption.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
batch_size: z
|
|
77
|
+
.number()
|
|
78
|
+
.int()
|
|
79
|
+
.min(1)
|
|
80
|
+
.max(200)
|
|
81
|
+
.optional()
|
|
82
|
+
.default(32)
|
|
83
|
+
.describe("Chunks embedded per transaction. Lower uses less memory, higher is faster."),
|
|
84
|
+
},
|
|
85
|
+
}, async (args) => {
|
|
86
|
+
const res = await reindex({
|
|
87
|
+
batchSize: args.batch_size ?? 32,
|
|
88
|
+
onProgress: (p) => {
|
|
89
|
+
if (p.total > 0)
|
|
90
|
+
process.stderr.write(`reindex ${p.phase}: ${p.done}/${p.total}\n`);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
94
|
+
});
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
37
96
|
/* RAG: ingest */
|
|
38
97
|
/* ------------------------------------------------------------------ */
|
|
39
98
|
server.registerTool("rag_ingest_text", {
|
|
40
99
|
description: "Store raw text as a knowledge document (chunked + embedded for later retrieval)",
|
|
41
100
|
inputSchema: {
|
|
42
|
-
content: z.string().min(1),
|
|
43
|
-
title: z.string().describe("Document title"),
|
|
44
|
-
contentType: z.string().optional().describe("e.g. markdown, typescript, text, json"),
|
|
45
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
101
|
+
content: z.string().min(1).max(MAX_INGEST_CHARS),
|
|
102
|
+
title: z.string().max(MAX_TITLE).describe("Document title"),
|
|
103
|
+
contentType: z.string().max(64).optional().describe("e.g. markdown, typescript, text, json"),
|
|
104
|
+
metadata: z.record(z.string().max(MAX_TAG), z.unknown()).optional(),
|
|
46
105
|
},
|
|
47
106
|
}, async (args) => {
|
|
48
107
|
const res = await ingestText(args.content, args.title, {
|
|
@@ -52,10 +111,10 @@ server.registerTool("rag_ingest_text", {
|
|
|
52
111
|
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
53
112
|
});
|
|
54
113
|
server.registerTool("rag_ingest_file", {
|
|
55
|
-
description: "Read a file and store it as a knowledge document",
|
|
114
|
+
description: "Read a file and store it as a knowledge document. Honours RAG_ALLOWED_DIRS when set.",
|
|
56
115
|
inputSchema: {
|
|
57
|
-
path: z.string().describe("Absolute path to the file"),
|
|
58
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
116
|
+
path: z.string().min(1).max(MAX_PATH).describe("Absolute path to the file"),
|
|
117
|
+
metadata: z.record(z.string().max(MAX_TAG), z.unknown()).optional(),
|
|
59
118
|
},
|
|
60
119
|
}, async (args) => {
|
|
61
120
|
const res = await ingestFile(args.path, { metadata: args.metadata });
|
|
@@ -64,9 +123,9 @@ server.registerTool("rag_ingest_file", {
|
|
|
64
123
|
server.registerTool("rag_ingest_dir", {
|
|
65
124
|
description: "Recursively ingest supported source files from a directory",
|
|
66
125
|
inputSchema: {
|
|
67
|
-
path: z.string().describe("Absolute path to the directory"),
|
|
126
|
+
path: z.string().min(1).max(MAX_PATH).describe("Absolute path to the directory"),
|
|
68
127
|
recursive: z.boolean().optional().default(true),
|
|
69
|
-
extensions: z.array(z.string()).optional().describe("File extensions to include, e.g. ['.ts', '.md']"),
|
|
128
|
+
extensions: z.array(z.string().min(1).max(16)).min(1).max(64).optional().describe("File extensions to include, e.g. ['.ts', '.md']"),
|
|
70
129
|
},
|
|
71
130
|
}, async (args) => {
|
|
72
131
|
const res = await ingestDirectory(args.path, { recursive: args.recursive, extensions: args.extensions });
|
|
@@ -76,19 +135,20 @@ server.registerTool("rag_ingest_dir", {
|
|
|
76
135
|
/* RAG: search / retrieve */
|
|
77
136
|
/* ------------------------------------------------------------------ */
|
|
78
137
|
server.registerTool("rag_search", {
|
|
79
|
-
description: "Hybrid semantic + keyword search over stored documents. Returns chunks ranked by relevance with scores.",
|
|
138
|
+
description: "Hybrid semantic + keyword search over stored documents. Returns chunks ranked by relevance with scores. Set explain to see per-leg scores (vector cosine, keyword BM25, RRF contribution) and diagnose ranking.",
|
|
80
139
|
inputSchema: {
|
|
81
|
-
query: z.string().min(1),
|
|
140
|
+
query: z.string().min(1).max(MAX_QUERY),
|
|
82
141
|
top_k: z.number().int().min(1).max(50).optional().default(10),
|
|
83
142
|
min_score: z.number().min(0).max(1).optional().default(0.08),
|
|
84
|
-
doc_id: z.string().optional().describe("Only search within this document"),
|
|
85
|
-
source: z.string().optional().describe("Only search documents with this source (e.g. 'opencode-db', 'session-log')"),
|
|
143
|
+
doc_id: z.string().max(MAX_ID).optional().describe("Only search within this document"),
|
|
144
|
+
source: z.string().max(MAX_ID).optional().describe("Only search documents with this source (e.g. 'opencode-db', 'session-log')"),
|
|
145
|
+
explain: z.boolean().optional().default(false).describe("Include per-leg vector/keyword/RRF scores on every hit"),
|
|
86
146
|
},
|
|
87
147
|
}, async (args) => {
|
|
88
148
|
const hits = await searchDocs(args.query, args.top_k ?? 10, args.min_score ?? 0.08, {
|
|
89
149
|
docId: args.doc_id,
|
|
90
150
|
source: args.source,
|
|
91
|
-
});
|
|
151
|
+
}, args.explain ?? false);
|
|
92
152
|
return {
|
|
93
153
|
content: [{ type: "text", text: JSON.stringify(hits, null, 2) }],
|
|
94
154
|
};
|
|
@@ -96,11 +156,11 @@ server.registerTool("rag_search", {
|
|
|
96
156
|
server.registerTool("rag_retrieve", {
|
|
97
157
|
description: "Retrieve relevant context chunks as a ready-to-inject context block for the LLM. Prefer this over rag_search when the result will be used as context.",
|
|
98
158
|
inputSchema: {
|
|
99
|
-
query: z.string().min(1),
|
|
159
|
+
query: z.string().min(1).max(MAX_QUERY),
|
|
100
160
|
top_k: z.number().int().min(1).max(20).optional().default(6),
|
|
101
161
|
min_score: z.number().min(0).max(1).optional().default(0.08),
|
|
102
|
-
doc_id: z.string().optional().describe("Only retrieve within this document"),
|
|
103
|
-
source: z.string().optional().describe("Only retrieve documents with this source"),
|
|
162
|
+
doc_id: z.string().max(MAX_ID).optional().describe("Only retrieve within this document"),
|
|
163
|
+
source: z.string().max(MAX_ID).optional().describe("Only retrieve documents with this source"),
|
|
104
164
|
},
|
|
105
165
|
}, async (args) => {
|
|
106
166
|
const res = await retrieve(args.query, args.top_k ?? 6, args.min_score ?? 0.08, {
|
|
@@ -124,10 +184,13 @@ server.registerTool("rag_retrieve", {
|
|
|
124
184
|
/* RAG: document management */
|
|
125
185
|
/* ------------------------------------------------------------------ */
|
|
126
186
|
server.registerTool("rag_list_documents", {
|
|
127
|
-
description: "List
|
|
128
|
-
inputSchema: {
|
|
129
|
-
|
|
130
|
-
|
|
187
|
+
description: "List stored knowledge documents (newest first, paginated)",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
limit: z.number().int().min(1).max(1000).optional().default(100).describe("Max documents to return"),
|
|
190
|
+
offset: z.number().int().min(0).optional().default(0).describe("Skip this many documents"),
|
|
191
|
+
},
|
|
192
|
+
}, async (args) => {
|
|
193
|
+
const docs = listDocuments({ limit: args.limit, offset: args.offset });
|
|
131
194
|
return { content: [{ type: "text", text: JSON.stringify(docs, null, 2) }] };
|
|
132
195
|
});
|
|
133
196
|
server.registerTool("rag_document_stats", {
|
|
@@ -139,7 +202,7 @@ server.registerTool("rag_document_stats", {
|
|
|
139
202
|
server.registerTool("rag_delete_document", {
|
|
140
203
|
description: "Delete a document and all its chunks by ID",
|
|
141
204
|
inputSchema: {
|
|
142
|
-
doc_id: z.string().min(1),
|
|
205
|
+
doc_id: z.string().min(1).max(MAX_ID),
|
|
143
206
|
},
|
|
144
207
|
}, async (args) => {
|
|
145
208
|
const res = deleteDocument(args.doc_id);
|
|
@@ -151,23 +214,23 @@ server.registerTool("rag_delete_document", {
|
|
|
151
214
|
server.registerTool("rag_sync_session", {
|
|
152
215
|
description: "Ingest an opencode session's user inputs (or the latest active session) into the RAG store as searchable documents. Use with the session id from opencode, or omit session_id and pass directory to auto-pick the most recent session in that workspace.",
|
|
153
216
|
inputSchema: {
|
|
154
|
-
session_id: z.string().optional().describe("opencode session id; omit to use the latest active session"),
|
|
155
|
-
directory: z.string().optional().describe("Workspace path used to pick the latest session when session_id is omitted"),
|
|
156
|
-
limit: z.number().int().min(1).optional().describe("Only ingest the first N user messages"),
|
|
217
|
+
session_id: z.string().min(1).max(MAX_ID).optional().describe("opencode session id; omit to use the latest active session"),
|
|
218
|
+
directory: z.string().min(1).max(MAX_PATH).optional().describe("Workspace path used to pick the latest session when session_id is omitted (defaults to the server's working directory)"),
|
|
219
|
+
limit: z.number().int().min(1).max(2000).optional().describe("Only ingest the first N user messages"),
|
|
157
220
|
},
|
|
158
221
|
}, async (args) => {
|
|
159
222
|
const res = args.session_id
|
|
160
223
|
? await syncSession(args.session_id, { limit: args.limit })
|
|
161
|
-
: await syncLatest({ limit: args.limit, directory: args.directory });
|
|
224
|
+
: await syncLatest({ limit: args.limit, directory: args.directory ?? process.cwd() });
|
|
162
225
|
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
163
226
|
});
|
|
164
227
|
server.registerTool("memory_remember", {
|
|
165
228
|
description: "Store a piece of long-term memory (fact, decision, preference, instruction, task, insight, conversation). Use this to persist things that should be remembered across sessions.",
|
|
166
229
|
inputSchema: {
|
|
167
|
-
content: z.string().min(1).describe("The memory content, written as a self-contained statement"),
|
|
230
|
+
content: z.string().min(1).max(MAX_MEMORY_CONTENT).describe("The memory content, written as a self-contained statement"),
|
|
168
231
|
type: z.enum(MEMORY_TYPES).optional().default("fact"),
|
|
169
232
|
importance: z.number().min(0).max(1).optional().default(0.5).describe("0..1, higher = keep longer"),
|
|
170
|
-
tags: z.array(z.string()).optional().describe("Tags for filtering"),
|
|
233
|
+
tags: z.array(z.string().max(MAX_TAG)).max(MAX_TAGS).optional().describe("Tags for filtering"),
|
|
171
234
|
},
|
|
172
235
|
}, async (args) => {
|
|
173
236
|
const rec = await remember({
|
|
@@ -181,7 +244,7 @@ server.registerTool("memory_remember", {
|
|
|
181
244
|
server.registerTool("memory_recall", {
|
|
182
245
|
description: "Semantically search long-term memories relevant to a query. This is how you remember past context.",
|
|
183
246
|
inputSchema: {
|
|
184
|
-
query: z.string().min(1),
|
|
247
|
+
query: z.string().min(1).max(MAX_QUERY),
|
|
185
248
|
top_k: z.number().int().min(1).max(30).optional().default(8),
|
|
186
249
|
min_score: z.number().min(0).max(1).optional().default(0.1),
|
|
187
250
|
},
|
|
@@ -193,7 +256,7 @@ server.registerTool("memory_list", {
|
|
|
193
256
|
description: "List memories, optionally filtered by type, tag, or minimum importance",
|
|
194
257
|
inputSchema: {
|
|
195
258
|
type: z.enum(MEMORY_TYPES).optional(),
|
|
196
|
-
tag: z.string().optional(),
|
|
259
|
+
tag: z.string().max(MAX_TAG).optional(),
|
|
197
260
|
min_importance: z.number().min(0).max(1).optional(),
|
|
198
261
|
limit: z.number().int().min(1).max(500).optional().default(100),
|
|
199
262
|
},
|
|
@@ -209,7 +272,7 @@ server.registerTool("memory_list", {
|
|
|
209
272
|
server.registerTool("memory_get", {
|
|
210
273
|
description: "Get a single memory by ID",
|
|
211
274
|
inputSchema: {
|
|
212
|
-
id: z.string().min(1),
|
|
275
|
+
id: z.string().min(1).max(MAX_ID),
|
|
213
276
|
},
|
|
214
277
|
}, async (args) => {
|
|
215
278
|
const rec = getMemory(args.id);
|
|
@@ -220,13 +283,18 @@ server.registerTool("memory_get", {
|
|
|
220
283
|
server.registerTool("memory_update", {
|
|
221
284
|
description: "Update content / type / importance / tags of an existing memory",
|
|
222
285
|
inputSchema: {
|
|
223
|
-
id: z.string().min(1),
|
|
224
|
-
content: z.string().optional(),
|
|
286
|
+
id: z.string().min(1).max(MAX_ID),
|
|
287
|
+
content: z.string().max(MAX_MEMORY_CONTENT).optional(),
|
|
225
288
|
type: z.enum(MEMORY_TYPES).optional(),
|
|
226
289
|
importance: z.number().min(0).max(1).optional(),
|
|
227
|
-
tags: z.array(z.string()).optional(),
|
|
290
|
+
tags: z.array(z.string().max(MAX_TAG)).max(MAX_TAGS).optional(),
|
|
228
291
|
},
|
|
229
292
|
}, async (args) => {
|
|
293
|
+
// An all-undefined patch used to re-embed the text and bump updated_at,
|
|
294
|
+
// silently reordering the store.
|
|
295
|
+
if (args.content === undefined && args.type === undefined && args.importance === undefined && args.tags === undefined) {
|
|
296
|
+
return { content: [{ type: "text", text: "Nothing to update: provide content, type, importance and/or tags." }] };
|
|
297
|
+
}
|
|
230
298
|
const rec = await updateMemory(args.id, {
|
|
231
299
|
content: args.content,
|
|
232
300
|
type: args.type,
|
|
@@ -240,7 +308,7 @@ server.registerTool("memory_update", {
|
|
|
240
308
|
server.registerTool("memory_forget", {
|
|
241
309
|
description: "Delete a memory by ID",
|
|
242
310
|
inputSchema: {
|
|
243
|
-
id: z.string().min(1),
|
|
311
|
+
id: z.string().min(1).max(MAX_ID),
|
|
244
312
|
},
|
|
245
313
|
}, async (args) => {
|
|
246
314
|
const res = forget(args.id);
|
|
@@ -262,7 +330,7 @@ server.registerTool("memory_stats", {
|
|
|
262
330
|
server.registerTool("memory_context", {
|
|
263
331
|
description: "Build a compact context block from the most relevant memories for a given topic. Best for injecting memory into prompts.",
|
|
264
332
|
inputSchema: {
|
|
265
|
-
topic: z.string().min(1),
|
|
333
|
+
topic: z.string().min(1).max(MAX_TOPIC),
|
|
266
334
|
top_k: z.number().int().min(1).max(20).optional().default(6),
|
|
267
335
|
},
|
|
268
336
|
}, async (args) => {
|
|
@@ -282,4 +350,37 @@ server.registerTool("memory_context", {
|
|
|
282
350
|
const transport = new StdioServerTransport();
|
|
283
351
|
await server.connect(transport);
|
|
284
352
|
console.error(`RAG + Context Management MCP server running (db: ${path.resolve(STORAGE_DIR)})`);
|
|
353
|
+
/**
|
|
354
|
+
* Close the SQLite handle before the process exits.
|
|
355
|
+
*
|
|
356
|
+
* SQLite in DELETE journal mode rolls back on an abrupt exit, so a hard kill is
|
|
357
|
+
* safe, but it leaves the `-journal` file behind and skips any WAL checkpoint.
|
|
358
|
+
* On a stdio MCP server the host closing the pipe is the normal way this
|
|
359
|
+
* process dies, so an explicit shutdown keeps the store tidy and lets a WAL
|
|
360
|
+
* database truncate instead of growing.
|
|
361
|
+
*
|
|
362
|
+
* Exit is forced after a grace period because the host may not respond to
|
|
363
|
+
* `server.close()`, and a server that refuses to die is worse than one that
|
|
364
|
+
* drops an unflushed log line.
|
|
365
|
+
*/
|
|
366
|
+
let shuttingDown = false;
|
|
367
|
+
function shutdown(signal) {
|
|
368
|
+
if (shuttingDown)
|
|
369
|
+
return;
|
|
370
|
+
shuttingDown = true;
|
|
371
|
+
console.error(`RAG memory: ${signal} received, closing database.`);
|
|
372
|
+
try {
|
|
373
|
+
closeDB();
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
console.error(`RAG memory: database close failed: ${String(err)}`);
|
|
377
|
+
}
|
|
378
|
+
void server.close().finally(() => {
|
|
379
|
+
process.exit(0);
|
|
380
|
+
});
|
|
381
|
+
setTimeout(() => process.exit(0), 2_000).unref();
|
|
382
|
+
}
|
|
383
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
384
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
385
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
285
386
|
//# sourceMappingURL=rag-server.js.map
|