@mhalder/qdrant-mcp-server 3.2.0 → 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.
- package/.dagger/.gitattributes +1 -0
- package/.dagger/package.json +6 -0
- package/.dagger/src/index.ts +83 -0
- package/.dagger/tsconfig.json +13 -0
- package/.dagger/yarn.lock +8 -0
- package/.github/workflows/ci.yml +17 -27
- package/.github/workflows/release.yml +16 -19
- package/CHANGELOG.md +13 -0
- package/README.md +11 -9
- package/build/code/chunker/tree-sitter-chunker.d.ts.map +1 -1
- package/build/code/chunker/tree-sitter-chunker.js +15 -3
- package/build/code/chunker/tree-sitter-chunker.js.map +1 -1
- package/build/code/indexer.d.ts +1 -0
- package/build/code/indexer.d.ts.map +1 -1
- package/build/code/indexer.js +24 -4
- package/build/code/indexer.js.map +1 -1
- package/build/embeddings/cohere.d.ts +1 -0
- package/build/embeddings/cohere.d.ts.map +1 -1
- package/build/embeddings/cohere.js +8 -1
- package/build/embeddings/cohere.js.map +1 -1
- package/build/embeddings/cohere.test.js +11 -0
- package/build/embeddings/cohere.test.js.map +1 -1
- package/build/embeddings/factory.d.ts.map +1 -1
- package/build/embeddings/factory.js +2 -0
- package/build/embeddings/factory.js.map +1 -1
- package/build/embeddings/factory.test.js +12 -1
- package/build/embeddings/factory.test.js.map +1 -1
- package/build/embeddings/ollama.d.ts +1 -0
- package/build/embeddings/ollama.d.ts.map +1 -1
- package/build/embeddings/ollama.js +8 -1
- package/build/embeddings/ollama.js.map +1 -1
- package/build/embeddings/ollama.test.js +11 -0
- package/build/embeddings/ollama.test.js.map +1 -1
- package/build/embeddings/openai.d.ts +1 -0
- package/build/embeddings/openai.d.ts.map +1 -1
- package/build/embeddings/openai.js +8 -1
- package/build/embeddings/openai.js.map +1 -1
- package/build/embeddings/openai.test.js +11 -0
- package/build/embeddings/openai.test.js.map +1 -1
- package/build/embeddings/voyage.d.ts +1 -0
- package/build/embeddings/voyage.d.ts.map +1 -1
- package/build/embeddings/voyage.js +8 -1
- package/build/embeddings/voyage.js.map +1 -1
- package/build/embeddings/voyage.test.js +11 -0
- package/build/embeddings/voyage.test.js.map +1 -1
- package/build/git/indexer.d.ts +1 -0
- package/build/git/indexer.d.ts.map +1 -1
- package/build/git/indexer.js +16 -3
- package/build/git/indexer.js.map +1 -1
- package/build/git/indexer.test.js +15 -9
- package/build/git/indexer.test.js.map +1 -1
- package/build/index.js +35 -26
- package/build/index.js.map +1 -1
- package/build/index.test.js +105 -91
- package/build/index.test.js.map +1 -1
- package/build/logger.d.ts +4 -0
- package/build/logger.d.ts.map +1 -0
- package/build/logger.js +24 -0
- package/build/logger.js.map +1 -0
- package/build/qdrant/client.d.ts +1 -0
- package/build/qdrant/client.d.ts.map +1 -1
- package/build/qdrant/client.js +10 -0
- package/build/qdrant/client.js.map +1 -1
- package/build/qdrant/client.test.js +11 -0
- package/build/qdrant/client.test.js.map +1 -1
- package/build/tools/code.d.ts.map +1 -1
- package/build/tools/code.js +44 -13
- package/build/tools/code.js.map +1 -1
- package/build/tools/collection.d.ts.map +1 -1
- package/build/tools/collection.js +15 -8
- package/build/tools/collection.js.map +1 -1
- package/build/tools/document.d.ts.map +1 -1
- package/build/tools/document.js +9 -4
- package/build/tools/document.js.map +1 -1
- package/build/tools/federated.d.ts.map +1 -1
- package/build/tools/federated.js +9 -4
- package/build/tools/federated.js.map +1 -1
- package/build/tools/federated.test.js +11 -0
- package/build/tools/federated.test.js.map +1 -1
- package/build/tools/git-history.d.ts.map +1 -1
- package/build/tools/git-history.js +44 -12
- package/build/tools/git-history.js.map +1 -1
- package/build/tools/logging.d.ts +16 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +68 -0
- package/build/tools/logging.js.map +1 -0
- package/build/tools/logging.test.d.ts +2 -0
- package/build/tools/logging.test.d.ts.map +1 -0
- package/build/tools/logging.test.js +139 -0
- package/build/tools/logging.test.js.map +1 -0
- package/build/tools/schemas.d.ts +32 -19
- package/build/tools/schemas.d.ts.map +1 -1
- package/build/tools/schemas.js +9 -3
- package/build/tools/schemas.js.map +1 -1
- package/build/tools/search.d.ts.map +1 -1
- package/build/tools/search.js +13 -4
- package/build/tools/search.js.map +1 -1
- package/dagger.json +8 -0
- package/mise.toml +2 -0
- package/package.json +14 -13
- package/src/code/chunker/tree-sitter-chunker.ts +41 -9
- package/src/code/indexer.ts +41 -6
- package/src/embeddings/cohere.test.ts +12 -0
- package/src/embeddings/cohere.ts +10 -2
- package/src/embeddings/factory.test.ts +13 -1
- package/src/embeddings/factory.ts +3 -0
- package/src/embeddings/ollama.test.ts +12 -0
- package/src/embeddings/ollama.ts +10 -2
- package/src/embeddings/openai.test.ts +12 -0
- package/src/embeddings/openai.ts +10 -2
- package/src/embeddings/voyage.test.ts +12 -0
- package/src/embeddings/voyage.ts +10 -2
- package/src/git/indexer.test.ts +22 -16
- package/src/git/indexer.ts +30 -4
- package/src/index.test.ts +128 -106
- package/src/index.ts +59 -38
- package/src/logger.ts +33 -0
- package/src/qdrant/client.test.ts +12 -0
- package/src/qdrant/client.ts +22 -0
- package/src/tools/code.ts +107 -62
- package/src/tools/collection.ts +39 -22
- package/src/tools/document.ts +52 -22
- package/src/tools/federated.test.ts +12 -0
- package/src/tools/federated.ts +143 -125
- package/src/tools/git-history.ts +117 -60
- package/src/tools/logging.test.ts +206 -0
- package/src/tools/logging.ts +85 -0
- package/src/tools/schemas.ts +9 -3
- package/src/tools/search.ts +93 -71
- package/tests/code/chunker/tree-sitter-chunker.test.ts +13 -1
- package/tests/code/indexer.test.ts +12 -0
- 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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
133
|
-
: `
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
248
|
+
logger.info(
|
|
249
|
+
{ count: promptsConfig.prompts.length, file: PROMPTS_CONFIG_FILE },
|
|
250
|
+
"Loaded prompts config",
|
|
234
251
|
);
|
|
235
252
|
} catch (error) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
+
logger.warn({ clientIp }, "Rate limit exceeded");
|
|
381
402
|
} else {
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
+
logger.error({ err }, "Response stream error");
|
|
446
470
|
cleanupHandler();
|
|
447
471
|
});
|
|
448
472
|
} catch (error) {
|
|
449
473
|
clearTimeout(timeoutId);
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
|
package/src/qdrant/client.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
113
|
-
|
|
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
|
}
|