@juspay/neurolink
Version:
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
453 lines (452 loc) • 18.9 kB
JavaScript
/**
* Parameter Validation Utilities
* Provides consistent parameter validation across all tool interfaces
*/
import { SYSTEM_LIMITS } from "../core/constants.js";
import { isNonNullObject } from "./typeUtils.js";
// ============================================================================
// VALIDATION ERROR TYPES
// ============================================================================
/**
* Custom error class for parameter validation failures
* Provides detailed information about validation errors including field context and suggestions
*/
export class ValidationError extends Error {
field;
code;
suggestions;
/**
* Creates a new ValidationError
* @param message - Human-readable error message
* @param field - Name of the field that failed validation (optional)
* @param code - Error code for programmatic handling (optional)
* @param suggestions - Array of suggested fixes (optional)
*/
constructor(message, field, code, suggestions) {
super(message);
this.field = field;
this.code = code;
this.suggestions = suggestions;
this.name = "ValidationError";
}
}
// ============================================================================
// BASIC PARAMETER VALIDATORS
// ============================================================================
/**
* Validate that a string parameter is present and non-empty
*/
export function validateRequiredString(value, fieldName, minLength = 1) {
if (value === undefined || value === null) {
return new ValidationError(`${fieldName} is required`, fieldName, "REQUIRED_FIELD", [`Provide a valid ${fieldName.toLowerCase()}`]);
}
if (typeof value !== "string") {
return new ValidationError(`${fieldName} must be a string, received ${typeof value}`, fieldName, "INVALID_TYPE", [`Convert ${fieldName.toLowerCase()} to string format`]);
}
if (value.trim().length < minLength) {
return new ValidationError(`${fieldName} must be at least ${minLength} character${minLength > 1 ? "s" : ""} long`, fieldName, "MIN_LENGTH", [`Provide a meaningful ${fieldName.toLowerCase()}`]);
}
return null;
}
/**
* Validate that a number parameter is within acceptable range
*/
export function validateNumberRange(value, fieldName, min, max, required = false) {
if (value === undefined || value === null) {
if (required) {
return new ValidationError(`${fieldName} is required`, fieldName, "REQUIRED_FIELD", [`Provide a number between ${min} and ${max}`]);
}
return null; // Optional field
}
if (typeof value !== "number" || isNaN(value)) {
return new ValidationError(`${fieldName} must be a valid number, received ${typeof value}`, fieldName, "INVALID_TYPE", [`Provide a number between ${min} and ${max}`]);
}
if (value < min || value > max) {
return new ValidationError(`${fieldName} must be between ${min} and ${max}, received ${value}`, fieldName, "OUT_OF_RANGE", [`Use a value between ${min} and ${max}`]);
}
return null;
}
/**
* Validate that a function parameter is async and has correct signature
*/
export function validateAsyncFunction(value, fieldName, expectedParams = []) {
if (typeof value !== "function") {
return new ValidationError(`${fieldName} must be a function, received ${typeof value}`, fieldName, "INVALID_TYPE", [
"Provide an async function",
`Expected signature: async (${expectedParams.join(", ")}) => Promise<unknown>`,
]);
}
// Check if function appears to be async
const funcStr = value.toString();
const isAsync = funcStr.includes("async") || funcStr.includes("Promise");
if (!isAsync) {
return new ValidationError(`${fieldName} must be an async function that returns a Promise`, fieldName, "NOT_ASYNC", [
"Add 'async' keyword to function declaration",
"Return a Promise from the function",
`Example: async (${expectedParams.join(", ")}) => { return result; }`,
]);
}
return null;
}
/**
* Validate object structure with required properties
*/
export function validateObjectStructure(value, fieldName, requiredProperties, optionalProperties = []) {
if (!isNonNullObject(value)) {
return new ValidationError(`${fieldName} must be an object, received ${typeof value}`, fieldName, "INVALID_TYPE", [
`Provide an object with properties: ${requiredProperties.join(", ")}`,
...(optionalProperties.length > 0
? [`Optional properties: ${optionalProperties.join(", ")}`]
: []),
]);
}
const obj = value;
const missingProps = requiredProperties.filter((prop) => !(prop in obj));
if (missingProps.length > 0) {
return new ValidationError(`${fieldName} is missing required properties: ${missingProps.join(", ")}`, fieldName, "MISSING_PROPERTIES", [`Add missing properties: ${missingProps.join(", ")}`]);
}
return null;
}
// ============================================================================
// TOOL-SPECIFIC VALIDATORS
// ============================================================================
/**
* Validate tool name according to naming conventions
*/
export function validateToolName(name) {
const error = validateRequiredString(name, "Tool name", 1);
if (error) {
return error;
}
const toolName = name;
// Check naming conventions
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(toolName)) {
return new ValidationError("Tool name must start with a letter and contain only letters, numbers, underscores, and hyphens", "name", "INVALID_FORMAT", [
"Use alphanumeric characters, underscores, and hyphens only",
"Start with a letter",
"Examples: 'calculateSum', 'data_processor', 'api-client'",
]);
}
if (toolName.length > 64) {
return new ValidationError(`Tool name too long: ${toolName.length} characters (max: 64)`, "name", "MAX_LENGTH", ["Use a shorter, more concise name"]);
}
// Reserved names check
const reservedNames = ["execute", "validate", "setup", "init", "config"];
if (reservedNames.includes(toolName.toLowerCase())) {
return new ValidationError(`Tool name '${toolName}' is reserved`, "name", "RESERVED_NAME", ["Choose a different name", "Add a prefix or suffix to make it unique"]);
}
return null;
}
/**
* Validate tool description for clarity and usefulness
*/
export function validateToolDescription(description) {
const error = validateRequiredString(description, "Tool description", 10);
if (error) {
return error;
}
const desc = description;
if (desc.length > 500) {
return new ValidationError(`Tool description too long: ${desc.length} characters (max: 500)`, "description", "MAX_LENGTH", ["Keep description concise and focused", "Use under 500 characters"]);
}
// Check for meaningful content
const meaningfulWords = desc.split(/\s+/).filter((word) => word.length > 2);
if (meaningfulWords.length < 3) {
return new ValidationError("Tool description should be more descriptive", "description", "TOO_BRIEF", [
"Explain what the tool does",
"Include expected parameters",
"Describe the return value",
]);
}
return null;
}
/**
* Validate MCP tool structure comprehensively
*/
export function validateMCPTool(tool) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(tool)) {
errors.push(new ValidationError("Tool must be an object", "tool", "INVALID_TYPE", [
"Provide a valid tool object with name, description, and execute properties",
]));
return { isValid: false, errors, warnings, suggestions };
}
const mcpTool = tool;
// Validate name
const nameError = validateToolName(mcpTool.name);
if (nameError) {
errors.push(nameError);
}
// Validate description
const descError = validateToolDescription(mcpTool.description);
if (descError) {
errors.push(descError);
}
// Validate execute function
const execError = validateAsyncFunction(mcpTool.execute, "execute function", [
"params",
"context",
]);
if (execError) {
errors.push(execError);
}
// Additional MCP-specific validation
if (mcpTool.execute) {
try {
// Test execute function with mock data
const mockParams = {};
const mockContext = {
sessionId: "validation-test",
userId: "validation-user",
};
const result = mcpTool.execute(mockParams, mockContext);
const returnsPromise = result && typeof result === "object" && "then" in result;
if (!returnsPromise) {
errors.push(new ValidationError("Execute function must return a Promise", "execute", "NOT_PROMISE", [
"Ensure function returns a Promise<ToolResult>",
"Use async/await pattern",
"Return a result object with success property",
]));
}
}
catch (error) {
warnings.push(`Execute function validation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Check optional properties
if (mcpTool.inputSchema && !isNonNullObject(mcpTool.inputSchema)) {
warnings.push("inputSchema should be an object if provided");
suggestions.push("Provide a valid JSON schema or Zod schema for input validation");
}
if (mcpTool.outputSchema && !isNonNullObject(mcpTool.outputSchema)) {
warnings.push("outputSchema should be an object if provided");
suggestions.push("Provide a valid JSON schema or Zod schema for output validation");
}
return {
isValid: errors.length === 0,
errors,
warnings,
suggestions,
};
}
// ============================================================================
// OPTIONS VALIDATORS
// ============================================================================
/**
* Validate text generation options
*/
export function validateTextGenerationOptions(options) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(options)) {
errors.push(new ValidationError("Options must be an object", "options", "INVALID_TYPE"));
return { isValid: false, errors, warnings, suggestions };
}
const opts = options;
// Validate prompt
const promptError = validateRequiredString(opts.prompt, "prompt", 1);
if (promptError) {
errors.push(promptError);
}
if (opts.prompt && opts.prompt.length > SYSTEM_LIMITS.MAX_PROMPT_LENGTH) {
errors.push(new ValidationError(`Prompt too large: ${opts.prompt.length} characters (max: ${SYSTEM_LIMITS.MAX_PROMPT_LENGTH})`, "prompt", "MAX_LENGTH", [
"Break prompt into smaller chunks",
"Use summarization for long content",
"Consider using streaming for large inputs",
]));
}
// Validate temperature
const tempError = validateNumberRange(opts.temperature, "temperature", 0, 2);
if (tempError) {
errors.push(tempError);
}
// Validate maxTokens
const tokensError = validateNumberRange(opts.maxTokens, "maxTokens", 1, 200000);
if (tokensError) {
errors.push(tokensError);
}
// Validate timeout
if (opts.timeout !== undefined) {
if (typeof opts.timeout === "string") {
// Parse string timeouts like "30s", "2m", "1h"
if (!/^\d+[smh]?$/.test(opts.timeout)) {
errors.push(new ValidationError("Invalid timeout format. Use number (ms) or string like '30s', '2m', '1h'", "timeout", "INVALID_FORMAT", ["Use format: 30000 (ms), '30s', '2m', or '1h'"]));
}
}
else if (typeof opts.timeout === "number") {
if (opts.timeout < 1000 || opts.timeout > 600000) {
warnings.push("Timeout outside recommended range (1s - 10m)");
suggestions.push("Use timeout between 1000ms (1s) and 600000ms (10m)");
}
}
else {
errors.push(new ValidationError("Timeout must be a number (ms) or string", "timeout", "INVALID_TYPE"));
}
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
/**
* Validate stream options
*/
export function validateStreamOptions(options) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(options)) {
errors.push(new ValidationError("Options must be an object", "options", "INVALID_TYPE"));
return { isValid: false, errors, warnings, suggestions };
}
const opts = options;
// Validate input
if (!opts.input || !isNonNullObject(opts.input)) {
errors.push(new ValidationError("input is required and must be an object with text property", "input", "REQUIRED_FIELD", ["Provide input: { text: 'your prompt here' }"]));
}
else {
const inputError = validateRequiredString(opts.input.text, "input.text", 1);
if (inputError) {
errors.push(inputError);
}
}
// Validate temperature
const tempError = validateNumberRange(opts.temperature, "temperature", 0, 2);
if (tempError) {
errors.push(tempError);
}
// Validate maxTokens
const tokensError = validateNumberRange(opts.maxTokens, "maxTokens", 1, 200000);
if (tokensError) {
errors.push(tokensError);
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
/**
* Validate generate options (unified interface)
*/
export function validateGenerateOptions(options) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(options)) {
errors.push(new ValidationError("Options must be an object", "options", "INVALID_TYPE"));
return { isValid: false, errors, warnings, suggestions };
}
const opts = options;
// Validate input
if (!opts.input || !isNonNullObject(opts.input)) {
errors.push(new ValidationError("input is required and must be an object with text property", "input", "REQUIRED_FIELD", ["Provide input: { text: 'your prompt here' }"]));
}
else {
const inputError = validateRequiredString(opts.input.text, "input.text", 1);
if (inputError) {
errors.push(inputError);
}
}
// Common validation for temperature and maxTokens
const tempError = validateNumberRange(opts.temperature, "temperature", 0, 2);
if (tempError) {
errors.push(tempError);
}
const tokensError = validateNumberRange(opts.maxTokens, "maxTokens", 1, 200000);
if (tokensError) {
errors.push(tokensError);
}
// Validate factory config if present
if (opts.factoryConfig && !isNonNullObject(opts.factoryConfig)) {
warnings.push("factoryConfig should be an object if provided");
suggestions.push("Provide valid factory configuration or remove the property");
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// PARAMETER TRANSFORMATION VALIDATORS
// ============================================================================
/**
* Validate tool execution parameters
*/
export function validateToolExecutionParams(toolName, params, expectedSchema) {
const errors = [];
const warnings = [];
const suggestions = [];
// Basic parameter validation
if (params !== undefined && params !== null && !isNonNullObject(params)) {
errors.push(new ValidationError(`Parameters for tool '${toolName}' must be an object`, "params", "INVALID_TYPE", ["Provide parameters as an object: { key: value, ... }"]));
return { isValid: false, errors, warnings, suggestions };
}
// Schema validation (if provided)
if (expectedSchema && params) {
try {
// This is a placeholder for actual schema validation
// In practice, you would use Zod or JSON schema validation here
warnings.push("Schema validation not yet implemented");
suggestions.push("Implement Zod schema validation for tool parameters");
}
catch (error) {
errors.push(new ValidationError(`Parameter validation failed: ${error instanceof Error ? error.message : String(error)}`, "params", "SCHEMA_VALIDATION", ["Check parameter format against tool schema"]));
}
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// BATCH VALIDATION UTILITIES
// ============================================================================
/**
* Validate multiple tools at once
*/
export function validateToolBatch(tools) {
const validTools = [];
const invalidTools = [];
const results = {};
for (const [name, tool] of Object.entries(tools)) {
const nameValidation = validateToolName(name);
const toolValidation = validateMCPTool(tool);
const combinedResult = {
isValid: !nameValidation && toolValidation.isValid,
errors: nameValidation
? [nameValidation, ...toolValidation.errors]
: toolValidation.errors,
warnings: toolValidation.warnings,
suggestions: toolValidation.suggestions,
};
results[name] = combinedResult;
if (combinedResult.isValid) {
validTools.push(name);
}
else {
invalidTools.push(name);
}
}
return {
isValid: invalidTools.length === 0,
validTools,
invalidTools,
results,
};
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Create a validation error summary for logging
*/
export function createValidationSummary(result) {
const parts = [];
if (result.errors.length > 0) {
parts.push(`Errors: ${result.errors.map((e) => e.message).join("; ")}`);
}
if (result.warnings.length > 0) {
parts.push(`Warnings: ${result.warnings.join("; ")}`);
}
if (result.suggestions.length > 0) {
parts.push(`Suggestions: ${result.suggestions.join("; ")}`);
}
return parts.join(" | ");
}
/**
* Check if validation result has only warnings (no errors)
*/
export function hasOnlyWarnings(result) {
return result.errors.length === 0 && result.warnings.length > 0;
}