@mhalder/qdrant-mcp-server 1.2.0 → 1.3.1
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 +18 -0
- package/README.md +34 -0
- package/build/index.js +643 -443
- package/build/index.js.map +1 -1
- package/build/transport.test.d.ts +2 -0
- package/build/transport.test.d.ts.map +1 -0
- package/build/transport.test.js +168 -0
- package/build/transport.test.js.map +1 -0
- package/examples/hybrid-search/README.md +37 -0
- package/package.json +3 -1
- package/src/index.ts +719 -478
- package/src/transport.test.ts +202 -0
package/build/index.js
CHANGED
|
@@ -4,7 +4,10 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
8
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import Bottleneck from "bottleneck";
|
|
10
|
+
import express from "express";
|
|
8
11
|
import { z } from "zod";
|
|
9
12
|
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
10
13
|
import { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
|
|
@@ -15,6 +18,15 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")
|
|
|
15
18
|
// Validate environment variables
|
|
16
19
|
const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333";
|
|
17
20
|
const EMBEDDING_PROVIDER = (process.env.EMBEDDING_PROVIDER || "ollama").toLowerCase();
|
|
21
|
+
const TRANSPORT_MODE = (process.env.TRANSPORT_MODE || "stdio").toLowerCase();
|
|
22
|
+
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "3000", 10);
|
|
23
|
+
// Validate HTTP_PORT when HTTP mode is selected
|
|
24
|
+
if (TRANSPORT_MODE === "http") {
|
|
25
|
+
if (Number.isNaN(HTTP_PORT) || HTTP_PORT < 1 || HTTP_PORT > 65535) {
|
|
26
|
+
console.error(`Error: Invalid HTTP_PORT "${process.env.HTTP_PORT}". Must be a number between 1 and 65535.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
// Check for required API keys based on provider
|
|
19
31
|
if (EMBEDDING_PROVIDER !== "ollama") {
|
|
20
32
|
let apiKey;
|
|
@@ -101,532 +113,720 @@ async function checkOllamaAvailability() {
|
|
|
101
113
|
// Initialize clients
|
|
102
114
|
const qdrant = new QdrantManager(QDRANT_URL);
|
|
103
115
|
const embeddings = EmbeddingProviderFactory.createFromEnv();
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
const DeleteCollectionSchema = z.object({
|
|
146
|
-
name: z.string().describe("Name of the collection to delete"),
|
|
147
|
-
});
|
|
148
|
-
const GetCollectionInfoSchema = z.object({
|
|
149
|
-
name: z.string().describe("Name of the collection"),
|
|
150
|
-
});
|
|
151
|
-
const DeleteDocumentsSchema = z.object({
|
|
152
|
-
collection: z.string().describe("Name of the collection"),
|
|
153
|
-
ids: z.array(z.union([z.string(), z.number()])).describe("Array of document IDs to delete"),
|
|
154
|
-
});
|
|
155
|
-
const HybridSearchSchema = z.object({
|
|
156
|
-
collection: z.string().describe("Name of the collection to search"),
|
|
157
|
-
query: z.string().describe("Search query text"),
|
|
158
|
-
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
159
|
-
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
160
|
-
});
|
|
161
|
-
// List available tools
|
|
162
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
163
|
-
return {
|
|
164
|
-
tools: [
|
|
165
|
-
{
|
|
166
|
-
name: "create_collection",
|
|
167
|
-
description: "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.",
|
|
168
|
-
inputSchema: {
|
|
169
|
-
type: "object",
|
|
170
|
-
properties: {
|
|
171
|
-
name: {
|
|
172
|
-
type: "string",
|
|
173
|
-
description: "Name of the collection",
|
|
174
|
-
},
|
|
175
|
-
distance: {
|
|
176
|
-
type: "string",
|
|
177
|
-
enum: ["Cosine", "Euclid", "Dot"],
|
|
178
|
-
description: "Distance metric (default: Cosine)",
|
|
179
|
-
},
|
|
180
|
-
enableHybrid: {
|
|
181
|
-
type: "boolean",
|
|
182
|
-
description: "Enable hybrid search with sparse vectors (default: false)",
|
|
116
|
+
// Function to create a new MCP server instance
|
|
117
|
+
// This is needed for HTTP transport in stateless mode where each request gets its own server
|
|
118
|
+
function createServer() {
|
|
119
|
+
return new Server({
|
|
120
|
+
name: pkg.name,
|
|
121
|
+
version: pkg.version,
|
|
122
|
+
}, {
|
|
123
|
+
capabilities: {
|
|
124
|
+
tools: {},
|
|
125
|
+
resources: {},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Create a shared MCP server for stdio mode
|
|
130
|
+
const server = createServer();
|
|
131
|
+
// Function to register all handlers on a server instance
|
|
132
|
+
function registerHandlers(server) {
|
|
133
|
+
// List available tools
|
|
134
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
135
|
+
return {
|
|
136
|
+
tools: [
|
|
137
|
+
{
|
|
138
|
+
name: "create_collection",
|
|
139
|
+
description: "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.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
name: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Name of the collection",
|
|
146
|
+
},
|
|
147
|
+
distance: {
|
|
148
|
+
type: "string",
|
|
149
|
+
enum: ["Cosine", "Euclid", "Dot"],
|
|
150
|
+
description: "Distance metric (default: Cosine)",
|
|
151
|
+
},
|
|
152
|
+
enableHybrid: {
|
|
153
|
+
type: "boolean",
|
|
154
|
+
description: "Enable hybrid search with sparse vectors (default: false)",
|
|
155
|
+
},
|
|
183
156
|
},
|
|
157
|
+
required: ["name"],
|
|
184
158
|
},
|
|
185
|
-
required: ["name"],
|
|
186
159
|
},
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
160
|
+
{
|
|
161
|
+
name: "add_documents",
|
|
162
|
+
description: "Add documents to a collection. Documents will be automatically embedded using the configured embedding provider.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
collection: {
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Name of the collection",
|
|
169
|
+
},
|
|
170
|
+
documents: {
|
|
171
|
+
type: "array",
|
|
172
|
+
description: "Array of documents to add",
|
|
173
|
+
items: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
id: {
|
|
177
|
+
type: ["string", "number"],
|
|
178
|
+
description: "Unique identifier for the document",
|
|
179
|
+
},
|
|
180
|
+
text: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "Text content to embed and store",
|
|
183
|
+
},
|
|
184
|
+
metadata: {
|
|
185
|
+
type: "object",
|
|
186
|
+
description: "Optional metadata to store with the document",
|
|
187
|
+
},
|
|
215
188
|
},
|
|
189
|
+
required: ["id", "text"],
|
|
216
190
|
},
|
|
217
|
-
required: ["id", "text"],
|
|
218
191
|
},
|
|
219
192
|
},
|
|
193
|
+
required: ["collection", "documents"],
|
|
220
194
|
},
|
|
221
|
-
required: ["collection", "documents"],
|
|
222
195
|
},
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
196
|
+
{
|
|
197
|
+
name: "semantic_search",
|
|
198
|
+
description: "Search for documents using natural language queries. Returns the most semantically similar documents.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
collection: {
|
|
203
|
+
type: "string",
|
|
204
|
+
description: "Name of the collection to search",
|
|
205
|
+
},
|
|
206
|
+
query: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description: "Search query text",
|
|
209
|
+
},
|
|
210
|
+
limit: {
|
|
211
|
+
type: "number",
|
|
212
|
+
description: "Maximum number of results (default: 5)",
|
|
213
|
+
},
|
|
214
|
+
filter: {
|
|
215
|
+
type: "object",
|
|
216
|
+
description: "Optional metadata filter",
|
|
217
|
+
},
|
|
245
218
|
},
|
|
219
|
+
required: ["collection", "query"],
|
|
246
220
|
},
|
|
247
|
-
required: ["collection", "query"],
|
|
248
221
|
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
222
|
+
{
|
|
223
|
+
name: "list_collections",
|
|
224
|
+
description: "List all available collections in Qdrant.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {},
|
|
228
|
+
},
|
|
256
229
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
230
|
+
{
|
|
231
|
+
name: "delete_collection",
|
|
232
|
+
description: "Delete a collection and all its documents.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
name: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Name of the collection to delete",
|
|
239
|
+
},
|
|
267
240
|
},
|
|
241
|
+
required: ["name"],
|
|
268
242
|
},
|
|
269
|
-
required: ["name"],
|
|
270
243
|
},
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
244
|
+
{
|
|
245
|
+
name: "get_collection_info",
|
|
246
|
+
description: "Get detailed information about a collection including vector size, point count, and distance metric.",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
name: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Name of the collection",
|
|
253
|
+
},
|
|
281
254
|
},
|
|
255
|
+
required: ["name"],
|
|
282
256
|
},
|
|
283
|
-
required: ["name"],
|
|
284
257
|
},
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
258
|
+
{
|
|
259
|
+
name: "delete_documents",
|
|
260
|
+
description: "Delete specific documents from a collection by their IDs.",
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
collection: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Name of the collection",
|
|
267
|
+
},
|
|
268
|
+
ids: {
|
|
269
|
+
type: "array",
|
|
270
|
+
description: "Array of document IDs to delete",
|
|
271
|
+
items: {
|
|
272
|
+
type: ["string", "number"],
|
|
273
|
+
},
|
|
301
274
|
},
|
|
302
275
|
},
|
|
276
|
+
required: ["collection", "ids"],
|
|
303
277
|
},
|
|
304
|
-
required: ["collection", "ids"],
|
|
305
278
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
279
|
+
{
|
|
280
|
+
name: "hybrid_search",
|
|
281
|
+
description: "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.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
collection: {
|
|
286
|
+
type: "string",
|
|
287
|
+
description: "Name of the collection to search",
|
|
288
|
+
},
|
|
289
|
+
query: {
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Search query text",
|
|
292
|
+
},
|
|
293
|
+
limit: {
|
|
294
|
+
type: "number",
|
|
295
|
+
description: "Maximum number of results (default: 5)",
|
|
296
|
+
},
|
|
297
|
+
filter: {
|
|
298
|
+
type: "object",
|
|
299
|
+
description: "Optional metadata filter",
|
|
300
|
+
},
|
|
328
301
|
},
|
|
302
|
+
required: ["collection", "query"],
|
|
329
303
|
},
|
|
330
|
-
required: ["collection", "query"],
|
|
331
304
|
},
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
// Handle tool calls
|
|
309
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
310
|
+
const { name, arguments: args } = request.params;
|
|
311
|
+
try {
|
|
312
|
+
switch (name) {
|
|
313
|
+
case "create_collection": {
|
|
314
|
+
const { name, distance, enableHybrid } = CreateCollectionSchema.parse(args);
|
|
315
|
+
const vectorSize = embeddings.getDimensions();
|
|
316
|
+
await qdrant.createCollection(name, vectorSize, distance, enableHybrid || false);
|
|
317
|
+
let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
|
|
318
|
+
if (enableHybrid) {
|
|
319
|
+
message += " Hybrid search is enabled for this collection.";
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
content: [
|
|
323
|
+
{
|
|
324
|
+
type: "text",
|
|
325
|
+
text: message,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
};
|
|
348
329
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
330
|
+
case "add_documents": {
|
|
331
|
+
const { collection, documents } = AddDocumentsSchema.parse(args);
|
|
332
|
+
// Check if collection exists and get info
|
|
333
|
+
const exists = await qdrant.collectionExists(collection);
|
|
334
|
+
if (!exists) {
|
|
335
|
+
return {
|
|
336
|
+
content: [
|
|
337
|
+
{
|
|
338
|
+
type: "text",
|
|
339
|
+
text: `Error: Collection "${collection}" does not exist. Create it first using create_collection.`,
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
isError: true,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
346
|
+
// Generate embeddings for all documents
|
|
347
|
+
const texts = documents.map((doc) => doc.text);
|
|
348
|
+
const embeddingResults = await embeddings.embedBatch(texts);
|
|
349
|
+
// If hybrid search is enabled, generate sparse vectors and use appropriate method
|
|
350
|
+
if (collectionInfo.hybridEnabled) {
|
|
351
|
+
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
352
|
+
// Prepare points with both dense and sparse vectors
|
|
353
|
+
const points = documents.map((doc, index) => ({
|
|
354
|
+
id: doc.id,
|
|
355
|
+
vector: embeddingResults[index].embedding,
|
|
356
|
+
sparseVector: sparseGenerator.generate(doc.text),
|
|
357
|
+
payload: {
|
|
358
|
+
text: doc.text,
|
|
359
|
+
...doc.metadata,
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
362
|
+
await qdrant.addPointsWithSparse(collection, points);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// Standard dense-only vectors
|
|
366
|
+
const points = documents.map((doc, index) => ({
|
|
367
|
+
id: doc.id,
|
|
368
|
+
vector: embeddingResults[index].embedding,
|
|
369
|
+
payload: {
|
|
370
|
+
text: doc.text,
|
|
371
|
+
...doc.metadata,
|
|
372
|
+
},
|
|
373
|
+
}));
|
|
374
|
+
await qdrant.addPoints(collection, points);
|
|
375
|
+
}
|
|
363
376
|
return {
|
|
364
377
|
content: [
|
|
365
378
|
{
|
|
366
379
|
type: "text",
|
|
367
|
-
text: `
|
|
380
|
+
text: `Successfully added ${documents.length} document(s) to collection "${collection}".`,
|
|
368
381
|
},
|
|
369
382
|
],
|
|
370
|
-
isError: true,
|
|
371
383
|
};
|
|
372
384
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
})
|
|
390
|
-
|
|
385
|
+
case "semantic_search": {
|
|
386
|
+
const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
|
|
387
|
+
// Check if collection exists
|
|
388
|
+
const exists = await qdrant.collectionExists(collection);
|
|
389
|
+
if (!exists) {
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: `Error: Collection "${collection}" does not exist.`,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
isError: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// Generate embedding for query
|
|
401
|
+
const { embedding } = await embeddings.embed(query);
|
|
402
|
+
// Search
|
|
403
|
+
const results = await qdrant.search(collection, embedding, limit || 5, filter);
|
|
404
|
+
return {
|
|
405
|
+
content: [
|
|
406
|
+
{
|
|
407
|
+
type: "text",
|
|
408
|
+
text: JSON.stringify(results, null, 2),
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
};
|
|
391
412
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
await qdrant.addPoints(collection, points);
|
|
413
|
+
case "list_collections": {
|
|
414
|
+
const collections = await qdrant.listCollections();
|
|
415
|
+
return {
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: "text",
|
|
419
|
+
text: JSON.stringify(collections, null, 2),
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
};
|
|
403
423
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
type: "text",
|
|
408
|
-
text: `Successfully added ${documents.length} document(s) to collection "${collection}".`,
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
case "semantic_search": {
|
|
414
|
-
const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
|
|
415
|
-
// Check if collection exists
|
|
416
|
-
const exists = await qdrant.collectionExists(collection);
|
|
417
|
-
if (!exists) {
|
|
424
|
+
case "delete_collection": {
|
|
425
|
+
const { name } = DeleteCollectionSchema.parse(args);
|
|
426
|
+
await qdrant.deleteCollection(name);
|
|
418
427
|
return {
|
|
419
428
|
content: [
|
|
420
429
|
{
|
|
421
430
|
type: "text",
|
|
422
|
-
text: `
|
|
431
|
+
text: `Collection "${name}" deleted successfully.`,
|
|
423
432
|
},
|
|
424
433
|
],
|
|
425
|
-
isError: true,
|
|
426
434
|
};
|
|
427
435
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const results = await qdrant.search(collection, embedding, limit || 5, filter);
|
|
432
|
-
return {
|
|
433
|
-
content: [
|
|
434
|
-
{
|
|
435
|
-
type: "text",
|
|
436
|
-
text: JSON.stringify(results, null, 2),
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
case "list_collections": {
|
|
442
|
-
const collections = await qdrant.listCollections();
|
|
443
|
-
return {
|
|
444
|
-
content: [
|
|
445
|
-
{
|
|
446
|
-
type: "text",
|
|
447
|
-
text: JSON.stringify(collections, null, 2),
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
case "delete_collection": {
|
|
453
|
-
const { name } = DeleteCollectionSchema.parse(args);
|
|
454
|
-
await qdrant.deleteCollection(name);
|
|
455
|
-
return {
|
|
456
|
-
content: [
|
|
457
|
-
{
|
|
458
|
-
type: "text",
|
|
459
|
-
text: `Collection "${name}" deleted successfully.`,
|
|
460
|
-
},
|
|
461
|
-
],
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
case "get_collection_info": {
|
|
465
|
-
const { name } = GetCollectionInfoSchema.parse(args);
|
|
466
|
-
const info = await qdrant.getCollectionInfo(name);
|
|
467
|
-
return {
|
|
468
|
-
content: [
|
|
469
|
-
{
|
|
470
|
-
type: "text",
|
|
471
|
-
text: JSON.stringify(info, null, 2),
|
|
472
|
-
},
|
|
473
|
-
],
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
case "delete_documents": {
|
|
477
|
-
const { collection, ids } = DeleteDocumentsSchema.parse(args);
|
|
478
|
-
await qdrant.deletePoints(collection, ids);
|
|
479
|
-
return {
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
482
|
-
type: "text",
|
|
483
|
-
text: `Successfully deleted ${ids.length} document(s) from collection "${collection}".`,
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
case "hybrid_search": {
|
|
489
|
-
const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
|
|
490
|
-
// Check if collection exists
|
|
491
|
-
const exists = await qdrant.collectionExists(collection);
|
|
492
|
-
if (!exists) {
|
|
436
|
+
case "get_collection_info": {
|
|
437
|
+
const { name } = GetCollectionInfoSchema.parse(args);
|
|
438
|
+
const info = await qdrant.getCollectionInfo(name);
|
|
493
439
|
return {
|
|
494
440
|
content: [
|
|
495
441
|
{
|
|
496
442
|
type: "text",
|
|
497
|
-
text:
|
|
443
|
+
text: JSON.stringify(info, null, 2),
|
|
498
444
|
},
|
|
499
445
|
],
|
|
500
|
-
isError: true,
|
|
501
446
|
};
|
|
502
447
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
448
|
+
case "delete_documents": {
|
|
449
|
+
const { collection, ids } = DeleteDocumentsSchema.parse(args);
|
|
450
|
+
await qdrant.deletePoints(collection, ids);
|
|
506
451
|
return {
|
|
507
452
|
content: [
|
|
508
453
|
{
|
|
509
454
|
type: "text",
|
|
510
|
-
text: `
|
|
455
|
+
text: `Successfully deleted ${ids.length} document(s) from collection "${collection}".`,
|
|
511
456
|
},
|
|
512
457
|
],
|
|
513
|
-
isError: true,
|
|
514
458
|
};
|
|
515
459
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
460
|
+
case "hybrid_search": {
|
|
461
|
+
const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
|
|
462
|
+
// Check if collection exists
|
|
463
|
+
const exists = await qdrant.collectionExists(collection);
|
|
464
|
+
if (!exists) {
|
|
465
|
+
return {
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: "text",
|
|
469
|
+
text: `Error: Collection "${collection}" does not exist.`,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
isError: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
// Check if collection has hybrid search enabled
|
|
476
|
+
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
477
|
+
if (!collectionInfo.hybridEnabled) {
|
|
478
|
+
return {
|
|
479
|
+
content: [
|
|
480
|
+
{
|
|
481
|
+
type: "text",
|
|
482
|
+
text: `Error: Collection "${collection}" does not have hybrid search enabled. Create a new collection with enableHybrid set to true.`,
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// Generate dense embedding for query
|
|
489
|
+
const { embedding } = await embeddings.embed(query);
|
|
490
|
+
// Generate sparse vector for query
|
|
491
|
+
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
492
|
+
const sparseVector = sparseGenerator.generate(query);
|
|
493
|
+
// Perform hybrid search
|
|
494
|
+
const results = await qdrant.hybridSearch(collection, embedding, sparseVector, limit || 5, filter);
|
|
495
|
+
return {
|
|
496
|
+
content: [
|
|
497
|
+
{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: JSON.stringify(results, null, 2),
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
default:
|
|
505
|
+
return {
|
|
506
|
+
content: [
|
|
507
|
+
{
|
|
508
|
+
type: "text",
|
|
509
|
+
text: `Unknown tool: ${name}`,
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
isError: true,
|
|
513
|
+
};
|
|
531
514
|
}
|
|
532
|
-
default:
|
|
533
|
-
return {
|
|
534
|
-
content: [
|
|
535
|
-
{
|
|
536
|
-
type: "text",
|
|
537
|
-
text: `Unknown tool: ${name}`,
|
|
538
|
-
},
|
|
539
|
-
],
|
|
540
|
-
isError: true,
|
|
541
|
-
};
|
|
542
515
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
566
|
-
const collections = await qdrant.listCollections();
|
|
567
|
-
return {
|
|
568
|
-
resources: [
|
|
569
|
-
{
|
|
570
|
-
uri: "qdrant://collections",
|
|
571
|
-
name: "All Collections",
|
|
572
|
-
description: "List of all vector collections in Qdrant",
|
|
573
|
-
mimeType: "application/json",
|
|
574
|
-
},
|
|
575
|
-
...collections.map((name) => ({
|
|
576
|
-
uri: `qdrant://collection/${name}`,
|
|
577
|
-
name: `Collection: ${name}`,
|
|
578
|
-
description: `Details and statistics for collection "${name}"`,
|
|
579
|
-
mimeType: "application/json",
|
|
580
|
-
})),
|
|
581
|
-
],
|
|
582
|
-
};
|
|
583
|
-
});
|
|
584
|
-
// Read resource content
|
|
585
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
586
|
-
const { uri } = request.params;
|
|
587
|
-
if (uri === "qdrant://collections") {
|
|
516
|
+
catch (error) {
|
|
517
|
+
// Enhanced error details for debugging
|
|
518
|
+
const errorDetails = error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
519
|
+
console.error("Tool execution error:", {
|
|
520
|
+
tool: name,
|
|
521
|
+
error: errorDetails,
|
|
522
|
+
stack: error?.stack,
|
|
523
|
+
data: error?.data,
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: `Error: ${errorDetails}`,
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
isError: true,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
// List available resources
|
|
537
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
588
538
|
const collections = await qdrant.listCollections();
|
|
589
539
|
return {
|
|
590
|
-
|
|
540
|
+
resources: [
|
|
591
541
|
{
|
|
592
|
-
uri,
|
|
542
|
+
uri: "qdrant://collections",
|
|
543
|
+
name: "All Collections",
|
|
544
|
+
description: "List of all vector collections in Qdrant",
|
|
593
545
|
mimeType: "application/json",
|
|
594
|
-
text: JSON.stringify(collections, null, 2),
|
|
595
546
|
},
|
|
547
|
+
...collections.map((name) => ({
|
|
548
|
+
uri: `qdrant://collection/${name}`,
|
|
549
|
+
name: `Collection: ${name}`,
|
|
550
|
+
description: `Details and statistics for collection "${name}"`,
|
|
551
|
+
mimeType: "application/json",
|
|
552
|
+
})),
|
|
596
553
|
],
|
|
597
554
|
};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
555
|
+
});
|
|
556
|
+
// Read resource content
|
|
557
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
558
|
+
const { uri } = request.params;
|
|
559
|
+
if (uri === "qdrant://collections") {
|
|
560
|
+
const collections = await qdrant.listCollections();
|
|
561
|
+
return {
|
|
562
|
+
contents: [
|
|
563
|
+
{
|
|
564
|
+
uri,
|
|
565
|
+
mimeType: "application/json",
|
|
566
|
+
text: JSON.stringify(collections, null, 2),
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const collectionMatch = uri.match(/^qdrant:\/\/collection\/(.+)$/);
|
|
572
|
+
if (collectionMatch) {
|
|
573
|
+
const name = collectionMatch[1];
|
|
574
|
+
const info = await qdrant.getCollectionInfo(name);
|
|
575
|
+
return {
|
|
576
|
+
contents: [
|
|
577
|
+
{
|
|
578
|
+
uri,
|
|
579
|
+
mimeType: "application/json",
|
|
580
|
+
text: JSON.stringify(info, null, 2),
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
603
585
|
return {
|
|
604
586
|
contents: [
|
|
605
587
|
{
|
|
606
588
|
uri,
|
|
607
|
-
mimeType: "
|
|
608
|
-
text:
|
|
589
|
+
mimeType: "text/plain",
|
|
590
|
+
text: `Unknown resource: ${uri}`,
|
|
609
591
|
},
|
|
610
592
|
],
|
|
611
593
|
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
]
|
|
621
|
-
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// Register handlers on the shared server for stdio mode
|
|
597
|
+
registerHandlers(server);
|
|
598
|
+
// Tool schemas
|
|
599
|
+
const CreateCollectionSchema = z.object({
|
|
600
|
+
name: z.string().describe("Name of the collection"),
|
|
601
|
+
distance: z
|
|
602
|
+
.enum(["Cosine", "Euclid", "Dot"])
|
|
603
|
+
.optional()
|
|
604
|
+
.describe("Distance metric (default: Cosine)"),
|
|
605
|
+
enableHybrid: z
|
|
606
|
+
.boolean()
|
|
607
|
+
.optional()
|
|
608
|
+
.describe("Enable hybrid search with sparse vectors (default: false)"),
|
|
622
609
|
});
|
|
623
|
-
|
|
624
|
-
|
|
610
|
+
const AddDocumentsSchema = z.object({
|
|
611
|
+
collection: z.string().describe("Name of the collection"),
|
|
612
|
+
documents: z
|
|
613
|
+
.array(z.object({
|
|
614
|
+
id: z.union([z.string(), z.number()]).describe("Unique identifier for the document"),
|
|
615
|
+
text: z.string().describe("Text content to embed and store"),
|
|
616
|
+
metadata: z
|
|
617
|
+
.record(z.any())
|
|
618
|
+
.optional()
|
|
619
|
+
.describe("Optional metadata to store with the document"),
|
|
620
|
+
}))
|
|
621
|
+
.describe("Array of documents to add"),
|
|
622
|
+
});
|
|
623
|
+
const SemanticSearchSchema = z.object({
|
|
624
|
+
collection: z.string().describe("Name of the collection to search"),
|
|
625
|
+
query: z.string().describe("Search query text"),
|
|
626
|
+
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
627
|
+
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
628
|
+
});
|
|
629
|
+
const DeleteCollectionSchema = z.object({
|
|
630
|
+
name: z.string().describe("Name of the collection to delete"),
|
|
631
|
+
});
|
|
632
|
+
const GetCollectionInfoSchema = z.object({
|
|
633
|
+
name: z.string().describe("Name of the collection"),
|
|
634
|
+
});
|
|
635
|
+
const DeleteDocumentsSchema = z.object({
|
|
636
|
+
collection: z.string().describe("Name of the collection"),
|
|
637
|
+
ids: z.array(z.union([z.string(), z.number()])).describe("Array of document IDs to delete"),
|
|
638
|
+
});
|
|
639
|
+
const HybridSearchSchema = z.object({
|
|
640
|
+
collection: z.string().describe("Name of the collection to search"),
|
|
641
|
+
query: z.string().describe("Search query text"),
|
|
642
|
+
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
643
|
+
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
644
|
+
});
|
|
645
|
+
// Start server with stdio transport
|
|
646
|
+
async function startStdioServer() {
|
|
625
647
|
await checkOllamaAvailability();
|
|
626
648
|
const transport = new StdioServerTransport();
|
|
627
649
|
await server.connect(transport);
|
|
628
650
|
console.error("Qdrant MCP server running on stdio");
|
|
629
651
|
}
|
|
652
|
+
// Constants for HTTP server configuration
|
|
653
|
+
const RATE_LIMIT_MAX_REQUESTS = 100; // Max requests per window
|
|
654
|
+
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
655
|
+
const RATE_LIMIT_MAX_CONCURRENT = 10; // Max concurrent requests per IP
|
|
656
|
+
const RATE_LIMITER_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
657
|
+
const REQUEST_TIMEOUT_MS = 30 * 1000; // 30 seconds per request
|
|
658
|
+
const SHUTDOWN_GRACE_PERIOD_MS = 10 * 1000; // 10 seconds
|
|
659
|
+
// Start server with HTTP transport
|
|
660
|
+
async function startHttpServer() {
|
|
661
|
+
await checkOllamaAvailability();
|
|
662
|
+
const app = express();
|
|
663
|
+
app.use(express.json({ limit: "10mb" }));
|
|
664
|
+
// Configure Express to trust proxy for correct IP detection
|
|
665
|
+
app.set("trust proxy", true);
|
|
666
|
+
// Rate limiter group: max 100 requests per 15 minutes per IP, max 10 concurrent per IP
|
|
667
|
+
const rateLimiterGroup = new Bottleneck.Group({
|
|
668
|
+
reservoir: RATE_LIMIT_MAX_REQUESTS,
|
|
669
|
+
reservoirRefreshAmount: RATE_LIMIT_MAX_REQUESTS,
|
|
670
|
+
reservoirRefreshInterval: RATE_LIMIT_WINDOW_MS,
|
|
671
|
+
maxConcurrent: RATE_LIMIT_MAX_CONCURRENT,
|
|
672
|
+
});
|
|
673
|
+
// Helper function to send JSON-RPC error responses
|
|
674
|
+
const sendErrorResponse = (res, code, message, httpStatus = 500) => {
|
|
675
|
+
if (!res.headersSent) {
|
|
676
|
+
res.status(httpStatus).json({
|
|
677
|
+
jsonrpc: "2.0",
|
|
678
|
+
error: { code, message },
|
|
679
|
+
id: null,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
// Periodic cleanup of inactive rate limiters to prevent memory leaks
|
|
684
|
+
// Track last access time for each IP
|
|
685
|
+
const ipLastAccess = new Map();
|
|
686
|
+
const cleanupIntervalId = setInterval(() => {
|
|
687
|
+
const now = Date.now();
|
|
688
|
+
const keysToDelete = [];
|
|
689
|
+
ipLastAccess.forEach((lastAccess, ip) => {
|
|
690
|
+
if (now - lastAccess > RATE_LIMITER_CLEANUP_INTERVAL_MS) {
|
|
691
|
+
keysToDelete.push(ip);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
keysToDelete.forEach((ip) => {
|
|
695
|
+
rateLimiterGroup.deleteKey(ip);
|
|
696
|
+
ipLastAccess.delete(ip);
|
|
697
|
+
});
|
|
698
|
+
if (keysToDelete.length > 0) {
|
|
699
|
+
console.error(`Cleaned up ${keysToDelete.length} inactive rate limiters`);
|
|
700
|
+
}
|
|
701
|
+
}, RATE_LIMITER_CLEANUP_INTERVAL_MS);
|
|
702
|
+
// Rate limiting middleware
|
|
703
|
+
const rateLimitMiddleware = async (req, res, next) => {
|
|
704
|
+
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
|
705
|
+
try {
|
|
706
|
+
// Update last access time for this IP
|
|
707
|
+
ipLastAccess.set(clientIp, Date.now());
|
|
708
|
+
// Get or create a limiter for this specific IP
|
|
709
|
+
const limiter = rateLimiterGroup.key(clientIp);
|
|
710
|
+
await limiter.schedule(() => Promise.resolve());
|
|
711
|
+
next();
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
// Differentiate between rate limit errors and unexpected errors
|
|
715
|
+
if (error instanceof Bottleneck.BottleneckError) {
|
|
716
|
+
console.error(`Rate limit exceeded for IP ${clientIp}:`, error.message);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
console.error("Unexpected rate limiting error:", error);
|
|
720
|
+
}
|
|
721
|
+
sendErrorResponse(res, -32000, "Too many requests", 429);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
// Health check endpoint
|
|
725
|
+
app.get("/health", (_req, res) => {
|
|
726
|
+
res.json({
|
|
727
|
+
status: "ok",
|
|
728
|
+
mode: TRANSPORT_MODE,
|
|
729
|
+
version: pkg.version,
|
|
730
|
+
embedding_provider: EMBEDDING_PROVIDER,
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
app.post("/mcp", rateLimitMiddleware, async (req, res) => {
|
|
734
|
+
// Create a new server for each request
|
|
735
|
+
const requestServer = createServer();
|
|
736
|
+
registerHandlers(requestServer);
|
|
737
|
+
// Create transport with enableJsonResponse
|
|
738
|
+
const transport = new StreamableHTTPServerTransport({
|
|
739
|
+
sessionIdGenerator: undefined,
|
|
740
|
+
enableJsonResponse: true,
|
|
741
|
+
});
|
|
742
|
+
// Track cleanup state to prevent double cleanup
|
|
743
|
+
let cleanedUp = false;
|
|
744
|
+
const cleanup = async () => {
|
|
745
|
+
if (cleanedUp)
|
|
746
|
+
return;
|
|
747
|
+
cleanedUp = true;
|
|
748
|
+
await transport.close().catch(() => { });
|
|
749
|
+
await requestServer.close().catch(() => { });
|
|
750
|
+
};
|
|
751
|
+
// Set a timeout for the request to prevent hanging
|
|
752
|
+
const timeoutId = setTimeout(() => {
|
|
753
|
+
sendErrorResponse(res, -32000, "Request timeout", 504);
|
|
754
|
+
cleanup().catch((err) => {
|
|
755
|
+
console.error("Error during timeout cleanup:", err);
|
|
756
|
+
});
|
|
757
|
+
}, REQUEST_TIMEOUT_MS);
|
|
758
|
+
try {
|
|
759
|
+
// Connect server to transport
|
|
760
|
+
await requestServer.connect(transport);
|
|
761
|
+
// Handle the request - this triggers message processing
|
|
762
|
+
// The response will be sent asynchronously when the server calls transport.send()
|
|
763
|
+
await transport.handleRequest(req, res, req.body);
|
|
764
|
+
// Clean up AFTER the response finishes
|
|
765
|
+
// Listen to multiple events to ensure cleanup happens in all scenarios
|
|
766
|
+
const cleanupHandler = () => {
|
|
767
|
+
clearTimeout(timeoutId);
|
|
768
|
+
cleanup().catch((err) => {
|
|
769
|
+
console.error("Error during response cleanup:", err);
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
res.on("finish", cleanupHandler);
|
|
773
|
+
res.on("close", cleanupHandler);
|
|
774
|
+
res.on("error", (err) => {
|
|
775
|
+
console.error("Response stream error:", err);
|
|
776
|
+
cleanupHandler();
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
clearTimeout(timeoutId);
|
|
781
|
+
console.error("Error handling MCP request:", error);
|
|
782
|
+
sendErrorResponse(res, -32603, "Internal server error");
|
|
783
|
+
await cleanup();
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
const httpServer = app
|
|
787
|
+
.listen(HTTP_PORT, () => {
|
|
788
|
+
console.error(`Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`);
|
|
789
|
+
})
|
|
790
|
+
.on("error", (error) => {
|
|
791
|
+
console.error("HTTP server error:", error);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
});
|
|
794
|
+
// Graceful shutdown handling
|
|
795
|
+
let isShuttingDown = false;
|
|
796
|
+
const shutdown = () => {
|
|
797
|
+
if (isShuttingDown)
|
|
798
|
+
return;
|
|
799
|
+
isShuttingDown = true;
|
|
800
|
+
console.error("Shutdown signal received, closing HTTP server gracefully...");
|
|
801
|
+
// Clear the cleanup interval to allow graceful shutdown
|
|
802
|
+
clearInterval(cleanupIntervalId);
|
|
803
|
+
// Force shutdown after grace period
|
|
804
|
+
const forceTimeout = setTimeout(() => {
|
|
805
|
+
console.error("Forcing shutdown after timeout");
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}, SHUTDOWN_GRACE_PERIOD_MS);
|
|
808
|
+
httpServer.close(() => {
|
|
809
|
+
clearTimeout(forceTimeout);
|
|
810
|
+
console.error("HTTP server closed");
|
|
811
|
+
process.exit(0);
|
|
812
|
+
});
|
|
813
|
+
};
|
|
814
|
+
process.on("SIGTERM", shutdown);
|
|
815
|
+
process.on("SIGINT", shutdown);
|
|
816
|
+
}
|
|
817
|
+
// Main entry point
|
|
818
|
+
async function main() {
|
|
819
|
+
if (TRANSPORT_MODE === "http") {
|
|
820
|
+
await startHttpServer();
|
|
821
|
+
}
|
|
822
|
+
else if (TRANSPORT_MODE === "stdio") {
|
|
823
|
+
await startStdioServer();
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
console.error(`Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
630
830
|
main().catch((error) => {
|
|
631
831
|
console.error("Fatal error:", error);
|
|
632
832
|
process.exit(1);
|