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,288 @@
1
+ """Network diagnostic tools."""
2
+
3
+ import socket
4
+
5
+ import psutil
6
+
7
+ from mcp.types import ToolAnnotations
8
+
9
+ from linux_mcp_server.audit import log_tool_call
10
+ from linux_mcp_server.connection.ssh import execute_command
11
+ from linux_mcp_server.server import mcp
12
+ from linux_mcp_server.utils import format_bytes
13
+ from linux_mcp_server.utils.types import Host
14
+ from linux_mcp_server.utils.types import Username
15
+
16
+
17
+ @mcp.tool(
18
+ title="Get network interfaces",
19
+ description="Get detailed information about network interfaces including address and traffic statistics.",
20
+ annotations=ToolAnnotations(readOnlyHint=True),
21
+ )
22
+ @log_tool_call
23
+ async def get_network_interfaces( # noqa: C901
24
+ host: Host | None = None,
25
+ username: Username | None = None,
26
+ ) -> str:
27
+ """
28
+ Get network interface information.
29
+ """
30
+ try:
31
+ if host:
32
+ # Remote execution - use ip command
33
+ info = []
34
+ info.append("=== Network Interfaces ===\n")
35
+
36
+ # Get interface info
37
+ returncode, stdout, _ = await execute_command(
38
+ ["ip", "-brief", "address"],
39
+ host=host,
40
+ username=username,
41
+ )
42
+
43
+ if returncode == 0 and stdout:
44
+ info.append(stdout)
45
+
46
+ # Get detailed interface info
47
+ returncode, stdout, _ = await execute_command(
48
+ ["ip", "address"],
49
+ host=host,
50
+ username=username,
51
+ )
52
+
53
+ if returncode == 0 and stdout:
54
+ info.append("\n=== Detailed Interface Information ===")
55
+ info.append(stdout)
56
+
57
+ # Get network statistics using netstat or ss
58
+ returncode, stdout, _ = await execute_command(
59
+ ["cat", "/proc/net/dev"],
60
+ host=host,
61
+ username=username,
62
+ )
63
+
64
+ if returncode == 0 and stdout:
65
+ info.append("\n=== Network I/O Statistics ===")
66
+ info.append(stdout)
67
+
68
+ return "\n".join(info)
69
+ else:
70
+ # Local execution - use psutil
71
+ info = []
72
+ info.append("=== Network Interfaces ===\n")
73
+
74
+ # Get network interface addresses
75
+ net_if_addrs = psutil.net_if_addrs()
76
+ net_if_stats = psutil.net_if_stats()
77
+
78
+ for interface, addrs in sorted(net_if_addrs.items()):
79
+ info.append(f"\n{interface}:")
80
+
81
+ # Get interface stats
82
+ if interface in net_if_stats:
83
+ stats = net_if_stats[interface]
84
+ status = "UP" if stats.isup else "DOWN"
85
+ info.append(f" Status: {status}")
86
+ info.append(f" Speed: {stats.speed} Mbps")
87
+ info.append(f" MTU: {stats.mtu}")
88
+
89
+ # Get addresses
90
+ for addr in addrs:
91
+ if addr.family == socket.AF_INET:
92
+ info.append(f" IPv4 Address: {addr.address}")
93
+ if addr.netmask:
94
+ info.append(f" Netmask: {addr.netmask}")
95
+ if addr.broadcast:
96
+ info.append(f" Broadcast: {addr.broadcast}")
97
+ elif addr.family == socket.AF_INET6:
98
+ info.append(f" IPv6 Address: {addr.address}")
99
+ if addr.netmask:
100
+ info.append(f" Netmask: {addr.netmask}")
101
+ elif addr.family == psutil.AF_LINK:
102
+ info.append(f" MAC Address: {addr.address}")
103
+
104
+ # Network I/O statistics
105
+ net_io = psutil.net_io_counters()
106
+ info.append("\n\n=== Network I/O Statistics (total) ===")
107
+ info.append(f"Bytes Sent: {format_bytes(net_io.bytes_sent)}")
108
+ info.append(f"Bytes Received: {format_bytes(net_io.bytes_recv)}")
109
+ info.append(f"Packets Sent: {net_io.packets_sent}")
110
+ info.append(f"Packets Received: {net_io.packets_recv}")
111
+ info.append(f"Errors In: {net_io.errin}")
112
+ info.append(f"Errors Out: {net_io.errout}")
113
+ info.append(f"Drops In: {net_io.dropin}")
114
+ info.append(f"Drops Out: {net_io.dropout}")
115
+
116
+ return "\n".join(info)
117
+ except Exception as e:
118
+ return f"Error getting network interface information: {str(e)}"
119
+
120
+
121
+ @mcp.tool(
122
+ title="Get network connections",
123
+ description="Get detailed information about active network connections.",
124
+ annotations=ToolAnnotations(readOnlyHint=True),
125
+ )
126
+ @log_tool_call
127
+ async def get_network_connections(
128
+ host: Host | None = None,
129
+ username: Username | None = None,
130
+ ) -> str:
131
+ """
132
+ Get active network connections.
133
+ """
134
+ try:
135
+ if host:
136
+ # Remote execution - use ss or netstat command
137
+ # Try ss first (modern tool)
138
+ returncode, stdout, _ = await execute_command(
139
+ ["ss", "-tunap"],
140
+ host=host,
141
+ username=username,
142
+ )
143
+
144
+ if returncode == 0 and stdout:
145
+ info = []
146
+ info.append("=== Active Network Connections ===\n")
147
+ info.append(stdout)
148
+
149
+ # Count connections
150
+ lines = stdout.strip().split("\n")
151
+ info.append(f"\n\nTotal connections: {len(lines) - 1}") # -1 for header
152
+
153
+ return "\n".join(info)
154
+ else:
155
+ # Fallback to netstat
156
+ returncode, stdout, _ = await execute_command(
157
+ ["netstat", "-tunap"],
158
+ host=host,
159
+ username=username,
160
+ )
161
+
162
+ if returncode == 0 and stdout:
163
+ info = []
164
+ info.append("=== Active Network Connections ===\n")
165
+ info.append(stdout)
166
+ return "\n".join(info)
167
+ else:
168
+ return "Error: Neither ss nor netstat command available on remote host"
169
+ else:
170
+ # Local execution - use psutil
171
+ info = []
172
+ info.append("=== Active Network Connections ===\n")
173
+ info.append(f"{'Proto':<8} {'Local Address':<30} {'Remote Address':<30} {'Status':<15} {'PID/Program'}")
174
+ info.append("-" * 110)
175
+
176
+ # Get all network connections
177
+ connections = psutil.net_connections(kind="inet")
178
+
179
+ for conn in connections:
180
+ proto = "TCP" if conn.type == socket.SOCK_STREAM else "UDP"
181
+
182
+ local_addr = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "N/A"
183
+ remote_addr = f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else "N/A"
184
+ status = conn.status if conn.status else "N/A"
185
+
186
+ # Try to get process info
187
+ pid_info = str(conn.pid) if conn.pid else "N/A"
188
+ if conn.pid:
189
+ try:
190
+ proc = psutil.Process(conn.pid)
191
+ pid_info = f"{conn.pid}/{proc.name()}"
192
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
193
+ pass
194
+
195
+ info.append(f"{proto:<8} {local_addr:<30} {remote_addr:<30} {status:<15} {pid_info}")
196
+
197
+ info.append(f"\n\nTotal connections: {len(connections)}")
198
+
199
+ return "\n".join(info)
200
+ except psutil.AccessDenied:
201
+ return "Permission denied. This tool requires elevated privileges to view all network connections."
202
+ except Exception as e:
203
+ return f"Error getting network connections: {str(e)}"
204
+
205
+
206
+ @mcp.tool(
207
+ title="Get listening ports",
208
+ description="Get details on listening port, protocols, and services.",
209
+ annotations=ToolAnnotations(readOnlyHint=True),
210
+ )
211
+ @log_tool_call
212
+ async def get_listening_ports(
213
+ host: Host | None = None,
214
+ username: Username | None = None,
215
+ ) -> str:
216
+ """
217
+ Get listening ports.
218
+ """
219
+ try:
220
+ if host:
221
+ # Remote execution - use ss or netstat command
222
+ # Try ss first (modern tool)
223
+ returncode, stdout, _ = await execute_command(
224
+ ["ss", "-tulnp"],
225
+ host=host,
226
+ username=username,
227
+ )
228
+
229
+ if returncode == 0 and stdout:
230
+ info = []
231
+ info.append("=== Listening Ports ===\n")
232
+ info.append(stdout)
233
+
234
+ # Count listening ports
235
+ lines = stdout.strip().split("\n")
236
+ info.append(f"\n\nTotal listening ports: {len(lines) - 1}") # -1 for header
237
+
238
+ return "\n".join(info)
239
+ else:
240
+ # Fallback to netstat
241
+ returncode, stdout, _ = await execute_command(
242
+ ["netstat", "-tulnp"],
243
+ host=host,
244
+ username=username,
245
+ )
246
+
247
+ if returncode == 0 and stdout:
248
+ info = []
249
+ info.append("=== Listening Ports ===\n")
250
+ info.append(stdout)
251
+ return "\n".join(info)
252
+ else:
253
+ return "Error: Neither ss nor netstat command available on remote host"
254
+ else:
255
+ # Local execution - use psutil
256
+ info = []
257
+ info.append("=== Listening Ports ===\n")
258
+ info.append(f"{'Proto':<8} {'Local Address':<30} {'Status':<15} {'PID/Program'}")
259
+ info.append("-" * 80)
260
+
261
+ # Get connections in LISTEN state
262
+ connections = psutil.net_connections(kind="inet")
263
+ listening = [c for c in connections if c.status == "LISTEN" or c.type == socket.SOCK_DGRAM]
264
+
265
+ for conn in listening:
266
+ proto = "TCP" if conn.type == socket.SOCK_STREAM else "UDP"
267
+
268
+ local_addr = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "N/A"
269
+ status = conn.status if conn.status else "LISTENING"
270
+
271
+ # Try to get process info
272
+ pid_info = str(conn.pid) if conn.pid else "N/A"
273
+ if conn.pid:
274
+ try:
275
+ proc = psutil.Process(conn.pid)
276
+ pid_info = f"{conn.pid}/{proc.name()}"
277
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
278
+ pass
279
+
280
+ info.append(f"{proto:<8} {local_addr:<30} {status:<15} {pid_info}")
281
+
282
+ info.append(f"\n\nTotal listening ports: {len(listening)}")
283
+
284
+ return "\n".join(info)
285
+ except psutil.AccessDenied:
286
+ return "Permission denied. This tool requires elevated privileges to view all listening ports."
287
+ except Exception as e:
288
+ return f"Error getting listening ports: {str(e)}"
@@ -0,0 +1,273 @@
1
+ """Process management tools."""
2
+
3
+ import typing as t
4
+
5
+ from datetime import datetime
6
+
7
+ import psutil
8
+
9
+ from mcp.types import ToolAnnotations
10
+
11
+ from linux_mcp_server.audit import log_tool_call
12
+ from linux_mcp_server.connection.ssh import execute_command
13
+ from linux_mcp_server.server import mcp
14
+ from linux_mcp_server.utils import format_bytes
15
+ from linux_mcp_server.utils.types import Host
16
+ from linux_mcp_server.utils.types import Username
17
+ from linux_mcp_server.utils.validation import validate_pid
18
+
19
+
20
+ @mcp.tool(
21
+ title="List processes",
22
+ description="List running processes",
23
+ annotations=ToolAnnotations(readOnlyHint=True),
24
+ )
25
+ @log_tool_call
26
+ async def list_processes(
27
+ host: Host | None = None,
28
+ username: Username | None = None,
29
+ ) -> str:
30
+ try:
31
+ if host:
32
+ # Remote execution - use ps command
33
+ returncode, stdout, _ = await execute_command(["ps", "aux", "--sort=-%cpu"], host=host, username=username)
34
+
35
+ if returncode == 0 and stdout:
36
+ info = []
37
+ info.append("=== Running Processes ===\n")
38
+
39
+ lines = stdout.strip().split("\n")
40
+ # Take header and top 100 processes
41
+ if len(lines) > 101:
42
+ info.append("\n".join(lines[:101]))
43
+ info.append(f"\n\nTotal processes: {len(lines) - 1}")
44
+ info.append("Showing: Top 100 by CPU usage")
45
+ else:
46
+ info.append(stdout)
47
+ info.append(f"\n\nTotal processes: {len(lines) - 1}")
48
+
49
+ return "\n".join(info)
50
+ else:
51
+ return "Error executing ps command on remote host"
52
+ else:
53
+ # Local execution - use psutil
54
+ info = []
55
+ info.append("=== Running Processes ===\n")
56
+ info.append(f"{'PID':<8} {'User':<12} {'CPU%':<8} {'Memory%':<10} {'Status':<12} {'Name':<30} {'Command'}")
57
+ info.append("-" * 120)
58
+
59
+ # Get all processes
60
+ processes = []
61
+ for proc in psutil.process_iter(
62
+ ["pid", "name", "username", "cpu_percent", "memory_percent", "status", "cmdline"],
63
+ ):
64
+ try:
65
+ processes.append(proc.info)
66
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
67
+ pass
68
+
69
+ # Sort by CPU usage (descending)
70
+ processes.sort(key=lambda x: x.get("cpu_percent", 0) or 0, reverse=True)
71
+
72
+ # Show top processes (limit to reasonable number)
73
+ for proc_info in processes[:100]: # Show top 100 processes
74
+ pid = proc_info.get("pid", "N/A")
75
+ username_val = proc_info.get("username", "N/A")
76
+ if username_val and len(username_val) > 12:
77
+ username_val = username_val[:9] + "..."
78
+
79
+ cpu = proc_info.get("cpu_percent", 0) or 0
80
+ mem = proc_info.get("memory_percent", 0) or 0
81
+ status = proc_info.get("status", "N/A")
82
+ name = proc_info.get("name", "N/A")
83
+ if name and len(name) > 30:
84
+ name = name[:27] + "..."
85
+
86
+ cmdline = proc_info.get("cmdline", [])
87
+ if cmdline:
88
+ cmd = " ".join(cmdline)
89
+ if len(cmd) > 40:
90
+ cmd = cmd[:37] + "..."
91
+ else:
92
+ cmd = name
93
+
94
+ info.append(f"{pid:<8} {username_val:<12} {cpu:<8.1f} {mem:<10.1f} {status:<12} {name:<30} {cmd}")
95
+
96
+ # Add summary
97
+ total_processes = len(list(psutil.process_iter()))
98
+ info.append(f"\n\nTotal processes: {total_processes}")
99
+ info.append("Showing: Top 100 by CPU usage")
100
+
101
+ return "\n".join(info)
102
+ except Exception as e:
103
+ return f"Error listing processes: {str(e)}"
104
+
105
+
106
+ @mcp.tool(
107
+ title="Process details",
108
+ description="Get information about a specific process.",
109
+ annotations=ToolAnnotations(readOnlyHint=True),
110
+ )
111
+ @log_tool_call
112
+ async def get_process_info( # noqa: C901
113
+ pid: t.Annotated[int, "Process ID"],
114
+ host: Host | None = None,
115
+ username: Username | None = None,
116
+ ) -> str:
117
+ # Validate PID (accepts floats from LLMs)
118
+ validated_pid, error = validate_pid(pid)
119
+ if error:
120
+ return error
121
+
122
+ if validated_pid is None:
123
+ return "Invalid PID"
124
+
125
+ try:
126
+ if host:
127
+ # Remote execution - use ps command
128
+ returncode, stdout, _ = await execute_command(
129
+ ["ps", "-p", str(validated_pid), "-o", "pid,user,stat,pcpu,pmem,vsz,rss,etime,comm,args"],
130
+ host=host,
131
+ username=username,
132
+ )
133
+
134
+ if returncode != 0:
135
+ return f"Process with PID {validated_pid} does not exist on remote host."
136
+
137
+ if stdout:
138
+ info = []
139
+ info.append(f"=== Process Information for PID {validated_pid} ===\n")
140
+ info.append(stdout)
141
+
142
+ # Try to get more details with /proc
143
+ returncode, stdout, _ = await execute_command(
144
+ ["cat", f"/proc/{validated_pid}/status"],
145
+ host=host,
146
+ username=username,
147
+ )
148
+
149
+ if returncode == 0 and stdout:
150
+ info.append("\n=== Detailed Status (/proc) ===")
151
+ # Filter to show most relevant fields
152
+ relevant_fields = [
153
+ "Name:",
154
+ "State:",
155
+ "Tgid:",
156
+ "Pid:",
157
+ "PPid:",
158
+ "Threads:",
159
+ "VmPeak:",
160
+ "VmSize:",
161
+ "VmRSS:",
162
+ ]
163
+ for line in stdout.split("\n"):
164
+ if any(field in line for field in relevant_fields):
165
+ info.append(line)
166
+
167
+ return "\n".join(info)
168
+ else:
169
+ return f"Process with PID {validated_pid} does not exist on remote host."
170
+ else:
171
+ # Local execution - use psutil
172
+ # Check if process exists
173
+ if not psutil.pid_exists(validated_pid):
174
+ return f"Process with PID {validated_pid} does not exist."
175
+
176
+ proc = psutil.Process(validated_pid)
177
+ info = []
178
+
179
+ info.append(f"=== Process Information for PID {validated_pid} ===\n")
180
+
181
+ # Basic info
182
+ try:
183
+ info.append(f"Name: {proc.name()}")
184
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
185
+ pass
186
+
187
+ try:
188
+ info.append(f"Executable: {proc.exe()}")
189
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
190
+ info.append("Executable: [Access Denied]")
191
+
192
+ try:
193
+ info.append(f"Command Line: {' '.join(proc.cmdline())}")
194
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
195
+ pass
196
+
197
+ try:
198
+ info.append(f"Status: {proc.status()}")
199
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
200
+ pass
201
+
202
+ try:
203
+ info.append(f"User: {proc.username()}")
204
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
205
+ pass
206
+
207
+ # Process IDs
208
+ try:
209
+ info.append(f"\nPID: {proc.pid}")
210
+ info.append(f"Parent PID: {proc.ppid()}")
211
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
212
+ pass
213
+
214
+ # Resource usage
215
+ try:
216
+ info.append("\n=== Resource Usage ===")
217
+ cpu_percent = proc.cpu_percent(interval=0.1)
218
+ info.append(f"CPU Percent: {cpu_percent}%")
219
+
220
+ mem_info = proc.memory_info()
221
+ info.append(f"Memory RSS: {format_bytes(mem_info.rss)}")
222
+ info.append(f"Memory VMS: {format_bytes(mem_info.vms)}")
223
+ info.append(f"Memory Percent: {proc.memory_percent():.2f}%")
224
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
225
+ info.append("Resource usage: [Access Denied]")
226
+
227
+ # Timing
228
+ try:
229
+ create_time = datetime.fromtimestamp(proc.create_time())
230
+ info.append("\n=== Timing ===")
231
+ info.append(f"Created: {create_time.strftime('%Y-%m-%d %H:%M:%S')}")
232
+
233
+ cpu_times = proc.cpu_times()
234
+ info.append(f"CPU Time (user): {cpu_times.user:.2f}s")
235
+ info.append(f"CPU Time (system): {cpu_times.system:.2f}s")
236
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
237
+ pass
238
+
239
+ # Threads
240
+ try:
241
+ num_threads = proc.num_threads()
242
+ info.append(f"\nThreads: {num_threads}")
243
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
244
+ pass
245
+
246
+ # File descriptors
247
+ try:
248
+ num_fds = proc.num_fds()
249
+ info.append(f"Open File Descriptors: {num_fds}")
250
+ except (psutil.NoSuchProcess, psutil.AccessDenied, AttributeError):
251
+ pass # Not available on all systems
252
+
253
+ # Connections
254
+ try:
255
+ connections = proc.connections()
256
+ if connections:
257
+ info.append(f"\n=== Network Connections ({len(connections)}) ===")
258
+ for _, conn in enumerate(connections[:10]): # Show first 10
259
+ info.append(
260
+ f" {conn.type.name}: {conn.laddr} -> {conn.raddr if conn.raddr else 'N/A'} [{conn.status}]",
261
+ )
262
+ if len(connections) > 10:
263
+ info.append(f" ... and {len(connections) - 10} more")
264
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
265
+ pass
266
+
267
+ return "\n".join(info)
268
+ except psutil.NoSuchProcess:
269
+ return f"Process with PID {validated_pid} does not exist."
270
+ except psutil.AccessDenied:
271
+ return f"Access denied to process with PID {validated_pid}. Try running with elevated privileges."
272
+ except Exception as e:
273
+ return f"Error getting process information: {str(e)}"