use crate::router::McpTool; use crate::state::MemoryState; use crate::tools::*; use async_trait::async_trait; use serde_json::Value; use std::sync::Arc; pub struct UpdateEnvFingerprintHandler; #[async_trait] impl McpTool for UpdateEnvFingerprintHandler { fn name(&self) -> &'static str { "update_env_fingerprint" } fn schema(&self) -> Value { crate::mcp::tool_def::( "update_env_fingerprint", "Execute update_env_fingerprint", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: UpdateEnvFingerprintTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.env.env_fingerprints.modify(|fps| { fps.insert( req.namespace.clone(), crate::models::EnvFingerprint { namespace: req.namespace.clone(), os: std::env::consts::OS.to_string(), shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()), tool_versions: req.tool_versions, updated_at: crate::handlers::utils::now_secs(), }, ); }); Ok("Env fingerprint updated".to_string()) } } pub struct ReadEnvFingerprintHandler; #[async_trait] impl McpTool for ReadEnvFingerprintHandler { fn name(&self) -> &'static str { "read_env_fingerprint" } fn schema(&self) -> Value { crate::mcp::tool_def::( "read_env_fingerprint", "Execute read_env_fingerprint", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ReadEnvFingerprintTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state .env .env_fingerprints .read_with(|fps| fps.get(&req.namespace).cloned()); if let Some(fp) = data { let data = Ok::(serde_json::to_string(&fp)?)?; Ok(data) } else { Ok("{}".to_string()) } } } pub struct LogEnvRequirementHandler; #[async_trait] impl McpTool for LogEnvRequirementHandler { fn name(&self) -> &'static str { "log_env_requirement" } fn schema(&self) -> Value { crate::mcp::tool_def::( "log_env_requirement", "Execute log_env_requirement", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.env.env_requirements.modify(|reqs| { reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key)); reqs.push(crate::models::EnvRequirement { namespace: req.namespace, key: req.key, description: req.description, is_secret: req.is_secret, }); }); Ok("Env requirement logged".to_string()) } } pub struct RegisterEnvironmentHandler; #[async_trait] impl McpTool for RegisterEnvironmentHandler { fn name(&self) -> &'static str { "register_environment" } fn schema(&self) -> Value { crate::mcp::tool_def::( "register_environment", "Execute register_environment", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: RegisterEnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.env.environments.modify(|envs| { envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name)); envs.push(crate::models::EnvironmentDetail { namespace: req.namespace, name: req.name, url: req.url, description: req.description, requires_vpn: req.requires_vpn, updated_at: crate::handlers::utils::now_secs(), }); }); Ok("Environment registered".to_string()) } } pub struct GetEnvironmentDetailsHandler; #[async_trait] impl McpTool for GetEnvironmentDetailsHandler { fn name(&self) -> &'static str { "get_environment_details" } fn schema(&self) -> Value { crate::mcp::tool_def::( "get_environment_details", "Execute get_environment_details", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: GetEnvironmentDetailsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.env.environments.read_with(|envs| { let filtered: Vec<_> = envs .iter() .filter(|e| e.namespace == req.namespace) .collect(); Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } } pub struct EnvironmentHandler; #[async_trait] impl McpTool for EnvironmentHandler { fn name(&self) -> &'static str { "environment" } fn schema(&self) -> Value { crate::mcp::tool_def::( "environment", "Consolidated environment & infrastructure management", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); match req.action { EnvAction::UpdateFingerprint => { let tool_versions = req.tool_versions.unwrap_or_default(); UpdateEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns, "tool_versions": tool_versions}), state).await } EnvAction::ReadFingerprint => { ReadEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns}), state).await } EnvAction::LogRequirement => { let key = req.key.ok_or_else(|| { crate::error::AppError::Internal("key is required for 'log_requirement' action".to_string()) })?; let desc = req.description.unwrap_or_default(); let secret = req.is_secret.unwrap_or(false); LogEnvRequirementHandler.execute(serde_json::json!({"key": key, "description": desc, "is_secret": secret, "namespace": ns}), state).await } EnvAction::Register => { let name = req.name.ok_or_else(|| { crate::error::AppError::Internal("name is required for 'register' action".to_string()) })?; let url = req.url.unwrap_or_default(); let desc = req.description.unwrap_or_default(); let vpn = req.requires_vpn.unwrap_or(false); RegisterEnvironmentHandler.execute(serde_json::json!({"name": name, "url": url, "description": desc, "requires_vpn": vpn, "namespace": ns}), state).await } EnvAction::GetDetails => { GetEnvironmentDetailsHandler.execute(serde_json::json!({"namespace": ns}), state).await } } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; use tempfile::tempdir; #[tokio::test] async fn test_env_fingerprint() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let update_handler = UpdateEnvFingerprintHandler; let args = json!({ "namespace": "global", "tool_versions": { "rustc": "1.70.0" } }); let res = update_handler .execute(args, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res, "Env fingerprint updated"); let read_handler = ReadEnvFingerprintHandler; let res2 = read_handler .execute(json!({"namespace": "global"}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res2.contains("rustc")); assert!(res2.contains("1.70.0")); } #[tokio::test] async fn test_env_details() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); // Ensure namespace is present in test setup state.env.environments.modify(|e| { e.push(crate::models::EnvironmentDetail { namespace: "global".to_string(), name: "test".to_string(), url: "http://localhost".to_string(), description: "Test env".to_string(), requires_vpn: false, updated_at: crate::handlers::utils::now_secs(), }); }); let handler = GetEnvironmentDetailsHandler; let res = handler .execute(json!({"namespace": "global"}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res.contains("global")); } #[tokio::test] async fn test_env_requirements_and_registration() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let req_handler = LogEnvRequirementHandler; let args_req = json!({ "key": "vpn", "description": "Requires VPN", "is_secret": false, "requirement": "Needs specific VPN", "context": "For database access", "namespace": "global" }); let res1 = req_handler .execute(args_req, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res1, "Env requirement logged"); let reg_handler = RegisterEnvironmentHandler; let args_reg = json!({ "name": "prod", "url": "https://prod.local", "description": "Production", "requires_vpn": true, "namespace": "global" }); let res2 = reg_handler .execute(args_reg, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res2, "Environment registered"); let get_handler = GetEnvironmentDetailsHandler; let res3 = get_handler .execute(json!({"namespace": "global"}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res3.contains("prod.local")); assert!(!res3.is_empty()); } #[tokio::test] async fn test_empty_namespace_in_update_env_fingerprint() { let dir = tempfile::tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let update_handler = UpdateEnvFingerprintHandler; let args = serde_json::json!({ "namespace": "", "tool_versions": { "rustc": "1.70.0" } }); let res = update_handler .execute(args, state.clone()) .await; assert!(res.is_ok()); } }