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
13#[derive(Debug, Default)]
15pub struct WriteTool;
16
17#[derive(Debug, Deserialize)]
19struct WriteInput {
20 file_path: String,
22 content: String,
24}
25
26impl WriteTool {
27 pub fn new() -> Self {
29 Self
30 }
31
32 fn check_permission(
38 &self,
39 _input: &serde_json::Value,
40 _context: &ToolContext,
41 ) -> Option<ToolResult> {
42 None
44 }
45}
46
47#[async_trait]
48impl Tool for WriteTool {
49 fn name(&self) -> &str {
50 "Write"
51 }
52
53 fn description(&self) -> &str {
54 "Write content to a file. Creates the file if it doesn't exist, or overwrites it if it does. Creates parent directories as needed."
55 }
56
57 fn input_schema(&self) -> serde_json::Value {
58 json!({
59 "type": "object",
60 "required": ["file_path", "content"],
61 "properties": {
62 "file_path": {
63 "type": "string",
64 "description": "The absolute path to the file to write"
65 },
66 "content": {
67 "type": "string",
68 "description": "The content to write to the file"
69 }
70 }
71 })
72 }
73
74 fn kind(&self) -> ToolKind {
75 ToolKind::Edit
76 }
77
78 fn requires_permission(&self) -> bool {
79 true }
81
82 async fn execute(&self, input: serde_json::Value, context: &ToolContext) -> ToolResult {
83 if let Some(result) = self.check_permission(&input, context) {
85 return result;
86 }
87
88 let params: WriteInput = match serde_json::from_value(input) {
90 Ok(p) => p,
91 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
92 };
93
94 let path = if std::path::Path::new(¶ms.file_path).is_absolute() {
96 std::path::PathBuf::from(¶ms.file_path)
97 } else {
98 context.cwd.join(¶ms.file_path)
99 };
100
101 let total_start = Instant::now();
102
103 if let Some(parent) = path.parent() {
105 if !parent.exists() {
106 let dir_start = Instant::now();
107 if let Err(e) = tokio::fs::create_dir_all(parent).await {
108 return ToolResult::error(format!("Failed to create directory: {}", e));
109 }
110 tracing::debug!(
111 parent_dir = %parent.display(),
112 dir_creation_duration_ms = dir_start.elapsed().as_millis(),
113 "Parent directories created"
114 );
115 }
116 }
117
118 let file_existed = path.exists();
120
121 let write_start = Instant::now();
123 match tokio::fs::write(&path, ¶ms.content).await {
124 Ok(()) => {
125 let write_duration = write_start.elapsed();
126 let total_elapsed = total_start.elapsed();
127
128 let action = if file_existed { "Updated" } else { "Created" };
129 let lines = params.content.lines().count();
130 let bytes = params.content.len();
131
132 tracing::info!(
133 file_path = %path.display(),
134 action = %action,
135 lines = lines,
136 bytes = bytes,
137 write_duration_ms = write_duration.as_millis(),
138 total_elapsed_ms = total_elapsed.as_millis(),
139 "File write successful"
140 );
141
142 ToolResult::success(format!(
143 "{} {} ({} lines, {} bytes)",
144 action,
145 path.display(),
146 lines,
147 bytes
148 ))
149 .with_metadata(json!({
150 "path": path.display().to_string(),
151 "created": !file_existed,
152 "lines": lines,
153 "bytes": bytes,
154 "write_duration_ms": write_duration.as_millis(),
155 "total_elapsed_ms": total_elapsed.as_millis()
156 }))
157 }
158 Err(e) => {
159 let elapsed = total_start.elapsed();
160 tracing::error!(
161 file_path = %path.display(),
162 error = %e,
163 elapsed_ms = elapsed.as_millis(),
164 "File write failed"
165 );
166 ToolResult::error(format!("Failed to write file: {}", e))
167 }
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use tempfile::TempDir;
176
177 #[tokio::test]
178 async fn test_write_new_file() {
179 let temp_dir = TempDir::new().unwrap();
180 let file_path = temp_dir.path().join("new_file.txt");
181
182 let tool = WriteTool::new();
183 let context = ToolContext::new("test", temp_dir.path());
184
185 let result = tool
186 .execute(
187 json!({
188 "file_path": file_path.to_str().unwrap(),
189 "content": "Hello, World!"
190 }),
191 &context,
192 )
193 .await;
194
195 assert!(!result.is_error);
196 assert!(result.content.contains("Created"));
197
198 let content = std::fs::read_to_string(&file_path).unwrap();
200 assert_eq!(content, "Hello, World!");
201 }
202
203 #[tokio::test]
204 async fn test_write_overwrite_file() {
205 let temp_dir = TempDir::new().unwrap();
206 let file_path = temp_dir.path().join("existing.txt");
207
208 std::fs::write(&file_path, "Original content").unwrap();
210
211 let tool = WriteTool::new();
212 let context = ToolContext::new("test", temp_dir.path());
213
214 let result = tool
215 .execute(
216 json!({
217 "file_path": file_path.to_str().unwrap(),
218 "content": "New content"
219 }),
220 &context,
221 )
222 .await;
223
224 assert!(!result.is_error);
225 assert!(result.content.contains("Updated"));
226
227 let content = std::fs::read_to_string(&file_path).unwrap();
229 assert_eq!(content, "New content");
230 }
231
232 #[tokio::test]
233 async fn test_write_creates_directories() {
234 let temp_dir = TempDir::new().unwrap();
235 let file_path = temp_dir.path().join("nested/dir/file.txt");
236
237 let tool = WriteTool::new();
238 let context = ToolContext::new("test", temp_dir.path());
239
240 let result = tool
241 .execute(
242 json!({
243 "file_path": file_path.to_str().unwrap(),
244 "content": "Nested content"
245 }),
246 &context,
247 )
248 .await;
249
250 assert!(!result.is_error);
251 assert!(file_path.exists());
252 }
253
254 #[test]
255 fn test_write_tool_properties() {
256 let tool = WriteTool::new();
257 assert_eq!(tool.name(), "Write");
258 assert_eq!(tool.kind(), ToolKind::Edit);
259 assert!(tool.requires_permission());
260 }
261}