@mhalder/qdrant-mcp-server 2.1.0 → 2.1.2

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 (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +14 -2
  3. package/README.md +3 -2
  4. package/build/code/indexer.d.ts +5 -0
  5. package/build/code/indexer.d.ts.map +1 -1
  6. package/build/code/indexer.js +116 -14
  7. package/build/code/indexer.js.map +1 -1
  8. package/build/code/types.d.ts +4 -0
  9. package/build/code/types.d.ts.map +1 -1
  10. package/build/index.js +28 -831
  11. package/build/index.js.map +1 -1
  12. package/build/prompts/register.d.ts +10 -0
  13. package/build/prompts/register.d.ts.map +1 -0
  14. package/build/prompts/register.js +50 -0
  15. package/build/prompts/register.js.map +1 -0
  16. package/build/resources/index.d.ts +10 -0
  17. package/build/resources/index.d.ts.map +1 -0
  18. package/build/resources/index.js +60 -0
  19. package/build/resources/index.js.map +1 -0
  20. package/build/tools/code.d.ts +10 -0
  21. package/build/tools/code.d.ts.map +1 -0
  22. package/build/tools/code.js +132 -0
  23. package/build/tools/code.js.map +1 -0
  24. package/build/tools/collection.d.ts +12 -0
  25. package/build/tools/collection.d.ts.map +1 -0
  26. package/build/tools/collection.js +59 -0
  27. package/build/tools/collection.js.map +1 -0
  28. package/build/tools/document.d.ts +12 -0
  29. package/build/tools/document.d.ts.map +1 -0
  30. package/build/tools/document.js +84 -0
  31. package/build/tools/document.js.map +1 -0
  32. package/build/tools/index.d.ts +18 -0
  33. package/build/tools/index.d.ts.map +1 -0
  34. package/build/tools/index.js +30 -0
  35. package/build/tools/index.js.map +1 -0
  36. package/build/tools/schemas.d.ts +75 -0
  37. package/build/tools/schemas.d.ts.map +1 -0
  38. package/build/tools/schemas.js +114 -0
  39. package/build/tools/schemas.js.map +1 -0
  40. package/build/tools/search.d.ts +12 -0
  41. package/build/tools/search.d.ts.map +1 -0
  42. package/build/tools/search.js +79 -0
  43. package/build/tools/search.js.map +1 -0
  44. package/examples/code-search/README.md +19 -4
  45. package/package.json +1 -1
  46. package/src/code/indexer.ts +186 -38
  47. package/src/code/types.ts +5 -0
  48. package/src/index.ts +26 -983
  49. package/src/prompts/register.ts +71 -0
  50. package/src/resources/index.ts +79 -0
  51. package/src/tools/code.ts +195 -0
  52. package/src/tools/collection.ts +100 -0
  53. package/src/tools/document.ts +113 -0
  54. package/src/tools/index.ts +48 -0
  55. package/src/tools/schemas.ts +130 -0
  56. package/src/tools/search.ts +122 -0
  57. package/tests/code/indexer.test.ts +412 -74
  58. package/tests/code/integration.test.ts +239 -54
package/src/index.ts CHANGED
@@ -3,20 +3,11 @@
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
- import {
10
- CallToolRequestSchema,
11
- GetPromptRequestSchema,
12
- ListPromptsRequestSchema,
13
- ListResourcesRequestSchema,
14
- ListToolsRequestSchema,
15
- ReadResourceRequestSchema,
16
- } from "@modelcontextprotocol/sdk/types.js";
17
9
  import Bottleneck from "bottleneck";
18
10
  import express from "express";
19
- import { z } from "zod";
20
11
  import {
21
12
  DEFAULT_BATCH_SIZE,
22
13
  DEFAULT_CHUNK_OVERLAP,
@@ -28,15 +19,11 @@ import {
28
19
  import { CodeIndexer } from "./code/indexer.js";
29
20
  import type { CodeConfig } from "./code/types.js";
30
21
  import { EmbeddingProviderFactory } from "./embeddings/factory.js";
31
- import { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
32
- import {
33
- getPrompt,
34
- listPrompts,
35
- loadPromptsConfig,
36
- type PromptsConfig,
37
- } from "./prompts/index.js";
38
- import { renderTemplate, validateArguments } from "./prompts/template.js";
22
+ import { loadPromptsConfig, type PromptsConfig } from "./prompts/index.js";
23
+ import { registerAllPrompts } from "./prompts/register.js";
39
24
  import { QdrantManager } from "./qdrant/client.js";
25
+ import { registerAllResources } from "./resources/index.js";
26
+ import { registerAllTools } from "./tools/index.js";
40
27
 
41
28
  // Read package.json for version
42
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -213,979 +200,36 @@ if (existsSync(PROMPTS_CONFIG_FILE)) {
213
200
  }
214
201
  }
215
202
 
216
- // Function to create a new MCP server instance
217
- // This is needed for HTTP transport in stateless mode where each request gets its own server
218
- function createServer() {
219
- const capabilities: {
220
- tools: Record<string, never>;
221
- resources: Record<string, never>;
222
- prompts?: Record<string, never>;
223
- } = {
224
- tools: {},
225
- resources: {},
226
- };
227
-
228
- // Only add prompts capability if prompts are configured
229
- if (promptsConfig) {
230
- capabilities.prompts = {};
231
- }
232
-
233
- return new Server(
234
- {
203
+ // Function to create and configure a new MCP server instance
204
+ function createAndConfigureServer(): McpServer {
205
+ try {
206
+ const server = new McpServer({
235
207
  name: pkg.name,
236
208
  version: pkg.version,
237
- },
238
- {
239
- capabilities,
240
- },
241
- );
242
- }
243
-
244
- // Create a shared MCP server for stdio mode
245
- const server = createServer();
246
-
247
- // Function to register all handlers on a server instance
248
- function registerHandlers(server: Server) {
249
- // List available tools
250
- server.setRequestHandler(ListToolsRequestSchema, async () => {
251
- return {
252
- tools: [
253
- {
254
- name: "create_collection",
255
- description:
256
- "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.",
257
- inputSchema: {
258
- type: "object",
259
- properties: {
260
- name: {
261
- type: "string",
262
- description: "Name of the collection",
263
- },
264
- distance: {
265
- type: "string",
266
- enum: ["Cosine", "Euclid", "Dot"],
267
- description: "Distance metric (default: Cosine)",
268
- },
269
- enableHybrid: {
270
- type: "boolean",
271
- description:
272
- "Enable hybrid search with sparse vectors (default: false)",
273
- },
274
- },
275
- required: ["name"],
276
- },
277
- },
278
- {
279
- name: "add_documents",
280
- description:
281
- "Add documents to a collection. Documents will be automatically embedded using the configured embedding provider.",
282
- inputSchema: {
283
- type: "object",
284
- properties: {
285
- collection: {
286
- type: "string",
287
- description: "Name of the collection",
288
- },
289
- documents: {
290
- type: "array",
291
- description: "Array of documents to add",
292
- items: {
293
- type: "object",
294
- properties: {
295
- id: {
296
- type: ["string", "number"],
297
- description: "Unique identifier for the document",
298
- },
299
- text: {
300
- type: "string",
301
- description: "Text content to embed and store",
302
- },
303
- metadata: {
304
- type: "object",
305
- description:
306
- "Optional metadata to store with the document",
307
- },
308
- },
309
- required: ["id", "text"],
310
- },
311
- },
312
- },
313
- required: ["collection", "documents"],
314
- },
315
- },
316
- {
317
- name: "semantic_search",
318
- description:
319
- "Search for documents using natural language queries. Returns the most semantically similar documents.",
320
- inputSchema: {
321
- type: "object",
322
- properties: {
323
- collection: {
324
- type: "string",
325
- description: "Name of the collection to search",
326
- },
327
- query: {
328
- type: "string",
329
- description: "Search query text",
330
- },
331
- limit: {
332
- type: "number",
333
- description: "Maximum number of results (default: 5)",
334
- },
335
- filter: {
336
- type: "object",
337
- description: "Optional metadata filter",
338
- },
339
- },
340
- required: ["collection", "query"],
341
- },
342
- },
343
- {
344
- name: "list_collections",
345
- description: "List all available collections in Qdrant.",
346
- inputSchema: {
347
- type: "object",
348
- properties: {},
349
- },
350
- },
351
- {
352
- name: "delete_collection",
353
- description: "Delete a collection and all its documents.",
354
- inputSchema: {
355
- type: "object",
356
- properties: {
357
- name: {
358
- type: "string",
359
- description: "Name of the collection to delete",
360
- },
361
- },
362
- required: ["name"],
363
- },
364
- },
365
- {
366
- name: "get_collection_info",
367
- description:
368
- "Get detailed information about a collection including vector size, point count, and distance metric.",
369
- inputSchema: {
370
- type: "object",
371
- properties: {
372
- name: {
373
- type: "string",
374
- description: "Name of the collection",
375
- },
376
- },
377
- required: ["name"],
378
- },
379
- },
380
- {
381
- name: "delete_documents",
382
- description:
383
- "Delete specific documents from a collection by their IDs.",
384
- inputSchema: {
385
- type: "object",
386
- properties: {
387
- collection: {
388
- type: "string",
389
- description: "Name of the collection",
390
- },
391
- ids: {
392
- type: "array",
393
- description: "Array of document IDs to delete",
394
- items: {
395
- type: ["string", "number"],
396
- },
397
- },
398
- },
399
- required: ["collection", "ids"],
400
- },
401
- },
402
- {
403
- name: "hybrid_search",
404
- description:
405
- "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.",
406
- inputSchema: {
407
- type: "object",
408
- properties: {
409
- collection: {
410
- type: "string",
411
- description: "Name of the collection to search",
412
- },
413
- query: {
414
- type: "string",
415
- description: "Search query text",
416
- },
417
- limit: {
418
- type: "number",
419
- description: "Maximum number of results (default: 5)",
420
- },
421
- filter: {
422
- type: "object",
423
- description: "Optional metadata filter",
424
- },
425
- },
426
- required: ["collection", "query"],
427
- },
428
- },
429
- {
430
- name: "index_codebase",
431
- description:
432
- "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.",
433
- inputSchema: {
434
- type: "object",
435
- properties: {
436
- path: {
437
- type: "string",
438
- description:
439
- "Absolute or relative path to codebase root directory",
440
- },
441
- forceReindex: {
442
- type: "boolean",
443
- description:
444
- "Force full re-index even if already indexed (default: false)",
445
- },
446
- extensions: {
447
- type: "array",
448
- items: { type: "string" },
449
- description:
450
- "Custom file extensions to index (e.g., ['.proto', '.graphql'])",
451
- },
452
- ignorePatterns: {
453
- type: "array",
454
- items: { type: "string" },
455
- description:
456
- "Additional patterns to ignore (e.g., ['**/test/**', '**/*.test.ts'])",
457
- },
458
- },
459
- required: ["path"],
460
- },
461
- },
462
- {
463
- name: "search_code",
464
- description:
465
- "Search indexed codebase using natural language queries. Returns semantically relevant code chunks with file paths and line numbers.",
466
- inputSchema: {
467
- type: "object",
468
- properties: {
469
- path: {
470
- type: "string",
471
- description: "Path to codebase (must be indexed first)",
472
- },
473
- query: {
474
- type: "string",
475
- description:
476
- "Natural language search query (e.g., 'authentication logic')",
477
- },
478
- limit: {
479
- type: "number",
480
- description: "Maximum number of results (default: 5, max: 100)",
481
- },
482
- fileTypes: {
483
- type: "array",
484
- items: { type: "string" },
485
- description: "Filter by file extensions (e.g., ['.ts', '.py'])",
486
- },
487
- pathPattern: {
488
- type: "string",
489
- description:
490
- "Filter by path glob pattern (e.g., 'src/services/**')",
491
- },
492
- },
493
- required: ["path", "query"],
494
- },
495
- },
496
- {
497
- name: "reindex_changes",
498
- description:
499
- "Incrementally re-index only changed files. Detects added, modified, and deleted files since last index. Requires previous indexing with index_codebase.",
500
- inputSchema: {
501
- type: "object",
502
- properties: {
503
- path: {
504
- type: "string",
505
- description: "Path to codebase",
506
- },
507
- },
508
- required: ["path"],
509
- },
510
- },
511
- {
512
- name: "get_index_status",
513
- description: "Get indexing status and statistics for a codebase.",
514
- inputSchema: {
515
- type: "object",
516
- properties: {
517
- path: {
518
- type: "string",
519
- description: "Path to codebase",
520
- },
521
- },
522
- required: ["path"],
523
- },
524
- },
525
- {
526
- name: "clear_index",
527
- description:
528
- "Delete all indexed data for a codebase. This is irreversible and will remove the entire collection.",
529
- inputSchema: {
530
- type: "object",
531
- properties: {
532
- path: {
533
- type: "string",
534
- description: "Path to codebase",
535
- },
536
- },
537
- required: ["path"],
538
- },
539
- },
540
- ],
541
- };
542
- });
543
-
544
- // Handle tool calls
545
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
546
- const { name, arguments: args } = request.params;
547
-
548
- try {
549
- switch (name) {
550
- case "create_collection": {
551
- const { name, distance, enableHybrid } =
552
- CreateCollectionSchema.parse(args);
553
- const vectorSize = embeddings.getDimensions();
554
- await qdrant.createCollection(
555
- name,
556
- vectorSize,
557
- distance,
558
- enableHybrid || false,
559
- );
560
-
561
- let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
562
- if (enableHybrid) {
563
- message += " Hybrid search is enabled for this collection.";
564
- }
565
-
566
- return {
567
- content: [
568
- {
569
- type: "text",
570
- text: message,
571
- },
572
- ],
573
- };
574
- }
575
-
576
- case "add_documents": {
577
- const { collection, documents } = AddDocumentsSchema.parse(args);
578
-
579
- // Check if collection exists and get info
580
- const exists = await qdrant.collectionExists(collection);
581
- if (!exists) {
582
- return {
583
- content: [
584
- {
585
- type: "text",
586
- text: `Error: Collection "${collection}" does not exist. Create it first using create_collection.`,
587
- },
588
- ],
589
- isError: true,
590
- };
591
- }
592
-
593
- const collectionInfo = await qdrant.getCollectionInfo(collection);
594
-
595
- // Generate embeddings for all documents
596
- const texts = documents.map((doc) => doc.text);
597
- const embeddingResults = await embeddings.embedBatch(texts);
598
-
599
- // If hybrid search is enabled, generate sparse vectors and use appropriate method
600
- if (collectionInfo.hybridEnabled) {
601
- const sparseGenerator = new BM25SparseVectorGenerator();
602
-
603
- // Prepare points with both dense and sparse vectors
604
- const points = documents.map((doc, index) => ({
605
- id: doc.id,
606
- vector: embeddingResults[index].embedding,
607
- sparseVector: sparseGenerator.generate(doc.text),
608
- payload: {
609
- text: doc.text,
610
- ...doc.metadata,
611
- },
612
- }));
613
-
614
- await qdrant.addPointsWithSparse(collection, points);
615
- } else {
616
- // Standard dense-only vectors
617
- const points = documents.map((doc, index) => ({
618
- id: doc.id,
619
- vector: embeddingResults[index].embedding,
620
- payload: {
621
- text: doc.text,
622
- ...doc.metadata,
623
- },
624
- }));
625
-
626
- await qdrant.addPoints(collection, points);
627
- }
628
-
629
- return {
630
- content: [
631
- {
632
- type: "text",
633
- text: `Successfully added ${documents.length} document(s) to collection "${collection}".`,
634
- },
635
- ],
636
- };
637
- }
638
-
639
- case "semantic_search": {
640
- const { collection, query, limit, filter } =
641
- SemanticSearchSchema.parse(args);
642
-
643
- // Check if collection exists
644
- const exists = await qdrant.collectionExists(collection);
645
- if (!exists) {
646
- return {
647
- content: [
648
- {
649
- type: "text",
650
- text: `Error: Collection "${collection}" does not exist.`,
651
- },
652
- ],
653
- isError: true,
654
- };
655
- }
656
-
657
- // Generate embedding for query
658
- const { embedding } = await embeddings.embed(query);
659
-
660
- // Search
661
- const results = await qdrant.search(
662
- collection,
663
- embedding,
664
- limit || 5,
665
- filter,
666
- );
667
-
668
- return {
669
- content: [
670
- {
671
- type: "text",
672
- text: JSON.stringify(results, null, 2),
673
- },
674
- ],
675
- };
676
- }
677
-
678
- case "list_collections": {
679
- const collections = await qdrant.listCollections();
680
- return {
681
- content: [
682
- {
683
- type: "text",
684
- text: JSON.stringify(collections, null, 2),
685
- },
686
- ],
687
- };
688
- }
689
-
690
- case "delete_collection": {
691
- const { name } = DeleteCollectionSchema.parse(args);
692
- await qdrant.deleteCollection(name);
693
- return {
694
- content: [
695
- {
696
- type: "text",
697
- text: `Collection "${name}" deleted successfully.`,
698
- },
699
- ],
700
- };
701
- }
702
-
703
- case "get_collection_info": {
704
- const { name } = GetCollectionInfoSchema.parse(args);
705
- const info = await qdrant.getCollectionInfo(name);
706
- return {
707
- content: [
708
- {
709
- type: "text",
710
- text: JSON.stringify(info, null, 2),
711
- },
712
- ],
713
- };
714
- }
715
-
716
- case "delete_documents": {
717
- const { collection, ids } = DeleteDocumentsSchema.parse(args);
718
- await qdrant.deletePoints(collection, ids);
719
- return {
720
- content: [
721
- {
722
- type: "text",
723
- text: `Successfully deleted ${ids.length} document(s) from collection "${collection}".`,
724
- },
725
- ],
726
- };
727
- }
728
-
729
- case "hybrid_search": {
730
- const { collection, query, limit, filter } =
731
- HybridSearchSchema.parse(args);
732
-
733
- // Check if collection exists
734
- const exists = await qdrant.collectionExists(collection);
735
- if (!exists) {
736
- return {
737
- content: [
738
- {
739
- type: "text",
740
- text: `Error: Collection "${collection}" does not exist.`,
741
- },
742
- ],
743
- isError: true,
744
- };
745
- }
746
-
747
- // Check if collection has hybrid search enabled
748
- const collectionInfo = await qdrant.getCollectionInfo(collection);
749
- if (!collectionInfo.hybridEnabled) {
750
- return {
751
- content: [
752
- {
753
- type: "text",
754
- text: `Error: Collection "${collection}" does not have hybrid search enabled. Create a new collection with enableHybrid set to true.`,
755
- },
756
- ],
757
- isError: true,
758
- };
759
- }
760
-
761
- // Generate dense embedding for query
762
- const { embedding } = await embeddings.embed(query);
763
-
764
- // Generate sparse vector for query
765
- const sparseGenerator = new BM25SparseVectorGenerator();
766
- const sparseVector = sparseGenerator.generate(query);
767
-
768
- // Perform hybrid search
769
- const results = await qdrant.hybridSearch(
770
- collection,
771
- embedding,
772
- sparseVector,
773
- limit || 5,
774
- filter,
775
- );
776
-
777
- return {
778
- content: [
779
- {
780
- type: "text",
781
- text: JSON.stringify(results, null, 2),
782
- },
783
- ],
784
- };
785
- }
786
-
787
- case "index_codebase": {
788
- const IndexCodebaseSchema = z.object({
789
- path: z.string(),
790
- forceReindex: z.boolean().optional(),
791
- extensions: z.array(z.string()).optional(),
792
- ignorePatterns: z.array(z.string()).optional(),
793
- });
794
-
795
- const { path, forceReindex, extensions, ignorePatterns } =
796
- IndexCodebaseSchema.parse(args);
797
-
798
- const stats = await codeIndexer.indexCodebase(
799
- path,
800
- { forceReindex, extensions, ignorePatterns },
801
- (progress) => {
802
- // Progress callback - could send progress updates via SSE in future
803
- console.error(
804
- `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
805
- );
806
- },
807
- );
808
-
809
- let statusMessage = `Indexed ${stats.filesIndexed}/${stats.filesScanned} files (${stats.chunksCreated} chunks) in ${(stats.durationMs / 1000).toFixed(1)}s`;
810
-
811
- if (stats.status === "partial") {
812
- statusMessage += `\n\nWarnings:\n${stats.errors?.join("\n")}`;
813
- } else if (stats.status === "failed") {
814
- statusMessage = `Indexing failed:\n${stats.errors?.join("\n")}`;
815
- }
816
-
817
- return {
818
- content: [
819
- {
820
- type: "text",
821
- text: statusMessage,
822
- },
823
- ],
824
- isError: stats.status === "failed",
825
- };
826
- }
827
-
828
- case "search_code": {
829
- const SearchCodeSchema = z.object({
830
- path: z.string(),
831
- query: z.string(),
832
- limit: z.number().optional(),
833
- fileTypes: z.array(z.string()).optional(),
834
- pathPattern: z.string().optional(),
835
- });
836
-
837
- const { path, query, limit, fileTypes, pathPattern } =
838
- SearchCodeSchema.parse(args);
839
-
840
- const results = await codeIndexer.searchCode(path, query, {
841
- limit,
842
- fileTypes,
843
- pathPattern,
844
- });
845
-
846
- if (results.length === 0) {
847
- return {
848
- content: [
849
- {
850
- type: "text",
851
- text: `No results found for query: "${query}"`,
852
- },
853
- ],
854
- };
855
- }
856
-
857
- // Format results with file references
858
- const formattedResults = results
859
- .map(
860
- (r, idx) =>
861
- `\n--- Result ${idx + 1} (score: ${r.score.toFixed(3)}) ---\n` +
862
- `File: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
863
- `Language: ${r.language}\n\n` +
864
- `${r.content}\n`,
865
- )
866
- .join("\n");
867
-
868
- return {
869
- content: [
870
- {
871
- type: "text",
872
- text: `Found ${results.length} result(s):\n${formattedResults}`,
873
- },
874
- ],
875
- };
876
- }
877
-
878
- case "get_index_status": {
879
- const GetIndexStatusSchema = z.object({
880
- path: z.string(),
881
- });
882
-
883
- const { path } = GetIndexStatusSchema.parse(args);
884
- const status = await codeIndexer.getIndexStatus(path);
885
-
886
- if (!status.isIndexed) {
887
- return {
888
- content: [
889
- {
890
- type: "text",
891
- text: `Codebase at "${path}" is not indexed. Use index_codebase to index it first.`,
892
- },
893
- ],
894
- };
895
- }
896
-
897
- return {
898
- content: [
899
- {
900
- type: "text",
901
- text: JSON.stringify(status, null, 2),
902
- },
903
- ],
904
- };
905
- }
906
-
907
- case "reindex_changes": {
908
- const ReindexChangesSchema = z.object({
909
- path: z.string(),
910
- });
911
-
912
- const { path } = ReindexChangesSchema.parse(args);
913
-
914
- const stats = await codeIndexer.reindexChanges(path, (progress) => {
915
- console.error(
916
- `[${progress.phase}] ${progress.percentage}% - ${progress.message}`,
917
- );
918
- });
919
-
920
- let message = `Incremental re-index complete:\n`;
921
- message += `- Files added: ${stats.filesAdded}\n`;
922
- message += `- Files modified: ${stats.filesModified}\n`;
923
- message += `- Files deleted: ${stats.filesDeleted}\n`;
924
- message += `- Chunks added: ${stats.chunksAdded}\n`;
925
- message += `- Duration: ${(stats.durationMs / 1000).toFixed(1)}s`;
926
-
927
- if (
928
- stats.filesAdded === 0 &&
929
- stats.filesModified === 0 &&
930
- stats.filesDeleted === 0
931
- ) {
932
- message = `No changes detected. Codebase is up to date.`;
933
- }
934
-
935
- return {
936
- content: [
937
- {
938
- type: "text",
939
- text: message,
940
- },
941
- ],
942
- };
943
- }
944
-
945
- case "clear_index": {
946
- const ClearIndexSchema = z.object({
947
- path: z.string(),
948
- });
949
-
950
- const { path } = ClearIndexSchema.parse(args);
951
- await codeIndexer.clearIndex(path);
952
-
953
- return {
954
- content: [
955
- {
956
- type: "text",
957
- text: `Index cleared for codebase at "${path}".`,
958
- },
959
- ],
960
- };
961
- }
962
-
963
- default:
964
- return {
965
- content: [
966
- {
967
- type: "text",
968
- text: `Unknown tool: ${name}`,
969
- },
970
- ],
971
- isError: true,
972
- };
973
- }
974
- } catch (error: any) {
975
- // Enhanced error details for debugging
976
- const errorDetails =
977
- error instanceof Error ? error.message : JSON.stringify(error, null, 2);
978
-
979
- console.error("Tool execution error:", {
980
- tool: name,
981
- error: errorDetails,
982
- stack: error?.stack,
983
- data: error?.data,
984
- });
985
-
986
- return {
987
- content: [
988
- {
989
- type: "text",
990
- text: `Error: ${errorDetails}`,
991
- },
992
- ],
993
- isError: true,
994
- };
995
- }
996
- });
997
-
998
- // List available resources
999
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
1000
- const collections = await qdrant.listCollections();
1001
-
1002
- return {
1003
- resources: [
1004
- {
1005
- uri: "qdrant://collections",
1006
- name: "All Collections",
1007
- description: "List of all vector collections in Qdrant",
1008
- mimeType: "application/json",
1009
- },
1010
- ...collections.map((name) => ({
1011
- uri: `qdrant://collection/${name}`,
1012
- name: `Collection: ${name}`,
1013
- description: `Details and statistics for collection "${name}"`,
1014
- mimeType: "application/json",
1015
- })),
1016
- ],
1017
- };
1018
- });
1019
-
1020
- // Read resource content
1021
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1022
- const { uri } = request.params;
1023
-
1024
- if (uri === "qdrant://collections") {
1025
- const collections = await qdrant.listCollections();
1026
- return {
1027
- contents: [
1028
- {
1029
- uri,
1030
- mimeType: "application/json",
1031
- text: JSON.stringify(collections, null, 2),
1032
- },
1033
- ],
1034
- };
1035
- }
1036
-
1037
- const collectionMatch = uri.match(/^qdrant:\/\/collection\/(.+)$/);
1038
- if (collectionMatch) {
1039
- const name = collectionMatch[1];
1040
- const info = await qdrant.getCollectionInfo(name);
1041
- return {
1042
- contents: [
1043
- {
1044
- uri,
1045
- mimeType: "application/json",
1046
- text: JSON.stringify(info, null, 2),
1047
- },
1048
- ],
1049
- };
1050
- }
1051
-
1052
- return {
1053
- contents: [
1054
- {
1055
- uri,
1056
- mimeType: "text/plain",
1057
- text: `Unknown resource: ${uri}`,
1058
- },
1059
- ],
1060
- };
1061
- });
1062
-
1063
- // List available prompts
1064
- if (promptsConfig) {
1065
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
1066
- const prompts = listPrompts(promptsConfig!);
1067
-
1068
- return {
1069
- prompts: prompts.map((prompt) => ({
1070
- name: prompt.name,
1071
- description: prompt.description,
1072
- arguments: prompt.arguments.map((arg) => ({
1073
- name: arg.name,
1074
- description: arg.description,
1075
- required: arg.required,
1076
- })),
1077
- })),
1078
- };
1079
209
  });
1080
210
 
1081
- // Get prompt content
1082
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1083
- const { name, arguments: args } = request.params;
1084
-
1085
- const prompt = getPrompt(promptsConfig!, name);
1086
- if (!prompt) {
1087
- throw new Error(`Unknown prompt: ${name}`);
1088
- }
211
+ // Register all tools
212
+ registerAllTools(server, {
213
+ qdrant,
214
+ embeddings,
215
+ codeIndexer,
216
+ });
1089
217
 
1090
- try {
1091
- // Validate arguments
1092
- validateArguments(args || {}, prompt.arguments);
218
+ // Register all resources
219
+ registerAllResources(server, qdrant);
1093
220
 
1094
- // Render template
1095
- const rendered = renderTemplate(
1096
- prompt.template,
1097
- args || {},
1098
- prompt.arguments,
1099
- );
221
+ // Register all prompts (if configured)
222
+ registerAllPrompts(server, promptsConfig);
1100
223
 
1101
- return {
1102
- messages: [
1103
- {
1104
- role: "user",
1105
- content: {
1106
- type: "text",
1107
- text: rendered.text,
1108
- },
1109
- },
1110
- ],
1111
- };
1112
- } catch (error) {
1113
- throw new Error(
1114
- `Failed to render prompt "${name}": ${error instanceof Error ? error.message : String(error)}`,
1115
- );
1116
- }
1117
- });
224
+ return server;
225
+ } catch (error) {
226
+ console.error("Failed to configure MCP server:", error);
227
+ throw error;
1118
228
  }
1119
229
  }
1120
230
 
1121
- // Register handlers on the shared server for stdio mode
1122
- registerHandlers(server);
1123
-
1124
- // Tool schemas
1125
- const CreateCollectionSchema = z.object({
1126
- name: z.string().describe("Name of the collection"),
1127
- distance: z
1128
- .enum(["Cosine", "Euclid", "Dot"])
1129
- .optional()
1130
- .describe("Distance metric (default: Cosine)"),
1131
- enableHybrid: z
1132
- .boolean()
1133
- .optional()
1134
- .describe("Enable hybrid search with sparse vectors (default: false)"),
1135
- });
1136
-
1137
- const AddDocumentsSchema = z.object({
1138
- collection: z.string().describe("Name of the collection"),
1139
- documents: z
1140
- .array(
1141
- z.object({
1142
- id: z
1143
- .union([z.string(), z.number()])
1144
- .describe("Unique identifier for the document"),
1145
- text: z.string().describe("Text content to embed and store"),
1146
- metadata: z
1147
- .record(z.any())
1148
- .optional()
1149
- .describe("Optional metadata to store with the document"),
1150
- }),
1151
- )
1152
- .describe("Array of documents to add"),
1153
- });
1154
-
1155
- const SemanticSearchSchema = z.object({
1156
- collection: z.string().describe("Name of the collection to search"),
1157
- query: z.string().describe("Search query text"),
1158
- limit: z
1159
- .number()
1160
- .optional()
1161
- .describe("Maximum number of results (default: 5)"),
1162
- filter: z.record(z.any()).optional().describe("Optional metadata filter"),
1163
- });
1164
-
1165
- const DeleteCollectionSchema = z.object({
1166
- name: z.string().describe("Name of the collection to delete"),
1167
- });
1168
-
1169
- const GetCollectionInfoSchema = z.object({
1170
- name: z.string().describe("Name of the collection"),
1171
- });
1172
-
1173
- const DeleteDocumentsSchema = z.object({
1174
- collection: z.string().describe("Name of the collection"),
1175
- ids: z
1176
- .array(z.union([z.string(), z.number()]))
1177
- .describe("Array of document IDs to delete"),
1178
- });
1179
-
1180
- const HybridSearchSchema = z.object({
1181
- collection: z.string().describe("Name of the collection to search"),
1182
- query: z.string().describe("Search query text"),
1183
- limit: z
1184
- .number()
1185
- .optional()
1186
- .describe("Maximum number of results (default: 5)"),
1187
- filter: z.record(z.any()).optional().describe("Optional metadata filter"),
1188
- });
231
+ // Create a shared MCP server for stdio mode
232
+ const server = createAndConfigureServer();
1189
233
 
1190
234
  // Start server with stdio transport
1191
235
  async function startStdioServer() {
@@ -1311,8 +355,7 @@ async function startHttpServer() {
1311
355
 
1312
356
  app.post("/mcp", rateLimitMiddleware, async (req, res) => {
1313
357
  // Create a new server for each request
1314
- const requestServer = createServer();
1315
- registerHandlers(requestServer);
358
+ const requestServer = createAndConfigureServer();
1316
359
 
1317
360
  // Create transport with enableJsonResponse
1318
361
  const transport = new StreamableHTTPServerTransport({