server-management-lib 0.2.0__tar.gz

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.
Files changed (37) hide show
  1. server_management_lib-0.2.0/PKG-INFO +46 -0
  2. server_management_lib-0.2.0/README.md +14 -0
  3. server_management_lib-0.2.0/pyproject.toml +71 -0
  4. server_management_lib-0.2.0/setup.cfg +4 -0
  5. server_management_lib-0.2.0/src/server_management_lib/__init__.py +124 -0
  6. server_management_lib-0.2.0/src/server_management_lib/audit.py +80 -0
  7. server_management_lib-0.2.0/src/server_management_lib/base.py +55 -0
  8. server_management_lib-0.2.0/src/server_management_lib/config.py +385 -0
  9. server_management_lib-0.2.0/src/server_management_lib/executor.py +53 -0
  10. server_management_lib-0.2.0/src/server_management_lib/http_clients.py +636 -0
  11. server_management_lib-0.2.0/src/server_management_lib/local_executor.py +200 -0
  12. server_management_lib-0.2.0/src/server_management_lib/operations.py +33 -0
  13. server_management_lib-0.2.0/src/server_management_lib/output.py +61 -0
  14. server_management_lib-0.2.0/src/server_management_lib/py.typed +0 -0
  15. server_management_lib-0.2.0/src/server_management_lib/rate_limit.py +118 -0
  16. server_management_lib-0.2.0/src/server_management_lib/redaction.py +191 -0
  17. server_management_lib-0.2.0/src/server_management_lib/responses.py +126 -0
  18. server_management_lib-0.2.0/src/server_management_lib/security.py +1552 -0
  19. server_management_lib-0.2.0/src/server_management_lib/ssh_manager.py +447 -0
  20. server_management_lib-0.2.0/src/server_management_lib/transport.py +128 -0
  21. server_management_lib-0.2.0/src/server_management_lib/tunnel.py +380 -0
  22. server_management_lib-0.2.0/src/server_management_lib.egg-info/PKG-INFO +46 -0
  23. server_management_lib-0.2.0/src/server_management_lib.egg-info/SOURCES.txt +35 -0
  24. server_management_lib-0.2.0/src/server_management_lib.egg-info/dependency_links.txt +1 -0
  25. server_management_lib-0.2.0/src/server_management_lib.egg-info/requires.txt +6 -0
  26. server_management_lib-0.2.0/src/server_management_lib.egg-info/top_level.txt +1 -0
  27. server_management_lib-0.2.0/tests/test_audit.py +94 -0
  28. server_management_lib-0.2.0/tests/test_config.py +309 -0
  29. server_management_lib-0.2.0/tests/test_http_clients.py +1118 -0
  30. server_management_lib-0.2.0/tests/test_local_executor.py +208 -0
  31. server_management_lib-0.2.0/tests/test_operations.py +31 -0
  32. server_management_lib-0.2.0/tests/test_output.py +44 -0
  33. server_management_lib-0.2.0/tests/test_redaction.py +132 -0
  34. server_management_lib-0.2.0/tests/test_security.py +1031 -0
  35. server_management_lib-0.2.0/tests/test_ssh_manager.py +799 -0
  36. server_management_lib-0.2.0/tests/test_tooling.py +375 -0
  37. server_management_lib-0.2.0/tests/test_tunnel.py +445 -0
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: server-management-lib
3
+ Version: 0.2.0
4
+ Summary: Shared security, SSH/local execution, configuration and HTTP clients for the Server MCP suite.
5
+ Author-email: Alessio Elmi <alessio.elmi@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/elmuz/server-mcp
8
+ Project-URL: Repository, https://github.com/elmuz/server-mcp
9
+ Project-URL: Documentation, https://github.com/elmuz/server-mcp/tree/main/docs
10
+ Project-URL: Issues, https://github.com/elmuz/server-mcp/issues
11
+ Project-URL: Changelog, https://github.com/elmuz/server-mcp/blob/main/CHANGELOG.md
12
+ Keywords: mcp,model-context-protocol,linux,server-management,docker,homelab,self-hosting
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: asyncssh<3,>=2.22
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: aiohttp>=3.9.0
29
+ Requires-Dist: python-dotenv>=1.0.0
30
+ Requires-Dist: sqlparse>=0.5
31
+ Requires-Dist: pydantic>=2.12
32
+
33
+ # server-management-lib
34
+
35
+ Shared core for the Server MCP suite: security validation, SSH and local executors,
36
+ configuration resolution, output limits, audit logging, and read-only InfluxDB/Prometheus
37
+ clients.
38
+
39
+ Part of the [Server MCP suite](../../README.md).
40
+
41
+ - [Architecture](../../docs/architecture.md)
42
+ - [Security model](../../docs/security-model.md)
43
+ - [Configuration](../../docs/configuration.md)
44
+
45
+ The two MCP servers depend on this package through the uv workspace. It has no MCP
46
+ dependency and can be used by other server-management tools.
@@ -0,0 +1,14 @@
1
+ # server-management-lib
2
+
3
+ Shared core for the Server MCP suite: security validation, SSH and local executors,
4
+ configuration resolution, output limits, audit logging, and read-only InfluxDB/Prometheus
5
+ clients.
6
+
7
+ Part of the [Server MCP suite](../../README.md).
8
+
9
+ - [Architecture](../../docs/architecture.md)
10
+ - [Security model](../../docs/security-model.md)
11
+ - [Configuration](../../docs/configuration.md)
12
+
13
+ The two MCP servers depend on this package through the uv workspace. It has no MCP
14
+ dependency and can be used by other server-management tools.
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "server-management-lib"
3
+ version = "0.2.0"
4
+ description = "Shared security, SSH/local execution, configuration and HTTP clients for the Server MCP suite."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "asyncssh>=2.22,<3",
10
+ "pyyaml>=6.0",
11
+ "aiohttp>=3.9.0",
12
+ "python-dotenv>=1.0.0",
13
+ "sqlparse>=0.5",
14
+ "pydantic>=2.12",
15
+ ]
16
+
17
+ authors = [
18
+ { name = "Alessio Elmi", email = "alessio.elmi@gmail.com" },
19
+ ]
20
+ keywords = [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "linux",
24
+ "server-management",
25
+ "docker",
26
+ "homelab",
27
+ "self-hosting",
28
+ ]
29
+ classifiers = [
30
+ "Development Status :: 4 - Beta",
31
+ "Environment :: Console",
32
+ "Intended Audience :: System Administrators",
33
+ "Operating System :: POSIX :: Linux",
34
+ "Programming Language :: Python :: 3",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Programming Language :: Python :: 3.14",
39
+ "Topic :: System :: Systems Administration",
40
+ "Typing :: Typed",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/elmuz/server-mcp"
45
+ Repository = "https://github.com/elmuz/server-mcp"
46
+ Documentation = "https://github.com/elmuz/server-mcp/tree/main/docs"
47
+ Issues = "https://github.com/elmuz/server-mcp/issues"
48
+ Changelog = "https://github.com/elmuz/server-mcp/blob/main/CHANGELOG.md"
49
+
50
+ [build-system]
51
+ requires = ["setuptools>=61.0"]
52
+ build-backend = "setuptools.build_meta"
53
+
54
+ [tool.setuptools.package-data]
55
+ server_management_lib = ["py.typed"]
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
59
+ python_files = ["test_*.py"]
60
+ python_classes = ["Test*"]
61
+ python_functions = ["test_*"]
62
+ addopts = "--cov=src/server_management_lib --cov-report=term-missing --cov-fail-under=90"
63
+
64
+ [tool.coverage.run]
65
+ source = ["server_management_lib"]
66
+ branch = true
67
+
68
+ [tool.coverage.report]
69
+ fail_under = 90
70
+ show_missing = true
71
+ skip_covered = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,124 @@
1
+ """
2
+ Server Management Library
3
+
4
+ Shared components for server management MCPs:
5
+
6
+ - Configuration loading (YAML + environment + XDG paths)
7
+ - Security validation (commands, queries, paths)
8
+ - SSH connection management (host key verification by default)
9
+ - InfluxDB and Prometheus HTTP clients
10
+ """
11
+
12
+ from .audit import AuditLogger, create_audit_logger
13
+ from .base import (
14
+ ExecutorBase,
15
+ ExecutorError,
16
+ RemoteDirEntry,
17
+ RemoteFileError,
18
+ RemotePathError,
19
+ )
20
+ from .config import (
21
+ DEFAULT_CONFIG,
22
+ ConfigError,
23
+ ConfigNotFoundError,
24
+ config_schema,
25
+ load_config,
26
+ redact_config_values,
27
+ resolve_config_path,
28
+ )
29
+ from .executor import Executor, create_executor
30
+ from .http_clients import InfluxDBClient, PrometheusClient, parse_duration
31
+ from .local_executor import LocalExecutor
32
+ from .operations import Operation
33
+ from .output import DEFAULT_MAX_OUTPUT_BYTES, sanitize_output, strip_ansi
34
+ from .rate_limit import (
35
+ RateLimitConfig,
36
+ RateLimiter,
37
+ create_rate_limiter,
38
+ )
39
+ from .redaction import REDACTED_VALUE, redact_env_content, redact_secrets
40
+ from .responses import (
41
+ ToolOutput,
42
+ ToolResponse,
43
+ build_response,
44
+ make_structured_tool,
45
+ )
46
+ from .security import (
47
+ SNAPRAID_STATUS_SUDO_ARGV,
48
+ FileAccess,
49
+ SecurityValidator,
50
+ ServiceFilePath,
51
+ )
52
+ from .ssh_manager import SSHConfigurationError, SSHManager
53
+ from .transport import (
54
+ BearerTokenMiddleware,
55
+ is_loopback_host,
56
+ run_streamable_http,
57
+ validate_http_binding,
58
+ )
59
+ from .tunnel import (
60
+ ActiveTunnel,
61
+ SSHTunnel,
62
+ SSHTunnelError,
63
+ TunnelConfig,
64
+ TunnelEndpoint,
65
+ close_metric_tunnels,
66
+ open_metric_tunnels,
67
+ )
68
+
69
+ __all__ = [
70
+ "DEFAULT_CONFIG",
71
+ "DEFAULT_MAX_OUTPUT_BYTES",
72
+ "REDACTED_VALUE",
73
+ "SNAPRAID_STATUS_SUDO_ARGV",
74
+ "ActiveTunnel",
75
+ "AuditLogger",
76
+ "BearerTokenMiddleware",
77
+ "ConfigError",
78
+ "ConfigNotFoundError",
79
+ "Executor",
80
+ "ExecutorBase",
81
+ "ExecutorError",
82
+ "FileAccess",
83
+ "InfluxDBClient",
84
+ "LocalExecutor",
85
+ "Operation",
86
+ "PrometheusClient",
87
+ "RateLimitConfig",
88
+ "RateLimiter",
89
+ "RemoteDirEntry",
90
+ "RemoteFileError",
91
+ "RemotePathError",
92
+ "SSHConfigurationError",
93
+ "SSHManager",
94
+ "SSHTunnel",
95
+ "SSHTunnelError",
96
+ "SecurityValidator",
97
+ "ServiceFilePath",
98
+ "ToolOutput",
99
+ "ToolResponse",
100
+ "TunnelConfig",
101
+ "TunnelEndpoint",
102
+ "__version__",
103
+ "build_response",
104
+ "close_metric_tunnels",
105
+ "config_schema",
106
+ "create_audit_logger",
107
+ "create_executor",
108
+ "create_rate_limiter",
109
+ "is_loopback_host",
110
+ "load_config",
111
+ "make_structured_tool",
112
+ "open_metric_tunnels",
113
+ "parse_duration",
114
+ "redact_config_values",
115
+ "redact_env_content",
116
+ "redact_secrets",
117
+ "resolve_config_path",
118
+ "run_streamable_http",
119
+ "sanitize_output",
120
+ "strip_ansi",
121
+ "validate_http_binding",
122
+ ]
123
+
124
+ __version__ = "0.2.0"
@@ -0,0 +1,80 @@
1
+ """
2
+ Append-only audit log for privileged operations.
3
+
4
+ Every command, file read and blocked operation can be recorded as one JSON
5
+ object per line. Logging is best effort: an unwritable log must never break the
6
+ operation, but it is reported once.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import threading
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ REDACTED = "***REDACTED***"
19
+ SENSITIVE_KEY_PARTS = ("password", "token", "secret", "api_key", "apikey", "key")
20
+
21
+
22
+ def _redact(value: Any, key: str = "") -> Any:
23
+ if any(part in key.lower() for part in SENSITIVE_KEY_PARTS):
24
+ return REDACTED
25
+ if isinstance(value, dict):
26
+ return {k: _redact(v, str(k)) for k, v in value.items()}
27
+ if isinstance(value, list | tuple):
28
+ return [_redact(item) for item in value]
29
+ return value
30
+
31
+
32
+ class AuditLogger:
33
+ """Write JSONL audit records to ``path`` when enabled."""
34
+
35
+ def __init__(self, path: str | Path | None = None, *, enabled: bool = False):
36
+ self.enabled = bool(enabled and path)
37
+ self.path = Path(path).expanduser() if path else None
38
+ self._lock = threading.Lock()
39
+ self._warned = False
40
+
41
+ def log(self, event: str, **fields: Any) -> None:
42
+ if not self.enabled or self.path is None:
43
+ return
44
+
45
+ record = {
46
+ "ts": datetime.now(UTC).isoformat(timespec="milliseconds"),
47
+ "event": event,
48
+ **_redact(fields),
49
+ }
50
+ try:
51
+ parent = self.path.parent
52
+ parent_existed = parent.exists()
53
+ parent.mkdir(parents=True, exist_ok=True, mode=0o700)
54
+ if not parent_existed:
55
+ # mkdir honours the umask; make the new audit directory private.
56
+ parent.chmod(0o700)
57
+
58
+ line = json.dumps(record, default=str)
59
+ with self._lock, self.path.open("a", encoding="utf-8") as handle:
60
+ # The file may have been created by an older release with the
61
+ # default mode; tighten it on every write.
62
+ self.path.chmod(0o600)
63
+ handle.write(line + "\n")
64
+ except OSError as e: # pragma: no cover - environment dependent
65
+ if not self._warned:
66
+ logger.warning("Cannot write audit log %s: %s", self.path, e)
67
+ self._warned = True
68
+
69
+
70
+ def create_audit_logger(config: dict) -> AuditLogger:
71
+ """Build an :class:`AuditLogger` from the ``audit`` config section.
72
+
73
+ The HTTP clients receive the same logger as the executors so query
74
+ metadata is recorded in one audit log.
75
+ """
76
+ audit_config = config.get("audit", {}) or {}
77
+ return AuditLogger(
78
+ audit_config.get("path"),
79
+ enabled=audit_config.get("enabled", False),
80
+ )
@@ -0,0 +1,55 @@
1
+ """
2
+ Shared executor primitives.
3
+
4
+ An executor runs validated commands and reads remote/local files. SSH and local
5
+ implementations share output limits, audit logging and path-confinement rules.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from .audit import create_audit_logger
11
+ from .output import DEFAULT_MAX_OUTPUT_BYTES
12
+ from .security import SecurityValidator
13
+
14
+
15
+ class ExecutorError(RuntimeError):
16
+ """Base class for executor errors."""
17
+
18
+
19
+ class RemotePathError(ExecutorError):
20
+ """Raised when a path escapes its allowed base directory."""
21
+
22
+
23
+ class RemoteFileError(ExecutorError):
24
+ """Raised when a file cannot be read safely."""
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class RemoteDirEntry:
29
+ """A single directory entry returned by an executor."""
30
+
31
+ name: str
32
+ longname: str
33
+ size: int
34
+
35
+
36
+ def is_within(path: str, base: str) -> bool:
37
+ """Return True when ``path`` is ``base`` or below it (component-aware)."""
38
+ base = base.rstrip("/")
39
+ return path == base or path.startswith(base + "/")
40
+
41
+
42
+ class ExecutorBase:
43
+ """Configuration, output limits and audit logging shared by executors."""
44
+
45
+ def __init__(self, config: dict, security: SecurityValidator):
46
+ self.config = config
47
+ self.security = security
48
+ self.services_base_path = security.services_base_path
49
+
50
+ output_config = config.get("output", {}) or {}
51
+ self.max_output_bytes = int(
52
+ output_config.get("max_bytes", DEFAULT_MAX_OUTPUT_BYTES)
53
+ )
54
+
55
+ self.audit = create_audit_logger(config)