domain-rag-mcp-server 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +894 -0
  2. package/package.json +44 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,894 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { QdrantClient } from "@qdrant/js-client-rest";
11
+ import { v4 as uuidv4 } from "uuid";
12
+
13
+ // ../shared/src/config.ts
14
+ import { config as dotenvConfig } from "dotenv";
15
+ import { existsSync } from "fs";
16
+ import { join } from "path";
17
+ function loadEnvFiles() {
18
+ const possibleRoots = [
19
+ process.cwd(),
20
+ join(process.cwd(), ".."),
21
+ join(process.cwd(), "../..")
22
+ ];
23
+ for (const root of possibleRoots) {
24
+ const envLocal = join(root, ".env.local");
25
+ const envFile = join(root, ".env");
26
+ if (existsSync(envLocal)) {
27
+ dotenvConfig({ path: envLocal });
28
+ break;
29
+ }
30
+ if (existsSync(envFile)) {
31
+ dotenvConfig({ path: envFile });
32
+ break;
33
+ }
34
+ }
35
+ }
36
+ loadEnvFiles();
37
+ function getEnv(key, defaultValue) {
38
+ return process.env[key] || defaultValue;
39
+ }
40
+ function getEnvBool(key, defaultValue) {
41
+ const value = process.env[key];
42
+ if (value === void 0)
43
+ return defaultValue;
44
+ return value.toLowerCase() === "true" || value === "1";
45
+ }
46
+ function getEnvNumber(key, defaultValue) {
47
+ const value = process.env[key];
48
+ if (value === void 0)
49
+ return defaultValue;
50
+ const parsed = parseInt(value, 10);
51
+ return isNaN(parsed) ? defaultValue : parsed;
52
+ }
53
+ function getEnvFloat(key, defaultValue) {
54
+ const value = process.env[key];
55
+ if (value === void 0)
56
+ return defaultValue;
57
+ const parsed = parseFloat(value);
58
+ return isNaN(parsed) ? defaultValue : parsed;
59
+ }
60
+ function getEnvArray(key, defaultValue) {
61
+ const value = process.env[key];
62
+ if (!value)
63
+ return defaultValue;
64
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
65
+ }
66
+ function getEmbeddingDimensions(provider, model) {
67
+ if (provider === "openai") {
68
+ if (model.includes("text-embedding-3-large"))
69
+ return 3072;
70
+ if (model.includes("text-embedding-3-small"))
71
+ return 1536;
72
+ if (model.includes("text-embedding-ada"))
73
+ return 1536;
74
+ return 1536;
75
+ }
76
+ if (model.includes("nomic-embed-text"))
77
+ return 768;
78
+ if (model.includes("mxbai-embed-large"))
79
+ return 1024;
80
+ if (model.includes("all-minilm"))
81
+ return 384;
82
+ return 768;
83
+ }
84
+ function buildConfig() {
85
+ const embeddingProvider2 = getEnv("EMBEDDING_PROVIDER", "ollama");
86
+ const ollamaModel = getEnv("OLLAMA_MODEL", getEnv("EMBEDDINGS_MODEL", "nomic-embed-text"));
87
+ const openaiModel = getEnv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small");
88
+ const dimensions = getEmbeddingDimensions(
89
+ embeddingProvider2,
90
+ embeddingProvider2 === "openai" ? openaiModel : ollamaModel
91
+ );
92
+ return {
93
+ qdrant: {
94
+ url: getEnv("QDRANT_URL", "http://localhost:6333"),
95
+ apiKey: getEnv("QDRANT_API_KEY", ""),
96
+ collectionName: getEnv("QDRANT_COLLECTION", "domain_knowledge")
97
+ },
98
+ embeddings: {
99
+ provider: embeddingProvider2,
100
+ dimensions,
101
+ ollama: {
102
+ url: getEnv("OLLAMA_URL", getEnv("EMBEDDINGS_URL", "http://localhost:11434")),
103
+ model: ollamaModel
104
+ },
105
+ openai: {
106
+ apiKey: getEnv("OPENAI_API_KEY", ""),
107
+ model: openaiModel
108
+ }
109
+ },
110
+ sources: {
111
+ jira: {
112
+ enabled: getEnvBool("JIRA_ENABLED", true),
113
+ url: getEnv("JIRA_URL", ""),
114
+ username: getEnv("JIRA_USERNAME", ""),
115
+ apiToken: getEnv("JIRA_API_TOKEN", ""),
116
+ projects: getEnvArray("JIRA_PROJECTS", [])
117
+ },
118
+ confluence: {
119
+ enabled: getEnvBool("CONFLUENCE_ENABLED", true),
120
+ url: getEnv("CONFLUENCE_URL", ""),
121
+ username: getEnv("CONFLUENCE_USERNAME", ""),
122
+ apiToken: getEnv("CONFLUENCE_API_TOKEN", ""),
123
+ spaces: getEnvArray("CONFLUENCE_SPACES", [])
124
+ },
125
+ gitCommits: {
126
+ enabled: getEnvBool("GIT_COMMITS_ENABLED", true),
127
+ reposPath: getEnv("GIT_REPOS_PATH", getEnv("REPOS_PATH", ""))
128
+ },
129
+ code: {
130
+ enabled: getEnvBool("CODE_INDEXING_ENABLED", false),
131
+ // Disabled by default now
132
+ reposPath: getEnv("REPOS_PATH", "")
133
+ },
134
+ github: {
135
+ enabled: getEnvBool("GITHUB_COMMITS_ENABLED", false),
136
+ token: getEnv("GITHUB_TOKEN", ""),
137
+ orgs: getEnvArray("GITHUB_ORGS", []),
138
+ repos: getEnvArray("GITHUB_REPOS", [])
139
+ }
140
+ },
141
+ repositories: {
142
+ basePath: getEnv("REPOS_PATH", ""),
143
+ include: getEnvArray("REPOS_INCLUDE", []),
144
+ exclude: getEnvArray("REPOS_EXCLUDE", [
145
+ "node_modules",
146
+ ".git",
147
+ "dist",
148
+ "build",
149
+ "bin",
150
+ "obj",
151
+ ".vs",
152
+ ".idea",
153
+ "packages",
154
+ "TestResults"
155
+ ])
156
+ },
157
+ fileExtensions: getEnvArray("FILE_EXTENSIONS", [
158
+ ".cs",
159
+ ".ts",
160
+ ".tsx",
161
+ ".js",
162
+ ".jsx",
163
+ ".json",
164
+ ".yaml",
165
+ ".yml",
166
+ ".sql"
167
+ ]),
168
+ chunking: {
169
+ maxChunkSize: getEnvNumber("CHUNK_MAX_SIZE", 1500),
170
+ chunkOverlap: getEnvNumber("CHUNK_OVERLAP", 200),
171
+ minChunkSize: getEnvNumber("CHUNK_MIN_SIZE", 100)
172
+ },
173
+ indexation: {
174
+ mode: getEnv("INDEXATION_MODE", "incremental"),
175
+ cron: getEnv("INDEXATION_CRON", "0 2 * * *")
176
+ },
177
+ logging: {
178
+ level: getEnv("LOG_LEVEL", "info")
179
+ },
180
+ sourceWeights: {
181
+ gitCommit: getEnvFloat("WEIGHT_GIT_COMMIT", 1.4),
182
+ code: getEnvFloat("WEIGHT_CODE", 1.3),
183
+ decision: getEnvFloat("WEIGHT_DECISION", 1.1),
184
+ jira: getEnvFloat("WEIGHT_JIRA", 1),
185
+ domainTerm: getEnvFloat("WEIGHT_DOMAIN_TERM", 1),
186
+ confluence: getEnvFloat("WEIGHT_CONFLUENCE", 0.8)
187
+ }
188
+ };
189
+ }
190
+ var config = buildConfig();
191
+ var legacyConfig = {
192
+ qdrant: {
193
+ url: config.qdrant.url,
194
+ collectionName: config.qdrant.collectionName
195
+ },
196
+ embeddings: {
197
+ url: config.embeddings.ollama.url,
198
+ model: config.embeddings.ollama.model,
199
+ dimensions: config.embeddings.dimensions
200
+ },
201
+ repositories: config.repositories,
202
+ fileExtensions: config.fileExtensions,
203
+ chunking: config.chunking
204
+ };
205
+ function getSourceWeightsMap() {
206
+ return {
207
+ git_commit: config.sourceWeights.gitCommit,
208
+ code: config.sourceWeights.code,
209
+ decision: config.sourceWeights.decision,
210
+ jira: config.sourceWeights.jira,
211
+ domain_term: config.sourceWeights.domainTerm,
212
+ confluence: config.sourceWeights.confluence
213
+ };
214
+ }
215
+
216
+ // ../shared/src/embeddings/types.ts
217
+ var BaseEmbeddingProvider = class {
218
+ /**
219
+ * Default batch implementation - calls getEmbedding sequentially
220
+ * Override in subclasses for native batch support
221
+ */
222
+ async getEmbeddings(texts) {
223
+ const embeddings = [];
224
+ for (const text of texts) {
225
+ const embedding = await this.getEmbedding(text);
226
+ embeddings.push(embedding);
227
+ }
228
+ return embeddings;
229
+ }
230
+ };
231
+
232
+ // ../shared/src/embeddings/ollama.ts
233
+ var OllamaProvider = class extends BaseEmbeddingProvider {
234
+ name = "ollama";
235
+ dimensions;
236
+ url;
237
+ model;
238
+ constructor(options) {
239
+ super();
240
+ this.url = options.url;
241
+ this.model = options.model;
242
+ this.dimensions = options.dimensions;
243
+ }
244
+ async getEmbedding(text) {
245
+ const response = await fetch(`${this.url}/api/embeddings`, {
246
+ method: "POST",
247
+ headers: {
248
+ "Content-Type": "application/json"
249
+ },
250
+ body: JSON.stringify({
251
+ model: this.model,
252
+ prompt: text
253
+ })
254
+ });
255
+ if (!response.ok) {
256
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
257
+ }
258
+ const data = await response.json();
259
+ return data.embedding;
260
+ }
261
+ async checkHealth() {
262
+ try {
263
+ const response = await fetch(`${this.url}/api/tags`);
264
+ if (!response.ok)
265
+ return false;
266
+ const data = await response.json();
267
+ const hasModel = data.models?.some((m) => m.name.includes(this.model));
268
+ if (!hasModel) {
269
+ console.log(`Model ${this.model} not found. Attempting to pull...`);
270
+ await this.pullModel();
271
+ }
272
+ return true;
273
+ } catch (error) {
274
+ console.error("Ollama health check failed:", error);
275
+ return false;
276
+ }
277
+ }
278
+ async pullModel() {
279
+ const response = await fetch(`${this.url}/api/pull`, {
280
+ method: "POST",
281
+ headers: {
282
+ "Content-Type": "application/json"
283
+ },
284
+ body: JSON.stringify({
285
+ name: this.model,
286
+ stream: false
287
+ })
288
+ });
289
+ if (!response.ok) {
290
+ throw new Error(`Failed to pull model: ${response.status}`);
291
+ }
292
+ console.log(`Model ${this.model} pulled successfully`);
293
+ }
294
+ };
295
+
296
+ // ../shared/src/embeddings/openai.ts
297
+ var OpenAIProvider = class extends BaseEmbeddingProvider {
298
+ name = "openai";
299
+ dimensions;
300
+ apiKey;
301
+ model;
302
+ baseUrl = "https://api.openai.com/v1";
303
+ constructor(options) {
304
+ super();
305
+ this.apiKey = options.apiKey;
306
+ this.model = options.model;
307
+ this.dimensions = options.dimensions;
308
+ }
309
+ async getEmbedding(text) {
310
+ const response = await fetch(`${this.baseUrl}/embeddings`, {
311
+ method: "POST",
312
+ headers: {
313
+ "Content-Type": "application/json",
314
+ "Authorization": `Bearer ${this.apiKey}`
315
+ },
316
+ body: JSON.stringify({
317
+ model: this.model,
318
+ input: text
319
+ })
320
+ });
321
+ if (!response.ok) {
322
+ const errorText = await response.text();
323
+ throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
324
+ }
325
+ const data = await response.json();
326
+ return data.data[0].embedding;
327
+ }
328
+ /**
329
+ * OpenAI supports native batch embeddings
330
+ * More efficient than sequential calls
331
+ */
332
+ async getEmbeddings(texts) {
333
+ const batchSize = 100;
334
+ const allEmbeddings = [];
335
+ for (let i = 0; i < texts.length; i += batchSize) {
336
+ const batch = texts.slice(i, i + batchSize);
337
+ const batchEmbeddings = await this.getBatchEmbeddings(batch);
338
+ allEmbeddings.push(...batchEmbeddings);
339
+ }
340
+ return allEmbeddings;
341
+ }
342
+ async getBatchEmbeddings(texts) {
343
+ const response = await fetch(`${this.baseUrl}/embeddings`, {
344
+ method: "POST",
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ "Authorization": `Bearer ${this.apiKey}`
348
+ },
349
+ body: JSON.stringify({
350
+ model: this.model,
351
+ input: texts
352
+ })
353
+ });
354
+ if (!response.ok) {
355
+ const errorText = await response.text();
356
+ throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
357
+ }
358
+ const data = await response.json();
359
+ const sorted = data.data.sort((a, b) => a.index - b.index);
360
+ return sorted.map((item) => item.embedding);
361
+ }
362
+ async checkHealth() {
363
+ try {
364
+ const response = await fetch(`${this.baseUrl}/models`, {
365
+ headers: {
366
+ "Authorization": `Bearer ${this.apiKey}`
367
+ }
368
+ });
369
+ if (!response.ok) {
370
+ console.error(`OpenAI health check failed: ${response.status}`);
371
+ return false;
372
+ }
373
+ return true;
374
+ } catch (error) {
375
+ console.error("OpenAI health check failed:", error);
376
+ return false;
377
+ }
378
+ }
379
+ };
380
+
381
+ // ../shared/src/embeddings/factory.ts
382
+ function createEmbeddingProvider() {
383
+ const providerType = config.embeddings.provider;
384
+ switch (providerType) {
385
+ case "ollama":
386
+ return new OllamaProvider({
387
+ url: config.embeddings.ollama.url,
388
+ model: config.embeddings.ollama.model,
389
+ dimensions: config.embeddings.dimensions
390
+ });
391
+ case "openai":
392
+ if (!config.embeddings.openai.apiKey) {
393
+ throw new Error("OPENAI_API_KEY is required when EMBEDDING_PROVIDER=openai");
394
+ }
395
+ return new OpenAIProvider({
396
+ apiKey: config.embeddings.openai.apiKey,
397
+ model: config.embeddings.openai.model,
398
+ dimensions: config.embeddings.dimensions
399
+ });
400
+ default:
401
+ throw new Error(`Unknown embedding provider: ${providerType}`);
402
+ }
403
+ }
404
+ var _provider = null;
405
+ function getEmbeddingProvider() {
406
+ if (!_provider) {
407
+ _provider = createEmbeddingProvider();
408
+ }
409
+ return _provider;
410
+ }
411
+
412
+ // src/index.ts
413
+ var SOURCE_WEIGHTS = getSourceWeightsMap();
414
+ var qdrant = new QdrantClient({
415
+ url: config.qdrant.url,
416
+ ...config.qdrant.apiKey ? { apiKey: config.qdrant.apiKey } : {}
417
+ });
418
+ var embeddingProvider = getEmbeddingProvider();
419
+ async function getEmbedding(text) {
420
+ return embeddingProvider.getEmbedding(text);
421
+ }
422
+ async function ensureCollection() {
423
+ try {
424
+ await qdrant.getCollection(config.qdrant.collectionName);
425
+ } catch {
426
+ await qdrant.createCollection(config.qdrant.collectionName, {
427
+ vectors: {
428
+ size: config.embeddings.dimensions,
429
+ distance: "Cosine"
430
+ }
431
+ });
432
+ await qdrant.createPayloadIndex(config.qdrant.collectionName, {
433
+ field_name: "source_type",
434
+ field_schema: "keyword"
435
+ });
436
+ }
437
+ }
438
+ async function searchWithWeights(options) {
439
+ const { query, limit = 10, sourceTypes, applyWeights = true, minScore = 0.3 } = options;
440
+ const embedding = await getEmbedding(query);
441
+ const filter = {};
442
+ if (sourceTypes && sourceTypes.length > 0) {
443
+ filter.must = [
444
+ { key: "source_type", match: { any: sourceTypes } }
445
+ ];
446
+ }
447
+ const fetchLimit = applyWeights ? limit * 2 : limit;
448
+ const results = await qdrant.search(config.qdrant.collectionName, {
449
+ vector: embedding,
450
+ limit: fetchLimit,
451
+ filter: Object.keys(filter).length > 0 ? filter : void 0,
452
+ with_payload: true,
453
+ score_threshold: minScore
454
+ });
455
+ let searchResults = results.map((r) => {
456
+ const payload = r.payload;
457
+ const sourceType = payload.source_type || "";
458
+ const weight = SOURCE_WEIGHTS[sourceType] || 1;
459
+ const weightedScore = applyWeights ? (r.score || 0) * weight : r.score || 0;
460
+ return {
461
+ content: payload.content || "",
462
+ source_type: sourceType,
463
+ source_id: payload.source_id || "",
464
+ source_url: payload.source_url,
465
+ metadata: payload.metadata || payload,
466
+ score: r.score || 0,
467
+ weighted_score: weightedScore
468
+ };
469
+ });
470
+ if (applyWeights) {
471
+ searchResults.sort((a, b) => b.weighted_score - a.weighted_score);
472
+ }
473
+ return searchResults.slice(0, limit);
474
+ }
475
+ function formatResults(results, title) {
476
+ if (results.length === 0) {
477
+ return `No relevant results found for ${title}.`;
478
+ }
479
+ let response = `## ${title}
480
+
481
+ `;
482
+ response += `Found ${results.length} results (weighted by source reliability):
483
+
484
+ `;
485
+ for (const r of results) {
486
+ const weight = SOURCE_WEIGHTS[r.source_type] || 1;
487
+ const relevance = (r.weighted_score * 100).toFixed(1);
488
+ response += `### [${r.source_type.toUpperCase()}] ${r.source_id}
489
+ `;
490
+ response += `Relevance: ${relevance}% (weight: ${weight}x)
491
+ `;
492
+ if (r.source_url) {
493
+ response += `URL: ${r.source_url}
494
+ `;
495
+ }
496
+ response += `
497
+ ${r.content.substring(0, 1500)}${r.content.length > 1500 ? "..." : ""}
498
+
499
+ `;
500
+ response += "---\n\n";
501
+ }
502
+ return response;
503
+ }
504
+ var tools = [
505
+ // Agent-based search tools
506
+ {
507
+ name: "search_as_analyst",
508
+ description: "Search as a Business Analyst - focuses on Jira tasks, requirements, user stories, and Confluence documentation. Use for business logic, requirements, and feature understanding.",
509
+ inputSchema: {
510
+ type: "object",
511
+ properties: {
512
+ query: {
513
+ type: "string",
514
+ description: "What business requirement, task, or feature are you looking for?"
515
+ },
516
+ limit: {
517
+ type: "number",
518
+ description: "Maximum results (default: 10)",
519
+ default: 10
520
+ }
521
+ },
522
+ required: ["query"]
523
+ }
524
+ },
525
+ {
526
+ name: "search_as_architect",
527
+ description: "Search as a Software Architect - focuses on code, git commits, and system design. Use for understanding implementations, architecture, and technical decisions.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ query: {
532
+ type: "string",
533
+ description: "What code, service, or architectural pattern are you looking for?"
534
+ },
535
+ limit: {
536
+ type: "number",
537
+ description: "Maximum results (default: 10)",
538
+ default: 10
539
+ },
540
+ repo: {
541
+ type: "string",
542
+ description: "Filter by repository name (optional)"
543
+ }
544
+ },
545
+ required: ["query"]
546
+ }
547
+ },
548
+ {
549
+ name: "search_as_qa",
550
+ description: "Search as a QA Engineer - focuses on tests, bugs, and quality-related content. Use for understanding test coverage, finding bugs, and quality patterns.",
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ query: {
555
+ type: "string",
556
+ description: "What tests, bugs, or quality aspects are you looking for?"
557
+ },
558
+ limit: {
559
+ type: "number",
560
+ description: "Maximum results (default: 10)",
561
+ default: 10
562
+ }
563
+ },
564
+ required: ["query"]
565
+ }
566
+ },
567
+ {
568
+ name: "search_as_docs",
569
+ description: "Search as a Documentation Specialist - focuses on Confluence documentation, guides, and explanations. Use for onboarding, tutorials, and understanding processes.",
570
+ inputSchema: {
571
+ type: "object",
572
+ properties: {
573
+ query: {
574
+ type: "string",
575
+ description: "What documentation or guide are you looking for?"
576
+ },
577
+ limit: {
578
+ type: "number",
579
+ description: "Maximum results (default: 10)",
580
+ default: 10
581
+ }
582
+ },
583
+ required: ["query"]
584
+ }
585
+ },
586
+ {
587
+ name: "search_knowledge",
588
+ description: "Search across ALL knowledge sources with intelligent weighting. Code has higher weight than documentation. Use for general questions about the domain.",
589
+ inputSchema: {
590
+ type: "object",
591
+ properties: {
592
+ query: {
593
+ type: "string",
594
+ description: "What are you looking for?"
595
+ },
596
+ limit: {
597
+ type: "number",
598
+ description: "Maximum results (default: 15)",
599
+ default: 15
600
+ }
601
+ },
602
+ required: ["query"]
603
+ }
604
+ },
605
+ // Legacy tools
606
+ {
607
+ name: "search_code",
608
+ description: "Search for code snippets only. Use search_as_architect for better results.",
609
+ inputSchema: {
610
+ type: "object",
611
+ properties: {
612
+ query: { type: "string", description: "Search query" },
613
+ limit: { type: "number", default: 5 },
614
+ repo: { type: "string", description: "Filter by repo" }
615
+ },
616
+ required: ["query"]
617
+ }
618
+ },
619
+ {
620
+ name: "search_jira",
621
+ description: "Search Jira issues only. Use search_as_analyst for better results.",
622
+ inputSchema: {
623
+ type: "object",
624
+ properties: {
625
+ query: { type: "string", description: "Search query" },
626
+ limit: { type: "number", default: 10 }
627
+ },
628
+ required: ["query"]
629
+ }
630
+ },
631
+ {
632
+ name: "search_git_commits",
633
+ description: "Search git commits with Jira correlation.",
634
+ inputSchema: {
635
+ type: "object",
636
+ properties: {
637
+ query: { type: "string", description: "Search query" },
638
+ limit: { type: "number", default: 10 }
639
+ },
640
+ required: ["query"]
641
+ }
642
+ },
643
+ {
644
+ name: "add_domain_term",
645
+ description: "Add a new domain term to the glossary.",
646
+ inputSchema: {
647
+ type: "object",
648
+ properties: {
649
+ term: { type: "string", description: "The term to define" },
650
+ definition: { type: "string", description: "Definition" },
651
+ aliases: { type: "array", items: { type: "string" } }
652
+ },
653
+ required: ["term", "definition"]
654
+ }
655
+ },
656
+ {
657
+ name: "add_decision",
658
+ description: "Record an architectural decision.",
659
+ inputSchema: {
660
+ type: "object",
661
+ properties: {
662
+ title: { type: "string" },
663
+ context: { type: "string" },
664
+ decision: { type: "string" },
665
+ reasoning: { type: "string" },
666
+ jira_keys: { type: "array", items: { type: "string" } }
667
+ },
668
+ required: ["title", "context", "decision"]
669
+ }
670
+ },
671
+ {
672
+ name: "get_index_stats",
673
+ description: "Get statistics about the RAG index.",
674
+ inputSchema: {
675
+ type: "object",
676
+ properties: {}
677
+ }
678
+ }
679
+ ];
680
+ async function handleSearchAsAnalyst(args) {
681
+ const results = await searchWithWeights({
682
+ query: args.query,
683
+ limit: args.limit || 10,
684
+ sourceTypes: ["jira", "confluence"]
685
+ });
686
+ return formatResults(results, "Business Analyst Search");
687
+ }
688
+ async function handleSearchAsArchitect(args) {
689
+ let results = await searchWithWeights({
690
+ query: args.query,
691
+ limit: (args.limit || 10) * 2,
692
+ sourceTypes: ["code", "git_commit"]
693
+ });
694
+ if (args.repo) {
695
+ results = results.filter((r) => {
696
+ const meta = r.metadata;
697
+ return meta.repo?.toLowerCase().includes(args.repo.toLowerCase());
698
+ });
699
+ }
700
+ return formatResults(results.slice(0, args.limit || 10), "Software Architect Search");
701
+ }
702
+ async function handleSearchAsQA(args) {
703
+ const testQuery = `${args.query} test spec bug`;
704
+ const results = await searchWithWeights({
705
+ query: testQuery,
706
+ limit: (args.limit || 10) * 2,
707
+ sourceTypes: ["code", "jira"]
708
+ });
709
+ const prioritized = results.filter((r) => {
710
+ const isTest = r.source_id.toLowerCase().includes("test") || r.source_id.toLowerCase().includes("spec") || r.content.toLowerCase().includes("describe(") || r.content.toLowerCase().includes("[test]");
711
+ const isBug = r.metadata.issue_type?.toString().toLowerCase().includes("bug");
712
+ return isTest || isBug;
713
+ });
714
+ const others = results.filter((r) => !prioritized.includes(r));
715
+ const combined = [...prioritized, ...others].slice(0, args.limit || 10);
716
+ return formatResults(combined, "QA Engineer Search");
717
+ }
718
+ async function handleSearchAsDocs(args) {
719
+ const results = await searchWithWeights({
720
+ query: args.query,
721
+ limit: args.limit || 10,
722
+ sourceTypes: ["confluence"],
723
+ applyWeights: false
724
+ // No weighting for docs-only search
725
+ });
726
+ return formatResults(results, "Documentation Search");
727
+ }
728
+ async function handleSearchKnowledge(args) {
729
+ const results = await searchWithWeights({
730
+ query: args.query,
731
+ limit: args.limit || 15,
732
+ applyWeights: true
733
+ });
734
+ return formatResults(results, "Knowledge Base Search (Weighted)");
735
+ }
736
+ async function handleSearchCode(args) {
737
+ let results = await searchWithWeights({
738
+ query: args.query,
739
+ limit: args.limit || 5,
740
+ sourceTypes: ["code"]
741
+ });
742
+ if (args.repo) {
743
+ results = results.filter((r) => {
744
+ const meta = r.metadata;
745
+ return meta.repo?.toLowerCase().includes(args.repo.toLowerCase());
746
+ });
747
+ }
748
+ return formatResults(results, "Code Search");
749
+ }
750
+ async function handleSearchJira(args) {
751
+ const results = await searchWithWeights({
752
+ query: args.query,
753
+ limit: args.limit || 10,
754
+ sourceTypes: ["jira"]
755
+ });
756
+ return formatResults(results, "Jira Search");
757
+ }
758
+ async function handleSearchGitCommits(args) {
759
+ const results = await searchWithWeights({
760
+ query: args.query,
761
+ limit: args.limit || 10,
762
+ sourceTypes: ["git_commit"]
763
+ });
764
+ return formatResults(results, "Git Commits Search");
765
+ }
766
+ async function handleAddTerm(args) {
767
+ const embedding = await getEmbedding(`${args.term}: ${args.definition}`);
768
+ await qdrant.upsert(config.qdrant.collectionName, {
769
+ wait: true,
770
+ points: [{
771
+ id: uuidv4(),
772
+ vector: embedding,
773
+ payload: {
774
+ source_type: "domain_term",
775
+ source_id: `term:${args.term}`,
776
+ term: args.term,
777
+ definition: args.definition,
778
+ aliases: args.aliases || [],
779
+ content: `${args.term}: ${args.definition}`,
780
+ indexed_at: (/* @__PURE__ */ new Date()).toISOString()
781
+ }
782
+ }]
783
+ });
784
+ return `Term "${args.term}" added to glossary.`;
785
+ }
786
+ async function handleAddDecision(args) {
787
+ const text = `${args.title}. ${args.context}. ${args.decision}`;
788
+ const embedding = await getEmbedding(text);
789
+ await qdrant.upsert(config.qdrant.collectionName, {
790
+ wait: true,
791
+ points: [{
792
+ id: uuidv4(),
793
+ vector: embedding,
794
+ payload: {
795
+ source_type: "decision",
796
+ source_id: `decision:${Date.now()}`,
797
+ title: args.title,
798
+ context: args.context,
799
+ decision: args.decision,
800
+ reasoning: args.reasoning || null,
801
+ jira_keys: args.jira_keys || [],
802
+ content: text,
803
+ indexed_at: (/* @__PURE__ */ new Date()).toISOString()
804
+ }
805
+ }]
806
+ });
807
+ return `Decision "${args.title}" recorded.`;
808
+ }
809
+ async function handleGetStats() {
810
+ let response = "## RAG Index Statistics\n\n";
811
+ try {
812
+ const collectionInfo = await qdrant.getCollection(config.qdrant.collectionName);
813
+ response += `**Total documents:** ${collectionInfo.points_count}
814
+
815
+ `;
816
+ const sourceTypes = ["code", "confluence", "jira", "git_commit", "domain_term", "decision"];
817
+ response += "**By source type:**\n";
818
+ response += "| Type | Count | Weight |\n";
819
+ response += "|------|-------|--------|\n";
820
+ for (const sourceType of sourceTypes) {
821
+ const count = await qdrant.count(config.qdrant.collectionName, {
822
+ filter: { must: [{ key: "source_type", match: { value: sourceType } }] },
823
+ exact: true
824
+ });
825
+ if (count.count > 0) {
826
+ const weight = SOURCE_WEIGHTS[sourceType] || 1;
827
+ response += `| ${sourceType} | ${count.count} | ${weight}x |
828
+ `;
829
+ }
830
+ }
831
+ } catch {
832
+ response += "Collection not initialized. Run indexation first.";
833
+ }
834
+ return response;
835
+ }
836
+ async function main() {
837
+ await ensureCollection();
838
+ const server = new Server(
839
+ { name: "domain-rag-server", version: "2.1.0" },
840
+ { capabilities: { tools: {} } }
841
+ );
842
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
843
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
844
+ const { name, arguments: args } = request.params;
845
+ try {
846
+ let result;
847
+ switch (name) {
848
+ case "search_as_analyst":
849
+ result = await handleSearchAsAnalyst(args);
850
+ break;
851
+ case "search_as_architect":
852
+ result = await handleSearchAsArchitect(args);
853
+ break;
854
+ case "search_as_qa":
855
+ result = await handleSearchAsQA(args);
856
+ break;
857
+ case "search_as_docs":
858
+ result = await handleSearchAsDocs(args);
859
+ break;
860
+ case "search_knowledge":
861
+ result = await handleSearchKnowledge(args);
862
+ break;
863
+ case "search_code":
864
+ result = await handleSearchCode(args);
865
+ break;
866
+ case "search_jira":
867
+ result = await handleSearchJira(args);
868
+ break;
869
+ case "search_git_commits":
870
+ result = await handleSearchGitCommits(args);
871
+ break;
872
+ case "add_domain_term":
873
+ result = await handleAddTerm(args);
874
+ break;
875
+ case "add_decision":
876
+ result = await handleAddDecision(args);
877
+ break;
878
+ case "get_index_stats":
879
+ result = await handleGetStats();
880
+ break;
881
+ default:
882
+ throw new Error(`Unknown tool: ${name}`);
883
+ }
884
+ return { content: [{ type: "text", text: result }] };
885
+ } catch (error) {
886
+ const message = error instanceof Error ? error.message : "Unknown error";
887
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
888
+ }
889
+ });
890
+ const transport = new StdioServerTransport();
891
+ await server.connect(transport);
892
+ console.error(`Domain RAG MCP Server v2.1 started (embedding: ${embeddingProvider.name}, qdrant: ${config.qdrant.url})`);
893
+ }
894
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "domain-rag-mcp-server",
3
+ "version": "2.1.0",
4
+ "description": "MCP server for domain RAG search — connects to Qdrant + Ollama/OpenAI for semantic search across Jira, Confluence, Git commits, and code",
5
+ "type": "module",
6
+ "main": "dist/index.mjs",
7
+ "bin": {
8
+ "domain-rag-mcp-server": "dist/index.mjs"
9
+ },
10
+ "files": [
11
+ "dist/index.mjs"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "build:npm": "node build.mjs",
16
+ "start": "node dist/index.mjs",
17
+ "dev": "tsx src/index.ts"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "rag",
22
+ "qdrant",
23
+ "jira",
24
+ "confluence",
25
+ "git",
26
+ "semantic-search",
27
+ "claude",
28
+ "cursor"
29
+ ],
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0",
32
+ "@qdrant/js-client-rest": "^1.7.0",
33
+ "dotenv": "^16.3.1",
34
+ "uuid": "^9.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "@domain-rag/shared": "file:../shared",
38
+ "@types/node": "^20.10.0",
39
+ "@types/uuid": "^9.0.7",
40
+ "esbuild": "^0.20.2",
41
+ "tsx": "^4.7.0",
42
+ "typescript": "^5.3.3"
43
+ }
44
+ }