@aeriondyseti/vector-memory-mcp 2.2.2 → 2.2.6
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 +8 -7
- package/scripts/lancedb-extract.ts +111 -46
- package/scripts/migrate-from-lancedb.ts +2 -2
- package/scripts/smoke-test.ts +1 -1
- package/scripts/sync-version.ts +35 -0
- package/scripts/warmup.ts +2 -2
- 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/{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 +115 -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 +13 -10
- package/{src → server}/migration.ts +2 -2
- package/{src → server/transports}/http/mcp-transport.ts +2 -2
- package/{src → server/transports}/http/server.ts +34 -4
- package/{src → server/transports}/mcp/handlers.ts +5 -5
- package/server/transports/mcp/resources.ts +161 -0
- package/{src → server/transports}/mcp/server.ts +14 -3
- package/server/utils/formatting.ts +143 -0
- package/src/db/migrations.ts +0 -108
- /package/{src/types → server/core}/conversation.ts +0 -0
- /package/{src/services → server/core}/embeddings.service.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,115 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-migration step: remove vec0 virtual table entries from sqlite_master
|
|
5
|
+
* and drop their shadow tables using the sqlite3 CLI.
|
|
6
|
+
*
|
|
7
|
+
* Must run BEFORE bun:sqlite opens the database because:
|
|
8
|
+
* - bun:sqlite cannot modify sqlite_master (no writable_schema support)
|
|
9
|
+
* - DROP TABLE on a virtual table requires the extension module to be loaded
|
|
10
|
+
* - SQLite 3.51+ has defensive mode on by default, requiring .dbconfig override
|
|
11
|
+
*
|
|
12
|
+
* Safe to call on any database — it's a no-op if there are no vec0 tables.
|
|
13
|
+
*/
|
|
14
|
+
export function removeVec0Tables(dbPath: string): void {
|
|
15
|
+
const result = Bun.spawnSync({
|
|
16
|
+
cmd: ["sqlite3", dbPath],
|
|
17
|
+
stdin: new TextEncoder().encode(
|
|
18
|
+
[
|
|
19
|
+
".dbconfig defensive off",
|
|
20
|
+
".dbconfig writable_schema on",
|
|
21
|
+
// Drop shadow tables (regular tables, no extension needed)
|
|
22
|
+
"DROP TABLE IF EXISTS memories_vec_rowids;",
|
|
23
|
+
"DROP TABLE IF EXISTS memories_vec_chunks;",
|
|
24
|
+
"DROP TABLE IF EXISTS memories_vec_info;",
|
|
25
|
+
"DROP TABLE IF EXISTS memories_vec_vector_chunks00;",
|
|
26
|
+
"DROP TABLE IF EXISTS memories_vec_migration_tmp;",
|
|
27
|
+
"DROP TABLE IF EXISTS conversation_history_vec_rowids;",
|
|
28
|
+
"DROP TABLE IF EXISTS conversation_history_vec_chunks;",
|
|
29
|
+
"DROP TABLE IF EXISTS conversation_history_vec_info;",
|
|
30
|
+
"DROP TABLE IF EXISTS conversation_history_vec_vector_chunks00;",
|
|
31
|
+
"DROP TABLE IF EXISTS conversation_history_vec_migration_tmp;",
|
|
32
|
+
// Remove orphaned vec0 virtual table entries from schema
|
|
33
|
+
"DELETE FROM sqlite_master WHERE sql LIKE '%vec0%';",
|
|
34
|
+
].join("\n"),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
if (result.exitCode !== 0) {
|
|
38
|
+
const stderr = result.stderr.toString().trim();
|
|
39
|
+
if (!stderr.includes("unable to open database")) {
|
|
40
|
+
throw new Error(`vec0 cleanup failed: ${stderr}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run all schema migrations. Safe to call on every startup (uses IF NOT EXISTS).
|
|
47
|
+
*
|
|
48
|
+
* IMPORTANT: Call removeVec0Tables(dbPath) before opening the database
|
|
49
|
+
* with bun:sqlite if the database may contain vec0 virtual tables.
|
|
50
|
+
*/
|
|
51
|
+
export function runMigrations(db: Database): void {
|
|
52
|
+
// -- Memories --
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
content TEXT NOT NULL,
|
|
57
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
58
|
+
created_at INTEGER NOT NULL,
|
|
59
|
+
updated_at INTEGER NOT NULL,
|
|
60
|
+
superseded_by TEXT,
|
|
61
|
+
usefulness REAL NOT NULL DEFAULT 0.0,
|
|
62
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
last_accessed INTEGER
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
|
|
67
|
+
db.exec(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS memories_vec (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
vector BLOB NOT NULL
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
db.exec(`
|
|
75
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
76
|
+
id UNINDEXED,
|
|
77
|
+
content
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
|
|
81
|
+
// -- Conversation History --
|
|
82
|
+
db.exec(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS conversation_history (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
content TEXT NOT NULL,
|
|
86
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
87
|
+
created_at INTEGER NOT NULL,
|
|
88
|
+
session_id TEXT NOT NULL,
|
|
89
|
+
role TEXT NOT NULL,
|
|
90
|
+
message_index_start INTEGER NOT NULL,
|
|
91
|
+
message_index_end INTEGER NOT NULL,
|
|
92
|
+
project TEXT NOT NULL
|
|
93
|
+
)
|
|
94
|
+
`);
|
|
95
|
+
|
|
96
|
+
db.exec(`
|
|
97
|
+
CREATE TABLE IF NOT EXISTS conversation_history_vec (
|
|
98
|
+
id TEXT PRIMARY KEY,
|
|
99
|
+
vector BLOB NOT NULL
|
|
100
|
+
)
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
db.exec(`
|
|
104
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_fts USING fts5(
|
|
105
|
+
id UNINDEXED,
|
|
106
|
+
content
|
|
107
|
+
)
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
// -- Indexes --
|
|
111
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_session_id ON conversation_history(session_id)`);
|
|
112
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_project ON conversation_history(project)`);
|
|
113
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_role ON conversation_history(role)`);
|
|
114
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_created_at ON conversation_history(created_at)`);
|
|
115
|
+
}
|
|
@@ -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,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { loadConfig, parseCliArgs } from "./config/index.js";
|
|
4
|
-
import { connectToDatabase } from "./
|
|
5
|
-
import { MemoryRepository } from "./
|
|
6
|
-
import { ConversationRepository } from "./
|
|
7
|
-
import { EmbeddingsService } from "./
|
|
8
|
-
import { MemoryService } from "./
|
|
9
|
-
import { ConversationHistoryService } from "./
|
|
10
|
-
import { startServer } from "./mcp/server.js";
|
|
11
|
-
import { startHttpServer
|
|
4
|
+
import { connectToDatabase } from "./core/connection.js";
|
|
5
|
+
import { MemoryRepository } from "./core/memory.repository.js";
|
|
6
|
+
import { ConversationRepository } from "./core/conversation.repository.js";
|
|
7
|
+
import { EmbeddingsService } from "./core/embeddings.service.js";
|
|
8
|
+
import { MemoryService } from "./core/memory.service.js";
|
|
9
|
+
import { ConversationHistoryService } from "./core/conversation.service.js";
|
|
10
|
+
import { startServer } from "./transports/mcp/server.js";
|
|
11
|
+
import { startHttpServer } from "./transports/http/server.js";
|
|
12
12
|
import { isLanceDbDirectory, migrate, formatMigrationSummary } from "./migration.js";
|
|
13
13
|
|
|
14
14
|
async function runMigrate(args: string[]): Promise<void> {
|
|
@@ -56,7 +56,7 @@ async function main(): Promise<void> {
|
|
|
56
56
|
`[vector-memory-mcp] ⚠️ Legacy LanceDB data detected at ${config.dbPath}\n` +
|
|
57
57
|
` Your data must be migrated to the new SQLite format.\n` +
|
|
58
58
|
` Run: vector-memory-mcp migrate\n` +
|
|
59
|
-
` Or: bun run
|
|
59
|
+
` Or: bun run server/index.ts migrate\n`
|
|
60
60
|
);
|
|
61
61
|
process.exit(1);
|
|
62
62
|
}
|
|
@@ -69,6 +69,10 @@ async function main(): Promise<void> {
|
|
|
69
69
|
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
70
70
|
const memoryService = new MemoryService(repository, embeddings);
|
|
71
71
|
|
|
72
|
+
if (config.pluginMode) {
|
|
73
|
+
console.error("[vector-memory-mcp] Running in plugin mode");
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
// Conditionally initialize conversation history indexing
|
|
73
77
|
if (config.conversationHistory.enabled) {
|
|
74
78
|
const conversationRepository = new ConversationRepository(db);
|
|
@@ -88,7 +92,6 @@ async function main(): Promise<void> {
|
|
|
88
92
|
// Graceful shutdown handler
|
|
89
93
|
const shutdown = () => {
|
|
90
94
|
console.error("[vector-memory-mcp] Shutting down...");
|
|
91
|
-
removeLockfile();
|
|
92
95
|
if (httpStop) httpStop();
|
|
93
96
|
db.close();
|
|
94
97
|
process.exit(0);
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import { existsSync, statSync } from "fs";
|
|
13
13
|
import { resolve, dirname } from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
|
-
import { connectToDatabase } from "./
|
|
16
|
-
import { serializeVector } from "./
|
|
15
|
+
import { connectToDatabase } from "./core/connection.js";
|
|
16
|
+
import { serializeVector } from "./core/sqlite-utils.js";
|
|
17
17
|
|
|
18
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
|
|
@@ -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
|
+
import { MigrationService } from "../../core/migration.service.js";
|
|
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
|
});
|
|
@@ -243,6 +245,34 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
243
245
|
}
|
|
244
246
|
});
|
|
245
247
|
|
|
248
|
+
// Migrate from external memory database
|
|
249
|
+
app.post("/migrate", async (c) => {
|
|
250
|
+
try {
|
|
251
|
+
const body = await c.req.json().catch(() => null);
|
|
252
|
+
if (!body || typeof body !== "object") {
|
|
253
|
+
return c.json({ error: "Invalid or missing JSON body" }, 400);
|
|
254
|
+
}
|
|
255
|
+
const source = body.source;
|
|
256
|
+
|
|
257
|
+
if (!source || typeof source !== "string") {
|
|
258
|
+
return c.json({ error: "Missing or invalid 'source' field" }, 400);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const repository = memoryService.getRepository();
|
|
262
|
+
const migrationService = new MigrationService(
|
|
263
|
+
repository,
|
|
264
|
+
memoryService.getEmbeddings(),
|
|
265
|
+
repository.getDb(),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const result = await migrationService.migrate(source);
|
|
269
|
+
return c.json(result);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
272
|
+
return c.json({ error: message }, 500);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
246
276
|
// Get single memory
|
|
247
277
|
app.get("/memories/:id", async (c) => {
|
|
248
278
|
try {
|
|
@@ -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,161 @@
|
|
|
1
|
+
const MIGRATE_GUIDE = `# Migrating External Memory Databases
|
|
2
|
+
|
|
3
|
+
The vector-memory-mcp server exposes a \`POST /migrate\` HTTP endpoint that imports
|
|
4
|
+
memories from other database formats into the running instance. All imported
|
|
5
|
+
content is re-embedded with the server's current embedding model to guarantee
|
|
6
|
+
consistency.
|
|
7
|
+
|
|
8
|
+
## Endpoint
|
|
9
|
+
|
|
10
|
+
\`\`\`
|
|
11
|
+
POST http://<host>:<port>/migrate
|
|
12
|
+
Content-Type: application/json
|
|
13
|
+
|
|
14
|
+
{ "source": "/absolute/path/to/source/database" }
|
|
15
|
+
\`\`\`
|
|
16
|
+
|
|
17
|
+
## Discovering the Server Port
|
|
18
|
+
|
|
19
|
+
The HTTP server writes a lockfile at \`.vector-memory/server.lock\` in the
|
|
20
|
+
project's working directory. Read it to discover the current port:
|
|
21
|
+
|
|
22
|
+
\`\`\`json
|
|
23
|
+
{ "port": 3271, "pid": 12345 }
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
## Supported Source Formats
|
|
27
|
+
|
|
28
|
+
The endpoint auto-detects the source format from the path provided.
|
|
29
|
+
|
|
30
|
+
### 1. LanceDB Directory
|
|
31
|
+
Provide the path to a LanceDB data directory (contains \`.lance\` files or
|
|
32
|
+
\`_versions\`/\`_indices\` subdirectories). Both memories and conversation
|
|
33
|
+
history are imported.
|
|
34
|
+
|
|
35
|
+
\`\`\`json
|
|
36
|
+
{ "source": "/path/to/project/.vector-memory" }
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
### 2. Own SQLite (Current or Older Schema)
|
|
40
|
+
Provide the path to a \`.db\` file that was created by any version of
|
|
41
|
+
vector-memory-mcp. The migrator handles missing columns (e.g. \`usefulness\`,
|
|
42
|
+
\`access_count\`) by using sensible defaults. Both memories and conversation
|
|
43
|
+
history are imported.
|
|
44
|
+
|
|
45
|
+
\`\`\`json
|
|
46
|
+
{ "source": "/path/to/old-project/.vector-memory/memories.db" }
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
### 3. CCCMemory SQLite
|
|
50
|
+
Provide the path to a CCCMemory database. The migrator extracts from the
|
|
51
|
+
\`decisions\`, \`mistakes\`, \`methodologies\`, \`research_findings\`,
|
|
52
|
+
\`solution_patterns\`, and \`working_memory\` tables. Each record is tagged
|
|
53
|
+
with \`source_type: "cccmemory"\` and the appropriate \`memory_type\` in
|
|
54
|
+
metadata.
|
|
55
|
+
|
|
56
|
+
\`\`\`json
|
|
57
|
+
{ "source": "/path/to/cccmemory.db" }
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
### 4. MCP Memory Service SQLite
|
|
61
|
+
Provide the path to an mcp-memory-service database. Memories with
|
|
62
|
+
\`deleted_at IS NULL\` are imported. Tags and memory type are preserved in
|
|
63
|
+
metadata.
|
|
64
|
+
|
|
65
|
+
\`\`\`json
|
|
66
|
+
{ "source": "/path/to/mcp-memory-service.db" }
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
### 5. MIF JSON (Shodh Memory Interchange Format)
|
|
70
|
+
Provide the path to a \`.json\` file exported from Shodh Memory. The file must
|
|
71
|
+
contain a top-level \`memories\` array. Memory type, tags, entities, and source
|
|
72
|
+
metadata are preserved.
|
|
73
|
+
|
|
74
|
+
\`\`\`json
|
|
75
|
+
{ "source": "/path/to/export.mif.json" }
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
## Response
|
|
79
|
+
|
|
80
|
+
The endpoint returns a JSON summary upon completion:
|
|
81
|
+
|
|
82
|
+
\`\`\`json
|
|
83
|
+
{
|
|
84
|
+
"source": "/path/to/source",
|
|
85
|
+
"format": "own-sqlite",
|
|
86
|
+
"memoriesImported": 142,
|
|
87
|
+
"memoriesSkipped": 3,
|
|
88
|
+
"conversationsImported": 0,
|
|
89
|
+
"conversationsSkipped": 0,
|
|
90
|
+
"errors": [],
|
|
91
|
+
"durationMs": 8320
|
|
92
|
+
}
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
- **memoriesImported**: Number of new memories written to the database.
|
|
96
|
+
- **memoriesSkipped**: Records skipped because a memory with the same ID
|
|
97
|
+
already exists (safe for idempotent re-runs).
|
|
98
|
+
- **conversationsImported / conversationsSkipped**: Same, for conversation
|
|
99
|
+
history chunks (LanceDB and own-sqlite formats only).
|
|
100
|
+
- **errors**: Per-record errors that did not abort the migration.
|
|
101
|
+
- **durationMs**: Wall-clock time for the entire operation.
|
|
102
|
+
|
|
103
|
+
## Important Notes
|
|
104
|
+
|
|
105
|
+
- **Re-embedding**: All content is re-embedded regardless of the source format.
|
|
106
|
+
This ensures vector consistency with the server's current model but means the
|
|
107
|
+
operation can take time for large databases (~50ms per record).
|
|
108
|
+
- **Idempotent**: Running the same migration twice is safe. Duplicate IDs are
|
|
109
|
+
skipped.
|
|
110
|
+
- **Non-destructive**: The source database is opened read-only and is never
|
|
111
|
+
modified.
|
|
112
|
+
- **Batched writes**: Records are inserted in batches of 100 within
|
|
113
|
+
transactions. If the process is interrupted, already-committed batches are
|
|
114
|
+
durable.
|
|
115
|
+
- **Error isolation**: A single bad record does not abort the migration. Check
|
|
116
|
+
the \`errors\` array in the response for any per-record failures.
|
|
117
|
+
|
|
118
|
+
## Workflow Example
|
|
119
|
+
|
|
120
|
+
1. Locate the source database file or directory.
|
|
121
|
+
2. Read \`.vector-memory/server.lock\` to get the port.
|
|
122
|
+
3. Send the migrate request:
|
|
123
|
+
\`\`\`bash
|
|
124
|
+
curl -X POST http://127.0.0.1:3271/migrate \\
|
|
125
|
+
-H "Content-Type: application/json" \\
|
|
126
|
+
-d '{"source": "/path/to/old/memories.db"}'
|
|
127
|
+
\`\`\`
|
|
128
|
+
4. Inspect the response summary.
|
|
129
|
+
5. Verify imported memories with a search:
|
|
130
|
+
\`\`\`bash
|
|
131
|
+
curl -X POST http://127.0.0.1:3271/search \\
|
|
132
|
+
-H "Content-Type: application/json" \\
|
|
133
|
+
-d '{"query": "test query", "limit": 5}'
|
|
134
|
+
\`\`\`
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
export const resources = [
|
|
138
|
+
{
|
|
139
|
+
uri: "vector-memory://guides/migrate",
|
|
140
|
+
name: "Migration Guide",
|
|
141
|
+
description:
|
|
142
|
+
"How to use the POST /migrate HTTP endpoint to import memories from external database formats (LanceDB, older SQLite, CCCMemory, MCP Memory Service, MIF JSON) into the running vector-memory instance.",
|
|
143
|
+
mimeType: "text/markdown",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const RESOURCE_CONTENT: Record<string, string> = {
|
|
148
|
+
"vector-memory://guides/migrate": MIGRATE_GUIDE,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export function readResource(uri: string): {
|
|
152
|
+
contents: Array<{ uri: string; mimeType: string; text: string }>;
|
|
153
|
+
} {
|
|
154
|
+
const text = RESOURCE_CONTENT[uri];
|
|
155
|
+
if (!text) {
|
|
156
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
contents: [{ uri, mimeType: "text/markdown", text }],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -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
|
+
}
|