Skip to main content
Glama
db.rs9.07 kB
use chrono::Utc; use once_cell::sync::Lazy; use rusqlite::{params, Connection}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Mutex; use uuid::Uuid; /// Secret record for API responses (value masked) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecretInfo { pub id: String, pub name: String, pub description: Option<String>, pub created_at: i64, pub updated_at: i64, } /// Full secret record including value (for internal use) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Secret { pub id: String, pub name: String, pub description: Option<String>, pub value: String, pub created_at: i64, pub updated_at: i64, } /// Search result (name and description only) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecretSearchResult { pub name: String, pub description: Option<String>, } /// Global database connection static DB: Lazy<Mutex<Option<Connection>>> = Lazy::new(|| Mutex::new(None)); /// Get the database file path fn get_db_path() -> PathBuf { let app_data = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); let app_dir = app_data.join("secret-mcp"); std::fs::create_dir_all(&app_dir).ok(); app_dir.join("secrets.db") } /// Get the database path as a string (for MCP server) pub fn get_db_path_string() -> String { get_db_path().to_string_lossy().to_string() } /// Initialize the database connection pub fn init_db() -> Result<(), String> { let db_path = get_db_path(); let conn = Connection::open(&db_path).map_err(|e| e.to_string())?; // Create secrets table conn.execute( "CREATE TABLE IF NOT EXISTS secrets ( id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, description TEXT, value TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL )", [], ) .map_err(|e| e.to_string())?; // Store connection globally let mut db = DB.lock().map_err(|e| e.to_string())?; *db = Some(conn); Ok(()) } /// Helper to get database connection fn with_db<T, F: FnOnce(&Connection) -> Result<T, String>>(f: F) -> Result<T, String> { let db = DB.lock().map_err(|e| e.to_string())?; let conn = db.as_ref().ok_or("Database not initialized")?; f(conn) } /// List all secrets (values masked) pub fn list_secrets() -> Result<Vec<SecretInfo>, String> { with_db(|conn| { let mut stmt = conn .prepare("SELECT id, name, description, created_at, updated_at FROM secrets ORDER BY name") .map_err(|e| e.to_string())?; let secrets = stmt .query_map([], |row| { Ok(SecretInfo { id: row.get(0)?, name: row.get(1)?, description: row.get(2)?, created_at: row.get(3)?, updated_at: row.get(4)?, }) }) .map_err(|e| e.to_string())? .collect::<Result<Vec<_>, _>>() .map_err(|e| e.to_string())?; Ok(secrets) }) } /// Get a single secret by ID (includes value) pub fn get_secret(id: &str) -> Result<Option<Secret>, String> { with_db(|conn| { let mut stmt = conn .prepare("SELECT id, name, description, value, created_at, updated_at FROM secrets WHERE id = ?") .map_err(|e| e.to_string())?; let mut rows = stmt.query(params![id]).map_err(|e| e.to_string())?; if let Some(row) = rows.next().map_err(|e| e.to_string())? { Ok(Some(Secret { id: row.get(0).map_err(|e| e.to_string())?, name: row.get(1).map_err(|e| e.to_string())?, description: row.get(2).map_err(|e| e.to_string())?, value: row.get(3).map_err(|e| e.to_string())?, created_at: row.get(4).map_err(|e| e.to_string())?, updated_at: row.get(5).map_err(|e| e.to_string())?, })) } else { Ok(None) } }) } /// Create a new secret pub fn create_secret(name: &str, description: Option<&str>, value: &str) -> Result<Secret, String> { let id = Uuid::new_v4().to_string(); let now = Utc::now().timestamp(); with_db(|conn| { conn.execute( "INSERT INTO secrets (id, name, description, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", params![id, name, description, value, now, now], ) .map_err(|e| e.to_string())?; Ok(Secret { id, name: name.to_string(), description: description.map(|s| s.to_string()), value: value.to_string(), created_at: now, updated_at: now, }) }) } /// Update an existing secret pub fn update_secret( id: &str, name: &str, description: Option<&str>, value: &str, ) -> Result<Secret, String> { let now = Utc::now().timestamp(); with_db(|conn| { let rows_affected = conn .execute( "UPDATE secrets SET name = ?, description = ?, value = ?, updated_at = ? WHERE id = ?", params![name, description, value, now, id], ) .map_err(|e| e.to_string())?; if rows_affected == 0 { return Err("Secret not found".to_string()); } // Get created_at from existing record let mut stmt = conn .prepare("SELECT created_at FROM secrets WHERE id = ?") .map_err(|e| e.to_string())?; let created_at: i64 = stmt .query_row(params![id], |row| row.get(0)) .map_err(|e| e.to_string())?; Ok(Secret { id: id.to_string(), name: name.to_string(), description: description.map(|s| s.to_string()), value: value.to_string(), created_at, updated_at: now, }) }) } /// Delete a secret pub fn delete_secret(id: &str) -> Result<bool, String> { with_db(|conn| { let rows_affected = conn .execute("DELETE FROM secrets WHERE id = ?", params![id]) .map_err(|e| e.to_string())?; Ok(rows_affected > 0) }) } /// Search secrets by name or description (fuzzy match) pub fn search_secrets(query: &str) -> Result<Vec<SecretSearchResult>, String> { with_db(|conn| { let pattern = format!("%{}%", query.to_lowercase()); let mut stmt = conn .prepare( "SELECT name, description FROM secrets WHERE LOWER(name) LIKE ? OR LOWER(COALESCE(description, '')) LIKE ? ORDER BY name", ) .map_err(|e| e.to_string())?; let results = stmt .query_map(params![&pattern, &pattern], |row| { Ok(SecretSearchResult { name: row.get(0)?, description: row.get(1)?, }) }) .map_err(|e| e.to_string())? .collect::<Result<Vec<_>, _>>() .map_err(|e| e.to_string())?; Ok(results) }) } /// Get secret values by names (for writing .env files) pub fn get_values_by_names(names: &[String]) -> Result<Vec<(String, String)>, String> { with_db(|conn| { let mut results = Vec::new(); for name in names { let mut stmt = conn .prepare("SELECT name, value FROM secrets WHERE name = ?") .map_err(|e| e.to_string())?; if let Ok(value) = stmt.query_row(params![name], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) }) { results.push(value); } } Ok(results) }) } /// Write secrets to a .env file pub fn write_env_file(keys: &[String], path: &str) -> Result<(usize, Vec<String>), String> { // Validate path - must be absolute let path = std::path::Path::new(path); if !path.is_absolute() { return Err("Path must be absolute".to_string()); } // Get values let values = get_values_by_names(keys)?; let found_names: std::collections::HashSet<_> = values.iter().map(|(n, _)| n.clone()).collect(); // Find missing keys let missing: Vec<String> = keys .iter() .filter(|k| !found_names.contains(*k)) .cloned() .collect(); // Build .env content let content: String = values .iter() .map(|(name, value)| { // Escape value if needed if value.contains(' ') || value.contains('"') || value.contains('\'') || value.contains('\n') { format!("{}=\"{}\"\n", name, value.replace('"', "\\\"")) } else { format!("{}={}\n", name, value) } }) .collect(); // Write file std::fs::write(path, content).map_err(|e| e.to_string())?; Ok((values.len(), missing)) }

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/AKarenin/Secret-mcp'

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