/// Validation constants for input field lengths. pub const MAX_NAME_LENGTH: usize = 256; pub const MAX_FOLDER_LENGTH: usize = 128; pub const MAX_ENTRY_TYPE_LENGTH: usize = 64; pub const MAX_NOTES_LENGTH: usize = 10000; pub const MAX_TAG_LENGTH: usize = 64; pub const MAX_TAG_COUNT: usize = 50; pub const MAX_META_KEY_LENGTH: usize = 128; pub const MAX_META_VALUE_LENGTH: usize = 4096; pub const MAX_META_COUNT: usize = 100; /// Validate input field lengths for MCP tools. /// /// Returns an error if any field exceeds its maximum length. pub fn validate_input_lengths( name: &str, folder: Option<&str>, entry_type: Option<&str>, notes: Option<&str>, ) -> Result<(), rmcp::ErrorData> { if name.chars().count() > MAX_NAME_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!("name must be at most {} characters", MAX_NAME_LENGTH), None, )); } if let Some(folder) = folder && folder.chars().count() > MAX_FOLDER_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!("folder must be at most {} characters", MAX_FOLDER_LENGTH), None, )); } if let Some(entry_type) = entry_type && entry_type.chars().count() > MAX_ENTRY_TYPE_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!("type must be at most {} characters", MAX_ENTRY_TYPE_LENGTH), None, )); } if let Some(notes) = notes && notes.chars().count() > MAX_NOTES_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!("notes must be at most {} characters", MAX_NOTES_LENGTH), None, )); } Ok(()) } /// Validate the tags list. /// /// Checks total count and per-tag character length. pub fn validate_tags(tags: &[String]) -> Result<(), rmcp::ErrorData> { if tags.len() > MAX_TAG_COUNT { return Err(rmcp::ErrorData::invalid_params( format!("at most {} tags are allowed", MAX_TAG_COUNT), None, )); } for tag in tags { if tag.chars().count() > MAX_TAG_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!( "tag '{}' exceeds the maximum length of {} characters", tag, MAX_TAG_LENGTH ), None, )); } } Ok(()) } /// Validate metadata KV strings (key=value / key:=json format). /// /// Checks total count and per-key/per-value character lengths. /// This is a best-effort check on the raw KV strings before parsing; /// keys containing `:` path separators are checked as a whole. pub fn validate_meta_entries(entries: &[String]) -> Result<(), rmcp::ErrorData> { if entries.len() > MAX_META_COUNT { return Err(rmcp::ErrorData::invalid_params( format!("at most {} metadata entries are allowed", MAX_META_COUNT), None, )); } for entry in entries { // key:=json — check both key and JSON value length if let Some((key, value)) = entry.split_once(":=") { if key.chars().count() > MAX_META_KEY_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!( "metadata key '{}' exceeds the maximum length of {} characters", key, MAX_META_KEY_LENGTH ), None, )); } if value.chars().count() > MAX_META_VALUE_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!( "metadata JSON value for key '{}' exceeds the maximum length of {} characters", key, MAX_META_VALUE_LENGTH ), None, )); } continue; } // key=value or key@path if let Some((key, value)) = entry.split_once('=') { if key.chars().count() > MAX_META_KEY_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!( "metadata key '{}' exceeds the maximum length of {} characters", key, MAX_META_KEY_LENGTH ), None, )); } if value.chars().count() > MAX_META_VALUE_LENGTH { return Err(rmcp::ErrorData::invalid_params( format!( "metadata value for key '{}' exceeds the maximum length of {} characters", key, MAX_META_VALUE_LENGTH ), None, )); } } else { // Fallback: entry without = or := — check total length let max_total = MAX_META_KEY_LENGTH + MAX_META_VALUE_LENGTH; if entry.chars().count() > max_total { let preview = entry.chars().take(50).collect::(); return Err(rmcp::ErrorData::invalid_params( format!( "metadata entry '{}' exceeds the maximum length of {} characters", preview, max_total ), None, )); } } } Ok(()) }