@aeriondyseti/vector-memory-mcp 0.9.0-dev.3 → 0.9.0-dev.5
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 -21
- package/package.json +8 -12
- package/src/config/index.ts +75 -0
- package/src/db/connection.ts +11 -0
- package/src/db/memory.repository.ts +205 -0
- package/src/db/schema.ts +42 -0
- package/src/http/mcp-transport.ts +255 -0
- package/src/http/server.ts +191 -0
- package/src/index.ts +70 -0
- package/src/mcp/handlers.ts +279 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +284 -0
- package/src/services/embeddings.service.ts +48 -0
- package/src/services/memory.service.ts +266 -0
- package/src/types/memory.ts +43 -0
- package/dist/scripts/publish.d.ts +0 -13
- package/dist/scripts/publish.d.ts.map +0 -1
- package/dist/scripts/publish.js +0 -56
- package/dist/scripts/publish.js.map +0 -1
- package/dist/scripts/test-runner.d.ts +0 -9
- package/dist/scripts/test-runner.d.ts.map +0 -1
- package/dist/scripts/test-runner.js +0 -61
- package/dist/scripts/test-runner.js.map +0 -1
- package/dist/scripts/warmup.d.ts +0 -8
- package/dist/scripts/warmup.d.ts.map +0 -1
- package/dist/scripts/warmup.js +0 -61
- package/dist/scripts/warmup.js.map +0 -1
- package/dist/src/config/index.d.ts +0 -23
- package/dist/src/config/index.d.ts.map +0 -1
- package/dist/src/config/index.js +0 -46
- package/dist/src/config/index.js.map +0 -1
- package/dist/src/db/connection.d.ts +0 -3
- package/dist/src/db/connection.d.ts.map +0 -1
- package/dist/src/db/connection.js +0 -10
- package/dist/src/db/connection.js.map +0 -1
- package/dist/src/db/memory.repository.d.ts +0 -13
- package/dist/src/db/memory.repository.d.ts.map +0 -1
- package/dist/src/db/memory.repository.js +0 -97
- package/dist/src/db/memory.repository.js.map +0 -1
- package/dist/src/db/schema.d.ts +0 -4
- package/dist/src/db/schema.d.ts.map +0 -1
- package/dist/src/db/schema.js +0 -12
- package/dist/src/db/schema.js.map +0 -1
- package/dist/src/http/mcp-transport.d.ts +0 -19
- package/dist/src/http/mcp-transport.d.ts.map +0 -1
- package/dist/src/http/mcp-transport.js +0 -191
- package/dist/src/http/mcp-transport.js.map +0 -1
- package/dist/src/http/server.d.ts +0 -12
- package/dist/src/http/server.d.ts.map +0 -1
- package/dist/src/http/server.js +0 -168
- package/dist/src/http/server.js.map +0 -1
- package/dist/src/index.d.ts +0 -3
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -59
- package/dist/src/index.js.map +0 -1
- package/dist/src/mcp/handlers.d.ts +0 -11
- package/dist/src/mcp/handlers.d.ts.map +0 -1
- package/dist/src/mcp/handlers.js +0 -175
- package/dist/src/mcp/handlers.js.map +0 -1
- package/dist/src/mcp/server.d.ts +0 -5
- package/dist/src/mcp/server.d.ts.map +0 -1
- package/dist/src/mcp/server.js +0 -22
- package/dist/src/mcp/server.js.map +0 -1
- package/dist/src/mcp/tools.d.ts +0 -9
- package/dist/src/mcp/tools.d.ts.map +0 -1
- package/dist/src/mcp/tools.js +0 -243
- package/dist/src/mcp/tools.js.map +0 -1
- package/dist/src/services/embeddings.service.d.ts +0 -12
- package/dist/src/services/embeddings.service.d.ts.map +0 -1
- package/dist/src/services/embeddings.service.js +0 -37
- package/dist/src/services/embeddings.service.js.map +0 -1
- package/dist/src/services/memory.service.d.ts +0 -31
- package/dist/src/services/memory.service.d.ts.map +0 -1
- package/dist/src/services/memory.service.js +0 -131
- package/dist/src/services/memory.service.js.map +0 -1
- package/dist/src/types/memory.d.ts +0 -17
- package/dist/src/types/memory.d.ts.map +0 -1
- package/dist/src/types/memory.js +0 -15
- package/dist/src/types/memory.js.map +0 -1
package/README.md
CHANGED
|
@@ -23,28 +23,21 @@ A local-first MCP server that provides vector-based memory storage. Uses local e
|
|
|
23
23
|
|
|
24
24
|
### Prerequisites
|
|
25
25
|
|
|
26
|
-
- [Bun](https://bun.sh/) 1.0+
|
|
26
|
+
- [Bun](https://bun.sh/) 1.0+
|
|
27
27
|
- An MCP-compatible client (Claude Code, Claude Desktop, etc.)
|
|
28
28
|
|
|
29
29
|
### Install
|
|
30
30
|
|
|
31
|
-
**With Bun (recommended):**
|
|
32
31
|
```bash
|
|
33
32
|
bun install -g @aeriondyseti/vector-memory-mcp
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
**With npm/Node.js:**
|
|
37
|
-
```bash
|
|
38
|
-
npm install -g @aeriondyseti/vector-memory-mcp
|
|
39
|
-
```
|
|
40
|
-
|
|
41
35
|
> First install downloads ML models (~90MB). This may take a minute.
|
|
42
36
|
|
|
43
37
|
### Configure
|
|
44
38
|
|
|
45
39
|
Add to your MCP client config (e.g., `~/.claude/settings.json`):
|
|
46
40
|
|
|
47
|
-
**With Bun:**
|
|
48
41
|
```json
|
|
49
42
|
{
|
|
50
43
|
"mcpServers": {
|
|
@@ -57,19 +50,6 @@ Add to your MCP client config (e.g., `~/.claude/settings.json`):
|
|
|
57
50
|
}
|
|
58
51
|
```
|
|
59
52
|
|
|
60
|
-
**With Node.js:**
|
|
61
|
-
```json
|
|
62
|
-
{
|
|
63
|
-
"mcpServers": {
|
|
64
|
-
"vector-memory": {
|
|
65
|
-
"type": "stdio",
|
|
66
|
-
"command": "npx",
|
|
67
|
-
"args": ["-y", "@aeriondyseti/vector-memory-mcp"]
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
53
|
### Use
|
|
74
54
|
|
|
75
55
|
Restart your MCP client. You now have access to:
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeriondyseti/vector-memory-mcp",
|
|
3
|
-
"version": "0.9.0-dev.
|
|
3
|
+
"version": "0.9.0-dev.5",
|
|
4
4
|
"description": "A zero-configuration RAG memory server for MCP clients",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "src/index.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"vector-memory-mcp": "
|
|
8
|
+
"vector-memory-mcp": "src/index.ts"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
11
|
+
"src",
|
|
12
12
|
"scripts",
|
|
13
13
|
"hooks",
|
|
14
14
|
"README.md",
|
|
@@ -24,11 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"homepage": "https://github.com/AerionDyseti/vector-memory-mcp#readme",
|
|
26
26
|
"scripts": {
|
|
27
|
-
"start": "
|
|
28
|
-
"dev": "bun run
|
|
29
|
-
"
|
|
30
|
-
"build": "tsc",
|
|
31
|
-
"prebuild": "rm -rf dist",
|
|
27
|
+
"start": "bun run src/index.ts",
|
|
28
|
+
"dev": "bun --watch run src/index.ts",
|
|
29
|
+
"build": "bun run typecheck",
|
|
32
30
|
"typecheck": "bunx tsc --noEmit",
|
|
33
31
|
"test": "bun run scripts/test-runner.ts",
|
|
34
32
|
"test:raw": "bun test --preload ./tests/preload.ts",
|
|
@@ -39,8 +37,7 @@
|
|
|
39
37
|
"postinstall": "bun run scripts/warmup.ts",
|
|
40
38
|
"publish:check": "bun run scripts/publish.ts --dry-run",
|
|
41
39
|
"publish:npm": "bun run scripts/publish.ts",
|
|
42
|
-
"publish:dev": "npm publish --access public --tag dev"
|
|
43
|
-
"prepublishOnly": "bun run build"
|
|
40
|
+
"publish:dev": "npm publish --access public --tag dev"
|
|
44
41
|
},
|
|
45
42
|
"keywords": [
|
|
46
43
|
"mcp",
|
|
@@ -51,7 +48,6 @@
|
|
|
51
48
|
],
|
|
52
49
|
"license": "MIT",
|
|
53
50
|
"dependencies": {
|
|
54
|
-
"@hono/node-server": "^1.19.7",
|
|
55
51
|
"@huggingface/transformers": "^3.8.0",
|
|
56
52
|
"@lancedb/lancedb": "^0.22.3",
|
|
57
53
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import arg from "arg";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export type TransportMode = "stdio" | "http" | "both";
|
|
5
|
+
|
|
6
|
+
export interface Config {
|
|
7
|
+
dbPath: string;
|
|
8
|
+
embeddingModel: string;
|
|
9
|
+
embeddingDimension: number;
|
|
10
|
+
httpPort: number;
|
|
11
|
+
httpHost: string;
|
|
12
|
+
enableHttp: boolean;
|
|
13
|
+
transportMode: TransportMode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConfigOverrides {
|
|
17
|
+
dbPath?: string;
|
|
18
|
+
httpPort?: number;
|
|
19
|
+
enableHttp?: boolean;
|
|
20
|
+
transportMode?: TransportMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Defaults - always use repo-local .vector-memory folder
|
|
24
|
+
const DEFAULT_DB_PATH = join(process.cwd(), ".vector-memory", "memories.db");
|
|
25
|
+
const DEFAULT_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
26
|
+
const DEFAULT_EMBEDDING_DIMENSION = 384;
|
|
27
|
+
const DEFAULT_HTTP_PORT = 3271;
|
|
28
|
+
const DEFAULT_HTTP_HOST = "127.0.0.1";
|
|
29
|
+
|
|
30
|
+
function resolvePath(path: string): string {
|
|
31
|
+
return path.startsWith("/") ? path : join(process.cwd(), path);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadConfig(overrides: ConfigOverrides = {}): Config {
|
|
35
|
+
const transportMode = overrides.transportMode ?? "stdio";
|
|
36
|
+
// HTTP enabled by default (needed for hooks), can disable with --no-http
|
|
37
|
+
const enableHttp = overrides.enableHttp ?? true;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
dbPath: resolvePath(overrides.dbPath ?? DEFAULT_DB_PATH),
|
|
41
|
+
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
|
42
|
+
embeddingDimension: DEFAULT_EMBEDDING_DIMENSION,
|
|
43
|
+
httpPort: overrides.httpPort ?? DEFAULT_HTTP_PORT,
|
|
44
|
+
httpHost: DEFAULT_HTTP_HOST,
|
|
45
|
+
enableHttp,
|
|
46
|
+
transportMode,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse CLI arguments into config overrides.
|
|
52
|
+
*/
|
|
53
|
+
export function parseCliArgs(argv: string[]): ConfigOverrides {
|
|
54
|
+
const args = arg(
|
|
55
|
+
{
|
|
56
|
+
"--db-file": String,
|
|
57
|
+
"--port": Number,
|
|
58
|
+
"--no-http": Boolean,
|
|
59
|
+
|
|
60
|
+
// Aliases
|
|
61
|
+
"-d": "--db-file",
|
|
62
|
+
"-p": "--port",
|
|
63
|
+
},
|
|
64
|
+
{ argv, permissive: true }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
dbPath: args["--db-file"],
|
|
69
|
+
httpPort: args["--port"],
|
|
70
|
+
enableHttp: args["--no-http"] ? false : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default config for imports that don't use CLI args
|
|
75
|
+
export const config = loadConfig();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
2
|
+
import { mkdirSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
|
|
5
|
+
export async function connectToDatabase(dbPath: string): Promise<lancedb.Connection> {
|
|
6
|
+
// Ensure directory exists
|
|
7
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
8
|
+
|
|
9
|
+
const db = await lancedb.connect(dbPath);
|
|
10
|
+
return db;
|
|
11
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
2
|
+
import { Index, rerankers, type Table } from "@lancedb/lancedb";
|
|
3
|
+
import { TABLE_NAME, memorySchema } from "./schema.js";
|
|
4
|
+
import {
|
|
5
|
+
type Memory,
|
|
6
|
+
type HybridRow,
|
|
7
|
+
DELETED_TOMBSTONE,
|
|
8
|
+
} from "../types/memory.js";
|
|
9
|
+
|
|
10
|
+
export class MemoryRepository {
|
|
11
|
+
// Mutex for FTS index creation - ensures only one index creation runs at a time
|
|
12
|
+
// Once set, this promise is never cleared (FTS index persists in the database)
|
|
13
|
+
private ftsIndexPromise: Promise<void> | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(private db: lancedb.Connection) { }
|
|
16
|
+
|
|
17
|
+
private async getTable() {
|
|
18
|
+
const names = await this.db.tableNames();
|
|
19
|
+
if (names.includes(TABLE_NAME)) {
|
|
20
|
+
return await this.db.openTable(TABLE_NAME);
|
|
21
|
+
}
|
|
22
|
+
// Create with empty data to initialize schema
|
|
23
|
+
return await this.db.createTable(TABLE_NAME, [], { schema: memorySchema });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ensures the FTS index exists on the content column.
|
|
28
|
+
* Uses a mutex pattern to prevent concurrent index creation.
|
|
29
|
+
* The key insight: we must capture the promise BEFORE any await.
|
|
30
|
+
*/
|
|
31
|
+
private ensureFtsIndex(): Promise<void> {
|
|
32
|
+
// If there's already a pending or completed index creation, return that promise
|
|
33
|
+
if (this.ftsIndexPromise) {
|
|
34
|
+
return this.ftsIndexPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Synchronously set the promise BEFORE any await
|
|
38
|
+
// This is critical for proper mutex behavior in JS async code
|
|
39
|
+
this.ftsIndexPromise = this.createFtsIndexIfNeeded().catch((error) => {
|
|
40
|
+
// Reset on error so the next call can retry
|
|
41
|
+
this.ftsIndexPromise = null;
|
|
42
|
+
throw error;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return this.ftsIndexPromise;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates the FTS index if it doesn't already exist.
|
|
50
|
+
* Gets its own table reference to ensure consistent index state.
|
|
51
|
+
*/
|
|
52
|
+
private async createFtsIndexIfNeeded(): Promise<void> {
|
|
53
|
+
const table = await this.getTable();
|
|
54
|
+
const indices = await table.listIndices();
|
|
55
|
+
const hasFtsIndex = indices.some(
|
|
56
|
+
(idx) => idx.columns.includes("content") && idx.indexType === "FTS"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!hasFtsIndex) {
|
|
60
|
+
await table.createIndex("content", {
|
|
61
|
+
config: Index.fts(),
|
|
62
|
+
});
|
|
63
|
+
// Wait for the index to be fully created and available
|
|
64
|
+
await table.waitForIndex(["content_idx"], 30);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Converts a raw LanceDB row to a Memory object.
|
|
70
|
+
*/
|
|
71
|
+
private rowToMemory(row: Record<string, unknown>): Memory {
|
|
72
|
+
// Handle Arrow Vector type conversion
|
|
73
|
+
// LanceDB returns an Arrow Vector object which is iterable but not an array
|
|
74
|
+
const vectorData = row.vector as unknown;
|
|
75
|
+
const embedding = Array.isArray(vectorData)
|
|
76
|
+
? vectorData
|
|
77
|
+
: Array.from(vectorData as Iterable<number>) as number[];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: row.id as string,
|
|
81
|
+
content: row.content as string,
|
|
82
|
+
embedding,
|
|
83
|
+
metadata: JSON.parse(row.metadata as string),
|
|
84
|
+
createdAt: new Date(row.created_at as number),
|
|
85
|
+
updatedAt: new Date(row.updated_at as number),
|
|
86
|
+
supersededBy: row.superseded_by as string | null,
|
|
87
|
+
usefulness: (row.usefulness as number) ?? 0,
|
|
88
|
+
accessCount: (row.access_count as number) ?? 0,
|
|
89
|
+
lastAccessed: row.last_accessed
|
|
90
|
+
? new Date(row.last_accessed as number)
|
|
91
|
+
: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async insert(memory: Memory): Promise<void> {
|
|
96
|
+
const table = await this.getTable();
|
|
97
|
+
await table.add([
|
|
98
|
+
{
|
|
99
|
+
id: memory.id,
|
|
100
|
+
vector: memory.embedding,
|
|
101
|
+
content: memory.content,
|
|
102
|
+
metadata: JSON.stringify(memory.metadata),
|
|
103
|
+
created_at: memory.createdAt.getTime(),
|
|
104
|
+
updated_at: memory.updatedAt.getTime(),
|
|
105
|
+
superseded_by: memory.supersededBy,
|
|
106
|
+
usefulness: memory.usefulness,
|
|
107
|
+
access_count: memory.accessCount,
|
|
108
|
+
last_accessed: memory.lastAccessed?.getTime() ?? null,
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async upsert(memory: Memory): Promise<void> {
|
|
114
|
+
const table = await this.getTable();
|
|
115
|
+
const existing = await table.query().where(`id = '${memory.id}'`).limit(1).toArray();
|
|
116
|
+
|
|
117
|
+
if (existing.length === 0) {
|
|
118
|
+
return await this.insert(memory);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await table.update({
|
|
122
|
+
where: `id = '${memory.id}'`,
|
|
123
|
+
values: {
|
|
124
|
+
vector: memory.embedding,
|
|
125
|
+
content: memory.content,
|
|
126
|
+
metadata: JSON.stringify(memory.metadata),
|
|
127
|
+
created_at: memory.createdAt.getTime(),
|
|
128
|
+
updated_at: memory.updatedAt.getTime(),
|
|
129
|
+
superseded_by: memory.supersededBy,
|
|
130
|
+
usefulness: memory.usefulness,
|
|
131
|
+
access_count: memory.accessCount,
|
|
132
|
+
last_accessed: memory.lastAccessed?.getTime() ?? null,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async findById(id: string): Promise<Memory | null> {
|
|
138
|
+
const table = await this.getTable();
|
|
139
|
+
const results = await table.query().where(`id = '${id}'`).limit(1).toArray();
|
|
140
|
+
|
|
141
|
+
if (results.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.rowToMemory(results[0] as Record<string, unknown>);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async markDeleted(id: string): Promise<boolean> {
|
|
149
|
+
const table = await this.getTable();
|
|
150
|
+
|
|
151
|
+
// Verify existence first to match previous behavior (return false if not found)
|
|
152
|
+
const existing = await table.query().where(`id = '${id}'`).limit(1).toArray();
|
|
153
|
+
if (existing.length === 0) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
await table.update({
|
|
159
|
+
where: `id = '${id}'`,
|
|
160
|
+
values: {
|
|
161
|
+
superseded_by: DELETED_TOMBSTONE,
|
|
162
|
+
updated_at: now,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Performs hybrid search combining vector similarity and full-text search.
|
|
171
|
+
* Uses RRF (Reciprocal Rank Fusion) to combine rankings from both search methods.
|
|
172
|
+
*
|
|
173
|
+
* @param embedding - Query embedding vector
|
|
174
|
+
* @param query - Text query for full-text search
|
|
175
|
+
* @param limit - Maximum number of results to return
|
|
176
|
+
* @returns Array of HybridRow containing full Memory data plus RRF score
|
|
177
|
+
*/
|
|
178
|
+
async findHybrid(embedding: number[], query: string, limit: number): Promise<HybridRow[]> {
|
|
179
|
+
// Ensure FTS index exists (with mutex to prevent concurrent creation)
|
|
180
|
+
// This must happen BEFORE getTable to ensure proper mutex behavior
|
|
181
|
+
await this.ensureFtsIndex();
|
|
182
|
+
|
|
183
|
+
const table = await this.getTable();
|
|
184
|
+
|
|
185
|
+
// Create RRF reranker with k=60 (standard parameter)
|
|
186
|
+
const reranker = await rerankers.RRFReranker.create(60);
|
|
187
|
+
|
|
188
|
+
// Perform hybrid search: combine vector search and full-text search
|
|
189
|
+
const results = await table
|
|
190
|
+
.query()
|
|
191
|
+
.nearestTo(embedding)
|
|
192
|
+
.fullTextSearch(query)
|
|
193
|
+
.rerank(reranker)
|
|
194
|
+
.limit(limit)
|
|
195
|
+
.toArray();
|
|
196
|
+
|
|
197
|
+
return results.map((row) => {
|
|
198
|
+
const memory = this.rowToMemory(row as Record<string, unknown>);
|
|
199
|
+
return {
|
|
200
|
+
...memory,
|
|
201
|
+
rrfScore: (row._relevance_score as number) ?? 0,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Schema,
|
|
3
|
+
Field,
|
|
4
|
+
FixedSizeList,
|
|
5
|
+
Float32,
|
|
6
|
+
Utf8,
|
|
7
|
+
Timestamp,
|
|
8
|
+
TimeUnit,
|
|
9
|
+
Int32,
|
|
10
|
+
} from "apache-arrow";
|
|
11
|
+
|
|
12
|
+
export const TABLE_NAME = "memories";
|
|
13
|
+
|
|
14
|
+
export const memorySchema = new Schema([
|
|
15
|
+
new Field("id", new Utf8(), false),
|
|
16
|
+
new Field(
|
|
17
|
+
"vector",
|
|
18
|
+
new FixedSizeList(384, new Field("item", new Float32())),
|
|
19
|
+
false
|
|
20
|
+
),
|
|
21
|
+
new Field("content", new Utf8(), false),
|
|
22
|
+
new Field("metadata", new Utf8(), false), // JSON string
|
|
23
|
+
new Field(
|
|
24
|
+
"created_at",
|
|
25
|
+
new Timestamp(TimeUnit.MILLISECOND),
|
|
26
|
+
false
|
|
27
|
+
),
|
|
28
|
+
new Field(
|
|
29
|
+
"updated_at",
|
|
30
|
+
new Timestamp(TimeUnit.MILLISECOND),
|
|
31
|
+
false
|
|
32
|
+
),
|
|
33
|
+
new Field("superseded_by", new Utf8(), true), // Nullable
|
|
34
|
+
new Field("usefulness", new Float32(), false),
|
|
35
|
+
new Field("access_count", new Int32(), false),
|
|
36
|
+
new Field(
|
|
37
|
+
"last_accessed",
|
|
38
|
+
new Timestamp(TimeUnit.MILLISECOND),
|
|
39
|
+
true
|
|
40
|
+
),
|
|
41
|
+
]);
|
|
42
|
+
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP HTTP Transport Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides StreamableHTTP transport for MCP over HTTP.
|
|
5
|
+
* and other HTTP-based MCP clients to connect to the memory server.
|
|
6
|
+
*
|
|
7
|
+
* This implementation handles the MCP protocol directly using Hono's streaming
|
|
8
|
+
* capabilities, since StreamableHTTPServerTransport expects Node.js req/res objects.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Hono } from "hono";
|
|
12
|
+
import { streamSSE } from "hono/streaming";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import {
|
|
16
|
+
CallToolRequestSchema,
|
|
17
|
+
ListToolsRequestSchema,
|
|
18
|
+
type JSONRPCMessage,
|
|
19
|
+
type JSONRPCRequest,
|
|
20
|
+
type JSONRPCNotification,
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
23
|
+
|
|
24
|
+
import { tools } from "../mcp/tools.js";
|
|
25
|
+
import { handleToolCall } from "../mcp/handlers.js";
|
|
26
|
+
import type { MemoryService } from "../services/memory.service.js";
|
|
27
|
+
|
|
28
|
+
interface Session {
|
|
29
|
+
server: Server;
|
|
30
|
+
serverTransport: InstanceType<typeof InMemoryTransport>;
|
|
31
|
+
clientTransport: InstanceType<typeof InMemoryTransport>;
|
|
32
|
+
pendingResponses: Map<string | number, (response: JSONRPCMessage) => void>;
|
|
33
|
+
sseClients: Set<(message: JSONRPCMessage) => void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates MCP routes for a Hono app.
|
|
38
|
+
*
|
|
39
|
+
* Uses InMemoryTransport internally and bridges to HTTP/SSE manually,
|
|
40
|
+
* since StreamableHTTPServerTransport requires Node.js req/res objects.
|
|
41
|
+
*/
|
|
42
|
+
export function createMcpRoutes(memoryService: MemoryService): Hono {
|
|
43
|
+
const app = new Hono();
|
|
44
|
+
|
|
45
|
+
// Store active sessions by session ID
|
|
46
|
+
const sessions: Map<string, Session> = new Map();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new MCP server instance configured with memory tools.
|
|
50
|
+
*/
|
|
51
|
+
async function createSession(): Promise<Session> {
|
|
52
|
+
const server = new Server(
|
|
53
|
+
{ name: "vector-memory-mcp", version: "0.6.0" },
|
|
54
|
+
{ capabilities: { tools: {} } }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
58
|
+
return { tools };
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
62
|
+
const { name, arguments: args } = request.params;
|
|
63
|
+
return handleToolCall(name, args, memoryService);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Create linked in-memory transports
|
|
67
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
68
|
+
|
|
69
|
+
// Connect server to its transport
|
|
70
|
+
await server.connect(serverTransport);
|
|
71
|
+
|
|
72
|
+
const session: Session = {
|
|
73
|
+
server,
|
|
74
|
+
serverTransport,
|
|
75
|
+
clientTransport,
|
|
76
|
+
pendingResponses: new Map(),
|
|
77
|
+
sseClients: new Set(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Handle messages from server (responses and notifications)
|
|
81
|
+
clientTransport.onmessage = (message: JSONRPCMessage) => {
|
|
82
|
+
// Check if this is a response to a pending request
|
|
83
|
+
if ("id" in message && message.id !== undefined) {
|
|
84
|
+
const resolver = session.pendingResponses.get(message.id);
|
|
85
|
+
if (resolver) {
|
|
86
|
+
resolver(message);
|
|
87
|
+
session.pendingResponses.delete(message.id);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Otherwise, broadcast to SSE clients (notifications)
|
|
93
|
+
for (const sendToClient of session.sseClients) {
|
|
94
|
+
sendToClient(message);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return session;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle POST requests - session initialization and message handling
|
|
103
|
+
*/
|
|
104
|
+
app.post("/mcp", async (c) => {
|
|
105
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
106
|
+
const body = await c.req.json();
|
|
107
|
+
|
|
108
|
+
let session: Session | undefined;
|
|
109
|
+
let newSessionId: string | undefined;
|
|
110
|
+
|
|
111
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
112
|
+
// Reuse existing session
|
|
113
|
+
session = sessions.get(sessionId)!;
|
|
114
|
+
} else if (isInitializeRequest(body)) {
|
|
115
|
+
// New session initialization
|
|
116
|
+
newSessionId = randomUUID();
|
|
117
|
+
session = await createSession();
|
|
118
|
+
sessions.set(newSessionId, session);
|
|
119
|
+
console.error(`[vector-memory-mcp] MCP session initialized: ${newSessionId}`);
|
|
120
|
+
} else {
|
|
121
|
+
// Invalid request - no session ID and not an initialize request
|
|
122
|
+
return c.json(
|
|
123
|
+
{
|
|
124
|
+
jsonrpc: "2.0",
|
|
125
|
+
error: {
|
|
126
|
+
code: -32000,
|
|
127
|
+
message: "Invalid session. Send initialize request without session ID to start.",
|
|
128
|
+
},
|
|
129
|
+
id: body.id ?? null,
|
|
130
|
+
},
|
|
131
|
+
400
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Send message to server and wait for response
|
|
136
|
+
const response = await sendAndWaitForResponse(session, body);
|
|
137
|
+
|
|
138
|
+
// Include session ID header for new sessions
|
|
139
|
+
if (newSessionId) {
|
|
140
|
+
c.header("mcp-session-id", newSessionId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return c.json(response);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handle GET requests - SSE stream for server-to-client notifications
|
|
148
|
+
*/
|
|
149
|
+
app.get("/mcp", async (c) => {
|
|
150
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
151
|
+
|
|
152
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
153
|
+
return c.json(
|
|
154
|
+
{
|
|
155
|
+
jsonrpc: "2.0",
|
|
156
|
+
error: { code: -32000, message: "Invalid or missing session ID" },
|
|
157
|
+
id: null,
|
|
158
|
+
},
|
|
159
|
+
400
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const session = sessions.get(sessionId)!;
|
|
164
|
+
|
|
165
|
+
return streamSSE(c, async (stream) => {
|
|
166
|
+
// Register this SSE client
|
|
167
|
+
const sendMessage = (message: JSONRPCMessage) => {
|
|
168
|
+
stream.writeSSE({
|
|
169
|
+
data: JSON.stringify(message),
|
|
170
|
+
event: "message",
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
session.sseClients.add(sendMessage);
|
|
175
|
+
|
|
176
|
+
// Keep connection open
|
|
177
|
+
try {
|
|
178
|
+
// Send a ping every 30 seconds to keep connection alive
|
|
179
|
+
while (true) {
|
|
180
|
+
await stream.sleep(30000);
|
|
181
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
session.sseClients.delete(sendMessage);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Handle DELETE requests - session termination
|
|
191
|
+
*/
|
|
192
|
+
app.delete("/mcp", async (c) => {
|
|
193
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
194
|
+
|
|
195
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
196
|
+
return c.json(
|
|
197
|
+
{
|
|
198
|
+
jsonrpc: "2.0",
|
|
199
|
+
error: { code: -32000, message: "Invalid or missing session ID" },
|
|
200
|
+
id: null,
|
|
201
|
+
},
|
|
202
|
+
400
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const session = sessions.get(sessionId)!;
|
|
207
|
+
|
|
208
|
+
// Close transports
|
|
209
|
+
await session.clientTransport.close();
|
|
210
|
+
await session.serverTransport.close();
|
|
211
|
+
await session.server.close();
|
|
212
|
+
|
|
213
|
+
sessions.delete(sessionId);
|
|
214
|
+
console.error(`[vector-memory-mcp] MCP session closed: ${sessionId}`);
|
|
215
|
+
|
|
216
|
+
return c.json({ success: true });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return app;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Send a message to the server and wait for its response.
|
|
224
|
+
*/
|
|
225
|
+
async function sendAndWaitForResponse(
|
|
226
|
+
session: Session,
|
|
227
|
+
message: JSONRPCRequest | JSONRPCNotification
|
|
228
|
+
): Promise<JSONRPCMessage> {
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
// Register response handler for requests (messages with id)
|
|
231
|
+
if ("id" in message && message.id !== undefined) {
|
|
232
|
+
session.pendingResponses.set(message.id, resolve);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Send message to server
|
|
236
|
+
session.clientTransport.send(message);
|
|
237
|
+
|
|
238
|
+
// For notifications (no id), resolve immediately with empty response
|
|
239
|
+
if (!("id" in message) || message.id === undefined) {
|
|
240
|
+
resolve({ jsonrpc: "2.0" } as JSONRPCMessage);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a message is an initialize request.
|
|
247
|
+
*/
|
|
248
|
+
function isInitializeRequest(body: unknown): boolean {
|
|
249
|
+
return (
|
|
250
|
+
typeof body === "object" &&
|
|
251
|
+
body !== null &&
|
|
252
|
+
"method" in body &&
|
|
253
|
+
(body as { method: string }).method === "initialize"
|
|
254
|
+
);
|
|
255
|
+
}
|