docker-mcp-server 2.2.7__tar.gz → 2.2.8__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 (182) hide show
  1. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.claude/commands/docker-sdk.md +6 -0
  2. docker_mcp_server-2.2.8/.github/workflows/announce-pull-requests.yaml +18 -0
  3. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/canary.yaml +3 -3
  4. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/code-review.yaml +2 -2
  5. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/codeql.yaml +3 -3
  6. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/premerge.yaml +19 -18
  7. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/publish-homebrew.yaml +1 -1
  8. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/publish.yaml +27 -11
  9. docker_mcp_server-2.2.8/.github/workflows/report-failures.yaml +32 -0
  10. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.gitignore +1 -0
  11. docker_mcp_server-2.2.8/.pre-commit-config.yaml +18 -0
  12. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/AGENTS.md +25 -14
  13. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/CONTRIBUTING.md +2 -2
  14. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/DOCKERHUB.md +1 -1
  15. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/Dockerfile +10 -4
  16. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/MCP_VS_SKILLS.md +136 -137
  17. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/PKG-INFO +20 -18
  18. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/README.md +17 -17
  19. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/SECURITY.md +2 -2
  20. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/ci.md +32 -12
  21. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/cli-shell-out.md +2 -2
  22. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/distribution.md +12 -6
  23. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/docker-sdk.md +47 -23
  24. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/hosts.md +1 -1
  25. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/server.md +26 -5
  26. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/tool-descriptions.md +2 -0
  27. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/claude-plugin/.claude-plugin/plugin.json +2 -2
  28. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/claude-plugin/pyproject.toml +2 -2
  29. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/server.py +248 -85
  30. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/_cli.py +3 -2
  31. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/_utils.py +20 -0
  32. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/buildx.py +127 -64
  33. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/compose.py +175 -120
  34. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/containers.py +419 -116
  35. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/context.py +5 -14
  36. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/images.py +87 -75
  37. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/networks.py +5 -5
  38. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/nodes.py +68 -27
  39. docker_mcp_server-2.2.8/docker_mcp/tools/plugins.py +227 -0
  40. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/prompts.py +29 -23
  41. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/registry.py +17 -23
  42. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/resources.py +23 -38
  43. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/scout.py +24 -31
  44. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/secrets.py +2 -8
  45. docker_mcp_server-2.2.8/docker_mcp/tools/services.py +677 -0
  46. docker_mcp_server-2.2.8/docker_mcp/tools/stack.py +448 -0
  47. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/swarm.py +7 -191
  48. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/system.py +297 -11
  49. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/volumes.py +6 -6
  50. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/manifest.json +1 -1
  51. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/pyproject.toml +9 -2
  52. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/scripts/claude_plugin_release.py +68 -5
  53. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/scripts/measure-comparison-figures.py +5 -5
  54. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/server.json +4 -4
  55. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/SKILL.md +31 -12
  56. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/buildx.md +28 -5
  57. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/compose.md +10 -6
  58. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/containers.md +26 -10
  59. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/images.md +10 -4
  60. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/networks-volumes.md +5 -2
  61. docker_mcp_server-2.2.8/skills/l337-docker/reference/observability.md +338 -0
  62. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/registry.md +73 -29
  63. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/scout.md +13 -4
  64. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/swarm.md +58 -26
  65. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/system.md +23 -14
  66. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/workflows/build-publish.md +4 -2
  67. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/workflows/deploy.md +9 -5
  68. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/workflows/maintenance.md +17 -7
  69. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/workflows/security.md +4 -3
  70. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/workflows/troubleshoot.md +3 -2
  71. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/conftest.py +4 -0
  72. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_compose.py +72 -0
  73. docker_mcp_server-2.2.8/tests/integration/test_containers.py +227 -0
  74. docker_mcp_server-2.2.8/tests/integration/test_nodes.py +33 -0
  75. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_plugins.py +8 -8
  76. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_registry.py +5 -3
  77. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_services.py +15 -10
  78. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_skill.py +227 -8
  79. docker_mcp_server-2.2.8/tests/integration/test_stack.py +214 -0
  80. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_swarm.py +32 -29
  81. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_buildx.py +141 -1
  82. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_claude_plugin_release.py +132 -0
  83. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_compose.py +192 -21
  84. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_containers.py +595 -60
  85. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_docs.py +34 -1
  86. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_docstrings.py +21 -1
  87. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_images.py +101 -30
  88. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_naming.py +92 -1
  89. docker_mcp_server-2.2.8/tests/test_nodes.py +193 -0
  90. docker_mcp_server-2.2.8/tests/test_plugins.py +200 -0
  91. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_prompts.py +10 -7
  92. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_registry.py +23 -14
  93. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_resources.py +0 -10
  94. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_server.py +310 -5
  95. docker_mcp_server-2.2.8/tests/test_services.py +743 -0
  96. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_skill.py +30 -0
  97. docker_mcp_server-2.2.8/tests/test_stack.py +744 -0
  98. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_surface_budget.py +8 -8
  99. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_swarm.py +1 -128
  100. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_system.py +289 -1
  101. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_utils.py +16 -0
  102. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/uv.lock +6 -2
  103. docker_mcp_server-2.2.7/MIGRATION-2.0.md +0 -219
  104. docker_mcp_server-2.2.7/docker_mcp/tools/plugins.py +0 -343
  105. docker_mcp_server-2.2.7/docker_mcp/tools/services.py +0 -470
  106. docker_mcp_server-2.2.7/docker_mcp/tools/stack.py +0 -272
  107. docker_mcp_server-2.2.7/skills/l337-docker/reference/observability.md +0 -208
  108. docker_mcp_server-2.2.7/tests/integration/test_containers.py +0 -105
  109. docker_mcp_server-2.2.7/tests/integration/test_nodes.py +0 -16
  110. docker_mcp_server-2.2.7/tests/integration/test_stack.py +0 -65
  111. docker_mcp_server-2.2.7/tests/test_nodes.py +0 -131
  112. docker_mcp_server-2.2.7/tests/test_plugins.py +0 -239
  113. docker_mcp_server-2.2.7/tests/test_services.py +0 -446
  114. docker_mcp_server-2.2.7/tests/test_stack.py +0 -252
  115. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.agents/policy/architecture.md +0 -0
  116. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.agents/policy/review-context.md +0 -0
  117. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.agents/policy/testing.md +0 -0
  118. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.claude/settings.json +0 -0
  119. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.dockerignore +0 -0
  120. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/CODEOWNERS +0 -0
  121. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  122. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  123. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  124. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/actions/file-failure-issue/action.yaml +0 -0
  125. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/copilot-instructions.md +0 -0
  126. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/dependabot.yaml +0 -0
  127. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/release.yml +0 -0
  128. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.github/workflows/images.yaml +0 -0
  129. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.mcpbignore +0 -0
  130. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/.python-version +0 -0
  131. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/CLAUDE.md +0 -0
  132. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/CODE_OF_CONDUCT.md +0 -0
  133. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/LICENSE +0 -0
  134. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/PRIVACY.md +0 -0
  135. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/architecture/agent-skill.md +0 -0
  136. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/assets/README.md +0 -0
  137. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/assets/icon.png +0 -0
  138. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/claude-plugin/README.md +0 -0
  139. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/claude-plugin/icon.png +0 -0
  140. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/__init__.py +0 -0
  141. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/__main__.py +0 -0
  142. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/_env.py +0 -0
  143. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/_hosts.py +0 -0
  144. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/exceptions.py +0 -0
  145. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/__init__.py +0 -0
  146. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/_labels.py +0 -0
  147. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/_ssh_proxy.py +0 -0
  148. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/docker_mcp/tools/configs.py +0 -0
  149. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/glama.json +0 -0
  150. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/mcpb_run.py +0 -0
  151. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/scripts/build-mcpb.sh +0 -0
  152. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/scripts/docker-mcp-server.rb.tpl +0 -0
  153. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/LICENSE +0 -0
  154. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/skills/l337-docker/reference/docs.md +0 -0
  155. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/__init__.py +0 -0
  156. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/conftest.py +0 -0
  157. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/__init__.py +0 -0
  158. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_buildx.py +0 -0
  159. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_cli.py +0 -0
  160. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_cli_flag_drift.py +0 -0
  161. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_context.py +0 -0
  162. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_file_payloads.py +0 -0
  163. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_images.py +0 -0
  164. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_networks.py +0 -0
  165. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_remote_exec.py +0 -0
  166. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_scout.py +0 -0
  167. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/integration/test_smoke.py +0 -0
  168. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_claude_plugin.py +0 -0
  169. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_cli.py +0 -0
  170. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_configs.py +0 -0
  171. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_context.py +0 -0
  172. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_env.py +0 -0
  173. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_hosts.py +0 -0
  174. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_labels.py +0 -0
  175. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_main.py +0 -0
  176. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_networks.py +0 -0
  177. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_pyproject_pins.py +0 -0
  178. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_remote_exec.py +0 -0
  179. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_scout.py +0 -0
  180. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_secrets.py +0 -0
  181. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_ssh_proxy.py +0 -0
  182. {docker_mcp_server-2.2.7 → docker_mcp_server-2.2.8}/tests/test_volumes.py +0 -0
@@ -48,6 +48,8 @@ The project structure maps SDK domains to tool files one-to-one:
48
48
 
49
49
  Read each `docker_mcp/tools/*.py` file and list every `@mcp.tool` decorated function and the `docker` module methods it calls.
50
50
 
51
+ The CLI-backed modules shell out to `docker` through `docker_mcp/tools/_cli.py:run_docker`; find them by grepping `docker_mcp/tools/*.py` for `run_docker` rather than from a list (skipping `_cli.py` and `_ssh_proxy.py`, which are the machinery itself), and record the CLI command each tool runs. For each one, check whether a first-party Python API now covers the operation: docker-py, or any other Python package Docker publishes (look in the `docker` GitHub organisation and on PyPI for one covering the features those tools shell out for). Enumerate docker-py's public modules from the source at the release being checked, not from the rendered docs fetched in Step 1, and where the rendered docs have no page for a module, read its source. Read [architecture/cli-shell-out.md](../../architecture/cli-shell-out.md) before judging a candidate: moving a tool off the CLI also moves it off the SSH remote-exec fallback and file staging.
52
+
51
53
  ### Step 3 - Produce a gap analysis
52
54
 
53
55
  Compare the SDK surface area from Step 1 against the MCP tool inventory from Step 2 and output a structured report:
@@ -63,6 +65,10 @@ Compare the SDK surface area from Step 1 against the MCP tool inventory from Ste
63
65
  - <`docker` module method>() - <one-line description> → should go in docker_mcp/tools/<file>.py
64
66
  ...
65
67
 
68
+ ### CLI Shell-outs With a First-party Python API Equivalent
69
+ - <MCP tool name> (docker_mcp/tools/<file>.py) runs `docker <command>` → could use <package and method>(), verified at <source location>; changes for callers: <remote-exec fallback, staging, credentials, context>
70
+ ...
71
+
66
72
  ### Verification Notes
67
73
  For any specific functions the user asked about: confirmed they exist (or do not exist)
68
74
  in the live documentation, with the exact signature.
@@ -0,0 +1,18 @@
1
+ name: 'Announce pull requests'
2
+
3
+ # Posts a pull request that needs action to the project's Slack channel through the shared
4
+ # announcer, which decides what to post (architecture/ci.md).
5
+ on:
6
+ pull_request:
7
+ types: [opened, reopened, ready_for_review]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ slack:
13
+ name: Announce to Slack
14
+ uses: L337-org/github-workflows/.github/workflows/slack-on-pull-request.yaml@v2
15
+ with:
16
+ channel: '#docker-mcp'
17
+ secrets:
18
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -42,7 +42,7 @@ jobs:
42
42
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
43
 
44
44
  - name: Set up uv
45
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
45
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
46
46
  with:
47
47
  # Pinned (matching the Dockerfile's uv image tag) so this step never needs the
48
48
  # live "resolve latest" fetch to raw.githubusercontent.com — that network call
@@ -58,7 +58,7 @@ jobs:
58
58
  run: |
59
59
  set -euo pipefail
60
60
  if [ "$GITHUB_EVENT_NAME" != "pull_request" ]; then
61
- latest="$(curl -fsSL https://pypi.org/pypi/docker-mcp-server/json | jq -r .info.version)"
61
+ latest="$(curl -fsSL --max-time 30 --retry 3 https://pypi.org/pypi/docker-mcp-server/json | jq -r .info.version)"
62
62
  echo "Latest published release: $latest"
63
63
  fi
64
64
  failed=0
@@ -104,7 +104,7 @@ jobs:
104
104
  os: [macos-latest, macos-15-intel, windows-latest]
105
105
  steps:
106
106
  - name: Set up uv
107
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
107
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
108
108
  with:
109
109
  # This job deliberately has no checkout — it tests the *published* package, not the
110
110
  # repo — so disable the repo-keyed cache and the empty-workdir warning it triggers.
@@ -3,7 +3,7 @@ permissions:
3
3
  contents: read
4
4
 
5
5
  # What this caller decides. The shared workflow decides everything else about the review, and
6
- # its README describes it at the commit pinned below.
6
+ # its README describes it at the version named below.
7
7
  #
8
8
  # - Opening a pull request, and every push to one, asks for a review at the depth the shared
9
9
  # workflow picks. A push is reviewed deliberately: the loop that invites, a reviewing agent and
@@ -41,7 +41,7 @@ jobs:
41
41
  permissions:
42
42
  contents: read
43
43
  pull-requests: write
44
- uses: L337-org/github-workflows/.github/workflows/claude-review.yaml@68eac351110ef94d36c0b98cec08339cd22c7ab7 # v1.2.0
44
+ uses: L337-org/github-workflows/.github/workflows/claude-review.yaml@v2
45
45
  with:
46
46
  pr-number: ${{ github.event.pull_request.number || github.event.issue.number }}
47
47
  depth: >-
@@ -33,12 +33,12 @@ jobs:
33
33
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
34
 
35
35
  - name: Initialize CodeQL
36
- uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
36
+ uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
37
37
  with:
38
38
  languages: ${{ matrix.language }}
39
39
 
40
40
  - name: Autobuild
41
- uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
41
+ uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
42
42
 
43
43
  - name: Perform CodeQL Analysis
44
- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
44
+ uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
@@ -22,7 +22,7 @@ jobs:
22
22
 
23
23
  repo-hygiene:
24
24
  # Conventions every repository in this organisation shares, checked by the one copy in the
25
- # shared workflows repository at the commit pinned here.
25
+ # shared workflows repository at the version tag named here.
26
26
  runs-on: [ubuntu-latest]
27
27
  name: Repository hygiene
28
28
  # 9s for the whole job on the first run of the shared action here, uv setup included.
@@ -32,25 +32,26 @@ jobs:
32
32
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
33
33
 
34
34
  - name: Repository conventions hold
35
- uses: L337-org/github-workflows/actions/repo-hygiene@68eac351110ef94d36c0b98cec08339cd22c7ab7 # v1.2.0
35
+ uses: L337-org/github-workflows/actions/repo-hygiene@v2
36
36
 
37
37
  action-pins:
38
- # Every `uses:` in .github/workflows and .github/actions names a full commit SHA, checked by
39
- # the shared copy pinned here. Its default directories are this repository's, so the local
38
+ # Every third-party `uses:` in .github/workflows and .github/actions names a full commit SHA,
39
+ # and the shared workflows repository is named at its major-version tag, checked by the
40
+ # shared copy. Its default directories are this repository's, so the local
40
41
  # file-failure-issue action is covered too. Why the pin matters most here: publish.yaml
41
42
  # mints an OIDC token for PyPI Trusted Publishing (`id-token: write`), pushes to GHCR
42
- # (`packages: write`) and uploads release assets (`contents: write`), so a repointed tag
43
- # would execute inside the jobs holding this project's strongest credentials.
43
+ # (`packages: write`) and uploads release assets (`contents: write`), so a repointed
44
+ # third-party tag would execute inside the jobs holding this project's strongest credentials.
44
45
  runs-on: [ubuntu-latest]
45
- name: Action pins are immutable
46
+ name: Action pin check
46
47
  # 5s for the whole job on the first run of the shared action here - a grep over the workflows.
47
48
  timeout-minutes: 5
48
49
  steps:
49
50
  - name: Check out source repository
50
51
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
51
52
 
52
- - name: Every `uses:` must name a 40-hex commit SHA
53
- uses: L337-org/github-workflows/actions/action-pins@68eac351110ef94d36c0b98cec08339cd22c7ab7 # v1.2.0
53
+ - name: Every `uses:` must name a 40-hex commit SHA, or the shared workflows' major-version tag
54
+ uses: L337-org/github-workflows/actions/action-pins@v2
54
55
 
55
56
  fresh-resolve-import:
56
57
  # Answers a question `uv sync --locked` (used by every other job here) cannot: would a
@@ -77,7 +78,7 @@ jobs:
77
78
  python-version: "3.14"
78
79
 
79
80
  - name: Set up uv
80
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
81
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
81
82
 
82
83
  - name: Resolve and install from pyproject.toml, ignoring uv.lock
83
84
  # A failure here is a resolve/build/install failure (an unsatisfiable specifier, a
@@ -127,7 +128,7 @@ jobs:
127
128
  python-version: "3.14"
128
129
 
129
130
  - name: Set up uv
130
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
131
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
131
132
  with:
132
133
  # The release jobs' uv, so the rehearsal locks with the same version; a test in
133
134
  # tests/test_claude_plugin_release.py keeps them equal.
@@ -185,7 +186,7 @@ jobs:
185
186
  python-version: "3.14"
186
187
 
187
188
  - name: Set up uv
188
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
189
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
189
190
  with:
190
191
  version: "0.11.21"
191
192
  enable-cache: false
@@ -214,7 +215,7 @@ jobs:
214
215
  python-version: "3.14"
215
216
 
216
217
  - name: Set up uv
217
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
218
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
218
219
 
219
220
  - name: Install dependencies
220
221
  # --locked fails if uv.lock disagrees with pyproject.toml instead of silently
@@ -245,7 +246,7 @@ jobs:
245
246
  python-version: "3.14"
246
247
 
247
248
  - name: Set up uv
248
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
249
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
249
250
 
250
251
  - name: Install dependencies
251
252
  # --locked fails if uv.lock disagrees with pyproject.toml instead of silently
@@ -278,7 +279,7 @@ jobs:
278
279
  python-version: "3.14"
279
280
 
280
281
  - name: Set up uv
281
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
282
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
282
283
 
283
284
  - name: Install dependencies
284
285
  run: uv sync --all-groups --locked
@@ -308,7 +309,7 @@ jobs:
308
309
  python-version: "3.14"
309
310
 
310
311
  - name: Set up uv
311
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
312
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
312
313
 
313
314
  - name: Install dependencies
314
315
  # --locked fails if uv.lock disagrees with pyproject.toml instead of silently
@@ -347,7 +348,7 @@ jobs:
347
348
  python-version: "3.14"
348
349
 
349
350
  - name: Set up uv
350
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
351
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
351
352
 
352
353
  - name: Install dependencies
353
354
  # --locked fails if uv.lock disagrees with pyproject.toml instead of silently
@@ -366,7 +367,7 @@ jobs:
366
367
  docker scout version 2>/dev/null || echo "scout not installed; scout integration tests will skip"
367
368
 
368
369
  - name: Initialize a single-node swarm
369
- # Needed for service_wait / node_wait integration tests (skip_if_no_swarm), which
370
+ # Needed for system_wait's service / node integration tests (skip_if_no_swarm), which
370
371
  # otherwise skip cleanly on a non-swarm daemon. No teardown needed — the runner is
371
372
  # ephemeral and torn down after the job.
372
373
  run: docker swarm init
@@ -71,7 +71,7 @@ jobs:
71
71
  run: git fetch --depth=1 origin main && git checkout origin/main -- scripts/docker-mcp-server.rb.tpl
72
72
 
73
73
  - name: Install uv
74
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
74
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
75
75
 
76
76
  - name: Resolve version, Python target, and macOS floor
77
77
  run: |
@@ -179,7 +179,7 @@ jobs:
179
179
  python-version: "3.14"
180
180
 
181
181
  - name: Install uv
182
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
182
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
183
183
 
184
184
  - name: Build sdist and wheel
185
185
  run: uv build
@@ -202,7 +202,7 @@ jobs:
202
202
  env:
203
203
  VERSION: ${{ needs.preflight.outputs.version }}
204
204
  run: |
205
- code="$(curl -sS --max-time 30 -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/docker-mcp-server/$VERSION/json")"
205
+ code="$(curl -sS --max-time 30 --retry 3 -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/docker-mcp-server/$VERSION/json")"
206
206
  case "$code" in
207
207
  200) echo "uploaded=false" >> "$GITHUB_OUTPUT"
208
208
  echo "::notice::docker-mcp-server $VERSION is already on PyPI; this run uploads nothing (skip-existing)." ;;
@@ -508,7 +508,7 @@ jobs:
508
508
  # asset-CDN lag.
509
509
  mkdir -p _mcpb
510
510
  for i in $(seq 1 3); do
511
- if gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.mcpb' --dir _mcpb --clobber; then
511
+ if timeout 120 gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.mcpb' --dir _mcpb --clobber; then
512
512
  break
513
513
  fi
514
514
  echo "::notice::.mcpb asset for $TAG not readable yet (attempt $i/3); waiting 10s"
@@ -544,7 +544,8 @@ jobs:
544
544
  MCP_PUBLISHER_SHA256: ab128162b0616090b47cf245afe0a23f3ef08936fdce19074f5ba0a4469281ac
545
545
  run: |
546
546
  set -euo pipefail
547
- curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
547
+ curl -fsSL --connect-timeout 30 --speed-limit 102400 --speed-time 30 --max-time 300 --retry 3 \
548
+ "https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
548
549
  -o mcp-publisher.tar.gz
549
550
  echo "${MCP_PUBLISHER_SHA256} mcp-publisher.tar.gz" | sha256sum -c -
550
551
  tar xzf mcp-publisher.tar.gz mcp-publisher
@@ -617,7 +618,7 @@ jobs:
617
618
  run: |
618
619
  set -euo pipefail
619
620
  for i in $(seq 1 5); do
620
- if curl -fsS "https://pypi.org/pypi/docker-mcp-server/${VERSION}/json" >/dev/null; then
621
+ if curl -fsS --max-time 30 "https://pypi.org/pypi/docker-mcp-server/${VERSION}/json" >/dev/null; then
621
622
  echo "PyPI OK: docker-mcp-server ${VERSION}"; exit 0
622
623
  fi
623
624
  echo "::notice::PyPI does not serve ${VERSION} yet (attempt $i/5); waiting 15s"
@@ -759,7 +760,16 @@ jobs:
759
760
  run: |
760
761
  set -euo pipefail
761
762
  mkdir -p _verify
762
- gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.mcpb*' --dir _verify
763
+ # A release asset comes from the same CDN whose 504s these downloads retry elsewhere, and a
764
+ # failure here turns a converged release red; the count check below catches every attempt
765
+ # failing.
766
+ for i in $(seq 1 3); do
767
+ if timeout 120 gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.mcpb*' --dir _verify --clobber; then
768
+ break
769
+ fi
770
+ echo "::notice::.mcpb assets for $TAG not downloadable (attempt $i/3); waiting 10s"
771
+ sleep 10
772
+ done
763
773
  shopt -s nullglob
764
774
  mcpb=(_verify/*.mcpb); sums=(_verify/*.sha256)
765
775
  if [ "${#mcpb[@]}" -ne 1 ] || [ "${#sums[@]}" -ne 1 ]; then
@@ -775,8 +785,14 @@ jobs:
775
785
  run: |
776
786
  set -euo pipefail
777
787
  mkdir -p _skill
778
- gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
779
- --pattern 'l337-docker-skill-*' --dir _skill
788
+ for i in $(seq 1 3); do
789
+ if timeout 120 gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
790
+ --pattern 'l337-docker-skill-*' --dir _skill --clobber; then
791
+ break
792
+ fi
793
+ echo "::notice::skill archive for $TAG not downloadable (attempt $i/3); waiting 10s"
794
+ sleep 10
795
+ done
780
796
  shopt -s nullglob
781
797
  tars=(_skill/*.tar.gz); sums=(_skill/*.sha256)
782
798
  if [ "${#tars[@]}" -ne 1 ] || [ "${#sums[@]}" -ne 1 ]; then
@@ -802,7 +818,7 @@ jobs:
802
818
  set -euo pipefail
803
819
  url="https://registry.modelcontextprotocol.io/v0/servers?search=io.github.L337-org/docker-mcp-server"
804
820
  for i in $(seq 1 5); do
805
- if curl -fsS "$url" | jq -e --arg v "$VERSION" \
821
+ if curl -fsS --max-time 30 "$url" | jq -e --arg v "$VERSION" \
806
822
  '.servers[] | select(.server.version == $v)' >/dev/null; then
807
823
  echo "MCP Registry OK: ${VERSION} listed"; exit 0
808
824
  fi
@@ -864,7 +880,7 @@ jobs:
864
880
 
865
881
  - name: Set up uv
866
882
  if: steps.plugin.outputs.has_plugin == 'true'
867
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
883
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
868
884
  with:
869
885
  # Pinned, so the lock is written by a known uv; a test in
870
886
  # tests/test_claude_plugin_release.py keeps the rehearsal's pins equal to these.
@@ -938,7 +954,7 @@ jobs:
938
954
  python-version: "3.14"
939
955
 
940
956
  - name: Set up uv
941
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
957
+ uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
942
958
  with:
943
959
  version: "0.11.21"
944
960
  enable-cache: false
@@ -0,0 +1,32 @@
1
+ name: 'Report failures'
2
+
3
+ # Posts a failed run of a workflow named below, started by one of the events below, to the
4
+ # project's Slack channel through the shared reporter (architecture/ci.md). GitHub reads this
5
+ # trigger only from the default branch.
6
+ on:
7
+ workflow_run:
8
+ workflows:
9
+ - 'Weekly canary'
10
+ - 'CodeQL'
11
+ - 'Code review'
12
+ - 'Build container images'
13
+ - 'Check inbound changes'
14
+ - 'Release'
15
+ - 'Announce pull requests'
16
+ types: [completed]
17
+
18
+ permissions:
19
+ # Both read by the shared reporter, which GitHub refuses to start without them: to name the
20
+ # failed jobs and find a run that replaced a cancelled one, and to tell a tag from a branch.
21
+ actions: read
22
+ contents: read
23
+
24
+ jobs:
25
+ slack:
26
+ name: Report to Slack
27
+ uses: L337-org/github-workflows/.github/workflows/slack-on-failure.yaml@v2
28
+ with:
29
+ channel: '#docker-mcp'
30
+ events: '["schedule", "push", "release", "pull_request"]'
31
+ secrets:
32
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -2,6 +2,7 @@
2
2
  /.*
3
3
  !/.agents/
4
4
  !/.python-version
5
+ !/.pre-commit-config.yaml
5
6
  !/.gitignore
6
7
  !/.dockerignore
7
8
  !/.mcpbignore
@@ -0,0 +1,18 @@
1
+ # Installed with `uv run pre-commit install` (CONTRIBUTING.md). Local hooks through `uv run`, not the
2
+ # ruff-pre-commit repo, so the hook runs the ruff version uv.lock resolves for the dev group rather than a
3
+ # second pin that can drift from it.
4
+ repos:
5
+ - repo: local
6
+ hooks:
7
+ - id: ruff-check
8
+ name: ruff check --fix
9
+ entry: uv run ruff check --fix
10
+ language: system
11
+ types_or: [python, pyi]
12
+ require_serial: true
13
+ - id: ruff-format
14
+ name: ruff format
15
+ entry: uv run ruff format
16
+ language: system
17
+ types_or: [python, pyi]
18
+ require_serial: true
@@ -37,7 +37,7 @@ Read the linked file before judging a substantive change to that area.
37
37
  | code calling `docker` | [architecture/docker-sdk.md](architecture/docker-sdk.md) | an unverified method; a hand-built route that is not in the Engine API spec; a reach-in past the public SDK introduced without recorded sign-off |
38
38
  | `Dockerfile`, `manifest.json`, `server.json`, `claude-plugin/`, release workflows | [architecture/distribution.md](architecture/distribution.md) | a version-declaring file missing from the bump checklist there, `preflight`, or the version tests; a registry ownership marker no longer matching `server.json`'s `name` |
39
39
  | `skills/l337-docker/` | [architecture/agent-skill.md](architecture/agent-skill.md) | the skill described as equivalent to the server; tool-permission frontmatter added; a hand-edited figure in `MCP_VS_SKILLS.md` |
40
- | `.github/workflows/` | [architecture/ci.md](architecture/ci.md) | a `uses:` naming a tag or branch; a new job without `timeout-minutes` where the workflow requires one |
40
+ | `.github/workflows/` | [architecture/ci.md](architecture/ci.md) | a third-party `uses:` naming a tag or branch (`L337-org/github-workflows` is named at its major-version tag); a new job without `timeout-minutes` where the workflow requires one |
41
41
 
42
42
  ## Project
43
43
 
@@ -136,7 +136,10 @@ Any tool wrapping a `docker` CLI feature (Compose, Stack, Buildx, Scout, Context
136
136
  `docker_mcp/tools/_cli.py:run_docker` — never call `subprocess.run` directly from a tool module. That
137
137
  helper is the single place holding binary resolution, `shell=False`, UTF-8 decoding, the output byte
138
138
  cap, Windows console suppression and the environment allow-list. Always pass an explicit `timeout=`.
139
- Never pass `shell=True`, never build paths by string concatenation, never expand `~` or globs yourself.
139
+ Never pass `shell=True`, never build paths by string concatenation, and never expand a glob or a
140
+ variable. Expand only a leading `~`, only where the tool documents it (`buildx_build`'s path
141
+ parameters), and only with `os.path.expanduser`: pathlib's `expanduser()` raises a bare
142
+ `RuntimeError` for an unknown `~user`, where `os.path` leaves the value for the CLI to report.
140
143
 
141
144
  **Read [architecture/cli-shell-out.md](architecture/cli-shell-out.md) before touching any of it**, and
142
145
  before adding a CLI-backed tool. It covers the SSH remote-exec fallback (which changes *which host* a
@@ -152,8 +155,9 @@ module method you have not confirmed in the docs. Do not assume a method exists
152
155
  plausible; if you cannot confirm it, say so and do not use it.
153
156
 
154
157
  Prefer the high-level object API; drop to `_get_client().api` only for documented gaps, verified the
155
- same way. **"The method exists" and "the method works" are separate claims** — `plugin_push` is the
156
- standing example of a documented, importable SDK method that 404s against every daemon.
158
+ same way. **"The method exists" and "the method works" are separate claims** — docker-py's
159
+ `Plugin.push()` is the standing example of a documented, importable SDK method that 404s against
160
+ every daemon.
157
161
 
158
162
  **Reaching past the public SDK is not an agent's call to make.** Where the public path fails, stop,
159
163
  write up what you found, and escalate for a human decision rather than implementing a workaround.
@@ -186,6 +190,17 @@ discovery layers. Four cleanup rounds have chased the same failure.
186
190
  `timeout_seconds`. `tests/test_naming.py` enforces all of this (approved prefixes, banned short forms and
187
191
  1.x spellings, canonical shared-param descriptions) — a violating tool fails CI.
188
192
  - New Docker functionality goes in the matching `docker_mcp/tools/<domain>.py` file, not in a new file.
193
+ - **Waiting is one tool, `system_wait`.** A new thing to wait for is a new `kind` or `until` in `_WAIT_KINDS`
194
+ (`system.py`) with its helper in the owning domain's module, never a new `*_wait` tool and never a blocking
195
+ option on an action tool: action tools return once their work is started. The exceptions are
196
+ `container_exec` and `container_run(timeout_seconds=...)`: both serve running something quick and getting
197
+ the answer straight back, which would otherwise take several calls. Both are bounded; see
198
+ [architecture/server.md](architecture/server.md#waiting). `kind`/`until` are built from the
199
+ enabled domains at import, so a disabled domain's waits are not offered. Its `target` parameter is a
200
+ deliberate exception to the identifier spellings above, because one argument names a container, service,
201
+ node, stack, compose project or `repository:tag`. `system` and `containers` cannot be disabled
202
+ (`ALWAYS_ON_DOMAINS` in `server.py`); naming either in `DOCKER_MCP_SERVER_DISABLE` is reported as ignored,
203
+ not silently dropped.
189
204
  - Every new `docker_mcp/tools/` file must be imported in `docker_mcp/tools/__init__.py` (private `_*.py` helpers excluded).
190
205
  - Every new `docker_mcp/tools/<module>.py` must have a matching `tests/test_<module>.py`.
191
206
  - Tool functions are decorated with `@tool()` (imported from `docker_mcp.server`) and must have a `TOOL_CATEGORIES` entry in `docker_mcp/server.py`.
@@ -237,7 +252,7 @@ discovery layers. Four cleanup rounds have chased the same failure.
237
252
 
238
253
  Resources this server **creates** are stamped with `docker-mcp-server.*` provenance labels (`.managed=true`, `.version`, `.tool`, `.created`) via `docker_mcp/tools/_labels.py`, so the agent/operator can later enumerate that footprint — the `managed_only=True` arg on `container_list` / `network_list` / `volume_list` / `service_list`, or `--filter label=docker-mcp-server.managed=true`. The `prune_managed` prompt tears down only the managed footprint. Stamping is **on by default** and additive (a caller-supplied label always wins on a key collision); `DOCKER_MCP_SERVER_NO_LABELS=1` turns it off. The prefix is the bare project name (deliberately not reverse-DNS) and is a single constant in `_labels.py`.
239
254
 
240
- When adding a new create tool that accepts a `labels` dict, route it through `_labels.py:with_provenance(labels, "<tool_name>")` (it accepts the dict/list/None shapes the SDK accepts and returns `None` — feed it through `drop_none` — when stamping is off and the caller passed nothing). The seven stamped creators today are `container_run`, `container_create`, `network_create`, `volume_create`, `service_create` (service-level `labels` only, not `container_labels`), `config_create`, `secret_create`. **Image builds are intentionally NOT stamped** — a build label changes the resulting image digest. Compose/stack containers (created via CLI shell-out) are also unstamped, as is `plugin_create` — the Engine's plugin-create call accepts no labels field, so there is nothing to stamp. The rule is conditional on the tool *accepting a `labels` dict*: a creator with nowhere to put a label is an expected exception, not an oversight, and should say so in its docstring. New `managed_only`-style label filters go through `_labels.py:managed_filter`.
255
+ When adding a new create tool that accepts a `labels` dict, route it through `_labels.py:with_provenance(labels, "<tool_name>")` (it accepts the dict/list/None shapes the SDK accepts and returns `None` — feed it through `drop_none` — when stamping is off and the caller passed nothing). The seven stamped creators today are `container_run`, `container_create`, `network_create`, `volume_create`, `service_create` (service-level `labels` only, not `container_labels`), `config_create`, `secret_create`. **Image builds are intentionally NOT stamped** — a build label changes the resulting image digest. Compose/stack containers (created via CLI shell-out) are also unstamped. The rule is conditional on the tool *accepting a `labels` dict*: a creator with nowhere to put a label is an expected exception, not an oversight, and should say so in its docstring. New `managed_only`-style label filters go through `_labels.py:managed_filter`.
241
256
 
242
257
  ## Invariants that fail silently
243
258
 
@@ -354,17 +369,14 @@ Removing an entry is a real decision, not a tidy-up.
354
369
  **SDK surface deliberately not wrapped, or wrapped unobviously.** A recurring audit routine
355
370
  re-proposes these; it has no memory of last time.
356
371
 
357
- - **`plugin_push`'s hand-built endpoint call.** docker-py's `Plugin.push()` / `APIClient.push_plugin()`
358
- both POST to `/plugins/{name}/pull`, a route the Engine does not define, so they 404 against every
359
- daemon. Our call is the working path, not debt. Revisit only if upstream fixes the URL.
360
372
  - **`Container.attach` / `attach_socket` / `resize` unwrapped.** An interactive bidirectional stream
361
373
  does not fit a request/response tool call. `container_exec` covers scripted execution.
362
374
  - **`service_rollback`'s `api.inspect_service` + `api.update_service`.** The high-level
363
375
  `Service`/`ServiceCollection` expose no `rollback`. Permanently low-level.
364
376
  - **`system_logout`'s `api._auth_configs`.** There is no `logout` anywhere in the SDK and no server-side
365
377
  session to end. Permanently low-level.
366
- - **`swarm_task_list` / `swarm_task_inspect`'s `api.tasks()` / `api.inspect_task()`.** docker-py has no
367
- task collection at all. These documented `APIClient` methods are the only public path.
378
+ - **`service_ps`'s `api.tasks()` with no service, and `service_logs(task=...)`'s hand-built
379
+ `GET /tasks/{id}/logs`.** docker-py has no task collection and no task-logs method at all.
368
380
 
369
381
  **Other settled decisions.**
370
382
 
@@ -384,7 +396,7 @@ re-proposes these; it has no memory of last time.
384
396
  (pyupgrade, 3.14 target) may rewrite to the unparenthesized form.
385
397
 
386
398
  - **Image builds are not provenance-stamped.** A build label changes the resulting image digest.
387
- Compose and stack containers, and `plugin_create`, are unstamped for their own recorded reasons.
399
+ Compose and stack containers are unstamped for their own recorded reasons.
388
400
  - **Cross-host redirects are followed on purpose.** Registries answer blob fetches with a redirect to a
389
401
  CDN on another host as normal operation, so refusing them breaks `registry_image_config`. httpx
390
402
  strips `Authorization` on any cross-origin redirect, and a redirect reaches nothing a tool argument
@@ -471,9 +483,8 @@ afterthought: at this server's size it is the dominant cost of connecting to it
471
483
  `tests/test_surface_budget.py` holds the ceilings: per tool, per component, and in total.
472
484
 
473
485
  **What is measured is the wire form, not the docstring.** A tool costs its name, its description
474
- and its whole input schema, and the schema is usually the larger half - `buildx_build` is 5,091
475
- bytes on the wire against 3,398 of description. A docstring-only budget would go green on a
476
- change that added twenty parameters.
486
+ and its whole input schema, so a docstring-only budget would go green on a change that added
487
+ twenty parameters. `tests/test_surface_budget.py`'s header gives the command that measures both.
477
488
 
478
489
  **Bytes, not tokens.** Tokens are what a model pays, but they need a tokenizer pinned to a model
479
490
  that changes underneath us, so the figure moves without the surface moving. That is why
@@ -8,7 +8,7 @@ This is an [MCP](https://modelcontextprotocol.io) server: it exposes Docker oper
8
8
 
9
9
  The practical mental model for finding your way around:
10
10
 
11
- - One file per Docker feature area (`containers.py`, `images.py`, `swarm.py`, ...), each backed by either the [docker-py SDK](https://docker-py.readthedocs.io/en/stable/) or a `docker` CLI shell-out (`compose.py`, `stack.py`, `context.py`, `buildx.py`, `scout.py`). See the tree below for the full map.
11
+ - One file per Docker feature area (`containers.py`, `images.py`, `swarm.py`, ...), each backed by either the [docker-py SDK](https://docker-py.readthedocs.io/en/stable/) or a `docker` CLI shell-out (`compose.py`, `stack.py` for `stack_deploy` only, `context.py`, `buildx.py`, `scout.py`). See the tree below for the full map.
12
12
  - Every tool function is registered centrally in `docker_mcp/server.py`, which is also where read-only/destructive classification, naming enforcement, and the domain-disable switches live.
13
13
  - `AGENTS.md` (repo root) is the always-loaded brief for every assistant, and the map of everything else: it carries the conventions and a table pointing at the deeper notes. Those notes live in `architecture/` - one file per area (`server.md`, `hosts.md`, `cli-shell-out.md`, `tool-descriptions.md`, `docker-sdk.md`, `distribution.md`, `agent-skill.md`, `ci.md`), and they are the canonical detail for humans and assistants alike. This file (`CONTRIBUTING.md`) covers the practical day-to-day: setup, the checklist for adding a tool, testing conventions, and submitting changes. If something isn't covered here, look at `AGENTS.md`'s table and follow it into `architecture/`.
14
14
 
@@ -35,7 +35,7 @@ The practical mental model for finding your way around:
35
35
  │ ├── swarm.py
36
36
  │ ├── plugins.py
37
37
  │ ├── compose.py # `docker compose` CLI plugin (shells out via _cli.py)
38
- │ ├── stack.py # `docker stack` (Compose-on-Swarm) CLI (shells out via _cli.py)
38
+ │ ├── stack.py # Compose-on-Swarm: `docker stack deploy` via _cli.py, the rest via docker-py
39
39
  │ ├── context.py # `docker context` CLI (shells out via _cli.py)
40
40
  │ ├── buildx.py # `docker buildx` CLI plugin (shells out via _cli.py)
41
41
  │ ├── scout.py # `docker scout` CLI plugin (shells out via _cli.py)
@@ -38,7 +38,7 @@ The only host prerequisite is Docker. Point your MCP client at `docker run`:
38
38
 
39
39
  ## Image variants
40
40
 
41
- Two multi-arch (linux/amd64 + linux/arm64) variants are published, both built from one `Dockerfile`. The CLI-backed domains (Compose, Stack, Buildx, Scout, Context) shell out to the `docker` CLI and its plugins.
41
+ Two multi-arch (linux/amd64 + linux/arm64) variants are published, both built from one `Dockerfile`. The CLI-backed domains (Compose, Stack, Buildx, Scout, Context) shell out to the `docker` CLI and its plugins; in Stack, only `stack_deploy` does.
42
42
 
43
43
  | Variant | Tags | Approx. size | Includes |
44
44
  |---------|------|-------------|----------|
@@ -55,13 +55,19 @@ ARG DISABLE_DOMAINS=""
55
55
  ARG SCOUT_VERSION=1.21.0
56
56
 
57
57
  # Install the docker CLI + compose/buildx plugins (and optionally scout) for the CLI-backed domains.
58
- # Skipped entirely for the `lite` variant (INSTALL_CLI=0).
58
+ # Skipped entirely for the `lite` variant (INSTALL_CLI=0). Every curl download is bounded and
59
+ # retried, because one 504 from GitHub's release CDN or download.docker.com otherwise fails the
60
+ # whole build; curl's --retry only retries timeouts and HTTP 408, 429, 500, 502, 503 and 504, so a
61
+ # 404 or a bad checksum still fails. --speed-limit/--speed-time abandon a stalled attempt after
62
+ # 30 s, and --max-time caps a slow one, so it must stay large enough for the scout tarball; both
63
+ # apply per attempt, not in total. apt's own fetches follow apt's retry settings, which this does
64
+ # not change.
59
65
  RUN if [ "$INSTALL_CLI" = "1" ]; then \
60
66
  set -eux; \
61
67
  apt-get update; \
62
68
  apt-get install -y --no-install-recommends ca-certificates curl gnupg; \
63
69
  install -m 0755 -d /etc/apt/keyrings; \
64
- curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc; \
70
+ curl -fsSL --connect-timeout 30 --speed-limit 102400 --speed-time 30 --max-time 300 --retry 3 https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc; \
65
71
  chmod a+r /etc/apt/keyrings/docker.asc; \
66
72
  printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian %s stable\n' \
67
73
  "$(dpkg --print-architecture)" "$DEBIAN_RELEASE" > /etc/apt/sources.list.d/docker.list; \
@@ -72,8 +78,8 @@ RUN if [ "$INSTALL_CLI" = "1" ]; then \
72
78
  scout_tar="docker-scout_${SCOUT_VERSION}_linux_${scout_arch}.tar.gz"; \
73
79
  scout_base="https://github.com/docker/scout-cli/releases/download/v${SCOUT_VERSION}"; \
74
80
  mkdir -p /usr/libexec/docker/cli-plugins; \
75
- curl -fsSL "${scout_base}/${scout_tar}" -o "/tmp/${scout_tar}"; \
76
- curl -fsSL "${scout_base}/docker-scout_${SCOUT_VERSION}_checksums.txt" -o /tmp/scout_checksums.txt; \
81
+ curl -fsSL --connect-timeout 30 --speed-limit 102400 --speed-time 30 --max-time 300 --retry 3 "${scout_base}/${scout_tar}" -o "/tmp/${scout_tar}"; \
82
+ curl -fsSL --connect-timeout 30 --speed-limit 102400 --speed-time 30 --max-time 300 --retry 3 "${scout_base}/docker-scout_${SCOUT_VERSION}_checksums.txt" -o /tmp/scout_checksums.txt; \
77
83
  (cd /tmp && grep -F "${scout_tar}" scout_checksums.txt | sha256sum -c -); \
78
84
  tar -xzf "/tmp/${scout_tar}" -C /usr/libexec/docker/cli-plugins docker-scout; \
79
85
  chmod +x /usr/libexec/docker/cli-plugins/docker-scout; \