//! Filesystem-backed [`McpToolPermissionStore`] for project MCP tool permissions. use std::sync::Arc; use async_trait::async_trait; use domain::ports::{FileSystem, FsError, McpToolPermissionStore, RemotePath, StoreError}; use domain::{Project, ProjectMcpToolPermissions}; use crate::orchestrator::mcp::tools; const MCP_TOOL_PERMISSIONS_FILE: &str = "mcp-tool-permissions.json"; /// JSON-file implementation for `/.ideai/mcp-tool-permissions.json`. #[derive(Clone)] pub struct FsMcpToolPermissionStore { fs: Arc, } impl FsMcpToolPermissionStore { /// Builds the store from an injected filesystem port. #[must_use] pub fn new(fs: Arc) -> Self { Self { fs } } fn path(project: &Project) -> RemotePath { let root = project.root.as_str().trim_end_matches(['/', '\\']); RemotePath::new(format!("{root}/.ideai/{MCP_TOOL_PERMISSIONS_FILE}")) } async fn ensure_ideai(&self, project: &Project) -> Result<(), StoreError> { let root = project.root.as_str().trim_end_matches(['/', '\\']); self.fs .create_dir_all(&RemotePath::new(format!("{root}/.ideai"))) .await .map_err(|e| StoreError::Io(e.to_string())) } fn validate(permissions: &ProjectMcpToolPermissions) -> Result<(), StoreError> { let known_tools = tools::classified_tool_names(); permissions .validate(&known_tools) .map_err(|e| StoreError::Serialization(e.to_string())) } } #[async_trait] impl McpToolPermissionStore for FsMcpToolPermissionStore { async fn load_mcp_tool_permissions( &self, project: &Project, ) -> Result { match self.fs.read(&Self::path(project)).await { Ok(bytes) => { let permissions = serde_json::from_slice::(&bytes) .map_err(|e| StoreError::Serialization(e.to_string()))?; Self::validate(&permissions)?; Ok(permissions) } Err(FsError::NotFound(_)) => Ok(ProjectMcpToolPermissions::default()), Err(e) => Err(StoreError::Io(e.to_string())), } } async fn save_mcp_tool_permissions( &self, project: &Project, permissions: &ProjectMcpToolPermissions, ) -> Result<(), StoreError> { Self::validate(permissions)?; self.ensure_ideai(project).await?; let mut bytes = serde_json::to_vec_pretty(permissions) .map_err(|e| StoreError::Serialization(e.to_string()))?; bytes.push(b'\n'); self.fs .write(&Self::path(project), &bytes) .await .map_err(|e| StoreError::Io(e.to_string())) } }