@aeriondyseti/vector-memory-mcp 2.2.3 → 2.2.6-dev.1
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 +33 -13
- package/package.json +12 -12
- package/{src → server}/config/index.ts +10 -2
- package/{src/db → server/core}/connection.ts +10 -2
- package/{src/db → server/core}/conversation.repository.ts +1 -1
- package/{src/services → server/core}/conversation.service.ts +2 -2
- package/server/core/embeddings.service.ts +125 -0
- package/{src/db → server/core}/memory.repository.ts +5 -1
- package/{src/services → server/core}/memory.service.ts +20 -4
- package/server/core/migration.service.ts +882 -0
- package/server/core/migrations.ts +263 -0
- package/{src/services → server/core}/parsers/claude-code.parser.ts +1 -1
- package/{src/services → server/core}/parsers/types.ts +1 -1
- package/{src → server}/index.ts +16 -48
- package/{src → server/transports}/http/mcp-transport.ts +2 -2
- package/{src → server/transports}/http/server.ts +6 -4
- package/{src → server/transports}/mcp/handlers.ts +5 -5
- package/server/transports/mcp/resources.ts +20 -0
- package/{src → server/transports}/mcp/server.ts +14 -3
- package/server/utils/formatting.ts +143 -0
- package/scripts/lancedb-extract.ts +0 -181
- package/scripts/migrate-from-lancedb.ts +0 -56
- package/scripts/smoke-test.ts +0 -699
- package/scripts/test-runner.ts +0 -76
- package/scripts/warmup.ts +0 -72
- package/src/db/migrations.ts +0 -108
- package/src/migration.ts +0 -203
- package/src/services/embeddings.service.ts +0 -48
- /package/{src/types → server/core}/conversation.ts +0 -0
- /package/{src/types → server/core}/memory.ts +0 -0
- /package/{src/db → server/core}/sqlite-utils.ts +0 -0
- /package/{src → server/transports}/mcp/tools.ts +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { EmbeddingsService } from "./embeddings.service.js";
|
|
3
|
+
import { serializeVector } from "./sqlite-utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pre-migration step: remove vec0 virtual table entries from sqlite_master
|
|
7
|
+
* and drop their shadow tables using the sqlite3 CLI.
|
|
8
|
+
*
|
|
9
|
+
* Must run BEFORE bun:sqlite opens the database because:
|
|
10
|
+
* - bun:sqlite cannot modify sqlite_master (no writable_schema support)
|
|
11
|
+
* - DROP TABLE on a virtual table requires the extension module to be loaded
|
|
12
|
+
* - SQLite 3.51+ has defensive mode on by default, requiring .dbconfig override
|
|
13
|
+
*
|
|
14
|
+
* Safe to call on any database — it's a no-op if there are no vec0 tables.
|
|
15
|
+
*/
|
|
16
|
+
export function removeVec0Tables(dbPath: string): void {
|
|
17
|
+
const result = Bun.spawnSync({
|
|
18
|
+
cmd: ["sqlite3", dbPath],
|
|
19
|
+
stdin: new TextEncoder().encode(
|
|
20
|
+
[
|
|
21
|
+
".dbconfig defensive off",
|
|
22
|
+
".dbconfig writable_schema on",
|
|
23
|
+
// Drop shadow tables (regular tables, no extension needed)
|
|
24
|
+
"DROP TABLE IF EXISTS memories_vec_rowids;",
|
|
25
|
+
"DROP TABLE IF EXISTS memories_vec_chunks;",
|
|
26
|
+
"DROP TABLE IF EXISTS memories_vec_info;",
|
|
27
|
+
"DROP TABLE IF EXISTS memories_vec_vector_chunks00;",
|
|
28
|
+
"DROP TABLE IF EXISTS memories_vec_migration_tmp;",
|
|
29
|
+
"DROP TABLE IF EXISTS conversation_history_vec_rowids;",
|
|
30
|
+
"DROP TABLE IF EXISTS conversation_history_vec_chunks;",
|
|
31
|
+
"DROP TABLE IF EXISTS conversation_history_vec_info;",
|
|
32
|
+
"DROP TABLE IF EXISTS conversation_history_vec_vector_chunks00;",
|
|
33
|
+
"DROP TABLE IF EXISTS conversation_history_vec_migration_tmp;",
|
|
34
|
+
// Remove orphaned vec0 virtual table entries from schema
|
|
35
|
+
"DELETE FROM sqlite_master WHERE sql LIKE '%vec0%';",
|
|
36
|
+
].join("\n"),
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
if (result.exitCode !== 0) {
|
|
40
|
+
const stderr = result.stderr.toString().trim();
|
|
41
|
+
if (!stderr.includes("unable to open database")) {
|
|
42
|
+
throw new Error(`vec0 cleanup failed: ${stderr}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run all schema migrations. Safe to call on every startup (uses IF NOT EXISTS).
|
|
49
|
+
*
|
|
50
|
+
* IMPORTANT: Call removeVec0Tables(dbPath) before opening the database
|
|
51
|
+
* with bun:sqlite if the database may contain vec0 virtual tables.
|
|
52
|
+
*/
|
|
53
|
+
export function runMigrations(db: Database): void {
|
|
54
|
+
// -- Memories --
|
|
55
|
+
db.exec(`
|
|
56
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
content TEXT NOT NULL,
|
|
59
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
60
|
+
created_at INTEGER NOT NULL,
|
|
61
|
+
updated_at INTEGER NOT NULL,
|
|
62
|
+
superseded_by TEXT,
|
|
63
|
+
usefulness REAL NOT NULL DEFAULT 0.0,
|
|
64
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
last_accessed INTEGER
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
db.exec(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS memories_vec (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
vector BLOB NOT NULL
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
db.exec(`
|
|
77
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
78
|
+
id UNINDEXED,
|
|
79
|
+
content
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
// -- Conversation History --
|
|
84
|
+
db.exec(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS conversation_history (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
content TEXT NOT NULL,
|
|
88
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
89
|
+
created_at INTEGER NOT NULL,
|
|
90
|
+
session_id TEXT NOT NULL,
|
|
91
|
+
role TEXT NOT NULL,
|
|
92
|
+
message_index_start INTEGER NOT NULL,
|
|
93
|
+
message_index_end INTEGER NOT NULL,
|
|
94
|
+
project TEXT NOT NULL
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
db.exec(`
|
|
99
|
+
CREATE TABLE IF NOT EXISTS conversation_history_vec (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
vector BLOB NOT NULL
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_fts USING fts5(
|
|
107
|
+
id UNINDEXED,
|
|
108
|
+
content
|
|
109
|
+
)
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
// -- Indexes --
|
|
113
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_session_id ON conversation_history(session_id)`);
|
|
114
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_project ON conversation_history(project)`);
|
|
115
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_role ON conversation_history(role)`);
|
|
116
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_created_at ON conversation_history(created_at)`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Backfill missing vectors in memories_vec and conversation_history_vec.
|
|
121
|
+
*
|
|
122
|
+
* After the vec0-to-BLOB migration, existing rows may lack vector embeddings.
|
|
123
|
+
* This re-embeds their content and inserts into the _vec tables.
|
|
124
|
+
* Idempotent: skips rows that already have vectors. Fast no-op when fully backfilled.
|
|
125
|
+
*/
|
|
126
|
+
export async function backfillVectors(
|
|
127
|
+
db: Database,
|
|
128
|
+
embeddings: EmbeddingsService,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
// Fast sentinel check: skip the LEFT JOIN queries entirely when backfill is done
|
|
131
|
+
const sentinel = db
|
|
132
|
+
.prepare("SELECT 1 FROM memories_vec LIMIT 1")
|
|
133
|
+
.get();
|
|
134
|
+
const memoriesExist = db.prepare("SELECT 1 FROM memories LIMIT 1").get();
|
|
135
|
+
const convosExist = db.prepare("SELECT 1 FROM conversation_history LIMIT 1").get();
|
|
136
|
+
|
|
137
|
+
// If vec tables have data and source tables have data, backfill is likely complete.
|
|
138
|
+
// Only run the expensive LEFT JOIN when there's reason to suspect gaps.
|
|
139
|
+
const convoSentinel = db
|
|
140
|
+
.prepare("SELECT 1 FROM conversation_history_vec LIMIT 1")
|
|
141
|
+
.get();
|
|
142
|
+
const mayNeedMemoryBackfill = memoriesExist && !sentinel;
|
|
143
|
+
const mayNeedConvoBackfill = convosExist && !convoSentinel;
|
|
144
|
+
|
|
145
|
+
// If both vec tables are populated, do a quick count check to confirm
|
|
146
|
+
if (!mayNeedMemoryBackfill && !mayNeedConvoBackfill) {
|
|
147
|
+
if (memoriesExist) {
|
|
148
|
+
const gap = db.prepare(
|
|
149
|
+
`SELECT 1 FROM memories m LEFT JOIN memories_vec v ON m.id = v.id
|
|
150
|
+
WHERE v.id IS NULL OR length(v.vector) = 0 LIMIT 1`,
|
|
151
|
+
).get();
|
|
152
|
+
if (!gap && convosExist) {
|
|
153
|
+
const convoGap = db.prepare(
|
|
154
|
+
`SELECT 1 FROM conversation_history c LEFT JOIN conversation_history_vec v ON c.id = v.id
|
|
155
|
+
WHERE v.id IS NULL OR length(v.vector) = 0 LIMIT 1`,
|
|
156
|
+
).get();
|
|
157
|
+
if (!convoGap) return;
|
|
158
|
+
} else if (!gap && !convosExist) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
return; // No data at all
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Memories ──────────────────────────────────────────────────────
|
|
167
|
+
const missingMemories = db
|
|
168
|
+
.prepare(
|
|
169
|
+
`SELECT m.id, m.content, json_extract(m.metadata, '$.type') AS type
|
|
170
|
+
FROM memories m
|
|
171
|
+
LEFT JOIN memories_vec v ON m.id = v.id
|
|
172
|
+
WHERE v.id IS NULL OR length(v.vector) = 0`,
|
|
173
|
+
)
|
|
174
|
+
.all() as Array<{ id: string; content: string; type: string | null }>;
|
|
175
|
+
|
|
176
|
+
if (missingMemories.length > 0) {
|
|
177
|
+
console.error(
|
|
178
|
+
`[vector-memory-mcp] Backfilling vectors for ${missingMemories.length} memories...`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const insertVec = db.prepare(
|
|
182
|
+
"INSERT OR REPLACE INTO memories_vec (id, vector) VALUES (?, ?)",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const zeroVector = serializeVector(
|
|
186
|
+
new Array(embeddings.dimension).fill(0),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Separate waypoints from content that needs embedding
|
|
190
|
+
const toEmbed = missingMemories.filter((r) => r.type !== "waypoint");
|
|
191
|
+
const waypoints = missingMemories.filter((r) => r.type === "waypoint");
|
|
192
|
+
|
|
193
|
+
// Batch embed all non-waypoint content
|
|
194
|
+
const vectors = toEmbed.length > 0
|
|
195
|
+
? await embeddings.embedBatch(toEmbed.map((r) => r.content))
|
|
196
|
+
: [];
|
|
197
|
+
|
|
198
|
+
db.exec("BEGIN");
|
|
199
|
+
try {
|
|
200
|
+
for (const row of waypoints) {
|
|
201
|
+
insertVec.run(row.id, zeroVector);
|
|
202
|
+
}
|
|
203
|
+
for (let i = 0; i < toEmbed.length; i++) {
|
|
204
|
+
insertVec.run(toEmbed[i].id, serializeVector(vectors[i]));
|
|
205
|
+
}
|
|
206
|
+
db.exec("COMMIT");
|
|
207
|
+
} catch (e) {
|
|
208
|
+
db.exec("ROLLBACK");
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.error(
|
|
213
|
+
`[vector-memory-mcp] Backfilled ${missingMemories.length} memory vectors`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Conversation history ──────────────────────────────────────────
|
|
218
|
+
const missingConvos = db
|
|
219
|
+
.prepare(
|
|
220
|
+
`SELECT c.id, c.content
|
|
221
|
+
FROM conversation_history c
|
|
222
|
+
LEFT JOIN conversation_history_vec v ON c.id = v.id
|
|
223
|
+
WHERE v.id IS NULL OR length(v.vector) = 0`,
|
|
224
|
+
)
|
|
225
|
+
.all() as Array<{ id: string; content: string }>;
|
|
226
|
+
|
|
227
|
+
if (missingConvos.length > 0) {
|
|
228
|
+
console.error(
|
|
229
|
+
`[vector-memory-mcp] Backfilling vectors for ${missingConvos.length} conversation chunks...`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const insertConvoVec = db.prepare(
|
|
233
|
+
"INSERT OR REPLACE INTO conversation_history_vec (id, vector) VALUES (?, ?)",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Batch embed in chunks of 32
|
|
237
|
+
const BATCH_SIZE = 32;
|
|
238
|
+
db.exec("BEGIN");
|
|
239
|
+
try {
|
|
240
|
+
for (let i = 0; i < missingConvos.length; i += BATCH_SIZE) {
|
|
241
|
+
const batch = missingConvos.slice(i, i + BATCH_SIZE);
|
|
242
|
+
const vecs = await embeddings.embedBatch(batch.map((r) => r.content));
|
|
243
|
+
for (let j = 0; j < batch.length; j++) {
|
|
244
|
+
insertConvoVec.run(batch[j].id, serializeVector(vecs[j]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if ((i + BATCH_SIZE) % 100 < BATCH_SIZE) {
|
|
248
|
+
console.error(
|
|
249
|
+
`[vector-memory-mcp] ...${Math.min(i + BATCH_SIZE, missingConvos.length)}/${missingConvos.length} conversation chunks`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
db.exec("COMMIT");
|
|
254
|
+
} catch (e) {
|
|
255
|
+
db.exec("ROLLBACK");
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.error(
|
|
260
|
+
`[vector-memory-mcp] Backfilled ${missingConvos.length} conversation vectors`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "fs/promises";
|
|
2
2
|
import { basename, dirname, join } from "path";
|
|
3
|
-
import type { ParsedMessage, SessionFileInfo } from "
|
|
3
|
+
import type { ParsedMessage, SessionFileInfo } from "../conversation.js";
|
|
4
4
|
import type { SessionLogParser } from "./types.js";
|
|
5
5
|
|
|
6
6
|
// UUID pattern for session IDs
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ParsedMessage, SessionFileInfo } from "
|
|
1
|
+
import type { ParsedMessage, SessionFileInfo } from "../conversation.js";
|
|
2
2
|
|
|
3
3
|
/** Interface for parsing session log files into structured messages */
|
|
4
4
|
export interface SessionLogParser {
|
package/{src → server}/index.ts
RENAMED
|
@@ -1,34 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { loadConfig, parseCliArgs } from "./config/index.js";
|
|
4
|
-
import { connectToDatabase } from "./
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
async function runMigrate(args: string[]): Promise<void> {
|
|
15
|
-
const overrides = parseCliArgs(args.slice(1)); // skip "migrate"
|
|
16
|
-
const config = loadConfig(overrides);
|
|
17
|
-
|
|
18
|
-
const source = config.dbPath;
|
|
19
|
-
const target = source.endsWith(".sqlite") ? source.replace(/\.sqlite$/, "-migrated.sqlite") : source + ".sqlite";
|
|
20
|
-
|
|
21
|
-
if (!isLanceDbDirectory(source)) {
|
|
22
|
-
console.error(
|
|
23
|
-
`[vector-memory-mcp] No LanceDB data found at ${source}\n` +
|
|
24
|
-
` Nothing to migrate. The server will create a fresh SQLite database on startup.`
|
|
25
|
-
);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const result = await migrate({ source, target });
|
|
30
|
-
console.error(formatMigrationSummary(source, target, result));
|
|
31
|
-
}
|
|
4
|
+
import { connectToDatabase } from "./core/connection.js";
|
|
5
|
+
import { backfillVectors } from "./core/migrations.js";
|
|
6
|
+
import { MemoryRepository } from "./core/memory.repository.js";
|
|
7
|
+
import { ConversationRepository } from "./core/conversation.repository.js";
|
|
8
|
+
import { EmbeddingsService } from "./core/embeddings.service.js";
|
|
9
|
+
import { MemoryService } from "./core/memory.service.js";
|
|
10
|
+
import { ConversationHistoryService } from "./core/conversation.service.js";
|
|
11
|
+
import { startServer } from "./transports/mcp/server.js";
|
|
12
|
+
import { startHttpServer } from "./transports/http/server.js";
|
|
32
13
|
|
|
33
14
|
async function main(): Promise<void> {
|
|
34
15
|
const args = process.argv.slice(2);
|
|
@@ -40,35 +21,23 @@ async function main(): Promise<void> {
|
|
|
40
21
|
return;
|
|
41
22
|
}
|
|
42
23
|
|
|
43
|
-
// Check for migrate command
|
|
44
|
-
if (args[0] === "migrate") {
|
|
45
|
-
await runMigrate(args);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
24
|
// Parse CLI args and load config
|
|
50
25
|
const overrides = parseCliArgs(args);
|
|
51
26
|
const config = loadConfig(overrides);
|
|
52
27
|
|
|
53
|
-
//
|
|
54
|
-
if (isLanceDbDirectory(config.dbPath)) {
|
|
55
|
-
console.error(
|
|
56
|
-
`[vector-memory-mcp] ⚠️ Legacy LanceDB data detected at ${config.dbPath}\n` +
|
|
57
|
-
` Your data must be migrated to the new SQLite format.\n` +
|
|
58
|
-
` Run: vector-memory-mcp migrate\n` +
|
|
59
|
-
` Or: bun run src/index.ts migrate\n`
|
|
60
|
-
);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Initialize database
|
|
28
|
+
// Initialize database and backfill any missing vectors before services start
|
|
65
29
|
const db = connectToDatabase(config.dbPath);
|
|
30
|
+
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
31
|
+
await backfillVectors(db, embeddings);
|
|
66
32
|
|
|
67
33
|
// Initialize layers
|
|
68
34
|
const repository = new MemoryRepository(db);
|
|
69
|
-
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
70
35
|
const memoryService = new MemoryService(repository, embeddings);
|
|
71
36
|
|
|
37
|
+
if (config.pluginMode) {
|
|
38
|
+
console.error("[vector-memory-mcp] Running in plugin mode");
|
|
39
|
+
}
|
|
40
|
+
|
|
72
41
|
// Conditionally initialize conversation history indexing
|
|
73
42
|
if (config.conversationHistory.enabled) {
|
|
74
43
|
const conversationRepository = new ConversationRepository(db);
|
|
@@ -88,7 +57,6 @@ async function main(): Promise<void> {
|
|
|
88
57
|
// Graceful shutdown handler
|
|
89
58
|
const shutdown = () => {
|
|
90
59
|
console.error("[vector-memory-mcp] Shutting down...");
|
|
91
|
-
removeLockfile();
|
|
92
60
|
if (httpStop) httpStop();
|
|
93
61
|
db.close();
|
|
94
62
|
process.exit(0);
|
|
@@ -24,8 +24,8 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
|
24
24
|
import { tools } from "../mcp/tools.js";
|
|
25
25
|
import { handleToolCall } from "../mcp/handlers.js";
|
|
26
26
|
import { SERVER_INSTRUCTIONS } from "../mcp/server.js";
|
|
27
|
-
import { VERSION } from "
|
|
28
|
-
import type { MemoryService } from "
|
|
27
|
+
import { VERSION } from "../../config/index.js";
|
|
28
|
+
import type { MemoryService } from "../../core/memory.service.js";
|
|
29
29
|
|
|
30
30
|
interface Session {
|
|
31
31
|
server: Server;
|
|
@@ -3,11 +3,12 @@ import { cors } from "hono/cors";
|
|
|
3
3
|
import { createServer } from "net";
|
|
4
4
|
import { writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
|
-
import type { MemoryService } from "
|
|
7
|
-
import type { Config } from "
|
|
8
|
-
import { isDeleted } from "
|
|
6
|
+
import type { MemoryService } from "../../core/memory.service.js";
|
|
7
|
+
import type { Config } from "../../config/index.js";
|
|
8
|
+
import { isDeleted } from "../../core/memory.js";
|
|
9
9
|
import { createMcpRoutes } from "./mcp-transport.js";
|
|
10
|
-
import type { Memory, SearchIntent } from "
|
|
10
|
+
import type { Memory, SearchIntent } from "../../core/memory.js";
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Check if a port is available by attempting to bind to it
|
|
@@ -109,6 +110,7 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
109
110
|
embeddingModel: config.embeddingModel,
|
|
110
111
|
embeddingDimension: config.embeddingDimension,
|
|
111
112
|
historyEnabled: config.conversationHistory.enabled,
|
|
113
|
+
pluginMode: config.pluginMode,
|
|
112
114
|
},
|
|
113
115
|
});
|
|
114
116
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import type { MemoryService } from "
|
|
3
|
-
import type { ConversationHistoryService } from "
|
|
4
|
-
import type { SearchIntent } from "
|
|
5
|
-
import type { HistoryFilters, SearchResult } from "
|
|
6
|
-
import { DEBUG } from "
|
|
2
|
+
import type { MemoryService } from "../../core/memory.service.js";
|
|
3
|
+
import type { ConversationHistoryService } from "../../core/conversation.service.js";
|
|
4
|
+
import type { SearchIntent } from "../../core/memory.js";
|
|
5
|
+
import type { HistoryFilters, SearchResult } from "../../core/conversation.js";
|
|
6
|
+
import { DEBUG } from "../../config/index.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Safely coerce a tool argument to an array. Handles the case where the MCP
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const resources: Array<{
|
|
2
|
+
uri: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
}> = [];
|
|
7
|
+
|
|
8
|
+
const RESOURCE_CONTENT: Record<string, string> = {};
|
|
9
|
+
|
|
10
|
+
export function readResource(uri: string): {
|
|
11
|
+
contents: Array<{ uri: string; mimeType: string; text: string }>;
|
|
12
|
+
} {
|
|
13
|
+
const text = RESOURCE_CONTENT[uri];
|
|
14
|
+
if (!text) {
|
|
15
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
contents: [{ uri, mimeType: "text/markdown", text }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -3,12 +3,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import {
|
|
4
4
|
CallToolRequestSchema,
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
|
+
ListResourcesRequestSchema,
|
|
7
|
+
ReadResourceRequestSchema,
|
|
6
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { resources, readResource } from "./resources.js";
|
|
7
10
|
|
|
8
11
|
import { tools } from "./tools.js";
|
|
9
12
|
import { handleToolCall } from "./handlers.js";
|
|
10
|
-
import type { MemoryService } from "
|
|
11
|
-
import { VERSION } from "
|
|
13
|
+
import type { MemoryService } from "../../core/memory.service.js";
|
|
14
|
+
import { VERSION } from "../../config/index.js";
|
|
12
15
|
|
|
13
16
|
export const SERVER_INSTRUCTIONS = `This server is the user's canonical memory system. It provides persistent, semantic vector memory that survives across conversations and sessions.
|
|
14
17
|
|
|
@@ -20,7 +23,7 @@ export function createServer(memoryService: MemoryService): Server {
|
|
|
20
23
|
const server = new Server(
|
|
21
24
|
{ name: "vector-memory-mcp", version: VERSION },
|
|
22
25
|
{
|
|
23
|
-
capabilities: { tools: {} },
|
|
26
|
+
capabilities: { tools: {}, resources: {} },
|
|
24
27
|
instructions: SERVER_INSTRUCTIONS,
|
|
25
28
|
}
|
|
26
29
|
);
|
|
@@ -34,6 +37,14 @@ export function createServer(memoryService: MemoryService): Server {
|
|
|
34
37
|
return handleToolCall(name, args, memoryService);
|
|
35
38
|
});
|
|
36
39
|
|
|
40
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
41
|
+
return { resources };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
45
|
+
return readResource(request.params.uri);
|
|
46
|
+
});
|
|
47
|
+
|
|
37
48
|
return server;
|
|
38
49
|
}
|
|
39
50
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared text formatting utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides ANSI styling, Nerd Font icons, horizontal rules, structured
|
|
5
|
+
* message builders, debug logging, and time formatting. Used by both the
|
|
6
|
+
* MCP server and the Claude Code plugin hooks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── ANSI escape codes ───────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export const ansi = {
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
bold: "\x1b[1m",
|
|
14
|
+
dim: "\x1b[2m",
|
|
15
|
+
italic: "\x1b[3m",
|
|
16
|
+
underline: "\x1b[4m",
|
|
17
|
+
|
|
18
|
+
// Foreground colors
|
|
19
|
+
red: "\x1b[31m",
|
|
20
|
+
green: "\x1b[32m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
22
|
+
blue: "\x1b[34m",
|
|
23
|
+
magenta: "\x1b[35m",
|
|
24
|
+
cyan: "\x1b[36m",
|
|
25
|
+
white: "\x1b[37m",
|
|
26
|
+
gray: "\x1b[90m",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
// ── Nerd Font glyphs (single-width) ────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export const icon = {
|
|
32
|
+
check: "\uf00c", // nf-fa-check
|
|
33
|
+
cross: "\uf00d", // nf-fa-close
|
|
34
|
+
book: "\uf02d", // nf-fa-book
|
|
35
|
+
branch: "\ue0a0", // Powerline branch
|
|
36
|
+
clock: "\uf017", // nf-fa-clock_o
|
|
37
|
+
warning: "\uf071", // nf-fa-warning
|
|
38
|
+
bolt: "\uf0e7", // nf-fa-bolt
|
|
39
|
+
brain: "\uf5dc", // nf-mdi-brain
|
|
40
|
+
search: "\uf002", // nf-fa-search
|
|
41
|
+
gear: "\uf013", // nf-fa-gear
|
|
42
|
+
database: "\uf1c0", // nf-fa-database
|
|
43
|
+
arrow: "\uf061", // nf-fa-arrow_right
|
|
44
|
+
dot: "\u00b7", // middle dot (standard unicode)
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
// ── Rule line ───────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const RULE_WIDTH = 42;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a horizontal rule with an optional inline title.
|
|
53
|
+
* e.g. "── Vector Memory ──────────────────────"
|
|
54
|
+
*/
|
|
55
|
+
export function rule(title?: string): string {
|
|
56
|
+
if (!title) {
|
|
57
|
+
return `${ansi.cyan}${"─".repeat(RULE_WIDTH)}${ansi.reset}`;
|
|
58
|
+
}
|
|
59
|
+
const label = ` ${ansi.bold}${title}${ansi.reset} `;
|
|
60
|
+
// "── " prefix = 3 visual chars
|
|
61
|
+
const prefix = `${ansi.cyan}── ${ansi.reset}`;
|
|
62
|
+
// Calculate remaining dashes (account for title visual length)
|
|
63
|
+
const remaining = RULE_WIDTH - 3 - title.length - 2; // 2 for spaces around title
|
|
64
|
+
const suffix = `${ansi.cyan}${"─".repeat(Math.max(1, remaining))}${ansi.reset}`;
|
|
65
|
+
return `${prefix}${label}${suffix}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── System message builder ──────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export interface MessageLine {
|
|
71
|
+
icon?: string;
|
|
72
|
+
iconColor?: string;
|
|
73
|
+
text: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build a user-facing systemMessage with horizontal rules and content lines.
|
|
78
|
+
*
|
|
79
|
+
* Output format:
|
|
80
|
+
* ── Title ──────────────────────────────
|
|
81
|
+
* icon text
|
|
82
|
+
* icon text
|
|
83
|
+
* ──────────────────────────────────────
|
|
84
|
+
*
|
|
85
|
+
* Prepends an empty line so the content starts below the hook label prefix.
|
|
86
|
+
*/
|
|
87
|
+
export function buildSystemMessage(
|
|
88
|
+
title: string,
|
|
89
|
+
lines: MessageLine[]
|
|
90
|
+
): string {
|
|
91
|
+
const parts = [
|
|
92
|
+
"", // push below "HookName says:" prefix
|
|
93
|
+
rule(title),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
if (line.icon) {
|
|
98
|
+
const color = line.iconColor ?? "";
|
|
99
|
+
const reset = line.iconColor ? ansi.reset : "";
|
|
100
|
+
parts.push(` ${color}${line.icon}${reset} ${line.text}`);
|
|
101
|
+
} else {
|
|
102
|
+
parts.push(` ${line.text}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
parts.push(rule());
|
|
107
|
+
return parts.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Diagnostic logging ──────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Log a diagnostic message to stderr when VECTOR_MEMORY_DEBUG=1.
|
|
114
|
+
*/
|
|
115
|
+
export function debug(label: string, message: string): void {
|
|
116
|
+
if (process.env.VECTOR_MEMORY_DEBUG !== "1") return;
|
|
117
|
+
console.error(
|
|
118
|
+
`${ansi.gray}[${label}]${ansi.reset} ${ansi.dim}${message}${ansi.reset}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Time formatting ─────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export function timeAgo(iso: string): string {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const then = new Date(iso).getTime();
|
|
127
|
+
if (Number.isNaN(then)) {
|
|
128
|
+
debug("timeAgo", `invalid ISO string: ${iso}`);
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
const seconds = Math.floor((now - then) / 1000);
|
|
132
|
+
if (seconds < 0) {
|
|
133
|
+
debug("timeAgo", `negative delta (${seconds}s) — clock skew or future timestamp`);
|
|
134
|
+
return "just now";
|
|
135
|
+
}
|
|
136
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
137
|
+
const minutes = Math.floor(seconds / 60);
|
|
138
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
139
|
+
const hours = Math.floor(minutes / 60);
|
|
140
|
+
if (hours < 24) return `${hours}h ago`;
|
|
141
|
+
const days = Math.floor(hours / 24);
|
|
142
|
+
return `${days}d ago`;
|
|
143
|
+
}
|