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,328 @@
|
|
|
1
|
+
"""Core MCP server for Linux diagnostics using FastMCP."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from .audit import log_tool_call, log_tool_complete
|
|
10
|
+
from .tools import system_info, services, processes, logs, network, storage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Initialize FastMCP server
|
|
17
|
+
mcp = FastMCP("linux-diagnostics")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# System Information Tools
|
|
21
|
+
@mcp.tool()
|
|
22
|
+
async def get_system_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
23
|
+
"""Get basic system information including OS version, kernel, hostname, and uptime.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
27
|
+
username: SSH username for remote host (required if host is provided)
|
|
28
|
+
"""
|
|
29
|
+
return await _execute_tool("get_system_info", system_info.get_system_info,
|
|
30
|
+
host=host, username=username)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
async def get_cpu_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
35
|
+
"""Get CPU information and load averages.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
39
|
+
username: SSH username for remote host (required if host is provided)
|
|
40
|
+
"""
|
|
41
|
+
return await _execute_tool("get_cpu_info", system_info.get_cpu_info,
|
|
42
|
+
host=host, username=username)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
async def get_memory_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
47
|
+
"""Get memory usage including RAM and swap details.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
51
|
+
username: SSH username for remote host (required if host is provided)
|
|
52
|
+
"""
|
|
53
|
+
return await _execute_tool("get_memory_info", system_info.get_memory_info,
|
|
54
|
+
host=host, username=username)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@mcp.tool()
|
|
58
|
+
async def get_disk_usage(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
59
|
+
"""Get filesystem usage and mount points.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
63
|
+
username: SSH username for remote host (required if host is provided)
|
|
64
|
+
"""
|
|
65
|
+
return await _execute_tool("get_disk_usage", system_info.get_disk_usage,
|
|
66
|
+
host=host, username=username)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
async def get_hardware_info(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
71
|
+
"""Get hardware information including CPU architecture, PCI devices, USB devices, and memory hardware.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
75
|
+
username: SSH username for remote host (required if host is provided)
|
|
76
|
+
"""
|
|
77
|
+
return await _execute_tool("get_hardware_info", system_info.get_hardware_info,
|
|
78
|
+
host=host, username=username)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Service Management Tools
|
|
82
|
+
@mcp.tool()
|
|
83
|
+
async def list_services(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
84
|
+
"""List all systemd services with their current status.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
88
|
+
username: SSH username for remote host (required if host is provided)
|
|
89
|
+
"""
|
|
90
|
+
return await _execute_tool("list_services", services.list_services,
|
|
91
|
+
host=host, username=username)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@mcp.tool()
|
|
95
|
+
async def get_service_status(service_name: str, host: Optional[str] = None,
|
|
96
|
+
username: Optional[str] = None) -> str:
|
|
97
|
+
"""Get detailed status of a specific systemd service.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
service_name: Name of the service
|
|
101
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
102
|
+
username: SSH username for remote host (required if host is provided)
|
|
103
|
+
"""
|
|
104
|
+
return await _execute_tool("get_service_status", services.get_service_status,
|
|
105
|
+
service_name=service_name, host=host, username=username)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mcp.tool()
|
|
109
|
+
async def get_service_logs(service_name: str, lines: int = 50,
|
|
110
|
+
host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
111
|
+
"""Get recent logs for a specific systemd service.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
service_name: Name of the service
|
|
115
|
+
lines: Number of log lines to retrieve (default: 50)
|
|
116
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
117
|
+
username: SSH username for remote host (required if host is provided)
|
|
118
|
+
"""
|
|
119
|
+
return await _execute_tool("get_service_logs", services.get_service_logs,
|
|
120
|
+
service_name=service_name, lines=lines,
|
|
121
|
+
host=host, username=username)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Process Management Tools
|
|
125
|
+
@mcp.tool()
|
|
126
|
+
async def list_processes(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
127
|
+
"""List running processes with CPU and memory usage.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
131
|
+
username: SSH username for remote host (required if host is provided)
|
|
132
|
+
"""
|
|
133
|
+
return await _execute_tool("list_processes", processes.list_processes,
|
|
134
|
+
host=host, username=username)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
async def get_process_info(pid: int, host: Optional[str] = None,
|
|
139
|
+
username: Optional[str] = None) -> str:
|
|
140
|
+
"""Get detailed information about a specific process.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
pid: Process ID
|
|
144
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
145
|
+
username: SSH username for remote host (required if host is provided)
|
|
146
|
+
"""
|
|
147
|
+
return await _execute_tool("get_process_info", processes.get_process_info,
|
|
148
|
+
pid=pid, host=host, username=username)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Log and Audit Tools
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
async def get_journal_logs(unit: Optional[str] = None, priority: Optional[str] = None,
|
|
154
|
+
since: Optional[str] = None, lines: int = 100,
|
|
155
|
+
host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
156
|
+
"""Query systemd journal logs with optional filters.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
unit: Filter by systemd unit
|
|
160
|
+
priority: Filter by priority (emerg, alert, crit, err, warning, notice, info, debug)
|
|
161
|
+
since: Show entries since specified time (e.g., '1 hour ago', '2024-01-01')
|
|
162
|
+
lines: Number of log lines to retrieve (default: 100)
|
|
163
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
164
|
+
username: SSH username for remote host (required if host is provided)
|
|
165
|
+
"""
|
|
166
|
+
return await _execute_tool("get_journal_logs", logs.get_journal_logs,
|
|
167
|
+
unit=unit, priority=priority, since=since, lines=lines,
|
|
168
|
+
host=host, username=username)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@mcp.tool()
|
|
172
|
+
async def get_audit_logs(lines: int = 100, host: Optional[str] = None,
|
|
173
|
+
username: Optional[str] = None) -> str:
|
|
174
|
+
"""Get audit logs if available.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
lines: Number of log lines to retrieve (default: 100)
|
|
178
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
179
|
+
username: SSH username for remote host (required if host is provided)
|
|
180
|
+
"""
|
|
181
|
+
return await _execute_tool("get_audit_logs", logs.get_audit_logs,
|
|
182
|
+
lines=lines, host=host, username=username)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@mcp.tool()
|
|
186
|
+
async def read_log_file(log_path: str, lines: int = 100,
|
|
187
|
+
host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
188
|
+
"""Read a specific log file (whitelist-controlled via LINUX_MCP_ALLOWED_LOG_PATHS).
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
log_path: Path to the log file
|
|
192
|
+
lines: Number of lines to retrieve from the end (default: 100)
|
|
193
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
194
|
+
username: SSH username for remote host (required if host is provided)
|
|
195
|
+
"""
|
|
196
|
+
return await _execute_tool("read_log_file", logs.read_log_file,
|
|
197
|
+
log_path=log_path, lines=lines, host=host, username=username)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Network Tools
|
|
201
|
+
@mcp.tool()
|
|
202
|
+
async def get_network_interfaces(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
203
|
+
"""Get network interface information including IP addresses.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
207
|
+
username: SSH username for remote host (required if host is provided)
|
|
208
|
+
"""
|
|
209
|
+
return await _execute_tool("get_network_interfaces", network.get_network_interfaces,
|
|
210
|
+
host=host, username=username)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@mcp.tool()
|
|
214
|
+
async def get_network_connections(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
215
|
+
"""Get active network connections.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
219
|
+
username: SSH username for remote host (required if host is provided)
|
|
220
|
+
"""
|
|
221
|
+
return await _execute_tool("get_network_connections", network.get_network_connections,
|
|
222
|
+
host=host, username=username)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
async def get_listening_ports(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
227
|
+
"""Get ports that are listening on the system.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
231
|
+
username: SSH username for remote host (required if host is provided)
|
|
232
|
+
"""
|
|
233
|
+
return await _execute_tool("get_listening_ports", network.get_listening_ports,
|
|
234
|
+
host=host, username=username)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Storage Tools
|
|
238
|
+
@mcp.tool()
|
|
239
|
+
async def list_block_devices(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
240
|
+
"""List block devices and partitions.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
244
|
+
username: SSH username for remote host (required if host is provided)
|
|
245
|
+
"""
|
|
246
|
+
return await _execute_tool("list_block_devices", storage.list_block_devices,
|
|
247
|
+
host=host, username=username)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@mcp.tool()
|
|
251
|
+
async def list_directories_by_size(path: str, top_n: int,
|
|
252
|
+
host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
253
|
+
"""List directories sorted by size (largest first). Uses efficient Linux du command.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
path: Directory path to analyze
|
|
257
|
+
top_n: Number of top largest directories to return (1-1000)
|
|
258
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
259
|
+
username: SSH username for remote host (required if host is provided)
|
|
260
|
+
"""
|
|
261
|
+
return await _execute_tool("list_directories_by_size", storage.list_directories_by_size,
|
|
262
|
+
path=path, top_n=top_n, host=host, username=username)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@mcp.tool()
|
|
266
|
+
async def list_directories_by_name(path: str, reverse: bool = False,
|
|
267
|
+
host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
268
|
+
"""List directories sorted alphabetically by name. Uses efficient Linux find command.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
path: Directory path to analyze
|
|
272
|
+
reverse: Sort in reverse order (Z-A) (default: False)
|
|
273
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
274
|
+
username: SSH username for remote host (required if host is provided)
|
|
275
|
+
"""
|
|
276
|
+
return await _execute_tool("list_directories_by_name", storage.list_directories_by_name,
|
|
277
|
+
path=path, reverse=reverse, host=host, username=username)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@mcp.tool()
|
|
281
|
+
async def list_directories_by_modified_date(path: str, newest_first: bool = True,
|
|
282
|
+
host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
283
|
+
"""List directories sorted by modification date. Uses efficient Linux find command.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
path: Directory path to analyze
|
|
287
|
+
newest_first: Show newest first (default: True)
|
|
288
|
+
host: Remote host to connect to via SSH (optional, executes locally if not provided)
|
|
289
|
+
username: SSH username for remote host (required if host is provided)
|
|
290
|
+
"""
|
|
291
|
+
return await _execute_tool("list_directories_by_modified_date",
|
|
292
|
+
storage.list_directories_by_modified_date,
|
|
293
|
+
path=path, newest_first=newest_first, host=host, username=username)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
async def _execute_tool(tool_name: str, handler, **kwargs):
|
|
297
|
+
"""Execute a tool with logging and error handling.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
tool_name: Name of the tool being executed
|
|
301
|
+
handler: The tool function to call
|
|
302
|
+
**kwargs: Arguments to pass to the tool function
|
|
303
|
+
"""
|
|
304
|
+
# Log tool invocation
|
|
305
|
+
log_tool_call(tool_name, kwargs)
|
|
306
|
+
|
|
307
|
+
start_time = time.time()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
result = await handler(**kwargs)
|
|
311
|
+
duration = time.time() - start_time
|
|
312
|
+
log_tool_complete(tool_name, status="success", duration=duration)
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
duration = time.time() - start_time
|
|
317
|
+
log_tool_complete(tool_name, status="error", duration=duration, error=str(e))
|
|
318
|
+
raise
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main():
|
|
322
|
+
"""Run the MCP server using FastMCP."""
|
|
323
|
+
logger.info(f"Initialized linux-diagnostics v0.1.0")
|
|
324
|
+
logger.info("Starting FastMCP server")
|
|
325
|
+
|
|
326
|
+
# Run the FastMCP server (it creates its own event loop)
|
|
327
|
+
mcp.run()
|
|
328
|
+
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Log and audit tools."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .validation import validate_line_count
|
|
8
|
+
from .ssh_executor import execute_command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_journal_logs(
|
|
12
|
+
unit: str = None,
|
|
13
|
+
priority: str = None,
|
|
14
|
+
since: str = None,
|
|
15
|
+
lines: int = 100,
|
|
16
|
+
host: Optional[str] = None,
|
|
17
|
+
username: Optional[str] = None
|
|
18
|
+
) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Get systemd journal logs.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
unit: Filter by systemd unit
|
|
24
|
+
priority: Filter by priority level
|
|
25
|
+
since: Show entries since specified time
|
|
26
|
+
lines: Number of log lines to retrieve (default: 100)
|
|
27
|
+
host: Optional remote host to connect to
|
|
28
|
+
username: Optional SSH username (required if host is provided)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Formatted string with journal logs
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
# Validate lines parameter (accepts floats from LLMs)
|
|
35
|
+
lines, _ = validate_line_count(lines, default=100)
|
|
36
|
+
|
|
37
|
+
cmd = ["journalctl", "-n", str(lines), "--no-pager"]
|
|
38
|
+
|
|
39
|
+
if unit:
|
|
40
|
+
cmd.extend(["-u", unit])
|
|
41
|
+
|
|
42
|
+
if priority:
|
|
43
|
+
cmd.extend(["-p", priority])
|
|
44
|
+
|
|
45
|
+
if since:
|
|
46
|
+
cmd.extend(["--since", since])
|
|
47
|
+
|
|
48
|
+
returncode, stdout, stderr = await execute_command(cmd, host=host, username=username)
|
|
49
|
+
|
|
50
|
+
if returncode != 0:
|
|
51
|
+
return f"Error reading journal logs: {stderr}"
|
|
52
|
+
|
|
53
|
+
if not stdout or stdout.strip() == "":
|
|
54
|
+
return "No journal entries found matching the criteria."
|
|
55
|
+
|
|
56
|
+
# Build filter description
|
|
57
|
+
filters = []
|
|
58
|
+
if unit:
|
|
59
|
+
filters.append(f"unit={unit}")
|
|
60
|
+
if priority:
|
|
61
|
+
filters.append(f"priority={priority}")
|
|
62
|
+
if since:
|
|
63
|
+
filters.append(f"since={since}")
|
|
64
|
+
|
|
65
|
+
filter_desc = ", ".join(filters) if filters else "no filters"
|
|
66
|
+
|
|
67
|
+
result = [f"=== Journal Logs (last {lines} entries, {filter_desc}) ===\n"]
|
|
68
|
+
result.append(stdout)
|
|
69
|
+
|
|
70
|
+
return "\n".join(result)
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
return "Error: journalctl command not found. This tool requires systemd."
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return f"Error reading journal logs: {str(e)}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def get_audit_logs(
|
|
78
|
+
lines: int = 100,
|
|
79
|
+
host: Optional[str] = None,
|
|
80
|
+
username: Optional[str] = None
|
|
81
|
+
) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Get audit logs.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
lines: Number of log lines to retrieve (default: 100)
|
|
87
|
+
host: Optional remote host to connect to
|
|
88
|
+
username: Optional SSH username (required if host is provided)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Formatted string with audit logs
|
|
92
|
+
"""
|
|
93
|
+
# Validate lines parameter (accepts floats from LLMs)
|
|
94
|
+
lines, _ = validate_line_count(lines, default=100)
|
|
95
|
+
|
|
96
|
+
audit_log_path = "/var/log/audit/audit.log"
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# For local execution, check if file exists
|
|
100
|
+
if not host and not os.path.exists(audit_log_path):
|
|
101
|
+
return f"Audit log file not found at {audit_log_path}. Audit logging may not be enabled."
|
|
102
|
+
|
|
103
|
+
# Use tail to read last N lines
|
|
104
|
+
returncode, stdout, stderr = await execute_command(
|
|
105
|
+
["tail", "-n", str(lines), audit_log_path],
|
|
106
|
+
host=host,
|
|
107
|
+
username=username
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if returncode != 0:
|
|
111
|
+
if "Permission denied" in stderr:
|
|
112
|
+
return f"Permission denied reading audit logs. This tool requires elevated privileges (root) to read {audit_log_path}."
|
|
113
|
+
return f"Error reading audit logs: {stderr}"
|
|
114
|
+
|
|
115
|
+
if not stdout or stdout.strip() == "":
|
|
116
|
+
return "No audit log entries found."
|
|
117
|
+
|
|
118
|
+
result = [f"=== Audit Logs (last {lines} entries) ===\n"]
|
|
119
|
+
result.append(stdout)
|
|
120
|
+
|
|
121
|
+
return "\n".join(result)
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
return "Error: tail command not found."
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return f"Error reading audit logs: {str(e)}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def read_log_file(
|
|
129
|
+
log_path: str,
|
|
130
|
+
lines: int = 100,
|
|
131
|
+
host: Optional[str] = None,
|
|
132
|
+
username: Optional[str] = None
|
|
133
|
+
) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Read a specific log file.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
log_path: Path to the log file
|
|
139
|
+
lines: Number of lines to retrieve from the end (default: 100)
|
|
140
|
+
host: Optional remote host to connect to
|
|
141
|
+
username: Optional SSH username (required if host is provided)
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Formatted string with log file contents
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
# Validate lines parameter (accepts floats from LLMs)
|
|
148
|
+
lines, _ = validate_line_count(lines, default=100)
|
|
149
|
+
|
|
150
|
+
# Get allowed log paths from environment variable
|
|
151
|
+
allowed_paths_env = os.getenv("LINUX_MCP_ALLOWED_LOG_PATHS", "")
|
|
152
|
+
|
|
153
|
+
if not allowed_paths_env:
|
|
154
|
+
return (
|
|
155
|
+
"No log files are allowed. Set LINUX_MCP_ALLOWED_LOG_PATHS environment variable "
|
|
156
|
+
"with comma-separated list of allowed log file paths."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
allowed_paths = [p.strip() for p in allowed_paths_env.split(",") if p.strip()]
|
|
160
|
+
|
|
161
|
+
# For local execution, validate path
|
|
162
|
+
if not host:
|
|
163
|
+
try:
|
|
164
|
+
requested_path = Path(log_path).resolve()
|
|
165
|
+
except Exception:
|
|
166
|
+
return f"Invalid log file path: {log_path}"
|
|
167
|
+
|
|
168
|
+
# Check if the requested path is in the allowed list
|
|
169
|
+
is_allowed = False
|
|
170
|
+
for allowed_path in allowed_paths:
|
|
171
|
+
try:
|
|
172
|
+
allowed_resolved = Path(allowed_path).resolve()
|
|
173
|
+
if requested_path == allowed_resolved:
|
|
174
|
+
is_allowed = True
|
|
175
|
+
break
|
|
176
|
+
except Exception:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
if not is_allowed:
|
|
180
|
+
return (
|
|
181
|
+
f"Access to log file '{log_path}' is not allowed.\n"
|
|
182
|
+
f"Allowed log files: {', '.join(allowed_paths)}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Check if file exists
|
|
186
|
+
if not requested_path.exists():
|
|
187
|
+
return f"Log file not found: {log_path}"
|
|
188
|
+
|
|
189
|
+
if not requested_path.is_file():
|
|
190
|
+
return f"Path is not a file: {log_path}"
|
|
191
|
+
|
|
192
|
+
log_path_str = str(requested_path)
|
|
193
|
+
else:
|
|
194
|
+
# For remote execution, just check against whitelist without resolving
|
|
195
|
+
if log_path not in allowed_paths:
|
|
196
|
+
return (
|
|
197
|
+
f"Access to log file '{log_path}' is not allowed.\n"
|
|
198
|
+
f"Allowed log files: {', '.join(allowed_paths)}"
|
|
199
|
+
)
|
|
200
|
+
log_path_str = log_path
|
|
201
|
+
|
|
202
|
+
# Read the file using tail
|
|
203
|
+
returncode, stdout, stderr = await execute_command(
|
|
204
|
+
["tail", "-n", str(lines), log_path_str],
|
|
205
|
+
host=host,
|
|
206
|
+
username=username
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if returncode != 0:
|
|
210
|
+
if "Permission denied" in stderr:
|
|
211
|
+
return f"Permission denied reading log file: {log_path}"
|
|
212
|
+
return f"Error reading log file: {stderr}"
|
|
213
|
+
|
|
214
|
+
if not stdout or stdout.strip() == "":
|
|
215
|
+
return f"Log file is empty: {log_path}"
|
|
216
|
+
|
|
217
|
+
result = [f"=== Log File: {log_path} (last {lines} lines) ===\n"]
|
|
218
|
+
result.append(stdout)
|
|
219
|
+
|
|
220
|
+
return "\n".join(result)
|
|
221
|
+
except FileNotFoundError:
|
|
222
|
+
return "Error: tail command not found."
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return f"Error reading log file: {str(e)}"
|
|
225
|
+
|