@mhalder/qdrant-mcp-server 3.2.1 → 3.3.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +12 -21
  3. package/build/code/chunker/tree-sitter-chunker.d.ts.map +1 -1
  4. package/build/code/chunker/tree-sitter-chunker.js +15 -3
  5. package/build/code/chunker/tree-sitter-chunker.js.map +1 -1
  6. package/build/code/indexer.d.ts +1 -0
  7. package/build/code/indexer.d.ts.map +1 -1
  8. package/build/code/indexer.js +24 -4
  9. package/build/code/indexer.js.map +1 -1
  10. package/build/embeddings/cohere.d.ts +1 -0
  11. package/build/embeddings/cohere.d.ts.map +1 -1
  12. package/build/embeddings/cohere.js +8 -1
  13. package/build/embeddings/cohere.js.map +1 -1
  14. package/build/embeddings/cohere.test.js +11 -0
  15. package/build/embeddings/cohere.test.js.map +1 -1
  16. package/build/embeddings/factory.d.ts.map +1 -1
  17. package/build/embeddings/factory.js +2 -0
  18. package/build/embeddings/factory.js.map +1 -1
  19. package/build/embeddings/factory.test.js +12 -1
  20. package/build/embeddings/factory.test.js.map +1 -1
  21. package/build/embeddings/ollama.d.ts +1 -0
  22. package/build/embeddings/ollama.d.ts.map +1 -1
  23. package/build/embeddings/ollama.js +8 -1
  24. package/build/embeddings/ollama.js.map +1 -1
  25. package/build/embeddings/ollama.test.js +11 -0
  26. package/build/embeddings/ollama.test.js.map +1 -1
  27. package/build/embeddings/openai.d.ts +1 -0
  28. package/build/embeddings/openai.d.ts.map +1 -1
  29. package/build/embeddings/openai.js +8 -1
  30. package/build/embeddings/openai.js.map +1 -1
  31. package/build/embeddings/openai.test.js +11 -0
  32. package/build/embeddings/openai.test.js.map +1 -1
  33. package/build/embeddings/voyage.d.ts +1 -0
  34. package/build/embeddings/voyage.d.ts.map +1 -1
  35. package/build/embeddings/voyage.js +8 -1
  36. package/build/embeddings/voyage.js.map +1 -1
  37. package/build/embeddings/voyage.test.js +11 -0
  38. package/build/embeddings/voyage.test.js.map +1 -1
  39. package/build/git/indexer.d.ts +1 -0
  40. package/build/git/indexer.d.ts.map +1 -1
  41. package/build/git/indexer.js +16 -3
  42. package/build/git/indexer.js.map +1 -1
  43. package/build/git/indexer.test.js +15 -9
  44. package/build/git/indexer.test.js.map +1 -1
  45. package/build/index.js +35 -26
  46. package/build/index.js.map +1 -1
  47. package/build/index.test.js +105 -91
  48. package/build/index.test.js.map +1 -1
  49. package/build/logger.d.ts +4 -0
  50. package/build/logger.d.ts.map +1 -0
  51. package/build/logger.js +24 -0
  52. package/build/logger.js.map +1 -0
  53. package/build/qdrant/client.d.ts +1 -0
  54. package/build/qdrant/client.d.ts.map +1 -1
  55. package/build/qdrant/client.js +10 -0
  56. package/build/qdrant/client.js.map +1 -1
  57. package/build/qdrant/client.test.js +11 -0
  58. package/build/qdrant/client.test.js.map +1 -1
  59. package/build/tools/code.d.ts.map +1 -1
  60. package/build/tools/code.js +44 -13
  61. package/build/tools/code.js.map +1 -1
  62. package/build/tools/collection.d.ts.map +1 -1
  63. package/build/tools/collection.js +15 -8
  64. package/build/tools/collection.js.map +1 -1
  65. package/build/tools/document.d.ts.map +1 -1
  66. package/build/tools/document.js +9 -4
  67. package/build/tools/document.js.map +1 -1
  68. package/build/tools/federated.d.ts.map +1 -1
  69. package/build/tools/federated.js +9 -4
  70. package/build/tools/federated.js.map +1 -1
  71. package/build/tools/federated.test.js +11 -0
  72. package/build/tools/federated.test.js.map +1 -1
  73. package/build/tools/git-history.d.ts.map +1 -1
  74. package/build/tools/git-history.js +44 -12
  75. package/build/tools/git-history.js.map +1 -1
  76. package/build/tools/logging.d.ts +16 -0
  77. package/build/tools/logging.d.ts.map +1 -0
  78. package/build/tools/logging.js +68 -0
  79. package/build/tools/logging.js.map +1 -0
  80. package/build/tools/logging.test.d.ts +2 -0
  81. package/build/tools/logging.test.d.ts.map +1 -0
  82. package/build/tools/logging.test.js +139 -0
  83. package/build/tools/logging.test.js.map +1 -0
  84. package/build/tools/schemas.d.ts +32 -19
  85. package/build/tools/schemas.d.ts.map +1 -1
  86. package/build/tools/schemas.js +9 -3
  87. package/build/tools/schemas.js.map +1 -1
  88. package/build/tools/search.d.ts.map +1 -1
  89. package/build/tools/search.js +13 -4
  90. package/build/tools/search.js.map +1 -1
  91. package/mise.toml +2 -0
  92. package/package.json +14 -13
  93. package/src/code/chunker/tree-sitter-chunker.ts +41 -9
  94. package/src/code/indexer.ts +41 -6
  95. package/src/embeddings/cohere.test.ts +12 -0
  96. package/src/embeddings/cohere.ts +10 -2
  97. package/src/embeddings/factory.test.ts +13 -1
  98. package/src/embeddings/factory.ts +3 -0
  99. package/src/embeddings/ollama.test.ts +12 -0
  100. package/src/embeddings/ollama.ts +10 -2
  101. package/src/embeddings/openai.test.ts +12 -0
  102. package/src/embeddings/openai.ts +10 -2
  103. package/src/embeddings/voyage.test.ts +12 -0
  104. package/src/embeddings/voyage.ts +10 -2
  105. package/src/git/indexer.test.ts +22 -16
  106. package/src/git/indexer.ts +30 -4
  107. package/src/index.test.ts +128 -106
  108. package/src/index.ts +59 -38
  109. package/src/logger.ts +33 -0
  110. package/src/qdrant/client.test.ts +12 -0
  111. package/src/qdrant/client.ts +22 -0
  112. package/src/tools/code.ts +107 -62
  113. package/src/tools/collection.ts +39 -22
  114. package/src/tools/document.ts +52 -22
  115. package/src/tools/federated.test.ts +12 -0
  116. package/src/tools/federated.ts +143 -125
  117. package/src/tools/git-history.ts +117 -60
  118. package/src/tools/logging.test.ts +206 -0
  119. package/src/tools/logging.ts +85 -0
  120. package/src/tools/schemas.ts +9 -3
  121. package/src/tools/search.ts +93 -71
  122. package/tests/code/chunker/tree-sitter-chunker.test.ts +13 -1
  123. package/tests/code/indexer.test.ts +12 -0
  124. package/tests/code/integration.test.ts +14 -1
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
9
  import Bottleneck from "bottleneck";
10
10
  import express from "express";
11
+ import logger from "./logger.js";
11
12
  import {
12
13
  DEFAULT_BATCH_SIZE,
13
14
  DEFAULT_CHUNK_OVERLAP,
@@ -47,8 +48,9 @@ const PROMPTS_CONFIG_FILE =
47
48
  // Validate HTTP_PORT when HTTP mode is selected
48
49
  if (TRANSPORT_MODE === "http") {
49
50
  if (Number.isNaN(HTTP_PORT) || HTTP_PORT < 1 || HTTP_PORT > 65535) {
50
- console.error(
51
- `Error: Invalid HTTP_PORT "${process.env.HTTP_PORT}". Must be a number between 1 and 65535.`,
51
+ logger.fatal(
52
+ { port: process.env.HTTP_PORT },
53
+ "Invalid HTTP_PORT. Must be a number between 1 and 65535",
52
54
  );
53
55
  process.exit(1);
54
56
  }
@@ -73,15 +75,17 @@ if (EMBEDDING_PROVIDER !== "ollama") {
73
75
  requiredKeyName = "VOYAGE_API_KEY";
74
76
  break;
75
77
  default:
76
- console.error(
77
- `Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama.`,
78
+ logger.fatal(
79
+ { provider: EMBEDDING_PROVIDER },
80
+ "Unknown embedding provider. Supported providers: openai, cohere, voyage, ollama",
78
81
  );
79
82
  process.exit(1);
80
83
  }
81
84
 
82
85
  if (!apiKey) {
83
- console.error(
84
- `Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`,
86
+ logger.fatal(
87
+ { provider: EMBEDDING_PROVIDER, requiredKey: requiredKeyName },
88
+ `${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider`,
85
89
  );
86
90
  process.exit(1);
87
91
  }
@@ -123,14 +127,14 @@ async function checkOllamaAvailability() {
123
127
  ` ollama pull ${modelName}`;
124
128
  }
125
129
 
126
- console.error(errorMessage);
130
+ logger.fatal({ model: modelName }, errorMessage);
127
131
  process.exit(1);
128
132
  }
129
133
  } catch (error) {
130
134
  const errorMessage =
131
135
  error instanceof Error
132
- ? `Error: ${error.message}`
133
- : `Error: Ollama is not running at ${baseUrl}.\n`;
136
+ ? error.message
137
+ : `Ollama is not running at ${baseUrl}`;
134
138
 
135
139
  let helpText = "";
136
140
  if (isLocalhost) {
@@ -149,7 +153,7 @@ async function checkOllamaAvailability() {
149
153
  ` - The embedding model is available (e.g., nomic-embed-text)`;
150
154
  }
151
155
 
152
- console.error(`${errorMessage}\n${helpText}`);
156
+ logger.fatal({ baseUrl, err: error }, `${errorMessage}\n${helpText}`);
153
157
  process.exit(1);
154
158
  }
155
159
  }
@@ -157,7 +161,17 @@ async function checkOllamaAvailability() {
157
161
 
158
162
  // Initialize clients
159
163
  const qdrant = new QdrantManager(QDRANT_URL, QDRANT_API_KEY);
164
+ logger.info({ url: QDRANT_URL }, "Qdrant client initialized");
165
+
160
166
  const embeddings = EmbeddingProviderFactory.createFromEnv();
167
+ logger.info(
168
+ {
169
+ provider: EMBEDDING_PROVIDER,
170
+ model: embeddings.getModel(),
171
+ dimensions: embeddings.getDimensions(),
172
+ },
173
+ "Embedding provider initialized",
174
+ );
161
175
 
162
176
  // Initialize code indexer
163
177
  const codeConfig: CodeConfig = {
@@ -184,6 +198,7 @@ const codeConfig: CodeConfig = {
184
198
  };
185
199
 
186
200
  const codeIndexer = new CodeIndexer(qdrant, embeddings, codeConfig);
201
+ logger.debug({ codeConfig }, "Code indexer configured");
187
202
 
188
203
  // Initialize git history indexer
189
204
  const gitConfig: GitConfig = {
@@ -223,19 +238,21 @@ const gitConfig: GitConfig = {
223
238
  };
224
239
 
225
240
  const gitHistoryIndexer = new GitHistoryIndexer(qdrant, embeddings, gitConfig);
241
+ logger.debug({ gitConfig }, "Git history indexer configured");
226
242
 
227
243
  // Load prompts configuration if file exists
228
244
  let promptsConfig: PromptsConfig | null = null;
229
245
  if (existsSync(PROMPTS_CONFIG_FILE)) {
230
246
  try {
231
247
  promptsConfig = loadPromptsConfig(PROMPTS_CONFIG_FILE);
232
- console.error(
233
- `Loaded ${promptsConfig.prompts.length} prompts from ${PROMPTS_CONFIG_FILE}`,
248
+ logger.info(
249
+ { count: promptsConfig.prompts.length, file: PROMPTS_CONFIG_FILE },
250
+ "Loaded prompts config",
234
251
  );
235
252
  } catch (error) {
236
- console.error(
237
- `Failed to load prompts configuration from ${PROMPTS_CONFIG_FILE}:`,
238
- error,
253
+ logger.fatal(
254
+ { file: PROMPTS_CONFIG_FILE, err: error },
255
+ "Failed to load prompts configuration",
239
256
  );
240
257
  process.exit(1);
241
258
  }
@@ -265,7 +282,7 @@ function createAndConfigureServer(): McpServer {
265
282
 
266
283
  return server;
267
284
  } catch (error) {
268
- console.error("Failed to configure MCP server:", error);
285
+ logger.error({ err: error }, "Failed to configure MCP server");
269
286
  throw error;
270
287
  }
271
288
  }
@@ -278,7 +295,7 @@ async function startStdioServer() {
278
295
  await checkOllamaAvailability();
279
296
  const transport = new StdioServerTransport();
280
297
  await server.connect(transport);
281
- console.error("Qdrant MCP server running on stdio");
298
+ logger.info("Qdrant MCP server running on stdio");
282
299
  }
283
300
 
284
301
  // Constants for HTTP server configuration
@@ -294,8 +311,9 @@ const SHUTDOWN_GRACE_PERIOD_MS = 10 * 1000; // 10 seconds
294
311
 
295
312
  // Validate REQUEST_TIMEOUT_MS
296
313
  if (Number.isNaN(REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS <= 0) {
297
- console.error(
298
- `Error: Invalid HTTP_REQUEST_TIMEOUT_MS "${process.env.HTTP_REQUEST_TIMEOUT_MS}". Must be a positive integer.`,
314
+ logger.fatal(
315
+ { value: process.env.HTTP_REQUEST_TIMEOUT_MS },
316
+ "Invalid HTTP_REQUEST_TIMEOUT_MS. Must be a positive integer",
299
317
  );
300
318
  process.exit(1);
301
319
  }
@@ -354,7 +372,10 @@ async function startHttpServer() {
354
372
  });
355
373
 
356
374
  if (keysToDelete.length > 0) {
357
- console.error(`Cleaned up ${keysToDelete.length} inactive rate limiters`);
375
+ logger.debug(
376
+ { count: keysToDelete.length },
377
+ "Cleaned up inactive rate limiters",
378
+ );
358
379
  }
359
380
  }, RATE_LIMITER_CLEANUP_INTERVAL_MS);
360
381
 
@@ -377,9 +398,12 @@ async function startHttpServer() {
377
398
  } catch (error) {
378
399
  // Differentiate between rate limit errors and unexpected errors
379
400
  if (error instanceof Bottleneck.BottleneckError) {
380
- console.error(`Rate limit exceeded for IP ${clientIp}:`, error.message);
401
+ logger.warn({ clientIp }, "Rate limit exceeded");
381
402
  } else {
382
- console.error("Unexpected rate limiting error:", error);
403
+ logger.error(
404
+ { clientIp, err: error },
405
+ "Unexpected rate limiting error",
406
+ );
383
407
  }
384
408
  sendErrorResponse(res, -32000, "Too many requests", 429);
385
409
  }
@@ -418,7 +442,7 @@ async function startHttpServer() {
418
442
  const timeoutId = setTimeout(() => {
419
443
  sendErrorResponse(res, -32000, "Request timeout", 504);
420
444
  cleanup().catch((err) => {
421
- console.error("Error during timeout cleanup:", err);
445
+ logger.error({ err }, "Error during timeout cleanup");
422
446
  });
423
447
  }, REQUEST_TIMEOUT_MS);
424
448
 
@@ -435,19 +459,19 @@ async function startHttpServer() {
435
459
  const cleanupHandler = () => {
436
460
  clearTimeout(timeoutId);
437
461
  cleanup().catch((err) => {
438
- console.error("Error during response cleanup:", err);
462
+ logger.error({ err }, "Error during response cleanup");
439
463
  });
440
464
  };
441
465
 
442
466
  res.on("finish", cleanupHandler);
443
467
  res.on("close", cleanupHandler);
444
468
  res.on("error", (err) => {
445
- console.error("Response stream error:", err);
469
+ logger.error({ err }, "Response stream error");
446
470
  cleanupHandler();
447
471
  });
448
472
  } catch (error) {
449
473
  clearTimeout(timeoutId);
450
- console.error("Error handling MCP request:", error);
474
+ logger.error({ err: error }, "Error handling MCP request");
451
475
  sendErrorResponse(res, -32603, "Internal server error");
452
476
  await cleanup();
453
477
  }
@@ -455,12 +479,10 @@ async function startHttpServer() {
455
479
 
456
480
  const httpServer = app
457
481
  .listen(HTTP_PORT, () => {
458
- console.error(
459
- `Qdrant MCP server running on http://localhost:${HTTP_PORT}/mcp`,
460
- );
482
+ logger.info({ port: HTTP_PORT }, "Qdrant MCP server running on HTTP");
461
483
  })
462
484
  .on("error", (error) => {
463
- console.error("HTTP server error:", error);
485
+ logger.fatal({ err: error }, "HTTP server error");
464
486
  process.exit(1);
465
487
  });
466
488
 
@@ -471,22 +493,20 @@ async function startHttpServer() {
471
493
  if (isShuttingDown) return;
472
494
  isShuttingDown = true;
473
495
 
474
- console.error(
475
- "Shutdown signal received, closing HTTP server gracefully...",
476
- );
496
+ logger.info("Shutdown signal received, closing HTTP server gracefully");
477
497
 
478
498
  // Clear the cleanup interval to allow graceful shutdown
479
499
  clearInterval(cleanupIntervalId);
480
500
 
481
501
  // Force shutdown after grace period
482
502
  const forceTimeout = setTimeout(() => {
483
- console.error("Forcing shutdown after timeout");
503
+ logger.warn("Forcing shutdown after timeout");
484
504
  process.exit(1);
485
505
  }, SHUTDOWN_GRACE_PERIOD_MS);
486
506
 
487
507
  httpServer.close(() => {
488
508
  clearTimeout(forceTimeout);
489
- console.error("HTTP server closed");
509
+ logger.info("HTTP server closed");
490
510
  process.exit(0);
491
511
  });
492
512
  };
@@ -502,14 +522,15 @@ async function main() {
502
522
  } else if (TRANSPORT_MODE === "stdio") {
503
523
  await startStdioServer();
504
524
  } else {
505
- console.error(
506
- `Error: Invalid TRANSPORT_MODE "${TRANSPORT_MODE}". Supported modes: stdio, http.`,
525
+ logger.fatal(
526
+ { mode: TRANSPORT_MODE },
527
+ "Invalid TRANSPORT_MODE. Supported modes: stdio, http",
507
528
  );
508
529
  process.exit(1);
509
530
  }
510
531
  }
511
532
 
512
533
  main().catch((error) => {
513
- console.error("Fatal error:", error);
534
+ logger.fatal({ err: error }, "Fatal error");
514
535
  process.exit(1);
515
536
  });
package/src/logger.ts ADDED
@@ -0,0 +1,33 @@
1
+ import pino from "pino";
2
+
3
+ const VALID_LEVELS = [
4
+ "fatal",
5
+ "error",
6
+ "warn",
7
+ "info",
8
+ "debug",
9
+ "trace",
10
+ "silent",
11
+ ];
12
+
13
+ function resolveLogLevel(): string {
14
+ const level = process.env.LOG_LEVEL?.toLowerCase();
15
+ if (!level) return "info";
16
+
17
+ if (VALID_LEVELS.includes(level)) {
18
+ return level;
19
+ }
20
+
21
+ // Write warning directly to stderr since logger isn't initialized yet
22
+ process.stderr.write(
23
+ `WARNING: Invalid LOG_LEVEL "${process.env.LOG_LEVEL}". Valid levels: ${VALID_LEVELS.join(", ")}. Falling back to "info".\n`,
24
+ );
25
+ return "info";
26
+ }
27
+
28
+ const logger = pino(
29
+ { level: resolveLogLevel(), name: "qdrant-mcp" },
30
+ pino.destination(2),
31
+ );
32
+
33
+ export default logger;
@@ -20,6 +20,18 @@ vi.mock("@qdrant/js-client-rest", () => ({
20
20
  }),
21
21
  }));
22
22
 
23
+ vi.mock("../logger.js", () => ({
24
+ default: {
25
+ info: vi.fn(),
26
+ warn: vi.fn(),
27
+ error: vi.fn(),
28
+ debug: vi.fn(),
29
+ fatal: vi.fn(),
30
+ trace: vi.fn(),
31
+ child: vi.fn().mockReturnThis(),
32
+ },
33
+ }));
34
+
23
35
  describe("QdrantManager", () => {
24
36
  let manager: QdrantManager;
25
37
 
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { QdrantClient } from "@qdrant/js-client-rest";
3
+ import logger from "../logger.js";
3
4
 
4
5
  export interface CollectionInfo {
5
6
  name: string;
@@ -21,6 +22,7 @@ export interface SparseVector {
21
22
  }
22
23
 
23
24
  export class QdrantManager {
25
+ private log = logger.child({ component: "qdrant" });
24
26
  private client: QdrantClient;
25
27
 
26
28
  constructor(url: string = "http://localhost:6333", apiKey?: string) {
@@ -54,6 +56,10 @@ export class QdrantManager {
54
56
  distance: "Cosine" | "Euclid" | "Dot" = "Cosine",
55
57
  enableSparse: boolean = false,
56
58
  ): Promise<void> {
59
+ this.log.debug(
60
+ { collection: name, vectorSize, distance, enableSparse },
61
+ "createCollection",
62
+ );
57
63
  const config: any = {};
58
64
 
59
65
  // When hybrid search is enabled, use named vectors
@@ -131,6 +137,7 @@ export class QdrantManager {
131
137
  }
132
138
 
133
139
  async deleteCollection(name: string): Promise<void> {
140
+ this.log.debug({ collection: name }, "deleteCollection");
134
141
  await this.client.deleteCollection(name);
135
142
  }
136
143
 
@@ -142,6 +149,10 @@ export class QdrantManager {
142
149
  payload?: Record<string, any>;
143
150
  }>,
144
151
  ): Promise<void> {
152
+ this.log.debug(
153
+ { collection: collectionName, count: points.length },
154
+ "addPoints",
155
+ );
145
156
  try {
146
157
  // Normalize all IDs to ensure string IDs are in UUID format
147
158
  const normalizedPoints = points.map((point) => ({
@@ -168,6 +179,7 @@ export class QdrantManager {
168
179
  limit: number = 5,
169
180
  filter?: Record<string, any>,
170
181
  ): Promise<SearchResult[]> {
182
+ this.log.debug({ collection: collectionName, limit }, "search");
171
183
  // Convert simple key-value filter to Qdrant filter format
172
184
  // Accepts either:
173
185
  // 1. Simple format: {"category": "database"}
@@ -231,6 +243,10 @@ export class QdrantManager {
231
243
  collectionName: string,
232
244
  ids: (string | number)[],
233
245
  ): Promise<void> {
246
+ this.log.debug(
247
+ { collection: collectionName, count: ids.length },
248
+ "deletePoints",
249
+ );
234
250
  // Normalize IDs to ensure string IDs are in UUID format
235
251
  const normalizedIds = ids.map((id) => this.normalizeId(id));
236
252
 
@@ -248,6 +264,7 @@ export class QdrantManager {
248
264
  collectionName: string,
249
265
  filter: Record<string, any>,
250
266
  ): Promise<void> {
267
+ this.log.debug({ collection: collectionName }, "deletePointsByFilter");
251
268
  await this.client.delete(collectionName, {
252
269
  wait: true,
253
270
  filter: filter,
@@ -266,6 +283,7 @@ export class QdrantManager {
266
283
  filter?: Record<string, any>,
267
284
  _semanticWeight: number = 0.7,
268
285
  ): Promise<SearchResult[]> {
286
+ this.log.debug({ collection: collectionName, limit }, "hybridSearch");
269
287
  // Convert simple key-value filter to Qdrant filter format
270
288
  let qdrantFilter;
271
289
  if (filter && Object.keys(filter).length > 0) {
@@ -334,6 +352,10 @@ export class QdrantManager {
334
352
  payload?: Record<string, any>;
335
353
  }>,
336
354
  ): Promise<void> {
355
+ this.log.debug(
356
+ { collection: collectionName, count: points.length },
357
+ "addPointsWithSparse",
358
+ );
337
359
  try {
338
360
  // Normalize all IDs to ensure string IDs are in UUID format
339
361
  const normalizedPoints = points.map((point) => ({
package/src/tools/code.ts CHANGED
@@ -3,9 +3,13 @@
3
3
  */
4
4
 
5
5
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import logger from "../logger.js";
6
7
  import type { CodeIndexer } from "../code/indexer.js";
8
+ import { withToolLogging } from "./logging.js";
7
9
  import * as schemas from "./schemas.js";
8
10
 
11
+ const log = logger.child({ component: "tools" });
12
+
9
13
  export interface CodeToolDependencies {
10
14
  codeIndexer: CodeIndexer;
11
15
  }
@@ -25,31 +29,48 @@ export function registerCodeTools(
25
29
  "Index a codebase for semantic code search. Automatically discovers files, chunks code intelligently using AST-aware parsing, and stores in vector database. Respects .gitignore and other ignore files.",
26
30
  inputSchema: schemas.IndexCodebaseSchema,
27
31
  },
28
- async ({ path, forceReindex, extensions, ignorePatterns }) => {
29
- const stats = await codeIndexer.indexCodebase(
30
- path,
31
- { forceReindex, extensions, ignorePatterns },
32
- (progress) => {
33
- // Progress callback - could send progress updates via SSE in future
34
- console.error(
35
- `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
36
- );
37
- },
38
- );
39
-
40
- let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
41
-
42
- if (stats.status === "partial") {
43
- statusMessage += `\n\nWarnings:\n${stats.errors?.join("\n")}`;
44
- } else if (stats.status === "failed") {
45
- statusMessage = `Indexing failed:\n${stats.errors?.join("\n")}`;
46
- }
32
+ withToolLogging(
33
+ "index_codebase",
34
+ async ({ path, forceReindex, extensions, ignorePatterns }, extra) => {
35
+ log.info({ tool: "index_codebase", path, forceReindex }, "Tool called");
36
+ const progressToken = extra._meta?.progressToken;
37
+
38
+ const stats = await codeIndexer.indexCodebase(
39
+ path,
40
+ { forceReindex, extensions, ignorePatterns },
41
+ (progress) => {
42
+ log.debug(
43
+ { phase: progress.phase, percentage: progress.percentage },
44
+ progress.message,
45
+ );
46
+ if (progressToken !== undefined) {
47
+ extra.sendNotification({
48
+ method: "notifications/progress",
49
+ params: {
50
+ progressToken,
51
+ progress: progress.percentage,
52
+ total: 100,
53
+ message: `[${progress.phase}] ${progress.message}`,
54
+ },
55
+ });
56
+ }
57
+ },
58
+ );
47
59
 
48
- return {
49
- content: [{ type: "text", text: statusMessage }],
50
- isError: stats.status === "failed",
51
- };
52
- },
60
+ let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
61
+
62
+ if (stats.status === "partial") {
63
+ statusMessage += `\n\nWarnings:\n${stats.errors?.join("\n")}`;
64
+ } else if (stats.status === "failed") {
65
+ statusMessage = `Indexing failed:\n${stats.errors?.join("\n")}`;
66
+ }
67
+
68
+ return {
69
+ content: [{ type: "text", text: statusMessage }],
70
+ isError: stats.status === "failed",
71
+ };
72
+ },
73
+ ),
53
74
  );
54
75
 
55
76
  // search_code
@@ -61,41 +82,48 @@ export function registerCodeTools(
61
82
  "Search indexed codebase using natural language queries. Returns semantically relevant code chunks with file paths and line numbers.",
62
83
  inputSchema: schemas.SearchCodeSchema,
63
84
  },
64
- async ({ path, query, limit, fileTypes, pathPattern }) => {
65
- const results = await codeIndexer.searchCode(path, query, {
66
- limit,
67
- fileTypes,
68
- pathPattern,
69
- });
85
+ withToolLogging(
86
+ "search_code",
87
+ async ({ path, query, limit, fileTypes, pathPattern }) => {
88
+ log.info(
89
+ { tool: "search_code", path, query: query.substring(0, 80) },
90
+ "Tool called",
91
+ );
92
+ const results = await codeIndexer.searchCode(path, query, {
93
+ limit,
94
+ fileTypes,
95
+ pathPattern,
96
+ });
97
+
98
+ if (results.length === 0) {
99
+ return {
100
+ content: [
101
+ { type: "text", text: `No results found for query: "${query}"` },
102
+ ],
103
+ };
104
+ }
105
+
106
+ // Format results with file references
107
+ const formattedResults = results
108
+ .map(
109
+ (r, idx) =>
110
+ `\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
111
+ `File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
112
+ `Language: ${r.language}\n\n` +
113
+ `${r.content}\n`,
114
+ )
115
+ .join("\n");
70
116
 
71
- if (results.length === 0) {
72
117
  return {
73
118
  content: [
74
- { type: "text", text: `No results found for query: "${query}"` },
119
+ {
120
+ type: "text",
121
+ text: `Found ${results.length} result(s):\n${formattedResults}`,
122
+ },
75
123
  ],
76
124
  };
77
- }
78
-
79
- // Format results with file references
80
- const formattedResults = results
81
- .map(
82
- (r, idx) =>
83
- `\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
84
- `File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
85
- `Language: ${r.language}\n\n` +
86
- `${r.content}\n`,
87
- )
88
- .join("\n");
89
-
90
- return {
91
- content: [
92
- {
93
- type: "text",
94
- text: `Found ${results.length} result(s):\n${formattedResults}`,
95
- },
96
- ],
97
- };
98
- },
125
+ },
126
+ ),
99
127
  );
100
128
 
101
129
  // reindex_changes
@@ -107,11 +135,26 @@ export function registerCodeTools(
107
135
  "Incrementally re-index only changed files. Detects added, modified, and deleted files since last index. Requires previous indexing with index_codebase.",
108
136
  inputSchema: schemas.ReindexChangesSchema,
109
137
  },
110
- async ({ path }) => {
138
+ withToolLogging("reindex_changes", async ({ path }, extra) => {
139
+ log.info({ tool: "reindex_changes", path }, "Tool called");
140
+ const progressToken = extra._meta?.progressToken;
141
+
111
142
  const stats = await codeIndexer.reindexChanges(path, (progress) => {
112
- console.error(
113
- `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
143
+ log.debug(
144
+ { phase: progress.phase, percentage: progress.percentage },
145
+ progress.message,
114
146
  );
147
+ if (progressToken !== undefined) {
148
+ extra.sendNotification({
149
+ method: "notifications/progress",
150
+ params: {
151
+ progressToken,
152
+ progress: progress.percentage,
153
+ total: 100,
154
+ message: `[${progress.phase}] ${progress.message}`,
155
+ },
156
+ });
157
+ }
115
158
  });
116
159
 
117
160
  let message = `Incremental re-index complete:\n`;
@@ -132,7 +175,7 @@ export function registerCodeTools(
132
175
  return {
133
176
  content: [{ type: "text", text: message }],
134
177
  };
135
- },
178
+ }),
136
179
  );
137
180
 
138
181
  // get_index_status
@@ -143,7 +186,8 @@ export function registerCodeTools(
143
186
  description: "Get indexing status and statistics for a codebase.",
144
187
  inputSchema: schemas.GetIndexStatusSchema,
145
188
  },
146
- async ({ path }) => {
189
+ withToolLogging("get_index_status", async ({ path }) => {
190
+ log.info({ tool: "get_index_status", path }, "Tool called");
147
191
  const status = await codeIndexer.getIndexStatus(path);
148
192
 
149
193
  if (status.status === "not_indexed") {
@@ -171,7 +215,7 @@ export function registerCodeTools(
171
215
  return {
172
216
  content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
173
217
  };
174
- },
218
+ }),
175
219
  );
176
220
 
177
221
  // clear_index
@@ -183,13 +227,14 @@ export function registerCodeTools(
183
227
  "Delete all indexed data for a codebase. This is irreversible and will remove the entire collection.",
184
228
  inputSchema: schemas.ClearIndexSchema,
185
229
  },
186
- async ({ path }) => {
230
+ withToolLogging("clear_index", async ({ path }) => {
231
+ log.info({ tool: "clear_index", path }, "Tool called");
187
232
  await codeIndexer.clearIndex(path);
188
233
  return {
189
234
  content: [
190
235
  { type: "text", text: `Index cleared for codebase at "${path}".` },
191
236
  ],
192
237
  };
193
- },
238
+ }),
194
239
  );
195
240
  }