@quantic-impact/rvf-mcp-server 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/dist/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "./transports.js";
3
+ function parseArgs() {
4
+ const args = process.argv.slice(2);
5
+ let transport = "stdio";
6
+ let port = 3100;
7
+ for (let i = 0; i < args.length; i++) {
8
+ if (args[i] === "--transport" || args[i] === "-t") {
9
+ const val = args[++i];
10
+ if (val === "sse" || val === "stdio") {
11
+ transport = val;
12
+ } else {
13
+ console.error(`Unknown transport: ${val}. Use 'stdio' or 'sse'.`);
14
+ process.exit(1);
15
+ }
16
+ } else if (args[i] === "--port" || args[i] === "-p") {
17
+ port = parseInt(args[++i], 10);
18
+ if (isNaN(port) || port < 1 || port > 65535) {
19
+ console.error("Port must be between 1 and 65535");
20
+ process.exit(1);
21
+ }
22
+ } else if (args[i] === "--help" || args[i] === "-h") {
23
+ console.log(`
24
+ RVF MCP Server \u2014 Model Context Protocol server for RuVector Format
25
+
26
+ Usage:
27
+ rvf-mcp-server [options]
28
+
29
+ Options:
30
+ -t, --transport <stdio|sse> Transport mode (default: stdio)
31
+ -p, --port <number> SSE port (default: 3100)
32
+ -h, --help Show this help message
33
+
34
+ MCP Tools:
35
+ rvf_create_store Create a new vector store
36
+ rvf_open_store Open an existing store
37
+ rvf_close_store Close a store
38
+ rvf_ingest Insert vectors
39
+ rvf_query k-NN similarity search
40
+ rvf_delete Delete vectors by ID
41
+ rvf_delete_filter Delete by metadata filter
42
+ rvf_compact Reclaim dead space
43
+ rvf_status Store status
44
+ rvf_list_stores List open stores
45
+
46
+ stdio config (.mcp.json):
47
+ {
48
+ "mcpServers": {
49
+ "rvf": {
50
+ "command": "node",
51
+ "args": ["dist/cli.js"]
52
+ }
53
+ }
54
+ }
55
+ `);
56
+ process.exit(0);
57
+ }
58
+ }
59
+ return { transport, port };
60
+ }
61
+ async function main() {
62
+ const { transport, port } = parseArgs();
63
+ if (transport === "stdio") {
64
+ console.error("RVF MCP Server starting (stdio transport)...");
65
+ }
66
+ const server = await createServer(transport, port);
67
+ async function shutdown(signal) {
68
+ console.error(`
69
+ RVF MCP Server shutting down (${signal})...`);
70
+ try {
71
+ await server.close();
72
+ } catch (err) {
73
+ console.error("Error during shutdown:", err);
74
+ }
75
+ process.exit(0);
76
+ }
77
+ process.on("SIGINT", () => {
78
+ shutdown("SIGINT");
79
+ });
80
+ process.on("SIGTERM", () => {
81
+ shutdown("SIGTERM");
82
+ });
83
+ }
84
+ main().catch((err) => {
85
+ console.error("Fatal:", err);
86
+ process.exit(1);
87
+ });
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { RvfMcpServer } from "./server.js";
2
+ import { createStdioServer, createSseServer, createServer } from "./transports.js";
3
+ export {
4
+ RvfMcpServer,
5
+ createServer,
6
+ createSseServer,
7
+ createStdioServer
8
+ };
package/dist/server.js ADDED
@@ -0,0 +1,789 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { RvfDatabase, RvfError, RvfErrorCode } from "@quantic-impact/rvf";
4
+ const MetadataFilter = z.record(z.string(), z.any()).optional().describe("Metadata filter (exact match on fields)");
5
+ const MetadataFilterRequired = z.record(z.string(), z.any()).describe("Metadata filter \u2014 all matching vectors will be deleted");
6
+ const IngestEntrySchema = z.object({
7
+ id: z.string().describe("Unique vector ID"),
8
+ vector: z.array(z.number()).describe("Embedding vector (must match store dimensions)"),
9
+ metadata: z.record(z.string(), z.any()).optional().describe("Optional metadata key-value pairs")
10
+ });
11
+ function fnv1aFieldId(name) {
12
+ let h = 2166136261;
13
+ for (let i = 0; i < name.length; i++) {
14
+ h ^= name.charCodeAt(i);
15
+ h = Math.imul(h, 16777619);
16
+ }
17
+ return h >>> 0;
18
+ }
19
+ function buildFilterExpr(filter) {
20
+ const entries = Object.entries(filter);
21
+ if (entries.length === 1) {
22
+ const [key, val] = entries[0];
23
+ return { op: "eq", fieldId: fnv1aFieldId(key), value: val };
24
+ }
25
+ return {
26
+ op: "and",
27
+ exprs: entries.map(([key, val]) => ({
28
+ op: "eq",
29
+ fieldId: fnv1aFieldId(key),
30
+ value: val
31
+ }))
32
+ };
33
+ }
34
+ class RvfMcpServer {
35
+ mcp;
36
+ stores = /* @__PURE__ */ new Map();
37
+ nextId = 1;
38
+ opts;
39
+ constructor(options) {
40
+ this.opts = {
41
+ name: options?.name ?? "rvf-mcp-server",
42
+ version: options?.version ?? "0.1.0",
43
+ maxStores: options?.maxStores ?? 64
44
+ };
45
+ this.mcp = new McpServer(
46
+ { name: this.opts.name, version: this.opts.version },
47
+ {
48
+ capabilities: {
49
+ resources: {},
50
+ tools: {},
51
+ prompts: {}
52
+ }
53
+ }
54
+ );
55
+ this.registerTools();
56
+ this.registerResources();
57
+ this.registerPrompts();
58
+ }
59
+ // ─── Internal helpers ───────────────────────────────────────────────────
60
+ errorResponse(msg) {
61
+ return { content: [{ type: "text", text: `Error: ${msg}` }] };
62
+ }
63
+ async withStore(storeId, fn) {
64
+ const handle = this.stores.get(storeId);
65
+ if (!handle) {
66
+ return this.errorResponse(`store ${storeId} not found`);
67
+ }
68
+ try {
69
+ return await fn(handle);
70
+ } catch (err) {
71
+ if (err instanceof RvfError) {
72
+ return this.errorResponse(`[${RvfErrorCode[err.code]}] ${err.message}`);
73
+ }
74
+ return this.errorResponse(String(err));
75
+ }
76
+ }
77
+ // ─── Tool Registration ──────────────────────────────────────────────────
78
+ registerTools() {
79
+ this.mcp.tool(
80
+ "rvf_create_store",
81
+ "Create a new RVF vector store at the given path",
82
+ {
83
+ path: z.string().describe("File path for the new .rvf store"),
84
+ dimensions: z.number().int().positive().describe("Vector dimensionality"),
85
+ metric: z.enum(["l2", "cosine", "dotproduct"]).default("l2").describe("Distance metric")
86
+ },
87
+ async ({ path, dimensions, metric }) => {
88
+ if (this.stores.size >= this.opts.maxStores) {
89
+ return this.errorResponse(`max stores (${this.opts.maxStores}) reached`);
90
+ }
91
+ try {
92
+ const db = await RvfDatabase.create(path, {
93
+ dimensions,
94
+ metric
95
+ });
96
+ const id = `store_${this.nextId++}`;
97
+ this.stores.set(id, {
98
+ id,
99
+ path,
100
+ db,
101
+ readOnly: false,
102
+ openedAt: Date.now()
103
+ });
104
+ return {
105
+ content: [{
106
+ type: "text",
107
+ text: JSON.stringify({
108
+ storeId: id,
109
+ path,
110
+ dimensions,
111
+ metric,
112
+ status: "created"
113
+ }, null, 2)
114
+ }]
115
+ };
116
+ } catch (err) {
117
+ if (err instanceof RvfError) {
118
+ return this.errorResponse(`[${RvfErrorCode[err.code]}] ${err.message}`);
119
+ }
120
+ return this.errorResponse(String(err));
121
+ }
122
+ }
123
+ );
124
+ this.mcp.tool(
125
+ "rvf_open_store",
126
+ "Open an existing RVF store for reading and writing",
127
+ {
128
+ path: z.string().describe("Path to existing .rvf file"),
129
+ readOnly: z.boolean().default(false).describe("Open in read-only mode")
130
+ },
131
+ async ({ path, readOnly }) => {
132
+ if (this.stores.size >= this.opts.maxStores) {
133
+ return this.errorResponse(`max stores (${this.opts.maxStores}) reached`);
134
+ }
135
+ try {
136
+ const db = readOnly ? await RvfDatabase.openReadonly(path) : await RvfDatabase.open(path);
137
+ const id = `store_${this.nextId++}`;
138
+ this.stores.set(id, {
139
+ id,
140
+ path,
141
+ db,
142
+ readOnly,
143
+ openedAt: Date.now()
144
+ });
145
+ return {
146
+ content: [{
147
+ type: "text",
148
+ text: JSON.stringify({
149
+ storeId: id,
150
+ path,
151
+ readOnly,
152
+ status: "opened"
153
+ }, null, 2)
154
+ }]
155
+ };
156
+ } catch (err) {
157
+ if (err instanceof RvfError) {
158
+ return this.errorResponse(`[${RvfErrorCode[err.code]}] ${err.message}`);
159
+ }
160
+ return this.errorResponse(String(err));
161
+ }
162
+ }
163
+ );
164
+ this.mcp.tool(
165
+ "rvf_close_store",
166
+ "Close an open RVF store, releasing the writer lock",
167
+ {
168
+ storeId: z.string().describe("Store ID returned by create/open")
169
+ },
170
+ async ({ storeId }) => {
171
+ const result = await this.withStore(storeId, async (handle) => {
172
+ await handle.db.close();
173
+ this.stores.delete(storeId);
174
+ return {
175
+ content: [{
176
+ type: "text",
177
+ text: JSON.stringify({ storeId, status: "closed", path: handle.path }, null, 2)
178
+ }]
179
+ };
180
+ });
181
+ return result;
182
+ }
183
+ );
184
+ this.mcp.tool(
185
+ "rvf_ingest",
186
+ "Insert vectors into an RVF store",
187
+ {
188
+ storeId: z.string().describe("Target store ID"),
189
+ entries: z.array(IngestEntrySchema).describe("Vectors to insert")
190
+ },
191
+ // @ts-ignore — TS2589: deep inference from nested Zod schema
192
+ async ({ storeId, entries }) => {
193
+ const result = await this.withStore(storeId, async (handle) => {
194
+ if (handle.readOnly) {
195
+ return this.errorResponse("store is read-only");
196
+ }
197
+ const ingestResult = await handle.db.ingestBatch(
198
+ entries.map((e) => ({
199
+ id: e.id,
200
+ vector: e.vector,
201
+ metadata: e.metadata
202
+ }))
203
+ );
204
+ const status = await handle.db.status();
205
+ return {
206
+ content: [{
207
+ type: "text",
208
+ text: JSON.stringify({
209
+ accepted: ingestResult.accepted,
210
+ rejected: ingestResult.rejected,
211
+ epoch: ingestResult.epoch,
212
+ totalVectors: status.totalVectors
213
+ }, null, 2)
214
+ }]
215
+ };
216
+ });
217
+ return result;
218
+ }
219
+ );
220
+ this.mcp.tool(
221
+ "rvf_query",
222
+ "k-NN vector similarity search",
223
+ {
224
+ storeId: z.string().describe("Store ID to query"),
225
+ vector: z.array(z.number()).describe("Query embedding vector"),
226
+ k: z.number().int().positive().default(10).describe("Number of nearest neighbors"),
227
+ filter: MetadataFilter
228
+ },
229
+ // @ts-ignore — TS2589: deep inference from optional filter schema
230
+ async ({ storeId, vector, k, filter }) => {
231
+ const result = await this.withStore(storeId, async (handle) => {
232
+ const queryOpts = filter ? { filter: buildFilterExpr(filter) } : void 0;
233
+ const results = await handle.db.query(vector, k, queryOpts);
234
+ return {
235
+ content: [{
236
+ type: "text",
237
+ text: JSON.stringify({
238
+ results: results.map((r) => ({ id: r.id, distance: r.distance })),
239
+ count: results.length
240
+ }, null, 2)
241
+ }]
242
+ };
243
+ });
244
+ return result;
245
+ }
246
+ );
247
+ this.mcp.tool(
248
+ "rvf_delete",
249
+ "Delete vectors by their IDs",
250
+ {
251
+ storeId: z.string().describe("Store ID"),
252
+ ids: z.array(z.string()).describe("Vector IDs to delete")
253
+ },
254
+ async ({ storeId, ids }) => {
255
+ const result = await this.withStore(storeId, async (handle) => {
256
+ if (handle.readOnly) {
257
+ return this.errorResponse("store is read-only");
258
+ }
259
+ const delResult = await handle.db.delete(ids);
260
+ const status = await handle.db.status();
261
+ return {
262
+ content: [{
263
+ type: "text",
264
+ text: JSON.stringify({
265
+ deleted: delResult.deleted,
266
+ epoch: delResult.epoch,
267
+ remaining: status.totalVectors
268
+ }, null, 2)
269
+ }]
270
+ };
271
+ });
272
+ return result;
273
+ }
274
+ );
275
+ this.mcp.tool(
276
+ "rvf_delete_filter",
277
+ "Delete vectors matching a metadata filter",
278
+ {
279
+ storeId: z.string().describe("Store ID"),
280
+ filter: MetadataFilterRequired
281
+ },
282
+ // @ts-ignore — TS2589: deep inference from filter schema
283
+ async ({ storeId, filter }) => {
284
+ const result = await this.withStore(storeId, async (handle) => {
285
+ if (handle.readOnly) {
286
+ return this.errorResponse("store is read-only");
287
+ }
288
+ const filterExpr = buildFilterExpr(filter);
289
+ const delResult = await handle.db.deleteByFilter(filterExpr);
290
+ const status = await handle.db.status();
291
+ return {
292
+ content: [{
293
+ type: "text",
294
+ text: JSON.stringify({
295
+ deleted: delResult.deleted,
296
+ epoch: delResult.epoch,
297
+ remaining: status.totalVectors
298
+ }, null, 2)
299
+ }]
300
+ };
301
+ });
302
+ return result;
303
+ }
304
+ );
305
+ this.mcp.tool(
306
+ "rvf_compact",
307
+ "Compact store to reclaim dead space from deleted vectors",
308
+ {
309
+ storeId: z.string().describe("Store ID")
310
+ },
311
+ async ({ storeId }) => {
312
+ const result = await this.withStore(storeId, async (handle) => {
313
+ const compactResult = await handle.db.compact();
314
+ return {
315
+ content: [{
316
+ type: "text",
317
+ text: JSON.stringify({
318
+ storeId,
319
+ segmentsCompacted: compactResult.segmentsCompacted,
320
+ bytesReclaimed: compactResult.bytesReclaimed,
321
+ epoch: compactResult.epoch
322
+ }, null, 2)
323
+ }]
324
+ };
325
+ });
326
+ return result;
327
+ }
328
+ );
329
+ this.mcp.tool(
330
+ "rvf_status",
331
+ "Get the current status of an RVF store",
332
+ {
333
+ storeId: z.string().describe("Store ID")
334
+ },
335
+ async ({ storeId }) => {
336
+ const result = await this.withStore(storeId, async (handle) => {
337
+ const [status, dimension] = await Promise.all([
338
+ handle.db.status(),
339
+ handle.db.dimension()
340
+ ]);
341
+ return {
342
+ content: [{
343
+ type: "text",
344
+ text: JSON.stringify({
345
+ storeId: handle.id,
346
+ path: handle.path,
347
+ dimensions: dimension,
348
+ totalVectors: status.totalVectors,
349
+ totalSegments: status.totalSegments,
350
+ fileSizeBytes: status.fileSizeBytes,
351
+ epoch: status.epoch,
352
+ compactionState: status.compactionState,
353
+ deadSpaceRatio: status.deadSpaceRatio,
354
+ readOnly: status.readOnly,
355
+ openedAt: new Date(handle.openedAt).toISOString()
356
+ }, null, 2)
357
+ }]
358
+ };
359
+ });
360
+ return result;
361
+ }
362
+ );
363
+ this.mcp.tool(
364
+ "rvf_list_stores",
365
+ "List all open RVF stores",
366
+ {},
367
+ async () => {
368
+ const handles = Array.from(this.stores.values());
369
+ const list = await Promise.all(
370
+ handles.map(async (h) => {
371
+ try {
372
+ const [status, dimension] = await Promise.all([
373
+ h.db.status(),
374
+ h.db.dimension()
375
+ ]);
376
+ return {
377
+ storeId: h.id,
378
+ path: h.path,
379
+ dimensions: dimension,
380
+ totalVectors: status.totalVectors,
381
+ readOnly: status.readOnly
382
+ };
383
+ } catch {
384
+ return {
385
+ storeId: h.id,
386
+ path: h.path,
387
+ dimensions: null,
388
+ totalVectors: null,
389
+ readOnly: h.readOnly,
390
+ error: "failed to read status"
391
+ };
392
+ }
393
+ })
394
+ );
395
+ return {
396
+ content: [{
397
+ type: "text",
398
+ text: JSON.stringify({ stores: list, count: list.length }, null, 2)
399
+ }]
400
+ };
401
+ }
402
+ );
403
+ this.mcp.tool(
404
+ "rvf_put_meta",
405
+ "Store a key-value pair in store-level metadata (persisted in META_SEG)",
406
+ {
407
+ storeId: z.string().describe("Store ID from rvf_create_store or rvf_open_store"),
408
+ key: z.string().describe("Metadata key"),
409
+ value: z.string().describe("Metadata value (stored as UTF-8 bytes)")
410
+ },
411
+ async ({ storeId, key, value }) => {
412
+ const handle = this.stores.get(storeId);
413
+ if (!handle)
414
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
415
+ try {
416
+ await handle.db.putMeta(key, new TextEncoder().encode(value));
417
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, key }) }] };
418
+ } catch (e) {
419
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
420
+ }
421
+ }
422
+ );
423
+ this.mcp.tool(
424
+ "rvf_get_meta",
425
+ "Retrieve a value from store-level metadata by key",
426
+ {
427
+ storeId: z.string().describe("Store ID"),
428
+ key: z.string().describe("Metadata key to retrieve")
429
+ },
430
+ async ({ storeId, key }) => {
431
+ const handle = this.stores.get(storeId);
432
+ if (!handle)
433
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
434
+ try {
435
+ const result = await handle.db.getMeta(key);
436
+ if (result === null) {
437
+ return { content: [{ type: "text", text: JSON.stringify({ found: false, key }) }] };
438
+ }
439
+ return { content: [{ type: "text", text: JSON.stringify({ found: true, key, value: new TextDecoder().decode(result) }) }] };
440
+ } catch (e) {
441
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
442
+ }
443
+ }
444
+ );
445
+ this.mcp.tool(
446
+ "rvf_list_meta_keys",
447
+ "List all keys in store-level metadata",
448
+ {
449
+ storeId: z.string().describe("Store ID")
450
+ },
451
+ async ({ storeId }) => {
452
+ const handle = this.stores.get(storeId);
453
+ if (!handle)
454
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
455
+ try {
456
+ const keys = await handle.db.listMetaKeys();
457
+ return { content: [{ type: "text", text: JSON.stringify({ keys, count: keys.length }) }] };
458
+ } catch (e) {
459
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
460
+ }
461
+ }
462
+ );
463
+ this.mcp.tool(
464
+ "rvf_delete_meta",
465
+ "Delete a key from store-level metadata",
466
+ {
467
+ storeId: z.string().describe("Store ID"),
468
+ key: z.string().describe("Metadata key to delete")
469
+ },
470
+ async ({ storeId, key }) => {
471
+ const handle = this.stores.get(storeId);
472
+ if (!handle)
473
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
474
+ if (handle.readOnly)
475
+ return { content: [{ type: "text", text: "Error: store is read-only" }] };
476
+ try {
477
+ const existed = await handle.db.deleteMeta(key);
478
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, key, existed }) }] };
479
+ } catch (e) {
480
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
481
+ }
482
+ }
483
+ );
484
+ this.mcp.tool(
485
+ "rvf_flush_meta",
486
+ "Flush deferred metadata writes to disk (no-op if clean)",
487
+ {
488
+ storeId: z.string().describe("Store ID")
489
+ },
490
+ async ({ storeId }) => {
491
+ const handle = this.stores.get(storeId);
492
+ if (!handle)
493
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
494
+ if (handle.readOnly)
495
+ return { content: [{ type: "text", text: "Error: store is read-only" }] };
496
+ try {
497
+ await handle.db.flushMeta();
498
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, flushed: true }) }] };
499
+ } catch (e) {
500
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
501
+ }
502
+ }
503
+ );
504
+ this.mcp.tool(
505
+ "rvf_branch",
506
+ "Create a COW (copy-on-write) branch of a store",
507
+ {
508
+ storeId: z.string().describe("Parent store ID"),
509
+ childPath: z.string().describe("File path for the child branch")
510
+ },
511
+ async ({ storeId, childPath }) => {
512
+ const handle = this.stores.get(storeId);
513
+ if (!handle)
514
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
515
+ try {
516
+ const childDb = await handle.db.branch(childPath);
517
+ const childId = `store_${this.nextId++}`;
518
+ this.stores.set(childId, { id: childId, path: childPath, db: childDb, readOnly: false, openedAt: Date.now() });
519
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, childStoreId: childId, childPath }) }] };
520
+ } catch (e) {
521
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
522
+ }
523
+ }
524
+ );
525
+ this.mcp.tool(
526
+ "rvf_freeze",
527
+ "Freeze a COW branch (make it immutable)",
528
+ {
529
+ storeId: z.string().describe("Store ID to freeze")
530
+ },
531
+ async ({ storeId }) => {
532
+ const handle = this.stores.get(storeId);
533
+ if (!handle)
534
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
535
+ try {
536
+ await handle.db.freeze();
537
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, frozen: true }) }] };
538
+ } catch (e) {
539
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
540
+ }
541
+ }
542
+ );
543
+ this.mcp.tool(
544
+ "rvf_cow_stats",
545
+ "Get COW (copy-on-write) statistics for a store",
546
+ {
547
+ storeId: z.string().describe("Store ID")
548
+ },
549
+ async ({ storeId }) => {
550
+ const handle = this.stores.get(storeId);
551
+ if (!handle)
552
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
553
+ try {
554
+ const stats = await handle.db.cowStats();
555
+ return { content: [{ type: "text", text: JSON.stringify(stats) }] };
556
+ } catch (e) {
557
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
558
+ }
559
+ }
560
+ );
561
+ this.mcp.tool(
562
+ "rvf_witness_hash",
563
+ "Get the last witness hash from the audit chain",
564
+ {
565
+ storeId: z.string().describe("Store ID")
566
+ },
567
+ async ({ storeId }) => {
568
+ const handle = this.stores.get(storeId);
569
+ if (!handle)
570
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
571
+ try {
572
+ const hash = await handle.db.lastWitnessHash();
573
+ const hex = Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
574
+ return { content: [{ type: "text", text: JSON.stringify({ witnessHash: hex }) }] };
575
+ } catch (e) {
576
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
577
+ }
578
+ }
579
+ );
580
+ this.mcp.tool(
581
+ "rvf_query_audited",
582
+ "k-NN search with tamper-evident audit trail (appends WITNESS_SEG)",
583
+ {
584
+ storeId: z.string().describe("Store ID to query"),
585
+ vector: z.array(z.number()).describe("Query embedding vector"),
586
+ k: z.number().int().positive().default(10).describe("Number of nearest neighbors"),
587
+ filter: MetadataFilter
588
+ },
589
+ // @ts-ignore — TS2589: deep inference from optional filter schema
590
+ async ({ storeId, vector, k, filter }) => {
591
+ const result = await this.withStore(storeId, async (handle) => {
592
+ const queryOpts = filter ? { filter: buildFilterExpr(filter) } : void 0;
593
+ const results = await handle.db.queryAudited(vector, k, queryOpts);
594
+ return {
595
+ content: [{
596
+ type: "text",
597
+ text: JSON.stringify({
598
+ results: results.map((r) => ({ id: r.id, distance: r.distance })),
599
+ count: results.length,
600
+ audited: true
601
+ }, null, 2)
602
+ }]
603
+ };
604
+ });
605
+ return result;
606
+ }
607
+ );
608
+ this.mcp.tool(
609
+ "rvf_membership_contains",
610
+ "Check if a vector ID is in the membership filter (COW branches)",
611
+ {
612
+ storeId: z.string().describe("Store ID"),
613
+ id: z.number().int().describe("Vector ID to check")
614
+ },
615
+ async ({ storeId, id }) => {
616
+ const handle = this.stores.get(storeId);
617
+ if (!handle)
618
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
619
+ try {
620
+ const contains = await handle.db.membershipContains(id);
621
+ return { content: [{ type: "text", text: JSON.stringify({ id, contains }) }] };
622
+ } catch (e) {
623
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
624
+ }
625
+ }
626
+ );
627
+ this.mcp.tool(
628
+ "rvf_membership_add",
629
+ "Add a vector ID to the membership filter (COW branches only)",
630
+ {
631
+ storeId: z.string().describe("Store ID"),
632
+ id: z.number().int().describe("Vector ID to add")
633
+ },
634
+ async ({ storeId, id }) => {
635
+ const handle = this.stores.get(storeId);
636
+ if (!handle)
637
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
638
+ if (handle.readOnly)
639
+ return { content: [{ type: "text", text: "Error: store is read-only" }] };
640
+ try {
641
+ await handle.db.membershipAdd(id);
642
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, id, action: "added" }) }] };
643
+ } catch (e) {
644
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
645
+ }
646
+ }
647
+ );
648
+ this.mcp.tool(
649
+ "rvf_membership_remove",
650
+ "Remove a vector ID from the membership filter (COW branches only)",
651
+ {
652
+ storeId: z.string().describe("Store ID"),
653
+ id: z.number().int().describe("Vector ID to remove")
654
+ },
655
+ async ({ storeId, id }) => {
656
+ const handle = this.stores.get(storeId);
657
+ if (!handle)
658
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
659
+ if (handle.readOnly)
660
+ return { content: [{ type: "text", text: "Error: store is read-only" }] };
661
+ try {
662
+ await handle.db.membershipRemove(id);
663
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, id, action: "removed" }) }] };
664
+ } catch (e) {
665
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
666
+ }
667
+ }
668
+ );
669
+ this.mcp.tool(
670
+ "rvf_membership_count",
671
+ "Get the number of entries in the membership filter",
672
+ {
673
+ storeId: z.string().describe("Store ID")
674
+ },
675
+ async ({ storeId }) => {
676
+ const handle = this.stores.get(storeId);
677
+ if (!handle)
678
+ return { content: [{ type: "text", text: `Store '${storeId}' not found` }] };
679
+ try {
680
+ const count = await handle.db.membershipCount();
681
+ return { content: [{ type: "text", text: JSON.stringify({ count }) }] };
682
+ } catch (e) {
683
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
684
+ }
685
+ }
686
+ );
687
+ }
688
+ // ─── Resource Registration ──────────────────────────────────────────────
689
+ registerResources() {
690
+ this.mcp.resource(
691
+ "stores-list",
692
+ "rvf://stores",
693
+ { description: "List all open RVF stores and their status" },
694
+ async () => {
695
+ const handles = Array.from(this.stores.values());
696
+ const list = await Promise.all(
697
+ handles.map(async (h) => {
698
+ try {
699
+ const [status, dimension] = await Promise.all([
700
+ h.db.status(),
701
+ h.db.dimension()
702
+ ]);
703
+ return {
704
+ storeId: h.id,
705
+ path: h.path,
706
+ dimensions: dimension,
707
+ totalVectors: status.totalVectors
708
+ };
709
+ } catch {
710
+ return {
711
+ storeId: h.id,
712
+ path: h.path,
713
+ dimensions: null,
714
+ totalVectors: null
715
+ };
716
+ }
717
+ })
718
+ );
719
+ return {
720
+ contents: [{
721
+ uri: "rvf://stores",
722
+ mimeType: "application/json",
723
+ text: JSON.stringify({ stores: list }, null, 2)
724
+ }]
725
+ };
726
+ }
727
+ );
728
+ }
729
+ // ─── Prompt Registration ────────────────────────────────────────────────
730
+ registerPrompts() {
731
+ this.mcp.prompt(
732
+ "rvf-search",
733
+ "Search for similar vectors in an RVF store",
734
+ {
735
+ storeId: z.string().describe("Store ID to search"),
736
+ description: z.string().describe("Natural language description of what to search for")
737
+ },
738
+ async ({ storeId, description }) => ({
739
+ messages: [{
740
+ role: "user",
741
+ content: {
742
+ type: "text",
743
+ text: `Search the RVF store "${storeId}" for vectors similar to: "${description}". Use the rvf_query tool to perform the search. If you need to create an embedding from the description first, generate a suitable vector representation.`
744
+ }
745
+ }]
746
+ })
747
+ );
748
+ this.mcp.prompt(
749
+ "rvf-ingest",
750
+ "Ingest data into an RVF store",
751
+ {
752
+ storeId: z.string().describe("Store ID to ingest into"),
753
+ data: z.string().describe("Data to embed and ingest")
754
+ },
755
+ async ({ storeId, data }) => ({
756
+ messages: [{
757
+ role: "user",
758
+ content: {
759
+ type: "text",
760
+ text: `Ingest the following data into RVF store "${storeId}": ${data}. Generate appropriate vector embeddings and metadata, then use the rvf_ingest tool.`
761
+ }
762
+ }]
763
+ })
764
+ );
765
+ }
766
+ // ─── Connection ─────────────────────────────────────────────────────────
767
+ async connect(transport) {
768
+ await this.mcp.connect(transport);
769
+ }
770
+ async close() {
771
+ const handles = Array.from(this.stores.values());
772
+ await Promise.all(
773
+ handles.map(async (h) => {
774
+ try {
775
+ await h.db.close();
776
+ } catch {
777
+ }
778
+ })
779
+ );
780
+ this.stores.clear();
781
+ await this.mcp.close();
782
+ }
783
+ get storeCount() {
784
+ return this.stores.size;
785
+ }
786
+ }
787
+ export {
788
+ RvfMcpServer
789
+ };
@@ -0,0 +1,53 @@
1
+ import { RvfMcpServer } from "./server.js";
2
+ async function createStdioServer(options) {
3
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
4
+ const server = new RvfMcpServer(options);
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ return server;
8
+ }
9
+ async function createSseServer(port = 3100, options) {
10
+ const { SSEServerTransport } = await import("@modelcontextprotocol/sdk/server/sse.js");
11
+ const express = (await import("express")).default;
12
+ const app = express();
13
+ const server = new RvfMcpServer(options);
14
+ let sseTransport = null;
15
+ app.get("/sse", (req, res) => {
16
+ sseTransport = new SSEServerTransport("/messages", res);
17
+ server.connect(sseTransport).catch((err) => {
18
+ console.error("SSE connection error:", err);
19
+ });
20
+ });
21
+ app.post("/messages", (req, res) => {
22
+ if (!sseTransport) {
23
+ res.status(503).json({ error: "No SSE connection" });
24
+ return;
25
+ }
26
+ sseTransport.handlePostMessage(req, res);
27
+ });
28
+ app.get("/health", (_req, res) => {
29
+ res.json({
30
+ status: "ok",
31
+ server: options?.name ?? "rvf-mcp-server",
32
+ stores: server.storeCount
33
+ });
34
+ });
35
+ app.listen(port, () => {
36
+ console.error(`RVF MCP Server (SSE) listening on http://localhost:${port}`);
37
+ console.error(` SSE endpoint: http://localhost:${port}/sse`);
38
+ console.error(` Message endpoint: http://localhost:${port}/messages`);
39
+ console.error(` Health check: http://localhost:${port}/health`);
40
+ });
41
+ return server;
42
+ }
43
+ async function createServer(transport = "stdio", port = 3100, options) {
44
+ if (transport === "sse") {
45
+ return createSseServer(port, options);
46
+ }
47
+ return createStdioServer(options);
48
+ }
49
+ export {
50
+ createServer,
51
+ createSseServer,
52
+ createStdioServer
53
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@quantic-impact/rvf-mcp-server",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for RuVector Format (RVF) vector database with META_SEG, COW branching, and witness chain — stdio and SSE transports",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "rvf-mcp-server": "dist/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist/", "package.json"],
16
+ "keywords": ["rvf", "ruvector", "mcp", "vector-database", "model-context-protocol"],
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.0.0",
23
+ "@quantic-impact/rvf": "0.2.0",
24
+ "express": "^4.18.0",
25
+ "zod": "^3.22.0"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }