@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/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 { content: [{ type: 'text', text: `Error: max stores (${this.opts.maxStores}) reached` }] };
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 { content: [{ type: 'text', text: `Error: max stores (${this.opts.maxStores}) reached` }] };
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
- return {
116
- content: [{
117
- type: 'text',
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
- const handle = this.stores.get(storeId);
133
- if (!handle) {
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 entry of entries) {
142
- if (entry.vector.length !== handle.dimensions) {
177
+ for (const e of entries) {
178
+ if (e.vector.length !== h.dimensions)
143
179
  rejected++;
144
- continue;
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
- handle.vectors.set(entry.id, {
147
- vector: entry.vector,
148
- metadata: entry.metadata,
149
- });
150
- accepted++;
193
+ h.meta.apply(sets, cleared);
151
194
  }
152
- return {
153
- content: [{
154
- type: 'text',
155
- text: JSON.stringify({
156
- accepted,
157
- rejected,
158
- totalVectors: handle.vectors.size,
159
- }, null, 2),
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
- const handle = this.stores.get(storeId);
172
- if (!handle) {
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
- if (vector.length !== handle.dimensions) {
176
- return {
177
- content: [{
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 handle = this.stores.get(storeId);
223
- if (!handle) {
224
- return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
225
- }
226
- if (handle.readOnly) {
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 handle = this.stores.get(storeId);
248
- if (!handle) {
249
- return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
250
- }
251
- if (handle.readOnly) {
252
- return { content: [{ type: 'text', text: 'Error: store is read-only' }] };
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 handle = this.stores.get(storeId);
282
- if (!handle) {
283
- return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
284
- }
285
- return {
286
- content: [{
287
- type: 'text',
288
- text: JSON.stringify({
289
- storeId,
290
- compacted: true,
291
- totalVectors: handle.vectors.size,
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 handle = this.stores.get(storeId);
301
- if (!handle) {
302
- return { content: [{ type: 'text', text: `Error: store ${storeId} not found` }] };
303
- }
304
- return {
305
- content: [{
306
- type: 'text',
307
- text: JSON.stringify({
308
- storeId: handle.id,
309
- path: handle.path,
310
- dimensions: handle.dimensions,
311
- metric: handle.metric,
312
- readOnly: handle.readOnly,
313
- totalVectors: handle.vectors.size,
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 = Array.from(this.stores.values()).map((h) => ({
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
- return {
330
- content: [{
331
- type: 'text',
332
- text: JSON.stringify({ stores: list, count: list.length }, null, 2),
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 = Array.from(this.stores.values()).map((h) => ({
342
- storeId: h.id,
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.vectors.size,
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 all stores
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