1use anyhow::{Context, Result};
5use serde::Deserialize;
6use serde_json::{json, Value};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::process::Stdio;
10use tokio::process::Command;
11use tracing::{debug, info};
12
13use crate::protocol::Tool;
14use crate::validation;
15
16#[derive(Debug, Deserialize)]
18pub struct ToolsConfig {
19 #[serde(default)]
20 pub include: Vec<String>,
21 #[serde(default)]
22 pub tools: Vec<ToolDefinition>,
23}
24
25#[derive(Debug, Clone, Deserialize)]
26pub struct ToolDefinition {
27 pub name: String,
28 pub description: String,
29 #[serde(default)]
30 pub command: String,
31 #[serde(default)]
32 pub args: Vec<ArgDefinition>,
33 #[serde(default)]
34 pub static_flags: Vec<String>,
35 pub internal_handler: Option<String>,
36 #[allow(dead_code)]
37 pub example_output: Option<Value>,
38 #[serde(default)]
39 pub validation: ValidationConfig,
40}
41
42#[derive(Debug, Clone, Deserialize, Default)]
43pub struct ValidationConfig {
44 #[serde(default)]
45 pub validate_paths: bool,
46 #[serde(default)]
47 pub allow_absolute_paths: bool,
48 #[serde(default)]
49 pub validate_args: bool,
50}
51
52#[derive(Debug, Clone, Deserialize)]
53pub struct ArgDefinition {
54 pub name: String,
55 pub description: String,
56 pub required: bool,
57 #[serde(rename = "type")]
58 pub arg_type: String,
59 pub cli_flag: Option<String>,
60 #[allow(dead_code)]
61 pub default: Option<String>,
62 #[serde(default)]
63 pub is_path: bool, }
65
66pub struct ToolManager {
67 tools: HashMap<String, ToolDefinition>,
68}
69
70impl ToolManager {
71 pub fn new() -> Self {
72 Self {
73 tools: HashMap::new(),
74 }
75 }
76
77 pub async fn load_from_file(&mut self, path: &Path) -> Result<()> {
79 info!("Loading tools from: {}", path.display());
80
81 let content = tokio::fs::read_to_string(path)
82 .await
83 .context("Failed to read tools file")?;
84
85 let config: ToolsConfig = serde_yaml::from_str(&content).context("Failed to parse YAML")?;
87
88 for include in &config.include {
90 let include_path = self.resolve_include_path(path, include)?;
91 info!("Including tools from: {}", include_path.display());
92
93 Box::pin(self.load_from_file(&include_path)).await?;
95 }
96
97 for tool in config.tools {
99 info!("Loaded tool: {}", tool.name);
100 self.tools.insert(tool.name.clone(), tool);
101 }
102
103 Ok(())
104 }
105
106 fn resolve_include_path(&self, base_path: &Path, include: &str) -> Result<PathBuf> {
107 let base_dir = base_path
108 .parent()
109 .ok_or_else(|| anyhow::anyhow!("Cannot determine parent directory"))?;
110
111 let include_path = if include.starts_with('/') {
113 PathBuf::from(include)
114 } else {
115 match include.starts_with("~/") {
116 true => {
117 if let Some(home) = directories::UserDirs::new() {
118 home.home_dir().join(&include[2..])
119 } else {
120 return Err(anyhow::anyhow!("Cannot resolve home directory"));
121 }
122 }
123 false => {
124 base_dir.join(include)
126 }
127 }
128 };
129
130 if !include_path.exists() {
131 return Err(anyhow::anyhow!(
132 "Include file not found: {}",
133 include_path.display()
134 ));
135 }
136
137 Ok(include_path)
138 }
139
140 pub async fn load_from_default_locations(&mut self) -> Result<()> {
141 let paths = vec![
143 PathBuf::from("./tools.yaml"),
144 PathBuf::from("~/.config/gamecode-mcp/tools.yaml"),
145 ];
146
147 if let Ok(tools_file) = std::env::var("GAMECODE_TOOLS_FILE") {
148 return self.load_from_file(Path::new(&tools_file)).await;
149 }
150
151 for path in paths {
152 let expanded = if path.starts_with("~") {
153 if let Some(home) = directories::UserDirs::new() {
154 home.home_dir().join(path.strip_prefix("~").unwrap())
155 } else {
156 continue;
157 }
158 } else {
159 path
160 };
161
162 if expanded.exists() {
163 return self.load_from_file(&expanded).await;
164 }
165 }
166
167 Err(anyhow::anyhow!("No tools.yaml file found"))
168 }
169
170 pub async fn load_mode(&mut self, mode: &str) -> Result<()> {
171 self.tools.clear();
173
174 let mode_file = format!("tools/profiles/{}.yaml", mode);
176 let mode_path = PathBuf::from(&mode_file);
177
178 if mode_path.exists() {
179 self.load_from_file(&mode_path).await
180 } else {
181 if let Some(home) = directories::UserDirs::new() {
183 let config_path = home
184 .home_dir()
185 .join(".config/gamecode-mcp")
186 .join(&mode_file);
187 if config_path.exists() {
188 return self.load_from_file(&config_path).await;
189 }
190 }
191
192 Err(anyhow::anyhow!("Mode configuration '{}' not found", mode))
193 }
194 }
195
196 pub async fn detect_and_load_mode(&mut self) -> Result<()> {
197 let detections = vec![
199 ("Cargo.toml", "rust"),
200 ("package.json", "javascript"),
201 ("requirements.txt", "python"),
202 ("go.mod", "go"),
203 ("pom.xml", "java"),
204 ("build.gradle", "java"),
205 ("Gemfile", "ruby"),
206 ];
207
208 for (file, mode) in detections {
209 if PathBuf::from(file).exists() {
210 info!("Detected {} project, loading {} tools", mode, mode);
211
212 let lang_file = format!("tools/languages/{}.yaml", mode);
214 if PathBuf::from(&lang_file).exists() {
215 self.load_from_file(Path::new(&lang_file)).await?;
216 }
217
218 if PathBuf::from("tools/core.yaml").exists() {
220 self.load_from_file(Path::new("tools/core.yaml")).await?;
221 }
222
223 if PathBuf::from(".git").exists() && PathBuf::from("tools/git.yaml").exists() {
225 self.load_from_file(Path::new("tools/git.yaml")).await?;
226 }
227
228 return Ok(());
229 }
230 }
231
232 self.load_from_default_locations().await
234 }
235
236 pub fn get_mcp_tools(&self) -> Vec<Tool> {
238 self.tools
239 .values()
240 .map(|def| {
241 let mut properties = serde_json::Map::new();
242 let mut required = Vec::new();
243
244 for arg in &def.args {
246 let arg_schema = match arg.arg_type.as_str() {
247 "string" => json!({
248 "type": "string",
249 "description": arg.description
250 }),
251 "number" => json!({
252 "type": "number",
253 "description": arg.description
254 }),
255 "boolean" => json!({
256 "type": "boolean",
257 "description": arg.description
258 }),
259 "array" => json!({
260 "type": "array",
261 "description": arg.description
262 }),
263 _ => json!({
264 "type": "string",
265 "description": arg.description
266 }),
267 };
268
269 properties.insert(arg.name.clone(), arg_schema);
270
271 if arg.required {
272 required.push(json!(arg.name));
273 }
274 }
275
276 let schema = json!({
277 "type": "object",
278 "properties": properties,
279 "required": required
280 });
281
282 Tool {
283 name: def.name.clone(),
284 description: def.description.clone(),
285 input_schema: schema,
286 }
287 })
288 .collect()
289 }
290
291 pub async fn execute_tool(&self, name: &str, args: Value) -> Result<Value> {
293 let tool = self
294 .tools
295 .get(name)
296 .ok_or_else(|| anyhow::anyhow!("Tool '{}' not found", name))?;
297
298 if let Some(handler) = &tool.internal_handler {
300 return self.execute_internal_handler(handler, &args).await;
301 }
302
303 if tool.command.is_empty() || tool.command == "internal" {
305 return Err(anyhow::anyhow!("Tool '{}' has no command", name));
306 }
307
308 let mut cmd = Command::new(&tool.command);
309
310 for flag in &tool.static_flags {
312 cmd.arg(flag);
313 }
314
315 if let Some(obj) = args.as_object() {
317 for arg_def in &tool.args {
318 if let Some(value) = obj.get(&arg_def.name) {
319 if tool.validation.validate_args {
321 validation::validate_typed_value(value, &arg_def.arg_type)?;
322 }
323
324 if arg_def.is_path && tool.validation.validate_paths {
326 if let Some(path_str) = value.as_str() {
327 validation::validate_path(path_str, tool.validation.allow_absolute_paths)?;
328 }
329 }
330
331 let arg_value = value.to_string().trim_matches('"').to_string();
332
333 if let Some(cli_flag) = &arg_def.cli_flag {
334 cmd.arg(cli_flag);
335 cmd.arg(&arg_value);
336 } else {
337 cmd.arg(&arg_value);
339 }
340 }
341 }
342 }
343
344 debug!("Executing command: {:?}", cmd);
345
346 let output = cmd
347 .stdout(Stdio::piped())
348 .stderr(Stdio::piped())
349 .output()
350 .await
351 .context("Failed to execute command")?;
352
353 if output.status.success() {
354 let stdout = String::from_utf8_lossy(&output.stdout);
355
356 if let Ok(json_value) = serde_json::from_str::<Value>(&stdout) {
358 Ok(json_value)
359 } else {
360 Ok(json!({
361 "output": stdout.trim(),
362 "status": "success"
363 }))
364 }
365 } else {
366 let stderr = String::from_utf8_lossy(&output.stderr);
367 Err(anyhow::anyhow!("Command failed: {}", stderr))
368 }
369 }
370
371 async fn execute_internal_handler(&self, handler: &str, args: &Value) -> Result<Value> {
373 match handler {
374 "add" => {
375 let a = args
376 .get("a")
377 .and_then(|v| v.as_f64())
378 .ok_or_else(|| anyhow::anyhow!("Missing parameter 'a'"))?;
379 let b = args
380 .get("b")
381 .and_then(|v| v.as_f64())
382 .ok_or_else(|| anyhow::anyhow!("Missing parameter 'b'"))?;
383 Ok(json!({
384 "result": a + b,
385 "operation": "addition"
386 }))
387 }
388 "multiply" => {
389 let a = args
390 .get("a")
391 .and_then(|v| v.as_f64())
392 .ok_or_else(|| anyhow::anyhow!("Missing parameter 'a'"))?;
393 let b = args
394 .get("b")
395 .and_then(|v| v.as_f64())
396 .ok_or_else(|| anyhow::anyhow!("Missing parameter 'b'"))?;
397 Ok(json!({
398 "result": a * b,
399 "operation": "multiplication"
400 }))
401 }
402 "list_files" => {
403 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
404
405 let mut files = Vec::new();
406 let mut entries = tokio::fs::read_dir(path).await?;
407
408 while let Some(entry) = entries.next_entry().await? {
409 let metadata = entry.metadata().await?;
410 files.push(json!({
411 "name": entry.file_name().to_string_lossy(),
412 "is_dir": metadata.is_dir(),
413 "size": metadata.len()
414 }));
415 }
416
417 Ok(json!({
418 "path": path,
419 "files": files
420 }))
421 }
422 "write_file" => {
423 let path = args
424 .get("path")
425 .and_then(|v| v.as_str())
426 .ok_or_else(|| anyhow::anyhow!("Missing parameter 'path'"))?;
427 let content = args
428 .get("content")
429 .and_then(|v| v.as_str())
430 .ok_or_else(|| anyhow::anyhow!("Missing parameter 'content'"))?;
431
432 tokio::fs::write(path, content).await?;
433
434 Ok(json!({
435 "status": "success",
436 "path": path,
437 "bytes_written": content.len()
438 }))
439 }
440 _ => Err(anyhow::anyhow!("Unknown internal handler: {}", handler)),
441 }
442 }
443}