@mhalder/qdrant-mcp-server 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +14 -2
- package/README.md +9 -8
- package/build/index.js +34 -832
- package/build/index.js.map +1 -1
- package/build/prompts/register.d.ts +10 -0
- package/build/prompts/register.d.ts.map +1 -0
- package/build/prompts/register.js +50 -0
- package/build/prompts/register.js.map +1 -0
- package/build/resources/index.d.ts +10 -0
- package/build/resources/index.d.ts.map +1 -0
- package/build/resources/index.js +60 -0
- package/build/resources/index.js.map +1 -0
- package/build/tools/code.d.ts +10 -0
- package/build/tools/code.d.ts.map +1 -0
- package/build/tools/code.js +122 -0
- package/build/tools/code.js.map +1 -0
- package/build/tools/collection.d.ts +12 -0
- package/build/tools/collection.d.ts.map +1 -0
- package/build/tools/collection.js +59 -0
- package/build/tools/collection.js.map +1 -0
- package/build/tools/document.d.ts +12 -0
- package/build/tools/document.d.ts.map +1 -0
- package/build/tools/document.js +84 -0
- package/build/tools/document.js.map +1 -0
- package/build/tools/index.d.ts +18 -0
- package/build/tools/index.d.ts.map +1 -0
- package/build/tools/index.js +30 -0
- package/build/tools/index.js.map +1 -0
- package/build/tools/schemas.d.ts +75 -0
- package/build/tools/schemas.d.ts.map +1 -0
- package/build/tools/schemas.js +114 -0
- package/build/tools/schemas.js.map +1 -0
- package/build/tools/search.d.ts +12 -0
- package/build/tools/search.d.ts.map +1 -0
- package/build/tools/search.js +79 -0
- package/build/tools/search.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +38 -984
- package/src/prompts/register.ts +71 -0
- package/src/resources/index.ts +79 -0
- package/src/tools/code.ts +184 -0
- package/src/tools/collection.ts +100 -0
- package/src/tools/document.ts +113 -0
- package/src/tools/index.ts +48 -0
- package/src/tools/schemas.ts +130 -0
- package/src/tools/search.ts +122 -0
package/build/index.js
CHANGED
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
-
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
9
8
|
import Bottleneck from "bottleneck";
|
|
10
9
|
import express from "express";
|
|
11
|
-
import { z } from "zod";
|
|
12
10
|
import { DEFAULT_BATCH_SIZE, DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE, DEFAULT_CODE_EXTENSIONS, DEFAULT_IGNORE_PATTERNS, DEFAULT_SEARCH_LIMIT, } from "./code/config.js";
|
|
13
11
|
import { CodeIndexer } from "./code/indexer.js";
|
|
14
12
|
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { renderTemplate, validateArguments } from "./prompts/template.js";
|
|
13
|
+
import { loadPromptsConfig } from "./prompts/index.js";
|
|
14
|
+
import { registerAllPrompts } from "./prompts/register.js";
|
|
18
15
|
import { QdrantManager } from "./qdrant/client.js";
|
|
16
|
+
import { registerAllResources } from "./resources/index.js";
|
|
17
|
+
import { registerAllTools } from "./tools/index.js";
|
|
19
18
|
// Read package.json for version
|
|
20
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
20
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
@@ -145,833 +144,32 @@ if (existsSync(PROMPTS_CONFIG_FILE)) {
|
|
|
145
144
|
process.exit(1);
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
|
-
// Function to create a new MCP server instance
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
// Only add prompts capability if prompts are configured
|
|
156
|
-
if (promptsConfig) {
|
|
157
|
-
capabilities.prompts = {};
|
|
158
|
-
}
|
|
159
|
-
return new Server({
|
|
160
|
-
name: pkg.name,
|
|
161
|
-
version: pkg.version,
|
|
162
|
-
}, {
|
|
163
|
-
capabilities,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
// Create a shared MCP server for stdio mode
|
|
167
|
-
const server = createServer();
|
|
168
|
-
// Function to register all handlers on a server instance
|
|
169
|
-
function registerHandlers(server) {
|
|
170
|
-
// List available tools
|
|
171
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
172
|
-
return {
|
|
173
|
-
tools: [
|
|
174
|
-
{
|
|
175
|
-
name: "create_collection",
|
|
176
|
-
description: "Create a new vector collection in Qdrant. The collection will be configured with the embedding provider's dimensions automatically. Set enableHybrid to true to enable hybrid search combining semantic and keyword search.",
|
|
177
|
-
inputSchema: {
|
|
178
|
-
type: "object",
|
|
179
|
-
properties: {
|
|
180
|
-
name: {
|
|
181
|
-
type: "string",
|
|
182
|
-
description: "Name of the collection",
|
|
183
|
-
},
|
|
184
|
-
distance: {
|
|
185
|
-
type: "string",
|
|
186
|
-
enum: ["Cosine", "Euclid", "Dot"],
|
|
187
|
-
description: "Distance metric (default: Cosine)",
|
|
188
|
-
},
|
|
189
|
-
enableHybrid: {
|
|
190
|
-
type: "boolean",
|
|
191
|
-
description: "Enable hybrid search with sparse vectors (default: false)",
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
required: ["name"],
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
name: "add_documents",
|
|
199
|
-
description: "Add documents to a collection. Documents will be automatically embedded using the configured embedding provider.",
|
|
200
|
-
inputSchema: {
|
|
201
|
-
type: "object",
|
|
202
|
-
properties: {
|
|
203
|
-
collection: {
|
|
204
|
-
type: "string",
|
|
205
|
-
description: "Name of the collection",
|
|
206
|
-
},
|
|
207
|
-
documents: {
|
|
208
|
-
type: "array",
|
|
209
|
-
description: "Array of documents to add",
|
|
210
|
-
items: {
|
|
211
|
-
type: "object",
|
|
212
|
-
properties: {
|
|
213
|
-
id: {
|
|
214
|
-
type: ["string", "number"],
|
|
215
|
-
description: "Unique identifier for the document",
|
|
216
|
-
},
|
|
217
|
-
text: {
|
|
218
|
-
type: "string",
|
|
219
|
-
description: "Text content to embed and store",
|
|
220
|
-
},
|
|
221
|
-
metadata: {
|
|
222
|
-
type: "object",
|
|
223
|
-
description: "Optional metadata to store with the document",
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
required: ["id", "text"],
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
required: ["collection", "documents"],
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
name: "semantic_search",
|
|
235
|
-
description: "Search for documents using natural language queries. Returns the most semantically similar documents.",
|
|
236
|
-
inputSchema: {
|
|
237
|
-
type: "object",
|
|
238
|
-
properties: {
|
|
239
|
-
collection: {
|
|
240
|
-
type: "string",
|
|
241
|
-
description: "Name of the collection to search",
|
|
242
|
-
},
|
|
243
|
-
query: {
|
|
244
|
-
type: "string",
|
|
245
|
-
description: "Search query text",
|
|
246
|
-
},
|
|
247
|
-
limit: {
|
|
248
|
-
type: "number",
|
|
249
|
-
description: "Maximum number of results (default: 5)",
|
|
250
|
-
},
|
|
251
|
-
filter: {
|
|
252
|
-
type: "object",
|
|
253
|
-
description: "Optional metadata filter",
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
required: ["collection", "query"],
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
name: "list_collections",
|
|
261
|
-
description: "List all available collections in Qdrant.",
|
|
262
|
-
inputSchema: {
|
|
263
|
-
type: "object",
|
|
264
|
-
properties: {},
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
name: "delete_collection",
|
|
269
|
-
description: "Delete a collection and all its documents.",
|
|
270
|
-
inputSchema: {
|
|
271
|
-
type: "object",
|
|
272
|
-
properties: {
|
|
273
|
-
name: {
|
|
274
|
-
type: "string",
|
|
275
|
-
description: "Name of the collection to delete",
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
required: ["name"],
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: "get_collection_info",
|
|
283
|
-
description: "Get detailed information about a collection including vector size, point count, and distance metric.",
|
|
284
|
-
inputSchema: {
|
|
285
|
-
type: "object",
|
|
286
|
-
properties: {
|
|
287
|
-
name: {
|
|
288
|
-
type: "string",
|
|
289
|
-
description: "Name of the collection",
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
required: ["name"],
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
name: "delete_documents",
|
|
297
|
-
description: "Delete specific documents from a collection by their IDs.",
|
|
298
|
-
inputSchema: {
|
|
299
|
-
type: "object",
|
|
300
|
-
properties: {
|
|
301
|
-
collection: {
|
|
302
|
-
type: "string",
|
|
303
|
-
description: "Name of the collection",
|
|
304
|
-
},
|
|
305
|
-
ids: {
|
|
306
|
-
type: "array",
|
|
307
|
-
description: "Array of document IDs to delete",
|
|
308
|
-
items: {
|
|
309
|
-
type: ["string", "number"],
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
required: ["collection", "ids"],
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
name: "hybrid_search",
|
|
318
|
-
description: "Perform hybrid search combining semantic vector search with keyword search using BM25. This provides better results by combining the strengths of both approaches. The collection must be created with enableHybrid set to true.",
|
|
319
|
-
inputSchema: {
|
|
320
|
-
type: "object",
|
|
321
|
-
properties: {
|
|
322
|
-
collection: {
|
|
323
|
-
type: "string",
|
|
324
|
-
description: "Name of the collection to search",
|
|
325
|
-
},
|
|
326
|
-
query: {
|
|
327
|
-
type: "string",
|
|
328
|
-
description: "Search query text",
|
|
329
|
-
},
|
|
330
|
-
limit: {
|
|
331
|
-
type: "number",
|
|
332
|
-
description: "Maximum number of results (default: 5)",
|
|
333
|
-
},
|
|
334
|
-
filter: {
|
|
335
|
-
type: "object",
|
|
336
|
-
description: "Optional metadata filter",
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
required: ["collection", "query"],
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
name: "index_codebase",
|
|
344
|
-
description: "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.",
|
|
345
|
-
inputSchema: {
|
|
346
|
-
type: "object",
|
|
347
|
-
properties: {
|
|
348
|
-
path: {
|
|
349
|
-
type: "string",
|
|
350
|
-
description: "Absolute or relative path to codebase root directory",
|
|
351
|
-
},
|
|
352
|
-
forceReindex: {
|
|
353
|
-
type: "boolean",
|
|
354
|
-
description: "Force full re-index even if already indexed (default: false)",
|
|
355
|
-
},
|
|
356
|
-
extensions: {
|
|
357
|
-
type: "array",
|
|
358
|
-
items: { type: "string" },
|
|
359
|
-
description: "Custom file extensions to index (e.g., ['.proto', '.graphql'])",
|
|
360
|
-
},
|
|
361
|
-
ignorePatterns: {
|
|
362
|
-
type: "array",
|
|
363
|
-
items: { type: "string" },
|
|
364
|
-
description: "Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
required: ["path"],
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: "search_code",
|
|
372
|
-
description: "Search indexed codebase using natural language queries. Returns semantically relevant code chunks with file paths and line numbers.",
|
|
373
|
-
inputSchema: {
|
|
374
|
-
type: "object",
|
|
375
|
-
properties: {
|
|
376
|
-
path: {
|
|
377
|
-
type: "string",
|
|
378
|
-
description: "Path to codebase (must be indexed first)",
|
|
379
|
-
},
|
|
380
|
-
query: {
|
|
381
|
-
type: "string",
|
|
382
|
-
description: "Natural language search query (e.g., 'authentication logic')",
|
|
383
|
-
},
|
|
384
|
-
limit: {
|
|
385
|
-
type: "number",
|
|
386
|
-
description: "Maximum number of results (default: 5, max: 100)",
|
|
387
|
-
},
|
|
388
|
-
fileTypes: {
|
|
389
|
-
type: "array",
|
|
390
|
-
items: { type: "string" },
|
|
391
|
-
description: "Filter by file extensions (e.g., ['.ts', '.py'])",
|
|
392
|
-
},
|
|
393
|
-
pathPattern: {
|
|
394
|
-
type: "string",
|
|
395
|
-
description: "Filter by path glob pattern (e.g., 'src/services/**')",
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
required: ["path", "query"],
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
name: "reindex_changes",
|
|
403
|
-
description: "Incrementally re-index only changed files. Detects added, modified, and deleted files since last index. Requires previous indexing with index_codebase.",
|
|
404
|
-
inputSchema: {
|
|
405
|
-
type: "object",
|
|
406
|
-
properties: {
|
|
407
|
-
path: {
|
|
408
|
-
type: "string",
|
|
409
|
-
description: "Path to codebase",
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
required: ["path"],
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
name: "get_index_status",
|
|
417
|
-
description: "Get indexing status and statistics for a codebase.",
|
|
418
|
-
inputSchema: {
|
|
419
|
-
type: "object",
|
|
420
|
-
properties: {
|
|
421
|
-
path: {
|
|
422
|
-
type: "string",
|
|
423
|
-
description: "Path to codebase",
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
required: ["path"],
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
name: "clear_index",
|
|
431
|
-
description: "Delete all indexed data for a codebase. This is irreversible and will remove the entire collection.",
|
|
432
|
-
inputSchema: {
|
|
433
|
-
type: "object",
|
|
434
|
-
properties: {
|
|
435
|
-
path: {
|
|
436
|
-
type: "string",
|
|
437
|
-
description: "Path to codebase",
|
|
438
|
-
},
|
|
439
|
-
},
|
|
440
|
-
required: ["path"],
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
],
|
|
444
|
-
};
|
|
445
|
-
});
|
|
446
|
-
// Handle tool calls
|
|
447
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
448
|
-
const { name, arguments: args } = request.params;
|
|
449
|
-
try {
|
|
450
|
-
switch (name) {
|
|
451
|
-
case "create_collection": {
|
|
452
|
-
const { name, distance, enableHybrid } = CreateCollectionSchema.parse(args);
|
|
453
|
-
const vectorSize = embeddings.getDimensions();
|
|
454
|
-
await qdrant.createCollection(name, vectorSize, distance, enableHybrid || false);
|
|
455
|
-
let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
|
|
456
|
-
if (enableHybrid) {
|
|
457
|
-
message += " Hybrid search is enabled for this collection.";
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
content: [
|
|
461
|
-
{
|
|
462
|
-
type: "text",
|
|
463
|
-
text: message,
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
case "add_documents": {
|
|
469
|
-
const { collection, documents } = AddDocumentsSchema.parse(args);
|
|
470
|
-
// Check if collection exists and get info
|
|
471
|
-
const exists = await qdrant.collectionExists(collection);
|
|
472
|
-
if (!exists) {
|
|
473
|
-
return {
|
|
474
|
-
content: [
|
|
475
|
-
{
|
|
476
|
-
type: "text",
|
|
477
|
-
text: `Error: Collection "${collection}" does not exist. Create it first using create_collection.`,
|
|
478
|
-
},
|
|
479
|
-
],
|
|
480
|
-
isError: true,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
484
|
-
// Generate embeddings for all documents
|
|
485
|
-
const texts = documents.map((doc) => doc.text);
|
|
486
|
-
const embeddingResults = await embeddings.embedBatch(texts);
|
|
487
|
-
// If hybrid search is enabled, generate sparse vectors and use appropriate method
|
|
488
|
-
if (collectionInfo.hybridEnabled) {
|
|
489
|
-
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
490
|
-
// Prepare points with both dense and sparse vectors
|
|
491
|
-
const points = documents.map((doc, index) => ({
|
|
492
|
-
id: doc.id,
|
|
493
|
-
vector: embeddingResults[index].embedding,
|
|
494
|
-
sparseVector: sparseGenerator.generate(doc.text),
|
|
495
|
-
payload: {
|
|
496
|
-
text: doc.text,
|
|
497
|
-
...doc.metadata,
|
|
498
|
-
},
|
|
499
|
-
}));
|
|
500
|
-
await qdrant.addPointsWithSparse(collection, points);
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
// Standard dense-only vectors
|
|
504
|
-
const points = documents.map((doc, index) => ({
|
|
505
|
-
id: doc.id,
|
|
506
|
-
vector: embeddingResults[index].embedding,
|
|
507
|
-
payload: {
|
|
508
|
-
text: doc.text,
|
|
509
|
-
...doc.metadata,
|
|
510
|
-
},
|
|
511
|
-
}));
|
|
512
|
-
await qdrant.addPoints(collection, points);
|
|
513
|
-
}
|
|
514
|
-
return {
|
|
515
|
-
content: [
|
|
516
|
-
{
|
|
517
|
-
type: "text",
|
|
518
|
-
text: `Successfully added ${documents.length} document(s) to collection "${collection}".`,
|
|
519
|
-
},
|
|
520
|
-
],
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
case "semantic_search": {
|
|
524
|
-
const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
|
|
525
|
-
// Check if collection exists
|
|
526
|
-
const exists = await qdrant.collectionExists(collection);
|
|
527
|
-
if (!exists) {
|
|
528
|
-
return {
|
|
529
|
-
content: [
|
|
530
|
-
{
|
|
531
|
-
type: "text",
|
|
532
|
-
text: `Error: Collection "${collection}" does not exist.`,
|
|
533
|
-
},
|
|
534
|
-
],
|
|
535
|
-
isError: true,
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
// Generate embedding for query
|
|
539
|
-
const { embedding } = await embeddings.embed(query);
|
|
540
|
-
// Search
|
|
541
|
-
const results = await qdrant.search(collection, embedding, limit || 5, filter);
|
|
542
|
-
return {
|
|
543
|
-
content: [
|
|
544
|
-
{
|
|
545
|
-
type: "text",
|
|
546
|
-
text: JSON.stringify(results, null, 2),
|
|
547
|
-
},
|
|
548
|
-
],
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
case "list_collections": {
|
|
552
|
-
const collections = await qdrant.listCollections();
|
|
553
|
-
return {
|
|
554
|
-
content: [
|
|
555
|
-
{
|
|
556
|
-
type: "text",
|
|
557
|
-
text: JSON.stringify(collections, null, 2),
|
|
558
|
-
},
|
|
559
|
-
],
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
case "delete_collection": {
|
|
563
|
-
const { name } = DeleteCollectionSchema.parse(args);
|
|
564
|
-
await qdrant.deleteCollection(name);
|
|
565
|
-
return {
|
|
566
|
-
content: [
|
|
567
|
-
{
|
|
568
|
-
type: "text",
|
|
569
|
-
text: `Collection "${name}" deleted successfully.`,
|
|
570
|
-
},
|
|
571
|
-
],
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
case "get_collection_info": {
|
|
575
|
-
const { name } = GetCollectionInfoSchema.parse(args);
|
|
576
|
-
const info = await qdrant.getCollectionInfo(name);
|
|
577
|
-
return {
|
|
578
|
-
content: [
|
|
579
|
-
{
|
|
580
|
-
type: "text",
|
|
581
|
-
text: JSON.stringify(info, null, 2),
|
|
582
|
-
},
|
|
583
|
-
],
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
case "delete_documents": {
|
|
587
|
-
const { collection, ids } = DeleteDocumentsSchema.parse(args);
|
|
588
|
-
await qdrant.deletePoints(collection, ids);
|
|
589
|
-
return {
|
|
590
|
-
content: [
|
|
591
|
-
{
|
|
592
|
-
type: "text",
|
|
593
|
-
text: `Successfully deleted ${ids.length} document(s) from collection "${collection}".`,
|
|
594
|
-
},
|
|
595
|
-
],
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
case "hybrid_search": {
|
|
599
|
-
const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
|
|
600
|
-
// Check if collection exists
|
|
601
|
-
const exists = await qdrant.collectionExists(collection);
|
|
602
|
-
if (!exists) {
|
|
603
|
-
return {
|
|
604
|
-
content: [
|
|
605
|
-
{
|
|
606
|
-
type: "text",
|
|
607
|
-
text: `Error: Collection "${collection}" does not exist.`,
|
|
608
|
-
},
|
|
609
|
-
],
|
|
610
|
-
isError: true,
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
// Check if collection has hybrid search enabled
|
|
614
|
-
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
615
|
-
if (!collectionInfo.hybridEnabled) {
|
|
616
|
-
return {
|
|
617
|
-
content: [
|
|
618
|
-
{
|
|
619
|
-
type: "text",
|
|
620
|
-
text: `Error: Collection "${collection}" does not have hybrid search enabled. Create a new collection with enableHybrid set to true.`,
|
|
621
|
-
},
|
|
622
|
-
],
|
|
623
|
-
isError: true,
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
// Generate dense embedding for query
|
|
627
|
-
const { embedding } = await embeddings.embed(query);
|
|
628
|
-
// Generate sparse vector for query
|
|
629
|
-
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
630
|
-
const sparseVector = sparseGenerator.generate(query);
|
|
631
|
-
// Perform hybrid search
|
|
632
|
-
const results = await qdrant.hybridSearch(collection, embedding, sparseVector, limit || 5, filter);
|
|
633
|
-
return {
|
|
634
|
-
content: [
|
|
635
|
-
{
|
|
636
|
-
type: "text",
|
|
637
|
-
text: JSON.stringify(results, null, 2),
|
|
638
|
-
},
|
|
639
|
-
],
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
case "index_codebase": {
|
|
643
|
-
const IndexCodebaseSchema = z.object({
|
|
644
|
-
path: z.string(),
|
|
645
|
-
forceReindex: z.boolean().optional(),
|
|
646
|
-
extensions: z.array(z.string()).optional(),
|
|
647
|
-
ignorePatterns: z.array(z.string()).optional(),
|
|
648
|
-
});
|
|
649
|
-
const { path, forceReindex, extensions, ignorePatterns } = IndexCodebaseSchema.parse(args);
|
|
650
|
-
const stats = await codeIndexer.indexCodebase(path, { forceReindex, extensions, ignorePatterns }, (progress) => {
|
|
651
|
-
// Progress callback - could send progress updates via SSE in future
|
|
652
|
-
console.error(`[${progress.phase}] ${progress.percentage}% - ${progress.message}`);
|
|
653
|
-
});
|
|
654
|
-
let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
|
|
655
|
-
if (stats.status === "partial") {
|
|
656
|
-
statusMessage += `\n\nWarnings:\n${stats.errors?.join("\n")}`;
|
|
657
|
-
}
|
|
658
|
-
else if (stats.status === "failed") {
|
|
659
|
-
statusMessage = `Indexing failed:\n${stats.errors?.join("\n")}`;
|
|
660
|
-
}
|
|
661
|
-
return {
|
|
662
|
-
content: [
|
|
663
|
-
{
|
|
664
|
-
type: "text",
|
|
665
|
-
text: statusMessage,
|
|
666
|
-
},
|
|
667
|
-
],
|
|
668
|
-
isError: stats.status === "failed",
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
case "search_code": {
|
|
672
|
-
const SearchCodeSchema = z.object({
|
|
673
|
-
path: z.string(),
|
|
674
|
-
query: z.string(),
|
|
675
|
-
limit: z.number().optional(),
|
|
676
|
-
fileTypes: z.array(z.string()).optional(),
|
|
677
|
-
pathPattern: z.string().optional(),
|
|
678
|
-
});
|
|
679
|
-
const { path, query, limit, fileTypes, pathPattern } = SearchCodeSchema.parse(args);
|
|
680
|
-
const results = await codeIndexer.searchCode(path, query, {
|
|
681
|
-
limit,
|
|
682
|
-
fileTypes,
|
|
683
|
-
pathPattern,
|
|
684
|
-
});
|
|
685
|
-
if (results.length === 0) {
|
|
686
|
-
return {
|
|
687
|
-
content: [
|
|
688
|
-
{
|
|
689
|
-
type: "text",
|
|
690
|
-
text: `No results found for query: "${query}"`,
|
|
691
|
-
},
|
|
692
|
-
],
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
// Format results with file references
|
|
696
|
-
const formattedResults = results
|
|
697
|
-
.map((r, idx) => `\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
|
|
698
|
-
`File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
|
|
699
|
-
`Language: ${r.language}\n\n` +
|
|
700
|
-
`${r.content}\n`)
|
|
701
|
-
.join("\n");
|
|
702
|
-
return {
|
|
703
|
-
content: [
|
|
704
|
-
{
|
|
705
|
-
type: "text",
|
|
706
|
-
text: `Found ${results.length} result(s):\n${formattedResults}`,
|
|
707
|
-
},
|
|
708
|
-
],
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
case "get_index_status": {
|
|
712
|
-
const GetIndexStatusSchema = z.object({
|
|
713
|
-
path: z.string(),
|
|
714
|
-
});
|
|
715
|
-
const { path } = GetIndexStatusSchema.parse(args);
|
|
716
|
-
const status = await codeIndexer.getIndexStatus(path);
|
|
717
|
-
if (!status.isIndexed) {
|
|
718
|
-
return {
|
|
719
|
-
content: [
|
|
720
|
-
{
|
|
721
|
-
type: "text",
|
|
722
|
-
text: `Codebase at "${path}" is not indexed. Use index_codebase to index it first.`,
|
|
723
|
-
},
|
|
724
|
-
],
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
return {
|
|
728
|
-
content: [
|
|
729
|
-
{
|
|
730
|
-
type: "text",
|
|
731
|
-
text: JSON.stringify(status, null, 2),
|
|
732
|
-
},
|
|
733
|
-
],
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
case "reindex_changes": {
|
|
737
|
-
const ReindexChangesSchema = z.object({
|
|
738
|
-
path: z.string(),
|
|
739
|
-
});
|
|
740
|
-
const { path } = ReindexChangesSchema.parse(args);
|
|
741
|
-
const stats = await codeIndexer.reindexChanges(path, (progress) => {
|
|
742
|
-
console.error(`[${progress.phase}] ${progress.percentage}% - ${progress.message}`);
|
|
743
|
-
});
|
|
744
|
-
let message = `Incremental re-index complete:\n`;
|
|
745
|
-
message += `- Files added: ${stats.filesAdded}\n`;
|
|
746
|
-
message += `- Files modified: ${stats.filesModified}\n`;
|
|
747
|
-
message += `- Files deleted: ${stats.filesDeleted}\n`;
|
|
748
|
-
message += `- Chunks added: ${stats.chunksAdded}\n`;
|
|
749
|
-
message += `- Duration: ${(stats.durationMs / 1000).toFixed(1)}s`;
|
|
750
|
-
if (stats.filesAdded === 0 &&
|
|
751
|
-
stats.filesModified === 0 &&
|
|
752
|
-
stats.filesDeleted === 0) {
|
|
753
|
-
message = `No changes detected. Codebase is up to date.`;
|
|
754
|
-
}
|
|
755
|
-
return {
|
|
756
|
-
content: [
|
|
757
|
-
{
|
|
758
|
-
type: "text",
|
|
759
|
-
text: message,
|
|
760
|
-
},
|
|
761
|
-
],
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
case "clear_index": {
|
|
765
|
-
const ClearIndexSchema = z.object({
|
|
766
|
-
path: z.string(),
|
|
767
|
-
});
|
|
768
|
-
const { path } = ClearIndexSchema.parse(args);
|
|
769
|
-
await codeIndexer.clearIndex(path);
|
|
770
|
-
return {
|
|
771
|
-
content: [
|
|
772
|
-
{
|
|
773
|
-
type: "text",
|
|
774
|
-
text: `Index cleared for codebase at "${path}".`,
|
|
775
|
-
},
|
|
776
|
-
],
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
default:
|
|
780
|
-
return {
|
|
781
|
-
content: [
|
|
782
|
-
{
|
|
783
|
-
type: "text",
|
|
784
|
-
text: `Unknown tool: ${name}`,
|
|
785
|
-
},
|
|
786
|
-
],
|
|
787
|
-
isError: true,
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
catch (error) {
|
|
792
|
-
// Enhanced error details for debugging
|
|
793
|
-
const errorDetails = error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
794
|
-
console.error("Tool execution error:", {
|
|
795
|
-
tool: name,
|
|
796
|
-
error: errorDetails,
|
|
797
|
-
stack: error?.stack,
|
|
798
|
-
data: error?.data,
|
|
799
|
-
});
|
|
800
|
-
return {
|
|
801
|
-
content: [
|
|
802
|
-
{
|
|
803
|
-
type: "text",
|
|
804
|
-
text: `Error: ${errorDetails}`,
|
|
805
|
-
},
|
|
806
|
-
],
|
|
807
|
-
isError: true,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
// List available resources
|
|
812
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
813
|
-
const collections = await qdrant.listCollections();
|
|
814
|
-
return {
|
|
815
|
-
resources: [
|
|
816
|
-
{
|
|
817
|
-
uri: "qdrant://collections",
|
|
818
|
-
name: "All Collections",
|
|
819
|
-
description: "List of all vector collections in Qdrant",
|
|
820
|
-
mimeType: "application/json",
|
|
821
|
-
},
|
|
822
|
-
...collections.map((name) => ({
|
|
823
|
-
uri: `qdrant://collection/${name}`,
|
|
824
|
-
name: `Collection: ${name}`,
|
|
825
|
-
description: `Details and statistics for collection "${name}"`,
|
|
826
|
-
mimeType: "application/json",
|
|
827
|
-
})),
|
|
828
|
-
],
|
|
829
|
-
};
|
|
830
|
-
});
|
|
831
|
-
// Read resource content
|
|
832
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
833
|
-
const { uri } = request.params;
|
|
834
|
-
if (uri === "qdrant://collections") {
|
|
835
|
-
const collections = await qdrant.listCollections();
|
|
836
|
-
return {
|
|
837
|
-
contents: [
|
|
838
|
-
{
|
|
839
|
-
uri,
|
|
840
|
-
mimeType: "application/json",
|
|
841
|
-
text: JSON.stringify(collections, null, 2),
|
|
842
|
-
},
|
|
843
|
-
],
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
const collectionMatch = uri.match(/^qdrant:\/\/collection\/(.+)$/);
|
|
847
|
-
if (collectionMatch) {
|
|
848
|
-
const name = collectionMatch[1];
|
|
849
|
-
const info = await qdrant.getCollectionInfo(name);
|
|
850
|
-
return {
|
|
851
|
-
contents: [
|
|
852
|
-
{
|
|
853
|
-
uri,
|
|
854
|
-
mimeType: "application/json",
|
|
855
|
-
text: JSON.stringify(info, null, 2),
|
|
856
|
-
},
|
|
857
|
-
],
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
return {
|
|
861
|
-
contents: [
|
|
862
|
-
{
|
|
863
|
-
uri,
|
|
864
|
-
mimeType: "text/plain",
|
|
865
|
-
text: `Unknown resource: ${uri}`,
|
|
866
|
-
},
|
|
867
|
-
],
|
|
868
|
-
};
|
|
869
|
-
});
|
|
870
|
-
// List available prompts
|
|
871
|
-
if (promptsConfig) {
|
|
872
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
873
|
-
const prompts = listPrompts(promptsConfig);
|
|
874
|
-
return {
|
|
875
|
-
prompts: prompts.map((prompt) => ({
|
|
876
|
-
name: prompt.name,
|
|
877
|
-
description: prompt.description,
|
|
878
|
-
arguments: prompt.arguments.map((arg) => ({
|
|
879
|
-
name: arg.name,
|
|
880
|
-
description: arg.description,
|
|
881
|
-
required: arg.required,
|
|
882
|
-
})),
|
|
883
|
-
})),
|
|
884
|
-
};
|
|
147
|
+
// Function to create and configure a new MCP server instance
|
|
148
|
+
function createAndConfigureServer() {
|
|
149
|
+
try {
|
|
150
|
+
const server = new McpServer({
|
|
151
|
+
name: pkg.name,
|
|
152
|
+
version: pkg.version,
|
|
885
153
|
});
|
|
886
|
-
//
|
|
887
|
-
server
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
throw new Error(`Unknown prompt: ${name}`);
|
|
892
|
-
}
|
|
893
|
-
try {
|
|
894
|
-
// Validate arguments
|
|
895
|
-
validateArguments(args || {}, prompt.arguments);
|
|
896
|
-
// Render template
|
|
897
|
-
const rendered = renderTemplate(prompt.template, args || {}, prompt.arguments);
|
|
898
|
-
return {
|
|
899
|
-
messages: [
|
|
900
|
-
{
|
|
901
|
-
role: "user",
|
|
902
|
-
content: {
|
|
903
|
-
type: "text",
|
|
904
|
-
text: rendered.text,
|
|
905
|
-
},
|
|
906
|
-
},
|
|
907
|
-
],
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
catch (error) {
|
|
911
|
-
throw new Error(`Failed to render prompt "${name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
912
|
-
}
|
|
154
|
+
// Register all tools
|
|
155
|
+
registerAllTools(server, {
|
|
156
|
+
qdrant,
|
|
157
|
+
embeddings,
|
|
158
|
+
codeIndexer,
|
|
913
159
|
});
|
|
160
|
+
// Register all resources
|
|
161
|
+
registerAllResources(server, qdrant);
|
|
162
|
+
// Register all prompts (if configured)
|
|
163
|
+
registerAllPrompts(server, promptsConfig);
|
|
164
|
+
return server;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error("Failed to configure MCP server:", error);
|
|
168
|
+
throw error;
|
|
914
169
|
}
|
|
915
170
|
}
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
// Tool schemas
|
|
919
|
-
const CreateCollectionSchema = z.object({
|
|
920
|
-
name: z.string().describe("Name of the collection"),
|
|
921
|
-
distance: z
|
|
922
|
-
.enum(["Cosine", "Euclid", "Dot"])
|
|
923
|
-
.optional()
|
|
924
|
-
.describe("Distance metric (default: Cosine)"),
|
|
925
|
-
enableHybrid: z
|
|
926
|
-
.boolean()
|
|
927
|
-
.optional()
|
|
928
|
-
.describe("Enable hybrid search with sparse vectors (default: false)"),
|
|
929
|
-
});
|
|
930
|
-
const AddDocumentsSchema = z.object({
|
|
931
|
-
collection: z.string().describe("Name of the collection"),
|
|
932
|
-
documents: z
|
|
933
|
-
.array(z.object({
|
|
934
|
-
id: z
|
|
935
|
-
.union([z.string(), z.number()])
|
|
936
|
-
.describe("Unique identifier for the document"),
|
|
937
|
-
text: z.string().describe("Text content to embed and store"),
|
|
938
|
-
metadata: z
|
|
939
|
-
.record(z.any())
|
|
940
|
-
.optional()
|
|
941
|
-
.describe("Optional metadata to store with the document"),
|
|
942
|
-
}))
|
|
943
|
-
.describe("Array of documents to add"),
|
|
944
|
-
});
|
|
945
|
-
const SemanticSearchSchema = z.object({
|
|
946
|
-
collection: z.string().describe("Name of the collection to search"),
|
|
947
|
-
query: z.string().describe("Search query text"),
|
|
948
|
-
limit: z
|
|
949
|
-
.number()
|
|
950
|
-
.optional()
|
|
951
|
-
.describe("Maximum number of results (default: 5)"),
|
|
952
|
-
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
953
|
-
});
|
|
954
|
-
const DeleteCollectionSchema = z.object({
|
|
955
|
-
name: z.string().describe("Name of the collection to delete"),
|
|
956
|
-
});
|
|
957
|
-
const GetCollectionInfoSchema = z.object({
|
|
958
|
-
name: z.string().describe("Name of the collection"),
|
|
959
|
-
});
|
|
960
|
-
const DeleteDocumentsSchema = z.object({
|
|
961
|
-
collection: z.string().describe("Name of the collection"),
|
|
962
|
-
ids: z
|
|
963
|
-
.array(z.union([z.string(), z.number()]))
|
|
964
|
-
.describe("Array of document IDs to delete"),
|
|
965
|
-
});
|
|
966
|
-
const HybridSearchSchema = z.object({
|
|
967
|
-
collection: z.string().describe("Name of the collection to search"),
|
|
968
|
-
query: z.string().describe("Search query text"),
|
|
969
|
-
limit: z
|
|
970
|
-
.number()
|
|
971
|
-
.optional()
|
|
972
|
-
.describe("Maximum number of results (default: 5)"),
|
|
973
|
-
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
974
|
-
});
|
|
171
|
+
// Create a shared MCP server for stdio mode
|
|
172
|
+
const server = createAndConfigureServer();
|
|
975
173
|
// Start server with stdio transport
|
|
976
174
|
async function startStdioServer() {
|
|
977
175
|
await checkOllamaAvailability();
|
|
@@ -984,8 +182,13 @@ const RATE_LIMIT_MAX_REQUESTS = 100; // Max requests per window
|
|
|
984
182
|
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
985
183
|
const RATE_LIMIT_MAX_CONCURRENT = 10; // Max concurrent requests per IP
|
|
986
184
|
const RATE_LIMITER_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
987
|
-
const REQUEST_TIMEOUT_MS =
|
|
185
|
+
const REQUEST_TIMEOUT_MS = parseInt(process.env.HTTP_REQUEST_TIMEOUT_MS || "300000", 10);
|
|
988
186
|
const SHUTDOWN_GRACE_PERIOD_MS = 10 * 1000; // 10 seconds
|
|
187
|
+
// Validate REQUEST_TIMEOUT_MS
|
|
188
|
+
if (Number.isNaN(REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS <= 0) {
|
|
189
|
+
console.error(`Error: Invalid HTTP_REQUEST_TIMEOUT_MS "${process.env.HTTP_REQUEST_TIMEOUT_MS}". Must be a positive integer.`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
989
192
|
// Start server with HTTP transport
|
|
990
193
|
async function startHttpServer() {
|
|
991
194
|
await checkOllamaAvailability();
|
|
@@ -1062,8 +265,7 @@ async function startHttpServer() {
|
|
|
1062
265
|
});
|
|
1063
266
|
app.post("/mcp", rateLimitMiddleware, async (req, res) => {
|
|
1064
267
|
// Create a new server for each request
|
|
1065
|
-
const requestServer =
|
|
1066
|
-
registerHandlers(requestServer);
|
|
268
|
+
const requestServer = createAndConfigureServer();
|
|
1067
269
|
// Create transport with enableJsonResponse
|
|
1068
270
|
const transport = new StreamableHTTPServerTransport({
|
|
1069
271
|
sessionIdGenerator: undefined,
|