docker-mcp-server 2.2.0__tar.gz → 2.2.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.2.0 → docker_mcp_server-2.2.1}/.github/copilot-instructions.md +2 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/CLAUDE.md +9 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/PKG-INFO +2 -2
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/manifest.json +1 -1
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/pyproject.toml +11 -2
- docker_mcp_server-2.2.1/tests/test_pyproject_pins.py +178 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/uv.lock +4 -2
- docker_mcp_server-2.2.0/tests/test_pyproject_pins.py +0 -93
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.claude/commands/docker-sdk.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.claude/settings.json +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.dockerignore +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/CODEOWNERS +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/actions/file-failure-issue/action.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/dependabot.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/release.yml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/canary.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/codeql.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/images.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/premerge.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/publish-homebrew.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/publish.yaml +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.gitignore +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.mcpbignore +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.python-version +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/CODE_OF_CONDUCT.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/CONTRIBUTING.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/DOCKERHUB.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/Dockerfile +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/LICENSE +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/MIGRATION-2.0.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/PRIVACY.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/README.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/SECURITY.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/assets/README.md +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/assets/icon.png +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/__init__.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/__main__.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/_env.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/_hosts.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/server.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/__init__.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_cli.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_labels.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_ssh_proxy.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_utils.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/buildx.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/compose.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/configs.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/containers.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/context.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/images.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/networks.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/nodes.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/plugins.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/prompts.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/registry.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/resources.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/scout.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/secrets.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/services.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/stack.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/swarm.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/system.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/volumes.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/glama.json +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/mcpb_run.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/scripts/build-mcpb.sh +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/scripts/docker-mcp-server.rb.tpl +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/server.json +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/__init__.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/conftest.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/__init__.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/conftest.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_buildx.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_cli.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_compose.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_containers.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_context.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_file_payloads.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_networks.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_nodes.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_registry.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_remote_exec.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_scout.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_services.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_smoke.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_stack.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_buildx.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_cli.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_compose.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_configs.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_containers.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_context.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_docs.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_env.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_hosts.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_images.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_labels.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_main.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_naming.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_networks.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_nodes.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_plugins.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_prompts.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_registry.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_remote_exec.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_resources.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_scout.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_secrets.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_server.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_services.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_ssh_proxy.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_stack.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_swarm.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_system.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_utils.py +0 -0
- {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_volumes.py +0 -0
|
@@ -271,3 +271,5 @@ Changing this line to a later version of cryptography can break installs on Inte
|
|
|
271
271
|
From version 49, cryptography no longer ships a universal2 macOS wheel; on Intel macOS the resolver typically falls back to a source build that requires a newer Rust toolchain and may fail.
|
|
272
272
|
Changing to a later version of cryptography for other platforms is fine, but this pin should remain for macOS x86_64.
|
|
273
273
|
Do not change this line. If a PR touches this line, please add a strong comment and do not allow the change to be merged without a strong justification and approval from the repo owner.
|
|
274
|
+
|
|
275
|
+
There is also a major-version cap: `"mcp>=1.27.1,<2"`. mcp 2.0.0 (2026-07-28) removed `mcp.server.fastmcp`, which `server.py` imports `FastMCP` from — that surface moved to `mcp.server.mcpserver`, so 2.x is an API migration, not a bump. Premerge CI cannot see this: it installs `--locked`, so the lockfile's 1.x keeps every test green while a *fresh* resolve breaks at import. That is not hypothetical — the published 2.2.0 shipped uncapped and `uvx docker-mcp-server` failed at import on a clean machine, which 2.2.1 superseded. `tests/test_pyproject_pins.py` enforces the cap and also asserts that the module `server.py` imports `FastMCP` from is one the installed mcp actually provides. Treat a PR lifting this cap the same way as the cryptography pin: it needs the `mcp.server.mcpserver` port, not just a version bump.
|
|
@@ -55,6 +55,15 @@ non-required `Check docs mirror` job flags a PR that edits `CLAUDE.md` or
|
|
|
55
55
|
`.github/copilot-instructions.md` without the other (see the MIRROR RULE above) — it's a prompt to
|
|
56
56
|
double-check, not a merge blocker.
|
|
57
57
|
|
|
58
|
+
**Two dependency caps exist because a fresh resolve can break what `--locked` CI cannot see**, both
|
|
59
|
+
enforced by `tests/test_pyproject_pins.py`: `cryptography<49` on Intel macOS (49 dropped the
|
|
60
|
+
universal2 wheel), and **`mcp<2`** — mcp 2.0.0 removed `mcp.server.fastmcp`, which `server.py` imports
|
|
61
|
+
`FastMCP` from (it moved to `mcp.server.mcpserver`), so 2.x needs a port rather than a bump. The
|
|
62
|
+
published 2.2.0 shipped uncapped and `uvx docker-mcp-server` was dead on arrival at import while every
|
|
63
|
+
CI job stayed green, because CI installs `--locked` against a lockfile pinning mcp 1.x; 2.2.1
|
|
64
|
+
superseded it. When adding a direct dependency whose *import surface* we touch, consider whether a
|
|
65
|
+
major-version cap plus a pin guard belongs with it.
|
|
66
|
+
|
|
58
67
|
A **weekly canary** (`.github/workflows/canary.yaml`, Mondays + dispatch) hunts platform/ecosystem
|
|
59
68
|
drift premerge CI can't see: wheels-only (`--only-binary :all:`) dependency resolution for Intel
|
|
60
69
|
macOS / ARM macOS / Windows against both the repo `pyproject.toml` and the latest published PyPI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docker-mcp-server
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: MCP server for managing Docker resources via the Docker SDK for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/L337-org/docker-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/L337-org/docker-mcp
|
|
@@ -22,7 +22,7 @@ Requires-Python: >=3.14
|
|
|
22
22
|
Requires-Dist: cryptography<49; platform_system == 'Darwin' and platform_machine == 'x86_64'
|
|
23
23
|
Requires-Dist: docker[ssh]>=7.1.0
|
|
24
24
|
Requires-Dist: httpx>=0.28.1
|
|
25
|
-
Requires-Dist: mcp
|
|
25
|
+
Requires-Dist: mcp<2,>=1.27.1
|
|
26
26
|
Requires-Dist: requests>=2.26.0
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
@@ -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.2.
|
|
5
|
+
"version": "2.2.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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docker-mcp-server"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
description = "MCP server for managing Docker resources via the Docker SDK for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -24,7 +24,13 @@ dependencies = [
|
|
|
24
24
|
# system `ssh` binary, so it works identically on the host and inside the container images.
|
|
25
25
|
"docker[ssh]>=7.1.0",
|
|
26
26
|
"httpx>=0.28.1",
|
|
27
|
-
|
|
27
|
+
# Capped below 2: mcp 2.0.0 (2026-07-28) removed `mcp.server.fastmcp`, which `server.py`
|
|
28
|
+
# imports FastMCP from — that surface moved to `mcp.server.mcpserver`, so 2.x is an API
|
|
29
|
+
# migration rather than a bump. Without the cap a fresh resolve picks 2.x and `import
|
|
30
|
+
# docker_mcp` dies, which is exactly what happened to the published 2.2.0 (CI installs
|
|
31
|
+
# `--locked`, so the lockfile's 1.x kept every test green while `uvx docker-mcp-server`
|
|
32
|
+
# was dead on arrival). `tests/test_pyproject_pins.py` fails if this cap is lifted.
|
|
33
|
+
"mcp>=1.27.1,<2",
|
|
28
34
|
# Pulled in transitively by docker, but we import requests.exceptions directly
|
|
29
35
|
# (container_wait catches ReadTimeout), so declare it explicitly.
|
|
30
36
|
"requests>=2.26.0",
|
|
@@ -89,6 +95,9 @@ addopts = "-m 'not integration'"
|
|
|
89
95
|
|
|
90
96
|
[dependency-groups]
|
|
91
97
|
dev = [
|
|
98
|
+
# Pulled in transitively by pytest, but tests/test_pyproject_pins.py parses PEP 508 requirement
|
|
99
|
+
# strings with it directly, so declare it — the same reasoning as `requests` above.
|
|
100
|
+
"packaging>=24.0",
|
|
92
101
|
"pytest>=9.0.3",
|
|
93
102
|
"pyright>=1.1.409",
|
|
94
103
|
"ruff>=0.15.12",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from packaging.requirements import Requirement
|
|
8
|
+
|
|
9
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
+
PYPROJECT = _ROOT / "pyproject.toml"
|
|
11
|
+
MANIFEST = _ROOT / "manifest.json"
|
|
12
|
+
UV_LOCK = _ROOT / "uv.lock"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _version_tuple(version: str, length: int = 4) -> tuple[int, ...]:
|
|
16
|
+
parts = [int(p) for p in version.split(".")]
|
|
17
|
+
parts += [0] * (length - len(parts))
|
|
18
|
+
return tuple(parts[:length])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _dependency_name(requirement: str) -> str:
|
|
22
|
+
match = re.match(r"[A-Za-z0-9][A-Za-z0-9._-]*", requirement)
|
|
23
|
+
assert match, f"could not parse a dependency name from {requirement!r}"
|
|
24
|
+
return match.group(0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _admits(requirement: str, version: str) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Whether a requirement's specifiers allow `version` to be installed.
|
|
30
|
+
|
|
31
|
+
This is the question the caps actually pose, and asking it directly avoids two mistakes a bound
|
|
32
|
+
comparison makes. Ordering: PEP 508 does not fix specifier order, so `mcp<2,>=1.27.1` must read the
|
|
33
|
+
same as `mcp>=1.27.1,<2`. Strictness: `<=2` *admits* 2.0.0 — the very release that cannot import —
|
|
34
|
+
yet compares equal to a `<2` bound, so a guard built on bounds passes it. `packaging` answers the
|
|
35
|
+
real question and handles markers, extras, `!=` and `~=` for free.
|
|
36
|
+
|
|
37
|
+
args:
|
|
38
|
+
requirement - a PEP 508 requirement string from pyproject's dependency list
|
|
39
|
+
version - the version to test, normally the first known-bad one
|
|
40
|
+
returns: bool - True if that version satisfies the requirement
|
|
41
|
+
"""
|
|
42
|
+
return Requirement(requirement).specifier.contains(version)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_intel_macos_cryptography_pin_not_relaxed():
|
|
46
|
+
"""
|
|
47
|
+
Dependabot doesn't get an automatic Copilot review (bots aren't billable for
|
|
48
|
+
premium requests), so a bump here must be caught by a hard, deterministic
|
|
49
|
+
CI failure instead of relying on review. See the pyproject.toml comment:
|
|
50
|
+
cryptography>=49 dropped the macosx_10_9_universal2 wheel, breaking `uvx`
|
|
51
|
+
installs on Intel macOS.
|
|
52
|
+
"""
|
|
53
|
+
data = tomllib.loads(PYPROJECT.read_text())
|
|
54
|
+
deps = data["project"]["dependencies"]
|
|
55
|
+
|
|
56
|
+
cryptography_deps = [d for d in deps if _dependency_name(d) == "cryptography"]
|
|
57
|
+
assert cryptography_deps, "no direct 'cryptography' dependency found in pyproject.toml"
|
|
58
|
+
|
|
59
|
+
intel_macos_deps = [
|
|
60
|
+
d for d in cryptography_deps if "platform_system == 'Darwin'" in d and "platform_machine == 'x86_64'" in d
|
|
61
|
+
]
|
|
62
|
+
assert intel_macos_deps, (
|
|
63
|
+
"the Intel-macOS cryptography pin is missing: no 'cryptography' dependency is scoped to "
|
|
64
|
+
f"platform_system == 'Darwin' and platform_machine == 'x86_64'; found: {cryptography_deps!r}"
|
|
65
|
+
)
|
|
66
|
+
assert len(intel_macos_deps) == 1, f"expected exactly one Intel-macOS cryptography pin, found: {intel_macos_deps!r}"
|
|
67
|
+
|
|
68
|
+
dep = intel_macos_deps[0]
|
|
69
|
+
assert not _admits(dep, "49.0"), (
|
|
70
|
+
f"the Intel-macOS cryptography pin {dep!r} allows 49.0, which has no x86_64 macOS wheel — the "
|
|
71
|
+
"resolver then falls back to a source build needing a newer Rust toolchain than many users have. "
|
|
72
|
+
"See the pyproject.toml comment before lifting this cap."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_mcp_major_cap_not_relaxed():
|
|
77
|
+
"""
|
|
78
|
+
mcp 2.0.0 removed `mcp.server.fastmcp`, which `server.py` imports FastMCP from, so an
|
|
79
|
+
uncapped `mcp` resolves to a release that cannot import this package. That is not
|
|
80
|
+
hypothetical: the published 2.2.0 shipped uncapped and `uvx docker-mcp-server` failed at
|
|
81
|
+
import on a clean machine, while CI stayed green because it installs `--locked` against a
|
|
82
|
+
lockfile pinning 1.x. Lifting this cap needs the `mcp.server.mcpserver` migration first.
|
|
83
|
+
"""
|
|
84
|
+
data = tomllib.loads(PYPROJECT.read_text())
|
|
85
|
+
deps = data["project"]["dependencies"]
|
|
86
|
+
|
|
87
|
+
mcp_deps = [d for d in deps if _dependency_name(d) == "mcp"]
|
|
88
|
+
assert mcp_deps, "no direct 'mcp' dependency found in pyproject.toml"
|
|
89
|
+
assert len(mcp_deps) == 1, f"expected exactly one 'mcp' dependency, found: {mcp_deps!r}"
|
|
90
|
+
|
|
91
|
+
dep = mcp_deps[0]
|
|
92
|
+
assert not _admits(dep, "2.0.0"), (
|
|
93
|
+
f"the mcp dependency {dep!r} allows 2.0.0, which removed `mcp.server.fastmcp` — `import "
|
|
94
|
+
"docker_mcp` then fails outright. Port `server.py` to `mcp.server.mcpserver` before lifting "
|
|
95
|
+
"this cap; note `<=2` allows 2.0.0 and so is not a cap."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_the_declared_mcp_bound_matches_what_the_code_imports():
|
|
100
|
+
"""
|
|
101
|
+
The cap and the import have to agree, and only one of them is in pyproject: assert the
|
|
102
|
+
module we import FastMCP from is the one the *locked* mcp actually provides. Catches a
|
|
103
|
+
lockfile that drifts past the cap as well as a cap raised without porting the import.
|
|
104
|
+
"""
|
|
105
|
+
import importlib.util
|
|
106
|
+
|
|
107
|
+
from docker_mcp import server as server_module
|
|
108
|
+
|
|
109
|
+
source = Path(server_module.__file__).read_text(encoding="utf-8")
|
|
110
|
+
match = re.search(r"from (mcp[\w.]*) import FastMCP", source)
|
|
111
|
+
assert match, "server.py no longer imports FastMCP from an `mcp.*` module — update this guard"
|
|
112
|
+
assert importlib.util.find_spec(match.group(1)) is not None, (
|
|
113
|
+
f"server.py imports FastMCP from {match.group(1)!r}, which the installed mcp does not "
|
|
114
|
+
"provide — the pyproject cap and the code disagree"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# The release pipeline's preflight job re-asserts these against the release tag; the tests
|
|
119
|
+
# below catch the drift earlier, at PR time. server.json is intentionally NOT checked — its
|
|
120
|
+
# committed version is stale by design and stamped from the tag at release time.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_manifest_version_matches_pyproject():
|
|
124
|
+
"""
|
|
125
|
+
manifest.json (the MCPB bundle manifest) is documented as kept in step with
|
|
126
|
+
pyproject.toml; the publish workflow restamps it from the tag, but drift in the repo
|
|
127
|
+
still confuses local bundle builds (scripts/build-mcpb.sh only warns).
|
|
128
|
+
"""
|
|
129
|
+
pyproject_version = tomllib.loads(PYPROJECT.read_text())["project"]["version"]
|
|
130
|
+
manifest_version = json.loads(MANIFEST.read_text())["version"]
|
|
131
|
+
assert manifest_version == pyproject_version, (
|
|
132
|
+
f"manifest.json version {manifest_version!r} != pyproject.toml version {pyproject_version!r} — "
|
|
133
|
+
"bump them together"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_uv_lock_self_version_matches_pyproject():
|
|
138
|
+
"""
|
|
139
|
+
Catches "bumped pyproject.toml, forgot `uv lock`": the lockfile embeds this package's
|
|
140
|
+
own version, and a stale entry ships a lockfile that disagrees with the metadata.
|
|
141
|
+
"""
|
|
142
|
+
pyproject_version = tomllib.loads(PYPROJECT.read_text())["project"]["version"]
|
|
143
|
+
packages = tomllib.loads(UV_LOCK.read_text())["package"]
|
|
144
|
+
self_entries = [
|
|
145
|
+
p for p in packages if p["name"] == "docker-mcp-server" and p.get("source", {}).get("editable") == "."
|
|
146
|
+
]
|
|
147
|
+
assert len(self_entries) == 1, f"expected exactly one editable self-entry in uv.lock, found {len(self_entries)}"
|
|
148
|
+
lock_version = self_entries[0]["version"]
|
|
149
|
+
assert lock_version == pyproject_version, (
|
|
150
|
+
f"uv.lock self-entry version {lock_version!r} != pyproject.toml version {pyproject_version!r} — "
|
|
151
|
+
"run `uv lock` after bumping the version"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.parametrize(
|
|
156
|
+
("requirement", "admits_two"),
|
|
157
|
+
[
|
|
158
|
+
# Specifier order is not guaranteed; these three declare the same real cap.
|
|
159
|
+
("mcp>=1.27.1,<2", False),
|
|
160
|
+
("mcp<2,>=1.27.1", False),
|
|
161
|
+
("mcp<2", False),
|
|
162
|
+
# `<=2` reads like a cap and is not one: it admits 2.0.0, the release that cannot import.
|
|
163
|
+
("mcp<=2", True),
|
|
164
|
+
("mcp<=2.0.0", True),
|
|
165
|
+
# Uncapped, and a cap above the bad version — both states the guard must reject.
|
|
166
|
+
("mcp>=1.27.1", True),
|
|
167
|
+
("mcp>=1.27.1,<3", True),
|
|
168
|
+
# Markers and extras are the parser's problem, not the guard's.
|
|
169
|
+
("mcp<2; python_version >= '3.14'", False),
|
|
170
|
+
("mcp[ws]<2", False),
|
|
171
|
+
],
|
|
172
|
+
)
|
|
173
|
+
def test_admits_answers_the_question_the_caps_actually_pose(requirement, admits_two):
|
|
174
|
+
"""
|
|
175
|
+
A guard is only as good as its predicate. `<=2` is the case a bound comparison gets wrong, and a
|
|
176
|
+
guard that cries wolf on a reordered specifier gets disabled — so both are pinned here.
|
|
177
|
+
"""
|
|
178
|
+
assert _admits(requirement, "2.0.0") is admits_two
|
|
@@ -282,7 +282,7 @@ ssh = [
|
|
|
282
282
|
|
|
283
283
|
[[package]]
|
|
284
284
|
name = "docker-mcp-server"
|
|
285
|
-
version = "2.2.
|
|
285
|
+
version = "2.2.1"
|
|
286
286
|
source = { editable = "." }
|
|
287
287
|
dependencies = [
|
|
288
288
|
{ name = "cryptography", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" },
|
|
@@ -294,6 +294,7 @@ dependencies = [
|
|
|
294
294
|
|
|
295
295
|
[package.dev-dependencies]
|
|
296
296
|
dev = [
|
|
297
|
+
{ name = "packaging" },
|
|
297
298
|
{ name = "pre-commit" },
|
|
298
299
|
{ name = "pyright" },
|
|
299
300
|
{ name = "pytest" },
|
|
@@ -305,12 +306,13 @@ requires-dist = [
|
|
|
305
306
|
{ name = "cryptography", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "<49" },
|
|
306
307
|
{ name = "docker", extras = ["ssh"], specifier = ">=7.1.0" },
|
|
307
308
|
{ name = "httpx", specifier = ">=0.28.1" },
|
|
308
|
-
{ name = "mcp", specifier = ">=1.27.1" },
|
|
309
|
+
{ name = "mcp", specifier = ">=1.27.1,<2" },
|
|
309
310
|
{ name = "requests", specifier = ">=2.26.0" },
|
|
310
311
|
]
|
|
311
312
|
|
|
312
313
|
[package.metadata.requires-dev]
|
|
313
314
|
dev = [
|
|
315
|
+
{ name = "packaging", specifier = ">=24.0" },
|
|
314
316
|
{ name = "pre-commit", specifier = ">=4.6.0" },
|
|
315
317
|
{ name = "pyright", specifier = ">=1.1.409" },
|
|
316
318
|
{ name = "pytest", specifier = ">=9.0.3" },
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import re
|
|
3
|
-
import tomllib
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
_ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
-
PYPROJECT = _ROOT / "pyproject.toml"
|
|
8
|
-
MANIFEST = _ROOT / "manifest.json"
|
|
9
|
-
UV_LOCK = _ROOT / "uv.lock"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _version_tuple(version: str, length: int = 4) -> tuple[int, ...]:
|
|
13
|
-
parts = [int(p) for p in version.split(".")]
|
|
14
|
-
parts += [0] * (length - len(parts))
|
|
15
|
-
return tuple(parts[:length])
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _dependency_name(requirement: str) -> str:
|
|
19
|
-
match = re.match(r"[A-Za-z0-9][A-Za-z0-9._-]*", requirement)
|
|
20
|
-
assert match, f"could not parse a dependency name from {requirement!r}"
|
|
21
|
-
return match.group(0)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_intel_macos_cryptography_pin_not_relaxed():
|
|
25
|
-
"""
|
|
26
|
-
Dependabot doesn't get an automatic Copilot review (bots aren't billable for
|
|
27
|
-
premium requests), so a bump here must be caught by a hard, deterministic
|
|
28
|
-
CI failure instead of relying on review. See the pyproject.toml comment:
|
|
29
|
-
cryptography>=49 dropped the macosx_10_9_universal2 wheel, breaking `uvx`
|
|
30
|
-
installs on Intel macOS.
|
|
31
|
-
"""
|
|
32
|
-
data = tomllib.loads(PYPROJECT.read_text())
|
|
33
|
-
deps = data["project"]["dependencies"]
|
|
34
|
-
|
|
35
|
-
cryptography_deps = [d for d in deps if _dependency_name(d) == "cryptography"]
|
|
36
|
-
assert cryptography_deps, "no direct 'cryptography' dependency found in pyproject.toml"
|
|
37
|
-
|
|
38
|
-
intel_macos_deps = [
|
|
39
|
-
d for d in cryptography_deps if "platform_system == 'Darwin'" in d and "platform_machine == 'x86_64'" in d
|
|
40
|
-
]
|
|
41
|
-
assert intel_macos_deps, (
|
|
42
|
-
"the Intel-macOS cryptography pin is missing: no 'cryptography' dependency is scoped to "
|
|
43
|
-
f"platform_system == 'Darwin' and platform_machine == 'x86_64'; found: {cryptography_deps!r}"
|
|
44
|
-
)
|
|
45
|
-
assert len(intel_macos_deps) == 1, f"expected exactly one Intel-macOS cryptography pin, found: {intel_macos_deps!r}"
|
|
46
|
-
|
|
47
|
-
dep = intel_macos_deps[0]
|
|
48
|
-
m = re.search(r"cryptography\s*<\s*([0-9]+(?:\.[0-9]+)*)", dep)
|
|
49
|
-
assert m, f"cryptography pin must use a '<N[.N...]' upper bound, got: {dep!r}"
|
|
50
|
-
|
|
51
|
-
bound = _version_tuple(m.group(1))
|
|
52
|
-
max_allowed = _version_tuple("49")
|
|
53
|
-
assert bound <= max_allowed, (
|
|
54
|
-
f"cryptography upper bound raised to {m.group(1)} — 49.x has no x86_64 macOS wheel; "
|
|
55
|
-
"see the pyproject.toml comment before lifting this cap"
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# The release pipeline's preflight job re-asserts these against the release tag; the tests
|
|
60
|
-
# below catch the drift earlier, at PR time. server.json is intentionally NOT checked — its
|
|
61
|
-
# committed version is stale by design and stamped from the tag at release time.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_manifest_version_matches_pyproject():
|
|
65
|
-
"""
|
|
66
|
-
manifest.json (the MCPB bundle manifest) is documented as kept in step with
|
|
67
|
-
pyproject.toml; the publish workflow restamps it from the tag, but drift in the repo
|
|
68
|
-
still confuses local bundle builds (scripts/build-mcpb.sh only warns).
|
|
69
|
-
"""
|
|
70
|
-
pyproject_version = tomllib.loads(PYPROJECT.read_text())["project"]["version"]
|
|
71
|
-
manifest_version = json.loads(MANIFEST.read_text())["version"]
|
|
72
|
-
assert manifest_version == pyproject_version, (
|
|
73
|
-
f"manifest.json version {manifest_version!r} != pyproject.toml version {pyproject_version!r} — "
|
|
74
|
-
"bump them together"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_uv_lock_self_version_matches_pyproject():
|
|
79
|
-
"""
|
|
80
|
-
Catches "bumped pyproject.toml, forgot `uv lock`": the lockfile embeds this package's
|
|
81
|
-
own version, and a stale entry ships a lockfile that disagrees with the metadata.
|
|
82
|
-
"""
|
|
83
|
-
pyproject_version = tomllib.loads(PYPROJECT.read_text())["project"]["version"]
|
|
84
|
-
packages = tomllib.loads(UV_LOCK.read_text())["package"]
|
|
85
|
-
self_entries = [
|
|
86
|
-
p for p in packages if p["name"] == "docker-mcp-server" and p.get("source", {}).get("editable") == "."
|
|
87
|
-
]
|
|
88
|
-
assert len(self_entries) == 1, f"expected exactly one editable self-entry in uv.lock, found {len(self_entries)}"
|
|
89
|
-
lock_version = self_entries[0]["version"]
|
|
90
|
-
assert lock_version == pyproject_version, (
|
|
91
|
-
f"uv.lock self-entry version {lock_version!r} != pyproject.toml version {pyproject_version!r} — "
|
|
92
|
-
"run `uv lock` after bumping the version"
|
|
93
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
{docker_mcp_server-2.2.0 → docker_mcp_server-2.2.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
|
|
File without changes
|
|
File without changes
|