remote-server-mcp 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.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: remote-server-mcp
3
+ Version: 0.2.0
4
+ Summary: Security-first MCP server for managing Docker services on a remote server or on the local host.
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: mcp<3,>=2.3
27
+ Requires-Dist: server-management-lib<0.3,>=0.2
28
+
29
+ # remote-server-mcp
30
+
31
+ Security-first MCP server for Docker service management over SSH or on the local host.
32
+
33
+ Part of the [Server MCP suite](../../README.md). It exposes a fixed catalog of 21 tools
34
+ (directory and container discovery, container and host-service logs, status and health,
35
+ start/stop/restart, scoped Compose operations, confined file reads, server health and
36
+ read-only InfluxDB/Prometheus queries). No generic command execution.
37
+
38
+ The container CLI is configurable (`docker`, `podman` or `nerdctl`). `restart_service`,
39
+ `stop_service`, `compose_pull`, `compose_up` and `compose_down` require `confirm=true` and
40
+ are bounded by a per-process rate limit. Streamable HTTP is available with
41
+ `--transport streamable-http`: it binds to loopback by default and requires a bearer token
42
+ for any other address.
43
+
44
+ Host infrastructure services such as `telegraf`, `smartd` and `docker` are systemd units,
45
+ not containers. `get_host_service_status` and `get_host_service_logs` read them with
46
+ `systemctl`/`journalctl` and only accept units listed in `security.host_units`.
47
+
48
+ Container logs always include timestamps. A log request can add a `since`/`until` window
49
+ and target one container in a compose project; search matches the exact literal pattern.
50
+ Every tool publishes a structured `ToolResponse` next to the readable text. Composite and
51
+ listing responses receive a final `output.max_bytes` cap. When the audit log
52
+ is enabled, InfluxDB and Prometheus queries are recorded by length/SHA-256 fingerprint
53
+ rather than by query text.
54
+
55
+ Prometheus is optional. Its metric endpoints can be reached through an HTTPS reverse proxy
56
+ or a loopback-only `ssh_tunnel` created by the MCP; see
57
+ [configuration](../../docs/configuration.md#database-endpoints).
58
+
59
+ - [Tools](../../docs/tools.md)
60
+ - [Configuration](../../docs/configuration.md)
61
+ - [Security model](../../docs/security-model.md)
62
+ - [Getting started](../../docs/getting-started.md)
63
+
64
+ ```bash
65
+ uv run --directory apps/remote-server-mcp remote-server-mcp
66
+ ```
67
+
68
+ Requires a config file; see `config.example.yaml` in this directory.
@@ -0,0 +1,40 @@
1
+ # remote-server-mcp
2
+
3
+ Security-first MCP server for Docker service management over SSH or on the local host.
4
+
5
+ Part of the [Server MCP suite](../../README.md). It exposes a fixed catalog of 21 tools
6
+ (directory and container discovery, container and host-service logs, status and health,
7
+ start/stop/restart, scoped Compose operations, confined file reads, server health and
8
+ read-only InfluxDB/Prometheus queries). No generic command execution.
9
+
10
+ The container CLI is configurable (`docker`, `podman` or `nerdctl`). `restart_service`,
11
+ `stop_service`, `compose_pull`, `compose_up` and `compose_down` require `confirm=true` and
12
+ are bounded by a per-process rate limit. Streamable HTTP is available with
13
+ `--transport streamable-http`: it binds to loopback by default and requires a bearer token
14
+ for any other address.
15
+
16
+ Host infrastructure services such as `telegraf`, `smartd` and `docker` are systemd units,
17
+ not containers. `get_host_service_status` and `get_host_service_logs` read them with
18
+ `systemctl`/`journalctl` and only accept units listed in `security.host_units`.
19
+
20
+ Container logs always include timestamps. A log request can add a `since`/`until` window
21
+ and target one container in a compose project; search matches the exact literal pattern.
22
+ Every tool publishes a structured `ToolResponse` next to the readable text. Composite and
23
+ listing responses receive a final `output.max_bytes` cap. When the audit log
24
+ is enabled, InfluxDB and Prometheus queries are recorded by length/SHA-256 fingerprint
25
+ rather than by query text.
26
+
27
+ Prometheus is optional. Its metric endpoints can be reached through an HTTPS reverse proxy
28
+ or a loopback-only `ssh_tunnel` created by the MCP; see
29
+ [configuration](../../docs/configuration.md#database-endpoints).
30
+
31
+ - [Tools](../../docs/tools.md)
32
+ - [Configuration](../../docs/configuration.md)
33
+ - [Security model](../../docs/security-model.md)
34
+ - [Getting started](../../docs/getting-started.md)
35
+
36
+ ```bash
37
+ uv run --directory apps/remote-server-mcp remote-server-mcp
38
+ ```
39
+
40
+ Requires a config file; see `config.example.yaml` in this directory.
@@ -0,0 +1,75 @@
1
+ [project]
2
+ name = "remote-server-mcp"
3
+ version = "0.2.0"
4
+ description = "Security-first MCP server for managing Docker services on a remote server or on the local host."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "mcp>=2.3,<3",
10
+ "server-management-lib>=0.2,<0.3",
11
+ ]
12
+
13
+ authors = [
14
+ { name = "Alessio Elmi", email = "alessio.elmi@gmail.com" },
15
+ ]
16
+ keywords = [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "linux",
20
+ "server-management",
21
+ "docker",
22
+ "homelab",
23
+ "self-hosting",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Environment :: Console",
28
+ "Intended Audience :: System Administrators",
29
+ "Operating System :: POSIX :: Linux",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
+ "Topic :: System :: Systems Administration",
36
+ "Typing :: Typed",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/elmuz/server-mcp"
41
+ Repository = "https://github.com/elmuz/server-mcp"
42
+ Documentation = "https://github.com/elmuz/server-mcp/tree/main/docs"
43
+ Issues = "https://github.com/elmuz/server-mcp/issues"
44
+ Changelog = "https://github.com/elmuz/server-mcp/blob/main/CHANGELOG.md"
45
+
46
+ [project.scripts]
47
+ remote-server-mcp = "remote_server_mcp:cli"
48
+
49
+ [build-system]
50
+ requires = ["setuptools>=61.0"]
51
+ build-backend = "setuptools.build_meta"
52
+
53
+ [tool.setuptools.package-data]
54
+ remote_server_mcp = ["py.typed"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ python_files = ["test_*.py"]
59
+ python_classes = ["Test*"]
60
+ python_functions = ["test_*"]
61
+ markers = [
62
+ "integration: marks integration tests that require real SSH/DB access",
63
+ ]
64
+ # Keep the default suite deterministic. Run integration tests explicitly with:
65
+ # uv run pytest -m integration
66
+ addopts = "-m 'not integration' --cov=src/remote_server_mcp --cov-report=term-missing"
67
+
68
+ [tool.coverage.run]
69
+ source = ["remote_server_mcp"]
70
+ branch = true
71
+
72
+ [tool.coverage.report]
73
+ fail_under = 60
74
+ show_missing = true
75
+ skip_covered = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """
2
+ Remote Server MCP - Main entry point
3
+ """
4
+
5
+ from .server import cli, main, mcp
6
+
7
+ __version__ = "0.2.0"
8
+
9
+ __all__ = ["__version__", "cli", "main", "mcp"]
10
+
11
+ if __name__ == "__main__":
12
+ cli()
@@ -0,0 +1,241 @@
1
+ """
2
+ Operation catalog for the remote-server MCP.
3
+
4
+ Commands are built as argument vectors and rendered with :func:`shlex.join`, so
5
+ no user-controlled value is ever concatenated into a shell string. Operations
6
+ also declare their safety level, which is used for MCP tool annotations and
7
+ audit records.
8
+
9
+ The container CLI is configurable (``container_runtime``): Docker, Podman and
10
+ nerdctl share the same command surface for the operations below. The module
11
+ level constants remain the Docker catalog for backwards compatibility and for
12
+ the command-policy tests.
13
+ """
14
+
15
+ from dataclasses import dataclass
16
+
17
+ from server_management_lib import Operation
18
+
19
+ # ``docker ps``/``docker stats`` expand ``\\n`` and ``\\t`` inside their format
20
+ # strings, but ``docker inspect`` does not: it passes the text to the Go
21
+ # template verbatim, so a literal ``\\n`` ends up in the output and every
22
+ # field stays on one line. Emitting the newline from inside a template action
23
+ # (``{{"\\n"}}``) keeps the argv free of control characters (the command
24
+ # policy rejects those) and still produces real line breaks.
25
+ NEWLINE = '{{"\\n"}}'
26
+
27
+ INSPECT_FORMAT = (
28
+ f"Name: {{{{.Name}}}}{NEWLINE}"
29
+ f"State: {{{{.State.Status}}}}{NEWLINE}"
30
+ f"Running: {{{{.State.Running}}}}{NEWLINE}"
31
+ f"Health: {{{{if .State.Health}}}}{{{{.State.Health.Status}}}}{{{{else}}}}none"
32
+ f"{{{{end}}}}{NEWLINE}"
33
+ f"FailingStreak: {{{{if .State.Health}}}}{{{{.State.Health.FailingStreak}}}}"
34
+ f"{{{{else}}}}0{{{{end}}}}{NEWLINE}"
35
+ f"RestartCount: {{{{.RestartCount}}}}{NEWLINE}"
36
+ f"ExitCode: {{{{.State.ExitCode}}}}{NEWLINE}"
37
+ f"StartedAt: {{{{.State.StartedAt}}}}"
38
+ )
39
+
40
+ # ``docker stats`` *does* expand ``\\n`` before it parses the template, so it
41
+ # must keep the plain escape. Using the ``NEWLINE`` action here would turn the
42
+ # string literal into a real newline and fail with "unterminated quoted
43
+ # string".
44
+ STATS_FORMAT = "CPU: {{.CPUPerc}}\\nMemory: {{.MemUsage}}\\nNet I/O: {{.NetIO}}"
45
+ HEALTH_FORMAT = "{{json .State.Health}}"
46
+
47
+ # One line per container, including stopped and crashed ones. The compose
48
+ # project label is how a service directory resolves to its containers.
49
+ CONTAINER_LIST_FORMAT = (
50
+ '{{.Names}}\\t{{.Label "com.docker.compose.project"}}\\t{{.Status}}'
51
+ )
52
+
53
+
54
+ def container_logs_operation(
55
+ *, runtime: str = "docker", since: str | None = None, until: str | None = None
56
+ ) -> Operation:
57
+ """Build a ``<runtime> logs`` operation with optional time filters.
58
+
59
+ ``--timestamps`` is always enabled so log lines can be correlated with
60
+ host events. Time values are validated by the security layer before they
61
+ reach this function.
62
+ """
63
+ argv = [runtime, "logs", "--tail", "$lines", "--timestamps"]
64
+ if since:
65
+ argv.extend(("--since", "$since"))
66
+ if until:
67
+ argv.extend(("--until", "$until"))
68
+ argv.extend(("--", "$service"))
69
+ return Operation(tuple(argv))
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ContainerCatalog:
74
+ """Every container operation for one runtime."""
75
+
76
+ runtime: str
77
+ ps: Operation
78
+ ps_all: Operation
79
+ ps_unhealthy: Operation
80
+ inspect: Operation
81
+ stats: Operation
82
+ ports: Operation
83
+ health: Operation
84
+ restart: Operation
85
+ start: Operation
86
+ stop: Operation
87
+ compose_ps: Operation
88
+ compose_pull: Operation
89
+ compose_up: Operation
90
+ compose_down: Operation
91
+
92
+ def logs(self, *, since: str | None = None, until: str | None = None) -> Operation:
93
+ return container_logs_operation(runtime=self.runtime, since=since, until=until)
94
+
95
+
96
+ def build_container_catalog(runtime: str = "docker") -> ContainerCatalog:
97
+ """Build the container operation catalog for ``runtime``.
98
+
99
+ ``runtime`` is validated by :class:`SecurityValidator` before it reaches
100
+ this function; the command policy also rejects unknown runtimes.
101
+ """
102
+ return ContainerCatalog(
103
+ runtime=runtime,
104
+ ps=Operation((runtime, "ps", "--format", "{{.Names}}\\t{{.Status}}")),
105
+ ps_all=Operation((runtime, "ps", "-a", "--format", CONTAINER_LIST_FORMAT)),
106
+ ps_unhealthy=Operation(
107
+ (
108
+ runtime,
109
+ "ps",
110
+ "-a",
111
+ "--filter",
112
+ "health=unhealthy",
113
+ "--format",
114
+ "{{.Names}}\\t{{.Status}}",
115
+ )
116
+ ),
117
+ inspect=Operation(
118
+ (runtime, "inspect", "--format", INSPECT_FORMAT, "--", "$service")
119
+ ),
120
+ stats=Operation(
121
+ (
122
+ runtime,
123
+ "stats",
124
+ "--no-stream",
125
+ "--format",
126
+ STATS_FORMAT,
127
+ "--",
128
+ "$service",
129
+ )
130
+ ),
131
+ ports=Operation((runtime, "port", "--", "$service")),
132
+ health=Operation(
133
+ (runtime, "inspect", "--format", HEALTH_FORMAT, "--", "$service")
134
+ ),
135
+ restart=Operation(
136
+ (runtime, "restart", "--", "$service"),
137
+ read_only=False,
138
+ destructive=True,
139
+ ),
140
+ start=Operation((runtime, "start", "--", "$service"), read_only=False),
141
+ stop=Operation(
142
+ (runtime, "stop", "--", "$service"),
143
+ read_only=False,
144
+ destructive=True,
145
+ ),
146
+ # Compose commands are scoped to a service directory below
147
+ # services_path. The security layer validates the directory and the
148
+ # exact subcommand arguments.
149
+ compose_ps=Operation(
150
+ (
151
+ runtime,
152
+ "compose",
153
+ "--project-directory",
154
+ "$directory",
155
+ "ps",
156
+ "--format",
157
+ "{{.Name}} {{.Status}}",
158
+ )
159
+ ),
160
+ compose_pull=Operation(
161
+ (runtime, "compose", "--project-directory", "$directory", "pull"),
162
+ read_only=False,
163
+ ),
164
+ compose_up=Operation(
165
+ (
166
+ runtime,
167
+ "compose",
168
+ "--project-directory",
169
+ "$directory",
170
+ "up",
171
+ "-d",
172
+ "--remove-orphans",
173
+ ),
174
+ read_only=False,
175
+ ),
176
+ compose_down=Operation(
177
+ (
178
+ runtime,
179
+ "compose",
180
+ "--project-directory",
181
+ "$directory",
182
+ "down",
183
+ "--remove-orphans",
184
+ ),
185
+ read_only=False,
186
+ destructive=True,
187
+ ),
188
+ )
189
+
190
+
191
+ DEFAULT_CATALOG = build_container_catalog("docker")
192
+
193
+ ECHO_CONNECTION = Operation(("echo", "Connection successful"))
194
+ LS_SERVICES = Operation(("ls", "-1", "$path"))
195
+ # The trailing slash from -p distinguishes directories from plain files.
196
+ LS_SERVICE_DIRS = Operation(("ls", "-1", "-p", "$path"))
197
+ DOCKER_PS = DEFAULT_CATALOG.ps
198
+ DOCKER_PS_ALL = DEFAULT_CATALOG.ps_all
199
+ # Health status is only reported for containers with a healthcheck. Including
200
+ # stopped containers keeps a crashed or exited unhealthy service visible.
201
+ DOCKER_PS_UNHEALTHY = DEFAULT_CATALOG.ps_unhealthy
202
+ DOCKER_LOGS = DEFAULT_CATALOG.logs()
203
+ DOCKER_INSPECT = DEFAULT_CATALOG.inspect
204
+ DOCKER_STATS = DEFAULT_CATALOG.stats
205
+ DOCKER_PORTS = DEFAULT_CATALOG.ports
206
+ DOCKER_HEALTH = DEFAULT_CATALOG.health
207
+ DOCKER_RESTART = DEFAULT_CATALOG.restart
208
+ DOCKER_START = DEFAULT_CATALOG.start
209
+ DOCKER_STOP = DEFAULT_CATALOG.stop
210
+ COMPOSE_PS = DEFAULT_CATALOG.compose_ps
211
+ COMPOSE_PULL = DEFAULT_CATALOG.compose_pull
212
+ COMPOSE_UP = DEFAULT_CATALOG.compose_up
213
+ COMPOSE_DOWN = DEFAULT_CATALOG.compose_down
214
+
215
+ UPTIME = Operation(("uptime",))
216
+ FREE_MEMORY = Operation(("free", "-h"))
217
+ DISK_USAGE = Operation(("df", "-h", "$path"))
218
+
219
+ # Host infrastructure services are systemd units, not Docker containers. The
220
+ # unit name is validated against security.host_units before rendering.
221
+ SYSTEMCTL_STATUS = Operation(("systemctl", "status", "--no-pager", "--full", "$unit"))
222
+ JOURNALCTL_UNIT = Operation(
223
+ (
224
+ "journalctl",
225
+ "-u",
226
+ "$unit",
227
+ "-n",
228
+ "$lines",
229
+ "--no-pager",
230
+ "--output",
231
+ "short-iso",
232
+ )
233
+ )
234
+
235
+
236
+ # Backwards-compatible alias used by the server and its tests.
237
+ def docker_logs_operation(
238
+ *, since: str | None = None, until: str | None = None
239
+ ) -> Operation:
240
+ """Docker ``logs`` operation (kept for existing callers)."""
241
+ return DEFAULT_CATALOG.logs(since=since, until=until)
File without changes