@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
350 lines (349 loc) • 14.2 kB
JavaScript
/**
* NeuroLink SDK Tool Registration API
* Simple interface for developers to register custom tools
*/
import { logger } from "../utils/logger.js";
import { createMCPServerInfo } from "../utils/mcpDefaults.js";
import { validateToolName, validateToolDescription, } from "../utils/parameterValidation.js";
/**
* Configuration constants for tool validation
*/
const envDescValue = parseInt(process.env.NEUROLINK_TOOL_DESCRIPTION_MAX_LENGTH || "200", 10);
// Allow 0 to mean unlimited (no length restriction)
const DEFAULT_DESCRIPTION_MAX_LENGTH = Number.isInteger(envDescValue) && envDescValue >= 0 ? envDescValue : 200;
const envNameValue = parseInt(process.env.NEUROLINK_TOOL_NAME_MAX_LENGTH || "50", 10);
// Allow 0 to mean unlimited (no length restriction)
const DEFAULT_NAME_MAX_LENGTH = Number.isInteger(envNameValue) && envNameValue >= 0 ? envNameValue : 50;
/**
* Enhanced validation configuration
*/
const VALIDATION_CONFIG = {
// Tool name constraints
NAME_MIN_LENGTH: 2,
NAME_MAX_LENGTH: DEFAULT_NAME_MAX_LENGTH,
// Description constraints
DESCRIPTION_MIN_LENGTH: 10,
DESCRIPTION_MAX_LENGTH: DEFAULT_DESCRIPTION_MAX_LENGTH,
// Reserved tool names that cannot be used
RESERVED_NAMES: new Set([
"system",
"internal",
"core",
"ai",
"assistant",
"help",
"debug",
"test",
"mock",
"default",
"config",
"admin",
"root",
"neurolink",
]),
// Recommended tool name patterns
RECOMMENDED_PATTERNS: [
"get_data",
"fetch_info",
"calculate_value",
"send_message",
"create_item",
"update_record",
"delete_file",
"validate_input",
],
// Pre-compiled regex patterns for performance optimization
COMPILED_PATTERN_REGEXES: [
"get_data",
"fetch_info",
"calculate_value",
"send_message",
"create_item",
"update_record",
"delete_file",
"validate_input",
].map((pattern) => new RegExp(pattern.replace(/_/g, "[_-]"), "i")),
};
/**
* Creates a MCPServerInfo from a set of tools
*/
export function createMCPServerFromTools(serverId, tools, metadata) {
const mcpTools = [];
for (const [name, tool] of Object.entries(tools)) {
mcpTools.push({
name,
description: tool.description || name,
inputSchema: {},
});
}
// SMART DEFAULTS: Use utility to eliminate manual MCPServerInfo creation
return createMCPServerInfo({
id: serverId,
name: metadata?.title || serverId,
tools: mcpTools,
description: metadata?.description || serverId,
category: metadata?.category,
// Detect category based on context if not provided
isExternal: metadata?.category === "external",
isBuiltIn: metadata?.category === "built-in",
isCustomTool: false,
});
}
/**
* Helper to create a tool with type safety
*/
export function createTool(config) {
return config;
}
/**
* Helper to create a validated tool with suggested improvements
*/
export function createValidatedTool(name, config, options = {}) {
const { strict = true, suggestions = true } = options;
try {
// Validate the tool
validateTool(name, config);
// Provide helpful suggestions if enabled
if (suggestions) {
provideToolSuggestions(name, config);
}
logger.debug(`Tool '${name}' created and validated successfully`);
return config;
}
catch (error) {
if (strict) {
throw error;
}
// Log warning but continue in non-strict mode
logger.warn(`Tool '${name}' validation failed in non-strict mode`, {
error: error instanceof Error ? error.message : String(error),
});
return config;
}
}
/**
* Provide helpful suggestions for tool improvement
*/
function provideToolSuggestions(name, tool) {
const suggestions = [];
// Check for common improvements
if (!tool.parameters) {
suggestions.push("Consider adding a parameters schema using Zod for better type safety and validation");
}
if (!tool.metadata?.category) {
suggestions.push("Adding a category in metadata helps organize tools: { metadata: { category: 'data' } }");
}
if (!tool.metadata?.version) {
suggestions.push("Adding a version helps track tool updates: { metadata: { version: '1.0.0' } }");
}
// Check description quality
const description = tool.description.toLowerCase();
if (!description.includes("return") && !description.includes("result")) {
suggestions.push("Consider describing what the tool returns for better AI understanding");
}
if (suggestions.length > 0) {
logger.debug(`Tool '${name}' suggestions for improvement:`, {
suggestions: suggestions.slice(0, 3), // Limit to avoid spam
});
}
}
/**
* Helper to create a tool with typed parameters
*/
export function createTypedTool(config) {
return config;
}
/**
* Enhanced tool name validation using centralized utilities
*/
function validateToolNameLegacy(name) {
const error = validateToolName(name);
if (error) {
throw error;
}
// Additional legacy-specific validations
const trimmedName = name.trim();
// Naming convention suggestions using pre-compiled patterns for performance
const hasGoodPattern = VALIDATION_CONFIG.COMPILED_PATTERN_REGEXES.some((patternRegex) => {
return patternRegex.test(trimmedName);
});
if (!hasGoodPattern && trimmedName.length > 10) {
logger.debug(`Tool name '${name}' could follow recommended patterns for better clarity. ` +
`Consider patterns like: ${VALIDATION_CONFIG.RECOMMENDED_PATTERNS.slice(0, 4).join(", ")}`);
}
}
/**
* Enhanced description validation using centralized utilities
*/
function validateToolDescriptionLegacy(name, description) {
const error = validateToolDescription(description);
if (error) {
throw new Error(`Tool '${name}': ${error.message}`);
}
// Additional legacy-specific validations
const trimmedDescription = description.trim();
// Quality suggestions
const hasActionWord = /^(get|fetch|calculate|send|create|update|delete|validate|process|generate|parse|convert)/i.test(trimmedDescription);
if (!hasActionWord) {
logger.debug(`Tool '${name}' description could start with an action word (get, fetch, calculate, etc.) for better clarity: "${trimmedDescription.substring(0, 30)}..."`);
}
}
/**
* Validate tool configuration with detailed error messages
*/
export function validateTool(name, tool) {
// Enhanced tool name validation using centralized utilities
validateToolNameLegacy(name);
// Validate tool object
if (!tool || typeof tool !== "object") {
throw new Error(`Tool '${name}' must be an object with description and execute properties. Received: ${typeof tool}. ` +
`Expected format: { description: "Tool description", execute: async (params) => { ... } }`);
}
// Enhanced description validation using centralized utilities
validateToolDescriptionLegacy(name, tool.description);
// Validate execute function with signature guidance
if (typeof tool.execute !== "function") {
throw new Error(`Tool '${name}' must have an execute function. ` +
`Expected signature: async (params?: ToolArgs) => Promise<unknown>. ` +
`Received: ${typeof tool.execute}. ` +
`Example: { execute: async (params) => { return { success: true, data: result }; } }`);
}
// Check for common mistake: using 'schema' instead of 'parameters'
if ("schema" in tool && !("parameters" in tool)) {
throw new Error(`Tool '${name}' uses 'schema' property, but NeuroLink expects 'parameters'. ` +
`Please change 'schema' to 'parameters' and use a Zod schema: ` +
`{ parameters: z.object({ ... }), execute: ... } ` +
`See documentation: https://docs.neurolink.com/tools`);
}
// Validate parameters schema if provided - support both Zod and custom schemas
if (tool.parameters) {
if (typeof tool.parameters !== "object") {
throw new Error(`Tool '${name}' parameters must be an object. ` +
`Received: ${typeof tool.parameters}`);
}
// Check for common schema validation methods (Zod uses 'parse', others might use 'validate')
const params = tool.parameters;
const hasValidationMethod = typeof params.parse === "function" ||
typeof params.validate === "function" ||
"_def" in params; // Zod schemas have _def property
// Check for plain JSON schema objects (common mistake)
if ("type" in params && "properties" in params && !hasValidationMethod) {
throw new Error(`Tool '${name}' appears to use a plain JSON schema object as parameters. ` +
`NeuroLink requires a Zod schema for proper type validation and tool integration. ` +
`Please change from:\n` +
` { type: 'object', properties: { ... } }\n` +
`To:\n` +
` z.object({ fieldName: z.string() })\n` +
`Import Zod with: import { z } from 'zod'`);
}
if (!hasValidationMethod) {
const errorMessage = typeof params.parse === "function" || "_def" in params
? `Tool '${name}' has a Zod-like schema but validation failed. Ensure it's a valid Zod schema: z.object({ ... })`
: typeof params.validate === "function"
? `Tool '${name}' has a validate method but it may not be callable. Ensure: { parameters: { validate: (data) => { ... } } }`
: `Tool '${name}' parameters must be a schema object with validation. ` +
`Supported formats:\n` +
`• Zod schema: { parameters: z.object({ value: z.string() }) }\n` +
`• Custom schema: { parameters: { validate: (data) => { ... } } }\n` +
`• Custom schema: { parameters: { parse: (data) => { ... } } }`;
throw new Error(errorMessage);
}
}
// Validate metadata if provided
if (tool.metadata) {
if (typeof tool.metadata !== "object" || Array.isArray(tool.metadata)) {
throw new Error(`Tool '${name}' metadata must be an object. Received: ${typeof tool.metadata}. ` +
`Example: { category: "data", version: "1.0.0", author: "team@company.com" }`);
}
// Validate metadata fields
if (tool.metadata.version && typeof tool.metadata.version !== "string") {
throw new Error(`Tool '${name}' metadata.version must be a string. Received: ${typeof tool.metadata.version}. ` +
`Example: "1.0.0", "2.1.3-beta"`);
}
if (tool.metadata.category && typeof tool.metadata.category !== "string") {
throw new Error(`Tool '${name}' metadata.category must be a string. Received: ${typeof tool.metadata.category}. ` +
`Example: "data", "communication", "utility"`);
}
if (tool.metadata.tags && !Array.isArray(tool.metadata.tags)) {
throw new Error(`Tool '${name}' metadata.tags must be an array of strings. Received: ${typeof tool.metadata.tags}. ` +
`Example: ["api", "external", "web"]`);
}
}
// Success feedback for debugging
logger.debug(`Tool '${name}' validation passed`, {
nameLength: name.length,
descriptionLength: tool.description.length,
hasParameters: !!tool.parameters,
hasMetadata: !!tool.metadata,
});
}
/**
* Utility to validate multiple tools at once
*/
export function validateTools(tools) {
const valid = [];
const invalid = [];
for (const [name, tool] of Object.entries(tools)) {
try {
validateTool(name, tool);
valid.push(name);
}
catch (error) {
invalid.push({
name,
error: error instanceof Error ? error.message : String(error),
});
}
}
logger.debug(`Bulk validation completed`, {
validCount: valid.length,
invalidCount: invalid.length,
totalTools: Object.keys(tools).length,
});
return { valid, invalid };
}
/**
* Get validation configuration for external inspection
*/
export function getValidationConfig() {
return { ...VALIDATION_CONFIG };
}
/**
* Check if a tool name is available (not reserved)
*/
export function isToolNameAvailable(name) {
if (!name || typeof name !== "string") {
return false;
}
const trimmedName = name.trim().toLowerCase();
return (trimmedName.length >= VALIDATION_CONFIG.NAME_MIN_LENGTH &&
trimmedName.length <= VALIDATION_CONFIG.NAME_MAX_LENGTH &&
!VALIDATION_CONFIG.RESERVED_NAMES.has(trimmedName) &&
/^[a-zA-Z0-9_-]+$/.test(trimmedName));
}
/**
* Suggest alternative tool names if the provided name is invalid
*/
export function suggestToolNames(baseName) {
if (!baseName || typeof baseName !== "string") {
return VALIDATION_CONFIG.RECOMMENDED_PATTERNS.slice(0, 3);
}
const cleanBase = baseName.toLowerCase().replace(/[^a-z0-9]/g, "_");
const suggestions = [];
// Add suffixes if the name is reserved
if (VALIDATION_CONFIG.RESERVED_NAMES.has(cleanBase)) {
suggestions.push(`${cleanBase}_tool`, `custom_${cleanBase}`, `${cleanBase}_helper`);
}
// Add pattern-based suggestions
const patterns = ["get_", "fetch_", "create_", "update_"];
patterns.forEach((pattern) => {
if (!cleanBase.startsWith(pattern.slice(0, -1))) {
suggestions.push(`${pattern}${cleanBase}`);
}
});
// Add recommended patterns if no good suggestions
if (suggestions.length === 0) {
suggestions.push(...VALIDATION_CONFIG.RECOMMENDED_PATTERNS.slice(0, 3));
}
return suggestions.slice(0, 5); // Limit to 5 suggestions
}