@ruvector/rvf-mcp-server 0.1.5 → 0.2.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 +5 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -12
- package/dist/cli.js.map +1 -1
- package/dist/server.d.ts +27 -1
- package/dist/server.js +242 -279
- package/dist/server.js.map +1 -1
- package/dist/stores.d.ts +46 -0
- package/dist/stores.js +196 -0
- package/dist/stores.js.map +1 -0
- package/package.json +2 -1
- package/src/cli.ts +23 -14
- package/src/server.ts +265 -305
- package/src/stores.ts +192 -0
package/dist/server.js
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RVF MCP Server — core server implementation.
|
|
3
3
|
*
|
|
4
|
-
* Registers all RVF tools, resources, and prompts with the MCP SDK.
|
|
4
|
+
* Registers all RVF tools, resources, and prompts with the MCP SDK. Stores
|
|
5
|
+
* are real on-disk `.rvf` files (via @ruvector/rvf) confined to a data dir,
|
|
6
|
+
* so queries are index-backed and data survives server restarts.
|
|
5
7
|
*/
|
|
6
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { RvfDatabase } from '@ruvector/rvf';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
7
11
|
import { z } from 'zod';
|
|
12
|
+
import { META_SUFFIX, MetadataLog, defaultDataDir, matchesFilter, resolveStorePath, } from './stores.js';
|
|
8
13
|
/** Upper bounds on model-supplied sizes (tool arguments are untrusted). */
|
|
9
14
|
export const LIMITS = {
|
|
10
15
|
maxDimensions: 65_536,
|
|
11
16
|
maxK: 10_000,
|
|
12
17
|
maxBatch: 10_000,
|
|
13
18
|
};
|
|
19
|
+
/** Auto-compaction after ingest: file larger than `minBytes` AND `ratio` x live vector bytes. */
|
|
20
|
+
export const AUTO_COMPACT = {
|
|
21
|
+
minBytes: 64 * 1024 * 1024,
|
|
22
|
+
ratio: 8,
|
|
23
|
+
};
|
|
14
24
|
const vectorSchema = z.array(z.number()).max(LIMITS.maxDimensions);
|
|
15
25
|
const metadataSchema = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]));
|
|
26
|
+
const reply = (body) => ({
|
|
27
|
+
content: [{ type: 'text', text: JSON.stringify(body, null, 2) }],
|
|
28
|
+
});
|
|
29
|
+
const fail = (message) => ({
|
|
30
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
31
|
+
});
|
|
32
|
+
const errMessage = (err) => (err instanceof Error ? err.message : String(err));
|
|
16
33
|
// ─── Server ─────────────────────────────────────────────────────────────────
|
|
17
34
|
export class RvfMcpServer {
|
|
18
35
|
mcp;
|
|
@@ -25,6 +42,8 @@ export class RvfMcpServer {
|
|
|
25
42
|
version: options?.version ?? '0.1.0',
|
|
26
43
|
defaultDimensions: options?.defaultDimensions ?? 128,
|
|
27
44
|
maxStores: options?.maxStores ?? 64,
|
|
45
|
+
dataDir: options?.dataDir ?? defaultDataDir(),
|
|
46
|
+
autoCompact: options?.autoCompact ?? AUTO_COMPACT,
|
|
28
47
|
};
|
|
29
48
|
this.mcp = new McpServer({ name: this.opts.name, version: this.opts.version }, {
|
|
30
49
|
capabilities: {
|
|
@@ -37,88 +56,111 @@ export class RvfMcpServer {
|
|
|
37
56
|
this.registerResources();
|
|
38
57
|
this.registerPrompts();
|
|
39
58
|
}
|
|
59
|
+
/** Directory that holds this server's stores. */
|
|
60
|
+
get dataDir() {
|
|
61
|
+
return this.opts.dataDir;
|
|
62
|
+
}
|
|
63
|
+
withStore(storeId, write, fn) {
|
|
64
|
+
const handle = this.stores.get(storeId);
|
|
65
|
+
if (!handle)
|
|
66
|
+
return Promise.resolve(fail(`store ${storeId} not found`));
|
|
67
|
+
if (write && handle.readOnly)
|
|
68
|
+
return Promise.resolve(fail('store is read-only'));
|
|
69
|
+
return fn(handle).catch((err) => fail(errMessage(err)));
|
|
70
|
+
}
|
|
71
|
+
register(handle) {
|
|
72
|
+
const full = { ...handle, id: `store_${this.nextId++}`, createdAt: Date.now() };
|
|
73
|
+
this.stores.set(full.id, full);
|
|
74
|
+
return full;
|
|
75
|
+
}
|
|
76
|
+
/** k-NN with exact-match metadata filtering applied to an over-fetched result. */
|
|
77
|
+
async search(h, vector, k, filter) {
|
|
78
|
+
const q = new Float32Array(vector);
|
|
79
|
+
if (!filter) {
|
|
80
|
+
const results = await h.db.query(q, k);
|
|
81
|
+
return { results, scanned: results.length };
|
|
82
|
+
}
|
|
83
|
+
const candidates = [...h.meta.entries.values()].filter((m) => matchesFilter(m, filter)).length;
|
|
84
|
+
if (candidates === 0)
|
|
85
|
+
return { results: [], scanned: 0 };
|
|
86
|
+
const total = (await h.db.status()).totalVectors;
|
|
87
|
+
let fetch = Math.min(total, Math.max(k * 4, 64));
|
|
88
|
+
for (;;) {
|
|
89
|
+
const raw = await h.db.query(q, fetch);
|
|
90
|
+
const hits = raw.filter((r) => matchesFilter(h.meta.entries.get(r.id), filter));
|
|
91
|
+
if (hits.length >= Math.min(k, candidates) || fetch >= total) {
|
|
92
|
+
return { results: hits.slice(0, k), scanned: raw.length };
|
|
93
|
+
}
|
|
94
|
+
fetch = Math.min(total, fetch * 4);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
40
97
|
// ─── Tool Registration ──────────────────────────────────────────────────
|
|
41
98
|
registerTools() {
|
|
42
99
|
// ── rvf_create_store ──────────────────────────────────────────────────
|
|
43
|
-
this.mcp.tool('rvf_create_store', 'Create a new RVF vector store at the given path', {
|
|
44
|
-
path: z.string().describe('File path for the new .rvf store'),
|
|
100
|
+
this.mcp.tool('rvf_create_store', 'Create a new RVF vector store at the given path (relative to the server data dir)', {
|
|
101
|
+
path: z.string().describe('File path for the new .rvf store, relative to the data dir'),
|
|
45
102
|
dimensions: z.number().int().positive().max(LIMITS.maxDimensions).describe('Vector dimensionality'),
|
|
46
103
|
metric: z.enum(['l2', 'cosine', 'dotproduct']).default('l2').describe('Distance metric'),
|
|
47
104
|
}, async ({ path, dimensions, metric }) => {
|
|
48
105
|
if (this.stores.size >= this.opts.maxStores) {
|
|
49
|
-
return
|
|
106
|
+
return fail(`max stores (${this.opts.maxStores}) reached`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const file = resolveStorePath(this.opts.dataDir, path, true);
|
|
110
|
+
const meta = new MetadataLog(file + META_SUFFIX, false);
|
|
111
|
+
if (fs.existsSync(meta.file))
|
|
112
|
+
throw new Error(`${path} already has metadata; use rvf_open_store`);
|
|
113
|
+
const db = await RvfDatabase.create(file, { dimensions, metric: metric }, 'node');
|
|
114
|
+
meta.header = { dimensions, metric };
|
|
115
|
+
meta.rewrite();
|
|
116
|
+
const h = this.register({ path, file, dimensions, metric, readOnly: false, db, meta });
|
|
117
|
+
return reply({ storeId: h.id, path, dimensions, metric, status: 'created' });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return fail(errMessage(err));
|
|
50
121
|
}
|
|
51
|
-
const id = `store_${this.nextId++}`;
|
|
52
|
-
const handle = {
|
|
53
|
-
id,
|
|
54
|
-
path,
|
|
55
|
-
dimensions,
|
|
56
|
-
metric,
|
|
57
|
-
readOnly: false,
|
|
58
|
-
vectors: new Map(),
|
|
59
|
-
createdAt: Date.now(),
|
|
60
|
-
};
|
|
61
|
-
this.stores.set(id, handle);
|
|
62
|
-
return {
|
|
63
|
-
content: [{
|
|
64
|
-
type: 'text',
|
|
65
|
-
text: JSON.stringify({
|
|
66
|
-
storeId: id,
|
|
67
|
-
path,
|
|
68
|
-
dimensions,
|
|
69
|
-
metric,
|
|
70
|
-
status: 'created',
|
|
71
|
-
}, null, 2),
|
|
72
|
-
}],
|
|
73
|
-
};
|
|
74
122
|
});
|
|
75
123
|
// ── rvf_open_store ────────────────────────────────────────────────────
|
|
76
|
-
this.mcp.tool('rvf_open_store', 'Open an existing RVF store for reading and writing', {
|
|
77
|
-
path: z.string().describe('Path to existing .rvf file'),
|
|
124
|
+
this.mcp.tool('rvf_open_store', 'Open an existing RVF store (relative to the server data dir) for reading and writing', {
|
|
125
|
+
path: z.string().describe('Path to existing .rvf file, relative to the data dir'),
|
|
78
126
|
readOnly: z.boolean().default(false).describe('Open in read-only mode'),
|
|
79
127
|
}, async ({ path, readOnly }) => {
|
|
80
128
|
if (this.stores.size >= this.opts.maxStores) {
|
|
81
|
-
return
|
|
129
|
+
return fail(`max stores (${this.opts.maxStores}) reached`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const file = resolveStorePath(this.opts.dataDir, path, false);
|
|
133
|
+
if (!fs.existsSync(file))
|
|
134
|
+
throw new Error(`store ${path} does not exist`);
|
|
135
|
+
const db = readOnly
|
|
136
|
+
? await RvfDatabase.openReadonly(file, 'node')
|
|
137
|
+
: await RvfDatabase.open(file, 'node');
|
|
138
|
+
const meta = new MetadataLog(file + META_SUFFIX, readOnly);
|
|
139
|
+
try {
|
|
140
|
+
meta.load();
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
await db.close();
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
const dimensions = await db.dimension();
|
|
147
|
+
const metric = meta.header?.metric ?? 'l2';
|
|
148
|
+
meta.header ??= { dimensions, metric };
|
|
149
|
+
const h = this.register({ path, file, dimensions, metric, readOnly, db, meta });
|
|
150
|
+
return reply({ storeId: h.id, path, readOnly, dimensions, metric, status: 'opened' });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return fail(errMessage(err));
|
|
82
154
|
}
|
|
83
|
-
const id = `store_${this.nextId++}`;
|
|
84
|
-
const handle = {
|
|
85
|
-
id,
|
|
86
|
-
path,
|
|
87
|
-
dimensions: this.opts.defaultDimensions,
|
|
88
|
-
metric: 'l2',
|
|
89
|
-
readOnly,
|
|
90
|
-
vectors: new Map(),
|
|
91
|
-
createdAt: Date.now(),
|
|
92
|
-
};
|
|
93
|
-
this.stores.set(id, handle);
|
|
94
|
-
return {
|
|
95
|
-
content: [{
|
|
96
|
-
type: 'text',
|
|
97
|
-
text: JSON.stringify({
|
|
98
|
-
storeId: id,
|
|
99
|
-
path,
|
|
100
|
-
readOnly,
|
|
101
|
-
status: 'opened',
|
|
102
|
-
}, null, 2),
|
|
103
|
-
}],
|
|
104
|
-
};
|
|
105
155
|
});
|
|
106
156
|
// ── rvf_close_store ───────────────────────────────────────────────────
|
|
107
157
|
this.mcp.tool('rvf_close_store', 'Close an open RVF store, releasing the writer lock', {
|
|
108
158
|
storeId: z.string().describe('Store ID returned by create/open'),
|
|
109
|
-
}, async ({ storeId }) => {
|
|
110
|
-
const handle = this.stores.get(storeId);
|
|
111
|
-
if (!handle) {
|
|
112
|
-
return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
|
|
113
|
-
}
|
|
159
|
+
}, async ({ storeId }) => this.withStore(storeId, false, async (h) => {
|
|
114
160
|
this.stores.delete(storeId);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
text: JSON.stringify({ storeId, status: 'closed', path: handle.path }, null, 2),
|
|
119
|
-
}],
|
|
120
|
-
};
|
|
121
|
-
});
|
|
161
|
+
await this.closeHandle(h);
|
|
162
|
+
return reply({ storeId, status: 'closed', path: h.path });
|
|
163
|
+
}));
|
|
122
164
|
// ── rvf_ingest ────────────────────────────────────────────────────────
|
|
123
165
|
this.mcp.tool('rvf_ingest', 'Insert vectors into an RVF store', {
|
|
124
166
|
storeId: z.string().describe('Target store ID'),
|
|
@@ -128,38 +170,46 @@ export class RvfMcpServer {
|
|
|
128
170
|
metadata: metadataSchema.optional()
|
|
129
171
|
.describe('Optional metadata key-value pairs'),
|
|
130
172
|
})).max(LIMITS.maxBatch).describe('Vectors to insert'),
|
|
131
|
-
}, async ({ storeId, entries }) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
|
|
135
|
-
}
|
|
136
|
-
if (handle.readOnly) {
|
|
137
|
-
return { content: [{ type: 'text', text: 'Error: store is read-only' }] };
|
|
138
|
-
}
|
|
139
|
-
let accepted = 0;
|
|
173
|
+
}, async ({ storeId, entries }) => this.withStore(storeId, true, async (h) => {
|
|
174
|
+
// Last write wins for duplicate ids within a batch (upsert semantics).
|
|
175
|
+
const batch = new Map();
|
|
140
176
|
let rejected = 0;
|
|
141
|
-
for (const
|
|
142
|
-
if (
|
|
177
|
+
for (const e of entries) {
|
|
178
|
+
if (e.vector.length !== h.dimensions)
|
|
143
179
|
rejected++;
|
|
144
|
-
|
|
180
|
+
else
|
|
181
|
+
batch.set(e.id, { vector: e.vector, metadata: e.metadata });
|
|
182
|
+
}
|
|
183
|
+
if (batch.size > 0) {
|
|
184
|
+
await h.db.ingestBatch([...batch].map(([id, e]) => ({ id, vector: new Float32Array(e.vector) })));
|
|
185
|
+
const sets = [];
|
|
186
|
+
const cleared = [];
|
|
187
|
+
for (const [id, e] of batch) {
|
|
188
|
+
if (e.metadata && Object.keys(e.metadata).length > 0)
|
|
189
|
+
sets.push([id, e.metadata]);
|
|
190
|
+
else
|
|
191
|
+
cleared.push(id);
|
|
145
192
|
}
|
|
146
|
-
|
|
147
|
-
vector: entry.vector,
|
|
148
|
-
metadata: entry.metadata,
|
|
149
|
-
});
|
|
150
|
-
accepted++;
|
|
193
|
+
h.meta.apply(sets, cleared);
|
|
151
194
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
let status = await h.db.status();
|
|
196
|
+
// rvf-runtime appends a manifest listing every segment on each ingest,
|
|
197
|
+
// so many small ingest calls grow the file quadratically (2,500 single
|
|
198
|
+
// ingests -> 231 MB for 1.3 MB of vectors). Bound it by compacting
|
|
199
|
+
// once the file is both large and mostly overhead. Compaction drops
|
|
200
|
+
// the in-memory HNSW, which is rebuilt lazily on the next query.
|
|
201
|
+
const liveBytes = status.totalVectors * h.dimensions * 4;
|
|
202
|
+
let autoCompacted = false;
|
|
203
|
+
// Stat the file directly: status().fileSizeBytes reads 0 in @ruvector/rvf <= 0.4.2.
|
|
204
|
+
const fileBytes = fs.statSync(h.file).size;
|
|
205
|
+
const { minBytes, ratio } = this.opts.autoCompact;
|
|
206
|
+
if (fileBytes > minBytes && fileBytes > ratio * Math.max(liveBytes, 1)) {
|
|
207
|
+
await h.db.compact();
|
|
208
|
+
status = await h.db.status();
|
|
209
|
+
autoCompacted = true;
|
|
210
|
+
}
|
|
211
|
+
return reply({ accepted: batch.size, rejected, totalVectors: status.totalVectors, autoCompacted });
|
|
212
|
+
}));
|
|
163
213
|
// ── rvf_query ─────────────────────────────────────────────────────────
|
|
164
214
|
this.mcp.tool('rvf_query', 'k-NN vector similarity search', {
|
|
165
215
|
storeId: z.string().describe('Store ID to query'),
|
|
@@ -167,182 +217,122 @@ export class RvfMcpServer {
|
|
|
167
217
|
k: z.number().int().positive().max(LIMITS.maxK).default(10).describe('Number of nearest neighbors'),
|
|
168
218
|
filter: metadataSchema.optional()
|
|
169
219
|
.describe('Metadata filter (exact match on fields)'),
|
|
170
|
-
}, async ({ storeId, vector, k, filter }) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
|
|
220
|
+
}, async ({ storeId, vector, k, filter }) => this.withStore(storeId, false, async (h) => {
|
|
221
|
+
if (vector.length !== h.dimensions) {
|
|
222
|
+
return fail(`dimension mismatch (query=${vector.length}, store=${h.dimensions})`);
|
|
174
223
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
type: 'text',
|
|
179
|
-
text: `Error: dimension mismatch (query=${vector.length}, store=${handle.dimensions})`,
|
|
180
|
-
}],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// Compute distances and sort
|
|
184
|
-
const results = [];
|
|
185
|
-
for (const [id, entry] of handle.vectors) {
|
|
186
|
-
// Apply metadata filter if provided
|
|
187
|
-
if (filter && entry.metadata) {
|
|
188
|
-
let match = true;
|
|
189
|
-
for (const [key, val] of Object.entries(filter)) {
|
|
190
|
-
if (entry.metadata[key] !== val) {
|
|
191
|
-
match = false;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (!match)
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
else if (filter && !entry.metadata) {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
const dist = computeDistance(vector, entry.vector, handle.metric);
|
|
202
|
-
results.push({ id, distance: dist });
|
|
203
|
-
}
|
|
204
|
-
results.sort((a, b) => a.distance - b.distance);
|
|
205
|
-
const topK = results.slice(0, k);
|
|
206
|
-
return {
|
|
207
|
-
content: [{
|
|
208
|
-
type: 'text',
|
|
209
|
-
text: JSON.stringify({
|
|
210
|
-
results: topK,
|
|
211
|
-
totalScanned: handle.vectors.size,
|
|
212
|
-
metric: handle.metric,
|
|
213
|
-
}, null, 2),
|
|
214
|
-
}],
|
|
215
|
-
};
|
|
216
|
-
});
|
|
224
|
+
const { results, scanned } = await this.search(h, vector, k, filter);
|
|
225
|
+
return reply({ results, totalScanned: scanned, metric: h.metric });
|
|
226
|
+
}));
|
|
217
227
|
// ── rvf_delete ────────────────────────────────────────────────────────
|
|
218
228
|
this.mcp.tool('rvf_delete', 'Delete vectors by their IDs', {
|
|
219
229
|
storeId: z.string().describe('Store ID'),
|
|
220
230
|
ids: z.array(z.string()).max(LIMITS.maxBatch).describe('Vector IDs to delete'),
|
|
221
|
-
}, async ({ storeId, ids }) => {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return { content: [{ type: 'text', text: 'Error: store is read-only' }] };
|
|
228
|
-
}
|
|
229
|
-
let deleted = 0;
|
|
230
|
-
for (const id of ids) {
|
|
231
|
-
if (handle.vectors.delete(id))
|
|
232
|
-
deleted++;
|
|
233
|
-
}
|
|
234
|
-
return {
|
|
235
|
-
content: [{
|
|
236
|
-
type: 'text',
|
|
237
|
-
text: JSON.stringify({ deleted, remaining: handle.vectors.size }, null, 2),
|
|
238
|
-
}],
|
|
239
|
-
};
|
|
240
|
-
});
|
|
231
|
+
}, async ({ storeId, ids }) => this.withStore(storeId, true, async (h) => {
|
|
232
|
+
const { deleted } = await h.db.delete(ids);
|
|
233
|
+
h.meta.apply([], ids);
|
|
234
|
+
const status = await h.db.status();
|
|
235
|
+
return reply({ deleted, remaining: status.totalVectors });
|
|
236
|
+
}));
|
|
241
237
|
// ── rvf_delete_filter ─────────────────────────────────────────────────
|
|
242
238
|
this.mcp.tool('rvf_delete_filter', 'Delete vectors matching a metadata filter', {
|
|
243
239
|
storeId: z.string().describe('Store ID'),
|
|
244
240
|
filter: metadataSchema
|
|
245
241
|
.describe('Metadata filter — all matching vectors will be deleted'),
|
|
246
|
-
}, async ({ storeId, filter }) => {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
let deleted = 0;
|
|
255
|
-
for (const [id, entry] of handle.vectors) {
|
|
256
|
-
if (!entry.metadata)
|
|
257
|
-
continue;
|
|
258
|
-
let match = true;
|
|
259
|
-
for (const [key, val] of Object.entries(filter)) {
|
|
260
|
-
if (entry.metadata[key] !== val) {
|
|
261
|
-
match = false;
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (match) {
|
|
266
|
-
handle.vectors.delete(id);
|
|
267
|
-
deleted++;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
content: [{
|
|
272
|
-
type: 'text',
|
|
273
|
-
text: JSON.stringify({ deleted, remaining: handle.vectors.size }, null, 2),
|
|
274
|
-
}],
|
|
275
|
-
};
|
|
276
|
-
});
|
|
242
|
+
}, async ({ storeId, filter }) => this.withStore(storeId, true, async (h) => {
|
|
243
|
+
const ids = [...h.meta.entries].filter(([, m]) => matchesFilter(m, filter)).map(([id]) => id);
|
|
244
|
+
const deleted = ids.length > 0 ? (await h.db.delete(ids)).deleted : 0;
|
|
245
|
+
h.meta.apply([], ids);
|
|
246
|
+
const status = await h.db.status();
|
|
247
|
+
return reply({ deleted, remaining: status.totalVectors });
|
|
248
|
+
}));
|
|
277
249
|
// ── rvf_compact ───────────────────────────────────────────────────────
|
|
278
250
|
this.mcp.tool('rvf_compact', 'Compact store to reclaim dead space from deleted vectors', {
|
|
279
251
|
storeId: z.string().describe('Store ID'),
|
|
280
|
-
}, async ({ storeId }) => {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}, null, 2),
|
|
293
|
-
}],
|
|
294
|
-
};
|
|
295
|
-
});
|
|
252
|
+
}, async ({ storeId }) => this.withStore(storeId, true, async (h) => {
|
|
253
|
+
const result = await h.db.compact();
|
|
254
|
+
h.meta.rewrite();
|
|
255
|
+
const status = await h.db.status();
|
|
256
|
+
return reply({
|
|
257
|
+
storeId,
|
|
258
|
+
compacted: true,
|
|
259
|
+
segmentsCompacted: result.segmentsCompacted,
|
|
260
|
+
bytesReclaimed: result.bytesReclaimed,
|
|
261
|
+
totalVectors: status.totalVectors,
|
|
262
|
+
});
|
|
263
|
+
}));
|
|
296
264
|
// ── rvf_status ────────────────────────────────────────────────────────
|
|
297
265
|
this.mcp.tool('rvf_status', 'Get the current status of an RVF store', {
|
|
298
266
|
storeId: z.string().describe('Store ID'),
|
|
299
|
-
}, async ({ storeId }) => {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
createdAt: new Date(handle.createdAt).toISOString(),
|
|
315
|
-
}, null, 2),
|
|
316
|
-
}],
|
|
317
|
-
};
|
|
318
|
-
});
|
|
267
|
+
}, async ({ storeId }) => this.withStore(storeId, false, async (h) => {
|
|
268
|
+
const status = await h.db.status();
|
|
269
|
+
return reply({
|
|
270
|
+
storeId: h.id,
|
|
271
|
+
path: h.path,
|
|
272
|
+
dimensions: h.dimensions,
|
|
273
|
+
metric: h.metric,
|
|
274
|
+
readOnly: h.readOnly,
|
|
275
|
+
totalVectors: status.totalVectors,
|
|
276
|
+
totalSegments: status.totalSegments,
|
|
277
|
+
fileSizeBytes: status.fileSizeBytes,
|
|
278
|
+
epoch: status.epoch,
|
|
279
|
+
createdAt: new Date(h.createdAt).toISOString(),
|
|
280
|
+
});
|
|
281
|
+
}));
|
|
319
282
|
// ── rvf_list_stores ───────────────────────────────────────────────────
|
|
320
283
|
this.mcp.tool('rvf_list_stores', 'List all open RVF stores', {}, async () => {
|
|
321
|
-
const list =
|
|
284
|
+
const list = await this.listStores();
|
|
285
|
+
return reply({
|
|
286
|
+
stores: list.map((h) => ({
|
|
287
|
+
storeId: h.storeId,
|
|
288
|
+
path: h.path,
|
|
289
|
+
dimensions: h.dimensions,
|
|
290
|
+
metric: h.metric,
|
|
291
|
+
totalVectors: h.totalVectors,
|
|
292
|
+
readOnly: h.readOnly,
|
|
293
|
+
})),
|
|
294
|
+
count: list.length,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async listStores() {
|
|
299
|
+
const out = [];
|
|
300
|
+
for (const h of this.stores.values()) {
|
|
301
|
+
let totalVectors = 0;
|
|
302
|
+
try {
|
|
303
|
+
totalVectors = (await h.db.status()).totalVectors;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// report the store even if its status call fails
|
|
307
|
+
}
|
|
308
|
+
out.push({
|
|
322
309
|
storeId: h.id,
|
|
323
310
|
path: h.path,
|
|
324
311
|
dimensions: h.dimensions,
|
|
325
312
|
metric: h.metric,
|
|
326
|
-
totalVectors: h.vectors.size,
|
|
327
313
|
readOnly: h.readOnly,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
314
|
+
totalVectors,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
async closeHandle(h) {
|
|
320
|
+
try {
|
|
321
|
+
h.meta.rewrite();
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await h.db.close();
|
|
325
|
+
}
|
|
336
326
|
}
|
|
337
327
|
// ─── Resource Registration ──────────────────────────────────────────────
|
|
338
328
|
registerResources() {
|
|
339
329
|
// List of open stores
|
|
340
330
|
this.mcp.resource('stores-list', 'rvf://stores', { description: 'List all open RVF stores and their status' }, async () => {
|
|
341
|
-
const list =
|
|
342
|
-
storeId: h.
|
|
331
|
+
const list = (await this.listStores()).map((h) => ({
|
|
332
|
+
storeId: h.storeId,
|
|
343
333
|
path: h.path,
|
|
344
334
|
dimensions: h.dimensions,
|
|
345
|
-
totalVectors: h.
|
|
335
|
+
totalVectors: h.totalVectors,
|
|
346
336
|
}));
|
|
347
337
|
return {
|
|
348
338
|
contents: [{
|
|
@@ -388,52 +378,25 @@ export class RvfMcpServer {
|
|
|
388
378
|
await this.mcp.connect(transport);
|
|
389
379
|
}
|
|
390
380
|
async close() {
|
|
391
|
-
// Close
|
|
381
|
+
// Close every store so native writer locks are released and metadata
|
|
382
|
+
// logs are compacted before the transport goes away.
|
|
383
|
+
const handles = [...this.stores.values()];
|
|
392
384
|
this.stores.clear();
|
|
385
|
+
let failure;
|
|
386
|
+
for (const h of handles) {
|
|
387
|
+
try {
|
|
388
|
+
await this.closeHandle(h);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
failure ??= err;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
393
394
|
await this.mcp.close();
|
|
395
|
+
if (failure)
|
|
396
|
+
throw failure;
|
|
394
397
|
}
|
|
395
398
|
get storeCount() {
|
|
396
399
|
return this.stores.size;
|
|
397
400
|
}
|
|
398
401
|
}
|
|
399
|
-
// ─── Distance Functions ─────────────────────────────────────────────────────
|
|
400
|
-
function computeDistance(a, b, metric) {
|
|
401
|
-
switch (metric) {
|
|
402
|
-
case 'cosine':
|
|
403
|
-
return cosineDistance(a, b);
|
|
404
|
-
case 'dotproduct':
|
|
405
|
-
return -dotProduct(a, b);
|
|
406
|
-
default: // l2
|
|
407
|
-
return l2Distance(a, b);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function l2Distance(a, b) {
|
|
411
|
-
let sum = 0;
|
|
412
|
-
for (let i = 0; i < a.length; i++) {
|
|
413
|
-
const d = a[i] - b[i];
|
|
414
|
-
sum += d * d;
|
|
415
|
-
}
|
|
416
|
-
return sum;
|
|
417
|
-
}
|
|
418
|
-
function dotProduct(a, b) {
|
|
419
|
-
let sum = 0;
|
|
420
|
-
for (let i = 0; i < a.length; i++) {
|
|
421
|
-
sum += a[i] * b[i];
|
|
422
|
-
}
|
|
423
|
-
return sum;
|
|
424
|
-
}
|
|
425
|
-
function cosineDistance(a, b) {
|
|
426
|
-
let dot = 0;
|
|
427
|
-
let normA = 0;
|
|
428
|
-
let normB = 0;
|
|
429
|
-
for (let i = 0; i < a.length; i++) {
|
|
430
|
-
dot += a[i] * b[i];
|
|
431
|
-
normA += a[i] * a[i];
|
|
432
|
-
normB += b[i] * b[i];
|
|
433
|
-
}
|
|
434
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
435
|
-
if (denom === 0)
|
|
436
|
-
return 1;
|
|
437
|
-
return 1 - dot / denom;
|
|
438
|
-
}
|
|
439
402
|
//# sourceMappingURL=server.js.map
|