1use 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
24pub struct SystemToolsServer {
26 base: BaseServer,
27}
28
29#[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#[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#[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#[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#[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 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 cmd.args(args);
105
106 if let Some(dir) = working_dir {
108 cmd.current_dir(dir);
109 }
110
111 cmd.stdout(Stdio::piped())
113 .stderr(Stdio::piped())
114 .stdin(Stdio::null());
115
116 let timeout_duration = Duration::from_secs(timeout.unwrap_or(30));
118
119 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 async fn get_system_info(&self) -> Result<SystemInfo> {
141 debug!("Gathering system information");
142
143 let os = std::env::consts::OS.to_string();
145 let arch = std::env::consts::ARCH.to_string();
146
147 let hostname = std::env::var("COMPUTERNAME")
149 .or_else(|_| std::env::var("HOSTNAME"))
150 .unwrap_or_else(|_| "unknown".to_string());
151
152 let username = std::env::var("USER")
154 .or_else(|_| std::env::var("USERNAME"))
155 .unwrap_or_else(|_| "unknown".to_string());
156
157 let uptime = SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .unwrap_or_default()
161 .as_secs();
162
163 let cpu_count = num_cpus::get();
165
166 let memory_total = 8 * 1024 * 1024 * 1024; let memory_available = 4 * 1024 * 1024 * 1024; let disk_usage = vec![DiskInfo {
172 mount_point: "/".to_string(),
173 total: 100 * 1024 * 1024 * 1024, available: 50 * 1024 * 1024 * 1024, used: 50 * 1024 * 1024 * 1024, 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 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 if let Some(filter_str) = filter {
201 if !key.to_lowercase().contains(&filter_str.to_lowercase()) {
202 continue;
203 }
204 }
205
206 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 env_vars.sort_by(|a, b| a.name.cmp(&b.name));
224
225 Ok(env_vars)
226 }
227
228 async fn get_processes(&self) -> Result<Vec<ProcessInfo>> {
230 debug!("Getting process information");
231
232 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, 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 fn is_safe_command(&self, command: &str) -> bool {
251 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 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 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) .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}