@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/build/index.js
CHANGED
|
@@ -113,524 +113,534 @@ async function checkOllamaAvailability() {
|
|
|
113
113
|
// Initialize clients
|
|
114
114
|
const qdrant = new QdrantManager(QDRANT_URL);
|
|
115
115
|
const embeddings = EmbeddingProviderFactory.createFromEnv();
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
const DeleteCollectionSchema = z.object({
|
|
158
|
-
name: z.string().describe("Name of the collection to delete"),
|
|
159
|
-
});
|
|
160
|
-
const GetCollectionInfoSchema = z.object({
|
|
161
|
-
name: z.string().describe("Name of the collection"),
|
|
162
|
-
});
|
|
163
|
-
const DeleteDocumentsSchema = z.object({
|
|
164
|
-
collection: z.string().describe("Name of the collection"),
|
|
165
|
-
ids: z.array(z.union([z.string(), z.number()])).describe("Array of document IDs to delete"),
|
|
166
|
-
});
|
|
167
|
-
const HybridSearchSchema = z.object({
|
|
168
|
-
collection: z.string().describe("Name of the collection to search"),
|
|
169
|
-
query: z.string().describe("Search query text"),
|
|
170
|
-
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
171
|
-
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
172
|
-
});
|
|
173
|
-
// List available tools
|
|
174
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
175
|
-
return {
|
|
176
|
-
tools: [
|
|
177
|
-
{
|
|
178
|
-
name: "create_collection",
|
|
179
|
-
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.",
|
|
180
|
-
inputSchema: {
|
|
181
|
-
type: "object",
|
|
182
|
-
properties: {
|
|
183
|
-
name: {
|
|
184
|
-
type: "string",
|
|
185
|
-
description: "Name of the collection",
|
|
186
|
-
},
|
|
187
|
-
distance: {
|
|
188
|
-
type: "string",
|
|
189
|
-
enum: ["Cosine", "Euclid", "Dot"],
|
|
190
|
-
description: "Distance metric (default: Cosine)",
|
|
191
|
-
},
|
|
192
|
-
enableHybrid: {
|
|
193
|
-
type: "boolean",
|
|
194
|
-
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
|
+
},
|
|
195
156
|
},
|
|
157
|
+
required: ["name"],
|
|
196
158
|
},
|
|
197
|
-
required: ["name"],
|
|
198
159
|
},
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
},
|
|
227
188
|
},
|
|
189
|
+
required: ["id", "text"],
|
|
228
190
|
},
|
|
229
|
-
required: ["id", "text"],
|
|
230
191
|
},
|
|
231
192
|
},
|
|
193
|
+
required: ["collection", "documents"],
|
|
232
194
|
},
|
|
233
|
-
required: ["collection", "documents"],
|
|
234
195
|
},
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
},
|
|
257
218
|
},
|
|
219
|
+
required: ["collection", "query"],
|
|
258
220
|
},
|
|
259
|
-
required: ["collection", "query"],
|
|
260
221
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
222
|
+
{
|
|
223
|
+
name: "list_collections",
|
|
224
|
+
description: "List all available collections in Qdrant.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {},
|
|
228
|
+
},
|
|
268
229
|
},
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
},
|
|
279
240
|
},
|
|
241
|
+
required: ["name"],
|
|
280
242
|
},
|
|
281
|
-
required: ["name"],
|
|
282
243
|
},
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
},
|
|
293
254
|
},
|
|
255
|
+
required: ["name"],
|
|
294
256
|
},
|
|
295
|
-
required: ["name"],
|
|
296
257
|
},
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
},
|
|
313
274
|
},
|
|
314
275
|
},
|
|
276
|
+
required: ["collection", "ids"],
|
|
315
277
|
},
|
|
316
|
-
required: ["collection", "ids"],
|
|
317
278
|
},
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
},
|
|
340
301
|
},
|
|
302
|
+
required: ["collection", "query"],
|
|
341
303
|
},
|
|
342
|
-
required: ["collection", "query"],
|
|
343
304
|
},
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
};
|
|
360
329
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
}
|
|
375
376
|
return {
|
|
376
377
|
content: [
|
|
377
378
|
{
|
|
378
379
|
type: "text",
|
|
379
|
-
text: `
|
|
380
|
+
text: `Successfully added ${documents.length} document(s) to collection "${collection}".`,
|
|
380
381
|
},
|
|
381
382
|
],
|
|
382
|
-
isError: true,
|
|
383
383
|
};
|
|
384
384
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
})
|
|
402
|
-
|
|
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
|
+
};
|
|
403
412
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
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
|
+
};
|
|
415
423
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
type: "text",
|
|
420
|
-
text: `Successfully added ${documents.length} document(s) to collection "${collection}".`,
|
|
421
|
-
},
|
|
422
|
-
],
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
case "semantic_search": {
|
|
426
|
-
const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
|
|
427
|
-
// Check if collection exists
|
|
428
|
-
const exists = await qdrant.collectionExists(collection);
|
|
429
|
-
if (!exists) {
|
|
424
|
+
case "delete_collection": {
|
|
425
|
+
const { name } = DeleteCollectionSchema.parse(args);
|
|
426
|
+
await qdrant.deleteCollection(name);
|
|
430
427
|
return {
|
|
431
428
|
content: [
|
|
432
429
|
{
|
|
433
430
|
type: "text",
|
|
434
|
-
text: `
|
|
431
|
+
text: `Collection "${name}" deleted successfully.`,
|
|
435
432
|
},
|
|
436
433
|
],
|
|
437
|
-
isError: true,
|
|
438
434
|
};
|
|
439
435
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const results = await qdrant.search(collection, embedding, limit || 5, filter);
|
|
444
|
-
return {
|
|
445
|
-
content: [
|
|
446
|
-
{
|
|
447
|
-
type: "text",
|
|
448
|
-
text: JSON.stringify(results, null, 2),
|
|
449
|
-
},
|
|
450
|
-
],
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
case "list_collections": {
|
|
454
|
-
const collections = await qdrant.listCollections();
|
|
455
|
-
return {
|
|
456
|
-
content: [
|
|
457
|
-
{
|
|
458
|
-
type: "text",
|
|
459
|
-
text: JSON.stringify(collections, null, 2),
|
|
460
|
-
},
|
|
461
|
-
],
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
case "delete_collection": {
|
|
465
|
-
const { name } = DeleteCollectionSchema.parse(args);
|
|
466
|
-
await qdrant.deleteCollection(name);
|
|
467
|
-
return {
|
|
468
|
-
content: [
|
|
469
|
-
{
|
|
470
|
-
type: "text",
|
|
471
|
-
text: `Collection "${name}" deleted successfully.`,
|
|
472
|
-
},
|
|
473
|
-
],
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
case "get_collection_info": {
|
|
477
|
-
const { name } = GetCollectionInfoSchema.parse(args);
|
|
478
|
-
const info = await qdrant.getCollectionInfo(name);
|
|
479
|
-
return {
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
482
|
-
type: "text",
|
|
483
|
-
text: JSON.stringify(info, null, 2),
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
case "delete_documents": {
|
|
489
|
-
const { collection, ids } = DeleteDocumentsSchema.parse(args);
|
|
490
|
-
await qdrant.deletePoints(collection, ids);
|
|
491
|
-
return {
|
|
492
|
-
content: [
|
|
493
|
-
{
|
|
494
|
-
type: "text",
|
|
495
|
-
text: `Successfully deleted ${ids.length} document(s) from collection "${collection}".`,
|
|
496
|
-
},
|
|
497
|
-
],
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
case "hybrid_search": {
|
|
501
|
-
const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
|
|
502
|
-
// Check if collection exists
|
|
503
|
-
const exists = await qdrant.collectionExists(collection);
|
|
504
|
-
if (!exists) {
|
|
436
|
+
case "get_collection_info": {
|
|
437
|
+
const { name } = GetCollectionInfoSchema.parse(args);
|
|
438
|
+
const info = await qdrant.getCollectionInfo(name);
|
|
505
439
|
return {
|
|
506
440
|
content: [
|
|
507
441
|
{
|
|
508
442
|
type: "text",
|
|
509
|
-
text:
|
|
443
|
+
text: JSON.stringify(info, null, 2),
|
|
510
444
|
},
|
|
511
445
|
],
|
|
512
|
-
isError: true,
|
|
513
446
|
};
|
|
514
447
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
448
|
+
case "delete_documents": {
|
|
449
|
+
const { collection, ids } = DeleteDocumentsSchema.parse(args);
|
|
450
|
+
await qdrant.deletePoints(collection, ids);
|
|
518
451
|
return {
|
|
519
452
|
content: [
|
|
520
453
|
{
|
|
521
454
|
type: "text",
|
|
522
|
-
text: `
|
|
455
|
+
text: `Successfully deleted ${ids.length} document(s) from collection "${collection}".`,
|
|
523
456
|
},
|
|
524
457
|
],
|
|
525
|
-
isError: true,
|
|
526
458
|
};
|
|
527
459
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
};
|
|
543
514
|
}
|
|
544
|
-
default:
|
|
545
|
-
return {
|
|
546
|
-
content: [
|
|
547
|
-
{
|
|
548
|
-
type: "text",
|
|
549
|
-
text: `Unknown tool: ${name}`,
|
|
550
|
-
},
|
|
551
|
-
],
|
|
552
|
-
isError: true,
|
|
553
|
-
};
|
|
554
515
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
578
|
-
const collections = await qdrant.listCollections();
|
|
579
|
-
return {
|
|
580
|
-
resources: [
|
|
581
|
-
{
|
|
582
|
-
uri: "qdrant://collections",
|
|
583
|
-
name: "All Collections",
|
|
584
|
-
description: "List of all vector collections in Qdrant",
|
|
585
|
-
mimeType: "application/json",
|
|
586
|
-
},
|
|
587
|
-
...collections.map((name) => ({
|
|
588
|
-
uri: `qdrant://collection/${name}`,
|
|
589
|
-
name: `Collection: ${name}`,
|
|
590
|
-
description: `Details and statistics for collection "${name}"`,
|
|
591
|
-
mimeType: "application/json",
|
|
592
|
-
})),
|
|
593
|
-
],
|
|
594
|
-
};
|
|
595
|
-
});
|
|
596
|
-
// Read resource content
|
|
597
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
598
|
-
const { uri } = request.params;
|
|
599
|
-
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 () => {
|
|
600
538
|
const collections = await qdrant.listCollections();
|
|
601
539
|
return {
|
|
602
|
-
|
|
540
|
+
resources: [
|
|
603
541
|
{
|
|
604
|
-
uri,
|
|
542
|
+
uri: "qdrant://collections",
|
|
543
|
+
name: "All Collections",
|
|
544
|
+
description: "List of all vector collections in Qdrant",
|
|
605
545
|
mimeType: "application/json",
|
|
606
|
-
text: JSON.stringify(collections, null, 2),
|
|
607
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
|
+
})),
|
|
608
553
|
],
|
|
609
554
|
};
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
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
|
+
}
|
|
615
585
|
return {
|
|
616
586
|
contents: [
|
|
617
587
|
{
|
|
618
588
|
uri,
|
|
619
|
-
mimeType: "
|
|
620
|
-
text:
|
|
589
|
+
mimeType: "text/plain",
|
|
590
|
+
text: `Unknown resource: ${uri}`,
|
|
621
591
|
},
|
|
622
592
|
],
|
|
623
593
|
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
]
|
|
633
|
-
|
|
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)"),
|
|
609
|
+
});
|
|
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"),
|
|
634
644
|
});
|
|
635
645
|
// Start server with stdio transport
|
|
636
646
|
async function startStdioServer() {
|
|
@@ -639,6 +649,13 @@ async function startStdioServer() {
|
|
|
639
649
|
await server.connect(transport);
|
|
640
650
|
console.error("Qdrant MCP server running on stdio");
|
|
641
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
|
|
642
659
|
// Start server with HTTP transport
|
|
643
660
|
async function startHttpServer() {
|
|
644
661
|
await checkOllamaAvailability();
|
|
@@ -648,20 +665,29 @@ async function startHttpServer() {
|
|
|
648
665
|
app.set("trust proxy", true);
|
|
649
666
|
// Rate limiter group: max 100 requests per 15 minutes per IP, max 10 concurrent per IP
|
|
650
667
|
const rateLimiterGroup = new Bottleneck.Group({
|
|
651
|
-
reservoir:
|
|
652
|
-
reservoirRefreshAmount:
|
|
653
|
-
reservoirRefreshInterval:
|
|
654
|
-
maxConcurrent:
|
|
668
|
+
reservoir: RATE_LIMIT_MAX_REQUESTS,
|
|
669
|
+
reservoirRefreshAmount: RATE_LIMIT_MAX_REQUESTS,
|
|
670
|
+
reservoirRefreshInterval: RATE_LIMIT_WINDOW_MS,
|
|
671
|
+
maxConcurrent: RATE_LIMIT_MAX_CONCURRENT,
|
|
655
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
|
+
};
|
|
656
683
|
// Periodic cleanup of inactive rate limiters to prevent memory leaks
|
|
657
684
|
// Track last access time for each IP
|
|
658
685
|
const ipLastAccess = new Map();
|
|
659
|
-
const INACTIVE_TIMEOUT = 60 * 60 * 1000; // 1 hour
|
|
660
686
|
const cleanupIntervalId = setInterval(() => {
|
|
661
687
|
const now = Date.now();
|
|
662
688
|
const keysToDelete = [];
|
|
663
689
|
ipLastAccess.forEach((lastAccess, ip) => {
|
|
664
|
-
if (now - lastAccess >
|
|
690
|
+
if (now - lastAccess > RATE_LIMITER_CLEANUP_INTERVAL_MS) {
|
|
665
691
|
keysToDelete.push(ip);
|
|
666
692
|
}
|
|
667
693
|
});
|
|
@@ -672,7 +698,7 @@ async function startHttpServer() {
|
|
|
672
698
|
if (keysToDelete.length > 0) {
|
|
673
699
|
console.error(`Cleaned up ${keysToDelete.length} inactive rate limiters`);
|
|
674
700
|
}
|
|
675
|
-
},
|
|
701
|
+
}, RATE_LIMITER_CLEANUP_INTERVAL_MS);
|
|
676
702
|
// Rate limiting middleware
|
|
677
703
|
const rateLimitMiddleware = async (req, res, next) => {
|
|
678
704
|
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
|
@@ -687,21 +713,12 @@ async function startHttpServer() {
|
|
|
687
713
|
catch (error) {
|
|
688
714
|
// Differentiate between rate limit errors and unexpected errors
|
|
689
715
|
if (error instanceof Bottleneck.BottleneckError) {
|
|
690
|
-
// Rate limit exceeded or Bottleneck operational error
|
|
691
716
|
console.error(`Rate limit exceeded for IP ${clientIp}:`, error.message);
|
|
692
717
|
}
|
|
693
718
|
else {
|
|
694
|
-
// Unexpected error in rate limiting logic
|
|
695
719
|
console.error("Unexpected rate limiting error:", error);
|
|
696
720
|
}
|
|
697
|
-
res
|
|
698
|
-
jsonrpc: "2.0",
|
|
699
|
-
error: {
|
|
700
|
-
code: -32000,
|
|
701
|
-
message: "Too many requests",
|
|
702
|
-
},
|
|
703
|
-
id: null,
|
|
704
|
-
});
|
|
721
|
+
sendErrorResponse(res, -32000, "Too many requests", 429);
|
|
705
722
|
}
|
|
706
723
|
};
|
|
707
724
|
// Health check endpoint
|
|
@@ -714,74 +731,56 @@ async function startHttpServer() {
|
|
|
714
731
|
});
|
|
715
732
|
});
|
|
716
733
|
app.post("/mcp", rateLimitMiddleware, async (req, res) => {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
// Create a new transport for each request in stateless mode.
|
|
722
|
-
// This prevents request ID collisions when different clients use the same JSON-RPC request IDs.
|
|
734
|
+
// Create a new server for each request
|
|
735
|
+
const requestServer = createServer();
|
|
736
|
+
registerHandlers(requestServer);
|
|
737
|
+
// Create transport with enableJsonResponse
|
|
723
738
|
const transport = new StreamableHTTPServerTransport({
|
|
724
|
-
sessionIdGenerator: undefined,
|
|
739
|
+
sessionIdGenerator: undefined,
|
|
725
740
|
enableJsonResponse: true,
|
|
726
741
|
});
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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(() => { });
|
|
733
750
|
};
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
await closeTransport();
|
|
740
|
-
if (!res.headersSent) {
|
|
741
|
-
res.status(408).json({
|
|
742
|
-
jsonrpc: "2.0",
|
|
743
|
-
error: {
|
|
744
|
-
code: -32000,
|
|
745
|
-
message: "Request timeout",
|
|
746
|
-
},
|
|
747
|
-
id: null,
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
}, REQUEST_TIMEOUT);
|
|
751
|
-
// Clean up transport when response closes
|
|
752
|
-
res.on("close", async () => {
|
|
753
|
-
await closeTransport();
|
|
754
|
-
if (timeoutId)
|
|
755
|
-
clearTimeout(timeoutId);
|
|
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
756
|
});
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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()
|
|
760
763
|
await transport.handleRequest(req, res, req.body);
|
|
761
|
-
//
|
|
762
|
-
|
|
764
|
+
// Clean up AFTER the response finishes
|
|
765
|
+
// Listen to multiple events to ensure cleanup happens in all scenarios
|
|
766
|
+
const cleanupHandler = () => {
|
|
763
767
|
clearTimeout(timeoutId);
|
|
764
|
-
|
|
765
|
-
|
|
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
|
+
});
|
|
766
778
|
}
|
|
767
779
|
catch (error) {
|
|
780
|
+
clearTimeout(timeoutId);
|
|
768
781
|
console.error("Error handling MCP request:", error);
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
jsonrpc: "2.0",
|
|
772
|
-
error: {
|
|
773
|
-
code: -32603,
|
|
774
|
-
message: "Internal server error",
|
|
775
|
-
},
|
|
776
|
-
id: null,
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
finally {
|
|
781
|
-
if (timeoutId)
|
|
782
|
-
clearTimeout(timeoutId);
|
|
783
|
-
// Ensure transport is closed even if an error occurs
|
|
784
|
-
await closeTransport();
|
|
782
|
+
sendErrorResponse(res, -32603, "Internal server error");
|
|
783
|
+
await cleanup();
|
|
785
784
|
}
|
|
786
785
|
});
|
|
787
786
|
const httpServer = app
|
|
@@ -801,11 +800,11 @@ async function startHttpServer() {
|
|
|
801
800
|
console.error("Shutdown signal received, closing HTTP server gracefully...");
|
|
802
801
|
// Clear the cleanup interval to allow graceful shutdown
|
|
803
802
|
clearInterval(cleanupIntervalId);
|
|
804
|
-
// Force shutdown after
|
|
803
|
+
// Force shutdown after grace period
|
|
805
804
|
const forceTimeout = setTimeout(() => {
|
|
806
805
|
console.error("Forcing shutdown after timeout");
|
|
807
806
|
process.exit(1);
|
|
808
|
-
},
|
|
807
|
+
}, SHUTDOWN_GRACE_PERIOD_MS);
|
|
809
808
|
httpServer.close(() => {
|
|
810
809
|
clearTimeout(forceTimeout);
|
|
811
810
|
console.error("HTTP server closed");
|