@mhalder/qdrant-mcp-server 1.1.1 → 1.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/CHANGELOG.md +5 -0
- package/README.md +2 -0
- package/biome.json +34 -0
- package/build/embeddings/sparse.d.ts +40 -0
- package/build/embeddings/sparse.d.ts.map +1 -0
- package/build/embeddings/sparse.js +105 -0
- package/build/embeddings/sparse.js.map +1 -0
- package/build/embeddings/sparse.test.d.ts +2 -0
- package/build/embeddings/sparse.test.d.ts.map +1 -0
- package/build/embeddings/sparse.test.js +69 -0
- package/build/embeddings/sparse.test.js.map +1 -0
- package/build/index.js +130 -30
- package/build/index.js.map +1 -1
- package/build/qdrant/client.d.ts +21 -2
- package/build/qdrant/client.d.ts.map +1 -1
- package/build/qdrant/client.js +131 -17
- package/build/qdrant/client.js.map +1 -1
- package/build/qdrant/client.test.js +429 -21
- package/build/qdrant/client.test.js.map +1 -1
- package/examples/README.md +16 -1
- package/examples/basic/README.md +1 -0
- package/examples/hybrid-search/README.md +199 -0
- package/package.json +1 -1
- package/src/embeddings/sparse.test.ts +87 -0
- package/src/embeddings/sparse.ts +127 -0
- package/src/index.ts +161 -57
- package/src/qdrant/client.test.ts +544 -56
- package/src/qdrant/client.ts +162 -22
- package/vitest.config.ts +3 -3
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
8
|
import {
|
|
@@ -8,24 +11,18 @@ import {
|
|
|
8
11
|
ListToolsRequestSchema,
|
|
9
12
|
ReadResourceRequestSchema,
|
|
10
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
import { QdrantManager } from "./qdrant/client.js";
|
|
12
|
-
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
13
14
|
import { z } from "zod";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
16
|
+
import { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
|
|
17
|
+
import { QdrantManager } from "./qdrant/client.js";
|
|
17
18
|
|
|
18
19
|
// Read package.json for version
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const pkg = JSON.parse(
|
|
21
|
-
readFileSync(join(__dirname, "../package.json"), "utf-8"),
|
|
22
|
-
);
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
23
22
|
|
|
24
23
|
// Validate environment variables
|
|
25
24
|
const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333";
|
|
26
|
-
const EMBEDDING_PROVIDER = (
|
|
27
|
-
process.env.EMBEDDING_PROVIDER || "ollama"
|
|
28
|
-
).toLowerCase();
|
|
25
|
+
const EMBEDDING_PROVIDER = (process.env.EMBEDDING_PROVIDER || "ollama").toLowerCase();
|
|
29
26
|
|
|
30
27
|
// Check for required API keys based on provider
|
|
31
28
|
if (EMBEDDING_PROVIDER !== "ollama") {
|
|
@@ -47,15 +44,13 @@ if (EMBEDDING_PROVIDER !== "ollama") {
|
|
|
47
44
|
break;
|
|
48
45
|
default:
|
|
49
46
|
console.error(
|
|
50
|
-
`Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama
|
|
47
|
+
`Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama.`
|
|
51
48
|
);
|
|
52
49
|
process.exit(1);
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
if (!apiKey) {
|
|
56
|
-
console.error(
|
|
57
|
-
`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`,
|
|
58
|
-
);
|
|
53
|
+
console.error(`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`);
|
|
59
54
|
process.exit(1);
|
|
60
55
|
}
|
|
61
56
|
}
|
|
@@ -64,8 +59,7 @@ if (EMBEDDING_PROVIDER !== "ollama") {
|
|
|
64
59
|
async function checkOllamaAvailability() {
|
|
65
60
|
if (EMBEDDING_PROVIDER === "ollama") {
|
|
66
61
|
const baseUrl = process.env.EMBEDDING_BASE_URL || "http://localhost:11434";
|
|
67
|
-
const isLocalhost =
|
|
68
|
-
baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
|
|
62
|
+
const isLocalhost = baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
|
|
69
63
|
|
|
70
64
|
try {
|
|
71
65
|
const response = await fetch(`${baseUrl}/api/version`);
|
|
@@ -78,7 +72,7 @@ async function checkOllamaAvailability() {
|
|
|
78
72
|
const { models } = await tagsResponse.json();
|
|
79
73
|
const modelName = process.env.EMBEDDING_MODEL || "nomic-embed-text";
|
|
80
74
|
const modelExists = models.some(
|
|
81
|
-
(m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
|
|
75
|
+
(m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
|
|
82
76
|
);
|
|
83
77
|
|
|
84
78
|
if (!modelExists) {
|
|
@@ -141,7 +135,7 @@ const server = new Server(
|
|
|
141
135
|
tools: {},
|
|
142
136
|
resources: {},
|
|
143
137
|
},
|
|
144
|
-
}
|
|
138
|
+
}
|
|
145
139
|
);
|
|
146
140
|
|
|
147
141
|
// Tool schemas
|
|
@@ -151,6 +145,10 @@ const CreateCollectionSchema = z.object({
|
|
|
151
145
|
.enum(["Cosine", "Euclid", "Dot"])
|
|
152
146
|
.optional()
|
|
153
147
|
.describe("Distance metric (default: Cosine)"),
|
|
148
|
+
enableHybrid: z
|
|
149
|
+
.boolean()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Enable hybrid search with sparse vectors (default: false)"),
|
|
154
152
|
});
|
|
155
153
|
|
|
156
154
|
const AddDocumentsSchema = z.object({
|
|
@@ -158,15 +156,13 @@ const AddDocumentsSchema = z.object({
|
|
|
158
156
|
documents: z
|
|
159
157
|
.array(
|
|
160
158
|
z.object({
|
|
161
|
-
id: z
|
|
162
|
-
.union([z.string(), z.number()])
|
|
163
|
-
.describe("Unique identifier for the document"),
|
|
159
|
+
id: z.union([z.string(), z.number()]).describe("Unique identifier for the document"),
|
|
164
160
|
text: z.string().describe("Text content to embed and store"),
|
|
165
161
|
metadata: z
|
|
166
162
|
.record(z.any())
|
|
167
163
|
.optional()
|
|
168
164
|
.describe("Optional metadata to store with the document"),
|
|
169
|
-
})
|
|
165
|
+
})
|
|
170
166
|
)
|
|
171
167
|
.describe("Array of documents to add"),
|
|
172
168
|
});
|
|
@@ -174,10 +170,7 @@ const AddDocumentsSchema = z.object({
|
|
|
174
170
|
const SemanticSearchSchema = z.object({
|
|
175
171
|
collection: z.string().describe("Name of the collection to search"),
|
|
176
172
|
query: z.string().describe("Search query text"),
|
|
177
|
-
limit: z
|
|
178
|
-
.number()
|
|
179
|
-
.optional()
|
|
180
|
-
.describe("Maximum number of results (default: 5)"),
|
|
173
|
+
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
181
174
|
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
182
175
|
});
|
|
183
176
|
|
|
@@ -191,9 +184,14 @@ const GetCollectionInfoSchema = z.object({
|
|
|
191
184
|
|
|
192
185
|
const DeleteDocumentsSchema = z.object({
|
|
193
186
|
collection: z.string().describe("Name of the collection"),
|
|
194
|
-
ids: z
|
|
195
|
-
|
|
196
|
-
|
|
187
|
+
ids: z.array(z.union([z.string(), z.number()])).describe("Array of document IDs to delete"),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const HybridSearchSchema = z.object({
|
|
191
|
+
collection: z.string().describe("Name of the collection to search"),
|
|
192
|
+
query: z.string().describe("Search query text"),
|
|
193
|
+
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
194
|
+
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
197
195
|
});
|
|
198
196
|
|
|
199
197
|
// List available tools
|
|
@@ -203,7 +201,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
203
201
|
{
|
|
204
202
|
name: "create_collection",
|
|
205
203
|
description:
|
|
206
|
-
"Create a new vector collection in Qdrant. The collection will be configured with the embedding provider's dimensions automatically.",
|
|
204
|
+
"Create a new vector collection in Qdrant. The collection will be configured with the embedding provider's dimensions automatically. Set enableHybrid to true to enable hybrid search combining semantic and keyword search.",
|
|
207
205
|
inputSchema: {
|
|
208
206
|
type: "object",
|
|
209
207
|
properties: {
|
|
@@ -216,6 +214,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
216
214
|
enum: ["Cosine", "Euclid", "Dot"],
|
|
217
215
|
description: "Distance metric (default: Cosine)",
|
|
218
216
|
},
|
|
217
|
+
enableHybrid: {
|
|
218
|
+
type: "boolean",
|
|
219
|
+
description: "Enable hybrid search with sparse vectors (default: false)",
|
|
220
|
+
},
|
|
219
221
|
},
|
|
220
222
|
required: ["name"],
|
|
221
223
|
},
|
|
@@ -323,8 +325,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
323
325
|
},
|
|
324
326
|
{
|
|
325
327
|
name: "delete_documents",
|
|
326
|
-
description:
|
|
327
|
-
"Delete specific documents from a collection by their IDs.",
|
|
328
|
+
description: "Delete specific documents from a collection by their IDs.",
|
|
328
329
|
inputSchema: {
|
|
329
330
|
type: "object",
|
|
330
331
|
properties: {
|
|
@@ -343,6 +344,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
343
344
|
required: ["collection", "ids"],
|
|
344
345
|
},
|
|
345
346
|
},
|
|
347
|
+
{
|
|
348
|
+
name: "hybrid_search",
|
|
349
|
+
description:
|
|
350
|
+
"Perform hybrid search combining semantic vector search with keyword search using BM25. This provides better results by combining the strengths of both approaches. The collection must be created with enableHybrid set to true.",
|
|
351
|
+
inputSchema: {
|
|
352
|
+
type: "object",
|
|
353
|
+
properties: {
|
|
354
|
+
collection: {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "Name of the collection to search",
|
|
357
|
+
},
|
|
358
|
+
query: {
|
|
359
|
+
type: "string",
|
|
360
|
+
description: "Search query text",
|
|
361
|
+
},
|
|
362
|
+
limit: {
|
|
363
|
+
type: "number",
|
|
364
|
+
description: "Maximum number of results (default: 5)",
|
|
365
|
+
},
|
|
366
|
+
filter: {
|
|
367
|
+
type: "object",
|
|
368
|
+
description: "Optional metadata filter",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
required: ["collection", "query"],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
346
374
|
],
|
|
347
375
|
};
|
|
348
376
|
});
|
|
@@ -354,14 +382,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
354
382
|
try {
|
|
355
383
|
switch (name) {
|
|
356
384
|
case "create_collection": {
|
|
357
|
-
const { name, distance } = CreateCollectionSchema.parse(args);
|
|
385
|
+
const { name, distance, enableHybrid } = CreateCollectionSchema.parse(args);
|
|
358
386
|
const vectorSize = embeddings.getDimensions();
|
|
359
|
-
await qdrant.createCollection(name, vectorSize, distance);
|
|
387
|
+
await qdrant.createCollection(name, vectorSize, distance, enableHybrid || false);
|
|
388
|
+
|
|
389
|
+
let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
|
|
390
|
+
if (enableHybrid) {
|
|
391
|
+
message += " Hybrid search is enabled for this collection.";
|
|
392
|
+
}
|
|
393
|
+
|
|
360
394
|
return {
|
|
361
395
|
content: [
|
|
362
396
|
{
|
|
363
397
|
type: "text",
|
|
364
|
-
text:
|
|
398
|
+
text: message,
|
|
365
399
|
},
|
|
366
400
|
],
|
|
367
401
|
};
|
|
@@ -370,7 +404,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
370
404
|
case "add_documents": {
|
|
371
405
|
const { collection, documents } = AddDocumentsSchema.parse(args);
|
|
372
406
|
|
|
373
|
-
// Check if collection exists
|
|
407
|
+
// Check if collection exists and get info
|
|
374
408
|
const exists = await qdrant.collectionExists(collection);
|
|
375
409
|
if (!exists) {
|
|
376
410
|
return {
|
|
@@ -384,21 +418,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
384
418
|
};
|
|
385
419
|
}
|
|
386
420
|
|
|
421
|
+
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
422
|
+
|
|
387
423
|
// Generate embeddings for all documents
|
|
388
424
|
const texts = documents.map((doc) => doc.text);
|
|
389
425
|
const embeddingResults = await embeddings.embedBatch(texts);
|
|
390
426
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
427
|
+
// If hybrid search is enabled, generate sparse vectors and use appropriate method
|
|
428
|
+
if (collectionInfo.hybridEnabled) {
|
|
429
|
+
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
430
|
+
|
|
431
|
+
// Prepare points with both dense and sparse vectors
|
|
432
|
+
const points = documents.map((doc, index) => ({
|
|
433
|
+
id: doc.id,
|
|
434
|
+
vector: embeddingResults[index].embedding,
|
|
435
|
+
sparseVector: sparseGenerator.generate(doc.text),
|
|
436
|
+
payload: {
|
|
437
|
+
text: doc.text,
|
|
438
|
+
...doc.metadata,
|
|
439
|
+
},
|
|
440
|
+
}));
|
|
400
441
|
|
|
401
|
-
|
|
442
|
+
await qdrant.addPointsWithSparse(collection, points);
|
|
443
|
+
} else {
|
|
444
|
+
// Standard dense-only vectors
|
|
445
|
+
const points = documents.map((doc, index) => ({
|
|
446
|
+
id: doc.id,
|
|
447
|
+
vector: embeddingResults[index].embedding,
|
|
448
|
+
payload: {
|
|
449
|
+
text: doc.text,
|
|
450
|
+
...doc.metadata,
|
|
451
|
+
},
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
await qdrant.addPoints(collection, points);
|
|
455
|
+
}
|
|
402
456
|
|
|
403
457
|
return {
|
|
404
458
|
content: [
|
|
@@ -411,8 +465,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
411
465
|
}
|
|
412
466
|
|
|
413
467
|
case "semantic_search": {
|
|
414
|
-
const { collection, query, limit, filter } =
|
|
415
|
-
SemanticSearchSchema.parse(args);
|
|
468
|
+
const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
|
|
416
469
|
|
|
417
470
|
// Check if collection exists
|
|
418
471
|
const exists = await qdrant.collectionExists(collection);
|
|
@@ -432,12 +485,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
485
|
const { embedding } = await embeddings.embed(query);
|
|
433
486
|
|
|
434
487
|
// Search
|
|
435
|
-
const results = await qdrant.search(
|
|
436
|
-
collection,
|
|
437
|
-
embedding,
|
|
438
|
-
limit || 5,
|
|
439
|
-
filter,
|
|
440
|
-
);
|
|
488
|
+
const results = await qdrant.search(collection, embedding, limit || 5, filter);
|
|
441
489
|
|
|
442
490
|
return {
|
|
443
491
|
content: [
|
|
@@ -500,6 +548,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
500
548
|
};
|
|
501
549
|
}
|
|
502
550
|
|
|
551
|
+
case "hybrid_search": {
|
|
552
|
+
const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
|
|
553
|
+
|
|
554
|
+
// Check if collection exists
|
|
555
|
+
const exists = await qdrant.collectionExists(collection);
|
|
556
|
+
if (!exists) {
|
|
557
|
+
return {
|
|
558
|
+
content: [
|
|
559
|
+
{
|
|
560
|
+
type: "text",
|
|
561
|
+
text: `Error: Collection "${collection}" does not exist.`,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
isError: true,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check if collection has hybrid search enabled
|
|
569
|
+
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
570
|
+
if (!collectionInfo.hybridEnabled) {
|
|
571
|
+
return {
|
|
572
|
+
content: [
|
|
573
|
+
{
|
|
574
|
+
type: "text",
|
|
575
|
+
text: `Error: Collection "${collection}" does not have hybrid search enabled. Create a new collection with enableHybrid set to true.`,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
isError: true,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Generate dense embedding for query
|
|
583
|
+
const { embedding } = await embeddings.embed(query);
|
|
584
|
+
|
|
585
|
+
// Generate sparse vector for query
|
|
586
|
+
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
587
|
+
const sparseVector = sparseGenerator.generate(query);
|
|
588
|
+
|
|
589
|
+
// Perform hybrid search
|
|
590
|
+
const results = await qdrant.hybridSearch(
|
|
591
|
+
collection,
|
|
592
|
+
embedding,
|
|
593
|
+
sparseVector,
|
|
594
|
+
limit || 5,
|
|
595
|
+
filter
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: JSON.stringify(results, null, 2),
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
503
608
|
default:
|
|
504
609
|
return {
|
|
505
610
|
content: [
|
|
@@ -513,8 +618,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
513
618
|
}
|
|
514
619
|
} catch (error: any) {
|
|
515
620
|
// Enhanced error details for debugging
|
|
516
|
-
const errorDetails =
|
|
517
|
-
error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
621
|
+
const errorDetails = error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
518
622
|
|
|
519
623
|
console.error("Tool execution error:", {
|
|
520
624
|
tool: name,
|