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