Skip to main content
Glama
2389-research

MCP Agent Social Media Server

validation.ts9.04 kB
// ABOUTME: JSON Schema validation utilities for MCP tools // ABOUTME: Provides runtime validation for tool inputs export interface ValidationError { field: string; message: string; } export class ValidationResult<T = unknown> { constructor( public isValid: boolean, public errors: ValidationError[] = [], public data?: T, ) {} static success<T>(data: T): ValidationResult<T> { return new ValidationResult<T>(true, [], data); } static failure<T = never>(errors: ValidationError[]): ValidationResult<T> { return new ValidationResult<T>(false, errors); } } export function validateString( value: unknown, field: string, options: { minLength?: number; maxLength?: number; required?: boolean } = {}, ): ValidationError[] { const errors: ValidationError[] = []; if (options.required && (value === undefined || value === null)) { if (field === 'content') { errors.push({ field, message: 'Content must not be empty' }); } else { errors.push({ field, message: `${field} is required` }); } return errors; } if (value !== undefined && value !== null) { if (typeof value !== 'string') { errors.push({ field, message: `${field} must be a string` }); return errors; } // For content validation, check if trimmed string is empty if (field === 'content' && options.required && value.trim().length === 0) { errors.push({ field, message: 'Content must not be empty' }); return errors; } if (options.minLength && value.length < options.minLength) { if (field === 'content') { errors.push({ field, message: 'Content must not be empty' }); } else { errors.push({ field, message: `${field} must be at least ${options.minLength} characters`, }); } } if (options.maxLength && value.length > options.maxLength) { errors.push({ field, message: `${field} must be at most ${options.maxLength} characters` }); } } return errors; } export function validateNumber( value: unknown, field: string, options: { min?: number; max?: number; required?: boolean } = {}, ): ValidationError[] { const errors: ValidationError[] = []; if (options.required && (value === undefined || value === null)) { errors.push({ field, message: `${field} is required` }); return errors; } if (value !== undefined && value !== null) { if (typeof value !== 'number' || Number.isNaN(value)) { errors.push({ field, message: `${field} must be a number` }); return errors; } if (options.min !== undefined && value < options.min) { errors.push({ field, message: `${field} must be at least ${options.min}` }); } if (options.max !== undefined && value > options.max) { errors.push({ field, message: `${field} must be at most ${options.max}` }); } } return errors; } export function validateArray<T = unknown>( value: unknown, field: string, options: { required?: boolean; itemValidator?: (item: T, index: number) => ValidationError[]; } = {}, ): ValidationError[] { const errors: ValidationError[] = []; if (options.required && (value === undefined || value === null)) { errors.push({ field, message: `${field} is required` }); return errors; } if (value !== undefined && value !== null) { if (!Array.isArray(value)) { errors.push({ field, message: `${field} must be an array` }); return errors; } if (options.itemValidator) { (value as T[]).forEach((item, index) => { const itemErrors = options.itemValidator?.(item, index); if (itemErrors) { errors.push( ...itemErrors.map((err) => ({ field: `${field}[${index}].${err.field}`, message: err.message, })), ); } }); } } return errors; } // Helper to trim string values consistently function trimStringValue(value: unknown): string | undefined { if (value === undefined || value === null) { return undefined; } if (typeof value === 'string') { const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } // Only accept primitive types that can be meaningfully converted to strings if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } // Reject objects, arrays, functions, etc. return undefined; } // Input types for validation interface LoginInput { agent_name?: unknown; } interface ReadPostsInput { limit?: unknown; offset?: unknown; agent_filter?: unknown; tag_filter?: unknown; thread_id?: unknown; } interface CreatePostInput { content?: unknown; tags?: unknown; parent_post_id?: unknown; } // Login tool validation export function validateLoginInput(input: LoginInput): ValidationResult<{ agent_name: string }> { const errors: ValidationError[] = []; // Special handling for agent_name if (input.agent_name === undefined || input.agent_name === null) { errors.push({ field: 'agent_name', message: 'Agent name must not be empty' }); } else if (typeof input.agent_name !== 'string') { errors.push({ field: 'agent_name', message: 'Agent name must be a string' }); } else if (input.agent_name.trim().length === 0) { errors.push({ field: 'agent_name', message: 'Agent name must not be empty' }); } if (errors.length > 0) { return ValidationResult.failure(errors); } const agentName = trimStringValue(input.agent_name); if (!agentName) { return ValidationResult.failure([ { field: 'agent_name', message: 'Agent name cannot be empty' }, ]); } return ValidationResult.success({ agent_name: agentName, }); } // Read posts tool validation export function validateReadPostsInput(input: ReadPostsInput): ValidationResult<{ limit: number; offset: number; agent_filter?: string; tag_filter?: string; thread_id?: string; }> { const errors: ValidationError[] = []; // Parse and validate numeric values const limit = typeof input.limit === 'number' ? input.limit : typeof input.limit === 'string' ? Number.parseInt(input.limit, 10) : 10; const offset = typeof input.offset === 'number' ? input.offset : typeof input.offset === 'string' ? Number.parseInt(input.offset, 10) : 0; // Apply defaults and trim string values const data = { limit: Number.isNaN(limit) ? 10 : limit, offset: Number.isNaN(offset) ? 0 : offset, agent_filter: trimStringValue(input.agent_filter), tag_filter: trimStringValue(input.tag_filter), thread_id: trimStringValue(input.thread_id), }; errors.push(...validateNumber(data.limit, 'limit', { min: 1, max: 100 })); errors.push(...validateNumber(data.offset, 'offset', { min: 0 })); // Check for empty string filters (before trimming converted them to undefined) if ( input.agent_filter !== undefined && input.agent_filter !== null && typeof input.agent_filter === 'string' && input.agent_filter.trim() === '' ) { errors.push({ field: 'agent_filter', message: 'agent_filter cannot be empty' }); } if ( input.tag_filter !== undefined && input.tag_filter !== null && typeof input.tag_filter === 'string' && input.tag_filter.trim() === '' ) { errors.push({ field: 'tag_filter', message: 'tag_filter cannot be empty' }); } if ( input.thread_id !== undefined && input.thread_id !== null && typeof input.thread_id === 'string' && input.thread_id.trim() === '' ) { errors.push({ field: 'thread_id', message: 'thread_id cannot be empty' }); } if (errors.length > 0) { return ValidationResult.failure(errors); } return ValidationResult.success(data); } // Create post tool validation export function validateCreatePostInput(input: CreatePostInput): ValidationResult<{ content: string; tags: string[]; parent_post_id?: string; }> { const errors: ValidationError[] = []; errors.push( ...validateString(input.content, 'content', { required: true, minLength: 1, }), ); errors.push(...validateString(input.parent_post_id, 'parent_post_id')); if (input.tags !== undefined) { errors.push( ...validateArray(input.tags, 'tags', { itemValidator: (item, _index) => validateString(item, 'item', {}), }), ); } if (errors.length > 0) { return ValidationResult.failure(errors); } // Filter and trim tags consistently const rawTags = Array.isArray(input.tags) ? input.tags : []; const filteredTags = rawTags .map((tag) => trimStringValue(tag)) .filter((tag): tag is string => tag !== undefined); const content = trimStringValue(input.content); if (!content) { return ValidationResult.failure([{ field: 'content', message: 'Content cannot be empty' }]); } return ValidationResult.success({ content, tags: filteredTags, parent_post_id: trimStringValue(input.parent_post_id), }); }

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/2389-research/mcp-socialmedia'

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