@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 +87 -0
- package/dist/index.js +8 -0
- package/dist/server.js +789 -0
- package/dist/transports.js +53 -0
- package/package.json +30 -0
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
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
|
+
}
|