linux-mcp-server 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,507 @@
1
+ """System information tools."""
2
+
3
+ import asyncio
4
+ import os
5
+ import platform
6
+ import subprocess
7
+ from datetime import datetime, timedelta
8
+ from typing import Optional
9
+ import psutil
10
+
11
+ from .ssh_executor import execute_command
12
+ from .utils import format_bytes
13
+
14
+
15
+ async def get_system_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
16
+ """
17
+ Get basic system information.
18
+
19
+ Args:
20
+ host: Optional remote host to connect to
21
+ username: Optional SSH username (required if host is provided)
22
+
23
+ Returns:
24
+ Formatted string with basic system information
25
+ """
26
+ info = []
27
+
28
+ try:
29
+ if host:
30
+ # Remote execution - use Linux commands
31
+ # Hostname
32
+ returncode, stdout, _ = await execute_command(
33
+ ["hostname"],
34
+ host=host,
35
+ username=username
36
+ )
37
+ if returncode == 0 and stdout:
38
+ info.append(f"Hostname: {stdout.strip()}")
39
+
40
+ # OS Information from /etc/os-release
41
+ returncode, stdout, _ = await execute_command(
42
+ ["cat", "/etc/os-release"],
43
+ host=host,
44
+ username=username
45
+ )
46
+ if returncode == 0 and stdout:
47
+ os_info = {}
48
+ for line in stdout.split('\n'):
49
+ line = line.strip()
50
+ if "=" in line:
51
+ key, value = line.split("=", 1)
52
+ os_info[key] = value.strip('"')
53
+
54
+ info.append(f"Operating System: {os_info.get('PRETTY_NAME', 'Unknown')}")
55
+ if 'VERSION_ID' in os_info:
56
+ info.append(f"OS Version: {os_info['VERSION_ID']}")
57
+
58
+ # Kernel version
59
+ returncode, stdout, _ = await execute_command(
60
+ ["uname", "-r"],
61
+ host=host,
62
+ username=username
63
+ )
64
+ if returncode == 0 and stdout:
65
+ info.append(f"Kernel Version: {stdout.strip()}")
66
+
67
+ # Architecture
68
+ returncode, stdout, _ = await execute_command(
69
+ ["uname", "-m"],
70
+ host=host,
71
+ username=username
72
+ )
73
+ if returncode == 0 and stdout:
74
+ info.append(f"Architecture: {stdout.strip()}")
75
+
76
+ # Uptime
77
+ returncode, stdout, _ = await execute_command(
78
+ ["uptime", "-p"],
79
+ host=host,
80
+ username=username
81
+ )
82
+ if returncode == 0 and stdout:
83
+ info.append(f"Uptime: {stdout.strip()}")
84
+
85
+ # Boot time
86
+ returncode, stdout, _ = await execute_command(
87
+ ["uptime", "-s"],
88
+ host=host,
89
+ username=username
90
+ )
91
+ if returncode == 0 and stdout:
92
+ info.append(f"Boot Time: {stdout.strip()}")
93
+ else:
94
+ # Local execution - use psutil and platform
95
+ # Hostname
96
+ hostname = platform.node()
97
+ info.append(f"Hostname: {hostname}")
98
+
99
+ # OS Information
100
+ if os.path.exists("/etc/os-release"):
101
+ os_info = {}
102
+ with open("/etc/os-release", "r") as f:
103
+ for line in f:
104
+ line = line.strip()
105
+ if "=" in line:
106
+ key, value = line.split("=", 1)
107
+ os_info[key] = value.strip('"')
108
+
109
+ info.append(f"Operating System: {os_info.get('PRETTY_NAME', 'Unknown')}")
110
+ if 'VERSION_ID' in os_info:
111
+ info.append(f"OS Version: {os_info['VERSION_ID']}")
112
+ else:
113
+ info.append(f"Operating System: {platform.system()} {platform.release()}")
114
+
115
+ # Kernel version
116
+ kernel = platform.release()
117
+ info.append(f"Kernel Version: {kernel}")
118
+
119
+ # Architecture
120
+ arch = platform.machine()
121
+ info.append(f"Architecture: {arch}")
122
+
123
+ # Uptime
124
+ boot_time = datetime.fromtimestamp(psutil.boot_time())
125
+ uptime = datetime.now() - boot_time
126
+ days = uptime.days
127
+ hours, remainder = divmod(uptime.seconds, 3600)
128
+ minutes, seconds = divmod(remainder, 60)
129
+ info.append(f"Uptime: {days}d {hours}h {minutes}m {seconds}s")
130
+ info.append(f"Boot Time: {boot_time.strftime('%Y-%m-%d %H:%M:%S')}")
131
+
132
+ return "\n".join(info)
133
+ except Exception as e:
134
+ return f"Error gathering system information: {str(e)}"
135
+
136
+
137
+ async def get_cpu_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
138
+ """
139
+ Get CPU information.
140
+
141
+ Args:
142
+ host: Optional remote host to connect to
143
+ username: Optional SSH username (required if host is provided)
144
+
145
+ Returns:
146
+ Formatted string with CPU information
147
+ """
148
+ info = []
149
+
150
+ try:
151
+ if host:
152
+ # Remote execution - use Linux commands
153
+ # Get CPU model from /proc/cpuinfo
154
+ returncode, stdout, _ = await execute_command(
155
+ ["grep", "-m", "1", "model name", "/proc/cpuinfo"],
156
+ host=host,
157
+ username=username
158
+ )
159
+ if returncode == 0 and stdout:
160
+ cpu_model = stdout.split(":", 1)[1].strip() if ":" in stdout else stdout.strip()
161
+ info.append(f"CPU Model: {cpu_model}")
162
+
163
+ # Get CPU core counts from /proc/cpuinfo
164
+ returncode, stdout, _ = await execute_command(
165
+ ["grep", "-c", "^processor", "/proc/cpuinfo"],
166
+ host=host,
167
+ username=username
168
+ )
169
+ if returncode == 0 and stdout:
170
+ logical_cores = int(stdout.strip())
171
+ info.append(f"CPU Logical Cores (threads): {logical_cores}")
172
+
173
+ # Get physical cores (using core id uniqueness)
174
+ returncode, stdout, _ = await execute_command(
175
+ ["grep", "^core id", "/proc/cpuinfo"],
176
+ host=host,
177
+ username=username
178
+ )
179
+ if returncode == 0 and stdout:
180
+ core_ids = set(line.split(":", 1)[1].strip() for line in stdout.strip().split('\n') if ":" in line)
181
+ physical_cores = len(core_ids)
182
+ info.append(f"CPU Physical Cores: {physical_cores}")
183
+
184
+ # Get CPU frequency from /proc/cpuinfo
185
+ returncode, stdout, _ = await execute_command(
186
+ ["grep", "-m", "1", "cpu MHz", "/proc/cpuinfo"],
187
+ host=host,
188
+ username=username
189
+ )
190
+ if returncode == 0 and stdout and ":" in stdout:
191
+ cpu_mhz = stdout.split(":", 1)[1].strip()
192
+ info.append(f"CPU Frequency: Current={cpu_mhz}MHz")
193
+
194
+ # Get load average
195
+ returncode, stdout, _ = await execute_command(
196
+ ["cat", "/proc/loadavg"],
197
+ host=host,
198
+ username=username
199
+ )
200
+ if returncode == 0 and stdout:
201
+ load_parts = stdout.strip().split()
202
+ if len(load_parts) >= 3:
203
+ info.append(f"\nLoad Average (1m, 5m, 15m): {load_parts[0]}, {load_parts[1]}, {load_parts[2]}")
204
+
205
+ # Get CPU usage using top (one iteration)
206
+ returncode, stdout, _ = await execute_command(
207
+ ["top", "-bn1"],
208
+ host=host,
209
+ username=username
210
+ )
211
+ if returncode == 0 and stdout:
212
+ for line in stdout.split('\n'):
213
+ if 'Cpu(s):' in line or '%Cpu' in line:
214
+ info.append(f"\n{line.strip()}")
215
+ break
216
+ else:
217
+ # Local execution - use psutil
218
+ # CPU count
219
+ physical_cores = psutil.cpu_count(logical=False)
220
+ logical_cores = psutil.cpu_count(logical=True)
221
+ info.append(f"CPU Physical Cores: {physical_cores}")
222
+ info.append(f"CPU Logical Cores (threads): {logical_cores}")
223
+
224
+ # CPU frequency
225
+ try:
226
+ cpu_freq = psutil.cpu_freq()
227
+ if cpu_freq:
228
+ info.append(f"CPU Frequency: Current={cpu_freq.current:.2f}MHz, Min={cpu_freq.min:.2f}MHz, Max={cpu_freq.max:.2f}MHz")
229
+ except Exception:
230
+ pass # CPU frequency might not be available
231
+
232
+ # CPU usage per core
233
+ cpu_percent = psutil.cpu_percent(interval=1, percpu=True)
234
+ info.append(f"\nCPU Usage per Core:")
235
+ for i, percent in enumerate(cpu_percent):
236
+ info.append(f" Core {i}: {percent}%")
237
+
238
+ # Overall CPU usage
239
+ overall_cpu = psutil.cpu_percent(interval=1)
240
+ info.append(f"\nOverall CPU Usage: {overall_cpu}%")
241
+
242
+ # Load average
243
+ load_avg = os.getloadavg()
244
+ info.append(f"\nLoad Average (1m, 5m, 15m): {load_avg[0]:.2f}, {load_avg[1]:.2f}, {load_avg[2]:.2f}")
245
+
246
+ # Try to get CPU model info from /proc/cpuinfo
247
+ try:
248
+ with open("/proc/cpuinfo", "r") as f:
249
+ for line in f:
250
+ if line.startswith("model name"):
251
+ cpu_model = line.split(":")[1].strip()
252
+ info.insert(0, f"CPU Model: {cpu_model}")
253
+ break
254
+ except Exception:
255
+ pass
256
+
257
+ return "\n".join(info)
258
+ except Exception as e:
259
+ return f"Error gathering CPU information: {str(e)}"
260
+
261
+
262
+ async def get_memory_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
263
+ """
264
+ Get memory information.
265
+
266
+ Args:
267
+ host: Optional remote host to connect to
268
+ username: Optional SSH username (required if host is provided)
269
+
270
+ Returns:
271
+ Formatted string with memory information
272
+ """
273
+ info = []
274
+
275
+ try:
276
+ if host:
277
+ # Remote execution - use free command
278
+ returncode, stdout, _ = await execute_command(
279
+ ["free", "-b"],
280
+ host=host,
281
+ username=username
282
+ )
283
+
284
+ if returncode == 0 and stdout:
285
+ lines = stdout.strip().split('\n')
286
+
287
+ # Parse memory line
288
+ for line in lines:
289
+ if line.startswith('Mem:'):
290
+ parts = line.split()
291
+ if len(parts) >= 7:
292
+ total = int(parts[1])
293
+ used = int(parts[2])
294
+ free = int(parts[3])
295
+ available = int(parts[6]) if len(parts) > 6 else free
296
+ percent = (used / total * 100) if total > 0 else 0
297
+
298
+ info.append("=== RAM Information ===")
299
+ info.append(f"Total: {format_bytes(total)}")
300
+ info.append(f"Available: {format_bytes(available)}")
301
+ info.append(f"Used: {format_bytes(used)} ({percent:.1f}%)")
302
+ info.append(f"Free: {format_bytes(free)}")
303
+
304
+ elif line.startswith('Swap:'):
305
+ parts = line.split()
306
+ if len(parts) >= 4:
307
+ total = int(parts[1])
308
+ used = int(parts[2])
309
+ free = int(parts[3])
310
+ percent = (used / total * 100) if total > 0 else 0
311
+
312
+ info.append("\n=== Swap Information ===")
313
+ info.append(f"Total: {format_bytes(total)}")
314
+ info.append(f"Used: {format_bytes(used)} ({percent:.1f}%)")
315
+ info.append(f"Free: {format_bytes(free)}")
316
+ else:
317
+ # Local execution - use psutil
318
+ # Virtual memory (RAM)
319
+ mem = psutil.virtual_memory()
320
+ info.append("=== RAM Information ===")
321
+ info.append(f"Total: {format_bytes(mem.total)}")
322
+ info.append(f"Available: {format_bytes(mem.available)}")
323
+ info.append(f"Used: {format_bytes(mem.used)} ({mem.percent}%)")
324
+ info.append(f"Free: {format_bytes(mem.free)}")
325
+
326
+ if hasattr(mem, 'buffers'):
327
+ info.append(f"Buffers: {format_bytes(mem.buffers)}")
328
+ if hasattr(mem, 'cached'):
329
+ info.append(f"Cached: {format_bytes(mem.cached)}")
330
+
331
+ # Swap memory
332
+ swap = psutil.swap_memory()
333
+ info.append("\n=== Swap Information ===")
334
+ info.append(f"Total: {format_bytes(swap.total)}")
335
+ info.append(f"Used: {format_bytes(swap.used)} ({swap.percent}%)")
336
+ info.append(f"Free: {format_bytes(swap.free)}")
337
+
338
+ return "\n".join(info)
339
+ except Exception as e:
340
+ return f"Error gathering memory information: {str(e)}"
341
+
342
+
343
+ async def get_disk_usage(host: Optional[str] = None, username: Optional[str] = None) -> str:
344
+ """
345
+ Get disk usage information.
346
+
347
+ Args:
348
+ host: Optional remote host to connect to
349
+ username: Optional SSH username (required if host is provided)
350
+
351
+ Returns:
352
+ Formatted string with disk usage information
353
+ """
354
+ info = []
355
+
356
+ try:
357
+ if host:
358
+ # Remote execution - use df command
359
+ returncode, stdout, _ = await execute_command(
360
+ ["df", "-h", "--output=source,size,used,avail,pcent,target"],
361
+ host=host,
362
+ username=username
363
+ )
364
+
365
+ if returncode == 0 and stdout:
366
+ info.append("=== Filesystem Usage ===\n")
367
+ info.append(stdout)
368
+ else:
369
+ # Fallback to basic df command
370
+ returncode, stdout, _ = await execute_command(
371
+ ["df", "-h"],
372
+ host=host,
373
+ username=username
374
+ )
375
+ if returncode == 0 and stdout:
376
+ info.append("=== Filesystem Usage ===\n")
377
+ info.append(stdout)
378
+ else:
379
+ # Local execution - use psutil
380
+ info.append("=== Filesystem Usage ===\n")
381
+ info.append(f"{'Filesystem':<30} {'Size':<10} {'Used':<10} {'Avail':<10} {'Use%':<6} {'Mounted on'}")
382
+ info.append("-" * 90)
383
+
384
+ # Get all disk partitions
385
+ partitions = psutil.disk_partitions(all=False)
386
+
387
+ for partition in partitions:
388
+ try:
389
+ usage = psutil.disk_usage(partition.mountpoint)
390
+ info.append(
391
+ f"{partition.device:<30} "
392
+ f"{format_bytes(usage.total):<10} "
393
+ f"{format_bytes(usage.used):<10} "
394
+ f"{format_bytes(usage.free):<10} "
395
+ f"{usage.percent:<6.1f} "
396
+ f"{partition.mountpoint}"
397
+ )
398
+ except PermissionError:
399
+ # Skip partitions we can't access
400
+ continue
401
+ except Exception as e:
402
+ info.append(f"{partition.device:<30} Error: {str(e)}")
403
+
404
+ # Disk I/O statistics
405
+ try:
406
+ disk_io = psutil.disk_io_counters()
407
+ if disk_io:
408
+ info.append("\n=== Disk I/O Statistics (since boot) ===")
409
+ info.append(f"Read: {format_bytes(disk_io.read_bytes)}")
410
+ info.append(f"Write: {format_bytes(disk_io.write_bytes)}")
411
+ info.append(f"Read Count: {disk_io.read_count}")
412
+ info.append(f"Write Count: {disk_io.write_count}")
413
+ except Exception:
414
+ pass # Disk I/O might not be available
415
+
416
+ return "\n".join(info)
417
+ except Exception as e:
418
+ return f"Error gathering disk usage information: {str(e)}"
419
+
420
+
421
+ async def get_hardware_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
422
+ """
423
+ Get hardware information.
424
+
425
+ Args:
426
+ host: Optional remote host to connect to
427
+ username: Optional SSH username (required if host is provided)
428
+
429
+ Returns:
430
+ Formatted string with hardware information
431
+ """
432
+ try:
433
+ info = []
434
+ info.append("=== Hardware Information ===\n")
435
+
436
+ # Try lscpu for CPU info
437
+ try:
438
+ returncode, stdout, stderr = await execute_command(
439
+ ["lscpu"],
440
+ host=host,
441
+ username=username
442
+ )
443
+
444
+ if returncode == 0:
445
+ info.append("=== CPU Architecture (lscpu) ===")
446
+ info.append(stdout)
447
+ except FileNotFoundError:
448
+ info.append("CPU info: lscpu command not available")
449
+
450
+ # Try lspci for PCI devices
451
+ try:
452
+ returncode, stdout, stderr = await execute_command(
453
+ ["lspci"],
454
+ host=host,
455
+ username=username
456
+ )
457
+
458
+ if returncode == 0:
459
+ pci_lines = stdout.strip().split('\n')
460
+
461
+ info.append("\n=== PCI Devices ===")
462
+ # Show first 50 devices to avoid overwhelming output
463
+ for line in pci_lines[:50]:
464
+ info.append(line)
465
+
466
+ if len(pci_lines) > 50:
467
+ info.append(f"\n... and {len(pci_lines) - 50} more PCI devices")
468
+ except FileNotFoundError:
469
+ info.append("\nPCI devices: lspci command not available")
470
+
471
+ # Try lsusb for USB devices
472
+ try:
473
+ returncode, stdout, stderr = await execute_command(
474
+ ["lsusb"],
475
+ host=host,
476
+ username=username
477
+ )
478
+
479
+ if returncode == 0:
480
+ info.append("\n\n=== USB Devices ===")
481
+ info.append(stdout)
482
+ except FileNotFoundError:
483
+ info.append("\nUSB devices: lsusb command not available")
484
+
485
+ # Memory hardware info from dmidecode (requires root)
486
+ try:
487
+ returncode, stdout, stderr = await execute_command(
488
+ ["dmidecode", "-t", "memory"],
489
+ host=host,
490
+ username=username
491
+ )
492
+
493
+ if returncode == 0:
494
+ info.append("\n\n=== Memory Hardware (dmidecode) ===")
495
+ info.append(stdout)
496
+ elif "Permission denied" in stderr:
497
+ info.append("\n\nMemory hardware info: Requires root privileges (dmidecode)")
498
+ except FileNotFoundError:
499
+ info.append("\nMemory hardware info: dmidecode command not available")
500
+
501
+ if len(info) == 1: # Only the header
502
+ info.append("No hardware information tools available.")
503
+
504
+ return "\n".join(info)
505
+ except Exception as e:
506
+ return f"Error getting hardware information: {str(e)}"
507
+
@@ -0,0 +1,27 @@
1
+ """Common utility functions for Linux MCP tools."""
2
+
3
+
4
+ def format_bytes(bytes_value: int) -> str:
5
+ """
6
+ Format bytes into human-readable format.
7
+
8
+ Args:
9
+ bytes_value: Number of bytes to format
10
+
11
+ Returns:
12
+ Human-readable string representation (e.g., "1.5GB", "256.0MB")
13
+
14
+ Examples:
15
+ >>> format_bytes(1024)
16
+ '1.0KB'
17
+ >>> format_bytes(1536)
18
+ '1.5KB'
19
+ >>> format_bytes(1073741824)
20
+ '1.0GB'
21
+ """
22
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
23
+ if bytes_value < 1024.0:
24
+ return f"{bytes_value:.1f}{unit}"
25
+ bytes_value /= 1024.0
26
+ return f"{bytes_value:.1f}PB"
27
+
@@ -0,0 +1,58 @@
1
+ """Input validation utilities for MCP tools.
2
+
3
+ Provides validation functions for handling numeric parameters where LLMs often
4
+ pass floats instead of integers.
5
+ """
6
+
7
+ from typing import Union, Optional, Tuple
8
+
9
+
10
+ def validate_positive_int(
11
+ value: Union[int, float],
12
+ param_name: str = "parameter",
13
+ min_value: int = 1,
14
+ max_value: Optional[int] = None
15
+ ) -> Tuple[Optional[int], Optional[str]]:
16
+ """
17
+ Validate and normalize a numeric value to a positive integer.
18
+
19
+ Accepts both int and float (LLMs often pass floats) and truncates to int.
20
+ Validates bounds and caps at max_value if specified.
21
+
22
+ Returns:
23
+ (validated_int, error_message) tuple. On success: (int_value, None).
24
+ On failure: (None, error_msg).
25
+ """
26
+ if not isinstance(value, (int, float)):
27
+ return None, f"Error: {param_name} must be a number"
28
+
29
+ int_value = int(value)
30
+
31
+ if int_value < min_value:
32
+ return None, f"Error: {param_name} must be at least {min_value}"
33
+
34
+ if max_value is not None and int_value > max_value:
35
+ int_value = max_value
36
+
37
+ return int_value, None
38
+
39
+
40
+ def validate_pid(pid: Union[int, float]) -> Tuple[Optional[int], Optional[str]]:
41
+ """Validate a process ID (PID). Accepts floats from LLMs and truncates to int."""
42
+ return validate_positive_int(pid, param_name="PID", min_value=1)
43
+
44
+
45
+ def validate_line_count(
46
+ lines: Union[int, float],
47
+ default: int = 100,
48
+ max_lines: int = 10000
49
+ ) -> Tuple[int, Optional[str]]:
50
+ """
51
+ Validate line count for log reading functions.
52
+
53
+ Accepts floats from LLMs, truncates to int, caps at max_lines.
54
+ Returns default value if validation fails.
55
+ """
56
+ validated, error = validate_positive_int(lines, "lines", 1, max_lines)
57
+ return (default, error) if error else (validated, None)
58
+