@juspay/neurolink 7.14.7 → 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 +6 -0
- package/dist/core/baseProvider.js +34 -5
- package/dist/lib/core/baseProvider.js +34 -5
- package/dist/lib/mcp/toolDiscoveryService.js +24 -40
- package/dist/lib/neurolink.js +73 -24
- package/dist/lib/utils/transformationUtils.js +103 -10
- package/dist/mcp/toolDiscoveryService.js +24 -40
- package/dist/neurolink.js +73 -24
- package/dist/utils/transformationUtils.js +103 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## [7.14.7](https://github.com/juspay/neurolink/compare/v7.14.6...v7.14.7) (2025-08-18)
|
|
2
8
|
|
|
3
9
|
### 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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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/lib/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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
logger.debug(`[${functionTag}] MCP generation
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
//
|
|
434
|
-
|
|
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
|
-
//
|
|
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:
|
|
445
|
-
enhancedWithTools:
|
|
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,
|
|
@@ -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:
|
|
31
|
-
input:
|
|
32
|
-
output:
|
|
33
|
-
duration:
|
|
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:
|
|
49
|
-
executionTime:
|
|
50
|
-
success:
|
|
51
|
-
serverId:
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
logger.debug(`[${functionTag}] MCP generation
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
//
|
|
434
|
-
|
|
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
|
-
//
|
|
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:
|
|
445
|
-
enhancedWithTools:
|
|
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,
|
|
@@ -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:
|
|
31
|
-
input:
|
|
32
|
-
output:
|
|
33
|
-
duration:
|
|
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:
|
|
49
|
-
executionTime:
|
|
50
|
-
success:
|
|
51
|
-
serverId:
|
|
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.
|
|
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",
|