Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
tool-validator.ts14.1 kB
/** * Tool Validator - Pre-validate MCP tools before scheduling * Implements MCP-native validation protocol with fallback * * Protocol: Tries tools/validate first (deep validation), falls back to schema validation */ import { logger } from '../../utils/logger.js'; export interface ValidationResult { valid: boolean; errors: string[]; warnings: string[]; validationMethod?: 'mcp-native' | 'schema-only' | 'test-execution'; schema?: any; testExecutionResult?: { success: boolean; output?: any; error?: string; duration: number; }; } export class ToolValidator { constructor(private orchestrator?: any) {} // NCPOrchestrator - using any to avoid circular dependency /** * Validate tool and parameters before scheduling * * Validation Strategy: * 1. Try MCP-native validation (tools/validate) - GOLD STANDARD * 2. Fall back to schema-only validation if not supported * 3. Optional: Test execution as final verification */ async validateTool( tool: string, parameters: Record<string, any>, options?: { testRun?: boolean; // Actually execute the tool once as a test timeout?: number; } ): Promise<ValidationResult> { const errors: string[] = []; const warnings: string[] = []; try { // Parse tool format const [mcpName, toolName] = tool.includes(':') ? tool.split(':') : [null, tool]; if (!mcpName || !toolName) { errors.push(`Invalid tool format: "${tool}". Expected format: "mcp_name:tool_name"`); return { valid: false, errors, warnings }; } // Get or create orchestrator const orchestrator = this.orchestrator || await this.createTemporaryOrchestrator(); const shouldCleanup = !this.orchestrator; // Only cleanup if we created it // Wait for orchestrator to finish indexing before validation if (orchestrator && 'waitForInitialization' in orchestrator) { await (orchestrator as any).waitForInitialization(); } // STEP 1: Try MCP-native validation (tools/validate) logger.info(`[ToolValidator] Attempting MCP-native validation for ${tool}`); const nativeValidation = await this.tryMCPNativeValidation( orchestrator, mcpName, toolName, parameters ); if (nativeValidation.supported && nativeValidation.result) { logger.info(`[ToolValidator] Using MCP-native validation for ${tool}`); if (!nativeValidation.result.valid) { errors.push(...nativeValidation.result.errors); } if (nativeValidation.result.warnings.length > 0) { warnings.push(...nativeValidation.result.warnings); } if (shouldCleanup) { await orchestrator.cleanup(); } return { valid: nativeValidation.result.valid, errors, warnings, validationMethod: 'mcp-native' }; } // STEP 2: Fall back to schema validation logger.info(`[ToolValidator] MCP doesn't support tools/validate, using schema validation`); warnings.push(`MCP "${mcpName}" doesn't support native validation - using basic schema validation only`); warnings.push(`For better validation, implement tools/validate in the MCP`); // Find the tool const toolInfo = await this.findTool(orchestrator, tool); if (!toolInfo) { if (shouldCleanup) { await orchestrator.cleanup(); } errors.push(`Tool not found: ${tool}. Use 'ncp find' to discover available tools.`); return { valid: false, errors, warnings }; } // Validate parameters against schema const schemaValidation = this.validateAgainstSchema( parameters, toolInfo.schema ); errors.push(...schemaValidation.errors); warnings.push(...schemaValidation.warnings); // If schema validation failed, don't proceed to test run if (errors.length > 0) { if (shouldCleanup) { await orchestrator.cleanup(); } return { valid: false, errors, warnings, schema: toolInfo.schema }; } // Optional: Test execution let testExecutionResult; if (options?.testRun) { logger.info(`[ToolValidator] Running test execution for ${tool}`); testExecutionResult = await this.runTestExecution( orchestrator, tool, parameters, options.timeout || 30000 // 30 second default timeout for test ); if (!testExecutionResult.success) { errors.push(`Test execution failed: ${testExecutionResult.error}`); } else { warnings.push('Test execution succeeded, but actual execution conditions may differ'); } } if (shouldCleanup) { await orchestrator.cleanup(); } return { valid: errors.length === 0, errors, warnings, validationMethod: options?.testRun ? 'test-execution' : 'schema-only', schema: toolInfo.schema, testExecutionResult }; } catch (error) { logger.error(`[ToolValidator] Validation error: ${error instanceof Error ? error.message : String(error)}`); errors.push(`Validation error: ${error instanceof Error ? error.message : String(error)}`); return { valid: false, errors, warnings }; } } /** * Create a temporary orchestrator (fallback when none provided) * WARNING: This is slow! Prefer dependency injection for production use. */ private async createTemporaryOrchestrator(): Promise<any> { logger.warn('[ToolValidator] No orchestrator provided - creating temporary instance (this is slow!)'); logger.warn('[ToolValidator] For better performance, inject an orchestrator via constructor'); const { NCPOrchestrator } = await import('../../orchestrator/ncp-orchestrator.js'); const orchestrator = new NCPOrchestrator('all', false); await orchestrator.initialize(); return orchestrator; } /** * Try MCP-native validation (tools/validate method) * * This is the GOLD STANDARD for validation - allows MCPs to do deep validation * like checking if paths exist, testing database connections, verifying permissions, etc. * * Implementation follows capability-based approach: * 1. Check if MCP announces experimental.toolValidation capability * 2. If yes, call the validate tool * 3. If no, return not supported (skip validation attempt) */ private async tryMCPNativeValidation( orchestrator: any, mcpName: string, toolName: string, parameters: Record<string, any> ): Promise<{ supported: boolean; result?: { valid: boolean; errors: string[]; warnings: string[]; }; }> { try { // Check if the MCP is an internal MCP const internalMCPManager = (orchestrator as any).internalMCPManager; if (internalMCPManager && internalMCPManager.isInternalMCP(mcpName)) { // STEP 1: Check if MCP announces validation capability (MCP protocol pattern) const hasValidationCapability = internalMCPManager.hasCapability( mcpName, 'experimental.toolValidation.supported' ); if (!hasValidationCapability) { // MCP doesn't announce validation support - skip validation attempt logger.debug(`[ToolValidator] MCP ${mcpName} doesn't announce validation capability`); return { supported: false }; } // STEP 2: MCP announces validation - call the validate tool logger.debug(`[ToolValidator] MCP ${mcpName} announces validation capability, using it`); try { const validateResult = await internalMCPManager.executeInternalTool( mcpName, 'validate', { tool: toolName, arguments: parameters } ); if (validateResult.success) { // Parse the validation response const content = validateResult.content; let validationData; if (typeof content === 'string') { try { validationData = JSON.parse(content); } catch { // If it's not JSON, assume it's an error message return { supported: false }; } } else if (Array.isArray(content) && content[0]?.text) { try { validationData = JSON.parse(content[0].text); } catch { return { supported: false }; } } else { validationData = content; } return { supported: true, result: { valid: validationData.valid || false, errors: validationData.errors || [], warnings: validationData.warnings || [] } }; } } catch (error) { // Tool execution failed - unexpected since capability was announced logger.warn(`[ToolValidator] MCP ${mcpName} announced validation but execution failed: ${error instanceof Error ? error.message : String(error)}`); return { supported: false }; } } // For external MCPs, we would call the MCP's tools/validate method // This would require extending the orchestrator to support raw MCP protocol calls // For now, return not supported for external MCPs logger.debug(`[ToolValidator] External MCP validation not yet implemented for ${mcpName}`); return { supported: false }; } catch (error) { logger.debug(`[ToolValidator] Native validation check failed: ${error instanceof Error ? error.message : String(error)}`); return { supported: false }; } } /** * Find tool in orchestrator */ private async findTool( orchestrator: any, tool: string ): Promise<{ schema: any } | null> { try { // Parse tool identifier (format: "mcpName:toolName") const [mcpName, toolName] = tool.includes(':') ? tool.split(':') : [null, tool]; if (!mcpName || !toolName) { logger.error(`[ToolValidator] Invalid tool format: ${tool}`); return null; } // Use getToolSchema for exact lookup (not semantic search) const schema = orchestrator.getToolSchema(mcpName, toolName); if (!schema) { logger.debug(`[ToolValidator] Tool not found: ${tool}`); return null; } return { schema }; } catch (error) { logger.error(`[ToolValidator] Error finding tool: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Validate parameters against JSON schema */ private validateAgainstSchema( parameters: Record<string, any>, schema: any ): { errors: string[]; warnings: string[] } { const errors: string[] = []; const warnings: string[] = []; if (!schema) { warnings.push('No schema available for parameter validation'); return { errors, warnings }; } const schemaProps = schema.properties || {}; const required = schema.required || []; // Check required parameters for (const requiredParam of required) { if (!(requiredParam in parameters)) { errors.push(`Missing required parameter: "${requiredParam}"`); } } // Check parameter types for (const [paramName, paramValue] of Object.entries(parameters)) { const paramSchema = schemaProps[paramName]; if (!paramSchema) { warnings.push(`Parameter "${paramName}" not in schema (may be ignored by tool)`); continue; } // Type validation const typeError = this.validateType(paramName, paramValue, paramSchema); if (typeError) { errors.push(typeError); } } return { errors, warnings }; } /** * Validate parameter type */ private validateType( paramName: string, value: any, schema: any ): string | null { const expectedType = schema.type; const actualType = Array.isArray(value) ? 'array' : typeof value; // Type mapping const typeMap: Record<string, string> = { 'string': 'string', 'number': 'number', 'integer': 'number', 'boolean': 'boolean', 'object': 'object', 'array': 'array' }; const mappedExpected = typeMap[expectedType]; const mappedActual = typeMap[actualType]; if (mappedExpected && mappedActual !== mappedExpected) { return `Parameter "${paramName}" type mismatch: expected ${expectedType}, got ${actualType}`; } // Additional validation for numbers if (expectedType === 'integer' && !Number.isInteger(value)) { return `Parameter "${paramName}" must be an integer, got ${value}`; } // Enum validation if (schema.enum && !schema.enum.includes(value)) { return `Parameter "${paramName}" must be one of: ${schema.enum.join(', ')}`; } return null; } /** * Run test execution */ private async runTestExecution( orchestrator: any, tool: string, parameters: Record<string, any>, timeout: number ): Promise<{ success: boolean; output?: any; error?: string; duration: number; }> { const startTime = Date.now(); try { const result = await Promise.race([ orchestrator.run(tool, parameters), new Promise((_, reject) => setTimeout(() => reject(new Error('Test execution timeout')), timeout) ) ]); const duration = Date.now() - startTime; if (result.success) { return { success: true, output: result.content, duration }; } else { return { success: false, error: result.error || 'Unknown error', duration }; } } catch (error) { const duration = Date.now() - startTime; return { success: false, error: error instanceof Error ? error.message : String(error), duration }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server