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.
- linux_mcp_server/__init__.py +4 -0
- linux_mcp_server/__main__.py +30 -0
- linux_mcp_server/audit.py +285 -0
- linux_mcp_server/logging_config.py +144 -0
- linux_mcp_server/server.py +328 -0
- linux_mcp_server/tools/__init__.py +2 -0
- linux_mcp_server/tools/logs.py +225 -0
- linux_mcp_server/tools/network.py +281 -0
- linux_mcp_server/tools/processes.py +257 -0
- linux_mcp_server/tools/services.py +147 -0
- linux_mcp_server/tools/ssh_executor.py +324 -0
- linux_mcp_server/tools/storage.py +358 -0
- linux_mcp_server/tools/system_info.py +507 -0
- linux_mcp_server/tools/utils.py +27 -0
- linux_mcp_server/tools/validation.py +58 -0
- linux_mcp_server-0.1.0.dev0.dist-info/METADATA +331 -0
- linux_mcp_server-0.1.0.dev0.dist-info/RECORD +20 -0
- linux_mcp_server-0.1.0.dev0.dist-info/WHEEL +4 -0
- linux_mcp_server-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- linux_mcp_server-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
|