diff --git a/src/backend/src/mcp.rs b/src/backend/src/mcp.rs index 41b28cb..02dcc42 100644 --- a/src/backend/src/mcp.rs +++ b/src/backend/src/mcp.rs @@ -113,6 +113,30 @@ "temperature": { "type": "number" } } } + }, + { + "name": "update_item", + "description": "Update an existing item's content and path", + "inputSchema": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "content": { "type": "string" }, + "path": { "type": "string" } + }, + "required": ["id", "content"] + } + }, + { + "name": "delete_item", + "description": "Delete an item by ID", + "inputSchema": { + "type": "object", + "properties": { + "id": { "type": "number" } + }, + "required": ["id"] + } } ] })), @@ -218,6 +242,40 @@ } } } + "update_item" => { + let id = args["id"].as_i64().unwrap_or(0) as i32; + let content = args["content"].as_str().unwrap_or(""); + let path = args["path"].as_str().unwrap_or(""); + match handle_update_item(&state, id, content, path).await { + Ok(res) => Some(res), + Err(e) => { + return Json(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + result: None, + error: Some( + serde_json::json!({ "code": -32000, "message": e.to_string() }), + ), + id: payload.id.unwrap_or(serde_json::Value::Null), + }) + } + } + } + "delete_item" => { + let id = args["id"].as_i64().unwrap_or(0) as i32; + match handle_delete_item(&state, id).await { + Ok(res) => Some(res), + Err(e) => { + return Json(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + result: None, + error: Some( + serde_json::json!({ "code": -32000, "message": e.to_string() }), + ), + id: payload.id.unwrap_or(serde_json::Value::Null), + }) + } + } + } _ => Some(serde_json::json!({ "error": "Unknown tool" })), } } @@ -359,6 +417,64 @@ })) } +async fn handle_update_item( + state: &AppState, + id: i32, + content: &str, + path: &str, +) -> anyhow::Result { + let db = &state.db; + + // 1. Check if item exists + let item = items::Entity::find_by_id(id) + .one(db) + .await? + .ok_or_else(|| anyhow::anyhow!("Item with id {} not found", id))?; + + // 2. Generate new embedding if content changed + let embedding = state.llama.get_embedding(content).await?; + let embedding_bytes: Vec = embedding.iter().flat_map(|f| f.to_le_bytes()).collect(); + + // 3. Update items table + let mut active: items::ActiveModel = item.into(); + active.content = Set(content.to_owned()); + if !path.is_empty() { + active.path = Set(Some(path.to_owned())); + } + active.update(db).await?; + + // 4. Update vec_items table + db.execute(Statement::from_sql_and_values( + DatabaseBackend::Sqlite, + "UPDATE vec_items SET embedding = ? WHERE id = ?", + [embedding_bytes.into(), id.into()], + )) + .await?; + + Ok(serde_json::json!({ + "content": [{ "type": "text", "text": format!("Updated item with id {}", id) }] + })) +} + +async fn handle_delete_item(state: &AppState, id: i32) -> anyhow::Result { + let db = &state.db; + + // 1. Delete from vec_items first (due to potentially manual management / constraints) + db.execute(Statement::from_sql_and_values( + DatabaseBackend::Sqlite, + "DELETE FROM vec_items WHERE id = ?", + [id.into()], + )) + .await?; + + // 2. Delete from items + items::Entity::delete_by_id(id).exec(db).await?; + + Ok(serde_json::json!({ + "content": [{ "type": "text", "text": format!("Deleted item with id {}", id) }] + })) +} + #[cfg(test)] mod tests { use super::*;