@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeriondyseti/vector-memory-mcp",
3
- "version": "2.0.0-rc.3",
3
+ "version": "2.0.0",
4
4
  "description": "A zero-configuration RAG memory server for MCP clients",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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 = this.db
185
- .prepare(
186
- "SELECT id FROM memories_fts WHERE memories_fts MATCH ? LIMIT ?",
187
- )
188
- .all(ftsQuery, candidateLimit) as Array<{ id: string }>;
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);
@@ -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, sorted descending.
32
+ * Returns a map of id -> combined RRF score.
33
33
  */
34
34
  export function hybridRRF(
35
35
  vectorResults: Array<{ id: string }>,
@@ -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.map((m) => ({ id: m.id, content: m.content }));
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
- const since = body.since ? new Date(body.since as string) : undefined;
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(
@@ -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
- const memories = args?.memories as Array<{
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
- const ids = args?.ids as string[];
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
- const updates = args?.updates as Array<{
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 as string;
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: parseHistoryFilters(args),
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
- const ids = args?.ids as string[];
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
- ? new Date(args.history_after as string)
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)) return Array.from(new Float32Array((vec as DataView).buffer));
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?: string | null;
74
+ supersededBy: string | null;
75
75
  usefulness?: number;
76
76
  accessCount?: number;
77
77
  lastAccessed?: Date | null;