@retrivora-ai/rag-engine 1.9.7 → 1.9.9

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 (60) hide show
  1. package/README.md +136 -136
  2. package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
  3. package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2327 -474
  7. package/dist/handlers/index.mjs +2329 -473
  8. package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
  9. package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
  10. package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
  11. package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
  12. package/dist/index.css +783 -550
  13. package/dist/index.d.mts +35 -7
  14. package/dist/index.d.ts +35 -7
  15. package/dist/index.js +419 -282
  16. package/dist/index.mjs +426 -292
  17. package/dist/server.d.mts +65 -6
  18. package/dist/server.d.ts +65 -6
  19. package/dist/server.js +2185 -317
  20. package/dist/server.mjs +2185 -317
  21. package/package.json +13 -8
  22. package/src/app/constants.tsx +37 -7
  23. package/src/app/page.tsx +2 -0
  24. package/src/components/ChatWidget.tsx +2 -1
  25. package/src/components/ChatWindow.tsx +4 -1
  26. package/src/components/CodeViewer.tsx +19 -14
  27. package/src/components/MarkdownComponents.tsx +44 -1
  28. package/src/components/MessageBubble.tsx +162 -50
  29. package/src/components/constants.tsx +228 -0
  30. package/src/config/RagConfig.ts +48 -2
  31. package/src/config/constants.ts +4 -0
  32. package/src/config/serverConfig.ts +15 -0
  33. package/src/core/ConfigResolver.ts +38 -6
  34. package/src/core/DatabaseStorage.ts +469 -0
  35. package/src/core/LicenseVerifier.ts +260 -0
  36. package/src/core/MultiAgentCoordinator.ts +239 -0
  37. package/src/core/Pipeline.ts +151 -18
  38. package/src/core/Retrivora.ts +7 -0
  39. package/src/core/VectorPlugin.ts +12 -3
  40. package/src/core/mcp.ts +261 -0
  41. package/src/handlers/index.ts +449 -63
  42. package/src/hooks/useRagChat.ts +96 -42
  43. package/src/hooks/useStoredMessages.ts +15 -4
  44. package/src/index.ts +3 -0
  45. package/src/llm/LLMFactory.ts +9 -1
  46. package/src/llm/providers/GroqProvider.ts +176 -0
  47. package/src/llm/providers/QwenProvider.ts +191 -0
  48. package/src/server.ts +7 -0
  49. package/src/types/chat.ts +14 -0
  50. package/src/types/props.ts +12 -0
  51. package/.env.example +0 -80
  52. package/LICENSE.txt +0 -21
  53. package/src/components/AmbientBackground.tsx +0 -29
  54. package/src/components/ArchitectureCard.tsx +0 -17
  55. package/src/components/ArchitectureCardsSection.tsx +0 -15
  56. package/src/components/DocViewer.tsx +0 -103
  57. package/src/components/Documentation.tsx +0 -121
  58. package/src/components/Hero.tsx +0 -59
  59. package/src/components/Lifecycle.tsx +0 -37
  60. package/src/components/Navbar.tsx +0 -55
@@ -9,6 +9,8 @@ import { Reranker } from '../rag/Reranker';
9
9
  import { LlamaIndexIngestor } from '../rag/LlamaIndexIngestor';
10
10
  import { LangChainAgent } from './LangChainAgent';
11
11
  import { RagConfig } from '../config/RagConfig';
12
+ import { MCPRegistry } from './mcp';
13
+ import { MultiAgentCoordinator } from './MultiAgentCoordinator';
12
14
  import { ProviderRegistry } from './ProviderRegistry';
13
15
  import { BatchProcessor, BatchOptions } from './BatchProcessor';
14
16
  import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
@@ -165,9 +167,13 @@ export class Pipeline {
165
167
  private entityExtractor?: EntityExtractor;
166
168
  private reranker: Reranker;
167
169
  private agent?: LangChainAgent;
170
+ private mcpRegistry?: MCPRegistry;
171
+ private multiAgentCoordinator?: MultiAgentCoordinator;
168
172
  /** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
169
173
  private embeddingCache = new LRUEmbeddingCache(500);
170
174
  private initialised = false;
175
+ /** Namespace-specific static cold context cache for CAG */
176
+ private coldContexts = new Map<string, string>();
171
177
 
172
178
  constructor(private config: RagConfig) {
173
179
  this.chunker = new DocumentChunker(
@@ -250,14 +256,52 @@ You are a helpful assistant. Use the provided context to answer questions accura
250
256
 
251
257
  await this.vectorDB.initialize();
252
258
 
253
- if (this.config.rag?.architecture === 'agentic') {
259
+ if (this.config.rag?.architecture === 'agentic' || this.config.rag?.architecture === 'multi-agent' || (this.config.mcpServers && this.config.mcpServers.length > 0)) {
260
+ this.mcpRegistry = new MCPRegistry(this.config.mcpServers || []);
261
+ this.multiAgentCoordinator = new MultiAgentCoordinator({
262
+ llmProvider: this.llmProvider,
263
+ mcpRegistry: this.mcpRegistry,
264
+ documentSearch: async (query: string) => {
265
+ return this.runNormalQuery(query);
266
+ }
267
+ });
254
268
  this.agent = new LangChainAgent(this, this.config);
255
269
  await this.agent.initialize(this.llmProvider as unknown);
256
270
  }
257
271
 
272
+ if (this.config.rag?.cag?.enabled) {
273
+ await this.loadColdContext(this.config.projectId);
274
+ }
275
+
258
276
  this.initialised = true;
259
277
  }
260
278
 
279
+ private async loadColdContext(ns: string): Promise<void> {
280
+ const cagConfig = this.config.rag?.cag;
281
+ if (!cagConfig || !cagConfig.enabled) return;
282
+
283
+ try {
284
+ const coldNs = cagConfig.coldNamespace || ns;
285
+ const limit = cagConfig.coldLimit ?? 20;
286
+
287
+ const queryText = cagConfig.coldQuery || 'documentation';
288
+ const queryVector = await this.embeddingProvider.embed(queryText, { taskType: 'query' });
289
+ const coldMatches = await this.vectorDB.query(queryVector, limit, coldNs);
290
+
291
+ if (coldMatches.length > 0) {
292
+ const formatted = coldMatches.map((m, i) => `[Cold Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n');
293
+ this.coldContexts.set(ns, formatted);
294
+ console.log(`[Pipeline] Cold RAG (CAG) loaded ${coldMatches.length} documents into cached context for namespace: ${ns}`);
295
+ } else {
296
+ this.coldContexts.set(ns, 'No cold context found.');
297
+ console.warn(`[Pipeline] Cold RAG (CAG) initialized but no documents were found in namespace: ${coldNs}`);
298
+ }
299
+ } catch (err) {
300
+ console.error(`[Pipeline] Failed to load Cold RAG (CAG) context for namespace ${ns}:`, err);
301
+ this.coldContexts.set(ns, 'Failed to load cold context.');
302
+ }
303
+ }
304
+
261
305
  /**
262
306
  * Ingest documents with automatic chunking, embedding, and batch upsert.
263
307
  */
@@ -386,12 +430,26 @@ You are a helpful assistant. Use the provided context to answer questions accura
386
430
  );
387
431
  }
388
432
 
433
+ async runNormalQuery(question: string, history: ChatMessage[] = [], namespace?: string): Promise<{ reply: string; sources: any[] }> {
434
+ const stream = this.askStreamInternal(question, history, namespace);
435
+ let reply = '';
436
+ let sources: any[] = [];
437
+ for await (const chunk of stream) {
438
+ if (typeof chunk === 'string') {
439
+ reply += chunk;
440
+ } else if (typeof chunk === 'object' && chunk !== null) {
441
+ if ('reply' in chunk && chunk.reply) reply += chunk.reply;
442
+ if ('sources' in chunk && chunk.sources) sources = chunk.sources;
443
+ }
444
+ }
445
+ return { reply, sources };
446
+ }
447
+
389
448
  async ask(question: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
390
449
  await this.initialize();
391
450
 
392
- if (this.config.rag?.architecture === 'agentic' && this.agent) {
393
- const agentReply = await this.agent.run(question, history);
394
- return { reply: agentReply, sources: [] };
451
+ if ((this.config.rag?.architecture === 'agentic' || this.config.rag?.architecture === 'multi-agent' || (this.config.mcpServers && this.config.mcpServers.length > 0)) && this.multiAgentCoordinator) {
452
+ return await this.multiAgentCoordinator.run(question, history);
395
453
  }
396
454
 
397
455
  const stream = this.askStream(question, history, namespace);
@@ -415,6 +473,21 @@ You are a helpful assistant. Use the provided context to answer questions accura
415
473
  return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
416
474
  }
417
475
 
476
+ async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
477
+ await this.initialize();
478
+ if ((this.config.rag?.architecture === 'agentic' || this.config.rag?.architecture === 'multi-agent' || (this.config.mcpServers && this.config.mcpServers.length > 0)) && this.multiAgentCoordinator) {
479
+ const stream = this.multiAgentCoordinator.runStream(question, history);
480
+ for await (const chunk of stream) {
481
+ yield chunk;
482
+ }
483
+ return;
484
+ }
485
+ const stream = this.askStreamInternal(question, history, namespace);
486
+ for await (const chunk of stream) {
487
+ yield chunk;
488
+ }
489
+ }
490
+
418
491
  /**
419
492
  * High-performance streaming RAG flow.
420
493
  * Yields text chunks first, then the retrieval metadata + observability trace at the end.
@@ -425,7 +498,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
425
498
  * - UITransformation is computed after text streaming and emitted with metadata
426
499
  * - SchemaMapper.train runs while answer generation streams
427
500
  */
428
- async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
501
+ async *askStreamInternal(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
429
502
  await this.initialize();
430
503
  const ns = namespace ?? this.config.projectId;
431
504
  const topK = this.config.rag?.topK ?? 5;
@@ -462,7 +535,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
462
535
  const cacheKey = `${ns}::${searchQuery}`;
463
536
  const cachedVector = this.embeddingCache.get(cacheKey);
464
537
 
465
- const useGraph = this.graphDB && this.config.rag?.useGraphRetrieval;
538
+ const arch = this.config.rag?.architecture;
539
+ const useGraph = arch !== 'naive' && arch !== 'retrieve-and-rerank' && this.graphDB && this.config.rag?.useGraphRetrieval;
466
540
  const [strategyResult, embeddedVector] = await Promise.all([
467
541
  QueryProcessor.determineRetrievalStrategy(
468
542
  searchQuery,
@@ -506,7 +580,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
506
580
  ? structuredSources
507
581
  : structuredSources.filter((m) => m.score >= scoreThreshold);
508
582
  const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
509
- if (!hasNumericPredicates && this.config.rag?.useReranking) {
583
+ const useReranking = arch !== 'naive' && (arch === 'retrieve-and-rerank' || this.config.rag?.useReranking);
584
+ if (!hasNumericPredicates && useReranking) {
510
585
  fullSources = await this.reranker.rerank(fullSources, question, rerankLimit);
511
586
  } else if (!wantsExhaustiveList) {
512
587
  fullSources = fullSources.slice(0, topK);
@@ -547,6 +622,28 @@ You are a helpful assistant. Use the provided context to answer questions accura
547
622
  context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
548
623
  }
549
624
 
625
+ if (this.config.rag?.cag?.enabled) {
626
+ if (!this.coldContexts.has(ns)) {
627
+ await this.loadColdContext(ns);
628
+ }
629
+ const coldContext = this.coldContexts.get(ns);
630
+ if (coldContext && coldContext !== 'No cold context found.' && coldContext !== 'Failed to load cold context.') {
631
+ context = `COLD CONTEXT (STATIC KNOWLEDGE):\n${coldContext}\n\nRETRIEVED HOT CONTEXT (REAL-TIME KNOWLEDGE):\n${context}`;
632
+ }
633
+ }
634
+
635
+ // Yield sources immediately to support early UI citation rendering (highly responsive)
636
+ yield {
637
+ reply: '',
638
+ sources: sources.map((s) => ({
639
+ id: s.id,
640
+ score: s.score,
641
+ content: s.content,
642
+ metadata: s.metadata ?? {},
643
+ namespace: ns,
644
+ } satisfies RetrievedChunk)),
645
+ } as ChatResponse;
646
+
550
647
  // 4.5 Schema Training — true fire-and-forget, never gates anything.
551
648
  const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
552
649
  const cachedSchema = allMetadataKeys.length > 0
@@ -735,7 +832,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
735
832
  const latency: LatencyBreakdown = {
736
833
  embedMs: Math.round(embedMs),
737
834
  retrieveMs: Math.round(retrieveMs),
738
- rerankMs: this.config.rag?.useReranking ? Math.round(rerankMs) : undefined,
835
+ rerankMs: (arch !== 'naive' && (arch === 'retrieve-and-rerank' || this.config.rag?.useReranking)) ? Math.round(rerankMs) : undefined,
739
836
  generateMs: Math.round(generateMs),
740
837
  totalMs: Math.round(totalMs),
741
838
  };
@@ -751,15 +848,15 @@ You are a helpful assistant. Use the provided context to answer questions accura
751
848
  estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
752
849
  };
753
850
 
754
- // 7. Await both UI transformation and hallucination score concurrently
755
- // before yielding the metadata frame. This guarantees the trace is always
756
- // complete when the consumer receives it — no post-yield mutation needed.
851
+ // 7. Await UI transformation and optionally hallucination score before yielding the metadata frame.
852
+ const awaitHallucination = this.config.llm.options?.awaitHallucination === true;
853
+
757
854
  const [uiTransformation, hallucinationResult] = await Promise.all([
758
855
  uiTransformationPromise,
759
- hallucinationScoringPromise,
856
+ awaitHallucination ? hallucinationScoringPromise : Promise.resolve(undefined),
760
857
  ]);
761
858
 
762
- const trace: ObservabilityTrace = {
859
+ const buildTrace = (hScore?: { score: number; reason: string }): ObservabilityTrace => ({
763
860
  requestId,
764
861
  query: question,
765
862
  rewrittenQuery,
@@ -775,12 +872,48 @@ You are a helpful assistant. Use the provided context to answer questions accura
775
872
  latency,
776
873
  tokens,
777
874
  timestamp: new Date().toISOString(),
778
- // Hallucination score is now always present when available
779
- ...(hallucinationResult ? {
780
- hallucinationScore: hallucinationResult.score,
781
- hallucinationReason: hallucinationResult.reason,
875
+ ...(hScore ? {
876
+ hallucinationScore: hScore.score,
877
+ hallucinationReason: hScore.reason,
782
878
  } : {}),
783
- };
879
+ });
880
+
881
+ const trace = buildTrace(hallucinationResult);
882
+
883
+ // Asynchronously trigger telemetry logging without blocking response yield
884
+ if (this.config.telemetry?.enabled) {
885
+ const telemetryUrl = this.config.telemetry.url || '/api/telemetry';
886
+ const absoluteUrl = telemetryUrl.startsWith('http')
887
+ ? telemetryUrl
888
+ : (process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`) + telemetryUrl;
889
+
890
+ // Start background process for telemetry, awaiting hallucination scoring if it was backgrounded
891
+ (async () => {
892
+ try {
893
+ let finalTrace = trace;
894
+ if (!awaitHallucination && runHallucination) {
895
+ const backgroundScoreResult = await hallucinationScoringPromise;
896
+ if (backgroundScoreResult) {
897
+ finalTrace = buildTrace(backgroundScoreResult);
898
+ }
899
+ }
900
+
901
+ await fetch(absoluteUrl, {
902
+ method: 'POST',
903
+ headers: {
904
+ 'Content-Type': 'application/json',
905
+ },
906
+ body: JSON.stringify({
907
+ trace: finalTrace,
908
+ licenseKey: this.config.licenseKey,
909
+ projectId: ns
910
+ })
911
+ });
912
+ } catch (err: any) {
913
+ console.warn(`[Pipeline Telemetry] Failed to send trace async to ${absoluteUrl}:`, err.message);
914
+ }
915
+ })();
916
+ }
784
917
 
785
918
  yield {
786
919
  reply: '',
@@ -2,6 +2,7 @@ import { RagConfig, RetrievalConfig, UniversalRagConfig } from '../config/RagCon
2
2
  import { ChatMessage, ChatResponse, IngestDocument } from '../types';
3
3
  import { ConfigResolver } from './ConfigResolver';
4
4
  import { ConfigValidator } from './ConfigValidator';
5
+ import { LicenseVerifier } from './LicenseVerifier';
5
6
  import { Pipeline } from './Pipeline';
6
7
  import { wrapError, RetrivoraErrorCode } from '../exceptions';
7
8
 
@@ -23,6 +24,12 @@ export class Retrivora {
23
24
  async initialize(): Promise<void> {
24
25
  try {
25
26
  await ConfigValidator.validateAndThrow(this.config);
27
+ LicenseVerifier.verify(
28
+ this.config.licenseKey,
29
+ this.config.projectId,
30
+ this.config.vectorDb?.provider
31
+ );
32
+ await LicenseVerifier.verifyIP(this.config.licenseKey);
26
33
  } catch (err) {
27
34
  throw wrapError(err, 'CONFIGURATION_ERROR');
28
35
  }
@@ -5,6 +5,7 @@ import { Pipeline } from './Pipeline';
5
5
  import { ProviderHealthCheck, HealthCheckResult } from './ProviderHealthCheck';
6
6
  import { IngestDocument, ChatResponse } from '../types';
7
7
  import { ChatMessage } from '../types';
8
+ import { LicenseVerifier } from './LicenseVerifier';
8
9
  import { wrapError, RetrivoraErrorCode } from '../exceptions';
9
10
 
10
11
  /**
@@ -24,14 +25,22 @@ export class VectorPlugin {
24
25
  private validationPromise: Promise<void>;
25
26
 
26
27
  constructor(hostConfig?: Partial<RagConfig>) {
27
- this.config = ConfigResolver.resolve(hostConfig);
28
+ const resolvedConfig = ConfigResolver.resolve(hostConfig);
29
+ this.config = resolvedConfig;
28
30
 
29
31
  // Start validation early
30
- this.validationPromise = ConfigValidator.validateAndThrow(this.config);
32
+ this.validationPromise = (async () => {
33
+ await ConfigValidator.validateAndThrow(resolvedConfig);
34
+ LicenseVerifier.verify(
35
+ resolvedConfig.licenseKey,
36
+ resolvedConfig.projectId,
37
+ resolvedConfig.vectorDb?.provider
38
+ );
39
+ })();
31
40
  // Prevent unhandled promise rejection warning
32
41
  this.validationPromise.catch(() => {});
33
42
 
34
- this.pipeline = new Pipeline(this.config);
43
+ this.pipeline = new Pipeline(resolvedConfig);
35
44
  }
36
45
 
37
46
  /**
@@ -0,0 +1,261 @@
1
+ import { spawn, ChildProcess } from 'child_process';
2
+ import { MCPServerConfig } from '../config/RagConfig';
3
+
4
+ interface MCPTool {
5
+ name: string;
6
+ description?: string;
7
+ inputSchema: any;
8
+ }
9
+
10
+ interface MCPCallResult {
11
+ content: Array<{ type: string; text?: string; [key: string]: any }>;
12
+ isError?: boolean;
13
+ }
14
+
15
+ export class MCPClient {
16
+ private childProcess?: ChildProcess;
17
+ private sseUrl?: string;
18
+ private messageIdCounter = 0;
19
+ private pendingRequests = new Map<number, { resolve: (val: any) => void; reject: (err: any) => void }>();
20
+ private stdioBuffer = '';
21
+ private initialized = false;
22
+
23
+ constructor(private config: MCPServerConfig) {}
24
+
25
+ private getNextMessageId(): number {
26
+ return ++this.messageIdCounter;
27
+ }
28
+
29
+ /**
30
+ * Connect and initialize the MCP server connection.
31
+ */
32
+ async connect(): Promise<void> {
33
+ if (this.initialized) return;
34
+
35
+ if (this.config.transport === 'stdio') {
36
+ await this.connectStdio();
37
+ } else {
38
+ await this.connectSSE();
39
+ }
40
+
41
+ // Call initialize
42
+ try {
43
+ const response = await this.sendJsonRpc('initialize', {
44
+ protocolVersion: '2024-11-05',
45
+ capabilities: {},
46
+ clientInfo: {
47
+ name: 'retrivora-sdk',
48
+ version: '1.0.0',
49
+ },
50
+ });
51
+
52
+ // Send initialized notification
53
+ await this.sendNotification('notifications/initialized');
54
+ this.initialized = true;
55
+ console.log(`[MCPClient] Connected and initialized server "${this.config.name}" successfully.`);
56
+ } catch (err: any) {
57
+ this.disconnect();
58
+ throw new Error(`[MCPClient] Failed to initialize server "${this.config.name}": ${err.message}`);
59
+ }
60
+ }
61
+
62
+ private async connectStdio(): Promise<void> {
63
+ const cmd = this.config.command;
64
+ if (!cmd) {
65
+ throw new Error(`[MCPClient] Command option is required for stdio transport on "${this.config.name}"`);
66
+ }
67
+
68
+ const args = this.config.args || [];
69
+ const env = { ...process.env, ...(this.config.env || {}) };
70
+
71
+ this.childProcess = spawn(cmd, args, { env, shell: true });
72
+
73
+ if (!this.childProcess || !this.childProcess.stdout || !this.childProcess.stdin) {
74
+ throw new Error(`[MCPClient] Failed to spawn stdio child process for "${this.config.name}"`);
75
+ }
76
+
77
+ this.childProcess.stdout.on('data', (data: Buffer) => {
78
+ this.stdioBuffer += data.toString('utf-8');
79
+ this.processStdioLines();
80
+ });
81
+
82
+ this.childProcess.stderr?.on('data', (data: Buffer) => {
83
+ console.warn(`[MCPClient] [stderr] [${this.config.name}]:`, data.toString('utf-8'));
84
+ });
85
+
86
+ this.childProcess.on('close', (code) => {
87
+ console.log(`[MCPClient] Connection closed for "${this.config.name}" with exit code: ${code}`);
88
+ this.disconnect();
89
+ });
90
+
91
+ // Wait a brief tick to ensure stdout is set up
92
+ await new Promise((resolve) => setTimeout(resolve, 100));
93
+ }
94
+
95
+ private processStdioLines() {
96
+ let newlineIndex: number;
97
+ while ((newlineIndex = this.stdioBuffer.indexOf('\n')) !== -1) {
98
+ const line = this.stdioBuffer.substring(0, newlineIndex).trim();
99
+ this.stdioBuffer = this.stdioBuffer.substring(newlineIndex + 1);
100
+
101
+ if (!line) continue;
102
+
103
+ try {
104
+ const payload = JSON.parse(line);
105
+ if (payload.id !== undefined) {
106
+ const req = this.pendingRequests.get(Number(payload.id));
107
+ if (req) {
108
+ this.pendingRequests.delete(Number(payload.id));
109
+ if (payload.error) {
110
+ req.reject(new Error(payload.error.message || JSON.stringify(payload.error)));
111
+ } else {
112
+ req.resolve(payload.result);
113
+ }
114
+ }
115
+ }
116
+ } catch (e) {
117
+ console.warn(`[MCPClient] Error parsing stdio JSON-RPC line from "${this.config.name}":`, e, 'Line content:', line);
118
+ }
119
+ }
120
+ }
121
+
122
+ private async connectSSE(): Promise<void> {
123
+ if (!this.config.url) {
124
+ throw new Error(`[MCPClient] URL is required for sse transport on "${this.config.name}"`);
125
+ }
126
+
127
+ // Set up standard SSE handshake / SSE connection
128
+ // In serverless contexts, we pull initial SSE transport configuration,
129
+ // and route HTTP POSTs to the client URL.
130
+ this.sseUrl = this.config.url;
131
+ }
132
+
133
+ private async sendJsonRpc(method: string, params: any): Promise<any> {
134
+ const id = this.getNextMessageId();
135
+ const request = {
136
+ jsonrpc: '2.0',
137
+ id,
138
+ method,
139
+ params,
140
+ };
141
+
142
+ if (this.config.transport === 'stdio') {
143
+ if (!this.childProcess || !this.childProcess.stdin) {
144
+ throw new Error(`[MCPClient] Server "${this.config.name}" is not connected (stdio process missing)`);
145
+ }
146
+ return new Promise((resolve, reject) => {
147
+ this.pendingRequests.set(id, { resolve, reject });
148
+ this.childProcess!.stdin!.write(JSON.stringify(request) + '\n', 'utf-8');
149
+ });
150
+ } else {
151
+ // SSE / HTTP POST fallback for cloud/serverless setups
152
+ if (!this.sseUrl) {
153
+ throw new Error(`[MCPClient] Server "${this.config.name}" SSE URL is not initialized`);
154
+ }
155
+ const response = await fetch(this.sseUrl, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify(request),
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`SSE JSON-RPC POST failed with status ${response.status}`);
162
+ }
163
+ const payload = await response.json();
164
+ if (payload.error) {
165
+ throw new Error(payload.error.message || JSON.stringify(payload.error));
166
+ }
167
+ return payload.result;
168
+ }
169
+ }
170
+
171
+ private async sendNotification(method: string, params?: any): Promise<void> {
172
+ const notification = {
173
+ jsonrpc: '2.0',
174
+ method,
175
+ params,
176
+ };
177
+
178
+ if (this.config.transport === 'stdio') {
179
+ if (this.childProcess?.stdin) {
180
+ this.childProcess.stdin.write(JSON.stringify(notification) + '\n', 'utf-8');
181
+ }
182
+ } else if (this.sseUrl) {
183
+ await fetch(this.sseUrl, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify(notification),
187
+ }).catch(err => console.warn('[MCPClient] Failed to send notification over SSE:', err));
188
+ }
189
+ }
190
+
191
+ /**
192
+ * List all tools offered by the MCP server.
193
+ */
194
+ async listTools(): Promise<MCPTool[]> {
195
+ await this.connect();
196
+ const result = await this.sendJsonRpc('tools/list', {});
197
+ return result?.tools || [];
198
+ }
199
+
200
+ /**
201
+ * Execute a tool on the MCP server.
202
+ */
203
+ async callTool(name: string, args: Record<string, any> = {}): Promise<MCPCallResult> {
204
+ await this.connect();
205
+ return await this.sendJsonRpc('tools/call', { name, arguments: args });
206
+ }
207
+
208
+ disconnect() {
209
+ this.initialized = false;
210
+ this.pendingRequests.forEach(req => req.reject(new Error('MCPClient disconnected')));
211
+ this.pendingRequests.clear();
212
+
213
+ if (this.childProcess) {
214
+ try {
215
+ this.childProcess.kill();
216
+ } catch { /* silent */ }
217
+ this.childProcess = undefined;
218
+ }
219
+ }
220
+ }
221
+
222
+ export class MCPRegistry {
223
+ private clients: MCPClient[] = [];
224
+
225
+ constructor(servers: MCPServerConfig[] = []) {
226
+ this.clients = servers.map(cfg => new MCPClient(cfg));
227
+ }
228
+
229
+ async getAllTools(): Promise<Array<{ serverName: string; tool: MCPTool }>> {
230
+ const list: Array<{ serverName: string; tool: MCPTool }> = [];
231
+ await Promise.all(
232
+ this.clients.map(async (client) => {
233
+ try {
234
+ const tools = await client.listTools();
235
+ tools.forEach(t => list.push({ serverName: client['config'].name, tool: t }));
236
+ } catch (e) {
237
+ console.warn(`[MCPRegistry] Failed to fetch tools from server "${client['config'].name}":`, e);
238
+ }
239
+ })
240
+ );
241
+ return list;
242
+ }
243
+
244
+ async callTool(toolName: string, args: Record<string, any> = {}): Promise<MCPCallResult> {
245
+ // Find client hosting this tool. (In standard MCP, tool names are unique per server block)
246
+ // We will search across all connected clients.
247
+ for (const client of this.clients) {
248
+ try {
249
+ const tools = await client.listTools();
250
+ if (tools.some(t => t.name === toolName)) {
251
+ return await client.callTool(toolName, args);
252
+ }
253
+ } catch { /* silent fallback */ }
254
+ }
255
+ throw new Error(`[MCPRegistry] Tool "${toolName}" not found on any registered MCP server.`);
256
+ }
257
+
258
+ disconnectAll() {
259
+ this.clients.forEach(c => c.disconnect());
260
+ }
261
+ }