@mhalder/qdrant-mcp-server 2.1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/CONTRIBUTING.md +14 -2
  3. package/build/index.js +28 -831
  4. package/build/index.js.map +1 -1
  5. package/build/prompts/register.d.ts +10 -0
  6. package/build/prompts/register.d.ts.map +1 -0
  7. package/build/prompts/register.js +50 -0
  8. package/build/prompts/register.js.map +1 -0
  9. package/build/resources/index.d.ts +10 -0
  10. package/build/resources/index.d.ts.map +1 -0
  11. package/build/resources/index.js +60 -0
  12. package/build/resources/index.js.map +1 -0
  13. package/build/tools/code.d.ts +10 -0
  14. package/build/tools/code.d.ts.map +1 -0
  15. package/build/tools/code.js +122 -0
  16. package/build/tools/code.js.map +1 -0
  17. package/build/tools/collection.d.ts +12 -0
  18. package/build/tools/collection.d.ts.map +1 -0
  19. package/build/tools/collection.js +59 -0
  20. package/build/tools/collection.js.map +1 -0
  21. package/build/tools/document.d.ts +12 -0
  22. package/build/tools/document.d.ts.map +1 -0
  23. package/build/tools/document.js +84 -0
  24. package/build/tools/document.js.map +1 -0
  25. package/build/tools/index.d.ts +18 -0
  26. package/build/tools/index.d.ts.map +1 -0
  27. package/build/tools/index.js +30 -0
  28. package/build/tools/index.js.map +1 -0
  29. package/build/tools/schemas.d.ts +75 -0
  30. package/build/tools/schemas.d.ts.map +1 -0
  31. package/build/tools/schemas.js +114 -0
  32. package/build/tools/schemas.js.map +1 -0
  33. package/build/tools/search.d.ts +12 -0
  34. package/build/tools/search.d.ts.map +1 -0
  35. package/build/tools/search.js +79 -0
  36. package/build/tools/search.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/index.ts +26 -983
  39. package/src/prompts/register.ts +71 -0
  40. package/src/resources/index.ts +79 -0
  41. package/src/tools/code.ts +184 -0
  42. package/src/tools/collection.ts +100 -0
  43. package/src/tools/document.ts +113 -0
  44. package/src/tools/index.ts +48 -0
  45. package/src/tools/schemas.ts +130 -0
  46. 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 { Server } from "@modelcontextprotocol/sdk/server/index.js";
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 { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
16
- import { getPrompt, listPrompts, loadPromptsConfig, } from "./prompts/index.js";
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
- // This is needed for HTTP transport in stateless mode where each request gets its own server
150
- function createServer() {
151
- const capabilities = {
152
- tools: {},
153
- resources: {},
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
- // Get prompt content
887
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
888
- const { name, arguments: args } = request.params;
889
- const prompt = getPrompt(promptsConfig, name);
890
- if (!prompt) {
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
- // Register handlers on the shared server for stdio mode
917
- registerHandlers(server);
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();
@@ -1067,8 +265,7 @@ async function startHttpServer() {
1067
265
  });
1068
266
  app.post("/mcp", rateLimitMiddleware, async (req, res) => {
1069
267
  // Create a new server for each request
1070
- const requestServer = createServer();
1071
- registerHandlers(requestServer);
268
+ const requestServer = createAndConfigureServer();
1072
269
  // Create transport with enableJsonResponse
1073
270
  const transport = new StreamableHTTPServerTransport({
1074
271
  sessionIdGenerator: undefined,