claude_code_acp/mcp/tools/
write.rs

1//! Write tool implementation
2//!
3//! Writes content to files on the filesystem.
4
5use async_trait::async_trait;
6use serde::Deserialize;
7use serde_json::json;
8use std::time::Instant;
9
10use super::base::{Tool, ToolKind};
11use crate::mcp::registry::{ToolContext, ToolResult};
12// TODO: Uncomment when implementing permission checks
13// use crate::settings::{PermissionCheckResult, PermissionDecision};
14
15/// Write tool for creating/overwriting files
16#[derive(Debug, Default)]
17pub struct WriteTool;
18
19/// Write tool input parameters
20#[derive(Debug, Deserialize)]
21struct WriteInput {
22    /// Path to the file to write
23    file_path: String,
24    /// Content to write to the file
25    content: String,
26}
27
28impl WriteTool {
29    /// Create a new Write tool instance
30    pub fn new() -> Self {
31        Self
32    }
33
34    /// Check permission before executing the tool
35    ///
36    /// TODO: Implement interactive permission request flow
37    ///
38    /// Current implementation: Always allow execution (commented out permission checks)
39    ///
40    /// Future implementation should:
41    /// 1. Check for explicit deny rules - block if matched
42    /// 2. Check for explicit allow rules - allow if matched
43    /// 3. For "Ask" decisions - send permission request to client via PermissionManager
44    /// 4. Wait for user response - allow or deny based on user choice
45    ///
46    /// Architecture note: SDK does NOT call can_use_tool for MCP tools, so we need
47    /// to implement the permission request flow within the tool execution path.
48    async fn check_permission(
49        &self,
50        _input: &serde_json::Value,
51        _context: &ToolContext,
52    ) -> Option<ToolResult> {
53        // TODO: Implement permission checking
54        // See Edit tool's check_permission for implementation template
55        None
56    }
57}
58
59#[async_trait]
60impl Tool for WriteTool {
61    fn name(&self) -> &str {
62        "Write"
63    }
64
65    fn description(&self) -> &str {
66        "Write content to a file. Creates the file if it doesn't exist, or overwrites it if it does. Creates parent directories as needed."
67    }
68
69    fn input_schema(&self) -> serde_json::Value {
70        json!({
71            "type": "object",
72            "required": ["file_path", "content"],
73            "properties": {
74                "file_path": {
75                    "type": "string",
76                    "description": "The absolute path to the file to write"
77                },
78                "content": {
79                    "type": "string",
80                    "description": "The content to write to the file"
81                }
82            }
83        })
84    }
85
86    fn kind(&self) -> ToolKind {
87        ToolKind::Edit
88    }
89
90    fn requires_permission(&self) -> bool {
91        true // Writing requires permission
92    }
93
94    async fn execute(&self, input: serde_json::Value, context: &ToolContext) -> ToolResult {
95        // Check permission before executing
96        if let Some(result) = self.check_permission(&input, context).await {
97            return result;
98        }
99
100        // Parse input
101        let params: WriteInput = match serde_json::from_value(input) {
102            Ok(p) => p,
103            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
104        };
105
106        // Resolve path relative to working directory if not absolute
107        let path = if std::path::Path::new(&params.file_path).is_absolute() {
108            std::path::PathBuf::from(&params.file_path)
109        } else {
110            context.cwd.join(&params.file_path)
111        };
112
113        let total_start = Instant::now();
114
115        // Create parent directories if they don't exist
116        if let Some(parent) = path.parent() {
117            if !parent.exists() {
118                let dir_start = Instant::now();
119                if let Err(e) = tokio::fs::create_dir_all(parent).await {
120                    return ToolResult::error(format!("Failed to create directory: {}", e));
121                }
122                tracing::debug!(
123                    parent_dir = %parent.display(),
124                    dir_creation_duration_ms = dir_start.elapsed().as_millis(),
125                    "Parent directories created"
126                );
127            }
128        }
129
130        // Check if file exists (for reporting)
131        let file_existed = path.exists();
132
133        // Write content to file
134        let write_start = Instant::now();
135        match tokio::fs::write(&path, &params.content).await {
136            Ok(()) => {
137                let write_duration = write_start.elapsed();
138                let total_elapsed = total_start.elapsed();
139
140                let action = if file_existed { "Updated" } else { "Created" };
141                let lines = params.content.lines().count();
142                let bytes = params.content.len();
143
144                tracing::info!(
145                    file_path = %path.display(),
146                    action = %action,
147                    lines = lines,
148                    bytes = bytes,
149                    write_duration_ms = write_duration.as_millis(),
150                    total_elapsed_ms = total_elapsed.as_millis(),
151                    "File write successful"
152                );
153
154                ToolResult::success(format!(
155                    "{} {} ({} lines, {} bytes)",
156                    action,
157                    path.display(),
158                    lines,
159                    bytes
160                ))
161                .with_metadata(json!({
162                    "path": path.display().to_string(),
163                    "created": !file_existed,
164                    "lines": lines,
165                    "bytes": bytes,
166                    "write_duration_ms": write_duration.as_millis(),
167                    "total_elapsed_ms": total_elapsed.as_millis()
168                }))
169            }
170            Err(e) => {
171                let elapsed = total_start.elapsed();
172                tracing::error!(
173                    file_path = %path.display(),
174                    error = %e,
175                    elapsed_ms = elapsed.as_millis(),
176                    "File write failed"
177                );
178                ToolResult::error(format!("Failed to write file: {}", e))
179            }
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use tempfile::TempDir;
188
189    #[tokio::test]
190    async fn test_write_new_file() {
191        let temp_dir = TempDir::new().unwrap();
192        let file_path = temp_dir.path().join("new_file.txt");
193
194        let tool = WriteTool::new();
195        let context = ToolContext::new("test", temp_dir.path());
196
197        let result = tool
198            .execute(
199                json!({
200                    "file_path": file_path.to_str().unwrap(),
201                    "content": "Hello, World!"
202                }),
203                &context,
204            )
205            .await;
206
207        assert!(!result.is_error);
208        assert!(result.content.contains("Created"));
209
210        // Verify file was created
211        let content = std::fs::read_to_string(&file_path).unwrap();
212        assert_eq!(content, "Hello, World!");
213    }
214
215    #[tokio::test]
216    async fn test_write_overwrite_file() {
217        let temp_dir = TempDir::new().unwrap();
218        let file_path = temp_dir.path().join("existing.txt");
219
220        // Create existing file
221        std::fs::write(&file_path, "Original content").unwrap();
222
223        let tool = WriteTool::new();
224        let context = ToolContext::new("test", temp_dir.path());
225
226        let result = tool
227            .execute(
228                json!({
229                    "file_path": file_path.to_str().unwrap(),
230                    "content": "New content"
231                }),
232                &context,
233            )
234            .await;
235
236        assert!(!result.is_error);
237        assert!(result.content.contains("Updated"));
238
239        // Verify content was replaced
240        let content = std::fs::read_to_string(&file_path).unwrap();
241        assert_eq!(content, "New content");
242    }
243
244    #[tokio::test]
245    async fn test_write_creates_directories() {
246        let temp_dir = TempDir::new().unwrap();
247        let file_path = temp_dir.path().join("nested/dir/file.txt");
248
249        let tool = WriteTool::new();
250        let context = ToolContext::new("test", temp_dir.path());
251
252        let result = tool
253            .execute(
254                json!({
255                    "file_path": file_path.to_str().unwrap(),
256                    "content": "Nested content"
257                }),
258                &context,
259            )
260            .await;
261
262        assert!(!result.is_error);
263        assert!(file_path.exists());
264    }
265
266    #[test]
267    fn test_write_tool_properties() {
268        let tool = WriteTool::new();
269        assert_eq!(tool.name(), "Write");
270        assert_eq!(tool.kind(), ToolKind::Edit);
271        assert!(tool.requires_permission());
272    }
273}