mcp_tools/servers/
system_tools.rs

1//! System Tools MCP Server
2//!
3//! Provides system operations and shell command execution via MCP protocol including:
4//! - Shell command execution
5//! - Environment variable access
6//! - Process management
7//! - System information gathering
8//! - File system operations
9
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::process::{Command, Stdio};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15use tokio::process::Command as TokioCommand;
16use tracing::{debug, info, warn};
17
18use crate::common::{
19    BaseServer, McpContent, McpServerBase, McpTool, McpToolRequest, McpToolResponse,
20    ServerCapabilities, ServerConfig,
21};
22use crate::{McpToolsError, Result};
23
24/// System Tools MCP Server
25pub struct SystemToolsServer {
26    base: BaseServer,
27}
28
29/// Command execution result
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CommandResult {
32    pub command: String,
33    pub args: Vec<String>,
34    pub exit_code: i32,
35    pub stdout: String,
36    pub stderr: String,
37    pub execution_time: u64,
38    pub working_directory: String,
39}
40
41/// System information
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct SystemInfo {
44    pub os: String,
45    pub arch: String,
46    pub hostname: String,
47    pub username: String,
48    pub uptime: u64,
49    pub cpu_count: usize,
50    pub memory_total: u64,
51    pub memory_available: u64,
52    pub disk_usage: Vec<DiskInfo>,
53}
54
55/// Disk usage information
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DiskInfo {
58    pub mount_point: String,
59    pub total: u64,
60    pub available: u64,
61    pub used: u64,
62    pub filesystem: String,
63}
64
65/// Process information
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ProcessInfo {
68    pub pid: u32,
69    pub name: String,
70    pub cpu_usage: f32,
71    pub memory_usage: u64,
72    pub status: String,
73    pub start_time: u64,
74}
75
76/// Environment variable information
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct EnvVarInfo {
79    pub name: String,
80    pub value: String,
81    pub is_system: bool,
82}
83
84impl SystemToolsServer {
85    pub async fn new(config: ServerConfig) -> Result<Self> {
86        let base = BaseServer::new(config).await?;
87        Ok(Self { base })
88    }
89
90    /// Execute shell command
91    async fn execute_command(
92        &self,
93        command: &str,
94        args: &[String],
95        working_dir: Option<&str>,
96        timeout: Option<u64>,
97    ) -> Result<CommandResult> {
98        debug!("Executing command: {} {:?}", command, args);
99
100        let start_time = std::time::Instant::now();
101        let mut cmd = TokioCommand::new(command);
102
103        // Set arguments
104        cmd.args(args);
105
106        // Set working directory
107        if let Some(dir) = working_dir {
108            cmd.current_dir(dir);
109        }
110
111        // Configure stdio
112        cmd.stdout(Stdio::piped())
113            .stderr(Stdio::piped())
114            .stdin(Stdio::null());
115
116        // Set timeout
117        let timeout_duration = Duration::from_secs(timeout.unwrap_or(30));
118
119        // Execute command with timeout
120        let output = tokio::time::timeout(timeout_duration, cmd.output())
121            .await
122            .map_err(|_| McpToolsError::Server("Command execution timed out".to_string()))?
123            .map_err(|e| McpToolsError::Server(format!("Failed to execute command: {}", e)))?;
124
125        let execution_time = start_time.elapsed().as_millis() as u64;
126        let working_directory = working_dir.unwrap_or(".").to_string();
127
128        Ok(CommandResult {
129            command: command.to_string(),
130            args: args.to_vec(),
131            exit_code: output.status.code().unwrap_or(-1),
132            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
133            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
134            execution_time,
135            working_directory,
136        })
137    }
138
139    /// Get system information
140    async fn get_system_info(&self) -> Result<SystemInfo> {
141        debug!("Gathering system information");
142
143        // Get basic system info
144        let os = std::env::consts::OS.to_string();
145        let arch = std::env::consts::ARCH.to_string();
146
147        // Get hostname (simplified)
148        let hostname = std::env::var("COMPUTERNAME")
149            .or_else(|_| std::env::var("HOSTNAME"))
150            .unwrap_or_else(|_| "unknown".to_string());
151
152        // Get username
153        let username = std::env::var("USER")
154            .or_else(|_| std::env::var("USERNAME"))
155            .unwrap_or_else(|_| "unknown".to_string());
156
157        // Get uptime (simplified - would need platform-specific code)
158        let uptime = SystemTime::now()
159            .duration_since(UNIX_EPOCH)
160            .unwrap_or_default()
161            .as_secs();
162
163        // Get CPU count
164        let cpu_count = num_cpus::get();
165
166        // Memory info (simplified - would need platform-specific code)
167        let memory_total = 8 * 1024 * 1024 * 1024; // 8GB placeholder
168        let memory_available = 4 * 1024 * 1024 * 1024; // 4GB placeholder
169
170        // Disk usage (simplified)
171        let disk_usage = vec![DiskInfo {
172            mount_point: "/".to_string(),
173            total: 100 * 1024 * 1024 * 1024,    // 100GB placeholder
174            available: 50 * 1024 * 1024 * 1024, // 50GB placeholder
175            used: 50 * 1024 * 1024 * 1024,      // 50GB placeholder
176            filesystem: "ext4".to_string(),
177        }];
178
179        Ok(SystemInfo {
180            os,
181            arch,
182            hostname,
183            username,
184            uptime,
185            cpu_count,
186            memory_total,
187            memory_available,
188            disk_usage,
189        })
190    }
191
192    /// Get environment variables
193    async fn get_environment_variables(&self, filter: Option<&str>) -> Result<Vec<EnvVarInfo>> {
194        debug!("Getting environment variables");
195
196        let mut env_vars = Vec::new();
197
198        for (key, value) in std::env::vars() {
199            // Apply filter if provided
200            if let Some(filter_str) = filter {
201                if !key.to_lowercase().contains(&filter_str.to_lowercase()) {
202                    continue;
203                }
204            }
205
206            // Determine if it's a system variable (simplified heuristic)
207            let is_system = key.starts_with("SYSTEM")
208                || key.starts_with("OS")
209                || key.starts_with("PROCESSOR")
210                || key == "PATH"
211                || key == "HOME"
212                || key == "USER"
213                || key == "USERNAME";
214
215            env_vars.push(EnvVarInfo {
216                name: key,
217                value,
218                is_system,
219            });
220        }
221
222        // Sort by name
223        env_vars.sort_by(|a, b| a.name.cmp(&b.name));
224
225        Ok(env_vars)
226    }
227
228    /// Get running processes (simplified)
229    async fn get_processes(&self) -> Result<Vec<ProcessInfo>> {
230        debug!("Getting process information");
231
232        // This is a simplified implementation
233        // In a real implementation, we would use platform-specific APIs
234        let processes = vec![ProcessInfo {
235            pid: std::process::id(),
236            name: "mcp-tools".to_string(),
237            cpu_usage: 1.5,
238            memory_usage: 50 * 1024 * 1024, // 50MB
239            status: "Running".to_string(),
240            start_time: SystemTime::now()
241                .duration_since(UNIX_EPOCH)
242                .unwrap_or_default()
243                .as_secs(),
244        }];
245
246        Ok(processes)
247    }
248
249    /// Check if command is safe to execute
250    fn is_safe_command(&self, command: &str) -> bool {
251        // Basic safety check - in production, this would be more comprehensive
252        let dangerous_commands = [
253            "rm",
254            "del",
255            "format",
256            "fdisk",
257            "mkfs",
258            "dd",
259            "shutdown",
260            "reboot",
261            "halt",
262            "poweroff",
263            "sudo",
264            "su",
265            "passwd",
266            "chmod",
267            "chown",
268            "iptables",
269            "ufw",
270            "firewall-cmd",
271        ];
272
273        !dangerous_commands
274            .iter()
275            .any(|&dangerous| command.contains(dangerous))
276    }
277}
278
279#[async_trait]
280impl McpServerBase for SystemToolsServer {
281    async fn get_capabilities(&self) -> Result<ServerCapabilities> {
282        let mut capabilities = self.base.get_capabilities().await?;
283
284        // Add System Tools-specific tools
285        let system_tools = vec![
286            McpTool {
287                name: "execute_command".to_string(),
288                description: "Execute shell commands with safety checks and timeout".to_string(),
289                input_schema: serde_json::json!({
290                    "type": "object",
291                    "properties": {
292                        "command": {
293                            "type": "string",
294                            "description": "Command to execute"
295                        },
296                        "args": {
297                            "type": "array",
298                            "items": {"type": "string"},
299                            "description": "Command arguments"
300                        },
301                        "working_dir": {
302                            "type": "string",
303                            "description": "Working directory for command execution"
304                        },
305                        "timeout": {
306                            "type": "integer",
307                            "description": "Timeout in seconds (default: 30, max: 300)",
308                            "minimum": 1,
309                            "maximum": 300
310                        }
311                    },
312                    "required": ["command"]
313                }),
314                category: "system".to_string(),
315                requires_permission: true,
316                permissions: vec!["system.execute".to_string()],
317            },
318            McpTool {
319                name: "get_system_info".to_string(),
320                description: "Get comprehensive system information including OS, hardware, and resource usage".to_string(),
321                input_schema: serde_json::json!({
322                    "type": "object",
323                    "properties": {}
324                }),
325                category: "system".to_string(),
326                requires_permission: false,
327                permissions: vec![],
328            },
329            McpTool {
330                name: "get_environment".to_string(),
331                description: "Get environment variables with optional filtering".to_string(),
332                input_schema: serde_json::json!({
333                    "type": "object",
334                    "properties": {
335                        "filter": {
336                            "type": "string",
337                            "description": "Filter environment variables by name (case-insensitive substring match)"
338                        },
339                        "include_system": {
340                            "type": "boolean",
341                            "description": "Include system environment variables (default: true)"
342                        }
343                    }
344                }),
345                category: "system".to_string(),
346                requires_permission: false,
347                permissions: vec![],
348            },
349            McpTool {
350                name: "get_processes".to_string(),
351                description: "Get information about running processes".to_string(),
352                input_schema: serde_json::json!({
353                    "type": "object",
354                    "properties": {
355                        "filter": {
356                            "type": "string",
357                            "description": "Filter processes by name"
358                        }
359                    }
360                }),
361                category: "system".to_string(),
362                requires_permission: true,
363                permissions: vec!["system.processes".to_string()],
364            },
365        ];
366
367        capabilities.tools = system_tools;
368        Ok(capabilities)
369    }
370
371    async fn handle_tool_request(&self, request: McpToolRequest) -> Result<McpToolResponse> {
372        info!("Handling System Tools request: {}", request.tool);
373
374        match request.tool.as_str() {
375            "execute_command" => {
376                debug!("Executing shell command");
377
378                let command = request
379                    .arguments
380                    .get("command")
381                    .and_then(|v| v.as_str())
382                    .ok_or_else(|| {
383                        McpToolsError::Server("Missing 'command' parameter".to_string())
384                    })?;
385
386                // Safety check
387                if !self.is_safe_command(command) {
388                    return Ok(McpToolResponse {
389                        id: request.id,
390                        content: vec![McpContent::text(
391                            "Command rejected for security reasons".to_string(),
392                        )],
393                        is_error: true,
394                        error: Some("Unsafe command detected".to_string()),
395                        metadata: HashMap::new(),
396                    });
397                }
398
399                let args: Vec<String> = request
400                    .arguments
401                    .get("args")
402                    .and_then(|v| v.as_array())
403                    .map(|arr| {
404                        arr.iter()
405                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
406                            .collect()
407                    })
408                    .unwrap_or_else(|| Vec::new());
409
410                let working_dir = request
411                    .arguments
412                    .get("working_dir")
413                    .and_then(|v| v.as_str());
414
415                let timeout = request.arguments.get("timeout").and_then(|v| v.as_u64());
416
417                let result = self
418                    .execute_command(command, &args, working_dir, timeout)
419                    .await?;
420
421                let content_text = format!(
422                    "Command Execution Complete\n\
423                    Command: {} {:?}\n\
424                    Exit Code: {}\n\
425                    Execution Time: {}ms\n\
426                    Working Directory: {}\n\n\
427                    STDOUT:\n{}\n\n\
428                    STDERR:\n{}",
429                    result.command,
430                    result.args,
431                    result.exit_code,
432                    result.execution_time,
433                    result.working_directory,
434                    if result.stdout.is_empty() {
435                        "(empty)"
436                    } else {
437                        &result.stdout
438                    },
439                    if result.stderr.is_empty() {
440                        "(empty)"
441                    } else {
442                        &result.stderr
443                    }
444                );
445
446                let mut metadata = HashMap::new();
447                metadata.insert("command_result".to_string(), serde_json::to_value(result)?);
448
449                Ok(McpToolResponse {
450                    id: request.id,
451                    content: vec![McpContent::text(content_text)],
452                    is_error: false,
453                    error: None,
454                    metadata,
455                })
456            }
457            "get_system_info" => {
458                debug!("Getting system information");
459
460                let system_info = self.get_system_info().await?;
461
462                let content_text = format!(
463                    "System Information\n\
464                    OS: {}\n\
465                    Architecture: {}\n\
466                    Hostname: {}\n\
467                    Username: {}\n\
468                    CPU Cores: {}\n\
469                    Memory Total: {} GB\n\
470                    Memory Available: {} GB\n\
471                    Uptime: {} seconds",
472                    system_info.os,
473                    system_info.arch,
474                    system_info.hostname,
475                    system_info.username,
476                    system_info.cpu_count,
477                    system_info.memory_total / (1024 * 1024 * 1024),
478                    system_info.memory_available / (1024 * 1024 * 1024),
479                    system_info.uptime
480                );
481
482                let mut metadata = HashMap::new();
483                metadata.insert(
484                    "system_info".to_string(),
485                    serde_json::to_value(system_info)?,
486                );
487
488                Ok(McpToolResponse {
489                    id: request.id,
490                    content: vec![McpContent::text(content_text)],
491                    is_error: false,
492                    error: None,
493                    metadata,
494                })
495            }
496            "get_environment" => {
497                debug!("Getting environment variables");
498
499                let filter = request.arguments.get("filter").and_then(|v| v.as_str());
500
501                let env_vars = self.get_environment_variables(filter).await?;
502
503                let content_text = format!(
504                    "Environment Variables\n\
505                    Total Variables: {}\n\
506                    Filter Applied: {}\n\n{}",
507                    env_vars.len(),
508                    filter.unwrap_or("None"),
509                    env_vars
510                        .iter()
511                        .take(20) // Limit to first 20 for display
512                        .map(|var| format!(
513                            "{}={}",
514                            var.name,
515                            if var.value.len() > 50 {
516                                format!("{}...", &var.value[..50])
517                            } else {
518                                var.value.clone()
519                            }
520                        ))
521                        .collect::<Vec<_>>()
522                        .join("\n")
523                );
524
525                let mut metadata = HashMap::new();
526                metadata.insert(
527                    "environment_variables".to_string(),
528                    serde_json::to_value(env_vars)?,
529                );
530
531                Ok(McpToolResponse {
532                    id: request.id,
533                    content: vec![McpContent::text(content_text)],
534                    is_error: false,
535                    error: None,
536                    metadata,
537                })
538            }
539            "get_processes" => {
540                debug!("Getting process information");
541
542                let processes = self.get_processes().await?;
543
544                let content_text = format!(
545                    "Running Processes\n\
546                    Total Processes: {}\n\n{}",
547                    processes.len(),
548                    processes
549                        .iter()
550                        .map(|proc| format!(
551                            "PID: {} | Name: {} | CPU: {:.1}% | Memory: {} MB | Status: {}",
552                            proc.pid,
553                            proc.name,
554                            proc.cpu_usage,
555                            proc.memory_usage / (1024 * 1024),
556                            proc.status
557                        ))
558                        .collect::<Vec<_>>()
559                        .join("\n")
560                );
561
562                let mut metadata = HashMap::new();
563                metadata.insert("processes".to_string(), serde_json::to_value(processes)?);
564
565                Ok(McpToolResponse {
566                    id: request.id,
567                    content: vec![McpContent::text(content_text)],
568                    is_error: false,
569                    error: None,
570                    metadata,
571                })
572            }
573            _ => {
574                warn!("Unknown System Tools request: {}", request.tool);
575                Err(McpToolsError::Server(format!(
576                    "Unknown System Tools request: {}",
577                    request.tool
578                )))
579            }
580        }
581    }
582
583    async fn get_stats(&self) -> Result<crate::common::ServerStats> {
584        self.base.get_stats().await
585    }
586
587    async fn initialize(&mut self) -> Result<()> {
588        info!("Initializing System Tools MCP Server");
589        Ok(())
590    }
591
592    async fn shutdown(&mut self) -> Result<()> {
593        info!("Shutting down System Tools MCP Server");
594        Ok(())
595    }
596}