@mhalder/qdrant-mcp-server 1.1.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.
Files changed (100) hide show
  1. package/.env.example +92 -0
  2. package/.github/workflows/ci.yml +61 -0
  3. package/.github/workflows/claude-code-review.yml +57 -0
  4. package/.github/workflows/claude.yml +50 -0
  5. package/.github/workflows/release.yml +52 -0
  6. package/.husky/commit-msg +1 -0
  7. package/.husky/pre-commit +1 -0
  8. package/.releaserc.json +59 -0
  9. package/.yamlfmt +4 -0
  10. package/CHANGELOG.md +73 -0
  11. package/CONTRIBUTING.md +176 -0
  12. package/LICENSE +21 -0
  13. package/README.md +714 -0
  14. package/build/embeddings/base.d.ts +23 -0
  15. package/build/embeddings/base.d.ts.map +1 -0
  16. package/build/embeddings/base.js +2 -0
  17. package/build/embeddings/base.js.map +1 -0
  18. package/build/embeddings/cohere.d.ts +17 -0
  19. package/build/embeddings/cohere.d.ts.map +1 -0
  20. package/build/embeddings/cohere.js +102 -0
  21. package/build/embeddings/cohere.js.map +1 -0
  22. package/build/embeddings/cohere.test.d.ts +2 -0
  23. package/build/embeddings/cohere.test.d.ts.map +1 -0
  24. package/build/embeddings/cohere.test.js +279 -0
  25. package/build/embeddings/cohere.test.js.map +1 -0
  26. package/build/embeddings/factory.d.ts +10 -0
  27. package/build/embeddings/factory.d.ts.map +1 -0
  28. package/build/embeddings/factory.js +98 -0
  29. package/build/embeddings/factory.js.map +1 -0
  30. package/build/embeddings/factory.test.d.ts +2 -0
  31. package/build/embeddings/factory.test.d.ts.map +1 -0
  32. package/build/embeddings/factory.test.js +329 -0
  33. package/build/embeddings/factory.test.js.map +1 -0
  34. package/build/embeddings/ollama.d.ts +18 -0
  35. package/build/embeddings/ollama.d.ts.map +1 -0
  36. package/build/embeddings/ollama.js +135 -0
  37. package/build/embeddings/ollama.js.map +1 -0
  38. package/build/embeddings/ollama.test.d.ts +2 -0
  39. package/build/embeddings/ollama.test.d.ts.map +1 -0
  40. package/build/embeddings/ollama.test.js +399 -0
  41. package/build/embeddings/ollama.test.js.map +1 -0
  42. package/build/embeddings/openai.d.ts +16 -0
  43. package/build/embeddings/openai.d.ts.map +1 -0
  44. package/build/embeddings/openai.js +108 -0
  45. package/build/embeddings/openai.js.map +1 -0
  46. package/build/embeddings/openai.test.d.ts +2 -0
  47. package/build/embeddings/openai.test.d.ts.map +1 -0
  48. package/build/embeddings/openai.test.js +283 -0
  49. package/build/embeddings/openai.test.js.map +1 -0
  50. package/build/embeddings/voyage.d.ts +19 -0
  51. package/build/embeddings/voyage.d.ts.map +1 -0
  52. package/build/embeddings/voyage.js +113 -0
  53. package/build/embeddings/voyage.js.map +1 -0
  54. package/build/embeddings/voyage.test.d.ts +2 -0
  55. package/build/embeddings/voyage.test.d.ts.map +1 -0
  56. package/build/embeddings/voyage.test.js +371 -0
  57. package/build/embeddings/voyage.test.js.map +1 -0
  58. package/build/index.d.ts +3 -0
  59. package/build/index.d.ts.map +1 -0
  60. package/build/index.js +534 -0
  61. package/build/index.js.map +1 -0
  62. package/build/index.test.d.ts +2 -0
  63. package/build/index.test.d.ts.map +1 -0
  64. package/build/index.test.js +241 -0
  65. package/build/index.test.js.map +1 -0
  66. package/build/qdrant/client.d.ts +37 -0
  67. package/build/qdrant/client.d.ts.map +1 -0
  68. package/build/qdrant/client.js +142 -0
  69. package/build/qdrant/client.js.map +1 -0
  70. package/build/qdrant/client.test.d.ts +2 -0
  71. package/build/qdrant/client.test.d.ts.map +1 -0
  72. package/build/qdrant/client.test.js +340 -0
  73. package/build/qdrant/client.test.js.map +1 -0
  74. package/commitlint.config.js +25 -0
  75. package/docker-compose.yml +22 -0
  76. package/docs/test_report.md +259 -0
  77. package/examples/README.md +315 -0
  78. package/examples/basic/README.md +111 -0
  79. package/examples/filters/README.md +262 -0
  80. package/examples/knowledge-base/README.md +207 -0
  81. package/examples/rate-limiting/README.md +376 -0
  82. package/package.json +59 -0
  83. package/scripts/verify-providers.js +238 -0
  84. package/src/embeddings/base.ts +25 -0
  85. package/src/embeddings/cohere.test.ts +408 -0
  86. package/src/embeddings/cohere.ts +152 -0
  87. package/src/embeddings/factory.test.ts +453 -0
  88. package/src/embeddings/factory.ts +163 -0
  89. package/src/embeddings/ollama.test.ts +543 -0
  90. package/src/embeddings/ollama.ts +196 -0
  91. package/src/embeddings/openai.test.ts +402 -0
  92. package/src/embeddings/openai.ts +158 -0
  93. package/src/embeddings/voyage.test.ts +520 -0
  94. package/src/embeddings/voyage.ts +168 -0
  95. package/src/index.test.ts +304 -0
  96. package/src/index.ts +614 -0
  97. package/src/qdrant/client.test.ts +456 -0
  98. package/src/qdrant/client.ts +195 -0
  99. package/tsconfig.json +19 -0
  100. package/vitest.config.ts +37 -0
@@ -0,0 +1,456 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { QdrantManager } from "./client.js";
3
+ import { QdrantClient } from "@qdrant/js-client-rest";
4
+
5
+ vi.mock("@qdrant/js-client-rest", () => ({
6
+ QdrantClient: vi.fn(),
7
+ }));
8
+
9
+ describe("QdrantManager", () => {
10
+ let manager: QdrantManager;
11
+ let mockClient: any;
12
+
13
+ beforeEach(() => {
14
+ mockClient = {
15
+ createCollection: vi.fn(),
16
+ getCollection: vi.fn(),
17
+ getCollections: vi.fn(),
18
+ deleteCollection: vi.fn(),
19
+ upsert: vi.fn(),
20
+ search: vi.fn(),
21
+ retrieve: vi.fn(),
22
+ delete: vi.fn(),
23
+ };
24
+
25
+ vi.mocked(QdrantClient).mockImplementation(() => mockClient as any);
26
+
27
+ manager = new QdrantManager("http://localhost:6333");
28
+ });
29
+
30
+ describe("createCollection", () => {
31
+ it("should create a collection with default distance metric", async () => {
32
+ await manager.createCollection("test-collection", 1536);
33
+
34
+ expect(mockClient.createCollection).toHaveBeenCalledWith(
35
+ "test-collection",
36
+ {
37
+ vectors: {
38
+ size: 1536,
39
+ distance: "Cosine",
40
+ },
41
+ },
42
+ );
43
+ });
44
+
45
+ it("should create a collection with custom distance metric", async () => {
46
+ await manager.createCollection("test-collection", 1536, "Euclid");
47
+
48
+ expect(mockClient.createCollection).toHaveBeenCalledWith(
49
+ "test-collection",
50
+ {
51
+ vectors: {
52
+ size: 1536,
53
+ distance: "Euclid",
54
+ },
55
+ },
56
+ );
57
+ });
58
+ });
59
+
60
+ describe("collectionExists", () => {
61
+ it("should return true if collection exists", async () => {
62
+ mockClient.getCollection.mockResolvedValue({ collection_name: "test" });
63
+
64
+ const exists = await manager.collectionExists("test");
65
+
66
+ expect(exists).toBe(true);
67
+ expect(mockClient.getCollection).toHaveBeenCalledWith("test");
68
+ });
69
+
70
+ it("should return false if collection does not exist", async () => {
71
+ mockClient.getCollection.mockRejectedValue(new Error("Not found"));
72
+
73
+ const exists = await manager.collectionExists("test");
74
+
75
+ expect(exists).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("listCollections", () => {
80
+ it("should return list of collection names", async () => {
81
+ mockClient.getCollections.mockResolvedValue({
82
+ collections: [
83
+ { name: "collection1" },
84
+ { name: "collection2" },
85
+ { name: "collection3" },
86
+ ],
87
+ });
88
+
89
+ const collections = await manager.listCollections();
90
+
91
+ expect(collections).toEqual([
92
+ "collection1",
93
+ "collection2",
94
+ "collection3",
95
+ ]);
96
+ });
97
+
98
+ it("should return empty array when no collections exist", async () => {
99
+ mockClient.getCollections.mockResolvedValue({
100
+ collections: [],
101
+ });
102
+
103
+ const collections = await manager.listCollections();
104
+
105
+ expect(collections).toEqual([]);
106
+ });
107
+ });
108
+
109
+ describe("getCollectionInfo", () => {
110
+ it("should return collection info with vector configuration", async () => {
111
+ mockClient.getCollection.mockResolvedValue({
112
+ collection_name: "test-collection",
113
+ points_count: 100,
114
+ config: {
115
+ params: {
116
+ vectors: {
117
+ size: 1536,
118
+ distance: "Cosine",
119
+ },
120
+ },
121
+ },
122
+ });
123
+
124
+ const info = await manager.getCollectionInfo("test-collection");
125
+
126
+ expect(info).toEqual({
127
+ name: "test-collection",
128
+ vectorSize: 1536,
129
+ pointsCount: 100,
130
+ distance: "Cosine",
131
+ });
132
+ });
133
+
134
+ it("should handle missing points_count", async () => {
135
+ mockClient.getCollection.mockResolvedValue({
136
+ collection_name: "test-collection",
137
+ config: {
138
+ params: {
139
+ vectors: {
140
+ size: 1536,
141
+ distance: "Dot",
142
+ },
143
+ },
144
+ },
145
+ });
146
+
147
+ const info = await manager.getCollectionInfo("test-collection");
148
+
149
+ expect(info.pointsCount).toBe(0);
150
+ });
151
+ });
152
+
153
+ describe("deleteCollection", () => {
154
+ it("should delete a collection", async () => {
155
+ await manager.deleteCollection("test-collection");
156
+
157
+ expect(mockClient.deleteCollection).toHaveBeenCalledWith(
158
+ "test-collection",
159
+ );
160
+ });
161
+ });
162
+
163
+ describe("addPoints", () => {
164
+ it("should add points to a collection", async () => {
165
+ const points = [
166
+ { id: 1, vector: [0.1, 0.2, 0.3], payload: { text: "test" } },
167
+ { id: 2, vector: [0.4, 0.5, 0.6], payload: { text: "test2" } },
168
+ ];
169
+
170
+ await manager.addPoints("test-collection", points);
171
+
172
+ expect(mockClient.upsert).toHaveBeenCalledWith("test-collection", {
173
+ wait: true,
174
+ points,
175
+ });
176
+ });
177
+
178
+ it("should add points without payload", async () => {
179
+ const points = [{ id: 1, vector: [0.1, 0.2, 0.3] }];
180
+
181
+ await manager.addPoints("test-collection", points);
182
+
183
+ expect(mockClient.upsert).toHaveBeenCalledWith("test-collection", {
184
+ wait: true,
185
+ points,
186
+ });
187
+ });
188
+
189
+ it("should normalize string IDs to UUID format", async () => {
190
+ const points = [
191
+ {
192
+ id: "my-custom-id",
193
+ vector: [0.1, 0.2, 0.3],
194
+ payload: { text: "test" },
195
+ },
196
+ ];
197
+
198
+ await manager.addPoints("test-collection", points);
199
+
200
+ // Verify the ID was normalized to UUID format
201
+ const calls = mockClient.upsert.mock.calls;
202
+ expect(calls).toHaveLength(1);
203
+ expect(calls[0][0]).toBe("test-collection");
204
+
205
+ const normalizedId = calls[0][1].points[0].id;
206
+ // Check that it's a valid UUID format
207
+ expect(normalizedId).toMatch(
208
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
209
+ );
210
+ // Ensure it's not the original ID
211
+ expect(normalizedId).not.toBe("my-custom-id");
212
+ });
213
+
214
+ it("should preserve UUID format IDs without modification", async () => {
215
+ const uuidId = "123e4567-e89b-12d3-a456-426614174000";
216
+ const points = [
217
+ { id: uuidId, vector: [0.1, 0.2, 0.3], payload: { text: "test" } },
218
+ ];
219
+
220
+ await manager.addPoints("test-collection", points);
221
+
222
+ // UUID should remain unchanged
223
+ expect(mockClient.upsert).toHaveBeenCalledWith("test-collection", {
224
+ wait: true,
225
+ points: [
226
+ {
227
+ id: uuidId,
228
+ vector: [0.1, 0.2, 0.3],
229
+ payload: { text: "test" },
230
+ },
231
+ ],
232
+ });
233
+ });
234
+
235
+ it("should throw error with error.data.status.error message", async () => {
236
+ const points = [{ id: 1, vector: [0.1, 0.2, 0.3] }];
237
+
238
+ mockClient.upsert.mockRejectedValue({
239
+ data: {
240
+ status: {
241
+ error: "Vector dimension mismatch",
242
+ },
243
+ },
244
+ });
245
+
246
+ await expect(
247
+ manager.addPoints("test-collection", points),
248
+ ).rejects.toThrow(
249
+ 'Failed to add points to collection "test-collection": Vector dimension mismatch',
250
+ );
251
+ });
252
+
253
+ it("should throw error with error.message fallback", async () => {
254
+ const points = [{ id: 1, vector: [0.1, 0.2, 0.3] }];
255
+
256
+ mockClient.upsert.mockRejectedValue(new Error("Network error"));
257
+
258
+ await expect(
259
+ manager.addPoints("test-collection", points),
260
+ ).rejects.toThrow(
261
+ 'Failed to add points to collection "test-collection": Network error',
262
+ );
263
+ });
264
+
265
+ it("should throw error with String(error) fallback", async () => {
266
+ const points = [{ id: 1, vector: [0.1, 0.2, 0.3] }];
267
+
268
+ mockClient.upsert.mockRejectedValue("Unknown error");
269
+
270
+ await expect(
271
+ manager.addPoints("test-collection", points),
272
+ ).rejects.toThrow(
273
+ 'Failed to add points to collection "test-collection": Unknown error',
274
+ );
275
+ });
276
+ });
277
+
278
+ describe("search", () => {
279
+ it("should search for similar vectors", async () => {
280
+ mockClient.search.mockResolvedValue([
281
+ { id: 1, score: 0.95, payload: { text: "result1" } },
282
+ { id: 2, score: 0.85, payload: { text: "result2" } },
283
+ ]);
284
+
285
+ const results = await manager.search(
286
+ "test-collection",
287
+ [0.1, 0.2, 0.3],
288
+ 5,
289
+ );
290
+
291
+ expect(results).toEqual([
292
+ { id: 1, score: 0.95, payload: { text: "result1" } },
293
+ { id: 2, score: 0.85, payload: { text: "result2" } },
294
+ ]);
295
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
296
+ vector: [0.1, 0.2, 0.3],
297
+ limit: 5,
298
+ filter: undefined,
299
+ });
300
+ });
301
+
302
+ it("should search with custom limit", async () => {
303
+ mockClient.search.mockResolvedValue([]);
304
+
305
+ await manager.search("test-collection", [0.1, 0.2, 0.3], 10);
306
+
307
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
308
+ vector: [0.1, 0.2, 0.3],
309
+ limit: 10,
310
+ filter: undefined,
311
+ });
312
+ });
313
+
314
+ it("should search with Qdrant format filter (must)", async () => {
315
+ mockClient.search.mockResolvedValue([]);
316
+
317
+ const filter = { must: [{ key: "category", match: { value: "test" } }] };
318
+ await manager.search("test-collection", [0.1, 0.2, 0.3], 5, filter);
319
+
320
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
321
+ vector: [0.1, 0.2, 0.3],
322
+ limit: 5,
323
+ filter,
324
+ });
325
+ });
326
+
327
+ it("should convert simple key-value filter to Qdrant format", async () => {
328
+ mockClient.search.mockResolvedValue([]);
329
+
330
+ const simpleFilter = { category: "database", type: "document" };
331
+ await manager.search("test-collection", [0.1, 0.2, 0.3], 5, simpleFilter);
332
+
333
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
334
+ vector: [0.1, 0.2, 0.3],
335
+ limit: 5,
336
+ filter: {
337
+ must: [
338
+ { key: "category", match: { value: "database" } },
339
+ { key: "type", match: { value: "document" } },
340
+ ],
341
+ },
342
+ });
343
+ });
344
+
345
+ it("should handle empty filter object", async () => {
346
+ mockClient.search.mockResolvedValue([]);
347
+
348
+ await manager.search("test-collection", [0.1, 0.2, 0.3], 5, {});
349
+
350
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
351
+ vector: [0.1, 0.2, 0.3],
352
+ limit: 5,
353
+ filter: undefined,
354
+ });
355
+ });
356
+
357
+ it("should search with Qdrant format filter (should)", async () => {
358
+ mockClient.search.mockResolvedValue([]);
359
+
360
+ const filter = {
361
+ should: [{ key: "tag", match: { value: "important" } }],
362
+ };
363
+ await manager.search("test-collection", [0.1, 0.2, 0.3], 5, filter);
364
+
365
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
366
+ vector: [0.1, 0.2, 0.3],
367
+ limit: 5,
368
+ filter,
369
+ });
370
+ });
371
+
372
+ it("should search with Qdrant format filter (must_not)", async () => {
373
+ mockClient.search.mockResolvedValue([]);
374
+
375
+ const filter = {
376
+ must_not: [{ key: "status", match: { value: "deleted" } }],
377
+ };
378
+ await manager.search("test-collection", [0.1, 0.2, 0.3], 5, filter);
379
+
380
+ expect(mockClient.search).toHaveBeenCalledWith("test-collection", {
381
+ vector: [0.1, 0.2, 0.3],
382
+ limit: 5,
383
+ filter,
384
+ });
385
+ });
386
+
387
+ it("should handle null payload in results", async () => {
388
+ mockClient.search.mockResolvedValue([
389
+ { id: 1, score: 0.95, payload: null },
390
+ ]);
391
+
392
+ const results = await manager.search("test-collection", [0.1, 0.2, 0.3]);
393
+
394
+ expect(results).toEqual([{ id: 1, score: 0.95, payload: undefined }]);
395
+ });
396
+ });
397
+
398
+ describe("getPoint", () => {
399
+ it("should retrieve a point by id", async () => {
400
+ mockClient.retrieve.mockResolvedValue([
401
+ { id: 1, payload: { text: "test" } },
402
+ ]);
403
+
404
+ const point = await manager.getPoint("test-collection", 1);
405
+
406
+ expect(point).toEqual({ id: 1, payload: { text: "test" } });
407
+ expect(mockClient.retrieve).toHaveBeenCalledWith("test-collection", {
408
+ ids: [1],
409
+ });
410
+ });
411
+
412
+ it("should return null if point not found", async () => {
413
+ mockClient.retrieve.mockResolvedValue([]);
414
+
415
+ const point = await manager.getPoint("test-collection", 1);
416
+
417
+ expect(point).toBeNull();
418
+ });
419
+
420
+ it("should handle errors gracefully", async () => {
421
+ mockClient.retrieve.mockRejectedValue(new Error("Not found"));
422
+
423
+ const point = await manager.getPoint("test-collection", 1);
424
+
425
+ expect(point).toBeNull();
426
+ });
427
+
428
+ it("should handle null payload", async () => {
429
+ mockClient.retrieve.mockResolvedValue([{ id: 1, payload: null }]);
430
+
431
+ const point = await manager.getPoint("test-collection", 1);
432
+
433
+ expect(point).toEqual({ id: 1, payload: undefined });
434
+ });
435
+ });
436
+
437
+ describe("deletePoints", () => {
438
+ it("should delete points by ids", async () => {
439
+ await manager.deletePoints("test-collection", [1, 2, 3]);
440
+
441
+ expect(mockClient.delete).toHaveBeenCalledWith("test-collection", {
442
+ wait: true,
443
+ points: [1, 2, 3],
444
+ });
445
+ });
446
+
447
+ it("should delete single point", async () => {
448
+ await manager.deletePoints("test-collection", ["doc-1"]);
449
+
450
+ expect(mockClient.delete).toHaveBeenCalledWith("test-collection", {
451
+ wait: true,
452
+ points: ["bb0e4f49-4437-94d9-01e8-969ff11bd112"], // Normalized UUID from 'doc-1'
453
+ });
454
+ });
455
+ });
456
+ });
@@ -0,0 +1,195 @@
1
+ import { QdrantClient } from '@qdrant/js-client-rest';
2
+ import { createHash } from 'crypto';
3
+
4
+ export interface CollectionInfo {
5
+ name: string;
6
+ vectorSize: number;
7
+ pointsCount: number;
8
+ distance: 'Cosine' | 'Euclid' | 'Dot';
9
+ }
10
+
11
+ export interface SearchResult {
12
+ id: string | number;
13
+ score: number;
14
+ payload?: Record<string, any>;
15
+ }
16
+
17
+ export class QdrantManager {
18
+ private client: QdrantClient;
19
+
20
+ constructor(url: string = 'http://localhost:6333') {
21
+ this.client = new QdrantClient({ url });
22
+ }
23
+
24
+ /**
25
+ * Converts a string ID to UUID format if it's not already a UUID.
26
+ * Qdrant requires string IDs to be in UUID format.
27
+ */
28
+ private normalizeId(id: string | number): string | number {
29
+ if (typeof id === 'number') {
30
+ return id;
31
+ }
32
+
33
+ // Check if already a valid UUID (8-4-4-4-12 format)
34
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
35
+ if (uuidRegex.test(id)) {
36
+ return id;
37
+ }
38
+
39
+ // Convert arbitrary string to deterministic UUID v5-like format
40
+ const hash = createHash('sha256').update(id).digest('hex');
41
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
42
+ }
43
+
44
+ async createCollection(
45
+ name: string,
46
+ vectorSize: number,
47
+ distance: 'Cosine' | 'Euclid' | 'Dot' = 'Cosine'
48
+ ): Promise<void> {
49
+ await this.client.createCollection(name, {
50
+ vectors: {
51
+ size: vectorSize,
52
+ distance,
53
+ },
54
+ });
55
+ }
56
+
57
+ async collectionExists(name: string): Promise<boolean> {
58
+ try {
59
+ await this.client.getCollection(name);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ async listCollections(): Promise<string[]> {
67
+ const response = await this.client.getCollections();
68
+ return response.collections.map((c) => c.name);
69
+ }
70
+
71
+ async getCollectionInfo(name: string): Promise<CollectionInfo> {
72
+ const info = await this.client.getCollection(name);
73
+ const vectorConfig = info.config.params.vectors;
74
+
75
+ // Handle both named and unnamed vector configurations
76
+ let size = 0;
77
+ let distance: 'Cosine' | 'Euclid' | 'Dot' = 'Cosine';
78
+
79
+ if (typeof vectorConfig === 'object' && vectorConfig !== null && 'size' in vectorConfig) {
80
+ size = typeof vectorConfig.size === 'number' ? vectorConfig.size : 0;
81
+ distance = vectorConfig.distance as 'Cosine' | 'Euclid' | 'Dot';
82
+ }
83
+
84
+ return {
85
+ name,
86
+ vectorSize: size,
87
+ pointsCount: info.points_count || 0,
88
+ distance,
89
+ };
90
+ }
91
+
92
+ async deleteCollection(name: string): Promise<void> {
93
+ await this.client.deleteCollection(name);
94
+ }
95
+
96
+ async addPoints(
97
+ collectionName: string,
98
+ points: Array<{
99
+ id: string | number;
100
+ vector: number[];
101
+ payload?: Record<string, any>;
102
+ }>
103
+ ): Promise<void> {
104
+ try {
105
+ // Normalize all IDs to ensure string IDs are in UUID format
106
+ const normalizedPoints = points.map(point => ({
107
+ ...point,
108
+ id: this.normalizeId(point.id),
109
+ }));
110
+
111
+ await this.client.upsert(collectionName, {
112
+ wait: true,
113
+ points: normalizedPoints,
114
+ });
115
+ } catch (error: any) {
116
+ const errorMessage = error?.data?.status?.error || error?.message || String(error);
117
+ throw new Error(`Failed to add points to collection "${collectionName}": ${errorMessage}`);
118
+ }
119
+ }
120
+
121
+ async search(
122
+ collectionName: string,
123
+ vector: number[],
124
+ limit: number = 5,
125
+ filter?: Record<string, any>
126
+ ): Promise<SearchResult[]> {
127
+ // Convert simple key-value filter to Qdrant filter format
128
+ // Accepts either:
129
+ // 1. Simple format: {"category": "database"}
130
+ // 2. Qdrant format: {must: [{key: "category", match: {value: "database"}}]}
131
+ let qdrantFilter;
132
+ if (filter && Object.keys(filter).length > 0) {
133
+ // Check if already in Qdrant format (has must/should/must_not keys)
134
+ if (filter.must || filter.should || filter.must_not) {
135
+ qdrantFilter = filter;
136
+ } else {
137
+ // Convert simple key-value format to Qdrant format
138
+ qdrantFilter = {
139
+ must: Object.entries(filter).map(([key, value]) => ({
140
+ key,
141
+ match: { value },
142
+ })),
143
+ };
144
+ }
145
+ }
146
+
147
+ const results = await this.client.search(collectionName, {
148
+ vector,
149
+ limit,
150
+ filter: qdrantFilter,
151
+ });
152
+
153
+ return results.map((result) => ({
154
+ id: result.id,
155
+ score: result.score,
156
+ payload: result.payload || undefined,
157
+ }));
158
+ }
159
+
160
+ async getPoint(
161
+ collectionName: string,
162
+ id: string | number
163
+ ): Promise<{ id: string | number; payload?: Record<string, any> } | null> {
164
+ try {
165
+ const normalizedId = this.normalizeId(id);
166
+ const points = await this.client.retrieve(collectionName, {
167
+ ids: [normalizedId],
168
+ });
169
+
170
+ if (points.length === 0) {
171
+ return null;
172
+ }
173
+
174
+ return {
175
+ id: points[0].id,
176
+ payload: points[0].payload || undefined,
177
+ };
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ async deletePoints(
184
+ collectionName: string,
185
+ ids: (string | number)[]
186
+ ): Promise<void> {
187
+ // Normalize IDs to ensure string IDs are in UUID format
188
+ const normalizedIds = ids.map(id => this.normalizeId(id));
189
+
190
+ await this.client.delete(collectionName, {
191
+ wait: true,
192
+ points: normalizedIds,
193
+ });
194
+ }
195
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./build",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "build"]
19
+ }
@@ -0,0 +1,37 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: ["text", "json", "html"],
10
+ exclude: [
11
+ "node_modules/",
12
+ "build/",
13
+ "dist/",
14
+ "**/*.test.ts",
15
+ "**/*.spec.ts",
16
+ "vitest.config.ts",
17
+ "src/index.ts", // MCP server SDK integration - tested via integration
18
+ "scripts/**", // Exclude utility scripts from coverage
19
+ ],
20
+ // Set thresholds for core business logic modules
21
+ thresholds: {
22
+ "src/qdrant/client.ts": {
23
+ lines: 90,
24
+ functions: 100,
25
+ branches: 80,
26
+ statements: 90,
27
+ },
28
+ "src/embeddings/openai.ts": {
29
+ lines: 100,
30
+ functions: 100,
31
+ branches: 90,
32
+ statements: 100,
33
+ },
34
+ },
35
+ },
36
+ },
37
+ });