mcp_servers/coding/tools/write_file/
mod.rs1use crate::Displayed;
2use crate::file_ops::{FileError, write_text_file};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6use utils::display_meta::{FileDiff, ToolDisplayMeta, ToolResultMeta, basename};
7
8#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "camelCase")]
10pub struct WriteFileArgs {
11 #[serde(alias = "file_path")]
13 pub file_path: String,
14 pub content: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
19#[serde(rename_all = "camelCase")]
20pub struct WriteFileResponse {
21 pub message: String,
23 pub bytes_written: usize,
25 pub file_path: String,
27}
28
29pub async fn write_file_contents(args: WriteFileArgs) -> Result<Displayed<WriteFileResponse>, FileError> {
30 let file_path = Path::new(&args.file_path);
31 let result = write_text_file(file_path, &args.content).await?;
32 let file_path = result.path.to_string_lossy().into_owned();
33 let display_meta = ToolDisplayMeta::new("Write file", basename(&file_path));
34 let file_diff =
35 FileDiff { path: file_path.clone(), old_text: result.original_content, new_text: Some(args.content) };
36
37 let response = WriteFileResponse {
38 message: format!("Successfully wrote {} bytes to {}", result.bytes_written, file_path),
39 bytes_written: result.bytes_written,
40 file_path,
41 };
42 Ok(Displayed::new(response, ToolResultMeta::with_file_diff(display_meta, file_diff)))
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::testing::TestWorkspace;
49 use std::fs;
50
51 #[tokio::test]
52 async fn write_file_produces_file_diff_with_no_old_text() {
53 let workspace = TestWorkspace::new();
54 let file_path = workspace.path("new_file.rs");
55
56 let content = "fn main() {\n println!(\"Hello\");\n}\n";
57 let result = write_file_contents(WriteFileArgs {
58 file_path: file_path.to_string_lossy().to_string(),
59 content: content.to_string(),
60 })
61 .await
62 .unwrap();
63
64 assert!(file_path.exists());
65 assert_eq!(fs::read_to_string(&file_path).unwrap(), content);
66
67 let meta = result.meta.unwrap();
68 let diff = meta.file_diff.unwrap();
69 assert!(diff.old_text.is_none());
70 assert_eq!(diff.new_text.as_deref(), Some(content));
71 assert_eq!(diff.path, file_path.to_string_lossy().to_string());
72 }
73
74 #[tokio::test]
75 async fn write_file_file_diff_has_correct_path() {
76 let workspace = TestWorkspace::new();
77 let file_path = workspace.path("test.rs");
78
79 let result = write_file_contents(WriteFileArgs {
80 file_path: file_path.to_string_lossy().to_string(),
81 content: "let x = 1;".to_string(),
82 })
83 .await
84 .unwrap();
85
86 let diff = result.meta.unwrap().file_diff.unwrap();
87 assert_eq!(diff.path, file_path.to_string_lossy().to_string());
88 }
89
90 #[tokio::test]
91 async fn write_file_handles_empty_content() {
92 let workspace = TestWorkspace::new();
93 let file_path = workspace.path("empty.txt");
94
95 let result = write_file_contents(WriteFileArgs {
96 file_path: file_path.to_string_lossy().to_string(),
97 content: String::new(),
98 })
99 .await
100 .unwrap();
101
102 let diff = result.meta.unwrap().file_diff.unwrap();
103 assert_eq!(diff.new_text.as_deref(), Some(""));
104 assert!(diff.old_text.is_none());
105 }
106
107 #[tokio::test]
108 async fn write_file_overwrites_existing_file() {
109 let workspace = TestWorkspace::new().file("existing.txt", "old content");
110 let file_path = workspace.path("existing.txt");
111
112 let new_content = "new content\nsecond line\n";
113 let result = write_file_contents(WriteFileArgs {
114 file_path: file_path.to_string_lossy().to_string(),
115 content: new_content.to_string(),
116 })
117 .await
118 .unwrap();
119
120 assert_eq!(fs::read_to_string(&file_path).unwrap(), new_content);
121
122 let diff = result.meta.unwrap().file_diff.unwrap();
123 assert_eq!(diff.old_text.as_deref(), Some("old content"));
124 assert_eq!(diff.new_text.as_deref(), Some(new_content));
125 }
126
127 #[test]
128 fn write_file_args_accepts_snake_case_file_path() {
129 let args: WriteFileArgs = serde_json::from_value(serde_json::json!({
130 "file_path": "/tmp/out.txt",
131 "content": "hello"
132 }))
133 .unwrap();
134
135 assert_eq!(args.file_path, "/tmp/out.txt");
136 assert_eq!(args.content, "hello");
137 }
138}