claude_code_acp/mcp/tools/
write.rs1use 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#[derive(Debug, Default)]
17pub struct WriteTool;
18
19#[derive(Debug, Deserialize)]
21struct WriteInput {
22 file_path: String,
24 content: String,
26}
27
28impl WriteTool {
29 pub fn new() -> Self {
31 Self
32 }
33
34 async fn check_permission(
49 &self,
50 _input: &serde_json::Value,
51 _context: &ToolContext,
52 ) -> Option<ToolResult> {
53 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 }
93
94 async fn execute(&self, input: serde_json::Value, context: &ToolContext) -> ToolResult {
95 if let Some(result) = self.check_permission(&input, context).await {
97 return result;
98 }
99
100 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 let path = if std::path::Path::new(¶ms.file_path).is_absolute() {
108 std::path::PathBuf::from(¶ms.file_path)
109 } else {
110 context.cwd.join(¶ms.file_path)
111 };
112
113 let total_start = Instant::now();
114
115 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 let file_existed = path.exists();
132
133 let write_start = Instant::now();
135 match tokio::fs::write(&path, ¶ms.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 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 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 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}