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