@mhalder/qdrant-mcp-server 1.1.1 → 1.3.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.
@@ -0,0 +1,168 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ describe("Transport Configuration", () => {
3
+ let originalEnv;
4
+ beforeEach(() => {
5
+ originalEnv = { ...process.env };
6
+ });
7
+ afterEach(() => {
8
+ process.env = originalEnv;
9
+ });
10
+ describe("HTTP Port Validation", () => {
11
+ it("should accept valid port numbers", () => {
12
+ const validPorts = ["1", "80", "443", "3000", "8080", "65535"];
13
+ validPorts.forEach((port) => {
14
+ const parsed = parseInt(port, 10);
15
+ expect(parsed).toBeGreaterThanOrEqual(1);
16
+ expect(parsed).toBeLessThanOrEqual(65535);
17
+ expect(Number.isNaN(parsed)).toBe(false);
18
+ });
19
+ });
20
+ it("should reject invalid port numbers", () => {
21
+ const invalidCases = [
22
+ { port: "0", reason: "port 0 is reserved" },
23
+ { port: "-1", reason: "negative ports are invalid" },
24
+ { port: "65536", reason: "exceeds maximum port" },
25
+ { port: "99999", reason: "exceeds maximum port" },
26
+ { port: "abc", reason: "non-numeric input" },
27
+ { port: "", reason: "empty string" },
28
+ ];
29
+ invalidCases.forEach(({ port, reason }) => {
30
+ const parsed = parseInt(port, 10);
31
+ const isValid = !Number.isNaN(parsed) && parsed >= 1 && parsed <= 65535;
32
+ expect(isValid, `Failed for ${port}: ${reason}`).toBe(false);
33
+ });
34
+ });
35
+ it("should use default port 3000 when HTTP_PORT is not set", () => {
36
+ const port = parseInt(process.env.HTTP_PORT || "3000", 10);
37
+ expect(port).toBe(3000);
38
+ });
39
+ it("should parse HTTP_PORT from environment", () => {
40
+ process.env.HTTP_PORT = "8080";
41
+ const port = parseInt(process.env.HTTP_PORT || "3000", 10);
42
+ expect(port).toBe(8080);
43
+ });
44
+ });
45
+ describe("Transport Mode Validation", () => {
46
+ it("should accept valid transport modes", () => {
47
+ const validModes = ["stdio", "http", "STDIO", "HTTP"];
48
+ validModes.forEach((mode) => {
49
+ const normalized = mode.toLowerCase();
50
+ expect(["stdio", "http"]).toContain(normalized);
51
+ });
52
+ });
53
+ it("should reject invalid transport modes", () => {
54
+ const invalidModes = ["tcp", "websocket", "grpc", ""];
55
+ invalidModes.forEach((mode) => {
56
+ const normalized = mode.toLowerCase();
57
+ expect(["stdio", "http"]).not.toContain(normalized);
58
+ });
59
+ });
60
+ it("should default to stdio when TRANSPORT_MODE is not set", () => {
61
+ const mode = (process.env.TRANSPORT_MODE || "stdio").toLowerCase();
62
+ expect(mode).toBe("stdio");
63
+ });
64
+ });
65
+ describe("Request Size Limits", () => {
66
+ it("should define request size limit for HTTP transport", () => {
67
+ const limit = "10mb";
68
+ expect(limit).toMatch(/^\d+mb$/);
69
+ });
70
+ it("should parse size limit correctly", () => {
71
+ const limit = "10mb";
72
+ const sizeInBytes = parseInt(limit, 10) * 1024 * 1024;
73
+ expect(sizeInBytes).toBe(10485760);
74
+ });
75
+ });
76
+ });
77
+ describe("HTTP Server Configuration", () => {
78
+ describe("Graceful Shutdown", () => {
79
+ it("should handle shutdown signals", async () => {
80
+ const mockServer = {
81
+ close: vi.fn((callback) => {
82
+ if (callback)
83
+ callback();
84
+ }),
85
+ };
86
+ const shutdown = () => {
87
+ mockServer.close(() => {
88
+ // Shutdown callback
89
+ });
90
+ };
91
+ shutdown();
92
+ expect(mockServer.close).toHaveBeenCalled();
93
+ });
94
+ it("should support timeout for forced shutdown", () => {
95
+ vi.useFakeTimers();
96
+ let forcedShutdown = false;
97
+ const timeout = setTimeout(() => {
98
+ forcedShutdown = true;
99
+ }, 10000);
100
+ vi.advanceTimersByTime(9999);
101
+ expect(forcedShutdown).toBe(false);
102
+ vi.advanceTimersByTime(1);
103
+ expect(forcedShutdown).toBe(true);
104
+ clearTimeout(timeout);
105
+ vi.useRealTimers();
106
+ });
107
+ });
108
+ describe("Error Handling", () => {
109
+ it("should return JSON-RPC 2.0 error format", () => {
110
+ const error = {
111
+ jsonrpc: "2.0",
112
+ error: {
113
+ code: -32603,
114
+ message: "Internal server error",
115
+ },
116
+ id: null,
117
+ };
118
+ expect(error.jsonrpc).toBe("2.0");
119
+ expect(error.error.code).toBe(-32603);
120
+ expect(error.error.message).toBeTruthy();
121
+ expect(error.id).toBeNull();
122
+ });
123
+ it("should use standard JSON-RPC error codes", () => {
124
+ const errorCodes = {
125
+ parseError: -32700,
126
+ invalidRequest: -32600,
127
+ methodNotFound: -32601,
128
+ invalidParams: -32602,
129
+ internalError: -32603,
130
+ };
131
+ expect(errorCodes.parseError).toBe(-32700);
132
+ expect(errorCodes.invalidRequest).toBe(-32600);
133
+ expect(errorCodes.methodNotFound).toBe(-32601);
134
+ expect(errorCodes.invalidParams).toBe(-32602);
135
+ expect(errorCodes.internalError).toBe(-32603);
136
+ });
137
+ });
138
+ });
139
+ describe("Transport Lifecycle", () => {
140
+ it("should close transport on response close", () => {
141
+ const mockTransport = {
142
+ close: vi.fn(),
143
+ };
144
+ const mockResponse = {
145
+ on: vi.fn((event, callback) => {
146
+ if (event === "close") {
147
+ callback();
148
+ }
149
+ }),
150
+ };
151
+ mockResponse.on("close", () => {
152
+ mockTransport.close();
153
+ });
154
+ expect(mockTransport.close).toHaveBeenCalled();
155
+ });
156
+ it("should handle transport connection", async () => {
157
+ const mockServer = {
158
+ connect: vi.fn().mockResolvedValue(undefined),
159
+ };
160
+ const mockTransport = {
161
+ handleRequest: vi.fn().mockResolvedValue(undefined),
162
+ close: vi.fn(),
163
+ };
164
+ await mockServer.connect(mockTransport);
165
+ expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
166
+ });
167
+ });
168
+ //# sourceMappingURL=transport.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.test.js","sourceRoot":"","sources":["../src/transport.test.ts"],"names":[],"mappings":"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"}
@@ -31,6 +31,21 @@ Configure MCP server as described in [main README](../README.md).
31
31
 
32
32
  ---
33
33
 
34
+ ### 🔀 [Hybrid Search](./hybrid-search/)
35
+
36
+ Combine semantic and keyword search for better results
37
+
38
+ - Understanding hybrid search benefits
39
+ - Creating hybrid-enabled collections
40
+ - Comparing semantic vs hybrid search
41
+ - Best practices for technical content
42
+
43
+ **Use cases:** Technical docs, product search, legal documents, code search
44
+
45
+ **Time:** 15-20 minutes | **Difficulty:** Intermediate
46
+
47
+ ---
48
+
34
49
  ### ⚡ [Rate Limiting](./rate-limiting/)
35
50
 
36
51
  Automatic rate limit handling for batch operations
@@ -77,7 +92,7 @@ Complex search filters with boolean logic
77
92
  ## Learning Path
78
93
 
79
94
  ```
80
- Basic → Rate Limiting → Knowledge Base → Advanced Filtering
95
+ Basic → Hybrid Search → Rate Limiting → Knowledge Base → Advanced Filtering
81
96
  ```
82
97
 
83
98
  ## Common Patterns
@@ -52,6 +52,7 @@ Delete collection "my-first-collection"
52
52
 
53
53
  Continue learning with these examples:
54
54
 
55
+ - **[Hybrid Search](../hybrid-search/)** - Combine semantic and keyword search
55
56
  - **[Knowledge Base](../knowledge-base/)** - Metadata and content organization
56
57
  - **[Advanced Filtering](../filters/)** - Complex search queries
57
58
  - **[Rate Limiting](../rate-limiting/)** - Batch processing patterns
@@ -0,0 +1,236 @@
1
+ # Hybrid Search
2
+
3
+ Combine semantic vector search with keyword (BM25) search for more accurate and comprehensive results.
4
+
5
+ **Time:** 15-20 minutes | **Difficulty:** Intermediate
6
+
7
+ ## What is Hybrid Search?
8
+
9
+ Hybrid search combines two search approaches:
10
+
11
+ 1. **Semantic Search**: Understands meaning and context using vector embeddings
12
+ 2. **Keyword Search**: Exact term matching using BM25 sparse vectors
13
+
14
+ The results are merged using **Reciprocal Rank Fusion (RRF)**, which combines rankings from both methods to produce the best overall results.
15
+
16
+ ## When to Use Hybrid Search
17
+
18
+ Hybrid search is ideal for:
19
+
20
+ - **Technical documentation**: Users search for exact function names + concepts
21
+ - **Product search**: Match SKUs/model numbers + descriptions
22
+ - **Legal documents**: Exact citations + semantic context
23
+ - **Code search**: Function names + natural language descriptions
24
+ - **Mixed queries**: "authentication JWT" (semantic + exact keyword)
25
+
26
+ ## Benefits
27
+
28
+ - **Best of both worlds**: Precision (keyword) + recall (semantic)
29
+ - **Better results for ambiguous queries**
30
+ - **Handles typos** (semantic) and **exact matches** (keyword)
31
+ - **More control** over result relevance
32
+
33
+ ## Workflow
34
+
35
+ ### 1. Create a Collection with Hybrid Search Enabled
36
+
37
+ ```
38
+ Create a collection named "technical_docs" with Cosine distance and enableHybrid set to true
39
+ ```
40
+
41
+ **Important**: Set `enableHybrid: true` to enable hybrid search capabilities.
42
+
43
+ ### 2. Add Documents
44
+
45
+ Documents are automatically indexed for both semantic and keyword search:
46
+
47
+ ```
48
+ Add these documents to technical_docs:
49
+ - id: 1, text: "The authenticateUser function validates JWT tokens for user sessions",
50
+ metadata: {"category": "authentication", "type": "function"}
51
+ - id: 2, text: "JWT (JSON Web Token) is a compact URL-safe means of representing claims",
52
+ metadata: {"category": "authentication", "type": "definition"}
53
+ - id: 3, text: "OAuth2 provides authorization framework for third-party applications",
54
+ metadata: {"category": "authentication", "type": "protocol"}
55
+ - id: 4, text: "The login endpoint requires username and password credentials",
56
+ metadata: {"category": "authentication", "type": "endpoint"}
57
+ ```
58
+
59
+ ### 3. Perform Hybrid Search
60
+
61
+ Search using both semantic understanding and keyword matching:
62
+
63
+ ```
64
+ Search technical_docs for "JWT authentication function" with limit 3 using hybrid_search
65
+ ```
66
+
67
+ **Result**: Documents are ranked by combining:
68
+
69
+ - Semantic similarity to "authentication function"
70
+ - Exact keyword matches for "JWT"
71
+
72
+ ### 4. Hybrid Search with Filters
73
+
74
+ Combine hybrid search with metadata filtering:
75
+
76
+ ```
77
+ Search technical_docs for "JWT token validation" with limit 2 and filter {"type": "function"} using hybrid_search
78
+ ```
79
+
80
+ ## Comparison: Semantic vs Hybrid Search
81
+
82
+ ### Semantic Search Only
83
+
84
+ ```
85
+ Search technical_docs for "JWT authentication" with limit 3 using semantic_search
86
+ ```
87
+
88
+ **Result**: May miss documents with exact "JWT" match if they're not semantically similar.
89
+
90
+ ### Hybrid Search
91
+
92
+ ```
93
+ Search technical_docs for "JWT authentication" with limit 3 using hybrid_search
94
+ ```
95
+
96
+ **Result**: Finds both:
97
+
98
+ - Documents semantically related to authentication
99
+ - Documents with exact "JWT" keyword match
100
+ - Best combination ranked by RRF
101
+
102
+ ## Example Scenarios
103
+
104
+ ### Scenario 1: Exact Term + Context
105
+
106
+ **Query**: "authenticateUser JWT"
107
+
108
+ **Hybrid Search finds**:
109
+
110
+ 1. Documents with `authenticateUser` function name (keyword match)
111
+ 2. Documents about JWT authentication (semantic match)
112
+ 3. Best combination of both
113
+
114
+ **Pure semantic search might miss**: Exact function name if using different terminology.
115
+
116
+ ### Scenario 2: Acronym + Description
117
+
118
+ **Query**: "API rate limiting"
119
+
120
+ **Hybrid Search finds**:
121
+
122
+ 1. Documents with "API" acronym (keyword match)
123
+ 2. Documents about rate limiting concepts (semantic match)
124
+ 3. Documents mentioning "API rate limiting" get highest score
125
+
126
+ ### Scenario 3: Typos + Exact Terms
127
+
128
+ **Query**: "OAuth2 authentification"
129
+
130
+ **Hybrid Search finds**:
131
+
132
+ 1. "OAuth2" exact matches (keyword - ignores typo in other term)
133
+ 2. Authentication concepts (semantic - understands "authentification" ≈ "authentication")
134
+
135
+ ## Technical Details
136
+
137
+ ### How It Works
138
+
139
+ 1. **Dense Vector Generation**: Your query is embedded using the configured embedding provider (Ollama, OpenAI, etc.)
140
+ 2. **Sparse Vector Generation**: Query is tokenized and BM25 scores are calculated
141
+ 3. **Parallel Search**: Both vectors are searched simultaneously
142
+ 4. **Result Fusion**: RRF combines rankings from both searches
143
+ 5. **Final Ranking**: Merged results with combined relevance scores
144
+
145
+ ### BM25 Sparse Vectors
146
+
147
+ The server uses a lightweight BM25 implementation for sparse vectors:
148
+
149
+ - Tokenization: Lowercase + whitespace splitting
150
+ - IDF scoring: Inverse document frequency
151
+ - Configurable parameters: k1=1.2, b=0.75
152
+
153
+ ### Reciprocal Rank Fusion (RRF)
154
+
155
+ RRF formula: `score = Σ(1 / (k + rank))` where k=60 (default)
156
+
157
+ Benefits:
158
+
159
+ - No score normalization needed
160
+ - Robust to differences in score scales
161
+ - Works well for combining different ranking methods
162
+
163
+ ## Best Practices
164
+
165
+ 1. **Enable hybrid for technical content**: Use when exact terms matter
166
+ 2. **Use semantic for general content**: Natural language queries without technical terms
167
+ 3. **Combine with filters**: Narrow down results by category or type
168
+ 4. **Test both approaches**: Compare semantic vs hybrid for your use case
169
+ 5. **Monitor performance**: Hybrid search requires more computation
170
+
171
+ ## Performance Considerations
172
+
173
+ - **Storage**: Hybrid collections require more space (dense + sparse vectors)
174
+ - **Indexing**: Document indexing is slightly slower
175
+ - **Query time**: Hybrid search performs two searches and fusion
176
+ - **Scalability**: Qdrant optimizes both vector types efficiently
177
+
178
+ ## Try It
179
+
180
+ Practice hybrid search with your own example:
181
+
182
+ ```
183
+ Create a collection named "my-hybrid-docs" with Cosine distance and enableHybrid set to true
184
+
185
+ Add these documents to my-hybrid-docs:
186
+ - id: "doc1", text: "The calculateTax function computes sales tax based on location and product type", metadata: {"category": "finance", "type": "function"}
187
+ - id: "doc2", text: "Tax calculation involves applying regional rates and exemptions", metadata: {"category": "finance", "type": "concept"}
188
+ - id: "doc3", text: "The generateInvoice method creates PDF invoices with itemized charges", metadata: {"category": "billing", "type": "function"}
189
+ - id: "doc4", text: "Invoice generation includes customer details, line items, and payment terms", metadata: {"category": "billing", "type": "concept"}
190
+
191
+ Search my-hybrid-docs for "calculateTax invoice" with limit 3 using hybrid_search
192
+
193
+ Search my-hybrid-docs for "tax calculation" with limit 2 using semantic_search
194
+
195
+ # Compare the results - notice how hybrid search finds both exact function names and concepts
196
+
197
+ Delete collection "my-hybrid-docs"
198
+ ```
199
+
200
+ ## Troubleshooting
201
+
202
+ ### "Collection does not have hybrid search enabled"
203
+
204
+ **Solution**: Create a new collection with `enableHybrid: true`. Existing collections cannot be converted.
205
+
206
+ ### Poor results with hybrid search
207
+
208
+ **Try**:
209
+
210
+ 1. Adjust query phrasing to include key terms
211
+ 2. Use metadata filters to narrow scope
212
+ 3. Increase `limit` to see more results
213
+ 4. Compare with pure semantic search
214
+
215
+ ### Slow query performance
216
+
217
+ **Solutions**:
218
+
219
+ 1. Reduce prefetch limit (contact support for tuning)
220
+ 2. Add filters to narrow search space
221
+ 3. Use fewer documents or partition data
222
+
223
+ ## Cleanup
224
+
225
+ ```
226
+ Delete collection "technical_docs"
227
+ ```
228
+
229
+ ## Next Steps
230
+
231
+ Continue learning with these examples:
232
+
233
+ - **[Knowledge Base](../knowledge-base/)** - Searchable documentation with metadata
234
+ - **[Advanced Filtering](../filters/)** - Complex search queries
235
+ - **[Rate Limiting](../rate-limiting/)** - Batch processing patterns
236
+ - **[Basic Usage](../basic/)** - Fundamental operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhalder/qdrant-mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for semantic search using local Qdrant and Ollama (default) with support for OpenAI, Cohere, and Voyage AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "@qdrant/js-client-rest": "^1.12.0",
38
38
  "bottleneck": "^2.19.5",
39
39
  "cohere-ai": "^7.19.0",
40
+ "express": "^5.1.0",
40
41
  "openai": "^4.77.3",
41
42
  "zod": "^3.24.1"
42
43
  },
@@ -47,6 +48,7 @@
47
48
  "@semantic-release/git": "^10.0.1",
48
49
  "@semantic-release/github": "^11.0.6",
49
50
  "@semantic-release/npm": "^12.0.2",
51
+ "@types/express": "^5.0.3",
50
52
  "@types/node": "^22.10.5",
51
53
  "@vitest/coverage-v8": "^2.1.8",
52
54
  "@vitest/ui": "^2.1.8",
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BM25SparseVectorGenerator } from "./sparse.js";
3
+
4
+ describe("BM25SparseVectorGenerator", () => {
5
+ it("should generate sparse vectors for simple text", () => {
6
+ const generator = new BM25SparseVectorGenerator();
7
+ const result = generator.generate("hello world");
8
+
9
+ expect(result.indices).toBeDefined();
10
+ expect(result.values).toBeDefined();
11
+ expect(result.indices.length).toBeGreaterThan(0);
12
+ expect(result.values.length).toBe(result.indices.length);
13
+ });
14
+
15
+ it("should generate different vectors for different texts", () => {
16
+ const generator = new BM25SparseVectorGenerator();
17
+ const result1 = generator.generate("hello world");
18
+ const result2 = generator.generate("goodbye world");
19
+
20
+ // Different texts should have different sparse representations
21
+ expect(result1.indices).not.toEqual(result2.indices);
22
+ });
23
+
24
+ it("should generate consistent vectors for the same text", () => {
25
+ const generator = new BM25SparseVectorGenerator();
26
+ const result1 = generator.generate("hello world");
27
+ const result2 = generator.generate("hello world");
28
+
29
+ expect(result1.indices).toEqual(result2.indices);
30
+ expect(result1.values).toEqual(result2.values);
31
+ });
32
+
33
+ it("should handle empty strings", () => {
34
+ const generator = new BM25SparseVectorGenerator();
35
+ const result = generator.generate("");
36
+
37
+ expect(result.indices).toHaveLength(0);
38
+ expect(result.values).toHaveLength(0);
39
+ });
40
+
41
+ it("should handle special characters and punctuation", () => {
42
+ const generator = new BM25SparseVectorGenerator();
43
+ const result = generator.generate("hello, world! how are you?");
44
+
45
+ expect(result.indices).toBeDefined();
46
+ expect(result.values).toBeDefined();
47
+ expect(result.indices.length).toBeGreaterThan(0);
48
+ });
49
+
50
+ it("should train on corpus and generate IDF scores", () => {
51
+ const generator = new BM25SparseVectorGenerator();
52
+ const corpus = ["the quick brown fox", "jumps over the lazy dog", "the fox is quick"];
53
+
54
+ generator.train(corpus);
55
+ const result = generator.generate("quick fox");
56
+
57
+ expect(result.indices).toBeDefined();
58
+ expect(result.values).toBeDefined();
59
+ expect(result.indices.length).toBeGreaterThan(0);
60
+ });
61
+
62
+ it("should use static generateSimple method", () => {
63
+ const result = BM25SparseVectorGenerator.generateSimple("hello world");
64
+
65
+ expect(result.indices).toBeDefined();
66
+ expect(result.values).toBeDefined();
67
+ expect(result.indices.length).toBeGreaterThan(0);
68
+ });
69
+
70
+ it("should lowercase and tokenize text properly", () => {
71
+ const generator = new BM25SparseVectorGenerator();
72
+ const result1 = generator.generate("HELLO WORLD");
73
+ const result2 = generator.generate("hello world");
74
+
75
+ // Should produce same results due to lowercasing
76
+ expect(result1.indices).toEqual(result2.indices);
77
+ });
78
+
79
+ it("should generate positive values", () => {
80
+ const generator = new BM25SparseVectorGenerator();
81
+ const result = generator.generate("hello world");
82
+
83
+ result.values.forEach((value) => {
84
+ expect(value).toBeGreaterThan(0);
85
+ });
86
+ });
87
+ });