@juspay/neurolink 7.14.6 → 7.14.8

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [7.14.8](https://github.com/juspay/neurolink/compare/v7.14.7...v7.14.8) (2025-08-19)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(mcp):** implement generic error handling for all MCP server response formats ([5aa707a](https://github.com/juspay/neurolink/commit/5aa707aa9874ed76ab067a1f7fb6e8301519ce7f))
6
+
7
+ ## [7.14.7](https://github.com/juspay/neurolink/compare/v7.14.6...v7.14.7) (2025-08-18)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(core):** add validation for tool registration ([caed431](https://github.com/juspay/neurolink/commit/caed431ca1a025599ae8f901a4f4cb36b970379c))
12
+
1
13
  ## [7.14.6](https://github.com/juspay/neurolink/compare/v7.14.5...v7.14.6) (2025-08-18)
2
14
 
3
15
  ### Bug Fixes
@@ -395,12 +395,41 @@ export class BaseProvider {
395
395
  parameters: z.object({}), // Use empty schema for custom tools
396
396
  execute: async (params) => {
397
397
  logger.debug(`[BaseProvider] Executing custom tool: ${toolName}`, { params });
398
- // Use the tool executor if available (from setupToolExecutor)
399
- if (this.toolExecutor) {
400
- return await this.toolExecutor(toolName, params);
398
+ try {
399
+ // Use the tool executor if available (from setupToolExecutor)
400
+ let result;
401
+ if (this.toolExecutor) {
402
+ result = await this.toolExecutor(toolName, params);
403
+ }
404
+ else {
405
+ result = await typedToolDef.execute(params);
406
+ }
407
+ // Log successful execution
408
+ logger.debug(`[BaseProvider] Tool execution successful: ${toolName}`, {
409
+ resultType: typeof result,
410
+ hasResult: result !== null && result !== undefined,
411
+ toolName,
412
+ });
413
+ return result;
401
414
  }
402
- else {
403
- return await typedToolDef.execute(params);
415
+ catch (error) {
416
+ logger.warn(`[BaseProvider] Tool execution failed: ${toolName}`, {
417
+ error: error instanceof Error ? error.message : String(error),
418
+ params,
419
+ toolName,
420
+ });
421
+ // GENERIC ERROR HANDLING FOR ALL MCP TOOLS:
422
+ // Return a generic error object that works with any MCP server
423
+ // The AI can interpret this and try different approaches
424
+ return {
425
+ _neurolinkToolError: true,
426
+ toolName: toolName,
427
+ error: error instanceof Error ? error.message : String(error),
428
+ timestamp: new Date().toISOString(),
429
+ params: params,
430
+ // Keep it simple - just indicate an error occurred
431
+ message: `Error calling ${toolName}: ${error instanceof Error ? error.message : String(error)}`
432
+ };
404
433
  }
405
434
  },
406
435
  });
@@ -395,12 +395,41 @@ export class BaseProvider {
395
395
  parameters: z.object({}), // Use empty schema for custom tools
396
396
  execute: async (params) => {
397
397
  logger.debug(`[BaseProvider] Executing custom tool: ${toolName}`, { params });
398
- // Use the tool executor if available (from setupToolExecutor)
399
- if (this.toolExecutor) {
400
- return await this.toolExecutor(toolName, params);
398
+ try {
399
+ // Use the tool executor if available (from setupToolExecutor)
400
+ let result;
401
+ if (this.toolExecutor) {
402
+ result = await this.toolExecutor(toolName, params);
403
+ }
404
+ else {
405
+ result = await typedToolDef.execute(params);
406
+ }
407
+ // Log successful execution
408
+ logger.debug(`[BaseProvider] Tool execution successful: ${toolName}`, {
409
+ resultType: typeof result,
410
+ hasResult: result !== null && result !== undefined,
411
+ toolName,
412
+ });
413
+ return result;
401
414
  }
402
- else {
403
- return await typedToolDef.execute(params);
415
+ catch (error) {
416
+ logger.warn(`[BaseProvider] Tool execution failed: ${toolName}`, {
417
+ error: error instanceof Error ? error.message : String(error),
418
+ params,
419
+ toolName,
420
+ });
421
+ // GENERIC ERROR HANDLING FOR ALL MCP TOOLS:
422
+ // Return a generic error object that works with any MCP server
423
+ // The AI can interpret this and try different approaches
424
+ return {
425
+ _neurolinkToolError: true,
426
+ toolName: toolName,
427
+ error: error instanceof Error ? error.message : String(error),
428
+ timestamp: new Date().toISOString(),
429
+ params: params,
430
+ // Keep it simple - just indicate an error occurred
431
+ message: `Error calling ${toolName}: ${error instanceof Error ? error.message : String(error)}`
432
+ };
404
433
  }
405
434
  },
406
435
  });
@@ -6,7 +6,7 @@
6
6
  import { EventEmitter } from "events";
7
7
  import { mcpLogger } from "../utils/logger.js";
8
8
  import { globalCircuitBreakerManager } from "./mcpCircuitBreaker.js";
9
- import { isObject, isString, isBoolean, isNullish, } from "../utils/typeUtils.js";
9
+ import { isObject, isNullish, } from "../utils/typeUtils.js";
10
10
  import { validateToolName, validateToolDescription, } from "../utils/parameterValidation.js";
11
11
  /**
12
12
  * ToolDiscoveryService
@@ -342,7 +342,7 @@ export class ToolDiscoveryService extends EventEmitter {
342
342
  // Update tool statistics
343
343
  this.updateToolStats(toolKey, true, duration);
344
344
  // Validate output if requested
345
- if (options.validateOutput !== false && result) {
345
+ if (options.validateOutput !== false) {
346
346
  this.validateToolOutput(result);
347
347
  }
348
348
  mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
@@ -445,46 +445,30 @@ export class ToolDiscoveryService extends EventEmitter {
445
445
  * Validate tool output with enhanced type safety
446
446
  */
447
447
  validateToolOutput(result) {
448
- // Check for null/undefined results
448
+ // GENERIC ERROR HANDLING FOR ALL MCP TOOLS
449
+ // Different MCP servers return different error formats, so we should be permissive
450
+ // and let the AI handle any response format instead of throwing errors
451
+ // Only throw for truly invalid responses (null/undefined)
449
452
  if (isNullish(result)) {
450
- throw new Error("Tool returned null or undefined result");
451
- }
452
- // Enhanced error detection for object results
453
- if (isObject(result)) {
454
- // Check for explicit error property
455
- if (result.error !== undefined) {
456
- const errorMessage = isString(result.error)
457
- ? result.error
458
- : "Tool execution failed with error";
459
- throw new Error(`Tool execution error: ${errorMessage}`);
460
- }
461
- // Check for boolean error flag
462
- if (isBoolean(result.isError) && result.isError === true) {
463
- const errorDetail = isString(result.message)
464
- ? `: ${result.message}`
465
- : "";
466
- throw new Error(`Tool execution failed${errorDetail}`);
467
- }
468
- // Check for common error status patterns
469
- if (isString(result.status) &&
470
- (result.status === "error" || result.status === "failed")) {
471
- const errorDetail = isString(result.message) || isString(result.reason)
472
- ? `: ${result.message || result.reason}`
473
- : "";
474
- throw new Error(`Tool execution failed${errorDetail}`);
475
- }
476
- // Check for success: false pattern
477
- if (isBoolean(result.success) && result.success === false) {
478
- const errorDetail = isString(result.message) || isString(result.error)
479
- ? `: ${result.message || result.error}`
480
- : "";
481
- throw new Error(`Tool execution unsuccessful${errorDetail}`);
482
- }
483
- }
484
- // Validate that string results are not empty
485
- if (isString(result) && result.trim() === "") {
486
- throw new Error("Tool returned empty string result");
453
+ mcpLogger.debug("[ToolDiscoveryService] Tool returned null/undefined, treating as empty response");
454
+ // Even null responses can be valid for some tools - don't throw
455
+ return;
487
456
  }
457
+ // Log what we received for debugging, but don't validate specific formats
458
+ mcpLogger.debug("[ToolDiscoveryService] Tool response received", {
459
+ type: typeof result,
460
+ isArray: Array.isArray(result),
461
+ isObject: isObject(result),
462
+ hasKeys: isObject(result) ? Object.keys(result).length : 0,
463
+ fullResponse: result // Log the complete response, not a truncated sample
464
+ });
465
+ // COMPLETELY PERMISSIVE APPROACH:
466
+ // - Any response format is valid (objects, strings, arrays, booleans, numbers)
467
+ // - Even error responses are passed to the AI to handle
468
+ // - The AI can interpret error messages and retry with different approaches
469
+ // - This works with any MCP server regardless of their response format
470
+ // No validation or throwing - let the AI handle everything
471
+ return;
488
472
  }
489
473
  /**
490
474
  * Update tool statistics
@@ -356,27 +356,55 @@ export class NeuroLink {
356
356
  }
357
357
  // Try MCP-enhanced generation first (if not explicitly disabled)
358
358
  if (!options.disableTools) {
359
- try {
360
- logger.debug(`[${functionTag}] Attempting MCP generation...`);
361
- const mcpResult = await this.tryMCPGeneration(options);
362
- if (mcpResult && mcpResult.content) {
363
- logger.debug(`[${functionTag}] MCP generation successful`);
364
- // Store conversation turn
365
- await storeConversationTurn(this.conversationMemory, options, mcpResult);
366
- return mcpResult;
359
+ let mcpAttempts = 0;
360
+ const maxMcpRetries = 2; // Allow retries for tool-related failures
361
+ while (mcpAttempts <= maxMcpRetries) {
362
+ try {
363
+ logger.debug(`[${functionTag}] Attempting MCP generation (attempt ${mcpAttempts + 1}/${maxMcpRetries + 1})...`);
364
+ const mcpResult = await this.tryMCPGeneration(options);
365
+ if (mcpResult && mcpResult.content) {
366
+ logger.debug(`[${functionTag}] MCP generation successful on attempt ${mcpAttempts + 1}`, {
367
+ contentLength: mcpResult.content.length,
368
+ toolsUsed: mcpResult.toolsUsed?.length || 0,
369
+ toolExecutions: mcpResult.toolExecutions?.length || 0,
370
+ });
371
+ // Store conversation turn
372
+ await storeConversationTurn(this.conversationMemory, options, mcpResult);
373
+ return mcpResult;
374
+ }
375
+ else {
376
+ logger.debug(`[${functionTag}] MCP generation returned empty result on attempt ${mcpAttempts + 1}:`, {
377
+ hasResult: !!mcpResult,
378
+ hasContent: !!(mcpResult && mcpResult.content),
379
+ contentLength: mcpResult?.content?.length || 0,
380
+ toolExecutions: mcpResult?.toolExecutions?.length || 0,
381
+ });
382
+ // If we got a result but no content, and we have tool executions, this might be a tool success case
383
+ if (mcpResult &&
384
+ mcpResult.toolExecutions &&
385
+ mcpResult.toolExecutions.length > 0) {
386
+ logger.debug(`[${functionTag}] Found tool executions but no content, continuing with result`);
387
+ // Store conversation turn even with empty content if tools executed
388
+ await storeConversationTurn(this.conversationMemory, options, mcpResult);
389
+ return mcpResult;
390
+ }
391
+ }
367
392
  }
368
- else {
369
- logger.debug(`[${functionTag}] MCP generation returned empty result:`, {
370
- hasResult: !!mcpResult,
371
- hasContent: !!(mcpResult && mcpResult.content),
372
- contentLength: mcpResult?.content?.length || 0,
393
+ catch (error) {
394
+ mcpAttempts++;
395
+ logger.debug(`[${functionTag}] MCP generation failed on attempt ${mcpAttempts}/${maxMcpRetries + 1}`, {
396
+ error: error instanceof Error ? error.message : String(error),
397
+ willRetry: mcpAttempts <= maxMcpRetries,
373
398
  });
399
+ // If this was the last attempt, break and fall back
400
+ if (mcpAttempts > maxMcpRetries) {
401
+ logger.debug(`[${functionTag}] All MCP attempts exhausted, falling back to direct generation`);
402
+ break;
403
+ }
404
+ // Small delay before retry to allow transient issues to resolve
405
+ await new Promise((resolve) => setTimeout(resolve, 500));
374
406
  }
375
- }
376
- catch (error) {
377
- logger.debug(`[${functionTag}] MCP generation failed, falling back`, {
378
- error: error instanceof Error ? error.message : String(error),
379
- });
407
+ mcpAttempts++;
380
408
  }
381
409
  }
382
410
  // Fall back to direct provider generation
@@ -430,19 +458,40 @@ export class NeuroLink {
430
458
  conversationMessages, // Inject conversation history
431
459
  });
432
460
  const responseTime = Date.now() - startTime;
433
- // Check if result is meaningful
434
- if (!result || !result.content || result.content.trim().length === 0) {
461
+ // Enhanced result validation - consider tool executions as valid results
462
+ const hasContent = result && result.content && result.content.trim().length > 0;
463
+ const hasToolExecutions = result && result.toolExecutions && result.toolExecutions.length > 0;
464
+ // Log detailed result analysis for debugging
465
+ mcpLogger.debug(`[${functionTag}] Result validation:`, {
466
+ hasResult: !!result,
467
+ hasContent,
468
+ hasToolExecutions,
469
+ contentLength: result?.content?.length || 0,
470
+ toolExecutionsCount: result?.toolExecutions?.length || 0,
471
+ toolsUsedCount: result?.toolsUsed?.length || 0,
472
+ });
473
+ // Accept result if it has content OR successful tool executions
474
+ if (!hasContent && !hasToolExecutions) {
475
+ mcpLogger.debug(`[${functionTag}] Result rejected: no content and no tool executions`);
435
476
  return null; // Let caller fall back to direct generation
436
477
  }
437
- // Return enhanced result with external tool information
478
+ // Transform tool executions with enhanced preservation
479
+ const transformedToolExecutions = transformToolExecutionsForMCP(result.toolExecutions);
480
+ // Log transformation results
481
+ mcpLogger.debug(`[${functionTag}] Tool execution transformation:`, {
482
+ originalCount: result?.toolExecutions?.length || 0,
483
+ transformedCount: transformedToolExecutions.length,
484
+ transformedTools: transformedToolExecutions.map((te) => te.toolName),
485
+ });
486
+ // Return enhanced result with preserved tool information
438
487
  return {
439
- content: result.content,
488
+ content: result.content || "", // Ensure content is never undefined
440
489
  provider: providerName,
441
490
  usage: result.usage,
442
491
  responseTime,
443
492
  toolsUsed: result.toolsUsed || [],
444
- toolExecutions: transformToolExecutionsForMCP(result.toolExecutions),
445
- enhancedWithTools: true,
493
+ toolExecutions: transformedToolExecutions,
494
+ enhancedWithTools: Boolean(hasToolExecutions), // Mark as enhanced if tools were actually used
446
495
  availableTools: transformToolsForMCP(availableTools),
447
496
  // Include analytics and evaluation from BaseProvider
448
497
  analytics: result.analytics,
@@ -808,6 +857,17 @@ export class NeuroLink {
808
857
  timestamp: Date.now(),
809
858
  });
810
859
  try {
860
+ // --- Start: Added Validation Logic ---
861
+ if (!name || typeof name !== "string") {
862
+ throw new Error("Invalid tool name");
863
+ }
864
+ if (!tool || typeof tool !== "object") {
865
+ throw new Error(`Invalid tool object provided for tool: ${name}`);
866
+ }
867
+ if (typeof tool.execute !== "function") {
868
+ throw new Error(`Tool '${name}' must have an execute method.`);
869
+ }
870
+ // --- End: Added Validation Logic ---
811
871
  // Import validation functions synchronously - they are pure functions
812
872
  let validateTool;
813
873
  let isToolNameAvailable;
@@ -24,13 +24,56 @@ export function transformToolExecutions(toolExecutions) {
24
24
  if (!toolExecutions || !Array.isArray(toolExecutions)) {
25
25
  return [];
26
26
  }
27
- return toolExecutions.map((te) => {
27
+ return toolExecutions.map((te, index) => {
28
28
  const teRecord = te;
29
+ // Enhanced tool name extraction with multiple fallback strategies
30
+ let toolName = teRecord.name ||
31
+ teRecord.toolName ||
32
+ teRecord.tool ||
33
+ "";
34
+ // If still no name, try to extract from nested objects
35
+ if (!toolName &&
36
+ teRecord.toolCall &&
37
+ typeof teRecord.toolCall === "object") {
38
+ const toolCall = teRecord.toolCall;
39
+ toolName =
40
+ toolCall.name || toolCall.toolName || "";
41
+ }
42
+ // Last resort: use index-based fallback to avoid "Unknown Tool"
43
+ if (!toolName) {
44
+ toolName = `tool_execution_${index}`;
45
+ }
46
+ // Enhanced input extraction
47
+ let input = teRecord.input ||
48
+ teRecord.parameters ||
49
+ teRecord.args ||
50
+ {};
51
+ // Extract input from nested toolCall if available
52
+ if (Object.keys(input).length === 0 &&
53
+ teRecord.toolCall &&
54
+ typeof teRecord.toolCall === "object") {
55
+ const toolCall = teRecord.toolCall;
56
+ input =
57
+ toolCall.input ||
58
+ toolCall.parameters ||
59
+ toolCall.args ||
60
+ {};
61
+ }
62
+ // Enhanced output extraction with success indication
63
+ let output = teRecord.output ||
64
+ teRecord.result ||
65
+ teRecord.response ||
66
+ "success";
67
+ // Enhanced duration extraction
68
+ let duration = teRecord.duration ??
69
+ teRecord.executionTime ??
70
+ teRecord.responseTime ??
71
+ 0;
29
72
  return {
30
- name: teRecord.name || "",
31
- input: teRecord.input || {},
32
- output: teRecord.output || "success",
33
- duration: teRecord.duration || 0,
73
+ name: toolName,
74
+ input: input,
75
+ output: output,
76
+ duration: duration,
34
77
  };
35
78
  });
36
79
  }
@@ -42,13 +85,63 @@ export function transformToolExecutionsForMCP(toolExecutions) {
42
85
  if (!toolExecutions || !Array.isArray(toolExecutions)) {
43
86
  return [];
44
87
  }
45
- return toolExecutions.map((te) => {
88
+ return toolExecutions.map((te, index) => {
46
89
  const teRecord = te;
90
+ // Enhanced tool name extraction matching the main function
91
+ let toolName = teRecord.name ||
92
+ teRecord.toolName ||
93
+ teRecord.tool ||
94
+ "";
95
+ // Try nested toolCall extraction
96
+ if (!toolName &&
97
+ teRecord.toolCall &&
98
+ typeof teRecord.toolCall === "object") {
99
+ const toolCall = teRecord.toolCall;
100
+ toolName =
101
+ toolCall.name || toolCall.toolName || "";
102
+ }
103
+ // Fallback to avoid empty names
104
+ if (!toolName) {
105
+ toolName = `mcp_tool_execution_${index}`;
106
+ }
107
+ // Enhanced execution time extraction
108
+ let executionTime = teRecord.duration ??
109
+ teRecord.executionTime ??
110
+ teRecord.responseTime ??
111
+ 0;
112
+ // Enhanced success detection - check for actual success indicators
113
+ let success = true; // Default to true
114
+ // Check for explicit success/error indicators
115
+ if (teRecord.success !== undefined) {
116
+ success = Boolean(teRecord.success);
117
+ }
118
+ else if (teRecord.error !== undefined) {
119
+ success = false;
120
+ }
121
+ else if (teRecord.status !== undefined) {
122
+ const status = String(teRecord.status).toLowerCase().trim();
123
+ success = !["error", "failed", "failure", "fail"].includes(status);
124
+ }
125
+ // Enhanced server ID extraction
126
+ let serverId = teRecord.serverId ||
127
+ teRecord.server ||
128
+ teRecord.source ||
129
+ undefined;
130
+ // Try to extract from nested structures
131
+ if (!serverId &&
132
+ teRecord.toolCall &&
133
+ typeof teRecord.toolCall === "object") {
134
+ const toolCall = teRecord.toolCall;
135
+ serverId =
136
+ toolCall.serverId ||
137
+ toolCall.server ||
138
+ undefined;
139
+ }
47
140
  return {
48
- toolName: teRecord.name || "",
49
- executionTime: teRecord.duration || 0,
50
- success: true, // Assume success if tool executed (AI providers handle failures differently)
51
- serverId: teRecord.serverId || undefined,
141
+ toolName: toolName,
142
+ executionTime: executionTime,
143
+ success: success,
144
+ serverId: serverId,
52
145
  };
53
146
  });
54
147
  }
@@ -6,7 +6,7 @@
6
6
  import { EventEmitter } from "events";
7
7
  import { mcpLogger } from "../utils/logger.js";
8
8
  import { globalCircuitBreakerManager } from "./mcpCircuitBreaker.js";
9
- import { isObject, isString, isBoolean, isNullish, } from "../utils/typeUtils.js";
9
+ import { isObject, isNullish, } from "../utils/typeUtils.js";
10
10
  import { validateToolName, validateToolDescription, } from "../utils/parameterValidation.js";
11
11
  /**
12
12
  * ToolDiscoveryService
@@ -342,7 +342,7 @@ export class ToolDiscoveryService extends EventEmitter {
342
342
  // Update tool statistics
343
343
  this.updateToolStats(toolKey, true, duration);
344
344
  // Validate output if requested
345
- if (options.validateOutput !== false && result) {
345
+ if (options.validateOutput !== false) {
346
346
  this.validateToolOutput(result);
347
347
  }
348
348
  mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
@@ -445,46 +445,30 @@ export class ToolDiscoveryService extends EventEmitter {
445
445
  * Validate tool output with enhanced type safety
446
446
  */
447
447
  validateToolOutput(result) {
448
- // Check for null/undefined results
448
+ // GENERIC ERROR HANDLING FOR ALL MCP TOOLS
449
+ // Different MCP servers return different error formats, so we should be permissive
450
+ // and let the AI handle any response format instead of throwing errors
451
+ // Only throw for truly invalid responses (null/undefined)
449
452
  if (isNullish(result)) {
450
- throw new Error("Tool returned null or undefined result");
451
- }
452
- // Enhanced error detection for object results
453
- if (isObject(result)) {
454
- // Check for explicit error property
455
- if (result.error !== undefined) {
456
- const errorMessage = isString(result.error)
457
- ? result.error
458
- : "Tool execution failed with error";
459
- throw new Error(`Tool execution error: ${errorMessage}`);
460
- }
461
- // Check for boolean error flag
462
- if (isBoolean(result.isError) && result.isError === true) {
463
- const errorDetail = isString(result.message)
464
- ? `: ${result.message}`
465
- : "";
466
- throw new Error(`Tool execution failed${errorDetail}`);
467
- }
468
- // Check for common error status patterns
469
- if (isString(result.status) &&
470
- (result.status === "error" || result.status === "failed")) {
471
- const errorDetail = isString(result.message) || isString(result.reason)
472
- ? `: ${result.message || result.reason}`
473
- : "";
474
- throw new Error(`Tool execution failed${errorDetail}`);
475
- }
476
- // Check for success: false pattern
477
- if (isBoolean(result.success) && result.success === false) {
478
- const errorDetail = isString(result.message) || isString(result.error)
479
- ? `: ${result.message || result.error}`
480
- : "";
481
- throw new Error(`Tool execution unsuccessful${errorDetail}`);
482
- }
483
- }
484
- // Validate that string results are not empty
485
- if (isString(result) && result.trim() === "") {
486
- throw new Error("Tool returned empty string result");
453
+ mcpLogger.debug("[ToolDiscoveryService] Tool returned null/undefined, treating as empty response");
454
+ // Even null responses can be valid for some tools - don't throw
455
+ return;
487
456
  }
457
+ // Log what we received for debugging, but don't validate specific formats
458
+ mcpLogger.debug("[ToolDiscoveryService] Tool response received", {
459
+ type: typeof result,
460
+ isArray: Array.isArray(result),
461
+ isObject: isObject(result),
462
+ hasKeys: isObject(result) ? Object.keys(result).length : 0,
463
+ fullResponse: result // Log the complete response, not a truncated sample
464
+ });
465
+ // COMPLETELY PERMISSIVE APPROACH:
466
+ // - Any response format is valid (objects, strings, arrays, booleans, numbers)
467
+ // - Even error responses are passed to the AI to handle
468
+ // - The AI can interpret error messages and retry with different approaches
469
+ // - This works with any MCP server regardless of their response format
470
+ // No validation or throwing - let the AI handle everything
471
+ return;
488
472
  }
489
473
  /**
490
474
  * Update tool statistics
package/dist/neurolink.js CHANGED
@@ -356,27 +356,55 @@ export class NeuroLink {
356
356
  }
357
357
  // Try MCP-enhanced generation first (if not explicitly disabled)
358
358
  if (!options.disableTools) {
359
- try {
360
- logger.debug(`[${functionTag}] Attempting MCP generation...`);
361
- const mcpResult = await this.tryMCPGeneration(options);
362
- if (mcpResult && mcpResult.content) {
363
- logger.debug(`[${functionTag}] MCP generation successful`);
364
- // Store conversation turn
365
- await storeConversationTurn(this.conversationMemory, options, mcpResult);
366
- return mcpResult;
359
+ let mcpAttempts = 0;
360
+ const maxMcpRetries = 2; // Allow retries for tool-related failures
361
+ while (mcpAttempts <= maxMcpRetries) {
362
+ try {
363
+ logger.debug(`[${functionTag}] Attempting MCP generation (attempt ${mcpAttempts + 1}/${maxMcpRetries + 1})...`);
364
+ const mcpResult = await this.tryMCPGeneration(options);
365
+ if (mcpResult && mcpResult.content) {
366
+ logger.debug(`[${functionTag}] MCP generation successful on attempt ${mcpAttempts + 1}`, {
367
+ contentLength: mcpResult.content.length,
368
+ toolsUsed: mcpResult.toolsUsed?.length || 0,
369
+ toolExecutions: mcpResult.toolExecutions?.length || 0,
370
+ });
371
+ // Store conversation turn
372
+ await storeConversationTurn(this.conversationMemory, options, mcpResult);
373
+ return mcpResult;
374
+ }
375
+ else {
376
+ logger.debug(`[${functionTag}] MCP generation returned empty result on attempt ${mcpAttempts + 1}:`, {
377
+ hasResult: !!mcpResult,
378
+ hasContent: !!(mcpResult && mcpResult.content),
379
+ contentLength: mcpResult?.content?.length || 0,
380
+ toolExecutions: mcpResult?.toolExecutions?.length || 0,
381
+ });
382
+ // If we got a result but no content, and we have tool executions, this might be a tool success case
383
+ if (mcpResult &&
384
+ mcpResult.toolExecutions &&
385
+ mcpResult.toolExecutions.length > 0) {
386
+ logger.debug(`[${functionTag}] Found tool executions but no content, continuing with result`);
387
+ // Store conversation turn even with empty content if tools executed
388
+ await storeConversationTurn(this.conversationMemory, options, mcpResult);
389
+ return mcpResult;
390
+ }
391
+ }
367
392
  }
368
- else {
369
- logger.debug(`[${functionTag}] MCP generation returned empty result:`, {
370
- hasResult: !!mcpResult,
371
- hasContent: !!(mcpResult && mcpResult.content),
372
- contentLength: mcpResult?.content?.length || 0,
393
+ catch (error) {
394
+ mcpAttempts++;
395
+ logger.debug(`[${functionTag}] MCP generation failed on attempt ${mcpAttempts}/${maxMcpRetries + 1}`, {
396
+ error: error instanceof Error ? error.message : String(error),
397
+ willRetry: mcpAttempts <= maxMcpRetries,
373
398
  });
399
+ // If this was the last attempt, break and fall back
400
+ if (mcpAttempts > maxMcpRetries) {
401
+ logger.debug(`[${functionTag}] All MCP attempts exhausted, falling back to direct generation`);
402
+ break;
403
+ }
404
+ // Small delay before retry to allow transient issues to resolve
405
+ await new Promise((resolve) => setTimeout(resolve, 500));
374
406
  }
375
- }
376
- catch (error) {
377
- logger.debug(`[${functionTag}] MCP generation failed, falling back`, {
378
- error: error instanceof Error ? error.message : String(error),
379
- });
407
+ mcpAttempts++;
380
408
  }
381
409
  }
382
410
  // Fall back to direct provider generation
@@ -430,19 +458,40 @@ export class NeuroLink {
430
458
  conversationMessages, // Inject conversation history
431
459
  });
432
460
  const responseTime = Date.now() - startTime;
433
- // Check if result is meaningful
434
- if (!result || !result.content || result.content.trim().length === 0) {
461
+ // Enhanced result validation - consider tool executions as valid results
462
+ const hasContent = result && result.content && result.content.trim().length > 0;
463
+ const hasToolExecutions = result && result.toolExecutions && result.toolExecutions.length > 0;
464
+ // Log detailed result analysis for debugging
465
+ mcpLogger.debug(`[${functionTag}] Result validation:`, {
466
+ hasResult: !!result,
467
+ hasContent,
468
+ hasToolExecutions,
469
+ contentLength: result?.content?.length || 0,
470
+ toolExecutionsCount: result?.toolExecutions?.length || 0,
471
+ toolsUsedCount: result?.toolsUsed?.length || 0,
472
+ });
473
+ // Accept result if it has content OR successful tool executions
474
+ if (!hasContent && !hasToolExecutions) {
475
+ mcpLogger.debug(`[${functionTag}] Result rejected: no content and no tool executions`);
435
476
  return null; // Let caller fall back to direct generation
436
477
  }
437
- // Return enhanced result with external tool information
478
+ // Transform tool executions with enhanced preservation
479
+ const transformedToolExecutions = transformToolExecutionsForMCP(result.toolExecutions);
480
+ // Log transformation results
481
+ mcpLogger.debug(`[${functionTag}] Tool execution transformation:`, {
482
+ originalCount: result?.toolExecutions?.length || 0,
483
+ transformedCount: transformedToolExecutions.length,
484
+ transformedTools: transformedToolExecutions.map((te) => te.toolName),
485
+ });
486
+ // Return enhanced result with preserved tool information
438
487
  return {
439
- content: result.content,
488
+ content: result.content || "", // Ensure content is never undefined
440
489
  provider: providerName,
441
490
  usage: result.usage,
442
491
  responseTime,
443
492
  toolsUsed: result.toolsUsed || [],
444
- toolExecutions: transformToolExecutionsForMCP(result.toolExecutions),
445
- enhancedWithTools: true,
493
+ toolExecutions: transformedToolExecutions,
494
+ enhancedWithTools: Boolean(hasToolExecutions), // Mark as enhanced if tools were actually used
446
495
  availableTools: transformToolsForMCP(availableTools),
447
496
  // Include analytics and evaluation from BaseProvider
448
497
  analytics: result.analytics,
@@ -808,6 +857,17 @@ export class NeuroLink {
808
857
  timestamp: Date.now(),
809
858
  });
810
859
  try {
860
+ // --- Start: Added Validation Logic ---
861
+ if (!name || typeof name !== "string") {
862
+ throw new Error("Invalid tool name");
863
+ }
864
+ if (!tool || typeof tool !== "object") {
865
+ throw new Error(`Invalid tool object provided for tool: ${name}`);
866
+ }
867
+ if (typeof tool.execute !== "function") {
868
+ throw new Error(`Tool '${name}' must have an execute method.`);
869
+ }
870
+ // --- End: Added Validation Logic ---
811
871
  // Import validation functions synchronously - they are pure functions
812
872
  let validateTool;
813
873
  let isToolNameAvailable;
@@ -24,13 +24,56 @@ export function transformToolExecutions(toolExecutions) {
24
24
  if (!toolExecutions || !Array.isArray(toolExecutions)) {
25
25
  return [];
26
26
  }
27
- return toolExecutions.map((te) => {
27
+ return toolExecutions.map((te, index) => {
28
28
  const teRecord = te;
29
+ // Enhanced tool name extraction with multiple fallback strategies
30
+ let toolName = teRecord.name ||
31
+ teRecord.toolName ||
32
+ teRecord.tool ||
33
+ "";
34
+ // If still no name, try to extract from nested objects
35
+ if (!toolName &&
36
+ teRecord.toolCall &&
37
+ typeof teRecord.toolCall === "object") {
38
+ const toolCall = teRecord.toolCall;
39
+ toolName =
40
+ toolCall.name || toolCall.toolName || "";
41
+ }
42
+ // Last resort: use index-based fallback to avoid "Unknown Tool"
43
+ if (!toolName) {
44
+ toolName = `tool_execution_${index}`;
45
+ }
46
+ // Enhanced input extraction
47
+ let input = teRecord.input ||
48
+ teRecord.parameters ||
49
+ teRecord.args ||
50
+ {};
51
+ // Extract input from nested toolCall if available
52
+ if (Object.keys(input).length === 0 &&
53
+ teRecord.toolCall &&
54
+ typeof teRecord.toolCall === "object") {
55
+ const toolCall = teRecord.toolCall;
56
+ input =
57
+ toolCall.input ||
58
+ toolCall.parameters ||
59
+ toolCall.args ||
60
+ {};
61
+ }
62
+ // Enhanced output extraction with success indication
63
+ let output = teRecord.output ||
64
+ teRecord.result ||
65
+ teRecord.response ||
66
+ "success";
67
+ // Enhanced duration extraction
68
+ let duration = teRecord.duration ??
69
+ teRecord.executionTime ??
70
+ teRecord.responseTime ??
71
+ 0;
29
72
  return {
30
- name: teRecord.name || "",
31
- input: teRecord.input || {},
32
- output: teRecord.output || "success",
33
- duration: teRecord.duration || 0,
73
+ name: toolName,
74
+ input: input,
75
+ output: output,
76
+ duration: duration,
34
77
  };
35
78
  });
36
79
  }
@@ -42,13 +85,63 @@ export function transformToolExecutionsForMCP(toolExecutions) {
42
85
  if (!toolExecutions || !Array.isArray(toolExecutions)) {
43
86
  return [];
44
87
  }
45
- return toolExecutions.map((te) => {
88
+ return toolExecutions.map((te, index) => {
46
89
  const teRecord = te;
90
+ // Enhanced tool name extraction matching the main function
91
+ let toolName = teRecord.name ||
92
+ teRecord.toolName ||
93
+ teRecord.tool ||
94
+ "";
95
+ // Try nested toolCall extraction
96
+ if (!toolName &&
97
+ teRecord.toolCall &&
98
+ typeof teRecord.toolCall === "object") {
99
+ const toolCall = teRecord.toolCall;
100
+ toolName =
101
+ toolCall.name || toolCall.toolName || "";
102
+ }
103
+ // Fallback to avoid empty names
104
+ if (!toolName) {
105
+ toolName = `mcp_tool_execution_${index}`;
106
+ }
107
+ // Enhanced execution time extraction
108
+ let executionTime = teRecord.duration ??
109
+ teRecord.executionTime ??
110
+ teRecord.responseTime ??
111
+ 0;
112
+ // Enhanced success detection - check for actual success indicators
113
+ let success = true; // Default to true
114
+ // Check for explicit success/error indicators
115
+ if (teRecord.success !== undefined) {
116
+ success = Boolean(teRecord.success);
117
+ }
118
+ else if (teRecord.error !== undefined) {
119
+ success = false;
120
+ }
121
+ else if (teRecord.status !== undefined) {
122
+ const status = String(teRecord.status).toLowerCase().trim();
123
+ success = !["error", "failed", "failure", "fail"].includes(status);
124
+ }
125
+ // Enhanced server ID extraction
126
+ let serverId = teRecord.serverId ||
127
+ teRecord.server ||
128
+ teRecord.source ||
129
+ undefined;
130
+ // Try to extract from nested structures
131
+ if (!serverId &&
132
+ teRecord.toolCall &&
133
+ typeof teRecord.toolCall === "object") {
134
+ const toolCall = teRecord.toolCall;
135
+ serverId =
136
+ toolCall.serverId ||
137
+ toolCall.server ||
138
+ undefined;
139
+ }
47
140
  return {
48
- toolName: teRecord.name || "",
49
- executionTime: teRecord.duration || 0,
50
- success: true, // Assume success if tool executed (AI providers handle failures differently)
51
- serverId: teRecord.serverId || undefined,
141
+ toolName: toolName,
142
+ executionTime: executionTime,
143
+ success: success,
144
+ serverId: serverId,
52
145
  };
53
146
  });
54
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "7.14.6",
3
+ "version": "7.14.8",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 9 major providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",