@juspay/neurolink 7.13.0 → 7.14.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 (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +89 -25
  3. package/dist/config/conversationMemoryConfig.js +2 -1
  4. package/dist/context/ContextManager.js +15 -4
  5. package/dist/context/config.js +5 -1
  6. package/dist/context/utils.js +1 -1
  7. package/dist/core/baseProvider.d.ts +16 -1
  8. package/dist/core/baseProvider.js +208 -9
  9. package/dist/core/conversationMemoryManager.js +3 -2
  10. package/dist/core/factory.js +13 -2
  11. package/dist/factories/providerFactory.js +5 -11
  12. package/dist/factories/providerRegistry.js +2 -2
  13. package/dist/lib/config/conversationMemoryConfig.js +2 -1
  14. package/dist/lib/context/ContextManager.js +15 -4
  15. package/dist/lib/context/config.js +5 -1
  16. package/dist/lib/context/utils.js +1 -1
  17. package/dist/lib/core/baseProvider.d.ts +16 -1
  18. package/dist/lib/core/baseProvider.js +208 -9
  19. package/dist/lib/core/conversationMemoryManager.js +3 -2
  20. package/dist/lib/core/factory.js +13 -2
  21. package/dist/lib/factories/providerFactory.js +5 -11
  22. package/dist/lib/factories/providerRegistry.js +2 -2
  23. package/dist/lib/mcp/externalServerManager.d.ts +115 -0
  24. package/dist/lib/mcp/externalServerManager.js +677 -0
  25. package/dist/lib/mcp/mcpCircuitBreaker.d.ts +184 -0
  26. package/dist/lib/mcp/mcpCircuitBreaker.js +338 -0
  27. package/dist/lib/mcp/mcpClientFactory.d.ts +104 -0
  28. package/dist/lib/mcp/mcpClientFactory.js +416 -0
  29. package/dist/lib/mcp/toolDiscoveryService.d.ts +192 -0
  30. package/dist/lib/mcp/toolDiscoveryService.js +578 -0
  31. package/dist/lib/neurolink.d.ts +111 -16
  32. package/dist/lib/neurolink.js +517 -50
  33. package/dist/lib/providers/googleVertex.d.ts +1 -1
  34. package/dist/lib/providers/googleVertex.js +23 -7
  35. package/dist/lib/types/externalMcp.d.ts +282 -0
  36. package/dist/lib/types/externalMcp.js +6 -0
  37. package/dist/lib/types/generateTypes.d.ts +0 -1
  38. package/dist/lib/types/index.d.ts +1 -0
  39. package/dist/mcp/externalServerManager.d.ts +115 -0
  40. package/dist/mcp/externalServerManager.js +677 -0
  41. package/dist/mcp/mcpCircuitBreaker.d.ts +184 -0
  42. package/dist/mcp/mcpCircuitBreaker.js +338 -0
  43. package/dist/mcp/mcpClientFactory.d.ts +104 -0
  44. package/dist/mcp/mcpClientFactory.js +416 -0
  45. package/dist/mcp/toolDiscoveryService.d.ts +192 -0
  46. package/dist/mcp/toolDiscoveryService.js +578 -0
  47. package/dist/neurolink.d.ts +111 -16
  48. package/dist/neurolink.js +517 -50
  49. package/dist/providers/googleVertex.d.ts +1 -1
  50. package/dist/providers/googleVertex.js +23 -7
  51. package/dist/types/externalMcp.d.ts +282 -0
  52. package/dist/types/externalMcp.js +6 -0
  53. package/dist/types/generateTypes.d.ts +0 -1
  54. package/dist/types/index.d.ts +1 -0
  55. package/package.json +1 -1
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Tool Discovery Service
3
+ * Automatically discovers and registers tools from external MCP servers
4
+ * Handles tool validation, transformation, and lifecycle management
5
+ */
6
+ import { EventEmitter } from "events";
7
+ import { mcpLogger } from "../utils/logger.js";
8
+ import { globalCircuitBreakerManager, } from "./mcpCircuitBreaker.js";
9
+ /**
10
+ * ToolDiscoveryService
11
+ * Handles automatic tool discovery and registration from external MCP servers
12
+ */
13
+ export class ToolDiscoveryService extends EventEmitter {
14
+ toolRegistry = new Map();
15
+ serverTools = new Map();
16
+ discoveryInProgress = new Set();
17
+ constructor() {
18
+ super();
19
+ }
20
+ /**
21
+ * Discover tools from an external MCP server
22
+ */
23
+ async discoverTools(serverId, client, timeout = 10000) {
24
+ const startTime = Date.now();
25
+ try {
26
+ // Prevent concurrent discovery for same server
27
+ if (this.discoveryInProgress.has(serverId)) {
28
+ return {
29
+ success: false,
30
+ error: `Discovery already in progress for server: ${serverId}`,
31
+ toolCount: 0,
32
+ tools: [],
33
+ duration: Date.now() - startTime,
34
+ serverId,
35
+ };
36
+ }
37
+ this.discoveryInProgress.add(serverId);
38
+ mcpLogger.info(`[ToolDiscoveryService] Starting tool discovery for server: ${serverId}`);
39
+ // Create circuit breaker for tool discovery
40
+ const circuitBreaker = globalCircuitBreakerManager.getBreaker(`tool-discovery-${serverId}`, {
41
+ failureThreshold: 2,
42
+ resetTimeout: 60000,
43
+ operationTimeout: timeout,
44
+ });
45
+ // Discover tools with circuit breaker protection
46
+ const tools = await circuitBreaker.execute(async () => {
47
+ return await this.performToolDiscovery(serverId, client, timeout);
48
+ });
49
+ // Register discovered tools
50
+ const registeredTools = await this.registerDiscoveredTools(serverId, tools);
51
+ const result = {
52
+ success: true,
53
+ toolCount: registeredTools.length,
54
+ tools: registeredTools,
55
+ duration: Date.now() - startTime,
56
+ serverId,
57
+ };
58
+ // Emit discovery completed event
59
+ this.emit("discoveryCompleted", {
60
+ serverId,
61
+ toolCount: registeredTools.length,
62
+ duration: result.duration,
63
+ timestamp: new Date(),
64
+ });
65
+ mcpLogger.info(`[ToolDiscoveryService] Discovery completed for ${serverId}: ${registeredTools.length} tools`);
66
+ return result;
67
+ }
68
+ catch (error) {
69
+ const errorMessage = error instanceof Error ? error.message : String(error);
70
+ mcpLogger.error(`[ToolDiscoveryService] Discovery failed for ${serverId}:`, error);
71
+ // Emit discovery failed event
72
+ this.emit("discoveryFailed", {
73
+ serverId,
74
+ error: errorMessage,
75
+ timestamp: new Date(),
76
+ });
77
+ return {
78
+ success: false,
79
+ error: errorMessage,
80
+ toolCount: 0,
81
+ tools: [],
82
+ duration: Date.now() - startTime,
83
+ serverId,
84
+ };
85
+ }
86
+ finally {
87
+ this.discoveryInProgress.delete(serverId);
88
+ }
89
+ }
90
+ /**
91
+ * Perform the actual tool discovery
92
+ */
93
+ async performToolDiscovery(serverId, client, timeout) {
94
+ // List tools from the MCP server
95
+ const listToolsPromise = client.listTools();
96
+ const timeoutPromise = this.createTimeoutPromise(timeout, "Tool discovery timeout");
97
+ const result = await Promise.race([listToolsPromise, timeoutPromise]);
98
+ if (!result || !result.tools) {
99
+ throw new Error("No tools returned from server");
100
+ }
101
+ mcpLogger.debug(`[ToolDiscoveryService] Discovered ${result.tools.length} tools from ${serverId}`);
102
+ return result.tools;
103
+ }
104
+ /**
105
+ * Register discovered tools
106
+ */
107
+ async registerDiscoveredTools(serverId, tools) {
108
+ const registeredTools = [];
109
+ // Clear existing tools for this server
110
+ this.clearServerTools(serverId);
111
+ for (const tool of tools) {
112
+ try {
113
+ const toolInfo = await this.createToolInfo(serverId, tool);
114
+ const validation = this.validateTool(toolInfo);
115
+ if (!validation.isValid) {
116
+ mcpLogger.warn(`[ToolDiscoveryService] Skipping invalid tool ${tool.name} from ${serverId}:`, validation.errors);
117
+ continue;
118
+ }
119
+ // Apply validation metadata
120
+ if (validation.metadata) {
121
+ toolInfo.metadata = {
122
+ ...toolInfo.metadata,
123
+ ...validation.metadata,
124
+ };
125
+ }
126
+ // Register the tool
127
+ const toolKey = this.createToolKey(serverId, tool.name);
128
+ this.toolRegistry.set(toolKey, toolInfo);
129
+ // Track server tools
130
+ if (!this.serverTools.has(serverId)) {
131
+ this.serverTools.set(serverId, new Set());
132
+ }
133
+ this.serverTools.get(serverId).add(tool.name);
134
+ registeredTools.push(toolInfo);
135
+ // Emit tool registered event
136
+ this.emit("toolRegistered", {
137
+ serverId,
138
+ toolName: tool.name,
139
+ toolInfo,
140
+ timestamp: new Date(),
141
+ });
142
+ mcpLogger.debug(`[ToolDiscoveryService] Registered tool: ${tool.name} from ${serverId}`);
143
+ }
144
+ catch (error) {
145
+ mcpLogger.error(`[ToolDiscoveryService] Failed to register tool ${tool.name} from ${serverId}:`, error);
146
+ }
147
+ }
148
+ return registeredTools;
149
+ }
150
+ /**
151
+ * Create tool info from MCP tool definition
152
+ */
153
+ async createToolInfo(serverId, tool) {
154
+ return {
155
+ name: tool.name,
156
+ description: tool.description || "No description provided",
157
+ serverId,
158
+ inputSchema: tool.inputSchema,
159
+ isAvailable: true,
160
+ stats: {
161
+ totalCalls: 0,
162
+ successfulCalls: 0,
163
+ failedCalls: 0,
164
+ averageExecutionTime: 0,
165
+ lastExecutionTime: 0,
166
+ },
167
+ metadata: {
168
+ category: this.inferToolCategory(tool),
169
+ version: "1.0.0",
170
+ deprecated: false,
171
+ },
172
+ };
173
+ }
174
+ /**
175
+ * Infer tool category from tool definition
176
+ */
177
+ inferToolCategory(tool) {
178
+ const name = tool.name.toLowerCase();
179
+ const description = (tool.description || "").toLowerCase();
180
+ // Common patterns for categorization
181
+ if (name.includes("git") || description.includes("git")) {
182
+ return "version-control";
183
+ }
184
+ if (name.includes("file") ||
185
+ name.includes("read") ||
186
+ name.includes("write")) {
187
+ return "file-system";
188
+ }
189
+ if (name.includes("api") ||
190
+ name.includes("http") ||
191
+ name.includes("request")) {
192
+ return "api";
193
+ }
194
+ if (name.includes("data") ||
195
+ name.includes("query") ||
196
+ name.includes("search")) {
197
+ return "data";
198
+ }
199
+ if (name.includes("auth") ||
200
+ name.includes("login") ||
201
+ name.includes("token")) {
202
+ return "authentication";
203
+ }
204
+ if (name.includes("deploy") ||
205
+ name.includes("build") ||
206
+ name.includes("ci")) {
207
+ return "deployment";
208
+ }
209
+ return "general";
210
+ }
211
+ /**
212
+ * Validate a tool
213
+ */
214
+ validateTool(toolInfo) {
215
+ const errors = [];
216
+ const warnings = [];
217
+ // Basic validation
218
+ if (!toolInfo.name || toolInfo.name.trim().length === 0) {
219
+ errors.push("Tool name is required");
220
+ }
221
+ if (toolInfo.name && !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(toolInfo.name)) {
222
+ errors.push("Tool name must start with a letter and contain only letters, numbers, underscores, and hyphens");
223
+ }
224
+ if (!toolInfo.description || toolInfo.description.trim().length === 0) {
225
+ warnings.push("Tool description is empty");
226
+ }
227
+ if (!toolInfo.serverId) {
228
+ errors.push("Server ID is required");
229
+ }
230
+ // Schema validation
231
+ if (toolInfo.inputSchema) {
232
+ try {
233
+ JSON.stringify(toolInfo.inputSchema);
234
+ }
235
+ catch {
236
+ errors.push("Input schema is not valid JSON");
237
+ }
238
+ }
239
+ // Infer metadata
240
+ const metadata = {
241
+ category: typeof toolInfo.metadata?.category === "string"
242
+ ? toolInfo.metadata.category
243
+ : "general",
244
+ complexity: this.inferComplexity(toolInfo),
245
+ requiresAuth: this.inferAuthRequirement(toolInfo),
246
+ isDeprecated: typeof toolInfo.metadata?.deprecated === "boolean"
247
+ ? toolInfo.metadata.deprecated
248
+ : false,
249
+ };
250
+ return {
251
+ isValid: errors.length === 0,
252
+ errors,
253
+ warnings,
254
+ metadata,
255
+ };
256
+ }
257
+ /**
258
+ * Infer tool complexity
259
+ */
260
+ inferComplexity(toolInfo) {
261
+ const schema = toolInfo.inputSchema;
262
+ if (!schema || !schema.properties) {
263
+ return "simple";
264
+ }
265
+ const propertyCount = Object.keys(schema.properties).length;
266
+ if (propertyCount <= 2) {
267
+ return "simple";
268
+ }
269
+ else if (propertyCount <= 5) {
270
+ return "moderate";
271
+ }
272
+ else {
273
+ return "complex";
274
+ }
275
+ }
276
+ /**
277
+ * Infer if tool requires authentication
278
+ */
279
+ inferAuthRequirement(toolInfo) {
280
+ const name = toolInfo.name.toLowerCase();
281
+ const description = toolInfo.description.toLowerCase();
282
+ return (name.includes("auth") ||
283
+ name.includes("login") ||
284
+ name.includes("token") ||
285
+ description.includes("authentication") ||
286
+ description.includes("credentials") ||
287
+ description.includes("permission"));
288
+ }
289
+ /**
290
+ * Execute a tool
291
+ */
292
+ async executeTool(toolName, serverId, client, parameters, options = {}) {
293
+ const startTime = Date.now();
294
+ try {
295
+ const toolKey = this.createToolKey(serverId, toolName);
296
+ const toolInfo = this.toolRegistry.get(toolKey);
297
+ if (!toolInfo) {
298
+ throw new Error(`Tool '${toolName}' not found for server '${serverId}'`);
299
+ }
300
+ if (!toolInfo.isAvailable) {
301
+ throw new Error(`Tool '${toolName}' is not available`);
302
+ }
303
+ // Validate input parameters if requested
304
+ if (options.validateInput !== false) {
305
+ this.validateToolParameters(toolInfo, parameters);
306
+ }
307
+ mcpLogger.debug(`[ToolDiscoveryService] Executing tool: ${toolName} on ${serverId}`, {
308
+ parameters,
309
+ });
310
+ // Create circuit breaker for tool execution
311
+ const circuitBreaker = globalCircuitBreakerManager.getBreaker(`tool-execution-${serverId}-${toolName}`, {
312
+ failureThreshold: 3,
313
+ resetTimeout: 30000,
314
+ operationTimeout: options.timeout || 30000,
315
+ });
316
+ // Execute tool with circuit breaker protection
317
+ const result = await circuitBreaker.execute(async () => {
318
+ const timeout = options.timeout || 30000;
319
+ const executePromise = client.callTool({
320
+ name: toolName,
321
+ arguments: parameters,
322
+ });
323
+ const timeoutPromise = this.createTimeoutPromise(timeout, `Tool execution timeout: ${toolName}`);
324
+ return await Promise.race([executePromise, timeoutPromise]);
325
+ });
326
+ const duration = Date.now() - startTime;
327
+ // Update tool statistics
328
+ this.updateToolStats(toolKey, true, duration);
329
+ // Validate output if requested
330
+ if (options.validateOutput !== false && result) {
331
+ this.validateToolOutput(result);
332
+ }
333
+ mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
334
+ duration,
335
+ hasContent: !!result.content,
336
+ });
337
+ return {
338
+ success: true,
339
+ data: result,
340
+ duration,
341
+ metadata: {
342
+ toolName,
343
+ serverId,
344
+ timestamp: Date.now(),
345
+ },
346
+ };
347
+ }
348
+ catch (error) {
349
+ const duration = Date.now() - startTime;
350
+ const errorMessage = error instanceof Error ? error.message : String(error);
351
+ // Update tool statistics
352
+ const toolKey = this.createToolKey(serverId, toolName);
353
+ this.updateToolStats(toolKey, false, duration);
354
+ mcpLogger.error(`[ToolDiscoveryService] Tool execution failed: ${toolName}`, error);
355
+ return {
356
+ success: false,
357
+ error: errorMessage,
358
+ duration,
359
+ metadata: {
360
+ toolName,
361
+ serverId,
362
+ timestamp: Date.now(),
363
+ },
364
+ };
365
+ }
366
+ }
367
+ /**
368
+ * Validate tool parameters
369
+ */
370
+ validateToolParameters(toolInfo, parameters) {
371
+ if (!toolInfo.inputSchema) {
372
+ return; // No schema to validate against
373
+ }
374
+ // Basic validation - check required properties
375
+ const schema = toolInfo.inputSchema;
376
+ if (schema.required && Array.isArray(schema.required)) {
377
+ for (const requiredProp of schema.required) {
378
+ if (typeof requiredProp === "string" && !(requiredProp in parameters)) {
379
+ throw new Error(`Missing required parameter: ${requiredProp}`);
380
+ }
381
+ }
382
+ }
383
+ // Type validation for properties
384
+ if (schema.properties) {
385
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
386
+ if (propName in parameters) {
387
+ this.validateParameterType(propName, parameters[propName], propSchema);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ /**
393
+ * Validate parameter type
394
+ */
395
+ validateParameterType(name, value, schema) {
396
+ if (!schema.type) {
397
+ return; // No type constraint
398
+ }
399
+ const expectedType = schema.type;
400
+ const actualType = typeof value;
401
+ switch (expectedType) {
402
+ case "string":
403
+ if (actualType !== "string") {
404
+ throw new Error(`Parameter '${name}' must be a string, got ${actualType}`);
405
+ }
406
+ break;
407
+ case "number":
408
+ if (actualType !== "number") {
409
+ throw new Error(`Parameter '${name}' must be a number, got ${actualType}`);
410
+ }
411
+ break;
412
+ case "boolean":
413
+ if (actualType !== "boolean") {
414
+ throw new Error(`Parameter '${name}' must be a boolean, got ${actualType}`);
415
+ }
416
+ break;
417
+ case "array":
418
+ if (!Array.isArray(value)) {
419
+ throw new Error(`Parameter '${name}' must be an array, got ${actualType}`);
420
+ }
421
+ break;
422
+ case "object":
423
+ if (actualType !== "object" || value === null || Array.isArray(value)) {
424
+ throw new Error(`Parameter '${name}' must be an object, got ${actualType}`);
425
+ }
426
+ break;
427
+ }
428
+ }
429
+ /**
430
+ * Validate tool output
431
+ */
432
+ validateToolOutput(result) {
433
+ // Basic output validation
434
+ if (!result) {
435
+ throw new Error("Tool returned no result");
436
+ }
437
+ // Check for error indicators
438
+ if (result.error) {
439
+ throw new Error(`Tool execution error: ${result.error}`);
440
+ }
441
+ if (result.isError === true) {
442
+ throw new Error("Tool execution failed");
443
+ }
444
+ }
445
+ /**
446
+ * Update tool statistics
447
+ */
448
+ updateToolStats(toolKey, success, duration) {
449
+ const toolInfo = this.toolRegistry.get(toolKey);
450
+ if (!toolInfo) {
451
+ return;
452
+ }
453
+ toolInfo.stats.totalCalls++;
454
+ toolInfo.lastCalled = new Date();
455
+ toolInfo.stats.lastExecutionTime = duration;
456
+ if (success) {
457
+ toolInfo.stats.successfulCalls++;
458
+ }
459
+ else {
460
+ toolInfo.stats.failedCalls++;
461
+ }
462
+ // Update average execution time
463
+ const totalTime = toolInfo.stats.averageExecutionTime * (toolInfo.stats.totalCalls - 1) +
464
+ duration;
465
+ toolInfo.stats.averageExecutionTime = totalTime / toolInfo.stats.totalCalls;
466
+ }
467
+ /**
468
+ * Get tool by name and server
469
+ */
470
+ getTool(toolName, serverId) {
471
+ const toolKey = this.createToolKey(serverId, toolName);
472
+ return this.toolRegistry.get(toolKey);
473
+ }
474
+ /**
475
+ * Get all tools for a server
476
+ */
477
+ getServerTools(serverId) {
478
+ const tools = [];
479
+ const serverToolNames = this.serverTools.get(serverId);
480
+ if (serverToolNames) {
481
+ for (const toolName of serverToolNames) {
482
+ const toolKey = this.createToolKey(serverId, toolName);
483
+ const toolInfo = this.toolRegistry.get(toolKey);
484
+ if (toolInfo) {
485
+ tools.push(toolInfo);
486
+ }
487
+ }
488
+ }
489
+ return tools;
490
+ }
491
+ /**
492
+ * Get all registered tools
493
+ */
494
+ getAllTools() {
495
+ return Array.from(this.toolRegistry.values());
496
+ }
497
+ /**
498
+ * Clear tools for a server
499
+ */
500
+ clearServerTools(serverId) {
501
+ const serverToolNames = this.serverTools.get(serverId);
502
+ if (serverToolNames) {
503
+ for (const toolName of serverToolNames) {
504
+ const toolKey = this.createToolKey(serverId, toolName);
505
+ this.toolRegistry.delete(toolKey);
506
+ // Emit tool unregistered event
507
+ this.emit("toolUnregistered", {
508
+ serverId,
509
+ toolName,
510
+ timestamp: new Date(),
511
+ });
512
+ }
513
+ this.serverTools.delete(serverId);
514
+ }
515
+ mcpLogger.debug(`[ToolDiscoveryService] Cleared tools for server: ${serverId}`);
516
+ }
517
+ /**
518
+ * Update tool availability
519
+ */
520
+ updateToolAvailability(toolName, serverId, isAvailable) {
521
+ const toolKey = this.createToolKey(serverId, toolName);
522
+ const toolInfo = this.toolRegistry.get(toolKey);
523
+ if (toolInfo) {
524
+ toolInfo.isAvailable = isAvailable;
525
+ mcpLogger.debug(`[ToolDiscoveryService] Updated availability for ${toolName}: ${isAvailable}`);
526
+ }
527
+ }
528
+ /**
529
+ * Create tool key for registry
530
+ */
531
+ createToolKey(serverId, toolName) {
532
+ return `${serverId}:${toolName}`;
533
+ }
534
+ /**
535
+ * Create timeout promise
536
+ */
537
+ createTimeoutPromise(timeout, message) {
538
+ return new Promise((_, reject) => {
539
+ setTimeout(() => {
540
+ reject(new Error(message));
541
+ }, timeout);
542
+ });
543
+ }
544
+ /**
545
+ * Get discovery statistics
546
+ */
547
+ getStatistics() {
548
+ const toolsByServer = {};
549
+ const toolsByCategory = {};
550
+ let availableTools = 0;
551
+ let unavailableTools = 0;
552
+ for (const toolInfo of this.toolRegistry.values()) {
553
+ // Count by server
554
+ toolsByServer[toolInfo.serverId] =
555
+ (toolsByServer[toolInfo.serverId] || 0) + 1;
556
+ // Count by category
557
+ const category = typeof toolInfo.metadata?.category === "string"
558
+ ? toolInfo.metadata.category
559
+ : "unknown";
560
+ toolsByCategory[category] = (toolsByCategory[category] || 0) + 1;
561
+ // Count availability
562
+ if (toolInfo.isAvailable) {
563
+ availableTools++;
564
+ }
565
+ else {
566
+ unavailableTools++;
567
+ }
568
+ }
569
+ return {
570
+ totalTools: this.toolRegistry.size,
571
+ availableTools,
572
+ unavailableTools,
573
+ totalServers: this.serverTools.size,
574
+ toolsByServer,
575
+ toolsByCategory,
576
+ };
577
+ }
578
+ }