Skip to main content
Glama
schema-validator.tsβ€’15.3 kB
import { ErrorType, HttpStatusCode, UniversalValidationError, } from '../errors/validation-errors.js'; import { UniversalResourceType } from '../types.js'; import { SanitizedObject, SanitizedValue } from '../schemas/common/types.js'; import { suggestResourceType, validateIdFields, validatePaginationParams, } from './field-validator.js'; export class InputSanitizer { static sanitizeString(input: unknown): string { if (typeof input !== 'string') { return String(input); } let s = input; s = s.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, '$1'); s = s.replace(/on\w+\s*=\s*([^>\s]*)/gi, '$1'); s = s.replace(/<\/?[^>]+>/g, ''); return s.replace(/\s+/g, ' ').trim(); } static normalizeEmail(email: unknown): string { if (typeof email !== 'string') { return String(email).trim().toLowerCase(); } return email.trim().toLowerCase(); } static sanitizeObject(obj: unknown): SanitizedValue { if (obj === null) return null; if (obj === undefined) return null; if (typeof obj === 'string') { return this.sanitizeString(obj); } if (Array.isArray(obj)) { return obj.map((item) => this.sanitizeObject(item)); } if (typeof obj === 'number' || typeof obj === 'boolean') { return obj; } if (typeof obj === 'object') { const result: Record<string, SanitizedValue> = {}; for (const [key, value] of Object.entries( obj as Record<string, unknown> )) { const lowerKey = key.toLowerCase(); if (lowerKey === 'email' && typeof value === 'string') { result[key] = this.normalizeEmail(value); continue; } if (lowerKey === 'email_address' && typeof value === 'string') { result[key] = this.normalizeEmail(value); continue; } if (lowerKey === 'email_addresses' && Array.isArray(value)) { result[key] = (value as unknown[]).map((v) => typeof v === 'string' ? this.normalizeEmail(v) : (this.sanitizeObject(v) as SanitizedValue) ); continue; } result[key] = this.sanitizeObject(value); } return result as SanitizedObject; } return String(obj); } } type ToolValidator = (params: SanitizedObject) => SanitizedObject; const toolValidators: Record<string, ToolValidator> = { // Universal tools with underscore names (Issue #776 Phase 0) records_search: (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', suggestion: 'Specify which type of records to search', example: `resource_type: 'companies' | 'people' | 'records' | 'tasks'`, } ); } return p; }, records_get_details: (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } if (!p.record_id) { throw new UniversalValidationError( 'Missing required parameter: record_id', ErrorType.USER_ERROR, { field: 'record_id', suggestion: 'Provide the unique identifier of the record to retrieve', example: `record_id: 'comp_abc123'`, } ); } return p; }, records_get_attributes: (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', suggestion: 'Specify which resource type to get attributes for', example: `resource_type: 'companies' | 'people' | 'records' | 'tasks'`, } ); } return p; }, records_discover_attributes: (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', suggestion: 'Specify which resource type to discover attributes for', example: `resource_type: 'companies' | 'people' | 'records' | 'tasks'`, } ); } return p; }, records_get_info: (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } if (!p.record_id) { throw new UniversalValidationError( 'Missing required parameter: record_id', ErrorType.USER_ERROR, { field: 'record_id', suggestion: 'Provide the unique identifier of the record to get info for', example: `record_id: 'comp_abc123'`, } ); } return p; }, // Legacy CRUD tools (still using hyphenated names) 'create-record': (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } if (!p.record_data) { throw new UniversalValidationError( 'Missing required parameter: record_data', ErrorType.USER_ERROR, { field: 'record_data', suggestion: 'Provide the data for creating the new record', example: `record_data: { name: 'Company Name', domain: 'example.com' }`, } ); } return p; }, 'update-record': (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } if (!p.record_id) { throw new UniversalValidationError( 'Missing required parameter: record_id', ErrorType.USER_ERROR, { field: 'record_id', example: `record_id: 'comp_abc123'` } ); } if (!p.record_data) { throw new UniversalValidationError( 'Missing required parameter: record_data', ErrorType.USER_ERROR, { field: 'record_data', suggestion: 'Provide the data to update the record with', example: `record_data: { name: 'Updated Name' }`, } ); } if (p.resource_type === 'tasks') { const forbidden = ['content', 'content_markdown', 'content_plaintext']; if (p.record_data && typeof p.record_data === 'object') { const recordData = p.record_data as Record<string, unknown>; for (const k of forbidden) { if (k in recordData) { throw new UniversalValidationError( 'Task content is immutable and cannot be updated' ); } } } } return p; }, 'delete-record': (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } if (!p.record_id) { throw new UniversalValidationError( 'Missing required parameter: record_id', ErrorType.USER_ERROR, { field: 'record_id', suggestion: 'Provide the ID of the record to delete', example: `record_id: 'comp_abc123'`, } ); } return p; }, 'create-note': (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'deals'` } ); } if (!p.record_id) { throw new UniversalValidationError( 'Missing required parameter: record_id', ErrorType.USER_ERROR, { field: 'record_id', suggestion: 'Provide the ID of the record to attach the note to', example: `record_id: '35dfdec5-f4a6-4a53-b5e0-f0809224e156'`, } ); } if (!p.title) { throw new UniversalValidationError( 'Missing required parameter: title', ErrorType.USER_ERROR, { field: 'title', suggestion: 'Provide a title for the note', example: `title: 'Meeting notes'`, } ); } if (!p.content) { throw new UniversalValidationError( 'Missing required parameter: content', ErrorType.USER_ERROR, { field: 'content', suggestion: 'Provide content for the note', example: `content: 'Discussion about project timeline'`, } ); } return p; }, 'get-notes': (p) => p, 'search-notes': (p) => p, 'update-note': (p) => { if (!p.note_id) { throw new UniversalValidationError( 'Missing required parameter: note_id', ErrorType.USER_ERROR, { field: 'note_id', suggestion: 'Provide the ID of the note to update', example: `note_id: 'note_abc123'`, } ); } return p; }, 'delete-note': (p) => { if (!p.note_id) { throw new UniversalValidationError( 'Missing required parameter: note_id', ErrorType.USER_ERROR, { field: 'note_id', suggestion: 'Provide the ID of the note to delete', example: `note_id: 'note_abc123'`, } ); } return p; }, records_batch: (p) => { if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } // Support both new flexible format (operations array) and legacy format (operation_type) const hasOperations = p.operations && Array.isArray(p.operations) && p.operations.length > 0; const hasLegacyFormat = p.operation_type; if (!hasOperations && !hasLegacyFormat) { throw new UniversalValidationError( 'Missing required parameters: either "operations" array or "operation_type"', ErrorType.USER_ERROR, { field: 'operations', example: `operations: [{ operation: 'create', record_data: { name: 'Example' } }]`, suggestion: 'Use either the new operations array format or legacy operation_type + records format', } ); } // Validate new format if (hasOperations) { const operations = p.operations as Array<Record<string, unknown>>; for (let index = 0; index < operations.length; index++) { const op = operations[index]; if (!op.operation) { throw new UniversalValidationError( `Missing operation type for operation at index ${index}`, ErrorType.USER_ERROR, { field: `operations[${index}].operation`, example: `operation: 'create'`, } ); } if (!op.record_data) { throw new UniversalValidationError( `Missing record_data for operation at index ${index}`, ErrorType.USER_ERROR, { field: `operations[${index}].record_data`, example: `record_data: { name: 'Example' }`, } ); } } } // Validate legacy format if (hasLegacyFormat) { const operationType = String(p.operation_type); if (['create', 'update'].includes(operationType) && !p.records) { throw new UniversalValidationError( `Missing required parameter for ${operationType} operations: records`, ErrorType.USER_ERROR, { field: 'records', suggestion: `Provide an array of record data for ${operationType} operations`, example: `records: [{ name: 'Company 1' }, { name: 'Company 2' }]`, } ); } if (['delete', 'get'].includes(operationType) && !p.record_ids) { throw new UniversalValidationError( `Missing required parameter for ${operationType} operations: record_ids`, ErrorType.USER_ERROR, { field: 'record_ids', suggestion: `Provide an array of record IDs for ${operationType} operations`, example: `record_ids: ['comp_abc123', 'comp_def456']`, } ); } } return p; }, 'list-notes': (p) => { const candidateParams = p as Record<string, unknown>; if (!p.record_id && typeof candidateParams.parent_record_id === 'string') { p.record_id = candidateParams.parent_record_id; } if (!p.resource_type) { throw new UniversalValidationError( 'Missing required parameter: resource_type', ErrorType.USER_ERROR, { field: 'resource_type', example: `resource_type: 'companies'` } ); } if (!p.record_id) { throw new UniversalValidationError( 'Missing required parameter: record_id', ErrorType.USER_ERROR, { field: 'record_id', suggestion: 'Provide the ID of the record to list notes for', example: `record_id: '35dfdec5-f4a6-4a53-b5e0-f0809224e156'`, } ); } return p; }, }; export function validateUniversalToolParams( toolName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any params: any ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { const sanitizedValue = InputSanitizer.sanitizeObject(params); if ( !sanitizedValue || typeof sanitizedValue !== 'object' || Array.isArray(sanitizedValue) ) { throw new UniversalValidationError( 'Invalid parameters: expected an object', ErrorType.USER_ERROR, { suggestion: 'Provide parameters as an object', example: '{ resource_type: "companies", ... }', httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, } ); } const sanitizedParams = sanitizedValue as SanitizedObject; validatePaginationParams(sanitizedParams); validateIdFields(sanitizedParams); if (sanitizedParams.resource_type) { const resourceType = String(sanitizedParams.resource_type); if ( !Object.values(UniversalResourceType).includes( resourceType as UniversalResourceType ) ) { const suggestion = suggestResourceType(resourceType); const validTypes = Object.values(UniversalResourceType).join(', '); throw new UniversalValidationError( `Invalid resource_type: '${resourceType}'`, ErrorType.USER_ERROR, { field: 'resource_type', suggestion: suggestion ? `Did you mean '${suggestion}'?` : undefined, example: `Expected one of: ${validTypes}`, httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, } ); } } const validator = toolValidators[toolName]; if (validator) return validator(sanitizedParams); return sanitizedParams; }

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/kesslerio/attio-mcp-server'

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