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.
- linux_mcp_server/__init__.py +4 -0
- linux_mcp_server/__main__.py +30 -0
- linux_mcp_server/audit.py +331 -0
- linux_mcp_server/connection/__init__.py +0 -0
- linux_mcp_server/connection/ssh.py +338 -0
- linux_mcp_server/logging_config.py +176 -0
- linux_mcp_server/server.py +18 -0
- linux_mcp_server/tools/__init__.py +51 -0
- linux_mcp_server/tools/logs.py +221 -0
- linux_mcp_server/tools/network.py +288 -0
- linux_mcp_server/tools/processes.py +273 -0
- linux_mcp_server/tools/services.py +149 -0
- linux_mcp_server/tools/storage.py +194 -0
- linux_mcp_server/tools/system_info.py +524 -0
- linux_mcp_server/utils/__init__.py +2 -0
- linux_mcp_server/utils/enum.py +16 -0
- linux_mcp_server/utils/format.py +26 -0
- linux_mcp_server/utils/types.py +5 -0
- linux_mcp_server/utils/validation.py +57 -0
- linux_mcp_server-0.1.0a2.dist-info/METADATA +233 -0
- linux_mcp_server-0.1.0a2.dist-info/RECORD +24 -0
- linux_mcp_server-0.1.0a2.dist-info/WHEEL +4 -0
- linux_mcp_server-0.1.0a2.dist-info/entry_points.txt +2 -0
- linux_mcp_server-0.1.0a2.dist-info/licenses/LICENSE +201 -0
|
@@ -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,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,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)
|