Skip to main content
Glama

MCP Ethers Wallet

ResponseValidator.ts15.4 kB
/** * @file ResponseValidator.ts * @description MCP response validation utilities */ import { CallToolResult, ListToolsResult, ListResourcesResult, ListPromptsResult } from '@modelcontextprotocol/sdk/types.js'; /** * Validation result for a single check */ export interface ValidationResult { valid: boolean; field?: string; expected?: any; actual?: any; message?: string; errors?: ValidationResult[]; } /** * Validation options */ export interface ValidationOptions { /** Allow additional properties not in schema */ allowAdditionalProperties?: boolean; /** Strict type checking */ strictTypes?: boolean; /** Check for required fields */ checkRequired?: boolean; /** Validate nested objects */ validateNested?: boolean; /** Custom validators */ customValidators?: Record<string, (value: any) => ValidationResult>; } /** * Response validator for MCP responses */ export class ResponseValidator { private options: ValidationOptions; constructor(options: ValidationOptions = {}) { this.options = { allowAdditionalProperties: true, strictTypes: true, checkRequired: true, validateNested: true, ...options }; } /** * Validates a tool call response */ validateToolResponse(response: any): ValidationResult { if (!response) { return { valid: false, message: 'Response is null or undefined' }; } const errors: ValidationResult[] = []; // Check for required fields if (!('content' in response) && !('isError' in response)) { errors.push({ valid: false, field: 'content/isError', message: 'Response must have either content or isError field' }); } // Validate content array if present if ('content' in response) { if (!Array.isArray(response.content)) { errors.push({ valid: false, field: 'content', expected: 'array', actual: typeof response.content, message: 'Content must be an array' }); } else { // Validate each content item response.content.forEach((item: any, index: number) => { const itemValidation = this.validateContentItem(item); if (!itemValidation.valid) { errors.push({ valid: false, field: `content[${index}]`, message: itemValidation.message, errors: itemValidation.errors }); } }); } } // Validate isError field if present if ('isError' in response && typeof response.isError !== 'boolean') { errors.push({ valid: false, field: 'isError', expected: 'boolean', actual: typeof response.isError, message: 'isError must be a boolean' }); } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates a content item */ private validateContentItem(item: any): ValidationResult { if (!item || typeof item !== 'object') { return { valid: false, message: 'Content item must be an object' }; } // Check for type field if (!('type' in item)) { return { valid: false, field: 'type', message: 'Content item must have a type field' }; } // Validate based on type switch (item.type) { case 'text': return this.validateTextContent(item); case 'image': return this.validateImageContent(item); case 'resource': return this.validateResourceContent(item); case 'resource_link': return this.validateResourceLinkContent(item); default: if (!this.options.allowAdditionalProperties) { return { valid: false, field: 'type', actual: item.type, message: `Unknown content type: ${item.type}` }; } return { valid: true }; } } /** * Validates text content */ private validateTextContent(item: any): ValidationResult { if (!('text' in item)) { return { valid: false, field: 'text', message: 'Text content must have a text field' }; } if (typeof item.text !== 'string') { return { valid: false, field: 'text', expected: 'string', actual: typeof item.text, message: 'Text field must be a string' }; } return { valid: true }; } /** * Validates image content */ private validateImageContent(item: any): ValidationResult { if (!('data' in item) && !('url' in item)) { return { valid: false, message: 'Image content must have either data or url field' }; } if ('mimeType' in item && typeof item.mimeType !== 'string') { return { valid: false, field: 'mimeType', expected: 'string', actual: typeof item.mimeType, message: 'mimeType must be a string' }; } return { valid: true }; } /** * Validates resource content */ private validateResourceContent(item: any): ValidationResult { if (!('resource' in item)) { return { valid: false, field: 'resource', message: 'Resource content must have a resource field' }; } const resource = item.resource; if (!resource || typeof resource !== 'object') { return { valid: false, field: 'resource', message: 'Resource must be an object' }; } // Validate resource properties if (!('uri' in resource)) { return { valid: false, field: 'resource.uri', message: 'Resource must have a uri field' }; } return { valid: true }; } /** * Validates resource link content */ private validateResourceLinkContent(item: any): ValidationResult { if (!('uri' in item)) { return { valid: false, field: 'uri', message: 'Resource link must have a uri field' }; } if (typeof item.uri !== 'string') { return { valid: false, field: 'uri', expected: 'string', actual: typeof item.uri, message: 'URI must be a string' }; } return { valid: true }; } /** * Validates a list tools response */ validateListToolsResponse(response: any): ValidationResult { if (!response) { return { valid: false, message: 'Response is null or undefined' }; } const errors: ValidationResult[] = []; // Check for tools array if (!('tools' in response)) { errors.push({ valid: false, field: 'tools', message: 'Response must have a tools field' }); } else if (!Array.isArray(response.tools)) { errors.push({ valid: false, field: 'tools', expected: 'array', actual: typeof response.tools, message: 'Tools must be an array' }); } else { // Validate each tool response.tools.forEach((tool: any, index: number) => { const toolValidation = this.validateTool(tool); if (!toolValidation.valid) { errors.push({ valid: false, field: `tools[${index}]`, message: toolValidation.message, errors: toolValidation.errors }); } }); } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates a tool definition */ private validateTool(tool: any): ValidationResult { if (!tool || typeof tool !== 'object') { return { valid: false, message: 'Tool must be an object' }; } const errors: ValidationResult[] = []; // Check required fields if (!('name' in tool)) { errors.push({ valid: false, field: 'name', message: 'Tool must have a name field' }); } else if (typeof tool.name !== 'string') { errors.push({ valid: false, field: 'name', expected: 'string', actual: typeof tool.name, message: 'Tool name must be a string' }); } if (!('description' in tool)) { errors.push({ valid: false, field: 'description', message: 'Tool must have a description field' }); } // Validate input schema if present if ('inputSchema' in tool && tool.inputSchema !== null) { if (typeof tool.inputSchema !== 'object') { errors.push({ valid: false, field: 'inputSchema', expected: 'object', actual: typeof tool.inputSchema, message: 'Input schema must be an object' }); } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates a list resources response */ validateListResourcesResponse(response: any): ValidationResult { if (!response) { return { valid: false, message: 'Response is null or undefined' }; } const errors: ValidationResult[] = []; // Check for resources array if (!('resources' in response)) { errors.push({ valid: false, field: 'resources', message: 'Response must have a resources field' }); } else if (!Array.isArray(response.resources)) { errors.push({ valid: false, field: 'resources', expected: 'array', actual: typeof response.resources, message: 'Resources must be an array' }); } else { // Validate each resource response.resources.forEach((resource: any, index: number) => { const resourceValidation = this.validateResource(resource); if (!resourceValidation.valid) { errors.push({ valid: false, field: `resources[${index}]`, message: resourceValidation.message, errors: resourceValidation.errors }); } }); } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates a resource definition */ private validateResource(resource: any): ValidationResult { if (!resource || typeof resource !== 'object') { return { valid: false, message: 'Resource must be an object' }; } const errors: ValidationResult[] = []; // Check required fields if (!('uri' in resource)) { errors.push({ valid: false, field: 'uri', message: 'Resource must have a uri field' }); } else if (typeof resource.uri !== 'string') { errors.push({ valid: false, field: 'uri', expected: 'string', actual: typeof resource.uri, message: 'Resource uri must be a string' }); } if (!('name' in resource)) { errors.push({ valid: false, field: 'name', message: 'Resource must have a name field' }); } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates a list prompts response */ validateListPromptsResponse(response: any): ValidationResult { if (!response) { return { valid: false, message: 'Response is null or undefined' }; } const errors: ValidationResult[] = []; // Check for prompts array if (!('prompts' in response)) { errors.push({ valid: false, field: 'prompts', message: 'Response must have a prompts field' }); } else if (!Array.isArray(response.prompts)) { errors.push({ valid: false, field: 'prompts', expected: 'array', actual: typeof response.prompts, message: 'Prompts must be an array' }); } else { // Validate each prompt response.prompts.forEach((prompt: any, index: number) => { const promptValidation = this.validatePrompt(prompt); if (!promptValidation.valid) { errors.push({ valid: false, field: `prompts[${index}]`, message: promptValidation.message, errors: promptValidation.errors }); } }); } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates a prompt definition */ private validatePrompt(prompt: any): ValidationResult { if (!prompt || typeof prompt !== 'object') { return { valid: false, message: 'Prompt must be an object' }; } const errors: ValidationResult[] = []; // Check required fields if (!('name' in prompt)) { errors.push({ valid: false, field: 'name', message: 'Prompt must have a name field' }); } else if (typeof prompt.name !== 'string') { errors.push({ valid: false, field: 'name', expected: 'string', actual: typeof prompt.name, message: 'Prompt name must be a string' }); } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Validates MCP protocol compliance */ validateProtocolCompliance(response: any): ValidationResult { const errors: ValidationResult[] = []; // Check JSON-RPC structure if applicable if ('jsonrpc' in response) { if (response.jsonrpc !== '2.0') { errors.push({ valid: false, field: 'jsonrpc', expected: '2.0', actual: response.jsonrpc, message: 'Invalid JSON-RPC version' }); } // Check for id in responses (not notifications) if ('result' in response || 'error' in response) { if (!('id' in response)) { errors.push({ valid: false, field: 'id', message: 'Response must have an id field' }); } } // Check for mutual exclusivity of result and error if ('result' in response && 'error' in response) { errors.push({ valid: false, message: 'Response cannot have both result and error fields' }); } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Creates a custom validator function */ static createCustomValidator( fn: (value: any) => boolean, message: string = 'Custom validation failed' ): (value: any) => ValidationResult { return (value: any) => ({ valid: fn(value), message: fn(value) ? undefined : message }); } /** * Combines multiple validators */ static combineValidators( ...validators: Array<(value: any) => ValidationResult> ): (value: any) => ValidationResult { return (value: any) => { const errors: ValidationResult[] = []; for (const validator of validators) { const result = validator(value); if (!result.valid) { errors.push(result); } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; }; } }

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/crazyrabbitLTC/mcp-ethers-server'

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