@lotargo/memory_plugin 1.4.621 → 1.5.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/README.md +352 -366
- package/mcp-server/admin/auth.js +31 -4
- package/mcp-server/cli/direct_commands.js +313 -0
- package/mcp-server/cli/handlers/cloud_actions.js +138 -0
- package/mcp-server/cli/handlers/diagnostics_actions.js +107 -0
- package/mcp-server/cli/handlers/engine_actions.js +214 -0
- package/mcp-server/cli/handlers/prompt_actions.js +24 -0
- package/mcp-server/cli/handlers/storage_actions.js +749 -0
- package/mcp-server/cli/quick_stats.js +39 -0
- package/mcp-server/cli/ui.js +565 -0
- package/mcp-server/cli.js +324 -2085
- package/mcp-server/config/auth_store.js +56 -9
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/database.js +14 -1
- package/mcp-server/db/migrations.js +28 -0
- package/mcp-server/fact_format.js +244 -177
- package/mcp-server/identity.js +152 -0
- package/mcp-server/index.js +42 -679
- package/mcp-server/memory.js +50 -63
- package/mcp-server/prompt_manager.js +1 -1
- package/mcp-server/tools/helpers.js +39 -0
- package/mcp-server/tools/identity_tools.js +277 -0
- package/mcp-server/tools/index.js +9 -0
- package/mcp-server/tools/memory_tools.js +506 -0
- package/mcp-server/tools/rag_tools.js +235 -0
- package/opencode-plugin/index.js +460 -48
- package/package.json +7 -3
- package/skills/using-memory/SKILL.md +31 -14
- package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
- package/mcp-server/benchmarks/quality_evaluator.js +0 -600
- package/mcp-server/benchmarks/run_benchmarks.js +0 -347
- package/mcp-server/benchmarks/stress_ingestion.js +0 -195
- package/mcp-server/benchmarks/test_dual_layer.js +0 -140
package/mcp-server/index.js
CHANGED
|
@@ -1,679 +1,42 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const { runCli } = await import("./cli.js");
|
|
44
|
-
await runCli();
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
await ensureDir();
|
|
49
|
-
|
|
50
|
-
const server = new McpServer({
|
|
51
|
-
name: "memory-agent",
|
|
52
|
-
version: "1.0.0",
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Optional string/number that tolerates null (some tool-call layers fill omitted
|
|
56
|
-
// optional args with null). Linking fields must NEVER be mandatory.
|
|
57
|
-
const optStr = () => z.string().optional().nullable();
|
|
58
|
-
const optNum = () => z.number().optional().nullable();
|
|
59
|
-
const defStr = (fallback) =>
|
|
60
|
-
z
|
|
61
|
-
.string()
|
|
62
|
-
.nullish()
|
|
63
|
-
.transform((v) => (v === null || v === undefined || v === "" ? fallback : v));
|
|
64
|
-
const defBool = (fallback) => z.boolean().nullish().transform((v) => (v === null || v === undefined ? fallback : v));
|
|
65
|
-
const defNum = (fallback) => z.number().nullish().transform((v) => (v === null || v === undefined ? fallback : v));
|
|
66
|
-
|
|
67
|
-
// --- Legacy Key-Value Memory Tools ---
|
|
68
|
-
|
|
69
|
-
// --- Legacy Key-Value Memory Tools & Agent Graph Linking ---
|
|
70
|
-
|
|
71
|
-
server.registerTool(
|
|
72
|
-
"remember",
|
|
73
|
-
{
|
|
74
|
-
description:
|
|
75
|
-
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
76
|
-
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
77
|
-
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
78
|
-
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
79
|
-
"ttl is OPTIONAL (e.g. '90d', '2w', '24h') — expired facts are shown with [EXPIRED] but not auto-deleted. " +
|
|
80
|
-
"keep=true protects the fact from forget deletion unless force=true. " +
|
|
81
|
-
"tags is OPTIONAL comma-separated text for filtering. " +
|
|
82
|
-
"supersedes is OPTIONAL: a number (from recall), id, or text of a fact this one replaces; " +
|
|
83
|
-
"the target is then marked [SUPERSEDED]. " +
|
|
84
|
-
"Translate the fact into English and keep it concise. " +
|
|
85
|
-
"scope: 'project' (default) or 'global'",
|
|
86
|
-
inputSchema: z.object({
|
|
87
|
-
fact: z.string().describe("The fact to remember, written in English"),
|
|
88
|
-
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
89
|
-
docId: optStr().describe("Optional document ID, title, or path to link this fact to"),
|
|
90
|
-
startLine: optNum().describe("Optional starting line number in target document"),
|
|
91
|
-
endLine: optNum().describe("Optional ending line number in target document"),
|
|
92
|
-
relationType: defStr("LINKS_TO").describe("Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'REFERENCES')"),
|
|
93
|
-
ttl: optStr().describe("Optional time-to-live, e.g. '90d', '2w', '24h', '12m'"),
|
|
94
|
-
keep: defBool(false).describe("Protect the fact from forget deletion unless force=true"),
|
|
95
|
-
tags: optStr().describe("Optional comma-separated tags, e.g. 'pref,arch'"),
|
|
96
|
-
supersedes: optStr().describe("Optional number, id, or text of the fact this one replaces"),
|
|
97
|
-
}),
|
|
98
|
-
},
|
|
99
|
-
async ({ fact, scope, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes }) => {
|
|
100
|
-
const key = scopeKey(scope, null, null);
|
|
101
|
-
const entries = await readMemory(key);
|
|
102
|
-
const factNormalized = fact.toLowerCase().trim();
|
|
103
|
-
let duplicate = false;
|
|
104
|
-
if (entries.some((e) => factText(e).toLowerCase().trim() === factNormalized)) {
|
|
105
|
-
duplicate = true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let supersededInfo = "";
|
|
109
|
-
if (!duplicate) {
|
|
110
|
-
const [date, time] = today().split(" ");
|
|
111
|
-
const meta = { ttl, tags };
|
|
112
|
-
if (keep) meta.keep = "1";
|
|
113
|
-
if (supersedes) {
|
|
114
|
-
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
115
|
-
if (targetIdx !== -1) {
|
|
116
|
-
const newId = nextFactId(entries);
|
|
117
|
-
const targetMeta = factMeta(entries[targetIdx]);
|
|
118
|
-
const targetId = targetMeta.id || nextFactId(entries);
|
|
119
|
-
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
|
|
120
|
-
meta.id = newId;
|
|
121
|
-
meta.supersedes = targetId;
|
|
122
|
-
supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
|
|
123
|
-
} else {
|
|
124
|
-
supersededInfo = " (note: supersedes target not found)";
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (!meta.id) meta.id = nextFactId(entries);
|
|
128
|
-
entries.push(formatFactEntry({ date, time, text: fact, meta }));
|
|
129
|
-
await writeMemory(key, entries);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let linkInfo = "";
|
|
133
|
-
if (docId) {
|
|
134
|
-
const { linkFactToDocument } = await import("./graph/knowledge_linker.js");
|
|
135
|
-
try {
|
|
136
|
-
const linkRes = linkFactToDocument({
|
|
137
|
-
factKey: key,
|
|
138
|
-
factText: fact,
|
|
139
|
-
docId,
|
|
140
|
-
startLine,
|
|
141
|
-
endLine,
|
|
142
|
-
relationType,
|
|
143
|
-
});
|
|
144
|
-
const linesStr = startLine ? `:L${startLine}${endLine ? `-${endLine}` : ""}` : "";
|
|
145
|
-
linkInfo = ` [Linked to Doc: "${linkRes.docTitle}"${linesStr}]`;
|
|
146
|
-
} catch (err) {
|
|
147
|
-
linkInfo = ` (Note: Fact saved, but document link failed: ${err.message})`;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { content: [{ type: "text", text: `Memory updated${supersededInfo}${linkInfo}` }] };
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
server.registerTool(
|
|
156
|
-
"recall",
|
|
157
|
-
{
|
|
158
|
-
description:
|
|
159
|
-
"Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
|
|
160
|
-
"scope: 'project', 'global', 'all' (default), or 'list_projects'. " +
|
|
161
|
-
"Use project: '<directory path>' with scope 'project'/'all' to read facts of a specific project from any working directory. " +
|
|
162
|
-
"query filters by keyword (all space-separated terms must match). " +
|
|
163
|
-
"tags filters by comma-separated tags. since/until filter by date (YYYY-MM-DD, inclusive). " +
|
|
164
|
-
"Expired facts are shown with [EXPIRED], protected ones with [KEEP]. The response includes the store file paths.",
|
|
165
|
-
inputSchema: z.object({
|
|
166
|
-
scope: defStr("all").describe("'project', 'global', 'all', or 'list_projects'"),
|
|
167
|
-
project: optStr().describe("Directory path of the project to read facts from (e.g. 'F:/projects/plugins/memory')"),
|
|
168
|
-
query: optStr().describe("Optional keyword filter; all space-separated terms must match"),
|
|
169
|
-
tags: optStr().describe("Optional comma-separated tag filter (any match)"),
|
|
170
|
-
since: optStr().describe("Optional start date filter, YYYY-MM-DD (inclusive)"),
|
|
171
|
-
until: optStr().describe("Optional end date filter, YYYY-MM-DD (inclusive)"),
|
|
172
|
-
}),
|
|
173
|
-
},
|
|
174
|
-
async ({ scope, project, query, tags, since, until }) => {
|
|
175
|
-
const { getLinksForFact } = await import("./graph/knowledge_linker.js");
|
|
176
|
-
const results = [];
|
|
177
|
-
|
|
178
|
-
const formatFactWithLinks = async (factLine, key) => {
|
|
179
|
-
let line = displayFact(factLine);
|
|
180
|
-
try {
|
|
181
|
-
const links = await getLinksForFact(key, factText(factLine));
|
|
182
|
-
if (links && links.length > 0) {
|
|
183
|
-
const docStr = links
|
|
184
|
-
.map((l) => {
|
|
185
|
-
const range = l.start_line ? `:L${l.start_line}${l.end_line ? `-${l.end_line}` : ""}` : "";
|
|
186
|
-
return `${l.doc_title || l.doc_path}${range}`;
|
|
187
|
-
})
|
|
188
|
-
.join(", ");
|
|
189
|
-
line += ` 🔗 [Linked Docs: ${docStr}]`;
|
|
190
|
-
}
|
|
191
|
-
} catch (e) {}
|
|
192
|
-
return line;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const collect = async (entries, key) => {
|
|
196
|
-
const matched = entries.filter(
|
|
197
|
-
(e) => matchesQuery(e, query) && matchesTags(e, tags) && inDateRange(e, since, until)
|
|
198
|
-
);
|
|
199
|
-
if (!matched.length) return;
|
|
200
|
-
if (results.length) results.push("");
|
|
201
|
-
results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
|
|
202
|
-
for (let i = 0; i < matched.length; i++) {
|
|
203
|
-
results.push(`${i + 1}. ${await formatFactWithLinks(matched[i], key)}`);
|
|
204
|
-
}
|
|
205
|
-
results.push(`Store file: ${storeFilePath(key)}`);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
if (scope === "list_projects") {
|
|
209
|
-
const stores = await listProjectStores();
|
|
210
|
-
if (!stores.length) {
|
|
211
|
-
return { content: [{ type: "text", text: "No project memory stores found." }] };
|
|
212
|
-
}
|
|
213
|
-
const lines = stores.map(
|
|
214
|
-
(s, i) => `${i + 1}. ${s.basename} — ${s.count} fact(s) [${s.file}]${s.path ? ` (bound to ${s.path})` : " (unbound legacy store)"}`
|
|
215
|
-
);
|
|
216
|
-
return {
|
|
217
|
-
content: [
|
|
218
|
-
{
|
|
219
|
-
type: "text",
|
|
220
|
-
text: `Project Memory Stores:\n${lines.join("\n")}\n\nUse recall(scope: "project", project: "<path>") to read a specific store.\n\nMemory dir: ${MEMORY_DIR}`,
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const target = project ? canonicalPath(project) : projectKey(null, null);
|
|
227
|
-
const label = project ? target : projectName();
|
|
228
|
-
if (scope !== "project") {
|
|
229
|
-
const global = await readMemory(GLOBAL_KEY);
|
|
230
|
-
await collect(global, GLOBAL_KEY);
|
|
231
|
-
}
|
|
232
|
-
if (scope !== "global") {
|
|
233
|
-
const local = await readMemory(target);
|
|
234
|
-
await collect(local, target);
|
|
235
|
-
}
|
|
236
|
-
const filtered = Boolean(query || tags || since || until);
|
|
237
|
-
const text = results.length
|
|
238
|
-
? `${results.join("\n")}\n\nMemory dir: ${MEMORY_DIR}`
|
|
239
|
-
: filtered
|
|
240
|
-
? "No facts match the search."
|
|
241
|
-
: "Memory is empty.";
|
|
242
|
-
return { content: [{ type: "text", text }] };
|
|
243
|
-
}
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
server.registerTool(
|
|
247
|
-
"forget",
|
|
248
|
-
{
|
|
249
|
-
description:
|
|
250
|
-
"Delete a fact by number (from recall), by range (e.g. '3-30', inclusive), or by text search. " +
|
|
251
|
-
"Protected facts (remember with keep=true) are skipped unless force=true.",
|
|
252
|
-
inputSchema: z.object({
|
|
253
|
-
query: z.string().describe("Number, range like '3-30', or text to search for"),
|
|
254
|
-
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
255
|
-
force: defBool(false).describe("Also delete protected (keep) facts"),
|
|
256
|
-
}),
|
|
257
|
-
},
|
|
258
|
-
async ({ query, scope, force }) => {
|
|
259
|
-
const key = scopeKey(scope, null, null);
|
|
260
|
-
const entries = await readMemory(key);
|
|
261
|
-
const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
|
|
262
|
-
const num = parseInt(query, 10);
|
|
263
|
-
let indices = [];
|
|
264
|
-
if (rangeMatch) {
|
|
265
|
-
const from = parseInt(rangeMatch[1], 10);
|
|
266
|
-
const to = parseInt(rangeMatch[2], 10);
|
|
267
|
-
if (from > 0 && to >= from && to <= entries.length) {
|
|
268
|
-
for (let i = from - 1; i < to; i++) indices.push(i);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (!indices.length && !isNaN(num) && num > 0 && num <= entries.length) {
|
|
272
|
-
indices.push(num - 1);
|
|
273
|
-
}
|
|
274
|
-
if (!indices.length) {
|
|
275
|
-
const q = query.toLowerCase();
|
|
276
|
-
indices = entries.reduce((acc, e, i) => (e.toLowerCase().includes(q) ? acc.concat(i) : acc), []);
|
|
277
|
-
}
|
|
278
|
-
if (!indices.length) {
|
|
279
|
-
return { content: [{ type: "text", text: "Not found." }] };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
|
|
283
|
-
const protectedCount = indices.length - removable.length;
|
|
284
|
-
if (removable.length) {
|
|
285
|
-
for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
|
|
286
|
-
await writeMemory(key, entries);
|
|
287
|
-
}
|
|
288
|
-
let text = removable.length ? "Memory updated" : "Nothing removed.";
|
|
289
|
-
if (protectedCount) text += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
|
|
290
|
-
return { content: [{ type: "text", text }] };
|
|
291
|
-
}
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
server.registerTool(
|
|
295
|
-
"update_fact",
|
|
296
|
-
{
|
|
297
|
-
description:
|
|
298
|
-
"Update the text of an existing fact by number (from recall), id, or text match, " +
|
|
299
|
-
"preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
|
|
300
|
-
inputSchema: z.object({
|
|
301
|
-
id: z.string().describe("Number (from recall), metadata id, or text of the fact to update"),
|
|
302
|
-
newText: z.string().describe("New fact text"),
|
|
303
|
-
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
304
|
-
}),
|
|
305
|
-
},
|
|
306
|
-
async ({ id, newText, scope }) => {
|
|
307
|
-
const key = scopeKey(scope, null, null);
|
|
308
|
-
const entries = await readMemory(key);
|
|
309
|
-
const idx = resolveFactIndex(entries, id);
|
|
310
|
-
if (idx === -1) throw new Error(`Fact not found: ${id}`);
|
|
311
|
-
const p = parseFactEntry(entries[idx]);
|
|
312
|
-
const oldText = p ? p.text : entries[idx];
|
|
313
|
-
const newLine = formatFactEntry({ date: p.date, time: p.time, text: newText, meta: p.meta });
|
|
314
|
-
entries[idx] = newLine;
|
|
315
|
-
await writeMemory(key, entries);
|
|
316
|
-
|
|
317
|
-
let linksUpdated = 0;
|
|
318
|
-
try {
|
|
319
|
-
const { getDatabase } = await import("./db/database.js");
|
|
320
|
-
const db = await getDatabase();
|
|
321
|
-
const res = await db
|
|
322
|
-
.prepare(
|
|
323
|
-
"UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?"
|
|
324
|
-
)
|
|
325
|
-
.run(newText, key, oldText);
|
|
326
|
-
linksUpdated = res.changes;
|
|
327
|
-
} catch (e) {}
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
content: [
|
|
331
|
-
{ type: "text", text: `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}` },
|
|
332
|
-
],
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
server.registerTool(
|
|
338
|
-
"memory_info",
|
|
339
|
-
{
|
|
340
|
-
description:
|
|
341
|
-
"Show memory storage paths (store file locations, MEMORY_DIR, SQLite DB), fact counts, " +
|
|
342
|
-
"Knowledge Base stats, and the installed package version.",
|
|
343
|
-
inputSchema: z.object({}),
|
|
344
|
-
},
|
|
345
|
-
async () => {
|
|
346
|
-
const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
|
|
347
|
-
const globalFile = storeFilePath(GLOBAL_KEY);
|
|
348
|
-
const projectFile = storeFilePath(projectKey(null, null));
|
|
349
|
-
|
|
350
|
-
let version = "unknown";
|
|
351
|
-
try {
|
|
352
|
-
version = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
353
|
-
} catch (e) {}
|
|
354
|
-
|
|
355
|
-
let rag = {};
|
|
356
|
-
try {
|
|
357
|
-
const { getDatabase } = await import("./db/database.js");
|
|
358
|
-
const db = await getDatabase();
|
|
359
|
-
const docCountRow = await db.prepare("SELECT COUNT(*) AS c FROM documents").get();
|
|
360
|
-
rag.documents = docCountRow ? docCountRow.c : 0;
|
|
361
|
-
const secCountRow = await db.prepare("SELECT COUNT(*) AS c FROM sections").get();
|
|
362
|
-
rag.sections = secCountRow ? secCountRow.c : 0;
|
|
363
|
-
const chunkCountRow = await db.prepare("SELECT COUNT(*) AS c FROM micro_chunks").get();
|
|
364
|
-
rag.chunks = chunkCountRow ? chunkCountRow.c : 0;
|
|
365
|
-
const edgeCountRow = await db.prepare("SELECT COUNT(*) AS c FROM graph_edges").get();
|
|
366
|
-
rag.edges = edgeCountRow ? edgeCountRow.c : 0;
|
|
367
|
-
const linkCountRow = await db.prepare("SELECT COUNT(*) AS c FROM knowledge_links").get();
|
|
368
|
-
rag.links = linkCountRow ? linkCountRow.c : 0;
|
|
369
|
-
} catch (e) {
|
|
370
|
-
rag.error = e.message;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const stores = await listProjectStores();
|
|
374
|
-
const lines = [
|
|
375
|
-
`Version: ${version}`,
|
|
376
|
-
`MEMORY_DIR: ${MEMORY_DIR}`,
|
|
377
|
-
`SQLite DB: ${dbPath}`,
|
|
378
|
-
`Global store: ${globalFile}`,
|
|
379
|
-
`Project store: ${projectFile}`,
|
|
380
|
-
`Project stores: ${stores.length}`,
|
|
381
|
-
`Facts (global): ${(await readMemoryRaw(GLOBAL_KEY)).length}`,
|
|
382
|
-
`Facts (project): ${(await readMemoryRaw(projectKey(null, null))).length}`,
|
|
383
|
-
];
|
|
384
|
-
if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
|
|
385
|
-
else lines.push(
|
|
386
|
-
`RAG: ${rag.documents} doc(s), ${rag.sections} section(s), ${rag.chunks} chunk(s), ${rag.edges} edge(s), ${rag.links} memory link(s)`
|
|
387
|
-
);
|
|
388
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
389
|
-
}
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
server.registerTool(
|
|
393
|
-
"link_knowledge",
|
|
394
|
-
{
|
|
395
|
-
description:
|
|
396
|
-
"Explicitly link a Notebook memory fact to a Knowledge Base document, section, or line range. " +
|
|
397
|
-
"Creates Agent-driven Graph Edges connecting memory to RAG documents.",
|
|
398
|
-
inputSchema: z.object({
|
|
399
|
-
action: z.enum(["link", "list_links", "get_doc_links"]).nullish().transform((v) => v || "link").describe("Action type"),
|
|
400
|
-
factText: optStr().describe("Memory fact text or keyword"),
|
|
401
|
-
docId: optStr().describe("Document ID, title, or file path"),
|
|
402
|
-
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
403
|
-
startLine: optNum().describe("Starting line number in target document"),
|
|
404
|
-
endLine: optNum().describe("Ending line number in target document"),
|
|
405
|
-
relationType: defStr("LINKS_TO").describe("Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'EXPLAINS')"),
|
|
406
|
-
}),
|
|
407
|
-
},
|
|
408
|
-
async ({ action, factText, docId, scope, startLine, endLine, relationType }) => {
|
|
409
|
-
const { linkFactToDocument, getLinksForDoc, listAllLinks } = await import("./graph/knowledge_linker.js");
|
|
410
|
-
const key = scopeKey(scope, null, null);
|
|
411
|
-
|
|
412
|
-
if (action === "link") {
|
|
413
|
-
if (!factText || !docId) {
|
|
414
|
-
throw new Error("factText and docId are required parameters for link action");
|
|
415
|
-
}
|
|
416
|
-
const res = linkFactToDocument({
|
|
417
|
-
factKey: key,
|
|
418
|
-
factText,
|
|
419
|
-
docId,
|
|
420
|
-
startLine,
|
|
421
|
-
endLine,
|
|
422
|
-
relationType,
|
|
423
|
-
});
|
|
424
|
-
return {
|
|
425
|
-
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (action === "get_doc_links") {
|
|
430
|
-
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
431
|
-
const links = getLinksForDoc(docId);
|
|
432
|
-
return {
|
|
433
|
-
content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (action === "list_links") {
|
|
438
|
-
const links = listAllLinks(key);
|
|
439
|
-
return {
|
|
440
|
-
content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
throw new Error(`Unknown action: ${action}`);
|
|
445
|
-
}
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
// --- Hybrid RAG Knowledge Engine Tools ---
|
|
449
|
-
|
|
450
|
-
server.registerTool(
|
|
451
|
-
"ingest_document",
|
|
452
|
-
{
|
|
453
|
-
description:
|
|
454
|
-
"Ingest a document into the RAG knowledge base. " +
|
|
455
|
-
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
456
|
-
"For type='file' the file is read from disk and indexed with a code-block wrapper. " +
|
|
457
|
-
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
458
|
-
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
459
|
-
"computes dense vectors, and extracts GraphRAG code symbols.",
|
|
460
|
-
inputSchema: z.object({
|
|
461
|
-
content: z.string().describe("Raw text content, file path, or web URL. For type='file' this can be the file path (reads from disk) or the file content directly"),
|
|
462
|
-
type: z.enum(["text", "file", "url"]).nullish().transform((v) => v || "text").describe("Input content type: 'text' (raw content), 'file' (reads from disk, wraps in code block), or 'url' (fetches page content)"),
|
|
463
|
-
title: optStr().describe("Document title"),
|
|
464
|
-
path: optStr().describe("Original document file path"),
|
|
465
|
-
generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
|
|
466
|
-
}),
|
|
467
|
-
},
|
|
468
|
-
async ({ content, type, title, path, generateEmbeddings }) => {
|
|
469
|
-
const { ingestDocument } = await import("./ingest/pipeline.js");
|
|
470
|
-
const result = await ingestDocument({
|
|
471
|
-
content,
|
|
472
|
-
type,
|
|
473
|
-
title: title || null,
|
|
474
|
-
path: path || null,
|
|
475
|
-
generateEmbeddings,
|
|
476
|
-
});
|
|
477
|
-
return {
|
|
478
|
-
content: [
|
|
479
|
-
{
|
|
480
|
-
type: "text",
|
|
481
|
-
text: JSON.stringify(
|
|
482
|
-
{
|
|
483
|
-
status: "success",
|
|
484
|
-
docId: result.docId,
|
|
485
|
-
title: result.title,
|
|
486
|
-
sectionsCount: result.sectionsCount,
|
|
487
|
-
microChunksCount: result.microChunksCount,
|
|
488
|
-
deduplicated: result.deduplicated,
|
|
489
|
-
},
|
|
490
|
-
null,
|
|
491
|
-
2
|
|
492
|
-
),
|
|
493
|
-
},
|
|
494
|
-
],
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
server.registerTool(
|
|
500
|
-
"query_knowledge_base",
|
|
501
|
-
{
|
|
502
|
-
description:
|
|
503
|
-
"Perform hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
504
|
-
"Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
|
|
505
|
-
inputSchema: z.object({
|
|
506
|
-
query: z.string().describe("Search query in natural language or symbol name"),
|
|
507
|
-
limit: defNum(5).describe("Maximum number of sections to return"),
|
|
508
|
-
instruction: optStr().describe(
|
|
509
|
-
"Optional task-specific retrieval instruction shaping embedding focus (e.g. 'Retrieve code snippets', 'Find user preferences'). " +
|
|
510
|
-
"Recommended when using E5/BGE models for domain-specific queries."
|
|
511
|
-
),
|
|
512
|
-
generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
|
|
513
|
-
}),
|
|
514
|
-
},
|
|
515
|
-
async ({ query, limit, instruction, generateEmbeddings }) => {
|
|
516
|
-
const { hybridQuery } = await import("./retrieval/retriever.js");
|
|
517
|
-
const { getConfig } = await import("./config/config_manager.js");
|
|
518
|
-
const activeConfig = getConfig();
|
|
519
|
-
|
|
520
|
-
const results = await hybridQuery({
|
|
521
|
-
query,
|
|
522
|
-
limit,
|
|
523
|
-
generateEmbeddings,
|
|
524
|
-
instruction: instruction || null,
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
if (!results || results.length === 0) {
|
|
528
|
-
return {
|
|
529
|
-
content: [
|
|
530
|
-
{
|
|
531
|
-
type: "text",
|
|
532
|
-
text: `[Active Model: ${activeConfig.embeddingModel}]\nNo matching knowledge found for query.`,
|
|
533
|
-
},
|
|
534
|
-
],
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()}]\n\n`;
|
|
539
|
-
|
|
540
|
-
const formatted = results
|
|
541
|
-
.map((r, i) => {
|
|
542
|
-
let header = `### [${i + 1}] ${r.doc_title || "Untitled"}`;
|
|
543
|
-
if (r.heading) header += ` > ${r.heading}`;
|
|
544
|
-
if (r.breadcrumbs) header += ` (${r.breadcrumbs})`;
|
|
545
|
-
let body = `Score: ${(r.score || 0).toFixed(4)}\n`;
|
|
546
|
-
if (r.defined_symbols && r.defined_symbols.length > 0) {
|
|
547
|
-
body += `Defined Symbols: ${r.defined_symbols.join(", ")}\n`;
|
|
548
|
-
}
|
|
549
|
-
body += `\n${r.snippet || r.full_section_content || ""}`;
|
|
550
|
-
return `${header}\n${body}`;
|
|
551
|
-
})
|
|
552
|
-
.join("\n\n---\n\n");
|
|
553
|
-
|
|
554
|
-
return { content: [{ type: "text", text: headerNote + formatted }] };
|
|
555
|
-
}
|
|
556
|
-
);
|
|
557
|
-
|
|
558
|
-
server.registerTool(
|
|
559
|
-
"manage_knowledge_base",
|
|
560
|
-
{
|
|
561
|
-
description:
|
|
562
|
-
"Manage the RAG knowledge base: inspect stats, list documents, read full raw document, delete documents, or export/import snapshots.",
|
|
563
|
-
inputSchema: z.object({
|
|
564
|
-
action: z.enum(["stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot"]).describe("Management action"),
|
|
565
|
-
docId: optStr().describe("Document ID, title, or path (required for read_document and delete)"),
|
|
566
|
-
snapshotPath: optStr().describe("File path for snapshot export/import"),
|
|
567
|
-
}),
|
|
568
|
-
},
|
|
569
|
-
async ({ action, docId, snapshotPath }) => {
|
|
570
|
-
const { getDatabase } = await import("./db/database.js");
|
|
571
|
-
const db = await getDatabase();
|
|
572
|
-
|
|
573
|
-
if (action === "stats") {
|
|
574
|
-
const docCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
|
|
575
|
-
const docCount = docCountRow ? docCountRow.cnt : 0;
|
|
576
|
-
const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections").get();
|
|
577
|
-
const secCount = secCountRow ? secCountRow.cnt : 0;
|
|
578
|
-
const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
|
|
579
|
-
const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
|
|
580
|
-
const edgeCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get();
|
|
581
|
-
const edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
|
|
582
|
-
return {
|
|
583
|
-
content: [
|
|
584
|
-
{
|
|
585
|
-
type: "text",
|
|
586
|
-
text: JSON.stringify(
|
|
587
|
-
{
|
|
588
|
-
documents: docCount,
|
|
589
|
-
sections: secCount,
|
|
590
|
-
micro_chunks: chunkCount,
|
|
591
|
-
graph_edges: edgeCount,
|
|
592
|
-
},
|
|
593
|
-
null,
|
|
594
|
-
2
|
|
595
|
-
),
|
|
596
|
-
},
|
|
597
|
-
],
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (action === "list") {
|
|
602
|
-
const docs = await db
|
|
603
|
-
.prepare("SELECT id, title, path, blob_hash, created_at FROM documents ORDER BY created_at DESC")
|
|
604
|
-
.all();
|
|
605
|
-
return {
|
|
606
|
-
content: [{ type: "text", text: JSON.stringify(docs, null, 2) }],
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (action === "read_document") {
|
|
611
|
-
if (!docId) throw new Error("docId parameter is required for read_document action");
|
|
612
|
-
const doc = await db
|
|
613
|
-
.prepare("SELECT id, title, path, blob_hash, created_at FROM documents WHERE id = ? OR path = ? OR title = ?")
|
|
614
|
-
.get(docId, docId, docId);
|
|
615
|
-
if (!doc) {
|
|
616
|
-
throw new Error(`Document not found in knowledge base for docId: ${docId}`);
|
|
617
|
-
}
|
|
618
|
-
const { readBlob } = await import("./storage/blob_store.js");
|
|
619
|
-
const rawContent = await readBlob(doc.blob_hash);
|
|
620
|
-
return {
|
|
621
|
-
content: [
|
|
622
|
-
{
|
|
623
|
-
type: "text",
|
|
624
|
-
text: JSON.stringify(
|
|
625
|
-
{
|
|
626
|
-
id: doc.id,
|
|
627
|
-
title: doc.title,
|
|
628
|
-
path: doc.path,
|
|
629
|
-
created_at: doc.created_at,
|
|
630
|
-
content: rawContent,
|
|
631
|
-
},
|
|
632
|
-
null,
|
|
633
|
-
2
|
|
634
|
-
),
|
|
635
|
-
},
|
|
636
|
-
],
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (action === "delete") {
|
|
641
|
-
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
642
|
-
const { deleteDocument } = await import("./ingest/pipeline.js");
|
|
643
|
-
const result = await deleteDocument(docId, db);
|
|
644
|
-
return {
|
|
645
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (action === "export_snapshot") {
|
|
650
|
-
const { exportSnapshot } = await import("./admin/snapshot.js");
|
|
651
|
-
const result = await exportSnapshot({ customDb: db, outputPath: snapshotPath || null });
|
|
652
|
-
return {
|
|
653
|
-
content: [
|
|
654
|
-
{
|
|
655
|
-
type: "text",
|
|
656
|
-
text: snapshotPath
|
|
657
|
-
? `Snapshot exported successfully to ${snapshotPath}`
|
|
658
|
-
: JSON.stringify(result, null, 2),
|
|
659
|
-
},
|
|
660
|
-
],
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (action === "import_snapshot") {
|
|
665
|
-
if (!snapshotPath) throw new Error("snapshotPath parameter is required for import_snapshot action");
|
|
666
|
-
const { importSnapshot } = await import("./admin/snapshot.js");
|
|
667
|
-
const result = await importSnapshot({ customDb: db, snapshotPathOrData: snapshotPath });
|
|
668
|
-
return {
|
|
669
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
throw new Error(`Unknown action: ${action}`);
|
|
674
|
-
}
|
|
675
|
-
);
|
|
676
|
-
|
|
677
|
-
const transport = new StdioServerTransport();
|
|
678
|
-
await server.connect(transport);
|
|
679
|
-
console.error(`memory-agent MCP server running, data dir: ${MEMORY_DIR}`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { ensureDir, MEMORY_DIR } from "./memory.js";
|
|
5
|
+
import { registerAllTools } from "./tools/index.js";
|
|
6
|
+
|
|
7
|
+
const cliArgs = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
if (cliArgs.includes("setup") || cliArgs.includes("install") || cliArgs.includes("--setup") || cliArgs.includes("-s")) {
|
|
10
|
+
const { runSetup } = await import("./setup.js");
|
|
11
|
+
await runSetup();
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
cliArgs.includes("cli") ||
|
|
17
|
+
cliArgs.includes("config") ||
|
|
18
|
+
cliArgs.includes("--cli") ||
|
|
19
|
+
cliArgs.includes("-c") ||
|
|
20
|
+
cliArgs.includes("login") ||
|
|
21
|
+
cliArgs.includes("logout") ||
|
|
22
|
+
cliArgs.includes("auth-status") ||
|
|
23
|
+
cliArgs.includes("auth_status") ||
|
|
24
|
+
cliArgs.includes("auth")
|
|
25
|
+
) {
|
|
26
|
+
const { runCli } = await import("./cli.js");
|
|
27
|
+
await runCli();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await ensureDir();
|
|
32
|
+
|
|
33
|
+
const server = new McpServer({
|
|
34
|
+
name: "memory-agent",
|
|
35
|
+
version: "1.5.0",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
registerAllTools(server);
|
|
39
|
+
|
|
40
|
+
const transport = new StdioServerTransport();
|
|
41
|
+
await server.connect(transport);
|
|
42
|
+
console.error(`memory-agent MCP server running, data dir: ${MEMORY_DIR}`);
|