json_mcp_server/json_tools/
operations.rs

1use 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        // Create parent directories if needed
96        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        // Write the file
155        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        // Check if file exists
178        if !Path::new(file_path).exists() {
179            return Ok(ToolResult::error(format!("File '{}' does not exist", file_path)));
180        }
181
182        // Read and parse the file
183        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}