Skip to main content
Glama
mcp-tool-validator.ts8.27 kB
/** * Main MCP Tool Validator * * Orchestrates the validation of MCP tool schemas against the specification */ import { MCPTool, ValidationResult, ValidationError, ValidationSeverity, ValidationReport, ToolValidationContext, SchemaValidationOptions, ValidationRule } from './types'; import { VALIDATION_RULES, MCP_SPECIFICATION_VERSION } from './constants'; import { SchemaValidator } from './schema-validator'; /** * Configuration for the MCP Tool Validator */ export interface MCPToolValidatorConfig extends SchemaValidationOptions { /** Whether to include performance metrics in reports */ includeMetrics?: boolean; /** Whether to validate tool names for uniqueness */ checkUniqueNames?: boolean; /** Whether to perform deep validation of nested schemas */ deepValidation?: boolean; } /** * Main validator class for MCP tool schemas */ export class MCPToolValidator { private config: MCPToolValidatorConfig; private schemaValidator: SchemaValidator; private validationRules: ValidationRule[]; constructor(config: MCPToolValidatorConfig = {}) { this.config = { strict: true, mcpVersion: MCP_SPECIFICATION_VERSION, checkDeprecated: true, validateJsonSchemaDraft: true, maxSchemaDepth: 10, includeMetrics: true, checkUniqueNames: true, deepValidation: true, ...config }; this.schemaValidator = new SchemaValidator(this.config); this.validationRules = [ ...VALIDATION_RULES, ...(this.config.customRules || []) ]; } /** * Validate a single MCP tool */ public validateTool(tool: MCPTool, toolIndex?: number): ValidationResult { const context: ToolValidationContext = { tool, toolIndex, options: this.config }; const allErrors: ValidationError[] = []; const allWarnings: ValidationError[] = []; const allInfo: ValidationError[] = []; // Run all validation rules for (const rule of this.validationRules) { try { const ruleErrors = rule.validate(tool, context); for (const error of ruleErrors) { switch (error.severity) { case ValidationSeverity.ERROR: allErrors.push(error); break; case ValidationSeverity.WARNING: allWarnings.push(error); break; case ValidationSeverity.INFO: allInfo.push(error); break; } } } catch (validationError) { // Handle errors in validation rules themselves allErrors.push({ code: 'VALIDATION_RULE_ERROR', message: `Error executing validation rule '${rule.id}': ${validationError instanceof Error ? validationError.message : 'Unknown error'}`, severity: ValidationSeverity.ERROR, context: { ruleId: rule.id, error: validationError instanceof Error ? validationError.message : validationError } }); } } // Additional schema validation if (this.config.deepValidation && tool.inputSchema) { const schemaResult = this.schemaValidator.validateSchema(tool.inputSchema); allErrors.push(...schemaResult.errors); allWarnings.push(...schemaResult.warnings); allInfo.push(...schemaResult.info); } const result: ValidationResult = { valid: allErrors.length === 0, errors: allErrors, warnings: allWarnings, info: allInfo, summary: { errorCount: allErrors.length, warningCount: allWarnings.length, infoCount: allInfo.length } }; return result; } /** * Validate multiple MCP tools */ public validateTools(tools: MCPTool[]): ValidationResult { const allErrors: ValidationError[] = []; const allWarnings: ValidationError[] = []; const allInfo: ValidationError[] = []; // Check for unique tool names if (this.config.checkUniqueNames) { const nameErrors = this.validateUniqueNames(tools); allErrors.push(...nameErrors); } // Validate each tool individually for (let i = 0; i < tools.length; i++) { const tool = tools[i]; if (!tool) continue; const result = this.validateTool(tool, i); // Add tool index to error paths for context const contextualizeError = (error: ValidationError): ValidationError => ({ ...error, path: error.path ? `tools[${i}].${error.path}` : `tools[${i}]`, context: { ...error.context, toolIndex: i, toolName: tool.name } }); allErrors.push(...result.errors.map(contextualizeError)); allWarnings.push(...result.warnings.map(contextualizeError)); allInfo.push(...result.info.map(contextualizeError)); } return { valid: allErrors.length === 0, errors: allErrors, warnings: allWarnings, info: allInfo, summary: { errorCount: allErrors.length, warningCount: allWarnings.length, infoCount: allInfo.length } }; } /** * Generate a comprehensive validation report */ public generateReport(tools: MCPTool[]): ValidationReport { const startTime = Date.now(); // Overall validation const overallResult = this.validateTools(tools); // Individual tool results const toolResults = tools.map((tool, index) => ({ toolName: tool.name || `Tool ${index}`, toolIndex: index, result: this.validateTool(tool, index) })); const endTime = Date.now(); const validationDurationMs = endTime - startTime; const report: ValidationReport = { timestamp: new Date().toISOString(), validatorVersion: '1.0.0', mcpVersion: this.config.mcpVersion || MCP_SPECIFICATION_VERSION, toolCount: tools.length, overall: overallResult, toolResults, metrics: { validationDurationMs, rulesExecuted: this.validationRules.length, averageTimePerTool: tools.length > 0 ? validationDurationMs / tools.length : 0 }, config: this.config }; return report; } /** * Validate that tool names are unique */ private validateUniqueNames(tools: MCPTool[]): ValidationError[] { const errors: ValidationError[] = []; const nameMap = new Map<string, number[]>(); // Build map of names to indices tools.forEach((tool, index) => { if (tool.name) { if (!nameMap.has(tool.name)) { nameMap.set(tool.name, []); } nameMap.get(tool.name)!.push(index); } }); // Find duplicates for (const [name, indices] of nameMap.entries()) { if (indices.length > 1) { indices.forEach(index => { errors.push({ code: 'TOOL_NAME_DUPLICATE', message: `Tool name '${name}' is used by multiple tools`, severity: ValidationSeverity.ERROR, path: `tools[${index}].name`, expected: 'unique tool name', actual: name, context: { duplicateIndices: indices, toolName: name }, specReference: 'MCP Specification - Tool Uniqueness' }); }); } } return errors; } /** * Add a custom validation rule */ public addValidationRule(rule: ValidationRule): void { this.validationRules.push(rule); } /** * Remove a validation rule by ID */ public removeValidationRule(ruleId: string): boolean { const initialLength = this.validationRules.length; this.validationRules = this.validationRules.filter(rule => rule.id !== ruleId); return this.validationRules.length < initialLength; } /** * Get all active validation rules */ public getValidationRules(): ValidationRule[] { return [...this.validationRules]; } /** * Update validator configuration */ public updateConfig(newConfig: Partial<MCPToolValidatorConfig>): void { this.config = { ...this.config, ...newConfig }; this.schemaValidator = new SchemaValidator(this.config); } /** * Get current validator configuration */ public getConfig(): MCPToolValidatorConfig { return { ...this.config }; } }

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/learnwithcc/tally-mcp'

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