@mhalder/qdrant-mcp-server 1.5.0 → 2.0.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/src/index.ts CHANGED
@@ -29,26 +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
- const EMBEDDING_PROVIDER = (process.env.EMBEDDING_PROVIDER || "ollama").toLowerCase();
49
+ const QDRANT_API_KEY = process.env.QDRANT_API_KEY;
50
+ const EMBEDDING_PROVIDER = (
51
+ process.env.EMBEDDING_PROVIDER || "ollama"
52
+ ).toLowerCase();
43
53
  const TRANSPORT_MODE = (process.env.TRANSPORT_MODE || "stdio").toLowerCase();
44
54
  const HTTP_PORT = parseInt(process.env.HTTP_PORT || "3000", 10);
45
- 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");
46
57
 
47
58
  // Validate HTTP_PORT when HTTP mode is selected
48
59
  if (TRANSPORT_MODE === "http") {
49
60
  if (Number.isNaN(HTTP_PORT) || HTTP_PORT < 1 || HTTP_PORT > 65535) {
50
61
  console.error(
51
- `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.`,
52
63
  );
53
64
  process.exit(1);
54
65
  }
@@ -74,13 +85,15 @@ if (EMBEDDING_PROVIDER !== "ollama") {
74
85
  break;
75
86
  default:
76
87
  console.error(
77
- `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.`,
78
89
  );
79
90
  process.exit(1);
80
91
  }
81
92
 
82
93
  if (!apiKey) {
83
- console.error(`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`);
94
+ console.error(
95
+ `Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`,
96
+ );
84
97
  process.exit(1);
85
98
  }
86
99
  }
@@ -89,7 +102,8 @@ if (EMBEDDING_PROVIDER !== "ollama") {
89
102
  async function checkOllamaAvailability() {
90
103
  if (EMBEDDING_PROVIDER === "ollama") {
91
104
  const baseUrl = process.env.EMBEDDING_BASE_URL || "http://localhost:11434";
92
- const isLocalhost = baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
105
+ const isLocalhost =
106
+ baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
93
107
 
94
108
  try {
95
109
  const response = await fetch(`${baseUrl}/api/version`);
@@ -102,7 +116,7 @@ async function checkOllamaAvailability() {
102
116
  const { models } = await tagsResponse.json();
103
117
  const modelName = process.env.EMBEDDING_MODEL || "nomic-embed-text";
104
118
  const modelExists = models.some(
105
- (m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
119
+ (m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`),
106
120
  );
107
121
 
108
122
  if (!modelExists) {
@@ -111,6 +125,7 @@ async function checkOllamaAvailability() {
111
125
  if (isLocalhost) {
112
126
  errorMessage +=
113
127
  `Pull it with:\n` +
128
+ ` - Using Podman: podman exec ollama ollama pull ${modelName}\n` +
114
129
  ` - Using Docker: docker exec ollama ollama pull ${modelName}\n` +
115
130
  ` - Or locally: ollama pull ${modelName}`;
116
131
  } else {
@@ -132,6 +147,7 @@ async function checkOllamaAvailability() {
132
147
  if (isLocalhost) {
133
148
  helpText =
134
149
  `Please start Ollama:\n` +
150
+ ` - Using Podman: podman compose up -d\n` +
135
151
  ` - Using Docker: docker compose up -d\n` +
136
152
  ` - Or install locally: curl -fsSL https://ollama.ai/install.sh | sh\n` +
137
153
  `\nThen pull the embedding model:\n` +
@@ -151,18 +167,30 @@ async function checkOllamaAvailability() {
151
167
  }
152
168
 
153
169
  // Initialize clients
154
- const qdrant = new QdrantManager(QDRANT_URL);
170
+ const qdrant = new QdrantManager(QDRANT_URL, QDRANT_API_KEY);
155
171
  const embeddings = EmbeddingProviderFactory.createFromEnv();
156
172
 
157
173
  // Initialize code indexer
158
174
  const codeConfig: CodeConfig = {
159
- chunkSize: parseInt(process.env.CODE_CHUNK_SIZE || String(DEFAULT_CHUNK_SIZE), 10),
160
- 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
+ ),
161
183
  enableASTChunking: process.env.CODE_ENABLE_AST !== "false",
162
184
  supportedExtensions: DEFAULT_CODE_EXTENSIONS,
163
185
  ignorePatterns: DEFAULT_IGNORE_PATTERNS,
164
- batchSize: parseInt(process.env.CODE_BATCH_SIZE || String(DEFAULT_BATCH_SIZE), 10),
165
- 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
+ ),
166
194
  enableHybridSearch: process.env.CODE_ENABLE_HYBRID === "true",
167
195
  };
168
196
 
@@ -173,9 +201,14 @@ let promptsConfig: PromptsConfig | null = null;
173
201
  if (existsSync(PROMPTS_CONFIG_FILE)) {
174
202
  try {
175
203
  promptsConfig = loadPromptsConfig(PROMPTS_CONFIG_FILE);
176
- 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
+ );
177
207
  } catch (error) {
178
- 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
+ );
179
212
  process.exit(1);
180
213
  }
181
214
  }
@@ -204,7 +237,7 @@ function createServer() {
204
237
  },
205
238
  {
206
239
  capabilities,
207
- }
240
+ },
208
241
  );
209
242
  }
210
243
 
@@ -235,7 +268,8 @@ function registerHandlers(server: Server) {
235
268
  },
236
269
  enableHybrid: {
237
270
  type: "boolean",
238
- description: "Enable hybrid search with sparse vectors (default: false)",
271
+ description:
272
+ "Enable hybrid search with sparse vectors (default: false)",
239
273
  },
240
274
  },
241
275
  required: ["name"],
@@ -268,7 +302,8 @@ function registerHandlers(server: Server) {
268
302
  },
269
303
  metadata: {
270
304
  type: "object",
271
- description: "Optional metadata to store with the document",
305
+ description:
306
+ "Optional metadata to store with the document",
272
307
  },
273
308
  },
274
309
  required: ["id", "text"],
@@ -344,7 +379,8 @@ function registerHandlers(server: Server) {
344
379
  },
345
380
  {
346
381
  name: "delete_documents",
347
- description: "Delete specific documents from a collection by their IDs.",
382
+ description:
383
+ "Delete specific documents from a collection by their IDs.",
348
384
  inputSchema: {
349
385
  type: "object",
350
386
  properties: {
@@ -399,21 +435,25 @@ function registerHandlers(server: Server) {
399
435
  properties: {
400
436
  path: {
401
437
  type: "string",
402
- description: "Absolute or relative path to codebase root directory",
438
+ description:
439
+ "Absolute or relative path to codebase root directory",
403
440
  },
404
441
  forceReindex: {
405
442
  type: "boolean",
406
- description: "Force full re-index even if already indexed (default: false)",
443
+ description:
444
+ "Force full re-index even if already indexed (default: false)",
407
445
  },
408
446
  extensions: {
409
447
  type: "array",
410
448
  items: { type: "string" },
411
- description: "Custom file extensions to index (e.g., ['.proto', '.graphql'])",
449
+ description:
450
+ "Custom file extensions to index (e.g., ['.proto', '.graphql'])",
412
451
  },
413
452
  ignorePatterns: {
414
453
  type: "array",
415
454
  items: { type: "string" },
416
- description: "Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
455
+ description:
456
+ "Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
417
457
  },
418
458
  },
419
459
  required: ["path"],
@@ -432,7 +472,8 @@ function registerHandlers(server: Server) {
432
472
  },
433
473
  query: {
434
474
  type: "string",
435
- description: "Natural language search query (e.g., 'authentication logic')",
475
+ description:
476
+ "Natural language search query (e.g., 'authentication logic')",
436
477
  },
437
478
  limit: {
438
479
  type: "number",
@@ -445,7 +486,8 @@ function registerHandlers(server: Server) {
445
486
  },
446
487
  pathPattern: {
447
488
  type: "string",
448
- description: "Filter by path glob pattern (e.g., 'src/services/**')",
489
+ description:
490
+ "Filter by path glob pattern (e.g., 'src/services/**')",
449
491
  },
450
492
  },
451
493
  required: ["path", "query"],
@@ -506,9 +548,15 @@ function registerHandlers(server: Server) {
506
548
  try {
507
549
  switch (name) {
508
550
  case "create_collection": {
509
- const { name, distance, enableHybrid } = CreateCollectionSchema.parse(args);
551
+ const { name, distance, enableHybrid } =
552
+ CreateCollectionSchema.parse(args);
510
553
  const vectorSize = embeddings.getDimensions();
511
- await qdrant.createCollection(name, vectorSize, distance, enableHybrid || false);
554
+ await qdrant.createCollection(
555
+ name,
556
+ vectorSize,
557
+ distance,
558
+ enableHybrid || false,
559
+ );
512
560
 
513
561
  let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
514
562
  if (enableHybrid) {
@@ -589,7 +637,8 @@ function registerHandlers(server: Server) {
589
637
  }
590
638
 
591
639
  case "semantic_search": {
592
- const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
640
+ const { collection, query, limit, filter } =
641
+ SemanticSearchSchema.parse(args);
593
642
 
594
643
  // Check if collection exists
595
644
  const exists = await qdrant.collectionExists(collection);
@@ -609,7 +658,12 @@ function registerHandlers(server: Server) {
609
658
  const { embedding } = await embeddings.embed(query);
610
659
 
611
660
  // Search
612
- 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
+ );
613
667
 
614
668
  return {
615
669
  content: [
@@ -673,7 +727,8 @@ function registerHandlers(server: Server) {
673
727
  }
674
728
 
675
729
  case "hybrid_search": {
676
- const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
730
+ const { collection, query, limit, filter } =
731
+ HybridSearchSchema.parse(args);
677
732
 
678
733
  // Check if collection exists
679
734
  const exists = await qdrant.collectionExists(collection);
@@ -716,7 +771,7 @@ function registerHandlers(server: Server) {
716
771
  embedding,
717
772
  sparseVector,
718
773
  limit || 5,
719
- filter
774
+ filter,
720
775
  );
721
776
 
722
777
  return {
@@ -745,8 +800,10 @@ function registerHandlers(server: Server) {
745
800
  { forceReindex, extensions, ignorePatterns },
746
801
  (progress) => {
747
802
  // Progress callback - could send progress updates via SSE in future
748
- console.error(`[${progress.phase}] ${progress.percentage}% - ${progress.message}`);
749
- }
803
+ console.error(
804
+ `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
805
+ );
806
+ },
750
807
  );
751
808
 
752
809
  let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
@@ -777,7 +834,8 @@ function registerHandlers(server: Server) {
777
834
  pathPattern: z.string().optional(),
778
835
  });
779
836
 
780
- const { path, query, limit, fileTypes, pathPattern } = SearchCodeSchema.parse(args);
837
+ const { path, query, limit, fileTypes, pathPattern } =
838
+ SearchCodeSchema.parse(args);
781
839
 
782
840
  const results = await codeIndexer.searchCode(path, query, {
783
841
  limit,
@@ -803,7 +861,7 @@ function registerHandlers(server: Server) {
803
861
  `\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
804
862
  `File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
805
863
  `Language: ${r.language}\n\n` +
806
- `${r.content}\n`
864
+ `${r.content}\n`,
807
865
  )
808
866
  .join("\n");
809
867
 
@@ -854,7 +912,9 @@ function registerHandlers(server: Server) {
854
912
  const { path } = ReindexChangesSchema.parse(args);
855
913
 
856
914
  const stats = await codeIndexer.reindexChanges(path, (progress) => {
857
- console.error(`[${progress.phase}] ${progress.percentage}% - ${progress.message}`);
915
+ console.error(
916
+ `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
917
+ );
858
918
  });
859
919
 
860
920
  let message = `Incremental re-index complete:\n`;
@@ -864,7 +924,11 @@ function registerHandlers(server: Server) {
864
924
  message += `- Chunks added: ${stats.chunksAdded}\n`;
865
925
  message += `- Duration: ${(stats.durationMs / 1000).toFixed(1)}s`;
866
926
 
867
- 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
+ ) {
868
932
  message = `No changes detected. Codebase is up to date.`;
869
933
  }
870
934
 
@@ -909,7 +973,8 @@ function registerHandlers(server: Server) {
909
973
  }
910
974
  } catch (error: any) {
911
975
  // Enhanced error details for debugging
912
- 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);
913
978
 
914
979
  console.error("Tool execution error:", {
915
980
  tool: name,
@@ -1027,7 +1092,11 @@ function registerHandlers(server: Server) {
1027
1092
  validateArguments(args || {}, prompt.arguments);
1028
1093
 
1029
1094
  // Render template
1030
- const rendered = renderTemplate(prompt.template, args || {}, prompt.arguments);
1095
+ const rendered = renderTemplate(
1096
+ prompt.template,
1097
+ args || {},
1098
+ prompt.arguments,
1099
+ );
1031
1100
 
1032
1101
  return {
1033
1102
  messages: [
@@ -1042,7 +1111,7 @@ function registerHandlers(server: Server) {
1042
1111
  };
1043
1112
  } catch (error) {
1044
1113
  throw new Error(
1045
- `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)}`,
1046
1115
  );
1047
1116
  }
1048
1117
  });
@@ -1070,13 +1139,15 @@ const AddDocumentsSchema = z.object({
1070
1139
  documents: z
1071
1140
  .array(
1072
1141
  z.object({
1073
- 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"),
1074
1145
  text: z.string().describe("Text content to embed and store"),
1075
1146
  metadata: z
1076
1147
  .record(z.any())
1077
1148
  .optional()
1078
1149
  .describe("Optional metadata to store with the document"),
1079
- })
1150
+ }),
1080
1151
  )
1081
1152
  .describe("Array of documents to add"),
1082
1153
  });
@@ -1084,7 +1155,10 @@ const AddDocumentsSchema = z.object({
1084
1155
  const SemanticSearchSchema = z.object({
1085
1156
  collection: z.string().describe("Name of the collection to search"),
1086
1157
  query: z.string().describe("Search query text"),
1087
- 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)"),
1088
1162
  filter: z.record(z.any()).optional().describe("Optional metadata filter"),
1089
1163
  });
1090
1164
 
@@ -1098,13 +1172,18 @@ const GetCollectionInfoSchema = z.object({
1098
1172
 
1099
1173
  const DeleteDocumentsSchema = z.object({
1100
1174
  collection: z.string().describe("Name of the collection"),
1101
- 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"),
1102
1178
  });
1103
1179
 
1104
1180
  const HybridSearchSchema = z.object({
1105
1181
  collection: z.string().describe("Name of the collection to search"),
1106
1182
  query: z.string().describe("Search query text"),
1107
- 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)"),
1108
1187
  filter: z.record(z.any()).optional().describe("Optional metadata filter"),
1109
1188
  });
1110
1189
 
@@ -1147,7 +1226,7 @@ async function startHttpServer() {
1147
1226
  res: express.Response,
1148
1227
  code: number,
1149
1228
  message: string,
1150
- httpStatus: number = 500
1229
+ httpStatus: number = 500,
1151
1230
  ) => {
1152
1231
  if (!res.headersSent) {
1153
1232
  res.status(httpStatus).json({
@@ -1186,7 +1265,7 @@ async function startHttpServer() {
1186
1265
  const rateLimitMiddleware = async (
1187
1266
  req: express.Request,
1188
1267
  res: express.Response,
1189
- next: express.NextFunction
1268
+ next: express.NextFunction,
1190
1269
  ) => {
1191
1270
  const clientIp = req.ip || req.socket.remoteAddress || "unknown";
1192
1271
 
@@ -1280,7 +1359,9 @@ async function startHttpServer() {
1280
1359
 
1281
1360
  const httpServer = app
1282
1361
  .listen(HTTP_PORT, () => {
1283
- console.error(`Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`);
1362
+ console.error(
1363
+ `Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`,
1364
+ );
1284
1365
  })
1285
1366
  .on("error", (error) => {
1286
1367
  console.error("HTTP server error:", error);
@@ -1294,7 +1375,9 @@ async function startHttpServer() {
1294
1375
  if (isShuttingDown) return;
1295
1376
  isShuttingDown = true;
1296
1377
 
1297
- console.error("Shutdown signal received, closing HTTP server gracefully...");
1378
+ console.error(
1379
+ "Shutdown signal received, closing HTTP server gracefully...",
1380
+ );
1298
1381
 
1299
1382
  // Clear the cleanup interval to allow graceful shutdown
1300
1383
  clearInterval(cleanupIntervalId);
@@ -1324,7 +1407,7 @@ async function main() {
1324
1407
  await startStdioServer();
1325
1408
  } else {
1326
1409
  console.error(
1327
- `Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`
1410
+ `Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`,
1328
1411
  );
1329
1412
  process.exit(1);
1330
1413
  }