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