docker-mcp-server 2.2.4__tar.gz → 2.2.5__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 (145) hide show
  1. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/copilot-instructions.md +1 -1
  2. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.mcpbignore +4 -0
  3. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/CLAUDE.md +4 -0
  4. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/MCP_VS_SKILLS.md +11 -3
  5. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/PKG-INFO +5 -5
  6. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/README.md +4 -4
  7. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/server.py +5 -1
  8. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/images.py +105 -1
  9. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/plugins.py +26 -1
  10. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/swarm.py +61 -0
  11. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/manifest.json +1 -1
  12. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/pyproject.toml +1 -1
  13. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/server.json +4 -4
  14. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/images.md +17 -0
  15. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/swarm.md +19 -0
  16. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/system.md +66 -0
  17. docker_mcp_server-2.2.5/tests/integration/test_images.py +61 -0
  18. docker_mcp_server-2.2.5/tests/integration/test_plugins.py +35 -0
  19. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_services.py +6 -1
  20. docker_mcp_server-2.2.5/tests/integration/test_swarm.py +69 -0
  21. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_images.py +125 -0
  22. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_plugins.py +20 -0
  23. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_swarm.py +27 -0
  24. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/uv.lock +1 -1
  25. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.claude/commands/docker-sdk.md +0 -0
  26. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.claude/settings.json +0 -0
  27. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.dockerignore +0 -0
  28. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/CODEOWNERS +0 -0
  29. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/actions/file-failure-issue/action.yaml +0 -0
  33. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/dependabot.yaml +0 -0
  34. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/release.yml +0 -0
  35. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/workflows/canary.yaml +0 -0
  36. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/workflows/codeql.yaml +0 -0
  37. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/workflows/images.yaml +0 -0
  38. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/workflows/premerge.yaml +0 -0
  39. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/workflows/publish-homebrew.yaml +0 -0
  40. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.github/workflows/publish.yaml +0 -0
  41. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.gitignore +0 -0
  42. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/.python-version +0 -0
  43. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/CODE_OF_CONDUCT.md +0 -0
  44. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/CONTRIBUTING.md +0 -0
  45. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/DOCKERHUB.md +0 -0
  46. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/Dockerfile +0 -0
  47. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/LICENSE +0 -0
  48. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/MIGRATION-2.0.md +0 -0
  49. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/PRIVACY.md +0 -0
  50. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/SECURITY.md +0 -0
  51. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/assets/README.md +0 -0
  52. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/assets/icon.png +0 -0
  53. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/__init__.py +0 -0
  54. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/__main__.py +0 -0
  55. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/_env.py +0 -0
  56. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/_hosts.py +0 -0
  57. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/__init__.py +0 -0
  58. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/_cli.py +0 -0
  59. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/_labels.py +0 -0
  60. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/_ssh_proxy.py +0 -0
  61. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/_utils.py +0 -0
  62. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/buildx.py +0 -0
  63. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/compose.py +0 -0
  64. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/configs.py +0 -0
  65. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/containers.py +0 -0
  66. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/context.py +0 -0
  67. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/networks.py +0 -0
  68. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/nodes.py +0 -0
  69. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/prompts.py +0 -0
  70. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/registry.py +0 -0
  71. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/resources.py +0 -0
  72. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/scout.py +0 -0
  73. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/secrets.py +0 -0
  74. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/services.py +0 -0
  75. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/stack.py +0 -0
  76. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/system.py +0 -0
  77. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/docker_mcp/tools/volumes.py +0 -0
  78. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/glama.json +0 -0
  79. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/mcpb_run.py +0 -0
  80. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/scripts/build-mcpb.sh +0 -0
  81. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/scripts/docker-mcp-server.rb.tpl +0 -0
  82. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/scripts/measure-comparison-figures.py +0 -0
  83. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/LICENSE +0 -0
  84. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/SKILL.md +0 -0
  85. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/buildx.md +0 -0
  86. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/compose.md +0 -0
  87. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/containers.md +0 -0
  88. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/docs.md +0 -0
  89. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/networks-volumes.md +0 -0
  90. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/observability.md +0 -0
  91. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/registry.md +0 -0
  92. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/reference/scout.md +0 -0
  93. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/workflows/build-publish.md +0 -0
  94. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/workflows/deploy.md +0 -0
  95. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/workflows/maintenance.md +0 -0
  96. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/workflows/security.md +0 -0
  97. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/skills/l337-docker/workflows/troubleshoot.md +0 -0
  98. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/__init__.py +0 -0
  99. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/conftest.py +0 -0
  100. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/__init__.py +0 -0
  101. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/conftest.py +0 -0
  102. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_buildx.py +0 -0
  103. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_cli.py +0 -0
  104. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_cli_flag_drift.py +0 -0
  105. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_compose.py +0 -0
  106. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_containers.py +0 -0
  107. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_context.py +0 -0
  108. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_file_payloads.py +0 -0
  109. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_networks.py +0 -0
  110. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_nodes.py +0 -0
  111. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_registry.py +0 -0
  112. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_remote_exec.py +0 -0
  113. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_scout.py +0 -0
  114. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_skill.py +0 -0
  115. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_smoke.py +0 -0
  116. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/integration/test_stack.py +0 -0
  117. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_buildx.py +0 -0
  118. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_cli.py +0 -0
  119. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_compose.py +0 -0
  120. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_configs.py +0 -0
  121. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_containers.py +0 -0
  122. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_context.py +0 -0
  123. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_docs.py +0 -0
  124. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_env.py +0 -0
  125. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_hosts.py +0 -0
  126. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_labels.py +0 -0
  127. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_main.py +0 -0
  128. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_naming.py +0 -0
  129. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_networks.py +0 -0
  130. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_nodes.py +0 -0
  131. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_prompts.py +0 -0
  132. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_pyproject_pins.py +0 -0
  133. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_registry.py +0 -0
  134. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_remote_exec.py +0 -0
  135. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_resources.py +0 -0
  136. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_scout.py +0 -0
  137. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_secrets.py +0 -0
  138. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_server.py +0 -0
  139. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_services.py +0 -0
  140. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_skill.py +0 -0
  141. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_ssh_proxy.py +0 -0
  142. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_stack.py +0 -0
  143. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_system.py +0 -0
  144. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_utils.py +0 -0
  145. {docker_mcp_server-2.2.4 → docker_mcp_server-2.2.5}/tests/test_volumes.py +0 -0
@@ -304,7 +304,7 @@ When the high-level SDK lacks a method (e.g. swarm node removal, service rollbac
304
304
 
305
305
  Treat "the method exists" as separate from "the method works": the rendered docs show a docstring, not the URL a method builds. `plugin_push` is the standing example — docker-py's `Plugin.push()` / `APIClient.push_plugin()` POST to `/plugins/{name}/pull`, which the Engine does not define (push is `POST /plugins/{name}/push`), so they 404 everywhere; broken since 2017 and still in `main`, because upstream has no test for it. Where a documented method is provably broken, the ladder is: another public SDK path, then the correct endpoint via docker-py's private request helpers (`_url`/`_post`/`_raise_for_status`/`_stream_helper`) resolved through `getattr` and guarded to raise an actionable message rather than `AttributeError` (as `system_logout` does with `api._auth_configs`), then a CLI shell-out if the domain is already CLI-backed. Anything below the first rung leaves the supported surface and is **a human's call, not an agent's**: a PR that adds a *new* private-helper reach-in should be flagged as needing explicit maintainer sign-off, even when it is well built — say so rather than approving it on the strength of the guards. Do flag one that is unguarded, undocumented, or that hits an endpoint absent from the Engine API spec (no compatibility promise, no deprecation cycle — a categorically worse bet than `plugin_push`'s, which uses a spec'd endpoint the CLI itself calls and reaches it through unofficial plumbing). Do **not** re-litigate the reach-ins already blessed above (`plugin_push`, `system_logout`, `stage_build_context`): being present on `main` *is* the sign-off, so a PR that merely touches or refactors one needs no fresh approval — only a genuinely new reach-in does. If a diff both adds and blesses one, that has already been decided; say nothing. And do not treat an audit or routine that *declined* to implement something and documented why as an unfinished job — that write-up is the intended escalation path, and is how `plugin_push` reached a human decision in the first place.
306
306
 
307
- **SDK audit exclusions.** A recurring audit routine looks for uncovered SDK surface and for low-level `client.api.*` calls a high-level method could replace. These were decided on the merits and must not be re-proposed (in review or by the routine): `Plugin.push()`/`APIClient.push_plugin()` — broken upstream, so `plugin_push`'s hand-built endpoint call is the working path, not debt to tidy away; `Container.attach`/`attach_socket`/`resize` — deliberately unwrapped, as an interactive bidirectional stream doesn't fit a request/response tool call (`container_exec` covers scripted exec); `service_rollback`'s `inspect_service`/`update_service` and `system_logout`'s `_auth_configs` — permanently low-level, as the high-level SDK has no `rollback` and no `logout` at all. Anything deliberately not wrapped, or wrapped unobviously, belongs on that list in `CLAUDE.md`.
307
+ **SDK audit exclusions.** A recurring audit routine looks for uncovered SDK surface and for low-level `client.api.*` calls a high-level method could replace. These were decided on the merits and must not be re-proposed (in review or by the routine): `Plugin.push()`/`APIClient.push_plugin()` — broken upstream, so `plugin_push`'s hand-built endpoint call is the working path, not debt to tidy away; `Container.attach`/`attach_socket`/`resize` — deliberately unwrapped, as an interactive bidirectional stream doesn't fit a request/response tool call (`container_exec` covers scripted exec); `service_rollback`'s `inspect_service`/`update_service` and `system_logout`'s `_auth_configs` — permanently low-level, as the high-level SDK has no `rollback` and no `logout` at all; `swarm_task_list`/`swarm_task_inspect`'s `api.tasks()`/`api.inspect_task()` — permanently low-level, as docker-py has no task collection at all. Anything deliberately not wrapped, or wrapped unobviously, belongs on that list in `CLAUDE.md`.
308
308
 
309
309
  Docker SDK docs: https://docker-py.readthedocs.io/en/stable/index.html
310
310
  Docker SDK low-level API: https://docker-py.readthedocs.io/en/stable/api.html
@@ -13,6 +13,10 @@ scripts/
13
13
  # server, not part of it — packing it here would ship ~140 KB of markdown the bundle never reads.
14
14
  skills/
15
15
  .github/
16
+ # Claude Code project config: the mirror-rule PostToolUse hook and the /docker-sdk command. Both are
17
+ # development tooling for this repo, meaningless to an installed bundle, and 2.2.4 shipped them by
18
+ # omission. `.dockerignore` already excludes this directory.
19
+ .claude/
16
20
  .idea/
17
21
  .vscode/
18
22
  .pytest_cache/
@@ -516,6 +516,10 @@ say why. **Anything deliberately not wrapped, or wrapped in an unobvious way, be
516
516
  permanently. The high-level `Service`/`ServiceCollection` expose no `rollback`.
517
517
  - **`system_logout`'s `api._auth_configs`** — stays low-level permanently. There is no `logout`
518
518
  anywhere in the SDK and no server-side session to end, so there is nothing to migrate to.
519
+ - **`swarm_task_list` / `swarm_task_inspect`'s `api.tasks()` / `api.inspect_task()`** — stay
520
+ low-level permanently. docker-py has no task collection at all (there is no `client.tasks`, and
521
+ `docker/models/` has no `tasks.py`), so these documented `APIClient` methods are the only public
522
+ path. Nothing to migrate to; do not propose one.
519
523
 
520
524
  The audit must also **check the latest published docker-py, not the pinned one**: `uv.lock` is
521
525
  routinely behind what `pyproject.toml`'s floor lets a fresh `uvx`/`pip install` resolve, so auditing
@@ -419,7 +419,7 @@ Legend: **✓** direct CLI equivalent; **≈** covered by a documented recipe (l
419
419
  | container_wait (exit) ✓ | `docker wait` |
420
420
  | container_wait (healthy) ≈ | `wait_healthy` loop, `reference/observability.md` |
421
421
 
422
- ### images (14) - `reference/images.md`
422
+ ### images (15) - `reference/images.md`
423
423
 
424
424
  | Tool | CLI |
425
425
  |---|---|
@@ -431,6 +431,7 @@ Legend: **✓** direct CLI equivalent; **≈** covered by a documented recipe (l
431
431
  | image_history ✓ | `docker history` |
432
432
  | image_inspect ✓ | `docker image inspect` |
433
433
  | image_save / load ✓ | `docker save -o` / `load -i` |
434
+ | image_import ✓ | `docker import` |
434
435
  | image_prune ✓ | `docker image prune` |
435
436
  | image_prune_builds ✓ | `docker builder prune` (`--reserved-space`, ex-`--keep-storage`) |
436
437
  | image_search ✓ | `docker search` (Hub only) |
@@ -447,7 +448,7 @@ All ✓ - each maps to the identically-named `docker compose <sub>`: `up`, `down
447
448
  `logs`, `build`, `pull`, `config`, `cp`, `exec`, `run`, `start`, `stop`, `restart`, `kill`,
448
449
  `pause`, `unpause`, `port`, `top`, `images`, `wait`. (`compose_list` → `docker compose ls`.)
449
450
 
450
- ### swarm (8) / services (10) / nodes (5) / secrets (4) / configs (4) / stack (5) - `reference/swarm.md`
451
+ ### swarm (10) / services (10) / nodes (5) / secrets (4) / configs (4) / stack (5) - `reference/swarm.md`
451
452
 
452
453
  | Tool | CLI |
453
454
  |---|---|
@@ -455,6 +456,8 @@ All ✓ - each maps to the identically-named `docker compose <sub>`: `up`, `down
455
456
  | swarm_join_tokens ✓ | `docker swarm join-token <worker\|manager>` |
456
457
  | swarm_unlock / unlock_key ✓ | `docker swarm unlock` / `unlock-key` |
457
458
  | swarm_inspect ✓ | `docker info --format '{{json .Swarm}}'` |
459
+ | swarm_task_list ≈ | `docker service ps $(docker service ls -q)`, `reference/swarm.md` - no CLI command lists the cluster's tasks in one shot |
460
+ | swarm_task_inspect ✓ | `docker inspect --type task` |
458
461
  | service_create / update / remove / logs / ps / inspect / list ✓ | `docker service <sub>` |
459
462
  | service_scale ✓ | `docker service scale` |
460
463
  | service_rollback ✓ | `docker service rollback` |
@@ -477,10 +480,15 @@ All ✓: `docker scout cves/quickview/compare/recommendations/sbom`.
477
480
 
478
481
  All ✓: `docker context create/ls/inspect/rm/use`.
479
482
 
480
- ### plugins (10) - `reference/system.md`
483
+ ### plugins (11) - `reference/system.md`
481
484
 
482
485
  All ✓: `docker plugin install/ls/inspect/enable/disable/set/upgrade/rm/push/create`.
483
486
  `plugin_configure` → `docker plugin set`.
487
+ `plugin_privileges` ≈ no `docker plugin` subcommand prints them, and `docker plugin inspect` needs
488
+ the plugin installed first; the skill reads the plugin's registry config blob over `curl` instead
489
+ (`reference/system.md`), reproducing the install prompt's privilege list without installing
490
+ anything. The recipe mirrors the daemon's own `computePrivileges`, so it reports whichever of the
491
+ seven privilege kinds a given plugin declares rather than a fixed subset.
484
492
 
485
493
  ### registry (7) - `reference/registry.md`
486
494
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docker-mcp-server
3
- Version: 2.2.4
3
+ Version: 2.2.5
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
@@ -265,10 +265,10 @@ Everything above targets one daemon. To manage **several in a single session** -
265
265
  Once loaded, the agent gets MCP tools grouped by Docker domain. A few examples:
266
266
 
267
267
  - **Containers** - `container_run`, `container_list` (`managed_only=True` to list only what this server created - see [Provenance labels](#provenance-labels)), `container_exec`, `container_logs`, `container_stop`, `container_commit`, `container_wait` (block until exit, `until="healthy"` to poll a healthcheck, or `until="log-match"` to poll for a log line containing `pattern`), `container_export` / `container_archive_get_to_file` / `container_archive_put` (stream tar archives to/from a host path)
268
- - **Images** - `image_build`, `image_pull`, `image_push`, `image_tag`, `image_prune`, `image_prune_builds` (clear the daemon's build cache - a separate resource from dangling images), `image_save` / `image_load` (stream image tarballs to/from a host path via `dest_path` / `from_file`)
269
- - **Plugins** - `plugin_install` (pull from a registry), `plugin_create` (build one from a local `config.json` + `rootfs`), `plugin_push` (publish it back), `plugin_enable` / `plugin_disable`, `plugin_configure`, `plugin_upgrade`, `plugin_list` / `plugin_inspect`, `plugin_remove` *(managed engine plugins - volume/network/logging drivers - not the CLI plugins that extend the `docker` command itself)*
268
+ - **Images** - `image_build`, `image_pull`, `image_push`, `image_tag`, `image_prune`, `image_prune_builds` (clear the daemon's build cache - a separate resource from dangling images), `image_save` / `image_load` (stream image tarballs to/from a host path via `dest_path` / `from_file`), `image_import` (build a single-layer image from a flat rootfs tarball, URL, or existing image - `docker import`, not to be confused with `image_load`)
269
+ - **Plugins** - `plugin_install` (pull from a registry), `plugin_privileges` (read what host access a plugin demands *before* installing it - the daemon grants them non-interactively), `plugin_create` (build one from a local `config.json` + `rootfs`), `plugin_push` (publish it back), `plugin_enable` / `plugin_disable`, `plugin_configure`, `plugin_upgrade`, `plugin_list` / `plugin_inspect`, `plugin_remove` *(managed engine plugins - volume/network/logging drivers - not the CLI plugins that extend the `docker` command itself)*
270
270
  - **Networks / Volumes** - `network_create`, `network_connect`, `volume_create`, `volume_prune`
271
- - **Swarm** - `swarm_init`, `swarm_join_tokens` (close the init → join loop), `swarm_update` (rotate join tokens / unlock key), `service_create`, `service_scale`, `service_rollback` (re-apply the previous service spec), `service_wait` (block until tasks converge, or a rolling update completes), `node_list`, `node_wait` (block until a node reaches a target state - e.g. `ready` after joining), `node_remove`, `secret_create`, `config_create`
271
+ - **Swarm** - `swarm_init`, `swarm_join_tokens` (close the init → join loop), `swarm_update` (rotate join tokens / unlock key), `service_create`, `service_scale`, `service_rollback` (re-apply the previous service spec), `service_wait` (block until tasks converge, or a rolling update completes), `swarm_task_list` / `swarm_task_inspect` (every task in the cluster, filterable by node or desired state - no single CLI command does this), `node_list`, `node_wait` (block until a node reaches a target state - e.g. `ready` after joining), `node_remove`, `secret_create`, `config_create`
272
272
  - **System** - `system_ping`, `system_info`, `system_version`, `system_df`, `system_events`, `host_list` (the configured daemons and which is the default - see [Managing several daemons](#managing-several-daemons)), `system_login` / `system_logout` (cache or clear registry credentials), `system_reconnect` (rebuild a host's SDK client to recover a wedged connection)
273
273
  - **Compose** - `compose_up`, `compose_down`, `compose_stop`, `compose_start`, `compose_restart`, `compose_pause` / `compose_unpause`, `compose_kill`, `compose_ps`, `compose_list`, `compose_images`, `compose_top`, `compose_port`, `compose_logs`, `compose_config`, `compose_build`, `compose_pull`, `compose_run`, `compose_exec`, `compose_cp`, `compose_wait` *(wraps the `docker compose` CLI plugin)*
274
274
  - **Stacks** - `stack_deploy`, `stack_list`, `stack_ps`, `stack_services`, `stack_remove` *(deploy a Compose file to a swarm as a stack; wraps the `docker stack` CLI - requires a swarm manager)*
@@ -460,7 +460,7 @@ Connecting this server to an AI agent grants it the same level of access as a lo
460
460
  - **Swarm secret material transits tool calls too.** Beyond registry credentials, several swarm tools carry secret material through arguments or return values that MCP clients may log: `secret_create(data=...)` and `config_create(data=...)` take the payload as an argument, `secret_inspect` / `config_inspect` return the stored object, `swarm_join(join_token=...)` and `swarm_unlock(key=...)` take cluster join/unlock secrets, and `swarm_unlock_key` and `swarm_join_tokens` *return* cluster credentials (rotation via `swarm_update` invalidates old tokens) - a manager join token lets its holder join the swarm as a manager (root-equivalent on the cluster). Treat all of these as exposed in any client that records tool traffic, and prefer provisioning swarm secrets and reading join tokens out-of-band on the host rather than through the agent. If an agent never needs to admit nodes, drop the whole surface with `DOCKER_MCP_SERVER_DISABLE=swarm` (see [Configuration](#configuration)).
461
461
  - **`container_exec`, `compose_exec`, and `compose_run` run arbitrary commands.** When any part of the command is derived from agent-controlled input, use an exec-form argv list that does not invoke a shell (e.g. `["python", "-V"]`). A list like `["sh", "-c", template]` that invokes a shell will interpret shell metacharacters in the untrusted substrings.
462
462
  - **Container archive paths.** `container_archive_get` and `container_archive_put` forward the supplied path verbatim to the daemon. The container is the trust boundary - if you do not trust its filesystem, do not assume `..` traversal will be rejected.
463
- - **File-path payload tools read and write the server host's filesystem.** `image_save`, `container_export` (with `dest_path`), and `container_archive_get_to_file` write to a `dest_path` on the host running this MCP server (refusing to overwrite an existing file unless `overwrite=True`); `image_load` and `container_archive_put` (with `from_file`) read a host path; `compose_cp` copies between a service container and a host path in either direction. These run as the server's user, so the agent can write any file that user can write and read any file it can read. Prefer the in-band byte tools (capped at 32 MiB) when you don't trust the agent with host filesystem access. `DOCKER_MCP_SERVER_READONLY` also drops the host-writing tools - but note it is not targeted at them: it registers *only* read-only tools, so `image_load` and `container_archive_put` (and every other mutating/destructive tool) go too. There is no switch that drops just the file-writers.
463
+ - **File-path payload tools read and write the server host's filesystem.** `image_save`, `container_export` (with `dest_path`), and `container_archive_get_to_file` write to a `dest_path` on the host running this MCP server (refusing to overwrite an existing file unless `overwrite=True`); `image_load`, `image_import`, and `container_archive_put` (with `from_file`) read a host path; `compose_cp` copies between a service container and a host path in either direction. `image_import(from_url=...)` is the one that leaves the host entirely: the *daemon* fetches the URL the caller named, in the daemon's own network namespace, so it can reach anything that daemon can reach (the same caller-chooses-the-destination footing as `image_pull`'s registry argument). These run as the server's user, so the agent can write any file that user can write and read any file it can read. Prefer the in-band byte tools (capped at 32 MiB) when you don't trust the agent with host filesystem access. `DOCKER_MCP_SERVER_READONLY` also drops the host-writing tools - but note it is not targeted at them: it registers *only* read-only tools, so `image_load` and `container_archive_put` (and every other mutating/destructive tool) go too. There is no switch that drops just the file-writers.
464
464
  - **Destructive operations have no built-in confirmation.** `prune_*`, `remove_*`, `container_kill`, `swarm_leave`, `compose_down(volumes=True)`, `compose_kill`, `stack_remove` (tears down every service in a stack), `buildx_prune` (always runs with `--force`), and `buildx_remove` execute immediately. These tools carry the `destructiveHint` annotation, so a client like Claude Code can gate them, and the shipped `clean_environment` prompt asks the agent to confirm before pruning volumes - but tool calls themselves are not gated by the server. For a hard guarantee, run with `DOCKER_MCP_SERVER_NO_DESTRUCTIVE=1` (drops them entirely) or `DOCKER_MCP_SERVER_READONLY=1` (see [Configuration](#configuration)); for an approval step, configure it at the MCP client.
465
465
  - **CLI shell-out attack surface.** Compose, Stack, Buildx, Scout, and Context tools spawn `docker` subprocesses on the host running this MCP server. Every invocation passes arguments as a list (no shell, no metacharacter interpretation), resolves the binary via `shutil.which`, and runs against a scrubbed environment (DOCKER_HOST and related vars only). Positional values (image refs, service / context / builder names, build contexts, bake targets) are additionally rejected if they start with `-`, so an argument can't be smuggled in as a CLI flag (e.g. a service named `--output=...`); the one deliberate exception is the trailing command in `compose_exec` / `compose_run`, which is meant to be an arbitrary argv. Filesystem paths supplied to `compose_*` (project_dir, files) are read by the docker CLI on the server host - passing an unfamiliar path can expose any compose file the server's user can read. **With no local docker CLI and an `ssh://` target** those subprocesses run on the remote host instead ([above](#talking-to-a-remote-daemon)), which shifts two things: the command executes as the remote SSH user with *its* registry credentials, and the files a command reads are copied to that host's temp directory first - so a `buildx_build` `--secret src=` file, or anything else in a staged directory, exists briefly on the remote disk (mode `0700`, removed when the call returns; only a dropped connection can leave it behind). Point staging-backed tools at directories you would be content to copy.
466
466
  - **The daemon set is fixed at startup; pick it deliberately.** When `DOCKER_HOST` / `DOCKER_MCP_SERVER_HOSTS` are unset, the server's *initial* SDK connection follows your active Docker context (`DOCKER_CONTEXT` / `currentContext`) - the same daemon your `docker` CLI targets - so if that context points at a remote or production daemon, the agent connects there too. Set `DOCKER_MCP_SERVER_HOSTS` (or `DOCKER_HOST`, or select a scoped context) before starting the server to pin the target(s) deliberately; with `DOCKER_MCP_SERVER_HOSTS` the `auto`/`local` endpoints are resolved and **pinned at startup**, so they can't drift if a context changes later. After startup, `context_use` only changes the CLI default for subsequent CLI-backed tools; SDK-backed tools keep using the daemon their pooled client connected to. **There is no runtime way to introduce or retarget a daemon at an arbitrary endpoint** - `system_reconnect` only *rebuilds* an already-configured host's client (to recover a wedged connection), it can't point it elsewhere; to add or change a daemon, edit `DOCKER_MCP_SERVER_HOSTS` and restart. This deliberately closes a trust-expansion vector (an agent can't move the root-equivalent boundary to an unvetted endpoint mid-session). `context_create(skip_tls_verify=True)` disables TLS verification for a context; use only against trusted local daemons.
@@ -238,10 +238,10 @@ Everything above targets one daemon. To manage **several in a single session** -
238
238
  Once loaded, the agent gets MCP tools grouped by Docker domain. A few examples:
239
239
 
240
240
  - **Containers** - `container_run`, `container_list` (`managed_only=True` to list only what this server created - see [Provenance labels](#provenance-labels)), `container_exec`, `container_logs`, `container_stop`, `container_commit`, `container_wait` (block until exit, `until="healthy"` to poll a healthcheck, or `until="log-match"` to poll for a log line containing `pattern`), `container_export` / `container_archive_get_to_file` / `container_archive_put` (stream tar archives to/from a host path)
241
- - **Images** - `image_build`, `image_pull`, `image_push`, `image_tag`, `image_prune`, `image_prune_builds` (clear the daemon's build cache - a separate resource from dangling images), `image_save` / `image_load` (stream image tarballs to/from a host path via `dest_path` / `from_file`)
242
- - **Plugins** - `plugin_install` (pull from a registry), `plugin_create` (build one from a local `config.json` + `rootfs`), `plugin_push` (publish it back), `plugin_enable` / `plugin_disable`, `plugin_configure`, `plugin_upgrade`, `plugin_list` / `plugin_inspect`, `plugin_remove` *(managed engine plugins - volume/network/logging drivers - not the CLI plugins that extend the `docker` command itself)*
241
+ - **Images** - `image_build`, `image_pull`, `image_push`, `image_tag`, `image_prune`, `image_prune_builds` (clear the daemon's build cache - a separate resource from dangling images), `image_save` / `image_load` (stream image tarballs to/from a host path via `dest_path` / `from_file`), `image_import` (build a single-layer image from a flat rootfs tarball, URL, or existing image - `docker import`, not to be confused with `image_load`)
242
+ - **Plugins** - `plugin_install` (pull from a registry), `plugin_privileges` (read what host access a plugin demands *before* installing it - the daemon grants them non-interactively), `plugin_create` (build one from a local `config.json` + `rootfs`), `plugin_push` (publish it back), `plugin_enable` / `plugin_disable`, `plugin_configure`, `plugin_upgrade`, `plugin_list` / `plugin_inspect`, `plugin_remove` *(managed engine plugins - volume/network/logging drivers - not the CLI plugins that extend the `docker` command itself)*
243
243
  - **Networks / Volumes** - `network_create`, `network_connect`, `volume_create`, `volume_prune`
244
- - **Swarm** - `swarm_init`, `swarm_join_tokens` (close the init → join loop), `swarm_update` (rotate join tokens / unlock key), `service_create`, `service_scale`, `service_rollback` (re-apply the previous service spec), `service_wait` (block until tasks converge, or a rolling update completes), `node_list`, `node_wait` (block until a node reaches a target state - e.g. `ready` after joining), `node_remove`, `secret_create`, `config_create`
244
+ - **Swarm** - `swarm_init`, `swarm_join_tokens` (close the init → join loop), `swarm_update` (rotate join tokens / unlock key), `service_create`, `service_scale`, `service_rollback` (re-apply the previous service spec), `service_wait` (block until tasks converge, or a rolling update completes), `swarm_task_list` / `swarm_task_inspect` (every task in the cluster, filterable by node or desired state - no single CLI command does this), `node_list`, `node_wait` (block until a node reaches a target state - e.g. `ready` after joining), `node_remove`, `secret_create`, `config_create`
245
245
  - **System** - `system_ping`, `system_info`, `system_version`, `system_df`, `system_events`, `host_list` (the configured daemons and which is the default - see [Managing several daemons](#managing-several-daemons)), `system_login` / `system_logout` (cache or clear registry credentials), `system_reconnect` (rebuild a host's SDK client to recover a wedged connection)
246
246
  - **Compose** - `compose_up`, `compose_down`, `compose_stop`, `compose_start`, `compose_restart`, `compose_pause` / `compose_unpause`, `compose_kill`, `compose_ps`, `compose_list`, `compose_images`, `compose_top`, `compose_port`, `compose_logs`, `compose_config`, `compose_build`, `compose_pull`, `compose_run`, `compose_exec`, `compose_cp`, `compose_wait` *(wraps the `docker compose` CLI plugin)*
247
247
  - **Stacks** - `stack_deploy`, `stack_list`, `stack_ps`, `stack_services`, `stack_remove` *(deploy a Compose file to a swarm as a stack; wraps the `docker stack` CLI - requires a swarm manager)*
@@ -433,7 +433,7 @@ Connecting this server to an AI agent grants it the same level of access as a lo
433
433
  - **Swarm secret material transits tool calls too.** Beyond registry credentials, several swarm tools carry secret material through arguments or return values that MCP clients may log: `secret_create(data=...)` and `config_create(data=...)` take the payload as an argument, `secret_inspect` / `config_inspect` return the stored object, `swarm_join(join_token=...)` and `swarm_unlock(key=...)` take cluster join/unlock secrets, and `swarm_unlock_key` and `swarm_join_tokens` *return* cluster credentials (rotation via `swarm_update` invalidates old tokens) - a manager join token lets its holder join the swarm as a manager (root-equivalent on the cluster). Treat all of these as exposed in any client that records tool traffic, and prefer provisioning swarm secrets and reading join tokens out-of-band on the host rather than through the agent. If an agent never needs to admit nodes, drop the whole surface with `DOCKER_MCP_SERVER_DISABLE=swarm` (see [Configuration](#configuration)).
434
434
  - **`container_exec`, `compose_exec`, and `compose_run` run arbitrary commands.** When any part of the command is derived from agent-controlled input, use an exec-form argv list that does not invoke a shell (e.g. `["python", "-V"]`). A list like `["sh", "-c", template]` that invokes a shell will interpret shell metacharacters in the untrusted substrings.
435
435
  - **Container archive paths.** `container_archive_get` and `container_archive_put` forward the supplied path verbatim to the daemon. The container is the trust boundary - if you do not trust its filesystem, do not assume `..` traversal will be rejected.
436
- - **File-path payload tools read and write the server host's filesystem.** `image_save`, `container_export` (with `dest_path`), and `container_archive_get_to_file` write to a `dest_path` on the host running this MCP server (refusing to overwrite an existing file unless `overwrite=True`); `image_load` and `container_archive_put` (with `from_file`) read a host path; `compose_cp` copies between a service container and a host path in either direction. These run as the server's user, so the agent can write any file that user can write and read any file it can read. Prefer the in-band byte tools (capped at 32 MiB) when you don't trust the agent with host filesystem access. `DOCKER_MCP_SERVER_READONLY` also drops the host-writing tools - but note it is not targeted at them: it registers *only* read-only tools, so `image_load` and `container_archive_put` (and every other mutating/destructive tool) go too. There is no switch that drops just the file-writers.
436
+ - **File-path payload tools read and write the server host's filesystem.** `image_save`, `container_export` (with `dest_path`), and `container_archive_get_to_file` write to a `dest_path` on the host running this MCP server (refusing to overwrite an existing file unless `overwrite=True`); `image_load`, `image_import`, and `container_archive_put` (with `from_file`) read a host path; `compose_cp` copies between a service container and a host path in either direction. `image_import(from_url=...)` is the one that leaves the host entirely: the *daemon* fetches the URL the caller named, in the daemon's own network namespace, so it can reach anything that daemon can reach (the same caller-chooses-the-destination footing as `image_pull`'s registry argument). These run as the server's user, so the agent can write any file that user can write and read any file it can read. Prefer the in-band byte tools (capped at 32 MiB) when you don't trust the agent with host filesystem access. `DOCKER_MCP_SERVER_READONLY` also drops the host-writing tools - but note it is not targeted at them: it registers *only* read-only tools, so `image_load` and `container_archive_put` (and every other mutating/destructive tool) go too. There is no switch that drops just the file-writers.
437
437
  - **Destructive operations have no built-in confirmation.** `prune_*`, `remove_*`, `container_kill`, `swarm_leave`, `compose_down(volumes=True)`, `compose_kill`, `stack_remove` (tears down every service in a stack), `buildx_prune` (always runs with `--force`), and `buildx_remove` execute immediately. These tools carry the `destructiveHint` annotation, so a client like Claude Code can gate them, and the shipped `clean_environment` prompt asks the agent to confirm before pruning volumes - but tool calls themselves are not gated by the server. For a hard guarantee, run with `DOCKER_MCP_SERVER_NO_DESTRUCTIVE=1` (drops them entirely) or `DOCKER_MCP_SERVER_READONLY=1` (see [Configuration](#configuration)); for an approval step, configure it at the MCP client.
438
438
  - **CLI shell-out attack surface.** Compose, Stack, Buildx, Scout, and Context tools spawn `docker` subprocesses on the host running this MCP server. Every invocation passes arguments as a list (no shell, no metacharacter interpretation), resolves the binary via `shutil.which`, and runs against a scrubbed environment (DOCKER_HOST and related vars only). Positional values (image refs, service / context / builder names, build contexts, bake targets) are additionally rejected if they start with `-`, so an argument can't be smuggled in as a CLI flag (e.g. a service named `--output=...`); the one deliberate exception is the trailing command in `compose_exec` / `compose_run`, which is meant to be an arbitrary argv. Filesystem paths supplied to `compose_*` (project_dir, files) are read by the docker CLI on the server host - passing an unfamiliar path can expose any compose file the server's user can read. **With no local docker CLI and an `ssh://` target** those subprocesses run on the remote host instead ([above](#talking-to-a-remote-daemon)), which shifts two things: the command executes as the remote SSH user with *its* registry credentials, and the files a command reads are copied to that host's temp directory first - so a `buildx_build` `--secret src=` file, or anything else in a staged directory, exists briefly on the remote disk (mode `0700`, removed when the call returns; only a dropped connection can leave it behind). Point staging-backed tools at directories you would be content to copy.
439
439
  - **The daemon set is fixed at startup; pick it deliberately.** When `DOCKER_HOST` / `DOCKER_MCP_SERVER_HOSTS` are unset, the server's *initial* SDK connection follows your active Docker context (`DOCKER_CONTEXT` / `currentContext`) - the same daemon your `docker` CLI targets - so if that context points at a remote or production daemon, the agent connects there too. Set `DOCKER_MCP_SERVER_HOSTS` (or `DOCKER_HOST`, or select a scoped context) before starting the server to pin the target(s) deliberately; with `DOCKER_MCP_SERVER_HOSTS` the `auto`/`local` endpoints are resolved and **pinned at startup**, so they can't drift if a context changes later. After startup, `context_use` only changes the CLI default for subsequent CLI-backed tools; SDK-backed tools keep using the daemon their pooled client connected to. **There is no runtime way to introduce or retarget a daemon at an arbitrary endpoint** - `system_reconnect` only *rebuilds* an already-configured host's client (to recover a wedged connection), it can't point it elsewhere; to add or change a daemon, edit `DOCKER_MCP_SERVER_HOSTS` and restart. This deliberately closes a trust-expansion vector (an agent can't move the root-equivalent boundary to an unvetted endpoint mid-session). `context_create(skip_tls_verify=True)` disables TLS verification for a context; use only against trusted local daemons.
@@ -83,6 +83,7 @@ TOOL_CATEGORIES: dict[str, ToolCategory] = {
83
83
  "image_prune": ToolCategory.DESTRUCTIVE,
84
84
  "image_prune_builds": ToolCategory.DESTRUCTIVE,
85
85
  "image_load": ToolCategory.MUTATING,
86
+ "image_import": ToolCategory.MUTATING,
86
87
  "image_save": ToolCategory.MUTATING, # can write a file on the server host (dest_path)
87
88
  "image_tag": ToolCategory.MUTATING,
88
89
  "image_history": ToolCategory.READ_ONLY,
@@ -136,10 +137,13 @@ TOOL_CATEGORIES: dict[str, ToolCategory] = {
136
137
  "swarm_unlock": ToolCategory.MUTATING,
137
138
  "swarm_unlock_key": ToolCategory.READ_ONLY,
138
139
  "swarm_join_tokens": ToolCategory.READ_ONLY,
140
+ "swarm_task_list": ToolCategory.READ_ONLY,
141
+ "swarm_task_inspect": ToolCategory.READ_ONLY,
139
142
  # plugins
140
143
  "plugin_create": ToolCategory.MUTATING,
141
144
  "plugin_inspect": ToolCategory.READ_ONLY,
142
145
  "plugin_install": ToolCategory.MUTATING,
146
+ "plugin_privileges": ToolCategory.READ_ONLY,
143
147
  "plugin_push": ToolCategory.MUTATING,
144
148
  "plugin_list": ToolCategory.READ_ONLY,
145
149
  "plugin_configure": ToolCategory.MUTATING,
@@ -486,7 +490,7 @@ _DOMAIN_BLURBS: dict[str, str] = {
486
490
  "volumes": "create/list/inspect/remove",
487
491
  "compose": "Docker Compose v2 (up/down/ps/logs/build/run/exec/...); CLI-backed",
488
492
  "stack": "Compose-on-Swarm (deploy/ls/ps/rm/services); CLI-backed",
489
- "swarm": "swarm init/join/leave/unlock, join-tokens; manager node only",
493
+ "swarm": "swarm init/join/leave/unlock, join-tokens, cluster-wide task list/inspect; manager node only",
490
494
  "services": "Swarm services (create/scale/update/rollback/logs/tasks); manager node only",
491
495
  "nodes": "Swarm nodes (list/inspect/update/remove); manager node only",
492
496
  "secrets": "Swarm secrets; manager node only",
@@ -315,7 +315,8 @@ def image_load(data: bytes | None = None, from_file: str | None = None, host: st
315
315
  Load an image from a tarball produced by `image_save`, from in-band bytes or a file on the server host.
316
316
 
317
317
  Counterpart of `image_save`; when the image lives in a registry, `image_pull` is the normal
318
- route. Pass exactly one of `data` (tarball bytes in band) or `from_file` (a path on the server host,
318
+ route, and for a flat rootfs archive that is not a `docker save` bundle use `image_import`.
319
+ Pass exactly one of `data` (tarball bytes in band) or `from_file` (a path on the server host,
319
320
  streamed straight to the daemon — preferred for anything but small images, since in-band bytes are
320
321
  base64-encoded by MCP). `from_file` is read by the server's user; `~` is expanded.
321
322
 
@@ -333,6 +334,109 @@ def image_load(data: bytes | None = None, from_file: str | None = None, host: st
333
334
  return [i.attrs for i in _get_client(host).images.load(handle)]
334
335
 
335
336
 
337
+ @tool()
338
+ def image_import(
339
+ repository: str | None = None,
340
+ tag: str | None = None,
341
+ from_file: str | None = None,
342
+ data: bytes | None = None,
343
+ from_url: str | None = None,
344
+ from_image: str | None = None,
345
+ changes: list | None = None,
346
+ host: str | None = None,
347
+ ) -> str:
348
+ """
349
+ Create an image from a flat root-filesystem tarball, like `docker import`.
350
+
351
+ Imports a *filesystem* archive as a new single-layer image with no build history — not the same
352
+ thing as `image_load`, which restores a `docker save` archive complete with its layers, tags and
353
+ history, so prefer `image_load` for anything `image_save` produced. Use this for a rootfs that
354
+ came from somewhere else: a `container_export` archive, a distro base tarball, a VM image dump.
355
+ The result has an empty config — no `CMD`/`ENTRYPOINT`/`ENV` — unless you supply `changes`, so an
356
+ imported image is usually not runnable until you set at least a command. Pass exactly one source
357
+ (`from_file`, `data`, `from_url` or `from_image`); ValueError otherwise. `from_url` and
358
+ `from_image` are fetched by the *daemon*, `from_file`/`data` are read here and uploaded; a
359
+ `from_file` path that is not a readable file raises rather than being retried as a URL. Unlike
360
+ the other image-creating tools this stamps no provenance labels: the Engine's import call accepts
361
+ no labels field, and `changes` does not cover `LABEL`.
362
+
363
+ args:
364
+ repository - Repository name to give the new image, e.g. "myorg/rootfs"; may include a tag
365
+ (`myorg/rootfs:v1`), and defaults to `:latest` when it does not. Omit to import untagged,
366
+ addressable only by the id in the returned progress (omit it entirely -- a blank string
367
+ is a ValueError, not a shorthand for untagged). A digest reference is refused by the
368
+ daemon. Required if `tag` is given
369
+ tag - Tag to apply, e.g. "v1". **Overrides** a tag already in `repository` rather than being
370
+ ignored, so passing `repository="myorg/rootfs:v1"` with `tag="v2"` yields `:v2`. Requires
371
+ `repository` (ValueError without it — the daemon would otherwise silently drop the tag
372
+ and import untagged). Blank is also a ValueError, not a shorthand for the default: the
373
+ daemon would substitute `latest` without saying so
374
+ from_file - Path to a rootfs tarball on the server host (`~` expanded), read by the server's
375
+ user; FileNotFoundError if it is not an existing regular file; exactly one source
376
+ data - Rootfs tarball contents in band (base64-encoded by MCP, so prefer `from_file` for
377
+ anything but small archives); exactly one source
378
+ from_url - URL the daemon fetches the tarball from; exactly one source
379
+ from_image - Name of an existing image to import from, like a Dockerfile `FROM`; exactly one
380
+ source
381
+ changes - Dockerfile instructions applied to the new image, e.g. ['CMD ["/bin/sh"]']; only
382
+ CMD, ENTRYPOINT, ENV, EXPOSE, ONBUILD, USER, VOLUME and WORKDIR are supported. Parsed
383
+ as real Dockerfile syntax, so shell form is wrapped exactly as a Dockerfile would wrap
384
+ it (`CMD /bin/sh` is stored as `["/bin/sh","-c","/bin/sh"]`) — use the exec form
385
+ `CMD ["/bin/sh"]` to store a bare argv
386
+ returns: str - The daemon's raw newline-delimited JSON progress records; the final record carries
387
+ the new image id as its `status`
388
+ """
389
+ sources = {"from_file": from_file, "data": data, "from_url": from_url, "from_image": from_image}
390
+ supplied = [name for name, value in sources.items() if value is not None]
391
+ if len(supplied) != 1:
392
+ raise ValueError(
393
+ "Pass exactly one of `from_file`, `data`, `from_url` or `from_image` "
394
+ f"(got {', '.join(supplied) if supplied else 'none'})."
395
+ )
396
+ # A bare `tag` is refused rather than forwarded: the Engine returns early when `repo` is empty
397
+ # (moby's httputils.RepoTagReference), so the tag is silently dropped and the image lands
398
+ # untagged -- a caller asking for `:v1` would get no error and no tag. A blank `repository`
399
+ # reaches that same early return, verified against a live daemon (repository="", tag=... imports
400
+ # with RepoTags []), so it is refused rather than read as "no repository": passing one is never
401
+ # meaningful, and treating it as absent would reopen the silent drop through the back door.
402
+ if repository is not None and not repository.strip():
403
+ raise ValueError("`repository` cannot be blank; omit it entirely to import untagged.")
404
+ # A blank `tag` is the same silent substitution one step further on: `RepoTagReference` tests
405
+ # `tag != ""`, so an empty tag skips the WithTag path and falls through to `TagNameOnly`, which
406
+ # supplies `:latest`. The caller asked for one tag and would get a different one, with no error --
407
+ # so it is refused alongside a blank `repository` rather than left as the asymmetric case.
408
+ if tag is not None and not tag.strip():
409
+ raise ValueError("`tag` cannot be blank; omit it to accept the daemon's default of `latest`.")
410
+ if tag is not None and repository is None:
411
+ raise ValueError(
412
+ "`tag` needs a `repository` to attach to; pass `repository`, or omit `tag` to import untagged."
413
+ )
414
+
415
+ # The high-level ImageCollection has no import; these four are the documented low-level calls.
416
+ api = _get_client(host).api
417
+ common = drop_none(repository=repository, tag=tag, changes=changes)
418
+ if from_file is not None:
419
+ # `import_image_from_file` is a one-line delegation to `import_image(src=...)`, which sends
420
+ # the path as `fromSrc` whenever `docker.utils.is_file(src)` is false -- so a missing path
421
+ # (or a directory) is not an error but an instruction to the *daemon* to fetch that string
422
+ # over HTTP, in the daemon's network namespace. Verified against a live daemon:
423
+ # `from_file="127.0.0.1:9/rootfs.tar"` produced `Get "http://127.0.0.1:9/rootfs.tar"`.
424
+ # `docker import` itself opens the file and reports ENOENT, so this guard restores CLI
425
+ # parity and keeps `from_url` the only source that leaves the host.
426
+ path = host_read_path(from_file)
427
+ if not path.is_file():
428
+ raise FileNotFoundError(
429
+ f"No such rootfs tarball: {path}. Pass `from_url` to have the daemon fetch a URL, "
430
+ "or `data` to send the archive in band."
431
+ )
432
+ return api.import_image_from_file(str(path), **common)
433
+ if data is not None:
434
+ return api.import_image_from_data(data, **common)
435
+ if from_url is not None:
436
+ return api.import_image_from_url(from_url, **common)
437
+ return api.import_image_from_image(cast(str, from_image), **common)
438
+
439
+
336
440
  @tool()
337
441
  def image_save(
338
442
  id_or_name: str,
@@ -63,7 +63,8 @@ def plugin_install(remote: str, local_name: str | None = None, host: str | None
63
63
  Install a plugin from Docker Hub.
64
64
 
65
65
  `remote` is a Docker Hub reference in `author/name:tag` form, e.g.
66
- `vieux/sshfs:latest`. The daemon handles permission grants non-interactively.
66
+ `vieux/sshfs:latest`. The daemon handles permission grants non-interactively — call
67
+ `plugin_privileges` first to see what host access the plugin is asking for.
67
68
  After installation use `plugin_inspect` to confirm the plugin's enabled state, then call
68
69
  `plugin_enable` to activate it if needed, and optionally `plugin_configure` first if
69
70
  it requires settings. Use `plugin_list` to list all plugins, or `plugin_remove` to
@@ -77,6 +78,30 @@ def plugin_install(remote: str, local_name: str | None = None, host: str | None
77
78
  return _get_client(host).plugins.install(remote, local_name=local_name).attrs
78
79
 
79
80
 
81
+ @tool()
82
+ def plugin_privileges(remote: str, host: str | None = None) -> list:
83
+ """
84
+ Ask the registry which host privileges a not-yet-installed plugin demands.
85
+
86
+ The review step before `plugin_install`, which grants these privileges non-interactively (the
87
+ daemon never prompts) — so this is the only chance to see what a plugin wants before it has it.
88
+ Worth checking for anything not already trusted: plugins routinely request host mounts, devices,
89
+ and elevated capabilities, and a granted privilege is host-level access, not container-scoped.
90
+ Reads the *remote* plugin from its registry and installs nothing; for the privileges of a plugin
91
+ already installed, read `Config` from `plugin_inspect` instead. Credentials come from
92
+ `system_login`, or from `~/.docker/config.json` if the host ran `docker login`. Raises if the
93
+ reference cannot be resolved in the registry.
94
+
95
+ args: remote - Registry plugin reference, `author/name:tag`; the `:latest` tag is optional and
96
+ is the default if omitted
97
+ returns: list - One dict per requested privilege ({"Name", "Description", "Value"}), e.g. Name
98
+ "mount" with Value ["/data"], or "capabilities" with Value ["CAP_SYS_ADMIN"]; empty if the
99
+ plugin requests none
100
+ """
101
+ # The high-level PluginCollection exposes no privileges call; this is the documented low-level one.
102
+ return _get_client(host).api.plugin_privileges(remote)
103
+
104
+
80
105
  @tool()
81
106
  def plugin_push(name: str, timeout_seconds: float = 300.0, host: str | None = None) -> dict:
82
107
  """
@@ -217,3 +217,64 @@ def swarm_join_tokens(host: str | None = None) -> dict:
217
217
  swarm = _get_client(host).swarm
218
218
  swarm.reload()
219
219
  return _read_join_tokens(swarm)
220
+
221
+
222
+ # --- cluster-wide task queries ---
223
+ #
224
+ # Tasks are swarm objects, so these live here and share the `swarm` domain gate: a user who sets
225
+ # DOCKER_MCP_SERVER_DISABLE=swarm expects everything named `swarm_*` to go with it. They are the
226
+ # one part of this module that is not cluster lifecycle, and they read what `services.py` writes --
227
+ # `service_ps` is the per-service view of the same task documents.
228
+ #
229
+ # Both stay on the low-level `client.api`: docker-py has no task collection at all (there is no
230
+ # `client.tasks`), so these documented `APIClient` methods are the only public path.
231
+
232
+
233
+ @tool()
234
+ def swarm_task_list(filters: dict | None = None, host: str | None = None) -> list:
235
+ """
236
+ List tasks across the whole swarm, like `docker service ps` with no service to scope it.
237
+
238
+ The cluster-wide view of what is actually scheduled. `service_ps` covers one service and
239
+ `stack_ps` one stack, so answering "what is failing anywhere" or "what is running on this node"
240
+ through those means looping over every service; this is one call. Filter by `node` for a node's
241
+ workload (the CLI's `docker node ps`), `desired-state` to separate what should be running from
242
+ what is shutting down, or `service` for a single service -- for which `service_ps` is the
243
+ simpler call. Each task carries its full `Spec`, including the `ContainerSpec` (image, command,
244
+ env), so this returns much more per task than the `service-tasks://{id_or_name}` resource's
245
+ computed rollout summary. Read-only. Requires a swarm manager: any other node raises
246
+ `docker.errors.APIError`.
247
+
248
+ args:
249
+ filters - Filter dict; keys: id, name, service, node, label, desired-state
250
+ (running|shutdown|accepted); omit for every task in the cluster
251
+ returns: list - One full task document per task (ID, ServiceID, NodeID, Slot, Spec, Status,
252
+ DesiredState), the same shape `service_ps` returns
253
+ """
254
+ return _get_client(host).api.tasks(filters=filters)
255
+
256
+
257
+ @tool()
258
+ def swarm_task_inspect(id_or_name: str, host: str | None = None) -> dict:
259
+ """
260
+ Inspect a single swarm task, like `docker inspect --type task`.
261
+
262
+ For when you already hold a task reference -- from a `swarm_task_list` or `service_ps` row, a
263
+ service event, or an error message -- and want just that task. `swarm_task_list` returns the
264
+ same document for every task, so prefer it when scanning; this is the single-object fetch.
265
+ To reach the container behind a running task, read `Status.ContainerStatus.ContainerID` and pass
266
+ it to `container_inspect` / `container_logs` -- but note the container may be on another node,
267
+ where those tools cannot see it, and `service_logs` aggregates across tasks instead. Read-only.
268
+ Requires a swarm manager; raises `docker.errors.APIError` if the task does not exist, if a
269
+ prefix matches more than one task, or if this node is not a manager.
270
+
271
+ args:
272
+ id_or_name - The task id, an unambiguous id prefix, or the task's full name -- which is the
273
+ container-name form `<service>.<slot>.<taskid>` (`<service>.<nodeid>.<taskid>` for a
274
+ global service), NOT the shorter `<service>.<slot>` that `docker service ps` prints in
275
+ its NAME column, which does not resolve. The daemon tries full id, then full name, then
276
+ prefix, and rejects an ambiguous prefix rather than picking a match
277
+ returns: dict - Full task inspect payload, as `docker inspect --type task`. Carries no name
278
+ field of its own; compose one from `ServiceID`/`Slot` if you need it
279
+ """
280
+ return _get_client(host).api.inspect_task(id_or_name)
@@ -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.4",
5
+ "version": "2.2.5",
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\nIt gives you much more control and flexibility than calling the Docker CLI directly: each operation is exposed as its own typed tool, marked read-only or not, with destructive actions separately flagged. This means a client can auto-approve reads while always confirming anything destructive, and the whole server can also be switched into a read-only or no-destructive mode as a blanket safeguard. Output is bounded rather than left to grow unboundedly - capped with a truncated flag instead of silently overflowing the agent's context.\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\nDocumentation is built for the agent, not just the person configuring it: an MCP resource exposes the Docker SDK reference in-session (with a tool-callable fallback for clients that can't read resources), and a live tool-catalog resource reports exactly what's registered under the current configuration. Each tool's own description names its nearest siblings and when to prefer each, states preconditions and side effects in plain language, and is honest about when it can still fail - so an agent can pick the right tool on the first try among 150+ options, not guess.\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.4"
3
+ version = "2.2.5"
4
4
  description = "MCP server for managing Docker resources via the Docker SDK for Python"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -7,22 +7,22 @@
7
7
  "url": "https://github.com/L337-org/docker-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "2.2.4",
10
+ "version": "2.2.5",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "docker-mcp-server",
15
- "version": "2.2.4",
15
+ "version": "2.2.5",
16
16
  "transport": { "type": "stdio" }
17
17
  },
18
18
  {
19
19
  "registryType": "oci",
20
- "identifier": "ghcr.io/l337-org/docker-mcp-server:2.2.4",
20
+ "identifier": "ghcr.io/l337-org/docker-mcp-server:2.2.5",
21
21
  "transport": { "type": "stdio" }
22
22
  },
23
23
  {
24
24
  "registryType": "mcpb",
25
- "identifier": "https://github.com/L337-org/docker-mcp/releases/download/v2.2.4/docker-mcp-server-2.2.4.mcpb",
25
+ "identifier": "https://github.com/L337-org/docker-mcp/releases/download/v2.2.5/docker-mcp-server-2.2.5.mcpb",
26
26
  "fileSha256": "0000000000000000000000000000000000000000000000000000000000000000",
27
27
  "transport": { "type": "stdio" }
28
28
  }
@@ -103,6 +103,23 @@ Always use `-o`/`-i`. A bare `docker save` writes a multi-hundred-megabyte tar t
103
103
  `export`/`import` (see `reference/containers.md`) flatten and lose all of it; they are not
104
104
  interchangeable.
105
105
 
106
+ ## Importing a flat rootfs
107
+
108
+ ```bash
109
+ docker import rootfs.tar myorg/rootfs:v1 # tar of a filesystem -> 1-layer image
110
+ docker import --change 'CMD /bin/sh' rootfs.tar myorg/rootfs:v1
111
+ docker import https://example.com/rootfs.tar myorg/rootfs:v1
112
+ ```
113
+
114
+ - `import` takes a **filesystem** tar, `load` takes a `docker save` bundle. Feeding a save bundle to
115
+ `import` "works" and produces a useless image whose root is the bundle's own metadata files.
116
+ - An imported image has an empty config - no `CMD`, `ENTRYPOINT` or `ENV` - so it will not run until
117
+ you set one. `--change` accepts only `CMD`, `ENTRYPOINT`, `ENV`, `EXPOSE`, `ONBUILD`, `USER`,
118
+ `VOLUME` and `WORKDIR`; `LABEL` is not among them, so an imported image cannot be
119
+ provenance-labelled at import time.
120
+ - The URL form is fetched by the **daemon**, not by the shell - so it resolves in the daemon's
121
+ network namespace, which matters against a remote host.
122
+
106
123
  ## Removing and pruning
107
124
 
108
125
  ```bash
@@ -82,6 +82,25 @@ docker service ps <svc> --no-trunc --format json | jq -rs \
82
82
  `--no-trunc` matters here - the `Error` column is where the real reason lives and it is truncated
83
83
  by default.
84
84
 
85
+ ### Tasks across the whole cluster
86
+
87
+ There is no CLI command that lists every task in the swarm; `docker service ps` always needs a
88
+ service. Fan out over the service list instead:
89
+
90
+ ```bash
91
+ for svc in $(docker service ls -q); do
92
+ docker service ps "$svc" --no-trunc --format json
93
+ done | jq -rs '.[] | select(.CurrentState | startswith("Running") | not)
94
+ | [.Name, .Node, .CurrentState, .Error] | @tsv'
95
+ docker node ps <node> --no-trunc # the other axis: every task on one node
96
+ docker inspect --type task <task-id> # one task, full document
97
+ ```
98
+
99
+ `docker service ps` prints `NAME` as `<service>.<slot>`, but `docker inspect --type task` will not
100
+ resolve that form - pass the task `ID` from the same row instead. (The MCP server exposes the
101
+ cluster-wide read directly as `swarm_task_list`, with `node`, `service` and `desired-state`
102
+ filters.)
103
+
85
104
  ### Creating and updating
86
105
 
87
106
  ```bash