@mhalder/qdrant-mcp-server 1.6.0 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhalder/qdrant-mcp-server",
3
- "version": "1.6.0",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for semantic search using local Qdrant and Ollama (default) with support for OpenAI, Cohere, and Voyage AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,9 @@
25
25
  ],
26
26
  "author": "mhalder",
27
27
  "license": "MIT",
28
+ "engines": {
29
+ "node": ">=22.0.0 <24.0.0"
30
+ },
28
31
  "repository": {
29
32
  "type": "git",
30
33
  "url": "https://github.com/mhalder/qdrant-mcp-server.git"
@@ -37,38 +40,38 @@
37
40
  "tree-sitter": "^0.25.0"
38
41
  },
39
42
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "^1.0.4",
41
- "@qdrant/js-client-rest": "^1.12.0",
43
+ "@modelcontextprotocol/sdk": "^1.25.2",
44
+ "@qdrant/js-client-rest": "^1.16.2",
42
45
  "bottleneck": "^2.19.5",
43
- "cohere-ai": "^7.19.0",
44
- "express": "^5.1.0",
46
+ "cohere-ai": "^7.20.0",
47
+ "express": "^5.2.1",
45
48
  "ignore": "^7.0.5",
46
- "openai": "^4.77.3",
49
+ "openai": "^4.104.0",
47
50
  "tree-sitter": "^0.25.0",
48
- "tree-sitter-bash": "^0.25.0",
51
+ "tree-sitter-bash": "^0.25.1",
49
52
  "tree-sitter-go": "^0.25.0",
50
53
  "tree-sitter-java": "^0.23.5",
51
54
  "tree-sitter-javascript": "^0.25.0",
52
55
  "tree-sitter-python": "^0.25.0",
53
56
  "tree-sitter-rust": "^0.24.0",
54
57
  "tree-sitter-typescript": "^0.23.2",
55
- "zod": "^3.24.1"
58
+ "zod": "^3.25.76"
56
59
  },
57
60
  "devDependencies": {
58
- "@commitlint/cli": "^20.1.0",
59
- "@commitlint/config-conventional": "^20.0.0",
61
+ "@commitlint/cli": "^20.3.1",
62
+ "@commitlint/config-conventional": "^20.3.1",
60
63
  "@semantic-release/changelog": "^6.0.3",
61
64
  "@semantic-release/git": "^10.0.1",
62
- "@semantic-release/github": "^11.0.6",
63
- "@semantic-release/npm": "^12.0.2",
64
- "@types/express": "^5.0.3",
65
- "@types/node": "^22.10.5",
66
- "@vitest/coverage-v8": "^2.1.8",
67
- "@vitest/ui": "^2.1.8",
65
+ "@semantic-release/github": "^12.0.2",
66
+ "@semantic-release/npm": "^13.1.3",
67
+ "@types/express": "^5.0.6",
68
+ "@types/node": "^22.19.7",
69
+ "@vitest/coverage-v8": "^4.0.17",
70
+ "@vitest/ui": "^4.0.17",
68
71
  "husky": "^9.1.7",
69
- "semantic-release": "^24.2.9",
70
- "tsx": "^4.19.2",
72
+ "semantic-release": "^25.0.2",
73
+ "tsx": "^4.21.0",
71
74
  "typescript": "^5.7.2",
72
- "vitest": "^2.1.8"
75
+ "vitest": "^4.0.17"
73
76
  }
74
77
  }
@@ -2,21 +2,22 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { CohereEmbeddings } from "./cohere.js";
3
3
  import { CohereClient } from "cohere-ai";
4
4
 
5
+ const mockClient = {
6
+ embed: vi.fn().mockResolvedValue({ embeddings: [[]] }),
7
+ };
8
+
5
9
  vi.mock("cohere-ai", () => ({
6
- CohereClient: vi.fn(),
10
+ CohereClient: vi.fn().mockImplementation(function () {
11
+ return mockClient;
12
+ }),
7
13
  }));
8
14
 
9
15
  describe("CohereEmbeddings", () => {
10
16
  let embeddings: CohereEmbeddings;
11
- let mockClient: any;
12
17
 
13
18
  beforeEach(() => {
14
- mockClient = {
15
- embed: vi.fn(),
16
- };
17
-
18
- vi.mocked(CohereClient).mockImplementation(() => mockClient as any);
19
-
19
+ mockClient.embed.mockReset().mockResolvedValue({ embeddings: [[]] });
20
+ vi.mocked(CohereClient).mockClear();
20
21
  embeddings = new CohereEmbeddings("test-api-key");
21
22
  });
22
23
 
@@ -2,23 +2,26 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { OpenAIEmbeddings } from "./openai.js";
3
3
  import OpenAI from "openai";
4
4
 
5
+ const mockOpenAI = {
6
+ embeddings: {
7
+ create: vi.fn().mockResolvedValue({ data: [{ embedding: [] }] }),
8
+ },
9
+ };
10
+
5
11
  vi.mock("openai", () => ({
6
- default: vi.fn(),
12
+ default: vi.fn().mockImplementation(function () {
13
+ return mockOpenAI;
14
+ }),
7
15
  }));
8
16
 
9
17
  describe("OpenAIEmbeddings", () => {
10
18
  let embeddings: OpenAIEmbeddings;
11
- let mockOpenAI: any;
12
19
 
13
20
  beforeEach(() => {
14
- mockOpenAI = {
15
- embeddings: {
16
- create: vi.fn(),
17
- },
18
- };
19
-
20
- vi.mocked(OpenAI).mockImplementation(() => mockOpenAI as any);
21
-
21
+ mockOpenAI.embeddings.create
22
+ .mockReset()
23
+ .mockResolvedValue({ data: [{ embedding: [] }] });
24
+ vi.mocked(OpenAI).mockClear();
22
25
  embeddings = new OpenAIEmbeddings("test-api-key");
23
26
  });
24
27
 
package/src/index.ts CHANGED
@@ -29,27 +29,37 @@ import { CodeIndexer } from "./code/indexer.js";
29
29
  import type { CodeConfig } from "./code/types.js";
30
30
  import { EmbeddingProviderFactory } from "./embeddings/factory.js";
31
31
  import { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
32
- import { getPrompt, listPrompts, loadPromptsConfig, type PromptsConfig } from "./prompts/index.js";
32
+ import {
33
+ getPrompt,
34
+ listPrompts,
35
+ loadPromptsConfig,
36
+ type PromptsConfig,
37
+ } from "./prompts/index.js";
33
38
  import { renderTemplate, validateArguments } from "./prompts/template.js";
34
39
  import { QdrantManager } from "./qdrant/client.js";
35
40
 
36
41
  // Read package.json for version
37
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
38
- const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
43
+ const pkg = JSON.parse(
44
+ readFileSync(join(__dirname, "../package.json"), "utf-8"),
45
+ );
39
46
 
40
47
  // Validate environment variables
41
48
  const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333";
42
49
  const QDRANT_API_KEY = process.env.QDRANT_API_KEY;
43
- const EMBEDDING_PROVIDER = (process.env.EMBEDDING_PROVIDER || "ollama").toLowerCase();
50
+ const EMBEDDING_PROVIDER = (
51
+ process.env.EMBEDDING_PROVIDER || "ollama"
52
+ ).toLowerCase();
44
53
  const TRANSPORT_MODE = (process.env.TRANSPORT_MODE || "stdio").toLowerCase();
45
54
  const HTTP_PORT = parseInt(process.env.HTTP_PORT || "3000", 10);
46
- const PROMPTS_CONFIG_FILE = process.env.PROMPTS_CONFIG_FILE || join(__dirname, "../prompts.json");
55
+ const PROMPTS_CONFIG_FILE =
56
+ process.env.PROMPTS_CONFIG_FILE || join(__dirname, "../prompts.json");
47
57
 
48
58
  // Validate HTTP_PORT when HTTP mode is selected
49
59
  if (TRANSPORT_MODE === "http") {
50
60
  if (Number.isNaN(HTTP_PORT) || HTTP_PORT < 1 || HTTP_PORT > 65535) {
51
61
  console.error(
52
- `Error: Invalid HTTP_PORT "${process.env.HTTP_PORT}". Must be a number between 1 and 65535.`
62
+ `Error: Invalid HTTP_PORT "${process.env.HTTP_PORT}". Must be a number between 1 and 65535.`,
53
63
  );
54
64
  process.exit(1);
55
65
  }
@@ -75,13 +85,15 @@ if (EMBEDDING_PROVIDER !== "ollama") {
75
85
  break;
76
86
  default:
77
87
  console.error(
78
- `Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama.`
88
+ `Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama.`,
79
89
  );
80
90
  process.exit(1);
81
91
  }
82
92
 
83
93
  if (!apiKey) {
84
- console.error(`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`);
94
+ console.error(
95
+ `Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`,
96
+ );
85
97
  process.exit(1);
86
98
  }
87
99
  }
@@ -90,7 +102,8 @@ if (EMBEDDING_PROVIDER !== "ollama") {
90
102
  async function checkOllamaAvailability() {
91
103
  if (EMBEDDING_PROVIDER === "ollama") {
92
104
  const baseUrl = process.env.EMBEDDING_BASE_URL || "http://localhost:11434";
93
- const isLocalhost = baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
105
+ const isLocalhost =
106
+ baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
94
107
 
95
108
  try {
96
109
  const response = await fetch(`${baseUrl}/api/version`);
@@ -103,7 +116,7 @@ async function checkOllamaAvailability() {
103
116
  const { models } = await tagsResponse.json();
104
117
  const modelName = process.env.EMBEDDING_MODEL || "nomic-embed-text";
105
118
  const modelExists = models.some(
106
- (m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
119
+ (m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`),
107
120
  );
108
121
 
109
122
  if (!modelExists) {
@@ -112,6 +125,7 @@ async function checkOllamaAvailability() {
112
125
  if (isLocalhost) {
113
126
  errorMessage +=
114
127
  `Pull it with:\n` +
128
+ ` - Using Podman: podman exec ollama ollama pull ${modelName}\n` +
115
129
  ` - Using Docker: docker exec ollama ollama pull ${modelName}\n` +
116
130
  ` - Or locally: ollama pull ${modelName}`;
117
131
  } else {
@@ -133,6 +147,7 @@ async function checkOllamaAvailability() {
133
147
  if (isLocalhost) {
134
148
  helpText =
135
149
  `Please start Ollama:\n` +
150
+ ` - Using Podman: podman compose up -d\n` +
136
151
  ` - Using Docker: docker compose up -d\n` +
137
152
  ` - Or install locally: curl -fsSL https://ollama.ai/install.sh | sh\n` +
138
153
  `\nThen pull the embedding model:\n` +
@@ -157,13 +172,25 @@ const embeddings = EmbeddingProviderFactory.createFromEnv();
157
172
 
158
173
  // Initialize code indexer
159
174
  const codeConfig: CodeConfig = {
160
- chunkSize: parseInt(process.env.CODE_CHUNK_SIZE || String(DEFAULT_CHUNK_SIZE), 10),
161
- chunkOverlap: parseInt(process.env.CODE_CHUNK_OVERLAP || String(DEFAULT_CHUNK_OVERLAP), 10),
175
+ chunkSize: parseInt(
176
+ process.env.CODE_CHUNK_SIZE || String(DEFAULT_CHUNK_SIZE),
177
+ 10,
178
+ ),
179
+ chunkOverlap: parseInt(
180
+ process.env.CODE_CHUNK_OVERLAP || String(DEFAULT_CHUNK_OVERLAP),
181
+ 10,
182
+ ),
162
183
  enableASTChunking: process.env.CODE_ENABLE_AST !== "false",
163
184
  supportedExtensions: DEFAULT_CODE_EXTENSIONS,
164
185
  ignorePatterns: DEFAULT_IGNORE_PATTERNS,
165
- batchSize: parseInt(process.env.CODE_BATCH_SIZE || String(DEFAULT_BATCH_SIZE), 10),
166
- defaultSearchLimit: parseInt(process.env.CODE_SEARCH_LIMIT || String(DEFAULT_SEARCH_LIMIT), 10),
186
+ batchSize: parseInt(
187
+ process.env.CODE_BATCH_SIZE || String(DEFAULT_BATCH_SIZE),
188
+ 10,
189
+ ),
190
+ defaultSearchLimit: parseInt(
191
+ process.env.CODE_SEARCH_LIMIT || String(DEFAULT_SEARCH_LIMIT),
192
+ 10,
193
+ ),
167
194
  enableHybridSearch: process.env.CODE_ENABLE_HYBRID === "true",
168
195
  };
169
196
 
@@ -174,9 +201,14 @@ let promptsConfig: PromptsConfig | null = null;
174
201
  if (existsSync(PROMPTS_CONFIG_FILE)) {
175
202
  try {
176
203
  promptsConfig = loadPromptsConfig(PROMPTS_CONFIG_FILE);
177
- console.error(`Loaded ${promptsConfig.prompts.length} prompts from ${PROMPTS_CONFIG_FILE}`);
204
+ console.error(
205
+ `Loaded ${promptsConfig.prompts.length} prompts from ${PROMPTS_CONFIG_FILE}`,
206
+ );
178
207
  } catch (error) {
179
- console.error(`Failed to load prompts configuration from ${PROMPTS_CONFIG_FILE}:`, error);
208
+ console.error(
209
+ `Failed to load prompts configuration from ${PROMPTS_CONFIG_FILE}:`,
210
+ error,
211
+ );
180
212
  process.exit(1);
181
213
  }
182
214
  }
@@ -205,7 +237,7 @@ function createServer() {
205
237
  },
206
238
  {
207
239
  capabilities,
208
- }
240
+ },
209
241
  );
210
242
  }
211
243
 
@@ -236,7 +268,8 @@ function registerHandlers(server: Server) {
236
268
  },
237
269
  enableHybrid: {
238
270
  type: "boolean",
239
- description: "Enable hybrid search with sparse vectors (default: false)",
271
+ description:
272
+ "Enable hybrid search with sparse vectors (default: false)",
240
273
  },
241
274
  },
242
275
  required: ["name"],
@@ -269,7 +302,8 @@ function registerHandlers(server: Server) {
269
302
  },
270
303
  metadata: {
271
304
  type: "object",
272
- description: "Optional metadata to store with the document",
305
+ description:
306
+ "Optional metadata to store with the document",
273
307
  },
274
308
  },
275
309
  required: ["id", "text"],
@@ -345,7 +379,8 @@ function registerHandlers(server: Server) {
345
379
  },
346
380
  {
347
381
  name: "delete_documents",
348
- description: "Delete specific documents from a collection by their IDs.",
382
+ description:
383
+ "Delete specific documents from a collection by their IDs.",
349
384
  inputSchema: {
350
385
  type: "object",
351
386
  properties: {
@@ -400,21 +435,25 @@ function registerHandlers(server: Server) {
400
435
  properties: {
401
436
  path: {
402
437
  type: "string",
403
- description: "Absolute or relative path to codebase root directory",
438
+ description:
439
+ "Absolute or relative path to codebase root directory",
404
440
  },
405
441
  forceReindex: {
406
442
  type: "boolean",
407
- description: "Force full re-index even if already indexed (default: false)",
443
+ description:
444
+ "Force full re-index even if already indexed (default: false)",
408
445
  },
409
446
  extensions: {
410
447
  type: "array",
411
448
  items: { type: "string" },
412
- description: "Custom file extensions to index (e.g., ['.proto', '.graphql'])",
449
+ description:
450
+ "Custom file extensions to index (e.g., ['.proto', '.graphql'])",
413
451
  },
414
452
  ignorePatterns: {
415
453
  type: "array",
416
454
  items: { type: "string" },
417
- description: "Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
455
+ description:
456
+ "Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
418
457
  },
419
458
  },
420
459
  required: ["path"],
@@ -433,7 +472,8 @@ function registerHandlers(server: Server) {
433
472
  },
434
473
  query: {
435
474
  type: "string",
436
- description: "Natural language search query (e.g., 'authentication logic')",
475
+ description:
476
+ "Natural language search query (e.g., 'authentication logic')",
437
477
  },
438
478
  limit: {
439
479
  type: "number",
@@ -446,7 +486,8 @@ function registerHandlers(server: Server) {
446
486
  },
447
487
  pathPattern: {
448
488
  type: "string",
449
- description: "Filter by path glob pattern (e.g., 'src/services/**')",
489
+ description:
490
+ "Filter by path glob pattern (e.g., 'src/services/**')",
450
491
  },
451
492
  },
452
493
  required: ["path", "query"],
@@ -507,9 +548,15 @@ function registerHandlers(server: Server) {
507
548
  try {
508
549
  switch (name) {
509
550
  case "create_collection": {
510
- const { name, distance, enableHybrid } = CreateCollectionSchema.parse(args);
551
+ const { name, distance, enableHybrid } =
552
+ CreateCollectionSchema.parse(args);
511
553
  const vectorSize = embeddings.getDimensions();
512
- await qdrant.createCollection(name, vectorSize, distance, enableHybrid || false);
554
+ await qdrant.createCollection(
555
+ name,
556
+ vectorSize,
557
+ distance,
558
+ enableHybrid || false,
559
+ );
513
560
 
514
561
  let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
515
562
  if (enableHybrid) {
@@ -590,7 +637,8 @@ function registerHandlers(server: Server) {
590
637
  }
591
638
 
592
639
  case "semantic_search": {
593
- const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
640
+ const { collection, query, limit, filter } =
641
+ SemanticSearchSchema.parse(args);
594
642
 
595
643
  // Check if collection exists
596
644
  const exists = await qdrant.collectionExists(collection);
@@ -610,7 +658,12 @@ function registerHandlers(server: Server) {
610
658
  const { embedding } = await embeddings.embed(query);
611
659
 
612
660
  // Search
613
- const results = await qdrant.search(collection, embedding, limit || 5, filter);
661
+ const results = await qdrant.search(
662
+ collection,
663
+ embedding,
664
+ limit || 5,
665
+ filter,
666
+ );
614
667
 
615
668
  return {
616
669
  content: [
@@ -674,7 +727,8 @@ function registerHandlers(server: Server) {
674
727
  }
675
728
 
676
729
  case "hybrid_search": {
677
- const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
730
+ const { collection, query, limit, filter } =
731
+ HybridSearchSchema.parse(args);
678
732
 
679
733
  // Check if collection exists
680
734
  const exists = await qdrant.collectionExists(collection);
@@ -717,7 +771,7 @@ function registerHandlers(server: Server) {
717
771
  embedding,
718
772
  sparseVector,
719
773
  limit || 5,
720
- filter
774
+ filter,
721
775
  );
722
776
 
723
777
  return {
@@ -746,8 +800,10 @@ function registerHandlers(server: Server) {
746
800
  { forceReindex, extensions, ignorePatterns },
747
801
  (progress) => {
748
802
  // Progress callback - could send progress updates via SSE in future
749
- console.error(`[${progress.phase}] ${progress.percentage}% - ${progress.message}`);
750
- }
803
+ console.error(
804
+ `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
805
+ );
806
+ },
751
807
  );
752
808
 
753
809
  let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
@@ -778,7 +834,8 @@ function registerHandlers(server: Server) {
778
834
  pathPattern: z.string().optional(),
779
835
  });
780
836
 
781
- const { path, query, limit, fileTypes, pathPattern } = SearchCodeSchema.parse(args);
837
+ const { path, query, limit, fileTypes, pathPattern } =
838
+ SearchCodeSchema.parse(args);
782
839
 
783
840
  const results = await codeIndexer.searchCode(path, query, {
784
841
  limit,
@@ -804,7 +861,7 @@ function registerHandlers(server: Server) {
804
861
  `\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
805
862
  `File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
806
863
  `Language: ${r.language}\n\n` +
807
- `${r.content}\n`
864
+ `${r.content}\n`,
808
865
  )
809
866
  .join("\n");
810
867
 
@@ -855,7 +912,9 @@ function registerHandlers(server: Server) {
855
912
  const { path } = ReindexChangesSchema.parse(args);
856
913
 
857
914
  const stats = await codeIndexer.reindexChanges(path, (progress) => {
858
- console.error(`[${progress.phase}] ${progress.percentage}% - ${progress.message}`);
915
+ console.error(
916
+ `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
917
+ );
859
918
  });
860
919
 
861
920
  let message = `Incremental re-index complete:\n`;
@@ -865,7 +924,11 @@ function registerHandlers(server: Server) {
865
924
  message += `- Chunks added: ${stats.chunksAdded}\n`;
866
925
  message += `- Duration: ${(stats.durationMs / 1000).toFixed(1)}s`;
867
926
 
868
- if (stats.filesAdded === 0 && stats.filesModified === 0 && stats.filesDeleted === 0) {
927
+ if (
928
+ stats.filesAdded === 0 &&
929
+ stats.filesModified === 0 &&
930
+ stats.filesDeleted === 0
931
+ ) {
869
932
  message = `No changes detected. Codebase is up to date.`;
870
933
  }
871
934
 
@@ -910,7 +973,8 @@ function registerHandlers(server: Server) {
910
973
  }
911
974
  } catch (error: any) {
912
975
  // Enhanced error details for debugging
913
- const errorDetails = error instanceof Error ? error.message : JSON.stringify(error, null, 2);
976
+ const errorDetails =
977
+ error instanceof Error ? error.message : JSON.stringify(error, null, 2);
914
978
 
915
979
  console.error("Tool execution error:", {
916
980
  tool: name,
@@ -1028,7 +1092,11 @@ function registerHandlers(server: Server) {
1028
1092
  validateArguments(args || {}, prompt.arguments);
1029
1093
 
1030
1094
  // Render template
1031
- const rendered = renderTemplate(prompt.template, args || {}, prompt.arguments);
1095
+ const rendered = renderTemplate(
1096
+ prompt.template,
1097
+ args || {},
1098
+ prompt.arguments,
1099
+ );
1032
1100
 
1033
1101
  return {
1034
1102
  messages: [
@@ -1043,7 +1111,7 @@ function registerHandlers(server: Server) {
1043
1111
  };
1044
1112
  } catch (error) {
1045
1113
  throw new Error(
1046
- `Failed to render prompt "${name}": ${error instanceof Error ? error.message : String(error)}`
1114
+ `Failed to render prompt "${name}": ${error instanceof Error ? error.message : String(error)}`,
1047
1115
  );
1048
1116
  }
1049
1117
  });
@@ -1071,13 +1139,15 @@ const AddDocumentsSchema = z.object({
1071
1139
  documents: z
1072
1140
  .array(
1073
1141
  z.object({
1074
- id: z.union([z.string(), z.number()]).describe("Unique identifier for the document"),
1142
+ id: z
1143
+ .union([z.string(), z.number()])
1144
+ .describe("Unique identifier for the document"),
1075
1145
  text: z.string().describe("Text content to embed and store"),
1076
1146
  metadata: z
1077
1147
  .record(z.any())
1078
1148
  .optional()
1079
1149
  .describe("Optional metadata to store with the document"),
1080
- })
1150
+ }),
1081
1151
  )
1082
1152
  .describe("Array of documents to add"),
1083
1153
  });
@@ -1085,7 +1155,10 @@ const AddDocumentsSchema = z.object({
1085
1155
  const SemanticSearchSchema = z.object({
1086
1156
  collection: z.string().describe("Name of the collection to search"),
1087
1157
  query: z.string().describe("Search query text"),
1088
- limit: z.number().optional().describe("Maximum number of results (default: 5)"),
1158
+ limit: z
1159
+ .number()
1160
+ .optional()
1161
+ .describe("Maximum number of results (default: 5)"),
1089
1162
  filter: z.record(z.any()).optional().describe("Optional metadata filter"),
1090
1163
  });
1091
1164
 
@@ -1099,13 +1172,18 @@ const GetCollectionInfoSchema = z.object({
1099
1172
 
1100
1173
  const DeleteDocumentsSchema = z.object({
1101
1174
  collection: z.string().describe("Name of the collection"),
1102
- ids: z.array(z.union([z.string(), z.number()])).describe("Array of document IDs to delete"),
1175
+ ids: z
1176
+ .array(z.union([z.string(), z.number()]))
1177
+ .describe("Array of document IDs to delete"),
1103
1178
  });
1104
1179
 
1105
1180
  const HybridSearchSchema = z.object({
1106
1181
  collection: z.string().describe("Name of the collection to search"),
1107
1182
  query: z.string().describe("Search query text"),
1108
- limit: z.number().optional().describe("Maximum number of results (default: 5)"),
1183
+ limit: z
1184
+ .number()
1185
+ .optional()
1186
+ .describe("Maximum number of results (default: 5)"),
1109
1187
  filter: z.record(z.any()).optional().describe("Optional metadata filter"),
1110
1188
  });
1111
1189
 
@@ -1122,9 +1200,20 @@ const RATE_LIMIT_MAX_REQUESTS = 100; // Max requests per window
1122
1200
  const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
1123
1201
  const RATE_LIMIT_MAX_CONCURRENT = 10; // Max concurrent requests per IP
1124
1202
  const RATE_LIMITER_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
1125
- const REQUEST_TIMEOUT_MS = 30 * 1000; // 30 seconds per request
1203
+ const REQUEST_TIMEOUT_MS = parseInt(
1204
+ process.env.HTTP_REQUEST_TIMEOUT_MS || "300000",
1205
+ 10,
1206
+ );
1126
1207
  const SHUTDOWN_GRACE_PERIOD_MS = 10 * 1000; // 10 seconds
1127
1208
 
1209
+ // Validate REQUEST_TIMEOUT_MS
1210
+ if (Number.isNaN(REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS <= 0) {
1211
+ console.error(
1212
+ `Error: Invalid HTTP_REQUEST_TIMEOUT_MS "${process.env.HTTP_REQUEST_TIMEOUT_MS}". Must be a positive integer.`,
1213
+ );
1214
+ process.exit(1);
1215
+ }
1216
+
1128
1217
  // Start server with HTTP transport
1129
1218
  async function startHttpServer() {
1130
1219
  await checkOllamaAvailability();
@@ -1148,7 +1237,7 @@ async function startHttpServer() {
1148
1237
  res: express.Response,
1149
1238
  code: number,
1150
1239
  message: string,
1151
- httpStatus: number = 500
1240
+ httpStatus: number = 500,
1152
1241
  ) => {
1153
1242
  if (!res.headersSent) {
1154
1243
  res.status(httpStatus).json({
@@ -1187,7 +1276,7 @@ async function startHttpServer() {
1187
1276
  const rateLimitMiddleware = async (
1188
1277
  req: express.Request,
1189
1278
  res: express.Response,
1190
- next: express.NextFunction
1279
+ next: express.NextFunction,
1191
1280
  ) => {
1192
1281
  const clientIp = req.ip || req.socket.remoteAddress || "unknown";
1193
1282
 
@@ -1281,7 +1370,9 @@ async function startHttpServer() {
1281
1370
 
1282
1371
  const httpServer = app
1283
1372
  .listen(HTTP_PORT, () => {
1284
- console.error(`Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`);
1373
+ console.error(
1374
+ `Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`,
1375
+ );
1285
1376
  })
1286
1377
  .on("error", (error) => {
1287
1378
  console.error("HTTP server error:", error);
@@ -1295,7 +1386,9 @@ async function startHttpServer() {
1295
1386
  if (isShuttingDown) return;
1296
1387
  isShuttingDown = true;
1297
1388
 
1298
- console.error("Shutdown signal received, closing HTTP server gracefully...");
1389
+ console.error(
1390
+ "Shutdown signal received, closing HTTP server gracefully...",
1391
+ );
1299
1392
 
1300
1393
  // Clear the cleanup interval to allow graceful shutdown
1301
1394
  clearInterval(cleanupIntervalId);
@@ -1325,7 +1418,7 @@ async function main() {
1325
1418
  await startStdioServer();
1326
1419
  } else {
1327
1420
  console.error(
1328
- `Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`
1421
+ `Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`,
1329
1422
  );
1330
1423
  process.exit(1);
1331
1424
  }