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.
- server_management_lib-0.2.0/PKG-INFO +46 -0
- server_management_lib-0.2.0/README.md +14 -0
- server_management_lib-0.2.0/pyproject.toml +71 -0
- server_management_lib-0.2.0/setup.cfg +4 -0
- server_management_lib-0.2.0/src/server_management_lib/__init__.py +124 -0
- server_management_lib-0.2.0/src/server_management_lib/audit.py +80 -0
- server_management_lib-0.2.0/src/server_management_lib/base.py +55 -0
- server_management_lib-0.2.0/src/server_management_lib/config.py +385 -0
- server_management_lib-0.2.0/src/server_management_lib/executor.py +53 -0
- server_management_lib-0.2.0/src/server_management_lib/http_clients.py +636 -0
- server_management_lib-0.2.0/src/server_management_lib/local_executor.py +200 -0
- server_management_lib-0.2.0/src/server_management_lib/operations.py +33 -0
- server_management_lib-0.2.0/src/server_management_lib/output.py +61 -0
- server_management_lib-0.2.0/src/server_management_lib/py.typed +0 -0
- server_management_lib-0.2.0/src/server_management_lib/rate_limit.py +118 -0
- server_management_lib-0.2.0/src/server_management_lib/redaction.py +191 -0
- server_management_lib-0.2.0/src/server_management_lib/responses.py +126 -0
- server_management_lib-0.2.0/src/server_management_lib/security.py +1552 -0
- server_management_lib-0.2.0/src/server_management_lib/ssh_manager.py +447 -0
- server_management_lib-0.2.0/src/server_management_lib/transport.py +128 -0
- server_management_lib-0.2.0/src/server_management_lib/tunnel.py +380 -0
- server_management_lib-0.2.0/src/server_management_lib.egg-info/PKG-INFO +46 -0
- server_management_lib-0.2.0/src/server_management_lib.egg-info/SOURCES.txt +35 -0
- server_management_lib-0.2.0/src/server_management_lib.egg-info/dependency_links.txt +1 -0
- server_management_lib-0.2.0/src/server_management_lib.egg-info/requires.txt +6 -0
- server_management_lib-0.2.0/src/server_management_lib.egg-info/top_level.txt +1 -0
- server_management_lib-0.2.0/tests/test_audit.py +94 -0
- server_management_lib-0.2.0/tests/test_config.py +309 -0
- server_management_lib-0.2.0/tests/test_http_clients.py +1118 -0
- server_management_lib-0.2.0/tests/test_local_executor.py +208 -0
- server_management_lib-0.2.0/tests/test_operations.py +31 -0
- server_management_lib-0.2.0/tests/test_output.py +44 -0
- server_management_lib-0.2.0/tests/test_redaction.py +132 -0
- server_management_lib-0.2.0/tests/test_security.py +1031 -0
- server_management_lib-0.2.0/tests/test_ssh_manager.py +799 -0
- server_management_lib-0.2.0/tests/test_tooling.py +375 -0
- 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,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)
|