@aeriondyseti/vector-memory-mcp 2.0.0-rc.3 → 2.0.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 +1 -1
- package/package.json +1 -1
- package/src/config/index.ts +5 -0
- package/src/db/memory.repository.ts +7 -5
- package/src/db/sqlite-utils.ts +1 -1
- package/src/http/server.ts +10 -2
- package/src/index.ts +1 -1
- package/src/mcp/handlers.ts +89 -12
- package/src/migration.ts +8 -3
- package/src/services/memory.service.ts +1 -0
- package/src/services/parsers/claude-code.parser.ts +1 -0
- package/src/types/conversation.ts +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A local-first MCP server that provides vector-based memory storage. Uses local e
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
- **Local & Private** - All embeddings generated locally, data stored in a single SQLite file
|
|
14
|
+
- **Local & Private** - All embeddings generated locally ([all-MiniLM-L6-v2](https://huggingface.co/Xenova/all-MiniLM-L6-v2), 384-dim), data stored in a single SQLite file
|
|
15
15
|
- **Semantic Search** - Hybrid vector + full-text search with intent-based ranking
|
|
16
16
|
- **Batch Operations** - Store, update, delete, and retrieve multiple memories at once
|
|
17
17
|
- **Session Waypoints** - Save and restore project context between sessions
|
package/package.json
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ import packageJson from "../../package.json" with { type: "json" };
|
|
|
5
5
|
|
|
6
6
|
export const VERSION = packageJson.version;
|
|
7
7
|
|
|
8
|
+
/** Debug mode: auto-enabled for pre-release versions (dev/rc), or via VECTOR_MEMORY_DEBUG env var */
|
|
9
|
+
export const DEBUG = process.env.VECTOR_MEMORY_DEBUG === "1"
|
|
10
|
+
|| VERSION.includes("-dev.")
|
|
11
|
+
|| VERSION.includes("-rc.");
|
|
12
|
+
|
|
8
13
|
export type TransportMode = "stdio" | "http" | "both";
|
|
9
14
|
|
|
10
15
|
export interface ConversationHistoryConfig {
|
|
@@ -181,11 +181,13 @@ export class MemoryRepository {
|
|
|
181
181
|
|
|
182
182
|
// Full-text search
|
|
183
183
|
const ftsQuery = sanitizeFtsQuery(query);
|
|
184
|
-
const ftsResults =
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
const ftsResults: Array<{ id: string }> = ftsQuery
|
|
185
|
+
? (this.db
|
|
186
|
+
.prepare(
|
|
187
|
+
"SELECT id FROM memories_fts WHERE memories_fts MATCH ? LIMIT ?",
|
|
188
|
+
)
|
|
189
|
+
.all(ftsQuery, candidateLimit) as Array<{ id: string }>)
|
|
190
|
+
: [];
|
|
189
191
|
|
|
190
192
|
// Compute RRF scores and pick top ids
|
|
191
193
|
const rrfScores = hybridRRF(vectorResults, ftsResults);
|
package/src/db/sqlite-utils.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function sanitizeFtsQuery(query: string): string {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Compute hybrid RRF scores from two ranked result lists.
|
|
32
|
-
* Returns a map of id -> combined RRF score
|
|
32
|
+
* Returns a map of id -> combined RRF score.
|
|
33
33
|
*/
|
|
34
34
|
export function hybridRRF(
|
|
35
35
|
vectorResults: Array<{ id: string }>,
|
package/src/http/server.ts
CHANGED
|
@@ -198,7 +198,9 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
198
198
|
// Fetch referenced memories in a single query
|
|
199
199
|
const memoryIds = (waypoint.metadata.memory_ids as string[] | undefined) ?? [];
|
|
200
200
|
const memories = await memoryService.getMultiple(memoryIds);
|
|
201
|
-
const referencedMemories = memories
|
|
201
|
+
const referencedMemories = memories
|
|
202
|
+
.filter((m) => !isDeleted(m))
|
|
203
|
+
.map((m) => ({ id: m.id, content: m.content }));
|
|
202
204
|
|
|
203
205
|
return c.json({
|
|
204
206
|
content: waypoint.content,
|
|
@@ -221,7 +223,13 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
const body = await c.req.json().catch(() => ({}));
|
|
224
|
-
|
|
226
|
+
let since: Date | undefined;
|
|
227
|
+
if (body.since) {
|
|
228
|
+
since = new Date(body.since as string);
|
|
229
|
+
if (isNaN(since.getTime())) {
|
|
230
|
+
return c.json({ error: "Invalid 'since' date format" }, 400);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
225
233
|
const result = await conversationService.indexConversations(
|
|
226
234
|
body.path as string | undefined,
|
|
227
235
|
since
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ async function runMigrate(args: string[]): Promise<void> {
|
|
|
16
16
|
const config = loadConfig(overrides);
|
|
17
17
|
|
|
18
18
|
const source = config.dbPath;
|
|
19
|
-
const target = source + ".sqlite";
|
|
19
|
+
const target = source.endsWith(".sqlite") ? source.replace(/\.sqlite$/, "-migrated.sqlite") : source + ".sqlite";
|
|
20
20
|
|
|
21
21
|
if (!isLanceDbDirectory(source)) {
|
|
22
22
|
console.error(
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -3,16 +3,64 @@ import type { MemoryService } from "../services/memory.service.js";
|
|
|
3
3
|
import type { ConversationHistoryService } from "../services/conversation.service.js";
|
|
4
4
|
import type { SearchIntent } from "../types/memory.js";
|
|
5
5
|
import type { HistoryFilters, SearchResult } from "../types/conversation.js";
|
|
6
|
+
import { DEBUG } from "../config/index.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Safely coerce a tool argument to an array. Handles the case where the MCP
|
|
10
|
+
* transport delivers a JSON-serialized string instead of a parsed array.
|
|
11
|
+
*/
|
|
12
|
+
function asArray<T>(value: unknown, fieldName: string): T[] {
|
|
13
|
+
if (Array.isArray(value)) return value;
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
if (DEBUG) {
|
|
16
|
+
console.error(
|
|
17
|
+
`[vector-memory-mcp] DEBUG: ${fieldName} received as string (${value.length} chars) instead of array — parsing`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(value);
|
|
22
|
+
if (Array.isArray(parsed)) return parsed;
|
|
23
|
+
if (DEBUG) {
|
|
24
|
+
console.error(
|
|
25
|
+
`[vector-memory-mcp] DEBUG: ${fieldName} parsed as ${typeof parsed}, not array`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
} catch { /* fall through */ }
|
|
29
|
+
} else if (DEBUG) {
|
|
30
|
+
console.error(
|
|
31
|
+
`[vector-memory-mcp] DEBUG: ${fieldName} has unexpected type: ${typeof value}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`${fieldName} must be an array`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function errorText(e: unknown): string {
|
|
38
|
+
return e instanceof Error ? e.message : String(e);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseDate(value: unknown, fieldName: string): Date | undefined {
|
|
42
|
+
if (value === undefined) return undefined;
|
|
43
|
+
const date = new Date(value as string);
|
|
44
|
+
if (isNaN(date.getTime())) {
|
|
45
|
+
throw new Error(`${fieldName} is not a valid date`);
|
|
46
|
+
}
|
|
47
|
+
return date;
|
|
48
|
+
}
|
|
6
49
|
|
|
7
50
|
export async function handleStoreMemories(
|
|
8
51
|
args: Record<string, unknown> | undefined,
|
|
9
52
|
service: MemoryService
|
|
10
53
|
): Promise<CallToolResult> {
|
|
11
|
-
|
|
54
|
+
let memories: Array<{
|
|
12
55
|
content: string;
|
|
13
56
|
embedding_text?: string;
|
|
14
57
|
metadata?: Record<string, unknown>;
|
|
15
58
|
}>;
|
|
59
|
+
try {
|
|
60
|
+
memories = asArray(args?.memories, "memories");
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return { isError: true, content: [{ type: "text", text: errorText(e) }] };
|
|
63
|
+
}
|
|
16
64
|
|
|
17
65
|
const ids: string[] = [];
|
|
18
66
|
for (const item of memories) {
|
|
@@ -41,7 +89,12 @@ export async function handleDeleteMemories(
|
|
|
41
89
|
args: Record<string, unknown> | undefined,
|
|
42
90
|
service: MemoryService
|
|
43
91
|
): Promise<CallToolResult> {
|
|
44
|
-
|
|
92
|
+
let ids: string[];
|
|
93
|
+
try {
|
|
94
|
+
ids = asArray(args?.ids, "ids");
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return { isError: true, content: [{ type: "text", text: errorText(e) }] };
|
|
97
|
+
}
|
|
45
98
|
const results: string[] = [];
|
|
46
99
|
|
|
47
100
|
for (const id of ids) {
|
|
@@ -66,16 +119,26 @@ export async function handleUpdateMemories(
|
|
|
66
119
|
args: Record<string, unknown> | undefined,
|
|
67
120
|
service: MemoryService
|
|
68
121
|
): Promise<CallToolResult> {
|
|
69
|
-
|
|
122
|
+
let updates: Array<{
|
|
70
123
|
id: string;
|
|
71
124
|
content?: string;
|
|
72
125
|
embedding_text?: string;
|
|
73
126
|
metadata?: Record<string, unknown>;
|
|
74
127
|
}>;
|
|
128
|
+
try {
|
|
129
|
+
updates = asArray(args?.updates, "updates");
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return { isError: true, content: [{ type: "text", text: errorText(e) }] };
|
|
132
|
+
}
|
|
75
133
|
|
|
76
134
|
const results: string[] = [];
|
|
77
135
|
|
|
78
136
|
for (const update of updates) {
|
|
137
|
+
if (!update.id || typeof update.id !== "string") {
|
|
138
|
+
results.push("Skipped update: missing required id field");
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
const memory = await service.update(update.id, {
|
|
80
143
|
content: update.content,
|
|
81
144
|
embeddingText: update.embedding_text,
|
|
@@ -103,7 +166,10 @@ export async function handleSearchMemories(
|
|
|
103
166
|
args: Record<string, unknown> | undefined,
|
|
104
167
|
service: MemoryService
|
|
105
168
|
): Promise<CallToolResult> {
|
|
106
|
-
const query = args?.query
|
|
169
|
+
const query = args?.query;
|
|
170
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
171
|
+
return { isError: true, content: [{ type: "text", text: "query is required and must be a non-empty string" }] };
|
|
172
|
+
}
|
|
107
173
|
const intent = (args?.intent as SearchIntent) ?? "fact_check";
|
|
108
174
|
const limit = (args?.limit as number) ?? 10;
|
|
109
175
|
const includeDeleted = (args?.include_deleted as boolean) ?? false;
|
|
@@ -111,10 +177,17 @@ export async function handleSearchMemories(
|
|
|
111
177
|
// history_only implies include_history
|
|
112
178
|
const includeHistory = historyOnly ? true : (args?.include_history as boolean | undefined);
|
|
113
179
|
|
|
180
|
+
let historyFilters: HistoryFilters;
|
|
181
|
+
try {
|
|
182
|
+
historyFilters = parseHistoryFilters(args);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { isError: true, content: [{ type: "text", text: errorText(e) }] };
|
|
185
|
+
}
|
|
186
|
+
|
|
114
187
|
const results = await service.search(query, intent, limit, includeDeleted, {
|
|
115
188
|
includeHistory,
|
|
116
189
|
historyOnly,
|
|
117
|
-
historyFilters
|
|
190
|
+
historyFilters,
|
|
118
191
|
});
|
|
119
192
|
|
|
120
193
|
if (results.length === 0) {
|
|
@@ -166,7 +239,12 @@ export async function handleGetMemories(
|
|
|
166
239
|
args: Record<string, unknown> | undefined,
|
|
167
240
|
service: MemoryService
|
|
168
241
|
): Promise<CallToolResult> {
|
|
169
|
-
|
|
242
|
+
let ids: string[];
|
|
243
|
+
try {
|
|
244
|
+
ids = asArray(args?.ids, "ids");
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return { isError: true, content: [{ type: "text", text: errorText(e) }] };
|
|
247
|
+
}
|
|
170
248
|
|
|
171
249
|
const memories = await service.getMultiple(ids);
|
|
172
250
|
const memoryMap = new Map(memories.map((m) => [m.id, m]));
|
|
@@ -261,12 +339,8 @@ function parseHistoryFilters(
|
|
|
261
339
|
return {
|
|
262
340
|
sessionId: args?.session_id as string | undefined,
|
|
263
341
|
role: args?.role_filter as string | undefined,
|
|
264
|
-
after: args?.history_after
|
|
265
|
-
|
|
266
|
-
: undefined,
|
|
267
|
-
before: args?.history_before
|
|
268
|
-
? new Date(args.history_before as string)
|
|
269
|
-
: undefined,
|
|
342
|
+
after: parseDate(args?.history_after, "history_after"),
|
|
343
|
+
before: parseDate(args?.history_before, "history_before"),
|
|
270
344
|
};
|
|
271
345
|
}
|
|
272
346
|
|
|
@@ -301,6 +375,9 @@ export async function handleIndexConversations(
|
|
|
301
375
|
const path = args?.path as string | undefined;
|
|
302
376
|
const sinceStr = args?.since as string | undefined;
|
|
303
377
|
const since = sinceStr ? new Date(sinceStr) : undefined;
|
|
378
|
+
if (since && isNaN(since.getTime())) {
|
|
379
|
+
return { isError: true, content: [{ type: "text", text: "Invalid 'since' date format" }] };
|
|
380
|
+
}
|
|
304
381
|
|
|
305
382
|
const result = await conversationService.indexConversations(path, since);
|
|
306
383
|
|
package/src/migration.ts
CHANGED
|
@@ -18,6 +18,7 @@ function toEpochMs(value: unknown): number {
|
|
|
18
18
|
if (typeof value === "number") return value;
|
|
19
19
|
if (value instanceof Date) return value.getTime();
|
|
20
20
|
if (typeof value === "bigint") return Number(value);
|
|
21
|
+
console.warn(`⚠️ Unexpected timestamp type: ${typeof value} (value: ${value}), using current time`);
|
|
21
22
|
return Date.now();
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -28,7 +29,10 @@ function toFloatArray(vec: unknown): number[] {
|
|
|
28
29
|
if (vec && typeof (vec as any).toArray === "function") {
|
|
29
30
|
return Array.from((vec as any).toArray());
|
|
30
31
|
}
|
|
31
|
-
if (ArrayBuffer.isView(vec))
|
|
32
|
+
if (ArrayBuffer.isView(vec)) {
|
|
33
|
+
const view = vec as DataView;
|
|
34
|
+
return Array.from(new Float32Array(view.buffer, view.byteOffset, view.byteLength / 4));
|
|
35
|
+
}
|
|
32
36
|
return [];
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -226,6 +230,7 @@ export async function migrate(opts: MigrateOptions): Promise<MigrateResult> {
|
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
// ── Finalize ────────────────────────────────────────────────────
|
|
233
|
+
await lanceDb.close?.();
|
|
229
234
|
sqliteDb.close();
|
|
230
235
|
|
|
231
236
|
const { size } = statSync(target);
|
|
@@ -247,8 +252,8 @@ export function formatMigrationSummary(
|
|
|
247
252
|
${result.memoriesMigrated} memories, ${result.conversationChunksMigrated} conversation chunks
|
|
248
253
|
|
|
249
254
|
Next steps:
|
|
250
|
-
1. Backup: mv ${source} ${source}.lance-backup
|
|
251
|
-
2. Activate: mv ${target} ${source}
|
|
255
|
+
1. Backup: mv "${source}" "${source}.lance-backup"
|
|
256
|
+
2. Activate: mv "${target}" "${source}"
|
|
252
257
|
3. Restart your MCP server
|
|
253
258
|
`;
|
|
254
259
|
}
|
|
@@ -237,6 +237,7 @@ export class MemoryService {
|
|
|
237
237
|
updatedAt: row.createdAt,
|
|
238
238
|
source: "conversation_history" as const,
|
|
239
239
|
score: row.rrfScore * historyWeight,
|
|
240
|
+
supersededBy: null,
|
|
240
241
|
sessionId: (row.metadata?.session_id as string) ?? "",
|
|
241
242
|
role: (row.metadata?.role as string) ?? "unknown",
|
|
242
243
|
messageIndexStart: (row.metadata?.message_index_start as number) ?? 0,
|
|
@@ -227,6 +227,7 @@ export class ClaudeCodeSessionParser implements SessionLogParser {
|
|
|
227
227
|
if (since && lastModified <= since) continue;
|
|
228
228
|
|
|
229
229
|
const sessionId = basename(entry, ".jsonl");
|
|
230
|
+
if (!UUID_PATTERN.test(sessionId)) continue;
|
|
230
231
|
const project = extractProjectFromDir(projectDir);
|
|
231
232
|
|
|
232
233
|
files.push({
|
|
@@ -71,7 +71,7 @@ export interface SearchResult {
|
|
|
71
71
|
source: "memory" | "conversation_history";
|
|
72
72
|
score: number;
|
|
73
73
|
// Memory-specific fields
|
|
74
|
-
supersededBy
|
|
74
|
+
supersededBy: string | null;
|
|
75
75
|
usefulness?: number;
|
|
76
76
|
accessCount?: number;
|
|
77
77
|
lastAccessed?: Date | null;
|