Skip to main content
Glama

Nexus MCP Server

json-rpc-validator.ts14.7 kB
import { logger } from './logger.js'; /** * JSON-RPC 2.0 specification types */ export interface JsonRpcRequest { jsonrpc: '2.0'; method: string; params?: unknown[] | Record<string, unknown>; id?: string | number | null; } export interface JsonRpcResponse { jsonrpc: '2.0'; result?: unknown; error?: JsonRpcError; id: string | number | null; } export interface JsonRpcError { code: number; message: string; data?: unknown; } export interface JsonRpcNotification { jsonrpc: '2.0'; method: string; params?: unknown[] | Record<string, unknown>; } export type JsonRpcMessage = | JsonRpcRequest | JsonRpcResponse | JsonRpcNotification; /** * Validation result for JSON-RPC messages */ export interface JsonRpcValidationResult { valid: boolean; errors: string[]; warnings: string[]; messageType?: 'request' | 'response' | 'notification'; method?: string; id?: string | number | null; } /** * Standard JSON-RPC error codes */ export const JSON_RPC_ERROR_CODES = { PARSE_ERROR: -32700, INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, INVALID_PARAMS: -32602, INTERNAL_ERROR: -32603, // Reserved for implementation-defined server-errors SERVER_ERROR_MIN: -32099, SERVER_ERROR_MAX: -32000, } as const; /** * JSON-RPC 2.0 compliance validator */ export class JsonRpcValidator { /** * Validate a JSON-RPC message according to the 2.0 specification */ static validateMessage(message: unknown): JsonRpcValidationResult { const result: JsonRpcValidationResult = { valid: true, errors: [], warnings: [], }; if (!message || typeof message !== 'object') { result.valid = false; result.errors.push('Message must be an object'); return result; } const msg = message as Record<string, unknown>; // Check jsonrpc version if (msg.jsonrpc !== '2.0') { result.valid = false; result.errors.push('Missing or invalid jsonrpc version (must be "2.0")'); } // Determine message type and validate accordingly if ('method' in msg) { if ('id' in msg) { // Request result.messageType = 'request'; result.method = msg.method as string; result.id = msg.id as string | number | null; this.validateRequest(msg, result); } else { // Notification result.messageType = 'notification'; result.method = msg.method as string; this.validateNotification(msg, result); } } else if ('result' in msg || 'error' in msg) { // Response result.messageType = 'response'; result.id = msg.id as string | number | null; this.validateResponse(msg, result); } else { result.valid = false; result.errors.push( 'Message must contain either "method" (request/notification) or "result"/"error" (response)' ); } // Log validation result logger.responseValidation( 'schema_validation', result.valid ? 'passed' : 'failed', { method: result.method, requestId: result.id?.toString(), validationErrors: result.errors, } ); return result; } /** * Validate JSON-RPC request */ private static validateRequest( msg: Record<string, unknown>, result: JsonRpcValidationResult ): void { // Method validation if (typeof msg.method !== 'string') { result.valid = false; result.errors.push('Method must be a string'); } else if (msg.method.startsWith('rpc.') && msg.method !== 'rpc.discover') { result.warnings.push('Method names beginning with "rpc." are reserved'); } // ID validation if ('id' in msg) { const id = msg.id; if (id !== null && typeof id !== 'string' && typeof id !== 'number') { result.valid = false; result.errors.push('ID must be a string, number, or null'); } } // Params validation if ('params' in msg) { const params = msg.params; if ( params !== undefined && !Array.isArray(params) && (typeof params !== 'object' || params === null) ) { result.valid = false; result.errors.push('Params must be an array, object, or undefined'); } } } /** * Validate JSON-RPC response */ private static validateResponse( msg: Record<string, unknown>, result: JsonRpcValidationResult ): void { // ID validation (required for responses) if (!('id' in msg)) { result.valid = false; result.errors.push('Response must contain an ID'); } else { const id = msg.id; if (id !== null && typeof id !== 'string' && typeof id !== 'number') { result.valid = false; result.errors.push('ID must be a string, number, or null'); } } // Result/Error validation const hasResult = 'result' in msg; const hasError = 'error' in msg; if (hasResult && hasError) { result.valid = false; result.errors.push('Response cannot contain both "result" and "error"'); } else if (!hasResult && !hasError) { result.valid = false; result.errors.push('Response must contain either "result" or "error"'); } // Error object validation if (hasError) { this.validateError(msg.error, result); } } /** * Validate JSON-RPC notification */ private static validateNotification( msg: Record<string, unknown>, result: JsonRpcValidationResult ): void { // Method validation if (typeof msg.method !== 'string') { result.valid = false; result.errors.push('Method must be a string'); } else if (msg.method.startsWith('rpc.')) { result.warnings.push('Method names beginning with "rpc." are reserved'); } // ID validation (notifications must not have ID) if ('id' in msg) { result.valid = false; result.errors.push('Notifications must not contain an ID'); } // Params validation if ('params' in msg) { const params = msg.params; if ( params !== undefined && !Array.isArray(params) && (typeof params !== 'object' || params === null) ) { result.valid = false; result.errors.push('Params must be an array, object, or undefined'); } } } /** * Validate JSON-RPC error object */ private static validateError( error: unknown, result: JsonRpcValidationResult ): void { if (!error || typeof error !== 'object') { result.valid = false; result.errors.push('Error must be an object'); return; } const err = error as Record<string, unknown>; // Code validation if (typeof err.code !== 'number') { result.valid = false; result.errors.push('Error code must be a number'); } else if (!Number.isInteger(err.code)) { result.valid = false; result.errors.push('Error code must be an integer'); } // Message validation if (typeof err.message !== 'string') { result.valid = false; result.errors.push('Error message must be a string'); } // Data validation (optional) // Data can be any type, so no validation needed } /** * Create a compliant JSON-RPC error response */ static createErrorResponse( id: string | number | null, code: number, message: string, data?: unknown ): JsonRpcResponse { const response: JsonRpcResponse = { jsonrpc: '2.0', error: { code, message, ...(data !== undefined && { data }), }, id, }; logger.jsonRpc('error', undefined, { id: id ?? undefined, error: response.error, }); return response; } /** * Create a compliant JSON-RPC success response */ static createSuccessResponse( id: string | number | null, result: unknown ): JsonRpcResponse { const response: JsonRpcResponse = { jsonrpc: '2.0', result, id, }; logger.jsonRpc('response', undefined, { id: id ?? undefined, responseSize: JSON.stringify(result).length, }); return response; } /** * Validate and sanitize a JSON-RPC response before transmission */ static validateAndSanitizeResponse(response: unknown): JsonRpcResponse { const startTime = Date.now(); try { // First validate the response structure const validation = this.validateMessage(response); if (!validation.valid) { logger.responseValidation('compliance_check', 'failed', { validationErrors: validation.errors, duration: Date.now() - startTime, }); // Return a compliant error response return this.createErrorResponse( validation.id || null, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, 'Invalid response format', { validationErrors: validation.errors } ); } if (validation.warnings.length > 0) { logger.responseValidation('compliance_check', 'warning', { validationErrors: validation.warnings, duration: Date.now() - startTime, }); } logger.responseValidation('compliance_check', 'passed', { method: validation.method, requestId: validation.id?.toString(), duration: Date.now() - startTime, }); return response as JsonRpcResponse; } catch (error) { logger.responseValidation('compliance_check', 'failed', { validationErrors: [ error instanceof Error ? error.message : String(error), ], duration: Date.now() - startTime, }); // Return a fallback error response return this.createErrorResponse( null, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, 'Response validation failed' ); } } /** * Validate MCP-specific JSON-RPC extensions */ static validateMcpExtensions( message: JsonRpcMessage ): JsonRpcValidationResult { const result: JsonRpcValidationResult = { valid: true, errors: [], warnings: [], }; // MCP uses specific method patterns if ('method' in message) { const method = message.method; // Check for MCP method patterns const mcpMethods = [ 'initialize', 'initialized', 'ping', 'shutdown', 'tools/list', 'tools/call', 'resources/list', 'resources/read', 'resources/subscribe', 'resources/unsubscribe', 'prompts/list', 'prompts/get', 'completion/complete', 'roots/list', 'sampling/createMessage', ]; const isKnownMcpMethod = mcpMethods.some( mcpMethod => method === mcpMethod || method.startsWith(mcpMethod + '/') ); if (!isKnownMcpMethod && !method.startsWith('notifications/')) { result.warnings.push(`Unknown MCP method: ${method}`); } } return result; } } /** * Pre-transmission validation with round-trip testing */ export function validatePreTransmission(response: unknown): { valid: boolean; sanitizedResponse?: JsonRpcResponse; errors: string[]; } { const startTime = Date.now(); const errors: string[] = []; try { // Step 1: Basic structure validation const structureValidation = JsonRpcValidator.validateMessage(response); if (!structureValidation.valid) { errors.push(...structureValidation.errors); } // Step 2: Sanitize and validate the response const sanitizedResponse = JsonRpcValidator.validateAndSanitizeResponse(response); // Step 3: Round-trip testing - serialize and deserialize const serialized = JSON.stringify(sanitizedResponse); let deserialized: unknown; try { deserialized = JSON.parse(serialized); } catch (parseError) { errors.push( `Round-trip test failed: ${parseError instanceof Error ? parseError.message : String(parseError)}` ); logger.responseValidation('pre_serialization', 'failed', { validationErrors: errors, duration: Date.now() - startTime, }); return { valid: false, errors }; } // Step 4: Validate the deserialized response matches structure const roundTripValidation = JsonRpcValidator.validateMessage(deserialized); if (!roundTripValidation.valid) { errors.push( `Round-trip validation failed: ${roundTripValidation.errors.join(', ')}` ); } // Step 5: Deep equality check (basic) try { const reserializedAfterRoundTrip = JSON.stringify(deserialized); if (serialized !== reserializedAfterRoundTrip) { errors.push( 'Round-trip test failed: serialized data does not match after deserialization' ); } } catch (error) { errors.push( `Round-trip comparison failed: ${error instanceof Error ? error.message : String(error)}` ); } const valid = errors.length === 0; logger.responseValidation( 'pre_serialization', valid ? 'passed' : 'failed', { validationErrors: valid ? undefined : errors, duration: Date.now() - startTime, responseSize: serialized.length, } ); return { valid, sanitizedResponse: valid ? sanitizedResponse : undefined, errors, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); errors.push(`Pre-transmission validation error: ${errorMessage}`); logger.responseValidation('pre_serialization', 'failed', { validationErrors: errors, duration: Date.now() - startTime, }); return { valid: false, errors }; } } /** * Response validation middleware for MCP servers */ export function createResponseValidationMiddleware() { return (response: unknown): JsonRpcResponse => { const startTime = Date.now(); // Perform pre-transmission validation with round-trip testing const validation = validatePreTransmission(response); if (!validation.valid) { logger.responseValidation('pre_serialization', 'failed', { validationErrors: validation.errors, duration: Date.now() - startTime, }); // Return a compliant error response return JsonRpcValidator.createErrorResponse( null, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, 'Pre-transmission validation failed', { validationErrors: validation.errors } ); } logger.responseValidation('pre_serialization', 'passed', { duration: Date.now() - startTime, responseSize: JSON.stringify(validation.sanitizedResponse).length, }); return validation.sanitizedResponse!; }; }

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/adawalli/nexus'

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