use anyhow::{Result, anyhow}; use log::debug; use serde_json::Value; use std::collections::HashMap; use crate::modes::server::common::AppState; use crate::services::async_item_service::AsyncItemService; use crate::services::error::CoreError; #[derive(Debug, thiserror::Error)] pub enum ToolError { #[error("Unknown tool: {0}")] UnknownTool(String), #[error("Invalid arguments: {0}")] InvalidArguments(String), #[error("Database error: {0}")] Database(#[from] rusqlite::Error), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("Parse error: {0}")] Parse(#[from] strum::ParseError), #[error("Other error: {0}")] Other(#[from] anyhow::Error), } pub struct KeepTools { state: AppState, } impl KeepTools { pub fn new(state: AppState) -> Self { Self { state } } pub async fn save_item(&self, args: Option) -> Result { let args = args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?; let content = args .get("content") .and_then(|v| v.as_str()) .ok_or_else(|| ToolError::InvalidArguments("Missing 'content' field".to_string()))?; let tags: Vec = args .get("tags") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let metadata: HashMap = args .get("metadata") .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_default(); debug!( "MCP: Saving item with {} bytes, {} tags, {} metadata entries", content.len(), tags.len(), metadata.len() ); let service = AsyncItemService::new( self.state.data_dir.clone(), self.state.db.clone(), self.state.item_service.clone(), self.state.cmd.clone(), self.state.settings.clone(), ); let item_with_meta = service .save_item_from_mcp(content.as_bytes().to_vec(), tags, metadata) .await .map_err(|e| ToolError::Other(anyhow::Error::from(e)))?; let item_id = item_with_meta .item .id .ok_or_else(|| anyhow!("Failed to get item ID"))?; Ok(format!("Successfully saved item with ID: {}", item_id)) } pub async fn get_item(&self, args: Option) -> Result { let args = args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?; let item_id = args.get("id").and_then(|v| v.as_i64()).ok_or_else(|| { ToolError::InvalidArguments("Missing or invalid 'id' field".to_string()) })?; let service = AsyncItemService::new( self.state.data_dir.clone(), self.state.db.clone(), self.state.item_service.clone(), self.state.cmd.clone(), self.state.settings.clone(), ); let item_with_content = match service.get_item_content(item_id).await { Ok(iwc) => iwc, Err(CoreError::ItemNotFound(_)) => { return Err(ToolError::InvalidArguments(format!( "Item {} not found", item_id ))); } Err(e) => return Err(ToolError::Other(anyhow::Error::from(e))), }; let content = String::from_utf8_lossy(&item_with_content.content).to_string(); let tags: Vec = item_with_content .item_with_meta .tags .iter() .map(|t| t.name.clone()) .collect(); let metadata = item_with_content.item_with_meta.meta_as_map(); let item = item_with_content.item_with_meta.item; let response = serde_json::json!({ "id": item_id, "content": content, "timestamp": item.ts.to_rfc3339(), "size": item.size, "compression": item.compression, "tags": tags, "metadata": metadata, }); Ok(serde_json::to_string_pretty(&response)?) } pub async fn get_latest_item(&self, args: Option) -> Result { let tags: Vec = args .as_ref() .and_then(|v| v.get("tags")) .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let service = AsyncItemService::new( self.state.data_dir.clone(), self.state.db.clone(), self.state.item_service.clone(), self.state.cmd.clone(), self.state.settings.clone(), ); let item_with_meta = match service.find_item(vec![], tags, HashMap::new()).await { Ok(iwm) => iwm, Err(CoreError::ItemNotFoundGeneric) => { return Err(ToolError::InvalidArguments("No items found".to_string())); } Err(e) => return Err(ToolError::Other(anyhow::Error::from(e))), }; let item_id = item_with_meta .item .id .ok_or_else(|| anyhow!("Item missing ID after find"))?; let item_with_content = service .get_item_content(item_id) .await .map_err(|e| ToolError::Other(anyhow::Error::from(e)))?; let content = String::from_utf8_lossy(&item_with_content.content).to_string(); let tags: Vec = item_with_content .item_with_meta .tags .iter() .map(|t| t.name.clone()) .collect(); let metadata = item_with_content.item_with_meta.meta_as_map(); let item = item_with_content.item_with_meta.item; let response = serde_json::json!({ "id": item_id, "content": content, "timestamp": item.ts.to_rfc3339(), "size": item.size, "compression": item.compression, "tags": tags, "metadata": metadata, }); Ok(serde_json::to_string_pretty(&response)?) } pub async fn list_items(&self, args: Option) -> Result { let args_ref = args.as_ref(); let tags: Vec = args_ref .and_then(|v| v.get("tags")) .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let limit = args_ref .and_then(|v| v.get("limit")) .and_then(|v| v.as_u64()) .unwrap_or(10) as usize; let offset = args_ref .and_then(|v| v.get("offset")) .and_then(|v| v.as_u64()) .unwrap_or(0) as usize; let service = AsyncItemService::new( self.state.data_dir.clone(), self.state.db.clone(), self.state.item_service.clone(), self.state.cmd.clone(), self.state.settings.clone(), ); let mut items_with_meta = service .list_items(tags, HashMap::new()) .await .map_err(|e| ToolError::Other(anyhow::Error::from(e)))?; // Sort by timestamp (newest first) and apply pagination items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts)); let items_with_meta: Vec<_> = items_with_meta .into_iter() .skip(offset) .take(limit) .collect(); let items_info: Vec<_> = items_with_meta .into_iter() .map(|item_with_meta| { let item_tags: Vec = item_with_meta.tags.iter().map(|t| t.name.clone()).collect(); let item_meta = item_with_meta.meta_as_map(); let item = item_with_meta.item; let item_id = item.id.unwrap_or(0); serde_json::json!({ "id": item_id, "timestamp": item.ts.to_rfc3339(), "size": item.size, "compression": item.compression, "tags": item_tags, "metadata": item_meta }) }) .collect(); let response = serde_json::json!({ "items": items_info, "count": items_info.len(), "offset": offset, "limit": limit }); Ok(serde_json::to_string_pretty(&response)?) } pub async fn search_items(&self, args: Option) -> Result { let tags: Vec = args .as_ref() .and_then(|v| v.get("tags")) .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let metadata: HashMap = args .as_ref() .and_then(|v| v.get("metadata")) .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_default(); let service = AsyncItemService::new( self.state.data_dir.clone(), self.state.db.clone(), self.state.item_service.clone(), self.state.cmd.clone(), self.state.settings.clone(), ); let mut items_with_meta = service .list_items(tags.clone(), metadata.clone()) .await .map_err(|e| ToolError::Other(anyhow::Error::from(e)))?; // Sort by timestamp (newest first) items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts)); let items_info: Vec<_> = items_with_meta .into_iter() .map(|item_with_meta| { let item_tags: Vec = item_with_meta.tags.iter().map(|t| t.name.clone()).collect(); let item_meta = item_with_meta.meta_as_map(); let item = item_with_meta.item; let item_id = item.id.unwrap_or(0); serde_json::json!({ "id": item_id, "timestamp": item.ts.to_rfc3339(), "size": item.size, "compression": item.compression, "tags": item_tags, "metadata": item_meta }) }) .collect(); let response = serde_json::json!({ "items": items_info, "count": items_info.len(), "search_criteria": { "tags": tags, "metadata": metadata } }); Ok(serde_json::to_string_pretty(&response)?) } }