json_mcp_server/json_tools/
operations.rs1use crate::mcp::protocol::{Tool, ToolCall, ToolResult};
2use crate::mcp::server::ToolHandler;
3use serde_json::{json, Value};
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8pub struct JsonOperations;
9
10impl JsonOperations {
11 pub fn new() -> Self {
12 Self
13 }
14
15 fn create_write_tool() -> Tool {
16 Tool {
17 name: "json-write".to_string(),
18 description: "Write or update a JSON file with support for different write modes".to_string(),
19 input_schema: json!({
20 "type": "object",
21 "properties": {
22 "file_path": {
23 "type": "string",
24 "description": "Path to the JSON file to write"
25 },
26 "data": {
27 "description": "JSON data to write. Can be any valid JSON value"
28 },
29 "mode": {
30 "type": "string",
31 "enum": ["replace", "merge", "append"],
32 "default": "replace",
33 "description": "Write mode: 'replace' overwrites file, 'merge' merges with existing JSON (objects only), 'append' appends to arrays"
34 },
35 "create_dirs": {
36 "type": "boolean",
37 "default": true,
38 "description": "Create parent directories if they don't exist"
39 },
40 "pretty": {
41 "type": "boolean",
42 "default": true,
43 "description": "Format JSON with indentation"
44 }
45 },
46 "required": ["file_path", "data"]
47 }),
48 }
49 }
50
51 fn create_validate_tool() -> Tool {
52 Tool {
53 name: "json-validate".to_string(),
54 description: "Validate JSON file syntax and structure".to_string(),
55 input_schema: json!({
56 "type": "object",
57 "properties": {
58 "file_path": {
59 "type": "string",
60 "description": "Path to the JSON file to validate"
61 },
62 "schema": {
63 "description": "Optional JSON schema to validate against"
64 }
65 },
66 "required": ["file_path"]
67 }),
68 }
69 }
70
71 async fn handle_write(&self, args: &HashMap<String, Value>) -> anyhow::Result<ToolResult> {
72 let file_path = args.get("file_path")
73 .and_then(|v| v.as_str())
74 .ok_or_else(|| anyhow::anyhow!(
75 "file_path is required. Usage example:\n{{\n \"file_path\": \"./output.json\",\n \"data\": {{\"key\": \"value\"}}\n}}"
76 ))?;
77
78 let data = args.get("data")
79 .ok_or_else(|| anyhow::anyhow!(
80 "data is required. Usage example:\n{{\n \"file_path\": \"./output.json\",\n \"data\": {{\"key\": \"value\"}}\n}}"
81 ))?;
82
83 let mode = args.get("mode")
84 .and_then(|v| v.as_str())
85 .unwrap_or("replace");
86
87 let create_dirs = args.get("create_dirs")
88 .and_then(|v| v.as_bool())
89 .unwrap_or(true);
90
91 let pretty = args.get("pretty")
92 .and_then(|v| v.as_bool())
93 .unwrap_or(true);
94
95 if create_dirs {
97 if let Some(parent) = Path::new(file_path).parent() {
98 fs::create_dir_all(parent)
99 .map_err(|e| anyhow::anyhow!("Failed to create directories: {}", e))?;
100 }
101 }
102
103 let final_data = match mode {
104 "replace" => data.clone(),
105 "merge" => {
106 if Path::new(file_path).exists() {
107 let existing_content = fs::read_to_string(file_path)
108 .map_err(|e| anyhow::anyhow!("Failed to read existing file: {}", e))?;
109
110 let mut existing_json: Value = serde_json::from_str(&existing_content)
111 .map_err(|e| anyhow::anyhow!("Failed to parse existing JSON: {}", e))?;
112
113 if let (Some(existing_obj), Some(new_obj)) = (existing_json.as_object_mut(), data.as_object()) {
114 for (key, value) in new_obj {
115 existing_obj.insert(key.clone(), value.clone());
116 }
117 existing_json
118 } else {
119 return Ok(ToolResult::error("Merge mode requires both existing and new data to be objects".to_string()));
120 }
121 } else {
122 data.clone()
123 }
124 },
125 "append" => {
126 if Path::new(file_path).exists() {
127 let existing_content = fs::read_to_string(file_path)
128 .map_err(|e| anyhow::anyhow!("Failed to read existing file: {}", e))?;
129
130 let mut existing_json: Value = serde_json::from_str(&existing_content)
131 .map_err(|e| anyhow::anyhow!("Failed to parse existing JSON: {}", e))?;
132
133 if let Some(existing_array) = existing_json.as_array_mut() {
134 if let Some(new_array) = data.as_array() {
135 existing_array.extend(new_array.clone());
136 } else {
137 existing_array.push(data.clone());
138 }
139 existing_json
140 } else {
141 return Ok(ToolResult::error("Append mode requires existing data to be an array".to_string()));
142 }
143 } else {
144 if data.is_array() {
145 data.clone()
146 } else {
147 json!([data])
148 }
149 }
150 },
151 _ => return Ok(ToolResult::error(format!("Unknown write mode: {}", mode))),
152 };
153
154 let content = if pretty {
156 serde_json::to_string_pretty(&final_data)?
157 } else {
158 serde_json::to_string(&final_data)?
159 };
160
161 fs::write(file_path, content)
162 .map_err(|e| anyhow::anyhow!("Failed to write file '{}': {}", file_path, e))?;
163
164 Ok(ToolResult::success(format!(
165 "Successfully wrote JSON to '{}' using {} mode",
166 file_path, mode
167 )))
168 }
169
170 async fn handle_validate(&self, args: &HashMap<String, Value>) -> anyhow::Result<ToolResult> {
171 let file_path = args.get("file_path")
172 .and_then(|v| v.as_str())
173 .ok_or_else(|| anyhow::anyhow!(
174 "file_path is required. Usage example:\n{{\n \"file_path\": \"./data.json\"\n}}"
175 ))?;
176
177 if !Path::new(file_path).exists() {
179 return Ok(ToolResult::error(format!("File '{}' does not exist", file_path)));
180 }
181
182 let content = fs::read_to_string(file_path)
184 .map_err(|e| anyhow::anyhow!("Failed to read file '{}': {}", file_path, e))?;
185
186 match serde_json::from_str::<Value>(&content) {
187 Ok(json_value) => {
188 let size = content.len();
189 let type_name = match &json_value {
190 Value::Object(_) => "object",
191 Value::Array(_) => "array",
192 Value::String(_) => "string",
193 Value::Number(_) => "number",
194 Value::Bool(_) => "boolean",
195 Value::Null => "null",
196 };
197
198 Ok(ToolResult::success(format!(
199 "JSON file '{}' is valid:\n- Type: {}\n- Size: {} bytes\n- Structure: {}",
200 file_path,
201 type_name,
202 size,
203 if json_value.is_object() {
204 format!("{} properties", json_value.as_object().unwrap().len())
205 } else if json_value.is_array() {
206 format!("{} elements", json_value.as_array().unwrap().len())
207 } else {
208 "primitive value".to_string()
209 }
210 )))
211 },
212 Err(e) => Ok(ToolResult::error(format!(
213 "JSON validation failed for '{}': {}",
214 file_path, e
215 ))),
216 }
217 }
218}
219
220#[async_trait::async_trait]
221impl ToolHandler for JsonOperations {
222 async fn get_tools(&self) -> anyhow::Result<Vec<Tool>> {
223 Ok(vec![
224 Self::create_write_tool(),
225 Self::create_validate_tool(),
226 ])
227 }
228
229 async fn call_tool(&self, tool_call: ToolCall) -> anyhow::Result<ToolResult> {
230 match tool_call.name.as_str() {
231 "json-write" => self.handle_write(&tool_call.arguments).await,
232 "json-validate" => self.handle_validate(&tool_call.arguments).await,
233 _ => Ok(ToolResult::error(format!("Unknown tool: {}", tool_call.name))),
234 }
235 }
236}