Skip to main content
Glama
validation.ts28 kB
/** * Argument validation utilities for FlintNote MCP Server tools * * Note: This file intentionally uses 'any' types for runtime validation * of unknown input data, which is the appropriate pattern for validation. */ /* eslint-disable @typescript-eslint/no-explicit-any */ export interface ValidationResult { isValid: boolean; errors: string[]; } export interface ValidationRule { field: string; required?: boolean; type?: 'string' | 'number' | 'boolean' | 'array' | 'object'; arrayItemType?: 'string' | 'number' | 'boolean' | 'object'; minLength?: number; maxLength?: number; allowEmpty?: boolean; customValidator?: (value: any) => string | null; } /** * Validates arguments against a set of validation rules */ export function validateArgs(args: any, rules: ValidationRule[]): ValidationResult { const errors: string[] = []; // Check if args is null or undefined if (args === null || args === undefined) { return { isValid: false, errors: ['Arguments object is required but was not provided'] }; } // Check if args is an object if (typeof args !== 'object' || Array.isArray(args)) { return { isValid: false, errors: ['Arguments must be an object'] }; } for (const rule of rules) { const value = args?.[rule.field]; const fieldErrors = validateField(rule.field, value, rule); errors.push(...fieldErrors); } return { isValid: errors.length === 0, errors }; } /** * Validates a single field against its rule */ function validateField(fieldName: string, value: any, rule: ValidationRule): string[] { const errors: string[] = []; // Check if required field is missing if (rule.required && (value === undefined || value === null)) { errors.push(`Required field '${fieldName}' is missing`); return errors; } // If field is not required and is missing, skip further validation if (!rule.required && (value === undefined || value === null)) { return errors; } // Check type if (rule.type && !validateType(value, rule.type, rule.arrayItemType)) { if (rule.type === 'array' && rule.arrayItemType) { errors.push( `Field '${fieldName}' must be an array of ${rule.arrayItemType} values` ); } else { errors.push(`Field '${fieldName}' must be of type ${rule.type}`); } } // Check string-specific validations if (rule.type === 'string' && typeof value === 'string') { if (!rule.allowEmpty && value.trim() === '') { errors.push(`Field '${fieldName}' cannot be empty`); } if (rule.minLength !== undefined && value.length < rule.minLength) { errors.push( `Field '${fieldName}' must be at least ${rule.minLength} characters long` ); } if (rule.maxLength !== undefined && value.length > rule.maxLength) { errors.push( `Field '${fieldName}' must be no more than ${rule.maxLength} characters long` ); } } // Check array-specific validations if (rule.type === 'array' && Array.isArray(value)) { if (!rule.allowEmpty && value.length === 0) { errors.push(`Field '${fieldName}' cannot be an empty array`); } if (rule.minLength !== undefined && value.length < rule.minLength) { errors.push(`Field '${fieldName}' must contain at least ${rule.minLength} items`); } if (rule.maxLength !== undefined && value.length > rule.maxLength) { errors.push( `Field '${fieldName}' must contain no more than ${rule.maxLength} items` ); } } // Run custom validator if provided if (rule.customValidator) { const customError = rule.customValidator(value); if (customError) { errors.push(`Field '${fieldName}': ${customError}`); } } return errors; } /** * Validates the type of a value */ function validateType(value: any, expectedType: string, arrayItemType?: string): boolean { switch (expectedType) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number' && !isNaN(value); case 'boolean': return typeof value === 'boolean'; case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value); case 'array': if (!Array.isArray(value)) return false; if (arrayItemType) { return value.every(item => validateType(item, arrayItemType)); } return true; default: return false; } } /** * Creates a validation error with a helpful message */ export function createValidationError(toolName: string, errors: string[]): Error { // For compatibility with existing tests, return simpler error messages if (errors.length === 1) { return new Error(errors[0]); } const errorMessage = [ `Invalid arguments for tool '${toolName}':`, ...errors.map(error => ` - ${error}`), '', 'Please check the tool documentation for correct usage.' ].join('\n'); return new Error(errorMessage); } /** * Predefined validation rules for each tool */ export const TOOL_VALIDATION_RULES: Record<string, ValidationRule[]> = { get_note: [ { field: 'identifier', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!value.includes('/')) { return 'identifier must be in format "type/filename"'; } const parts = value.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return 'identifier must be in format "type/filename" with both parts non-empty'; } return null; } }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false }, { field: 'fields', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true } ], get_notes: [ { field: 'identifiers', required: true, type: 'array', arrayItemType: 'string', allowEmpty: false, minLength: 1, customValidator: (value: any) => { for (const identifier of value) { if (!identifier.includes('/')) { return `identifier "${identifier}" must be in format "type/filename"`; } const parts = identifier.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return `identifier "${identifier}" must be in format "type/filename" with both parts non-empty`; } } return null; } }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false }, { field: 'fields', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true } ], create_note_type: [ { field: 'type_name', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { // Check for filesystem-safe characters if (!/^[a-zA-Z0-9_-]+$/.test(value)) { return 'Invalid note type name'; } return null; } }, { field: 'description', required: true, type: 'string', allowEmpty: false }, { field: 'agent_instructions', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true }, { field: 'metadata_schema', required: false, type: 'object' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], create_note: [ { field: 'type', required: false, // Not required when using batch creation type: 'string', allowEmpty: true }, { field: 'title', required: false, // Not required when using batch creation type: 'string', allowEmpty: false }, { field: 'content', required: false, // Not required when using batch creation type: 'string', allowEmpty: true }, { field: 'metadata', required: false, type: 'object' }, { field: 'notes', required: false, type: 'array', allowEmpty: true, customValidator: (value: any) => { if (!Array.isArray(value)) return null; for (let i = 0; i < value.length; i++) { const note = value[i]; if (typeof note !== 'object' || note === null) { return `notes[${i}] must be an object`; } if (note.type !== undefined && typeof note.type !== 'string') { return `notes[${i}].type must be a string`; } if (note.title !== undefined && typeof note.title !== 'string') { return `notes[${i}].title must be a string`; } if (note.content !== undefined && typeof note.content !== 'string') { return `notes[${i}].content must be a string`; } } return null; } }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], update_note: [ { field: 'identifier', required: false, // Not required when using batch updates type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!value || !value.includes('/')) { return 'identifier must be in format "type/filename"'; } return null; } }, { field: 'content', required: false, type: 'string', allowEmpty: true }, { field: 'metadata', required: false, type: 'object' }, { field: 'content_hash', required: false, // Required for single updates, checked in handler type: 'string', allowEmpty: false }, { field: 'updates', required: false, type: 'array', allowEmpty: false, customValidator: (value: any) => { if (!Array.isArray(value)) return null; for (let i = 0; i < value.length; i++) { const update = value[i]; if (typeof update !== 'object' || update === null) { return `updates[${i}] must be an object`; } if (!update.identifier || typeof update.identifier !== 'string') { return `updates[${i}].identifier is required and must be a string`; } if (!update.content_hash || typeof update.content_hash !== 'string') { return `updates[${i}].content_hash is required and must be a string`; } } return null; } }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], delete_note: [ { field: 'identifier', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!value.includes('/')) { return 'identifier must be in format "type/filename"'; } return null; } }, { field: 'confirm', required: false, type: 'boolean' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], rename_note: [ { field: 'identifier', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!value.includes('/')) { return 'identifier must be in format "type/filename"'; } return null; } }, { field: 'new_title', required: true, type: 'string', allowEmpty: false }, { field: 'content_hash', required: true, type: 'string', allowEmpty: false }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], search_notes: [ { field: 'query', required: false, type: 'string', allowEmpty: true }, { field: 'type_filter', required: false, type: 'string', allowEmpty: false }, { field: 'limit', required: false, type: 'number' }, { field: 'use_regex', required: false, type: 'boolean' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false }, { field: 'fields', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true } ], search_notes_sql: [ { field: 'query', required: true, type: 'string', allowEmpty: false }, { field: 'params', required: false, type: 'array', allowEmpty: true }, { field: 'limit', required: false, type: 'number' }, { field: 'timeout', required: false, type: 'number' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false }, { field: 'fields', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true } ], get_note_info: [ { field: 'title_or_filename', required: true, type: 'string', allowEmpty: false }, { field: 'type', required: false, type: 'string', allowEmpty: false }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], list_notes_by_type: [ { field: 'type', required: true, type: 'string', allowEmpty: false }, { field: 'limit', required: false, type: 'number' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], list_note_types: [ { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], update_note_type: [ { field: 'type_name', required: true, type: 'string', allowEmpty: false }, { field: 'instructions', required: false, type: 'string', allowEmpty: true }, { field: 'description', required: false, type: 'string', allowEmpty: false }, { field: 'metadata_schema', required: false, type: 'array', allowEmpty: true }, { field: 'content_hash', required: true, type: 'string', allowEmpty: false }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], get_note_type_info: [ { field: 'type_name', required: true, type: 'string', allowEmpty: false }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], create_vault: [ { field: 'id', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!/^[a-zA-Z0-9_-]+$/.test(value)) { return 'id must contain only letters, numbers, underscores, and hyphens'; } return null; } }, { field: 'name', required: true, type: 'string', allowEmpty: false }, { field: 'path', required: true, type: 'string', allowEmpty: false }, { field: 'description', required: false, type: 'string', allowEmpty: true }, { field: 'initialize', required: false, type: 'boolean' }, { field: 'switch_to', required: false, type: 'boolean' } ], switch_vault: [ { field: 'id', required: true, type: 'string', allowEmpty: false } ], remove_vault: [ { field: 'id', required: true, type: 'string', allowEmpty: false } ], update_vault: [ { field: 'id', required: true, type: 'string', allowEmpty: false }, { field: 'name', required: false, type: 'string', allowEmpty: false }, { field: 'description', required: false, type: 'string', allowEmpty: true } ], search_notes_advanced: [ { field: 'type', required: false, type: 'string', allowEmpty: false }, { field: 'metadata_filters', required: false, type: 'array', allowEmpty: true }, { field: 'updated_within', required: false, type: 'string', allowEmpty: false }, { field: 'updated_before', required: false, type: 'string', allowEmpty: false }, { field: 'created_within', required: false, type: 'string', allowEmpty: false }, { field: 'created_before', required: false, type: 'string', allowEmpty: false }, { field: 'content_contains', required: false, type: 'string', allowEmpty: false }, { field: 'sort', required: false, type: 'array', allowEmpty: true }, { field: 'limit', required: false, type: 'number' }, { field: 'offset', required: false, type: 'number' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false }, { field: 'fields', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true } ], get_note_links: [ { field: 'identifier', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!value.includes('/')) { return 'identifier must be in format "type/filename"'; } const parts = value.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return 'identifier must be in format "type/filename" with both parts non-empty'; } return null; } }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], get_backlinks: [ { field: 'identifier', required: true, type: 'string', allowEmpty: false, customValidator: (value: string) => { if (!value.includes('/')) { return 'identifier must be in format "type/filename"'; } const parts = value.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return 'identifier must be in format "type/filename" with both parts non-empty'; } return null; } }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], find_broken_links: [ { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], search_by_links: [ { field: 'has_links_to', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true, customValidator: (value: any) => { if (!Array.isArray(value)) return null; for (const identifier of value) { if (!identifier.includes('/')) { return `identifier "${identifier}" must be in format "type/filename"`; } const parts = identifier.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return `identifier "${identifier}" must be in format "type/filename" with both parts non-empty`; } } return null; } }, { field: 'linked_from', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true, customValidator: (value: any) => { if (!Array.isArray(value)) return null; for (const identifier of value) { if (!identifier.includes('/')) { return `identifier "${identifier}" must be in format "type/filename"`; } const parts = identifier.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return `identifier "${identifier}" must be in format "type/filename" with both parts non-empty`; } } return null; } }, { field: 'external_domains', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true }, { field: 'broken_links', required: false, type: 'boolean' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], migrate_links: [ { field: 'force', required: false, type: 'boolean' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], delete_note_type: [ { field: 'type_name', required: true, type: 'string', allowEmpty: false }, { field: 'action', required: true, type: 'string', customValidator: (value: any) => { if (!['error', 'migrate', 'delete'].includes(value)) { return 'action must be one of: error, migrate, delete'; } return null; } }, { field: 'target_type', required: false, type: 'string', allowEmpty: false }, { field: 'confirm', required: false, type: 'boolean' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], bulk_delete_notes: [ { field: 'type', required: false, type: 'string', allowEmpty: false }, { field: 'tags', required: false, type: 'array', arrayItemType: 'string', allowEmpty: true }, { field: 'pattern', required: false, type: 'string', allowEmpty: false }, { field: 'confirm', required: false, type: 'boolean' }, { field: 'vault_id', required: false, type: 'string', allowEmpty: false } ], move_note: [ { field: 'identifier', required: true, type: 'string', allowEmpty: false, customValidator: (value: any) => { if (!value.includes('/')) { return 'identifier must be in format "type/filename"'; } return null; } }, { field: 'new_type', required: true, type: 'string', allowEmpty: false }, { field: 'content_hash', required: true, type: 'string', allowEmpty: false }, { field: 'vault_id', required: false, type: 'string', allowEmpty: true } ] }; /** * Validates arguments for a specific tool */ export function validateToolArgs(toolName: string, args: any): void { const rules = TOOL_VALIDATION_RULES[toolName]; if (!rules) { throw new Error(`No validation rules defined for tool: ${toolName}`); } const result = validateArgs(args, rules); if (!result.isValid) { throw createValidationError(toolName, result.errors); } // Additional tool-specific validations switch (toolName) { case 'create_note': validateCreateNoteArgs(args); break; case 'update_note': validateUpdateNoteArgs(args); break; case 'search_notes_advanced': validateSearchNotesAdvancedArgs(args); break; case 'bulk_delete_notes': validateBulkDeleteNotesArgs(args); break; } } /** * Additional validation for create_note tool */ function validateCreateNoteArgs(args: any): void { const hasNotesArray = args.notes && Array.isArray(args.notes); const hasValidNotes = hasNotesArray && args.notes.length > 0; const hasSingleNote = args.type !== undefined && args.title !== undefined; // If notes array is provided but empty, let business logic handle it if (hasNotesArray && !hasValidNotes) { throw createValidationError('create_note', [ 'Multiple note creation requires at least one note to create' ]); } if (!hasValidNotes && !hasSingleNote) { throw createValidationError('create_note', [ 'Single note creation requires type, title, and content' ]); } if ( hasValidNotes && (args.type !== undefined || args.title !== undefined || args.content !== undefined) ) { throw createValidationError('create_note', [ 'Cannot provide both single note fields and batch notes array' ]); } } /** * Additional validation for update_note tool */ function validateUpdateNoteArgs(args: any): void { const hasUpdates = args.updates && Array.isArray(args.updates) && args.updates.length > 0; const hasSingleUpdate = args.identifier; if (!hasUpdates && !hasSingleUpdate) { throw createValidationError('update_note', [ 'Single note update requires identifier' ]); } if (hasUpdates && hasSingleUpdate) { throw createValidationError('update_note', [ 'Cannot provide both single update fields and batch updates array' ]); } if (hasSingleUpdate) { if (!args.content_hash) { throw createValidationError('update_note', ['content_hash is required']); } if (!args.content && !args.metadata) { throw createValidationError('update_note', [ 'Either content or metadata must be provided for update' ]); } } } /** * Additional validation for search_notes_advanced tool */ function validateSearchNotesAdvancedArgs(args: any): void { if (args.metadata_filters && Array.isArray(args.metadata_filters)) { for (let i = 0; i < args.metadata_filters.length; i++) { const filter = args.metadata_filters[i]; if (typeof filter !== 'object' || filter === null) { throw createValidationError('search_notes_advanced', [ `metadata_filters[${i}] must be an object` ]); } if (!filter.key || typeof filter.key !== 'string') { throw createValidationError('search_notes_advanced', [ `metadata_filters[${i}].key is required and must be a string` ]); } if (filter.value === undefined || filter.value === null) { throw createValidationError('search_notes_advanced', [ `metadata_filters[${i}].value is required` ]); } if ( filter.operator && !['=', '!=', '>', '<', '>=', '<=', 'LIKE', 'IN'].includes(filter.operator) ) { throw createValidationError('search_notes_advanced', [ `metadata_filters[${i}].operator must be one of: =, !=, >, <, >=, <=, LIKE, IN` ]); } } } if (args.sort && Array.isArray(args.sort)) { for (let i = 0; i < args.sort.length; i++) { const sortRule = args.sort[i]; if (typeof sortRule !== 'object' || sortRule === null) { throw createValidationError('search_notes_advanced', [ `sort[${i}] must be an object` ]); } if ( !sortRule.field || !['title', 'type', 'created', 'updated', 'size'].includes(sortRule.field) ) { throw createValidationError('search_notes_advanced', [ `sort[${i}].field must be one of: title, type, created, updated, size` ]); } if (!sortRule.order || !['asc', 'desc'].includes(sortRule.order)) { throw createValidationError('search_notes_advanced', [ `sort[${i}].order must be either 'asc' or 'desc'` ]); } } } } /** * Additional validation for bulk_delete_notes tool */ function validateBulkDeleteNotesArgs(args: any): void { const hasType = args.type && typeof args.type === 'string'; const hasTags = args.tags && Array.isArray(args.tags) && args.tags.length > 0; const hasPattern = args.pattern && typeof args.pattern === 'string'; if (!hasType && !hasTags && !hasPattern) { throw createValidationError('bulk_delete_notes', [ 'At least one filter must be provided: type, tags, or pattern' ]); } }

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/disnet/flint-note'

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