docker-mcp-server 2.1.0__tar.gz → 2.1.1__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.
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/PKG-INFO +1 -1
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/system.py +44 -4
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/manifest.json +1 -1
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/pyproject.toml +1 -1
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_system.py +72 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/uv.lock +1 -1
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.claude/commands/docker-sdk.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.claude/settings.json +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.dockerignore +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/CODEOWNERS +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/actions/file-failure-issue/action.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/copilot-instructions.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/dependabot.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/release.yml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/workflows/canary.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/workflows/codeql.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/workflows/images.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/workflows/premerge.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/workflows/publish-homebrew.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/workflows/publish.yaml +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.gitignore +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.mcpbignore +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.python-version +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/CLAUDE.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/CODE_OF_CONDUCT.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/CONTRIBUTING.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/DOCKERHUB.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/Dockerfile +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/LICENSE +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/MIGRATION-2.0.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/PRIVACY.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/README.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/SECURITY.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/assets/README.md +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/assets/icon.png +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/__init__.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/__main__.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/_env.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/_hosts.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/server.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/__init__.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/_cli.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/_labels.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/_ssh_proxy.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/_utils.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/buildx.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/compose.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/configs.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/containers.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/context.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/images.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/networks.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/nodes.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/plugins.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/prompts.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/registry.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/resources.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/scout.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/secrets.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/services.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/stack.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/swarm.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/docker_mcp/tools/volumes.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/glama.json +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/mcpb_run.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/scripts/build-mcpb.sh +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/scripts/docker-mcp-server.rb.tpl +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/server.json +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/__init__.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/conftest.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/__init__.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/conftest.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_buildx.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_cli.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_compose.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_containers.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_context.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_file_payloads.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_networks.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_nodes.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_registry.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_scout.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_services.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_smoke.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/integration/test_stack.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_buildx.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_cli.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_compose.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_configs.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_containers.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_context.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_env.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_hosts.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_images.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_labels.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_main.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_naming.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_networks.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_nodes.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_plugins.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_prompts.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_pyproject_pins.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_registry.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_resources.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_scout.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_secrets.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_server.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_services.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_ssh_proxy.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_stack.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_swarm.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_utils.py +0 -0
- {docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/tests/test_volumes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docker-mcp-server
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: MCP server for managing Docker resources via the Docker SDK for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/GavinLucas/docker-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/GavinLucas/docker-mcp
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
import threading
|
|
6
|
+
import urllib.parse
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import docker
|
|
@@ -20,6 +21,7 @@ from docker_mcp._hosts import (
|
|
|
20
21
|
resolve_auto,
|
|
21
22
|
)
|
|
22
23
|
from docker_mcp.server import tool
|
|
24
|
+
from docker_mcp.tools._ssh_proxy import parse_ssh_url
|
|
23
25
|
from docker_mcp.tools._utils import classify_host_kernel, close_stream_quietly, env_flag, in_container
|
|
24
26
|
|
|
25
27
|
# One lazily-built docker-py client per configured host label (the pool). FastMCP runs sync tools
|
|
@@ -114,13 +116,52 @@ def _close_client_quietly(client: docker.DockerClient) -> None:
|
|
|
114
116
|
pass
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
def _ensure_ssh_port(url: str) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Work around a docker-py bug for an `ssh://` URL with no explicit port: `docker.utils.parse_host()`
|
|
122
|
+
hardcodes port 22 into the URL *before* `SSHHTTPAdapter._create_paramiko_client` ever runs, so that
|
|
123
|
+
adapter's own `~/.ssh/config` `Port` fallback (which only fires while the port is still unset) never
|
|
124
|
+
triggers — a non-22 `Port` in `~/.ssh/config` is silently ignored. We splice in the configured port
|
|
125
|
+
ourselves first, reusing the exact `~/.ssh/config` lookup `_ssh_proxy.parse_ssh_url` already does for
|
|
126
|
+
the CLI-backed tools, so both tool families honor a non-default SSH port the same way.
|
|
127
|
+
|
|
128
|
+
args: url: str - a DOCKER_HOST/host URL; only `ssh://` URLs with no explicit port are affected
|
|
129
|
+
returns: str - `url` unchanged, or with the `~/.ssh/config` port spliced into the netloc
|
|
130
|
+
"""
|
|
131
|
+
if not url.startswith("ssh://"):
|
|
132
|
+
return url
|
|
133
|
+
parsed = urllib.parse.urlparse(url)
|
|
134
|
+
try:
|
|
135
|
+
has_port = parsed.port is not None
|
|
136
|
+
except ValueError:
|
|
137
|
+
# A malformed port (e.g. "ssh://host:abc") — leave url untouched so docker-py's own
|
|
138
|
+
# validation raises its clearer error instead of this helper failing first.
|
|
139
|
+
return url
|
|
140
|
+
if has_port:
|
|
141
|
+
return url
|
|
142
|
+
try:
|
|
143
|
+
# parse_ssh_url raises ValueError on a missing hostname (e.g. "ssh://" or "ssh://@") or a
|
|
144
|
+
# non-integer `Port` in ~/.ssh/config — leave url untouched in either case rather than
|
|
145
|
+
# raising a bare ValueError here instead of the connection error docker-py would produce.
|
|
146
|
+
target = parse_ssh_url(url)
|
|
147
|
+
except ValueError:
|
|
148
|
+
return url
|
|
149
|
+
if target.port is None:
|
|
150
|
+
return url
|
|
151
|
+
return urllib.parse.urlunparse(parsed._replace(netloc=f"{parsed.netloc}:{target.port}"))
|
|
152
|
+
|
|
153
|
+
|
|
117
154
|
def _build_default_client() -> docker.DockerClient:
|
|
118
155
|
"""Client for DOCKER_HOST when set (via from_env, honoring its TLS env), else the resolved endpoint.
|
|
119
156
|
|
|
120
157
|
auto/local resolution lives in docker_mcp._hosts now (the pure registry layer); resolve_auto() is
|
|
121
158
|
the relocated _resolve_default_base_url().
|
|
122
159
|
"""
|
|
123
|
-
|
|
160
|
+
docker_host = os.environ.get("DOCKER_HOST")
|
|
161
|
+
if docker_host:
|
|
162
|
+
fixed = _ensure_ssh_port(docker_host)
|
|
163
|
+
if fixed != docker_host:
|
|
164
|
+
return docker.from_env(environment={**os.environ, "DOCKER_HOST": fixed})
|
|
124
165
|
return docker.from_env()
|
|
125
166
|
base_url = resolve_auto()
|
|
126
167
|
return docker.DockerClient(base_url=base_url) if base_url else docker.from_env()
|
|
@@ -164,9 +205,8 @@ def _build_client(host: Host) -> docker.DockerClient:
|
|
|
164
205
|
tls = _tls_config_for(host)
|
|
165
206
|
if host.url is None:
|
|
166
207
|
return docker.DockerClient(tls=tls) if tls is not None else docker.DockerClient()
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
208
|
+
url = _ensure_ssh_port(host.url)
|
|
209
|
+
return docker.DockerClient(base_url=url, tls=tls) if tls is not None else docker.DockerClient(base_url=url)
|
|
170
210
|
|
|
171
211
|
|
|
172
212
|
def _get_client(host: str | None = None) -> docker.DockerClient:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.4",
|
|
3
3
|
"name": "docker-mcp-server",
|
|
4
4
|
"display_name": "Docker MCP Server",
|
|
5
|
-
"version": "2.1.
|
|
5
|
+
"version": "2.1.1",
|
|
6
6
|
"description": "Manage Docker through the Docker SDK for Python and the docker CLI: containers, images, networks, volumes, Compose, Swarm, Buildx, Scout, and OCI registries, exposed as MCP tools.",
|
|
7
7
|
"long_description": "Manage one or more Docker daemons through your AI agent — containers, images, networks, volumes, Compose, Swarm, Buildx, Scout, and OCI registries, exposed as MCP tools.\n\nDiscovery is automatic for your local socket with no configuration required. Or point it at additional remote daemons over TCP, TLS, or SSH (and mark any of them read-only to monitor without the risk of accidental changes). It also surfaces logs and stats as resources for triage. Read-only / no-destructive / disabled-domain switches let you register a restricted tool surface.\n\nAnswer questions like: ‘why did my local container crash?’, ‘is my production system under memory pressure?’, ‘what differences are there between my staging and production systems?’\n\nRuns entirely on your machine and sends no telemetry — see the Privacy Policy.",
|
|
8
8
|
"author": {
|
|
@@ -571,3 +571,75 @@ def test_build_client_platform_default_ignores_ambient_docker_host(monkeypatch):
|
|
|
571
571
|
assert system_module._build_client(Host("local", None)) is sentinel
|
|
572
572
|
ctor.assert_called_once_with() # no base_url -> platform socket/npipe; ambient DOCKER_HOST ignored
|
|
573
573
|
from_env.assert_not_called()
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# ---------- _ensure_ssh_port: work around docker-py hardcoding port 22 for a bare ssh:// URL ----------
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def test_ensure_ssh_port_ignores_non_ssh_url():
|
|
580
|
+
assert system_module._ensure_ssh_port("tcp://prod:2376") == "tcp://prod:2376"
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def test_ensure_ssh_port_leaves_explicit_port_alone(monkeypatch):
|
|
584
|
+
# An explicit port must win even if ~/.ssh/config also has one — no lookup should even be needed.
|
|
585
|
+
monkeypatch.setattr(system_module, "parse_ssh_url", MagicMock(side_effect=AssertionError("should not be called")))
|
|
586
|
+
assert system_module._ensure_ssh_port("ssh://bob@example.com:1234") == "ssh://bob@example.com:1234"
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def test_ensure_ssh_port_leaves_malformed_port_for_docker_py_to_reject(monkeypatch):
|
|
590
|
+
# A non-numeric port makes urlparse's .port property raise ValueError — must not propagate out of
|
|
591
|
+
# this helper; leave the url untouched so docker-py's own validation is what surfaces the error.
|
|
592
|
+
monkeypatch.setattr(system_module, "parse_ssh_url", MagicMock(side_effect=AssertionError("should not be called")))
|
|
593
|
+
assert system_module._ensure_ssh_port("ssh://bob@example.com:abc") == "ssh://bob@example.com:abc"
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_ensure_ssh_port_leaves_hostless_url_for_docker_py_to_reject(monkeypatch):
|
|
597
|
+
# No hostname (e.g. "ssh://" or "ssh://@") makes the real parse_ssh_url raise ValueError — must
|
|
598
|
+
# not propagate out of this helper; leave docker-py's own validation to reject the malformed url.
|
|
599
|
+
monkeypatch.setattr("os.path.exists", lambda _path: False) # no ~/.ssh/config lookup needed
|
|
600
|
+
assert system_module._ensure_ssh_port("ssh://") == "ssh://"
|
|
601
|
+
assert system_module._ensure_ssh_port("ssh://@") == "ssh://@"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def test_ensure_ssh_port_leaves_url_alone_when_ssh_config_port_is_malformed(tmp_path, monkeypatch):
|
|
605
|
+
# A non-integer `Port` in ~/.ssh/config makes parse_ssh_url raise ValueError (int() failure) —
|
|
606
|
+
# must not propagate out of this helper; leave docker-py's own validation to surface the error.
|
|
607
|
+
config = tmp_path / "config"
|
|
608
|
+
config.write_text("Host example.com\n Port notanumber\n")
|
|
609
|
+
monkeypatch.setattr("os.path.expanduser", lambda p: str(config) if p == "~/.ssh/config" else p)
|
|
610
|
+
assert system_module._ensure_ssh_port("ssh://bob@example.com") == "ssh://bob@example.com"
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def test_ensure_ssh_port_splices_in_ssh_config_port(tmp_path, monkeypatch):
|
|
614
|
+
config = tmp_path / "config"
|
|
615
|
+
config.write_text("Host example.com\n Port 1234\n")
|
|
616
|
+
monkeypatch.setattr("os.path.expanduser", lambda p: str(config) if p == "~/.ssh/config" else p)
|
|
617
|
+
assert system_module._ensure_ssh_port("ssh://bob@example.com") == "ssh://bob@example.com:1234"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_ensure_ssh_port_unchanged_when_config_has_no_port(monkeypatch):
|
|
621
|
+
monkeypatch.setattr("os.path.exists", lambda _path: False) # no ~/.ssh/config at all
|
|
622
|
+
assert system_module._ensure_ssh_port("ssh://bob@example.com") == "ssh://bob@example.com"
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def test_build_default_client_splices_ssh_config_port_into_docker_host(tmp_path, monkeypatch):
|
|
626
|
+
config = tmp_path / "config"
|
|
627
|
+
config.write_text("Host example.com\n Port 1234\n")
|
|
628
|
+
monkeypatch.setattr("os.path.expanduser", lambda p: str(config) if p == "~/.ssh/config" else p)
|
|
629
|
+
monkeypatch.setenv("DOCKER_HOST", "ssh://bob@example.com")
|
|
630
|
+
sentinel = MagicMock()
|
|
631
|
+
with patch("docker_mcp.tools.system.docker.from_env", return_value=sentinel) as from_env:
|
|
632
|
+
assert system_module._build_default_client() is sentinel
|
|
633
|
+
assert from_env.call_args.kwargs["environment"]["DOCKER_HOST"] == "ssh://bob@example.com:1234"
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def test_build_client_splices_ssh_config_port_for_multi_host_ssh_entry(tmp_path, monkeypatch):
|
|
637
|
+
_set_multi(monkeypatch)
|
|
638
|
+
config = tmp_path / "config"
|
|
639
|
+
config.write_text("Host example.com\n Port 1234\n")
|
|
640
|
+
monkeypatch.setattr("os.path.expanduser", lambda p: str(config) if p == "~/.ssh/config" else p)
|
|
641
|
+
host = Host("remote", "ssh://bob@example.com")
|
|
642
|
+
sentinel = MagicMock()
|
|
643
|
+
with patch("docker_mcp.tools.system.docker.DockerClient", return_value=sentinel) as ctor:
|
|
644
|
+
assert system_module._build_client(host) is sentinel
|
|
645
|
+
ctor.assert_called_once_with(base_url="ssh://bob@example.com:1234")
|
|
@@ -282,7 +282,7 @@ ssh = [
|
|
|
282
282
|
|
|
283
283
|
[[package]]
|
|
284
284
|
name = "docker-mcp-server"
|
|
285
|
-
version = "2.1.
|
|
285
|
+
version = "2.1.1"
|
|
286
286
|
source = { editable = "." }
|
|
287
287
|
dependencies = [
|
|
288
288
|
{ name = "cryptography", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
{docker_mcp_server-2.1.0 → docker_mcp_server-2.1.1}/.github/actions/file-failure-issue/action.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|