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.
Files changed (120) hide show
  1. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/copilot-instructions.md +2 -0
  2. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/CLAUDE.md +9 -0
  3. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/PKG-INFO +2 -2
  4. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/manifest.json +1 -1
  5. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/pyproject.toml +11 -2
  6. docker_mcp_server-2.2.1/tests/test_pyproject_pins.py +178 -0
  7. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/uv.lock +4 -2
  8. docker_mcp_server-2.2.0/tests/test_pyproject_pins.py +0 -93
  9. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.claude/commands/docker-sdk.md +0 -0
  10. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.claude/settings.json +0 -0
  11. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.dockerignore +0 -0
  12. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/CODEOWNERS +0 -0
  13. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/actions/file-failure-issue/action.yaml +0 -0
  17. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/dependabot.yaml +0 -0
  18. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/release.yml +0 -0
  19. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/canary.yaml +0 -0
  20. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/codeql.yaml +0 -0
  21. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/images.yaml +0 -0
  22. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/premerge.yaml +0 -0
  23. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/publish-homebrew.yaml +0 -0
  24. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.github/workflows/publish.yaml +0 -0
  25. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.gitignore +0 -0
  26. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.mcpbignore +0 -0
  27. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/.python-version +0 -0
  28. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/CODE_OF_CONDUCT.md +0 -0
  29. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/CONTRIBUTING.md +0 -0
  30. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/DOCKERHUB.md +0 -0
  31. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/Dockerfile +0 -0
  32. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/LICENSE +0 -0
  33. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/MIGRATION-2.0.md +0 -0
  34. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/PRIVACY.md +0 -0
  35. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/README.md +0 -0
  36. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/SECURITY.md +0 -0
  37. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/assets/README.md +0 -0
  38. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/assets/icon.png +0 -0
  39. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/__init__.py +0 -0
  40. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/__main__.py +0 -0
  41. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/_env.py +0 -0
  42. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/_hosts.py +0 -0
  43. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/server.py +0 -0
  44. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/__init__.py +0 -0
  45. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_cli.py +0 -0
  46. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_labels.py +0 -0
  47. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_ssh_proxy.py +0 -0
  48. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/_utils.py +0 -0
  49. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/buildx.py +0 -0
  50. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/compose.py +0 -0
  51. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/configs.py +0 -0
  52. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/containers.py +0 -0
  53. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/context.py +0 -0
  54. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/images.py +0 -0
  55. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/networks.py +0 -0
  56. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/nodes.py +0 -0
  57. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/plugins.py +0 -0
  58. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/prompts.py +0 -0
  59. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/registry.py +0 -0
  60. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/resources.py +0 -0
  61. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/scout.py +0 -0
  62. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/secrets.py +0 -0
  63. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/services.py +0 -0
  64. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/stack.py +0 -0
  65. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/swarm.py +0 -0
  66. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/system.py +0 -0
  67. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/docker_mcp/tools/volumes.py +0 -0
  68. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/glama.json +0 -0
  69. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/mcpb_run.py +0 -0
  70. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/scripts/build-mcpb.sh +0 -0
  71. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/scripts/docker-mcp-server.rb.tpl +0 -0
  72. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/server.json +0 -0
  73. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/__init__.py +0 -0
  74. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/conftest.py +0 -0
  75. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/__init__.py +0 -0
  76. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/conftest.py +0 -0
  77. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_buildx.py +0 -0
  78. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_cli.py +0 -0
  79. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_compose.py +0 -0
  80. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_containers.py +0 -0
  81. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_context.py +0 -0
  82. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_file_payloads.py +0 -0
  83. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_networks.py +0 -0
  84. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_nodes.py +0 -0
  85. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_registry.py +0 -0
  86. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_remote_exec.py +0 -0
  87. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_scout.py +0 -0
  88. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_services.py +0 -0
  89. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_smoke.py +0 -0
  90. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/integration/test_stack.py +0 -0
  91. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_buildx.py +0 -0
  92. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_cli.py +0 -0
  93. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_compose.py +0 -0
  94. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_configs.py +0 -0
  95. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_containers.py +0 -0
  96. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_context.py +0 -0
  97. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_docs.py +0 -0
  98. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_env.py +0 -0
  99. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_hosts.py +0 -0
  100. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_images.py +0 -0
  101. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_labels.py +0 -0
  102. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_main.py +0 -0
  103. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_naming.py +0 -0
  104. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_networks.py +0 -0
  105. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_nodes.py +0 -0
  106. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_plugins.py +0 -0
  107. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_prompts.py +0 -0
  108. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_registry.py +0 -0
  109. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_remote_exec.py +0 -0
  110. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_resources.py +0 -0
  111. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_scout.py +0 -0
  112. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_secrets.py +0 -0
  113. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_server.py +0 -0
  114. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_services.py +0 -0
  115. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_ssh_proxy.py +0 -0
  116. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_stack.py +0 -0
  117. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_swarm.py +0 -0
  118. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_system.py +0 -0
  119. {docker_mcp_server-2.2.0 → docker_mcp_server-2.2.1}/tests/test_utils.py +0 -0
  120. {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.0
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>=1.27.1
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.0",
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.0"
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
- "mcp>=1.27.1",
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.0"
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
- )