@juspay/neurolink 7.29.0 → 7.29.2
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 +12 -0
- package/dist/cli/commands/config.d.ts +3 -3
- package/dist/cli/commands/mcp.js +25 -0
- package/dist/cli/factories/commandFactory.d.ts +1 -0
- package/dist/cli/factories/commandFactory.js +115 -21
- package/dist/cli/index.js +8 -0
- package/dist/core/factory.js +77 -4
- package/dist/factories/providerFactory.js +3 -0
- package/dist/factories/providerRegistry.js +2 -2
- package/dist/lib/core/factory.js +77 -4
- package/dist/lib/factories/providerFactory.js +3 -0
- package/dist/lib/factories/providerRegistry.js +2 -2
- package/dist/lib/mcp/externalServerManager.js +13 -14
- package/dist/lib/mcp/flexibleToolValidator.d.ts +50 -0
- package/dist/lib/mcp/flexibleToolValidator.js +161 -0
- package/dist/lib/mcp/toolRegistry.d.ts +2 -2
- package/dist/lib/mcp/toolRegistry.js +25 -50
- package/dist/lib/neurolink.d.ts +299 -4
- package/dist/lib/neurolink.js +434 -73
- package/dist/lib/providers/amazonBedrock.d.ts +47 -6
- package/dist/lib/providers/amazonBedrock.js +282 -23
- package/dist/lib/providers/aws/credentialProvider.d.ts +58 -0
- package/dist/lib/providers/aws/credentialProvider.js +267 -0
- package/dist/lib/providers/aws/credentialTester.d.ts +49 -0
- package/dist/lib/providers/aws/credentialTester.js +394 -0
- package/dist/lib/providers/googleVertex.js +13 -4
- package/dist/lib/proxy/awsProxyIntegration.d.ts +23 -0
- package/dist/lib/proxy/awsProxyIntegration.js +285 -0
- package/dist/lib/proxy/proxyFetch.d.ts +9 -5
- package/dist/lib/proxy/proxyFetch.js +232 -98
- package/dist/lib/proxy/utils/noProxyUtils.d.ts +39 -0
- package/dist/lib/proxy/utils/noProxyUtils.js +149 -0
- package/dist/lib/types/providers.d.ts +43 -0
- package/dist/lib/utils/providerConfig.d.ts +1 -0
- package/dist/lib/utils/providerConfig.js +2 -1
- package/dist/lib/utils/providerHealth.js +123 -5
- package/dist/mcp/externalServerManager.js +13 -14
- package/dist/mcp/flexibleToolValidator.d.ts +50 -0
- package/dist/mcp/flexibleToolValidator.js +161 -0
- package/dist/mcp/toolRegistry.d.ts +2 -2
- package/dist/mcp/toolRegistry.js +25 -50
- package/dist/neurolink.d.ts +299 -4
- package/dist/neurolink.js +434 -73
- package/dist/providers/amazonBedrock.d.ts +47 -6
- package/dist/providers/amazonBedrock.js +282 -23
- package/dist/providers/aws/credentialProvider.d.ts +58 -0
- package/dist/providers/aws/credentialProvider.js +267 -0
- package/dist/providers/aws/credentialTester.d.ts +49 -0
- package/dist/providers/aws/credentialTester.js +394 -0
- package/dist/providers/googleVertex.js +13 -4
- package/dist/proxy/awsProxyIntegration.d.ts +23 -0
- package/dist/proxy/awsProxyIntegration.js +285 -0
- package/dist/proxy/proxyFetch.d.ts +9 -5
- package/dist/proxy/proxyFetch.js +232 -98
- package/dist/proxy/utils/noProxyUtils.d.ts +39 -0
- package/dist/proxy/utils/noProxyUtils.js +149 -0
- package/dist/types/providers.d.ts +43 -0
- package/dist/utils/providerConfig.d.ts +1 -0
- package/dist/utils/providerConfig.js +2 -1
- package/dist/utils/providerHealth.js +123 -5
- package/package.json +5 -1
|
@@ -359,9 +359,10 @@ export function createAnthropicBaseConfig() {
|
|
|
359
359
|
// =============================================================================
|
|
360
360
|
/**
|
|
361
361
|
* Gets AWS Region with default fallback
|
|
362
|
+
* Supports both AWS_REGION and AWS_DEFAULT_REGION for broader compatibility
|
|
362
363
|
*/
|
|
363
364
|
export function getAWSRegion() {
|
|
364
|
-
return process.env.AWS_REGION || "us-east-1";
|
|
365
|
+
return (process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1");
|
|
365
366
|
}
|
|
366
367
|
/**
|
|
367
368
|
* Gets AWS Session Token if available
|
|
@@ -130,6 +130,14 @@ export class ProviderHealthChecker {
|
|
|
130
130
|
*/
|
|
131
131
|
static async checkEnvironmentConfiguration(providerName, healthStatus) {
|
|
132
132
|
const requiredEnvVars = this.getRequiredEnvironmentVariables(providerName);
|
|
133
|
+
logger.debug(`[ProviderHealthChecker] Checking environment configuration for ${providerName}`, {
|
|
134
|
+
requiredEnvVars,
|
|
135
|
+
presentEnvVars: requiredEnvVars.map((envVar) => ({
|
|
136
|
+
name: envVar,
|
|
137
|
+
present: !!process.env[envVar],
|
|
138
|
+
hasValue: !!(process.env[envVar] && process.env[envVar].trim() !== ""),
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
133
141
|
let allConfigured = true;
|
|
134
142
|
const missingVars = [];
|
|
135
143
|
for (const envVar of requiredEnvVars) {
|
|
@@ -140,6 +148,12 @@ export class ProviderHealthChecker {
|
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
healthStatus.isConfigured = allConfigured;
|
|
151
|
+
logger.debug(`[ProviderHealthChecker] Environment configuration result for ${providerName}`, {
|
|
152
|
+
isConfigured: allConfigured,
|
|
153
|
+
missingVars,
|
|
154
|
+
totalRequired: requiredEnvVars.length,
|
|
155
|
+
totalMissing: missingVars.length,
|
|
156
|
+
});
|
|
143
157
|
if (!allConfigured) {
|
|
144
158
|
healthStatus.configurationIssues.push(`Missing required environment variables: ${missingVars.join(", ")}`);
|
|
145
159
|
healthStatus.recommendations.push(`Set the following environment variables: ${missingVars.join(", ")}`);
|
|
@@ -211,6 +225,12 @@ export class ProviderHealthChecker {
|
|
|
211
225
|
}
|
|
212
226
|
return;
|
|
213
227
|
}
|
|
228
|
+
// Providers that don't use API keys directly
|
|
229
|
+
if (providerName === AIProviderName.OLLAMA ||
|
|
230
|
+
providerName === AIProviderName.BEDROCK) {
|
|
231
|
+
healthStatus.hasApiKey = true;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
214
234
|
// 🔧 STANDARD HANDLING FOR OTHER PROVIDERS
|
|
215
235
|
const apiKeyVar = this.getApiKeyEnvironmentVariable(providerName);
|
|
216
236
|
const apiKey = process.env[apiKeyVar];
|
|
@@ -342,7 +362,9 @@ export class ProviderHealthChecker {
|
|
|
342
362
|
case AIProviderName.GOOGLE_AI:
|
|
343
363
|
return ["GOOGLE_AI_API_KEY"];
|
|
344
364
|
case AIProviderName.BEDROCK:
|
|
345
|
-
|
|
365
|
+
// Bedrock credentials are resolved via AWS SDK default provider chain.
|
|
366
|
+
// Region/auth validated in provider-specific checks.
|
|
367
|
+
return [];
|
|
346
368
|
case AIProviderName.AZURE:
|
|
347
369
|
return ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"];
|
|
348
370
|
case AIProviderName.OLLAMA:
|
|
@@ -556,13 +578,109 @@ export class ProviderHealthChecker {
|
|
|
556
578
|
}
|
|
557
579
|
break;
|
|
558
580
|
}
|
|
559
|
-
case AIProviderName.BEDROCK:
|
|
560
|
-
|
|
561
|
-
|
|
581
|
+
case AIProviderName.BEDROCK: {
|
|
582
|
+
logger.debug("Starting AWS Bedrock comprehensive health check", {
|
|
583
|
+
providerName,
|
|
584
|
+
});
|
|
585
|
+
// Check AWS region configuration
|
|
586
|
+
const awsRegion = process.env.AWS_REGION;
|
|
587
|
+
const validBedrockRegions = [
|
|
588
|
+
"us-east-1",
|
|
589
|
+
"us-west-2",
|
|
590
|
+
"ap-southeast-1",
|
|
591
|
+
"ap-northeast-1",
|
|
592
|
+
"eu-central-1",
|
|
593
|
+
"eu-west-1",
|
|
594
|
+
"ap-south-1",
|
|
595
|
+
];
|
|
596
|
+
logger.debug("AWS Region validation", {
|
|
597
|
+
hasAwsRegion: !!awsRegion,
|
|
598
|
+
awsRegion: awsRegion || "not set",
|
|
599
|
+
validBedrockRegions,
|
|
600
|
+
});
|
|
601
|
+
if (!awsRegion) {
|
|
562
602
|
healthStatus.configurationIssues.push("AWS_REGION not set");
|
|
563
|
-
healthStatus.recommendations.push(
|
|
603
|
+
healthStatus.recommendations.push(`Set AWS_REGION to a Bedrock-supported region: ${validBedrockRegions.join(", ")}`);
|
|
604
|
+
}
|
|
605
|
+
else if (!validBedrockRegions.includes(awsRegion)) {
|
|
606
|
+
healthStatus.configurationIssues.push(`AWS_REGION '${awsRegion}' may not support all Bedrock models`);
|
|
607
|
+
healthStatus.recommendations.push(`Consider using a primary Bedrock region: ${validBedrockRegions.slice(0, 3).join(", ")}`);
|
|
608
|
+
}
|
|
609
|
+
// Check AWS credentials configuration
|
|
610
|
+
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
611
|
+
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
612
|
+
const awsSessionToken = process.env.AWS_SESSION_TOKEN;
|
|
613
|
+
const awsProfile = process.env.AWS_PROFILE;
|
|
614
|
+
logger.debug("AWS Credentials validation", {
|
|
615
|
+
hasAccessKeyId: !!awsAccessKeyId,
|
|
616
|
+
hasSecretAccessKey: !!awsSecretAccessKey,
|
|
617
|
+
hasSessionToken: !!awsSessionToken,
|
|
618
|
+
hasProfile: !!awsProfile,
|
|
619
|
+
authMethod: awsProfile
|
|
620
|
+
? "AWS Profile"
|
|
621
|
+
: awsAccessKeyId
|
|
622
|
+
? "Access Keys"
|
|
623
|
+
: "None detected",
|
|
624
|
+
});
|
|
625
|
+
if (!awsAccessKeyId && !awsProfile) {
|
|
626
|
+
healthStatus.configurationIssues.push("No AWS credentials found");
|
|
627
|
+
healthStatus.recommendations.push("Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or configure AWS_PROFILE");
|
|
628
|
+
}
|
|
629
|
+
else if (awsAccessKeyId && !awsSecretAccessKey) {
|
|
630
|
+
healthStatus.configurationIssues.push("AWS_ACCESS_KEY_ID set but AWS_SECRET_ACCESS_KEY missing");
|
|
631
|
+
healthStatus.recommendations.push("Set AWS_SECRET_ACCESS_KEY to match your AWS_ACCESS_KEY_ID");
|
|
632
|
+
}
|
|
633
|
+
// Check for Bedrock-specific model configuration
|
|
634
|
+
const bedrockModel = process.env.BEDROCK_MODEL || process.env.BEDROCK_MODEL_ID;
|
|
635
|
+
const supportedModels = [
|
|
636
|
+
"anthropic.claude-3-sonnet-20240229-v1:0",
|
|
637
|
+
"anthropic.claude-3-haiku-20240307-v1:0",
|
|
638
|
+
"anthropic.claude-3-opus-20240229-v1:0",
|
|
639
|
+
"anthropic.claude-v2:1",
|
|
640
|
+
"amazon.titan-text-express-v1",
|
|
641
|
+
];
|
|
642
|
+
logger.debug("Bedrock Model validation", {
|
|
643
|
+
hasBedrockModel: !!bedrockModel,
|
|
644
|
+
bedrockModel: bedrockModel || "not set",
|
|
645
|
+
supportedModels: supportedModels.slice(0, 3),
|
|
646
|
+
});
|
|
647
|
+
if (!bedrockModel) {
|
|
648
|
+
healthStatus.recommendations.push("Set BEDROCK_MODEL or BEDROCK_MODEL_ID for faster startup (e.g., anthropic.claude-3-sonnet-20240229-v1:0)");
|
|
649
|
+
}
|
|
650
|
+
else if (!supportedModels.some((model) => model === bedrockModel)) {
|
|
651
|
+
healthStatus.recommendations.push(`Consider using a popular Bedrock model: ${supportedModels.slice(0, 3).join(", ")}`);
|
|
652
|
+
}
|
|
653
|
+
// Check for additional Bedrock configuration
|
|
654
|
+
const bedrockEndpoint = process.env.BEDROCK_ENDPOINT_URL;
|
|
655
|
+
if (bedrockEndpoint) {
|
|
656
|
+
logger.debug("Custom Bedrock endpoint detected", {
|
|
657
|
+
endpoint: bedrockEndpoint,
|
|
658
|
+
});
|
|
659
|
+
if (!bedrockEndpoint.startsWith("https://")) {
|
|
660
|
+
healthStatus.configurationIssues.push("BEDROCK_ENDPOINT_URL should use HTTPS");
|
|
661
|
+
healthStatus.recommendations.push("Update BEDROCK_ENDPOINT_URL to use HTTPS protocol");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// AWS SDK Configuration checks
|
|
665
|
+
const awsConfig = {
|
|
666
|
+
maxAttempts: process.env.AWS_MAX_ATTEMPTS,
|
|
667
|
+
retryMode: process.env.AWS_RETRY_MODE,
|
|
668
|
+
defaultsMode: process.env.AWS_DEFAULTS_MODE,
|
|
669
|
+
};
|
|
670
|
+
logger.debug("AWS SDK Configuration", {
|
|
671
|
+
awsConfig,
|
|
672
|
+
hasAdvancedConfig: Object.values(awsConfig).some(Boolean),
|
|
673
|
+
});
|
|
674
|
+
if (healthStatus.configurationIssues.length === 0) {
|
|
675
|
+
healthStatus.hasApiKey = true;
|
|
676
|
+
logger.debug("AWS Bedrock configuration appears valid", {
|
|
677
|
+
region: awsRegion,
|
|
678
|
+
hasCredentials: !!(awsAccessKeyId || awsProfile),
|
|
679
|
+
hasModel: !!bedrockModel,
|
|
680
|
+
});
|
|
564
681
|
}
|
|
565
682
|
break;
|
|
683
|
+
}
|
|
566
684
|
case AIProviderName.AZURE: {
|
|
567
685
|
// Check Azure OpenAI endpoint
|
|
568
686
|
const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
@@ -664,6 +664,9 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
664
664
|
const delay = Math.min(1000 *
|
|
665
665
|
Math.pow(this.config.restartBackoffMultiplier, instance.reconnectAttempts - 1), 30000);
|
|
666
666
|
mcpLogger.info(`[ExternalServerManager] Scheduling restart for ${serverId} in ${delay}ms (attempt ${instance.reconnectAttempts})`);
|
|
667
|
+
if (instance.restartTimer) {
|
|
668
|
+
return;
|
|
669
|
+
} // already scheduled
|
|
667
670
|
instance.restartTimer = setTimeout(async () => {
|
|
668
671
|
try {
|
|
669
672
|
await this.stopServer(serverId);
|
|
@@ -914,6 +917,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
914
917
|
}
|
|
915
918
|
try {
|
|
916
919
|
mcpLogger.debug(`[ExternalServerManager] Registering ${instance.toolsMap.size} tools with main registry for server: ${serverId}`);
|
|
920
|
+
const registrations = [];
|
|
917
921
|
for (const [toolName, tool] of instance.toolsMap.entries()) {
|
|
918
922
|
const toolId = `${serverId}.${toolName}`;
|
|
919
923
|
const toolInfo = {
|
|
@@ -923,21 +927,16 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
923
927
|
serverId: serverId,
|
|
924
928
|
category: detectCategory({ isExternal: true, serverId }),
|
|
925
929
|
};
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
return await this.executeTool(serverId, toolName, params, { timeout: this.config.defaultTimeout });
|
|
932
|
-
},
|
|
933
|
-
});
|
|
934
|
-
mcpLogger.debug(`[ExternalServerManager] Registered tool with main registry: ${toolId}`);
|
|
935
|
-
}
|
|
936
|
-
catch (registrationError) {
|
|
937
|
-
mcpLogger.warn(`[ExternalServerManager] Failed to register tool ${toolId} with main registry:`, registrationError);
|
|
938
|
-
}
|
|
930
|
+
registrations.push(toolRegistry.registerTool(toolId, toolInfo, {
|
|
931
|
+
execute: async (params) => await this.executeTool(serverId, toolName, params, {
|
|
932
|
+
timeout: this.config.defaultTimeout,
|
|
933
|
+
}),
|
|
934
|
+
}));
|
|
939
935
|
}
|
|
940
|
-
|
|
936
|
+
const results = await Promise.allSettled(registrations);
|
|
937
|
+
const ok = results.filter((r) => r.status === "fulfilled").length;
|
|
938
|
+
const failed = results.length - ok;
|
|
939
|
+
mcpLogger.info(`[ExternalServerManager] Registered ${ok}/${results.length} tools with main registry for ${serverId}${failed ? ` (${failed} failed)` : ""}`);
|
|
941
940
|
}
|
|
942
941
|
catch (error) {
|
|
943
942
|
mcpLogger.error(`[ExternalServerManager] Failed to register tools with main registry for ${serverId}:`, error);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexibleToolValidator - Universal Safety Checks Only
|
|
3
|
+
*
|
|
4
|
+
* Following Anthropic's MCP specification which intentionally leaves tool naming flexible,
|
|
5
|
+
* this validator only blocks truly dangerous cases to support maximum MCP tool compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1 Implementation:
|
|
8
|
+
* - Universal safety checks only (empty names, control characters, excessive length)
|
|
9
|
+
* - No context-specific validation or arbitrary pattern restrictions
|
|
10
|
+
* - Designed to support ALL legitimate MCP tools (github.create_repo, filesystem.read_file, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export interface FlexibleValidationResult {
|
|
13
|
+
isValid: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
warnings?: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare class FlexibleToolValidator {
|
|
18
|
+
private static readonly MAX_TOOL_NAME_LENGTH;
|
|
19
|
+
private static readonly MIN_TOOL_NAME_LENGTH;
|
|
20
|
+
/**
|
|
21
|
+
* Validate tool name with universal safety checks only
|
|
22
|
+
*
|
|
23
|
+
* This method only blocks truly dangerous cases:
|
|
24
|
+
* 1. Empty or whitespace-only names
|
|
25
|
+
* 2. Control characters that could break systems
|
|
26
|
+
* 3. Excessively long names that could cause memory issues
|
|
27
|
+
*
|
|
28
|
+
* Everything else is allowed to support maximum MCP tool compatibility.
|
|
29
|
+
*/
|
|
30
|
+
static validateToolName(toolId: string): FlexibleValidationResult;
|
|
31
|
+
/**
|
|
32
|
+
* Validate tool information with minimal safety checks
|
|
33
|
+
*/
|
|
34
|
+
static validateToolInfo(toolId: string, toolInfo: {
|
|
35
|
+
description?: string;
|
|
36
|
+
serverId?: string;
|
|
37
|
+
}): FlexibleValidationResult;
|
|
38
|
+
/**
|
|
39
|
+
* Get information about what this validator checks
|
|
40
|
+
*/
|
|
41
|
+
static getValidationInfo(): {
|
|
42
|
+
philosophy: string;
|
|
43
|
+
checks: string[];
|
|
44
|
+
whatIsAllowed: string[];
|
|
45
|
+
examples: {
|
|
46
|
+
valid: string[];
|
|
47
|
+
invalid: string[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexibleToolValidator - Universal Safety Checks Only
|
|
3
|
+
*
|
|
4
|
+
* Following Anthropic's MCP specification which intentionally leaves tool naming flexible,
|
|
5
|
+
* this validator only blocks truly dangerous cases to support maximum MCP tool compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1 Implementation:
|
|
8
|
+
* - Universal safety checks only (empty names, control characters, excessive length)
|
|
9
|
+
* - No context-specific validation or arbitrary pattern restrictions
|
|
10
|
+
* - Designed to support ALL legitimate MCP tools (github.create_repo, filesystem.read_file, etc.)
|
|
11
|
+
*/
|
|
12
|
+
import { registryLogger } from "../utils/logger.js";
|
|
13
|
+
export class FlexibleToolValidator {
|
|
14
|
+
// Universal safety limits (generous to support all legitimate tools)
|
|
15
|
+
static MAX_TOOL_NAME_LENGTH = 1000; // Much more generous than npm's 214
|
|
16
|
+
static MIN_TOOL_NAME_LENGTH = 1;
|
|
17
|
+
/**
|
|
18
|
+
* Validate tool name with universal safety checks only
|
|
19
|
+
*
|
|
20
|
+
* This method only blocks truly dangerous cases:
|
|
21
|
+
* 1. Empty or whitespace-only names
|
|
22
|
+
* 2. Control characters that could break systems
|
|
23
|
+
* 3. Excessively long names that could cause memory issues
|
|
24
|
+
*
|
|
25
|
+
* Everything else is allowed to support maximum MCP tool compatibility.
|
|
26
|
+
*/
|
|
27
|
+
static validateToolName(toolId) {
|
|
28
|
+
const warnings = [];
|
|
29
|
+
// Safety Check 1: Empty or whitespace-only names
|
|
30
|
+
if (!toolId || typeof toolId !== "string") {
|
|
31
|
+
return {
|
|
32
|
+
isValid: false,
|
|
33
|
+
error: "Tool name is required and must be a string",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Safety Check 2: Control characters that could break systems (check BEFORE trimming!)
|
|
37
|
+
// Only block truly dangerous control characters, not printable characters
|
|
38
|
+
//
|
|
39
|
+
// This regex blocks dangerous C0 control characters and DEL:
|
|
40
|
+
// - \x00-\x08: NULL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, BS
|
|
41
|
+
// - \x0B: Vertical Tab (VT)
|
|
42
|
+
// - \x0C: Form Feed (FF)
|
|
43
|
+
// - \x0E-\x1F: SO, SI, DLE, DC1-4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS-US
|
|
44
|
+
// - \x7F: DEL
|
|
45
|
+
//
|
|
46
|
+
// Explicitly ALLOWS these printable control characters:
|
|
47
|
+
// - \x09: TAB (horizontal tab) - commonly used in text
|
|
48
|
+
// - \x0A: LF (line feed) - commonly used in text
|
|
49
|
+
// - \x0D: CR (carriage return) - commonly used in text
|
|
50
|
+
// eslint-disable-next-line no-control-regex
|
|
51
|
+
const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(toolId);
|
|
52
|
+
if (hasControlCharacters) {
|
|
53
|
+
return {
|
|
54
|
+
isValid: false,
|
|
55
|
+
error: "Tool name contains control characters that could break systems",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const trimmedName = toolId.trim();
|
|
59
|
+
if (trimmedName.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
isValid: false,
|
|
62
|
+
error: "Tool name cannot be empty or whitespace-only",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Safety Check 3: Length limits (very generous)
|
|
66
|
+
if (trimmedName.length < this.MIN_TOOL_NAME_LENGTH) {
|
|
67
|
+
return {
|
|
68
|
+
isValid: false,
|
|
69
|
+
error: `Tool name must be at least ${this.MIN_TOOL_NAME_LENGTH} character long`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (trimmedName.length > this.MAX_TOOL_NAME_LENGTH) {
|
|
73
|
+
return {
|
|
74
|
+
isValid: false,
|
|
75
|
+
error: `Tool name exceeds maximum length of ${this.MAX_TOOL_NAME_LENGTH} characters`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Optional warnings for unusual but not dangerous patterns
|
|
79
|
+
if (trimmedName !== toolId) {
|
|
80
|
+
warnings.push("Tool name has leading/trailing whitespace (will be trimmed)");
|
|
81
|
+
}
|
|
82
|
+
if (trimmedName.length > 200) {
|
|
83
|
+
warnings.push("Tool name is unusually long but allowed");
|
|
84
|
+
}
|
|
85
|
+
registryLogger.debug(`✅ FlexibleToolValidator: Tool '${toolId}' passed universal safety checks`);
|
|
86
|
+
return {
|
|
87
|
+
isValid: true,
|
|
88
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Validate tool information with minimal safety checks
|
|
93
|
+
*/
|
|
94
|
+
static validateToolInfo(toolId, toolInfo) {
|
|
95
|
+
// First validate the tool name
|
|
96
|
+
const nameValidation = this.validateToolName(toolId);
|
|
97
|
+
if (!nameValidation.isValid) {
|
|
98
|
+
return nameValidation;
|
|
99
|
+
}
|
|
100
|
+
const warnings = [...(nameValidation.warnings || [])];
|
|
101
|
+
// Minimal safety checks for tool info
|
|
102
|
+
if (toolInfo.description && typeof toolInfo.description !== "string") {
|
|
103
|
+
return {
|
|
104
|
+
isValid: false,
|
|
105
|
+
error: "Tool description must be a string if provided",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (toolInfo.serverId && typeof toolInfo.serverId !== "string") {
|
|
109
|
+
return {
|
|
110
|
+
isValid: false,
|
|
111
|
+
error: "Tool serverId must be a string if provided",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
registryLogger.debug(`✅ FlexibleToolValidator: Tool info for '${toolId}' passed validation`);
|
|
115
|
+
return {
|
|
116
|
+
isValid: true,
|
|
117
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get information about what this validator checks
|
|
122
|
+
*/
|
|
123
|
+
static getValidationInfo() {
|
|
124
|
+
return {
|
|
125
|
+
philosophy: "Maximum flexibility with universal safety only - following Anthropic's MCP specification",
|
|
126
|
+
checks: [
|
|
127
|
+
"Empty or whitespace-only names",
|
|
128
|
+
"Excessive length (over 1000 characters)",
|
|
129
|
+
"Control characters that could break systems",
|
|
130
|
+
],
|
|
131
|
+
whatIsAllowed: [
|
|
132
|
+
"Dots (github.create_repo, filesystem.read_file)",
|
|
133
|
+
"Hyphens and underscores (my-tool, user_helper)",
|
|
134
|
+
"Numbers (tool1, my_tool_v2)",
|
|
135
|
+
"Unicode characters (🚀_tool, café_manager)",
|
|
136
|
+
"Mixed case (createRepo, ReadFile)",
|
|
137
|
+
"Long descriptive names (enterprise_database_connection_manager)",
|
|
138
|
+
"Any legitimate MCP tool naming pattern",
|
|
139
|
+
],
|
|
140
|
+
examples: {
|
|
141
|
+
valid: [
|
|
142
|
+
"github.create_repo",
|
|
143
|
+
"filesystem.read_file",
|
|
144
|
+
"my-custom-tool",
|
|
145
|
+
"user_helper",
|
|
146
|
+
"tool1",
|
|
147
|
+
"🚀_rocket_tool",
|
|
148
|
+
"enterprise.database.connection.manager",
|
|
149
|
+
"UPPERCASE_TOOL",
|
|
150
|
+
"mixed_Case.Tool-Name_123",
|
|
151
|
+
],
|
|
152
|
+
invalid: [
|
|
153
|
+
"", // Empty
|
|
154
|
+
" ", // Whitespace only
|
|
155
|
+
"tool\x00", // Control character
|
|
156
|
+
"a".repeat(1001), // Too long
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -29,7 +29,7 @@ export interface ToolExecutionOptions {
|
|
|
29
29
|
}
|
|
30
30
|
export declare class MCPToolRegistry extends MCPRegistry {
|
|
31
31
|
private tools;
|
|
32
|
-
private
|
|
32
|
+
private toolImplementations;
|
|
33
33
|
private toolExecutionStats;
|
|
34
34
|
private builtInServerInfos;
|
|
35
35
|
constructor();
|
|
@@ -122,7 +122,7 @@ export declare class MCPToolRegistry extends MCPRegistry {
|
|
|
122
122
|
* Register a tool with implementation directly
|
|
123
123
|
* This is used for external MCP server tools
|
|
124
124
|
*/
|
|
125
|
-
registerTool(toolId: string, toolInfo: ToolInfo, toolImpl: ToolImplementation): void
|
|
125
|
+
registerTool(toolId: string, toolInfo: ToolInfo, toolImpl: ToolImplementation): Promise<void>;
|
|
126
126
|
/**
|
|
127
127
|
* Remove a tool
|
|
128
128
|
*/
|
package/dist/mcp/toolRegistry.js
CHANGED
|
@@ -8,9 +8,10 @@ import { randomUUID } from "crypto";
|
|
|
8
8
|
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
9
9
|
import { directAgentTools } from "../agent/directTools.js";
|
|
10
10
|
import { detectCategory, createMCPServerInfo } from "../utils/mcpDefaults.js";
|
|
11
|
+
import { FlexibleToolValidator } from "./flexibleToolValidator.js";
|
|
11
12
|
export class MCPToolRegistry extends MCPRegistry {
|
|
12
13
|
tools = new Map();
|
|
13
|
-
|
|
14
|
+
toolImplementations = new Map(); // Store actual tool implementations
|
|
14
15
|
toolExecutionStats = new Map();
|
|
15
16
|
builtInServerInfos = []; // DIRECT storage for MCPServerInfo
|
|
16
17
|
constructor() {
|
|
@@ -38,7 +39,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
38
39
|
category: detectCategory({ isBuiltIn: true, serverId: "direct" }),
|
|
39
40
|
};
|
|
40
41
|
this.tools.set(toolId, toolInfo);
|
|
41
|
-
this.
|
|
42
|
+
this.toolImplementations.set(toolId, {
|
|
42
43
|
execute: async (params, context) => {
|
|
43
44
|
try {
|
|
44
45
|
// Direct tools from AI SDK expect their specific parameter structure
|
|
@@ -153,7 +154,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
153
154
|
// Register only with fully-qualified toolId to avoid collisions
|
|
154
155
|
this.tools.set(toolId, toolInfo);
|
|
155
156
|
// Store the actual tool implementation for execution using toolId as key
|
|
156
|
-
this.
|
|
157
|
+
this.toolImplementations.set(toolId, {
|
|
157
158
|
execute: tool.execute ||
|
|
158
159
|
(async () => {
|
|
159
160
|
throw new Error(`Tool ${tool.name} has no execute function`);
|
|
@@ -234,9 +235,9 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
234
235
|
...context,
|
|
235
236
|
};
|
|
236
237
|
// Get the tool implementation using the resolved toolId
|
|
237
|
-
const toolImpl = this.
|
|
238
|
+
const toolImpl = this.toolImplementations.get(toolId);
|
|
238
239
|
registryLogger.debug(`Looking for tool '${toolName}' (toolId: '${toolId}'), found: ${!!toolImpl}, type: ${typeof toolImpl?.execute}`);
|
|
239
|
-
registryLogger.debug(`Available tools:`, Array.from(this.
|
|
240
|
+
registryLogger.debug(`Available tools:`, Array.from(this.toolImplementations.keys()));
|
|
240
241
|
if (!toolImpl || typeof toolImpl?.execute !== "function") {
|
|
241
242
|
throw new Error(`Tool '${toolName}' implementation not found or not executable`);
|
|
242
243
|
}
|
|
@@ -450,55 +451,25 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
450
451
|
* Register a tool with implementation directly
|
|
451
452
|
* This is used for external MCP server tools
|
|
452
453
|
*/
|
|
453
|
-
registerTool(toolId, toolInfo, toolImpl) {
|
|
454
|
+
async registerTool(toolId, toolInfo, toolImpl) {
|
|
454
455
|
registryLogger.debug(`Registering tool: ${toolId}`);
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
(
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
catch (error) {
|
|
466
|
-
// Fallback: skip validation if import fails (graceful degradation)
|
|
467
|
-
registryLogger.warn("Tool validation module not available, skipping advanced validation", {
|
|
468
|
-
error: error instanceof Error ? error.message : String(error),
|
|
469
|
-
});
|
|
470
|
-
// Create minimal validation functions
|
|
471
|
-
validateTool = () => { }; // No-op
|
|
472
|
-
isToolNameAvailable = () => true; // Allow all names
|
|
473
|
-
suggestToolNames = () => ["alternative_tool"];
|
|
474
|
-
}
|
|
475
|
-
// Check if tool name is available (not reserved)
|
|
476
|
-
if (!isToolNameAvailable(toolId)) {
|
|
477
|
-
const suggestions = suggestToolNames(toolId);
|
|
478
|
-
registryLogger.error(`Tool registration failed for ${toolId}: Name not available`);
|
|
479
|
-
throw new Error(`Tool name '${toolId}' is not available (reserved or invalid format). ` +
|
|
480
|
-
`Suggested alternatives: ${suggestions.slice(0, 3).join(", ")}`);
|
|
456
|
+
// Universal safety validation using FlexibleToolValidator
|
|
457
|
+
// Only blocks truly dangerous cases to support maximum MCP tool compatibility
|
|
458
|
+
const validation = FlexibleToolValidator.validateToolInfo(toolId, {
|
|
459
|
+
description: toolInfo.description,
|
|
460
|
+
serverId: toolInfo.serverId,
|
|
461
|
+
});
|
|
462
|
+
if (!validation.isValid) {
|
|
463
|
+
registryLogger.error(`Tool registration failed for ${toolId}: ${validation.error}`);
|
|
464
|
+
throw new Error(`Tool validation failed: ${validation.error}`);
|
|
481
465
|
}
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
execute: async () => "",
|
|
486
|
-
parameters: undefined,
|
|
487
|
-
metadata: {
|
|
488
|
-
category: toolInfo.category,
|
|
489
|
-
serverId: toolInfo.serverId,
|
|
490
|
-
},
|
|
491
|
-
};
|
|
492
|
-
// Use comprehensive validation logic
|
|
493
|
-
try {
|
|
494
|
-
validateTool(toolId, toolForValidation);
|
|
495
|
-
}
|
|
496
|
-
catch (error) {
|
|
497
|
-
registryLogger.error(`Tool registration failed for ${toolId}:`, error instanceof Error ? error.message : String(error));
|
|
498
|
-
throw error;
|
|
466
|
+
// Log any warnings but allow registration to proceed
|
|
467
|
+
if (validation.warnings && validation.warnings.length > 0) {
|
|
468
|
+
registryLogger.warn(`Tool registration warnings for ${toolId}:`, validation.warnings);
|
|
499
469
|
}
|
|
470
|
+
registryLogger.debug(`✅ Tool '${toolId}' passed flexible validation - registration proceeding`);
|
|
500
471
|
this.tools.set(toolId, toolInfo);
|
|
501
|
-
this.
|
|
472
|
+
this.toolImplementations.set(toolId, toolImpl);
|
|
502
473
|
registryLogger.debug(`Successfully registered tool: ${toolId}`);
|
|
503
474
|
}
|
|
504
475
|
/**
|
|
@@ -509,6 +480,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
509
480
|
let removed = false;
|
|
510
481
|
if (this.tools.has(toolName)) {
|
|
511
482
|
this.tools.delete(toolName);
|
|
483
|
+
this.toolImplementations.delete(toolName); // Fix memory leak
|
|
512
484
|
this.toolExecutionStats.delete(toolName);
|
|
513
485
|
registryLogger.info(`Removed tool: ${toolName}`);
|
|
514
486
|
removed = true;
|
|
@@ -518,6 +490,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
518
490
|
for (const [toolId, tool] of Array.from(this.tools.entries())) {
|
|
519
491
|
if (tool.name === toolName) {
|
|
520
492
|
this.tools.delete(toolId);
|
|
493
|
+
this.toolImplementations.delete(toolId); // Fix memory leak
|
|
521
494
|
this.toolExecutionStats.delete(toolId);
|
|
522
495
|
registryLogger.info(`Removed tool: ${toolId}`);
|
|
523
496
|
removed = true;
|
|
@@ -567,6 +540,8 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
567
540
|
for (const [toolId, tool] of this.tools.entries()) {
|
|
568
541
|
if (tool.serverId === serverId) {
|
|
569
542
|
this.tools.delete(toolId);
|
|
543
|
+
this.toolImplementations.delete(toolId); // Fix memory leak
|
|
544
|
+
this.toolExecutionStats.delete(toolId); // Fix memory leak
|
|
570
545
|
removedTools.push(toolId);
|
|
571
546
|
}
|
|
572
547
|
}
|