docker-mcp-server 2.1.1__tar.gz → 2.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker_mcp_server-2.1.3/.claude/settings.json +15 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/copilot-instructions.md +69 -19
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/workflows/canary.yaml +2 -2
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/workflows/codeql.yaml +3 -3
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/workflows/premerge.yaml +4 -4
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/workflows/publish-homebrew.yaml +8 -5
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/workflows/publish.yaml +21 -11
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/CLAUDE.md +76 -19
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/CONTRIBUTING.md +1 -1
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/DOCKERHUB.md +7 -7
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/PKG-INFO +23 -23
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/PRIVACY.md +1 -1
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/README.md +19 -19
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/SECURITY.md +1 -1
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/buildx.py +7 -1
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/compose.py +10 -1
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/configs.py +6 -2
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/containers.py +50 -18
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/networks.py +18 -9
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/nodes.py +7 -3
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/plugins.py +14 -5
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/secrets.py +11 -5
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/services.py +9 -3
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/system.py +8 -2
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/volumes.py +7 -3
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/manifest.json +6 -6
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/pyproject.toml +4 -4
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/scripts/docker-mcp-server.rb.tpl +4 -4
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/server.json +4 -4
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_stack.py +14 -2
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/uv.lock +1 -1
- docker_mcp_server-2.1.1/.claude/settings.json +0 -15
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.claude/commands/docker-sdk.md +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.dockerignore +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/CODEOWNERS +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/actions/file-failure-issue/action.yaml +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/dependabot.yaml +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/release.yml +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.github/workflows/images.yaml +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.gitignore +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.mcpbignore +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/.python-version +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/CODE_OF_CONDUCT.md +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/Dockerfile +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/LICENSE +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/MIGRATION-2.0.md +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/assets/README.md +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/assets/icon.png +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/__init__.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/__main__.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/_env.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/_hosts.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/server.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/__init__.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/_cli.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/_labels.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/_ssh_proxy.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/_utils.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/context.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/images.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/prompts.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/registry.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/resources.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/scout.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/stack.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/docker_mcp/tools/swarm.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/glama.json +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/mcpb_run.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/scripts/build-mcpb.sh +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/__init__.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/conftest.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/__init__.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/conftest.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_buildx.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_cli.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_compose.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_containers.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_context.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_file_payloads.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_networks.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_nodes.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_registry.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_scout.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_services.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/integration/test_smoke.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_buildx.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_cli.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_compose.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_configs.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_containers.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_context.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_env.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_hosts.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_images.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_labels.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_main.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_naming.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_networks.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_nodes.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_plugins.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_prompts.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_pyproject_pins.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_registry.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_resources.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_scout.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_secrets.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_server.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_services.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_ssh_proxy.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_stack.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_swarm.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_system.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_utils.py +0 -0
- {docker_mcp_server-2.1.1 → docker_mcp_server-2.1.3}/tests/test_volumes.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "python3 -c \"import json,sys,os; d=json.load(sys.stdin); fp=(d.get('tool_input') or {}).get('file_path',''); root=os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd(); print(json.dumps({'hookSpecificOutput':{'hookEventName':'PostToolUse','additionalContext':'You edited CLAUDE.md \\u2014 MIRROR RULE: make the equivalent change in .github/copilot-instructions.md (it drives GitHub Copilot review of every PR). Treat the change as incomplete until both files reflect it.'}})) if fp and os.path.normpath(os.path.join(root, fp)) == os.path.normpath(os.path.join(root, 'CLAUDE.md')) else None\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
## Checklist
|
|
10
10
|
|
|
11
11
|
- [ ] `uv run pytest -v`, `uv run ruff check .`, `uv run ruff format --check .`, and `uv run pyright` all pass locally
|
|
12
|
-
- [ ] If this adds or changes a tool: the ["Checklist when adding a new tool module"](https://github.com/
|
|
12
|
+
- [ ] If this adds or changes a tool: the ["Checklist when adding a new tool module"](https://github.com/L337-org/docker-mcp/blob/main/CONTRIBUTING.md#checklist-when-adding-a-new-tool-module) in `CONTRIBUTING.md` has been followed (tests, prompts, resources, README, naming convention)
|
|
13
13
|
- [ ] If this changes project structure, conventions, env vars, or the tool/prompt/resource surface: both `CLAUDE.md` and `.github/copilot-instructions.md` are updated (see the mirror rule at the top of `CLAUDE.md`)
|
|
14
14
|
- [ ] If this changes a dependency: `uv.lock` is updated (`uv lock`) and committed alongside `pyproject.toml`
|
|
@@ -4,7 +4,7 @@ This file provides guidance to GitHub Copilot when working with code in this rep
|
|
|
4
4
|
|
|
5
5
|
## Project
|
|
6
6
|
|
|
7
|
-
`docker-mcp` is a Python MCP (Model Context Protocol) server that exposes the Docker SDK for Python — plus selected docker CLI features (Compose, Context, Buildx, Scout) and direct OCI-registry HTTPS access — as MCP tools. It requires Python >=3.14 and is managed with `uv`. It is published to PyPI as **`docker-mcp-server`** and as a container image to GHCR (`ghcr.io/
|
|
7
|
+
`docker-mcp` is a Python MCP (Model Context Protocol) server that exposes the Docker SDK for Python — plus selected docker CLI features (Compose, Context, Buildx, Scout) and direct OCI-registry HTTPS access — as MCP tools. It requires Python >=3.14 and is managed with `uv`. It is published to PyPI as **`docker-mcp-server`** and as a container image to GHCR (`ghcr.io/l337-org/docker-mcp-server`), mirrored to Docker Hub (`gavinlucas/docker-mcp-server`) when the opt-in `DOCKERHUB_*` release secrets are configured (the import package stays `docker_mcp`, the repo stays `…/docker-mcp`); two console scripts — `docker-mcp` and `docker-mcp-server` — both target `docker_mcp:main`. A third distribution channel packages the server as a **Claude Desktop Extension (`.mcpb`)** attached to each GitHub Release (see "Desktop Extension (MCPB)" below). A fourth channel (**Homebrew tap**) exists in `L337-org/homebrew-tap` but is currently **paused** — see "Homebrew tap" below.
|
|
8
8
|
|
|
9
9
|
The `docker` dep uses the `[ssh]` extra (paramiko), so `DOCKER_HOST=ssh://…` works via a pure-Python transport (no system `ssh` binary; works in the container images). docker-py auto-selects paramiko for `ssh://`, so there's no transport code — only the `ssh://` branch in `system._connection_help`. CLI-backed tools (Compose, Buildx, Context, Scout) shell out to `docker`, which would otherwise need the system `ssh` client — instead, `_cli.py:run_docker` detects `DOCKER_HOST=ssh://…` and routes the subprocess through a per-call local TCP proxy (`docker_mcp/tools/_ssh_proxy.py`) that opens its own paramiko connection and runs `docker system dial-stdio` over it, so the CLI authenticates the same way the docker-py-backed tools do, with no system `ssh` binary involved (except a `ProxyCommand` in `~/.ssh/config` for bastion/jump-host setups, which paramiko runs as an external command — commonly `ssh -W %h:%p ...` — for both tool families alike).
|
|
10
10
|
|
|
@@ -91,17 +91,17 @@ An additional distribution channel alongside the uvx-from-git install (unchanged
|
|
|
91
91
|
|
|
92
92
|
A third distribution channel for one-click install in Claude Desktop. Repo-root sources: `manifest.json` (MCPB manifest, `manifest_version` 0.4, `server.type: "uv"`), `mcpb_run.py` (bundle entry point — `from docker_mcp import main; main()`, at the root so `import docker_mcp` resolves under the host's managed `uv`), `.mcpbignore` (trims the packed bundle to source + `pyproject.toml` + `uv.lock` + `README.md`/`LICENSE` + manifest/entry-point + `assets/`), and `assets/icon.png` (512×512). It's a `uv`-type bundle: the host resolves deps from `pyproject.toml` at install time (no vendored venv). `manifest.json`'s `user_config` maps install-dialog fields to env — `DOCKER_MCP_SERVER_HOSTS` (the single host field; `DOCKER_HOST` is **not** exposed — the bare-value shorthand keeps the one-daemon case a one-liner) and the `DOCKER_MCP_SERVER_READONLY` / `_NO_DESTRUCTIVE` / `_DISABLE` switches (the container-only `_ALLOW_SELF_TERMINATE` is intentionally omitted — the bundle never runs containerized). The `mcpb` job in `.github/workflows/publish.yaml` rewrites the manifest `version` from the release tag, packs the `.mcpb` via `npx @anthropic-ai/mcpb`, and attaches it (plus a `.sha256` recording the bare filename, so `sha256sum -c` works on downloads) to each GitHub Release. `scripts/build-mcpb.sh` is the **developer-only** local equivalent of that pack step (version from `pyproject.toml`, auto-incrementing output in `dist/`, never mutates `manifest.json`) for smoke-testing in Claude Desktop — it is deliberately **not** used by CI. `PRIVACY.md` (no-telemetry statement) is referenced by the manifest's `privacy_policies`. Keep the manifest `version` aligned with `pyproject.toml`; update this section when the manifest's tool/env surface or the bundled file set changes.
|
|
93
93
|
|
|
94
|
-
### Homebrew tap (`
|
|
94
|
+
### Homebrew tap (`L337-org/homebrew-tap`) — PAUSED
|
|
95
95
|
|
|
96
|
-
The infrastructure exists (`scripts/docker-mcp-server.rb.tpl`, `.github/workflows/publish-homebrew.yaml`, `
|
|
96
|
+
The infrastructure exists (`scripts/docker-mcp-server.rb.tpl`, `.github/workflows/publish-homebrew.yaml`, `L337-org/homebrew-tap`) but the **release trigger is disabled** pending resolution of a Homebrew dylib linkage issue: pre-built PyPI wheels for `pydantic_core` lack `-headerpad_max_install_names`, so Homebrew's post-install relocation step fails to rewrite the `@rpath` ID (the binary's Mach-O header has no room for the longer absolute path). The workflow is `workflow_dispatch`-only until a fix is found. The channel is not advertised in the README. To re-enable: add `release: types: [published]` back to `publish-homebrew.yaml`'s `on:` block and re-add the Homebrew section to the README.
|
|
97
97
|
|
|
98
98
|
### MCP Registry (`server.json`)
|
|
99
99
|
|
|
100
|
-
A discovery listing in the official MCP Registry (`registry.modelcontextprotocol.io`), which stores only metadata pointing at the existing artifacts. `server.json` (repo root, name `io.github.
|
|
100
|
+
A discovery listing in the official MCP Registry (`registry.modelcontextprotocol.io`), which stores only metadata pointing at the existing artifacts. `server.json` (repo root, name `io.github.L337-org/docker-mcp-server`) declares three package types — `pypi`, `oci` (GHCR image), `mcpb` (release `.mcpb`). The `registry` job in `.github/workflows/publish.yaml` stamps the tag version + the published `.mcpb`'s `fileSha256` into `server.json`, authenticates via GitHub OIDC (`id-token: write`, no secret), and runs `mcp-publisher publish`. Ownership is verified per package against `server.json`'s `name`, so three markers must equal it: the `<!-- mcp-name: … -->` comment in `README.md` (PyPI), the `io.modelcontextprotocol.server.name` image label in the `images` job (OCI), and the `.mcpb` URL + hash (MCPB). Keep them in sync with `server.json`'s `name`.
|
|
101
101
|
|
|
102
102
|
### Release pipeline (`.github/workflows/publish.yaml`)
|
|
103
103
|
|
|
104
|
-
All publishing runs through one workflow on each **published GitHub Release**: `preflight` → `pypi` ∥ `images`(→`dockerhub-description`) ∥ `mcpb` → `registry` → `verify`, plus a `notify` job on failure. `preflight` resolves the tag once (every job checks out **the tag ref**) and fails fast if the tag disagrees with the committed `pyproject.toml` / `manifest.json` / `uv.lock` self-entry versions — preventing a split-brain release where PyPI ships the pyproject version and everything else ships the tag version. `verify` confirms all channels serve the version (PyPI JSON API, both GHCR tags, `.mcpb` asset + checksum, registry listing). `notify` files a deduplicated `ci-failure` + `wf:release` issue via `.github/actions/file-failure-issue` (one open issue per failure stream; a scheduled Claude responder routine polls `ci-failure` issues). Re-runs go through `workflow_dispatch` with the tag and are idempotent end-to-end (`skip-existing` on PyPI, re-pushed image tags, `--clobber` on the `.mcpb`, duplicate-tolerant registry publish). **PyPI Trusted Publishing pins the workflow *filename* (`publish.yaml`)** — when reviewing a PR that renames or splits this workflow, flag that the PyPI trusted publisher must be updated first. `.github/release.yml` (GitHub's release-*notes* config — a different file) groups generated notes; `publish-homebrew.yaml` (paused, dispatch-only) stays separate. When reviewing changes to the pipeline's job graph, ownership markers, or re-run semantics, check this section stays accurate.
|
|
104
|
+
All publishing runs through one workflow on each **published GitHub Release**: `preflight` → `pypi` ∥ `images`(→`dockerhub-description`) ∥ `mcpb` → `registry` → `verify`, plus a `notify` job on failure. `preflight` resolves the tag once (every job checks out **the tag ref**) and fails fast if the tag disagrees with the committed `pyproject.toml` / `manifest.json` / `uv.lock` self-entry versions — preventing a split-brain release where PyPI ships the pyproject version and everything else ships the tag version. It also refuses to run when `github.repository_owner` isn't `L337-org`, so a release published from a fork (or mis-timed around a repo transfer) fails before anything ships rather than half-publishing. `verify` confirms all channels serve the version (PyPI JSON API, both GHCR tags, `.mcpb` asset + checksum, registry listing). `notify` files a deduplicated `ci-failure` + `wf:release` issue via `.github/actions/file-failure-issue` (one open issue per failure stream; a scheduled Claude responder routine polls `ci-failure` issues). Re-runs go through `workflow_dispatch` with the tag and are idempotent end-to-end (`skip-existing` on PyPI, re-pushed image tags, `--clobber` on the `.mcpb`, duplicate-tolerant registry publish). **PyPI Trusted Publishing pins the workflow *filename* (`publish.yaml`)** — when reviewing a PR that renames or splits this workflow, flag that the PyPI trusted publisher must be updated first. `.github/release.yml` (GitHub's release-*notes* config — a different file) groups generated notes; `publish-homebrew.yaml` (paused, dispatch-only) stays separate. When reviewing changes to the pipeline's job graph, ownership markers, or re-run semantics, check this section stays accurate.
|
|
105
105
|
|
|
106
106
|
## Conventions
|
|
107
107
|
|
|
@@ -148,12 +148,18 @@ def mcp_example(name: str):
|
|
|
148
148
|
"""
|
|
149
149
|
Say hello to someone by name.
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
Use it for a single greeting; use `mcp_example_bulk` to greet many names in one call.
|
|
152
|
+
Read-only, no side effects.
|
|
153
|
+
|
|
154
|
+
args: name - The name to say hello to (any non-empty string)
|
|
152
155
|
returns: str - The greeting
|
|
153
156
|
"""
|
|
154
157
|
return f"Hello, {name}!"
|
|
155
158
|
```
|
|
156
159
|
|
|
160
|
+
(`mcp_example` and `mcp_example_bulk` are illustrative only and exist nowhere in the repo — in a
|
|
161
|
+
real docstring the discriminator must name an actually-registered sibling tool.)
|
|
162
|
+
|
|
157
163
|
- One-line summary sentence, then a blank line
|
|
158
164
|
- `args:` section lists each parameter as `name - description`. Do **not** repeat the parameter's
|
|
159
165
|
type — the type annotation already lands in the tool's `inputSchema`, which the client sees
|
|
@@ -163,19 +169,63 @@ def mcp_example(name: str):
|
|
|
163
169
|
- Keep descriptions terse: state every functional fact (defaults, accepted formats/values, return
|
|
164
170
|
keys, important caveats) but cut redundancy and verbose phrasing. The docstring is the entire
|
|
165
171
|
tool `description` the client pays tokens for on every session.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
|
|
173
|
+
#### Docstring quality standard (review checklist — push back on non-compliant docstrings)
|
|
174
|
+
|
|
175
|
+
Tool descriptions are scored externally on Glama's six-dimension Tool Definition Quality rubric
|
|
176
|
+
(<https://glama.ai/mcp/servers/L337-org/docker-mcp/score>): Purpose Clarity 25%, Usage Guidelines
|
|
177
|
+
20%, Behavioral Transparency 20%, Parameter Semantics 15%, Conciseness & Structure 10%, Contextual
|
|
178
|
+
Completeness 10%. Repeated cleanup PRs (#97, the 2.0 rename, #129, the 2026-07 bottom-20 pass) all
|
|
179
|
+
chased the same failure: docstrings that state *what* the tool does but never *when to use it over
|
|
180
|
+
its neighbors*, plus `args:`/`returns:` lines that merely restate the schema. **When a PR adds or
|
|
181
|
+
modifies an `@tool()` docstring, check it against every numbered item below and request changes
|
|
182
|
+
naming the specific item it fails.** The standard is a ratchet: it applies to docstrings the PR
|
|
183
|
+
touches — do not demand rewrites of untouched legacy docstrings.
|
|
184
|
+
|
|
185
|
+
1. **Summary = specific verb + resource**, with the distinguishing trait up front when a sibling
|
|
186
|
+
could be confused ("Send a signal to a running container (default SIGKILL — immediate, no
|
|
187
|
+
graceful shutdown)").
|
|
188
|
+
2. **A usage-guidance paragraph (1–5 sentences between the summary and `args:`) is required for
|
|
189
|
+
every tool, not just complex ones.** Flag a new/edited tool whose docstring jumps straight from
|
|
190
|
+
summary to `args:`. The paragraph must carry:
|
|
191
|
+
- at least one *discriminator* naming the sibling tool(s) an agent could reach for instead and
|
|
192
|
+
when to prefer which (`container_stop` vs `container_kill` vs `container_restart`;
|
|
193
|
+
`service_ps` vs `stack_ps` vs the `service-tasks://` resource);
|
|
194
|
+
- preconditions in prose (swarm manager only, plugin required, container must be
|
|
195
|
+
running/paused);
|
|
196
|
+
- side effects and destructive/irreversible behavior in prose — the scorer explicitly discounts
|
|
197
|
+
`readOnlyHint`/`destructiveHint` annotations as a substitute for description text;
|
|
198
|
+
- for CLI-backed tools, the error style ("does not raise on a non-zero CLI exit — inspect
|
|
199
|
+
`returncode`/`stderr`" vs "raises `RuntimeError` on CLI failure"). Flag an absolute "never
|
|
200
|
+
raises" claim — a missing binary/plugin or a subprocess timeout still raises even in action
|
|
201
|
+
tools.
|
|
202
|
+
Scale to the tool: a trivial read-only tool needs one discriminator sentence, not five.
|
|
203
|
+
3. **Every `args:` line adds semantics the schema cannot carry**: format, accepted values/ranges,
|
|
204
|
+
defaults, units, parameter interactions. Flag a line that merely echoes the parameter name
|
|
205
|
+
("name - The volume name") — it scores 2/5 on the rubric. Canonical shared-param prefixes in
|
|
206
|
+
`tests/test_naming.py` still apply — tool-specific detail is appended after the canonical
|
|
207
|
+
prefix, not reworded.
|
|
208
|
+
4. **`returns:` names the shape, not just the type** — for dict/list returns the load-bearing keys
|
|
209
|
+
must be listed (`{"Titles", "Processes"}`); flag a bare "dict - The X's attrs" on a
|
|
210
|
+
new/modified tool.
|
|
211
|
+
5. **Front-loaded and terse** — the description is paid for in every session's context.
|
|
212
|
+
6. **Every factual claim must be verifiable** against the docker-py docs / Engine API spec (Docker
|
|
213
|
+
SDK Policy below). Flag claims that contradict them — especially identifier semantics ("name or
|
|
214
|
+
id" for a resource actually addressed by name only), default values, and signal/timeout
|
|
215
|
+
behavior.
|
|
216
|
+
|
|
217
|
+
**Division of labor across the three discovery layers.** For a lazy-loading client (e.g. Claude
|
|
218
|
+
Code), tool schemas load on demand; always in context are only (1) the **tool names** and (2) the
|
|
219
|
+
**`instructions` router** (`_DOMAIN_BLURBS` / `build_instructions()` in `server.py`). Docstrings
|
|
220
|
+
are layer (3): a deferred tool cannot be invoked without fetching its definition, so it is read at
|
|
221
|
+
the moment of choice, side by side with sibling definitions — where the item-2 discriminators do
|
|
222
|
+
their work. When reviewing, apply the boundary: a *cross-domain* selection caveat that must be
|
|
223
|
+
visible before any schema is fetched (e.g. "prefer `dest_path` for large output") belongs in the
|
|
224
|
+
router's caveat list, not buried in one docstring; *sibling-level* discriminators belong in
|
|
225
|
+
docstrings and should not be duplicated into the router; flag docstrings padded with search
|
|
226
|
+
keywords (discoverability is the naming convention's and the router's job); and flag sibling
|
|
227
|
+
references that don't use the exact tool name (`container_kill`, never "the kill tool") — lazy
|
|
228
|
+
clients keyword-search descriptions, so exact names double as retrieval anchors.
|
|
179
229
|
|
|
180
230
|
### Bounding rules
|
|
181
231
|
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
42
42
|
|
|
43
43
|
- name: Set up uv
|
|
44
|
-
uses: astral-sh/setup-uv@
|
|
44
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
45
45
|
with:
|
|
46
46
|
# Pinned (matching the Dockerfile's uv image tag) so this step never needs the
|
|
47
47
|
# live "resolve latest" fetch to raw.githubusercontent.com — that network call
|
|
@@ -102,7 +102,7 @@ jobs:
|
|
|
102
102
|
os: [macos-latest, macos-15-intel, windows-latest]
|
|
103
103
|
steps:
|
|
104
104
|
- name: Set up uv
|
|
105
|
-
uses: astral-sh/setup-uv@
|
|
105
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
106
106
|
with:
|
|
107
107
|
# This job deliberately has no checkout — it tests the *published* package, not the
|
|
108
108
|
# repo — so disable the repo-keyed cache and the empty-workdir warning it triggers.
|
|
@@ -31,12 +31,12 @@ jobs:
|
|
|
31
31
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
32
32
|
|
|
33
33
|
- name: Initialize CodeQL
|
|
34
|
-
uses: github/codeql-action/init@
|
|
34
|
+
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
|
35
35
|
with:
|
|
36
36
|
languages: ${{ matrix.language }}
|
|
37
37
|
|
|
38
38
|
- name: Autobuild
|
|
39
|
-
uses: github/codeql-action/autobuild@
|
|
39
|
+
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
|
40
40
|
|
|
41
41
|
- name: Perform CodeQL Analysis
|
|
42
|
-
uses: github/codeql-action/analyze@
|
|
42
|
+
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
python-version: "3.14"
|
|
34
34
|
|
|
35
35
|
- name: Set up uv
|
|
36
|
-
uses: astral-sh/setup-uv@
|
|
36
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
37
37
|
|
|
38
38
|
- name: Install dependencies
|
|
39
39
|
# --locked fails if uv.lock disagrees with pyproject.toml instead of silently
|
|
@@ -62,7 +62,7 @@ jobs:
|
|
|
62
62
|
python-version: "3.14"
|
|
63
63
|
|
|
64
64
|
- name: Set up uv
|
|
65
|
-
uses: astral-sh/setup-uv@
|
|
65
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
66
66
|
|
|
67
67
|
- name: Install dependencies
|
|
68
68
|
# --locked fails if uv.lock disagrees with pyproject.toml instead of silently
|
|
@@ -91,7 +91,7 @@ jobs:
|
|
|
91
91
|
python-version: "3.14"
|
|
92
92
|
|
|
93
93
|
- name: Set up uv
|
|
94
|
-
uses: astral-sh/setup-uv@
|
|
94
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
95
95
|
|
|
96
96
|
- name: Install dependencies
|
|
97
97
|
# --locked fails if uv.lock disagrees with pyproject.toml instead of silently
|
|
@@ -158,7 +158,7 @@ jobs:
|
|
|
158
158
|
python-version: "3.14"
|
|
159
159
|
|
|
160
160
|
- name: Set up uv
|
|
161
|
-
uses: astral-sh/setup-uv@
|
|
161
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
162
162
|
|
|
163
163
|
- name: Install dependencies
|
|
164
164
|
# --locked fails if uv.lock disagrees with pyproject.toml instead of silently
|
|
@@ -2,7 +2,7 @@ name: 'Publish Homebrew formula'
|
|
|
2
2
|
|
|
3
3
|
# On each GitHub Release: downloads pre-built macOS wheels for arm64 and x86_64 from PyPI (using
|
|
4
4
|
# the frozen lockfile so versions are pinned), packages each set into a wheelhouse tarball, attaches
|
|
5
|
-
# both to the release, then pushes an updated Formula/docker-mcp-server.rb to
|
|
5
|
+
# both to the release, then pushes an updated Formula/docker-mcp-server.rb to L337-org/homebrew-tap.
|
|
6
6
|
#
|
|
7
7
|
# Runs on ubuntu-latest. Both wheelhouses are built via cross-platform pip downloads:
|
|
8
8
|
# arm64: --platform macosx_<floor>_arm64 + --platform macosx_<floor>_universal2
|
|
@@ -23,8 +23,11 @@ name: 'Publish Homebrew formula'
|
|
|
23
23
|
# guardrail; do not remove it.
|
|
24
24
|
#
|
|
25
25
|
# Requires the repository secret TAP_GITHUB_TOKEN: a fine-grained PAT with Contents read+write on
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# L337-org/homebrew-tap, issued with **L337-org as the resource owner** (a fine-grained PAT scoped
|
|
27
|
+
# to a personal account cannot access org repos, and org settings must allow fine-grained PAT
|
|
28
|
+
# access). Create it at github.com/settings/tokens and add it as a secret on this repo under
|
|
29
|
+
# Settings → Secrets → Actions. The pre-transfer token (resource owner GavinLucas) is invalid for
|
|
30
|
+
# the moved tap — re-issue before re-enabling this channel.
|
|
28
31
|
#
|
|
29
32
|
# workflow_dispatch lets you backfill a release that predates this workflow (supply the tag, e.g.
|
|
30
33
|
# v1.9.2). The release must already exist and docker-mcp-server must be live on PyPI for that version.
|
|
@@ -64,7 +67,7 @@ jobs:
|
|
|
64
67
|
run: git fetch --depth=1 origin main && git checkout origin/main -- scripts/docker-mcp-server.rb.tpl
|
|
65
68
|
|
|
66
69
|
- name: Install uv
|
|
67
|
-
uses: astral-sh/setup-uv@
|
|
70
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
68
71
|
|
|
69
72
|
- name: Resolve version, Python target, and macOS floor
|
|
70
73
|
run: |
|
|
@@ -206,7 +209,7 @@ jobs:
|
|
|
206
209
|
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
|
207
210
|
run: |
|
|
208
211
|
set -euo pipefail
|
|
209
|
-
git clone "https://x-access-token:${GH_TOKEN}@github.com/
|
|
212
|
+
git clone "https://x-access-token:${GH_TOKEN}@github.com/L337-org/homebrew-tap.git" tap
|
|
210
213
|
cd tap
|
|
211
214
|
git config user.name "github-actions[bot]"
|
|
212
215
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
@@ -15,7 +15,7 @@ name: Release
|
|
|
15
15
|
# channel actually converged so a partial release is loud instead of silent.
|
|
16
16
|
#
|
|
17
17
|
# PyPI Trusted Publishing pins the WORKFLOW FILENAME: the trusted publisher on PyPI must list
|
|
18
|
-
# `publish.yaml` (owner
|
|
18
|
+
# `publish.yaml` (owner L337-org, repo docker-mcp). If the pypi job fails with an OIDC
|
|
19
19
|
# `invalid-publisher` error, that registration is missing — add it on PyPI, then re-run.
|
|
20
20
|
#
|
|
21
21
|
# Partial-failure semantics: no rollback is attempted (PyPI files are immutable anyway). The
|
|
@@ -55,6 +55,16 @@ jobs:
|
|
|
55
55
|
tag: ${{ steps.resolve.outputs.tag }}
|
|
56
56
|
version: ${{ steps.resolve.outputs.version }}
|
|
57
57
|
steps:
|
|
58
|
+
- name: Assert canonical repository owner
|
|
59
|
+
# The GHCR path, MCP Registry name, and PyPI trusted publisher are all pinned to the
|
|
60
|
+
# L337-org identity. Publishing from anywhere else (a fork, or the window between the
|
|
61
|
+
# identity sweep landing and the repo transfer) would half-publish — PyPI first, then
|
|
62
|
+
# fail on GHCR/registry. Fail fast before anything ships; every other job needs preflight.
|
|
63
|
+
if: github.repository_owner != 'L337-org'
|
|
64
|
+
run: |
|
|
65
|
+
echo "::error::This pipeline publishes the L337-org identity but is running in ${GITHUB_REPOSITORY}. Refusing to publish."
|
|
66
|
+
exit 1
|
|
67
|
+
|
|
58
68
|
- name: Resolve tag and version
|
|
59
69
|
id: resolve
|
|
60
70
|
env:
|
|
@@ -142,7 +152,7 @@ jobs:
|
|
|
142
152
|
python-version: "3.14"
|
|
143
153
|
|
|
144
154
|
- name: Install uv
|
|
145
|
-
uses: astral-sh/setup-uv@
|
|
155
|
+
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
146
156
|
|
|
147
157
|
- name: Build sdist and wheel
|
|
148
158
|
run: uv build
|
|
@@ -160,7 +170,7 @@ jobs:
|
|
|
160
170
|
# Publishes the container images:
|
|
161
171
|
# full -> docker-mcp-server:latest + :<version>
|
|
162
172
|
# no-scout -> docker-mcp-server:no-scout + :<version>-no-scout
|
|
163
|
-
# Images go to GHCR (ghcr.io/
|
|
173
|
+
# Images go to GHCR (ghcr.io/l337-org/docker-mcp-server) always, and are mirrored to Docker
|
|
164
174
|
# Hub (docker.io/gavinlucas/docker-mcp-server) when the DOCKERHUB_USER / DOCKERHUB_TOKEN repo
|
|
165
175
|
# secrets are set. The Hub mirror is opt-in: with the secrets unset the Hub login/tags steps
|
|
166
176
|
# are skipped and only GHCR is pushed, so a release never fails for lack of Hub credentials.
|
|
@@ -202,7 +212,7 @@ jobs:
|
|
|
202
212
|
VERSION: ${{ needs.preflight.outputs.version }}
|
|
203
213
|
run: |
|
|
204
214
|
# GHCR always; mirror to Docker Hub only when the credentials are present (see header).
|
|
205
|
-
registries="ghcr.io/
|
|
215
|
+
registries="ghcr.io/l337-org/docker-mcp-server"
|
|
206
216
|
if [ -n "$DOCKERHUB_USER" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
|
|
207
217
|
registries="$registries docker.io/gavinlucas/docker-mcp-server"
|
|
208
218
|
fi
|
|
@@ -254,9 +264,9 @@ jobs:
|
|
|
254
264
|
# match `name` in server.json so the registry can verify we own this image (see the
|
|
255
265
|
# registry job below). image.version is the v-stripped version, matching the image tags.
|
|
256
266
|
labels: |
|
|
257
|
-
org.opencontainers.image.source=https://github.com/
|
|
267
|
+
org.opencontainers.image.source=https://github.com/L337-org/docker-mcp
|
|
258
268
|
org.opencontainers.image.version=${{ needs.preflight.outputs.version }}
|
|
259
|
-
io.modelcontextprotocol.server.name=io.github.
|
|
269
|
+
io.modelcontextprotocol.server.name=io.github.L337-org/docker-mcp-server
|
|
260
270
|
build-args: |
|
|
261
271
|
INSTALL_CLI=${{ matrix.install_cli }}
|
|
262
272
|
INSTALL_SCOUT=${{ matrix.install_scout }}
|
|
@@ -341,7 +351,7 @@ jobs:
|
|
|
341
351
|
# Publishes server.json to the official MCP Registry (registry.modelcontextprotocol.io). The
|
|
342
352
|
# registry stores only metadata — it points at the artifacts the sibling jobs above publish —
|
|
343
353
|
# and verifies we own each listed package via a marker that must equal server.json's `name`
|
|
344
|
-
# (io.github.
|
|
354
|
+
# (io.github.L337-org/docker-mcp-server):
|
|
345
355
|
# - pypi -> the `<!-- mcp-name: ... -->` comment in README.md (PyPI long-description)
|
|
346
356
|
# - oci -> the io.modelcontextprotocol.server.name image label (set in the images job)
|
|
347
357
|
# - mcpb -> the .mcpb release-asset URL (must contain "mcp") + a matching fileSha256
|
|
@@ -393,9 +403,9 @@ jobs:
|
|
|
393
403
|
.version = $v
|
|
394
404
|
| (.packages[] | select(.registryType == "pypi") | .version) = $v
|
|
395
405
|
| (.packages[] | select(.registryType == "oci") | .identifier)
|
|
396
|
-
= "ghcr.io/
|
|
406
|
+
= "ghcr.io/l337-org/docker-mcp-server:\($v)"
|
|
397
407
|
| (.packages[] | select(.registryType == "mcpb") | .identifier)
|
|
398
|
-
= "https://github.com/
|
|
408
|
+
= "https://github.com/L337-org/docker-mcp/releases/download/\($tag)/docker-mcp-server-\($v).mcpb"
|
|
399
409
|
| (.packages[] | select(.registryType == "mcpb") | .fileSha256) = $sha
|
|
400
410
|
' server.json > server.tmp && mv server.tmp server.json
|
|
401
411
|
echo "--- server.json ---"; cat server.json
|
|
@@ -481,7 +491,7 @@ jobs:
|
|
|
481
491
|
for tag in "${VERSION}" "${VERSION}-no-scout"; do
|
|
482
492
|
ok=""
|
|
483
493
|
for i in $(seq 1 5); do
|
|
484
|
-
if docker buildx imagetools inspect "ghcr.io/
|
|
494
|
+
if docker buildx imagetools inspect "ghcr.io/l337-org/docker-mcp-server:${tag}" >/dev/null 2>&1; then
|
|
485
495
|
echo "GHCR OK: :${tag}"; ok=1; break
|
|
486
496
|
fi
|
|
487
497
|
echo "::notice::GHCR tag :${tag} not readable yet (attempt $i/5); waiting 15s"
|
|
@@ -512,7 +522,7 @@ jobs:
|
|
|
512
522
|
- name: Verify the MCP Registry lists the version
|
|
513
523
|
run: |
|
|
514
524
|
set -euo pipefail
|
|
515
|
-
url="https://registry.modelcontextprotocol.io/v0/servers?search=io.github.
|
|
525
|
+
url="https://registry.modelcontextprotocol.io/v0/servers?search=io.github.L337-org/docker-mcp-server"
|
|
516
526
|
for i in $(seq 1 5); do
|
|
517
527
|
if curl -fsS "$url" | jq -e --arg v "$VERSION" \
|
|
518
528
|
'.servers[] | select(.server.version == $v)' >/dev/null; then
|
|
@@ -11,7 +11,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
11
11
|
|
|
12
12
|
## Project
|
|
13
13
|
|
|
14
|
-
`docker-mcp` is a Python MCP server (requires Python >=3.14) managed with `uv` that exposes the Docker SDK for Python as MCP tools. The entry point is the `docker_mcp` package, run with `python -m docker_mcp` or via the installed console script. It is **published to PyPI as `docker-mcp-server`** (the `docker-mcp` name was already taken) and as a container image to GHCR (`ghcr.io/
|
|
14
|
+
`docker-mcp` is a Python MCP server (requires Python >=3.14) managed with `uv` that exposes the Docker SDK for Python as MCP tools. The entry point is the `docker_mcp` package, run with `python -m docker_mcp` or via the installed console script. It is **published to PyPI as `docker-mcp-server`** (the `docker-mcp` name was already taken) and as a container image to GHCR (`ghcr.io/l337-org/docker-mcp-server`), mirrored to Docker Hub (`gavinlucas/docker-mcp-server`) when the opt-in `DOCKERHUB_*` release secrets are configured; the import package stays `docker_mcp` and the repo stays `…/docker-mcp`. Two console scripts are installed — `docker-mcp` and `docker-mcp-server` — both targeting `docker_mcp:main`. A third channel packages the server as a **Claude Desktop Extension (`.mcpb`)** attached to each GitHub Release — see "Desktop Extension (MCPB bundle)" below. A fourth channel (**Homebrew tap**) exists in `L337-org/homebrew-tap` but is currently **paused** — see "Homebrew tap" below.
|
|
15
15
|
|
|
16
16
|
The `docker` dependency is pulled with its `[ssh]` extra (paramiko), so `DOCKER_HOST=ssh://…` works through a pure-Python transport — no system `ssh` binary, identical on the host and in the container images. docker-py auto-selects paramiko for `ssh://` when present, so there is no transport code to maintain (just the `ssh://` branch in `system._connection_help`). CLI-backed tools (Compose, Buildx, Context, Scout) shell out to `docker`, which would otherwise use the *system* `ssh` — instead, `_cli.py:run_docker` detects `DOCKER_HOST=ssh://…` and routes the subprocess through a per-call local TCP proxy (`docker_mcp/tools/_ssh_proxy.py`) that opens its own paramiko connection (mirroring docker-py's `SSHHTTPAdapter` defaults) and runs `docker system dial-stdio` over it, so the CLI authenticates identically to the docker-py-backed tools with no system `ssh` binary involved (the one exception being a `ProxyCommand` in `~/.ssh/config` for bastion/jump-host setups, which paramiko runs as an external command — commonly `ssh -W %h:%p ...` — same as it would for the docker-py-backed tools).
|
|
17
17
|
|
|
@@ -157,17 +157,17 @@ An additional distribution channel alongside uvx-from-git (which is unchanged).
|
|
|
157
157
|
|
|
158
158
|
A third distribution channel (alongside uvx/PyPI and the container images) for one-click install in Claude Desktop. The repo root carries the bundle sources: `manifest.json` (the MCPB manifest, `manifest_version` 0.4, `server.type: "uv"`), `mcpb_run.py` (the bundle entry point — `from docker_mcp import main; main()`, kept at the root so `import docker_mcp` resolves however the host's managed `uv` lays out `sys.path`), `.mcpbignore` (trims the packed bundle to source + `pyproject.toml` + `uv.lock` + `README.md`/`LICENSE` + `manifest.json`/`mcpb_run.py` + `assets/`), and `assets/icon.png` (512×512). Because it's a `uv`-type bundle, the host resolves dependencies from `pyproject.toml` at install time — there's no vendored venv, so the bundle stays ~250 KB and cross-platform. `manifest.json`'s `user_config` block renders the install-dialog fields and maps them to env: `DOCKER_MCP_SERVER_HOSTS` (the single host field — `DOCKER_HOST` is deliberately **not** exposed; the bare-value shorthand keeps the simple one-daemon case a one-liner) plus the `DOCKER_MCP_SERVER_READONLY` / `_NO_DESTRUCTIVE` / `_DISABLE` switches (the container-only `_ALLOW_SELF_TERMINATE` is deliberately **not** exposed — the bundle never runs containerized). The `manifest.json` `version` is kept in step with `pyproject.toml` (`tests/test_pyproject_pins.py` asserts it), but the `mcpb` job in `.github/workflows/publish.yaml` rewrites it from the release tag at pack time so it can't drift; that job packs the `.mcpb` with `npx @anthropic-ai/mcpb` and attaches it (plus a `.sha256` recording the bare filename, so `sha256sum -c` works on downloads) to each GitHub Release. `scripts/build-mcpb.sh` is the **developer-only** local equivalent of that pack step (reads the version from `pyproject.toml`, packs to `dist/` with an auto-incrementing name, never mutates `manifest.json`) — for smoke-testing a bundle in Claude Desktop; it is deliberately **not** wired into CI, which uses the workflow above. `PRIVACY.md` (a no-telemetry/no-backend statement) is referenced by the manifest's `privacy_policies` and summarized in the README. **When the manifest's tool/env surface or the bundled file set changes, update this section.**
|
|
159
159
|
|
|
160
|
-
### Homebrew tap (`
|
|
160
|
+
### Homebrew tap (`L337-org/homebrew-tap`) — PAUSED
|
|
161
161
|
|
|
162
|
-
The infrastructure exists (`scripts/docker-mcp-server.rb.tpl`, `.github/workflows/publish-homebrew.yaml`, `
|
|
162
|
+
The infrastructure exists (`scripts/docker-mcp-server.rb.tpl`, `.github/workflows/publish-homebrew.yaml`, `L337-org/homebrew-tap`) but the **release trigger is disabled** pending resolution of a Homebrew dylib linkage issue: pre-built PyPI wheels for `pydantic_core` lack `-headerpad_max_install_names`, so Homebrew's post-install relocation step fails to rewrite the `@rpath` ID (the binary's Mach-O header has no room for the longer absolute path). The workflow is `workflow_dispatch`-only until a fix is found. The channel is not advertised in the README. To re-enable: add `release: types: [published]` back to `publish-homebrew.yaml`'s `on:` block and re-add the Homebrew section to the README.
|
|
163
163
|
|
|
164
164
|
### MCP Registry (`server.json`)
|
|
165
165
|
|
|
166
|
-
A discovery listing in the official MCP Registry (`registry.modelcontextprotocol.io`), which stores only **metadata** pointing at the artifacts the other channels already publish — so it's not a fourth artifact, just an index entry covering all of them. `server.json` (repo root, server name `io.github.
|
|
166
|
+
A discovery listing in the official MCP Registry (`registry.modelcontextprotocol.io`), which stores only **metadata** pointing at the artifacts the other channels already publish — so it's not a fourth artifact, just an index entry covering all of them. `server.json` (repo root, server name `io.github.L337-org/docker-mcp-server`) declares three package types in one entry: `pypi` (`docker-mcp-server`), `oci` (the GHCR image), and `mcpb` (the release `.mcpb`). The `registry` job in `.github/workflows/publish.yaml` stamps the tag version and the published `.mcpb`'s `fileSha256` into `server.json`, authenticates via **GitHub OIDC** (`id-token: write`, no stored secret), and runs `mcp-publisher publish`. The registry verifies we own each listed package by matching a marker against `server.json`'s `name`, so three markers must stay equal to it: the `<!-- mcp-name: … -->` comment in `README.md` (PyPI long-description), the `io.modelcontextprotocol.server.name` image label in the `images` job (OCI), and the `.mcpb` URL (must contain "mcp") + its hash (MCPB). The job runs `needs: [pypi, images, mcpb]`, so all three markers are live before it starts — its short retries only absorb registry-side read lag. **If `server.json`'s name/markers or the package set change, update this section.**
|
|
167
167
|
|
|
168
168
|
### Release pipeline (`.github/workflows/publish.yaml`)
|
|
169
169
|
|
|
170
|
-
All publishing runs through one workflow on each **published GitHub Release** (nothing publishes without that human step): `preflight` → `pypi` ∥ `images`(→`dockerhub-description`) ∥ `mcpb` → `registry` → `verify`, with a `notify` job on failure. `preflight` resolves the tag once for every job (each checks out **the tag ref**, not the event SHA) and fails fast if the tag disagrees with the committed `pyproject.toml` / `manifest.json` / `uv.lock` self-entry versions — preventing a split-brain release (PyPI ships the pyproject version; every other channel ships the tag version). `verify` confirms all four channels actually serve the version (PyPI JSON API, both GHCR tags, the `.mcpb` asset + checksum, the registry listing) so a partial release is loud. `notify` files (or comments on) a deduplicated issue labeled `ci-failure` + `wf:release` via the composite action `.github/actions/file-failure-issue` — one open issue per failure stream; a scheduled Claude responder routine polls `ci-failure` issues. Re-run a failed release via `workflow_dispatch` with the tag: every job is idempotent (`skip-existing` on PyPI, re-pushed image tags, `--clobber` on the `.mcpb`, duplicate-tolerant registry publish). **PyPI Trusted Publishing pins the workflow *filename*** — the trusted publisher on PyPI must name `publish.yaml`; if the `pypi` job fails OIDC, that registration is missing. Version-bump checklist for a release: edit `pyproject.toml` + `manifest.json`, run `uv lock`, merge, then publish a GitHub Release whose tag matches (`.github/release.yml` — GitHub's release-*notes* config, a different file — groups the generated notes, Dependabot PRs under "Dependencies"). `publish-homebrew.yaml` (paused, dispatch-only) stays separate. **When the pipeline's job graph, markers, or re-run semantics change, update this section.**
|
|
170
|
+
All publishing runs through one workflow on each **published GitHub Release** (nothing publishes without that human step): `preflight` → `pypi` ∥ `images`(→`dockerhub-description`) ∥ `mcpb` → `registry` → `verify`, with a `notify` job on failure. `preflight` resolves the tag once for every job (each checks out **the tag ref**, not the event SHA) and fails fast if the tag disagrees with the committed `pyproject.toml` / `manifest.json` / `uv.lock` self-entry versions — preventing a split-brain release (PyPI ships the pyproject version; every other channel ships the tag version). It also refuses to run when `github.repository_owner` isn't `L337-org`, so a release published from a fork (or mis-timed around a repo transfer) fails before anything ships rather than half-publishing. `verify` confirms all four channels actually serve the version (PyPI JSON API, both GHCR tags, the `.mcpb` asset + checksum, the registry listing) so a partial release is loud. `notify` files (or comments on) a deduplicated issue labeled `ci-failure` + `wf:release` via the composite action `.github/actions/file-failure-issue` — one open issue per failure stream; a scheduled Claude responder routine polls `ci-failure` issues. Re-run a failed release via `workflow_dispatch` with the tag: every job is idempotent (`skip-existing` on PyPI, re-pushed image tags, `--clobber` on the `.mcpb`, duplicate-tolerant registry publish). **PyPI Trusted Publishing pins the workflow *filename*** — the trusted publisher on PyPI must name `publish.yaml`; if the `pypi` job fails OIDC, that registration is missing. Version-bump checklist for a release: edit `pyproject.toml` + `manifest.json`, run `uv lock`, merge, then publish a GitHub Release whose tag matches (`.github/release.yml` — GitHub's release-*notes* config, a different file — groups the generated notes, Dependabot PRs under "Dependencies"). `publish-homebrew.yaml` (paused, dispatch-only) stays separate. **When the pipeline's job graph, markers, or re-run semantics change, update this section.**
|
|
171
171
|
|
|
172
172
|
## Conventions
|
|
173
173
|
|
|
@@ -247,12 +247,18 @@ def mcp_example(name: str):
|
|
|
247
247
|
"""
|
|
248
248
|
Say hello to someone by name.
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
Use it for a single greeting; use `mcp_example_bulk` to greet many names in one call.
|
|
251
|
+
Read-only, no side effects.
|
|
252
|
+
|
|
253
|
+
args: name - The name to say hello to (any non-empty string)
|
|
251
254
|
returns: str - The greeting
|
|
252
255
|
"""
|
|
253
256
|
return f"Hello, {name}!"
|
|
254
257
|
```
|
|
255
258
|
|
|
259
|
+
(`mcp_example` and `mcp_example_bulk` are illustrative only and exist nowhere in the repo — in a
|
|
260
|
+
real docstring the discriminator must name an actually-registered sibling tool.)
|
|
261
|
+
|
|
256
262
|
- One-line summary sentence, then a blank line
|
|
257
263
|
- `args:` section lists each parameter as `name - description`. Do **not** repeat the parameter's
|
|
258
264
|
type — the type annotation already lands in the tool's `inputSchema`, which the client sees
|
|
@@ -262,19 +268,70 @@ def mcp_example(name: str):
|
|
|
262
268
|
- Keep descriptions terse: state every functional fact (defaults, accepted formats/values, return
|
|
263
269
|
keys, important caveats) but cut redundancy and verbose phrasing. The docstring is the entire
|
|
264
270
|
tool `description` the client pays tokens for on every session.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
|
|
272
|
+
#### Docstring quality standard
|
|
273
|
+
|
|
274
|
+
Tool descriptions are scored externally on Glama's six-dimension Tool Definition Quality rubric
|
|
275
|
+
(<https://glama.ai/mcp/servers/L337-org/docker-mcp/score>): Purpose Clarity 25%, Usage Guidelines
|
|
276
|
+
20%, Behavioral Transparency 20%, Parameter Semantics 15%, Conciseness & Structure 10%, Contextual
|
|
277
|
+
Completeness 10%. Four rounds of cleanup (#97, the 2.0 rename, #129, and the 2026-07 bottom-20
|
|
278
|
+
pass — see [[project_glama_docstring_quality]] in memory) all chased the same failure: docstrings
|
|
279
|
+
that state *what* the tool does but never *when to use it over its neighbors*, plus `args:` /
|
|
280
|
+
`returns:` lines that merely restate the schema. The standard below exists to prevent a fifth
|
|
281
|
+
round. It applies to **every `@tool()` docstring added or modified in a PR** (a ratchet — untouched
|
|
282
|
+
legacy docstrings are cleaned opportunistically, not churned):
|
|
283
|
+
|
|
284
|
+
1. **Summary = specific verb + resource**, with the distinguishing trait up front when a sibling
|
|
285
|
+
could be confused ("Send a signal to a running container (default SIGKILL — immediate, no
|
|
286
|
+
graceful shutdown)").
|
|
287
|
+
2. **A usage-guidance paragraph (1–5 sentences between the summary and `args:`) is required for
|
|
288
|
+
every tool, not just complex ones.** Every tool in a 150+-tool server has neighbors. It must
|
|
289
|
+
carry:
|
|
290
|
+
- at least one *discriminator* naming the sibling tool(s) an agent could reach for instead and
|
|
291
|
+
when to prefer which (`container_stop` vs `container_kill` vs `container_restart`;
|
|
292
|
+
`service_ps` vs `stack_ps` vs the `service-tasks://` resource);
|
|
293
|
+
- preconditions in prose (swarm manager only, plugin required, container must be
|
|
294
|
+
running/paused);
|
|
295
|
+
- side effects and destructive/irreversible behavior in prose — the scorer explicitly discounts
|
|
296
|
+
`readOnlyHint`/`destructiveHint` annotations as a substitute for description text;
|
|
297
|
+
- for CLI-backed tools, the error style ("does not raise on a non-zero CLI exit — inspect
|
|
298
|
+
`returncode`/`stderr`" vs "raises `RuntimeError` on CLI failure"). Don't overpromise "never
|
|
299
|
+
raises" — a missing binary/plugin or a subprocess timeout still raises even in action tools.
|
|
300
|
+
Scale it to the tool: a trivial read-only tool needs one discriminator sentence, not five.
|
|
301
|
+
3. **Every `args:` line adds semantics the schema cannot carry**: format, accepted values/ranges,
|
|
302
|
+
defaults, units, and interactions with other parameters. A line that echoes the parameter name
|
|
303
|
+
("name - The volume name") scores 2/5 on the rubric — say what makes a value valid or how it
|
|
304
|
+
behaves ("name - The volume name (volumes have no separate id)"). Canonical shared-param
|
|
305
|
+
prefixes in `tests/test_naming.py` still apply — append tool-specific detail after the
|
|
306
|
+
canonical prefix rather than rewording it.
|
|
307
|
+
4. **`returns:` names the shape, not just the type.** There is no output schema, so this line is
|
|
308
|
+
all an agent gets: for dict/list returns name the load-bearing keys (`{"Titles", "Processes"}`;
|
|
309
|
+
`{"LayersSize", "Images", "Containers", "Volumes", "BuildCache"}`), never just "the attrs".
|
|
310
|
+
5. **Front-load and stay terse** — the description is paid for in every session's context; every
|
|
311
|
+
sentence must earn its place.
|
|
312
|
+
6. **Verify every factual claim** against the live docker-py docs / Engine API spec per the Docker
|
|
313
|
+
SDK Policy below — an unverified claim about identifier semantics (e.g. "name or id" for a
|
|
314
|
+
resource actually addressed by name only) is exactly the kind of thing PR review catches late.
|
|
315
|
+
|
|
316
|
+
**Division of labor across the three discovery layers.** For a lazy-loading client (e.g. Claude
|
|
317
|
+
Code), tool schemas load on demand; what is always in context is only (1) the **tool names** and
|
|
318
|
+
(2) the **`instructions` router**. Docstrings are layer (3): a deferred tool cannot be invoked
|
|
319
|
+
without fetching its definition, so the docstring is guaranteed to be read at the moment of
|
|
320
|
+
choice — typically side by side with the sibling definitions the same search returned, which is
|
|
321
|
+
where the item-2 discriminators do their work. Consequences: pre-fetch discoverability belongs to
|
|
322
|
+
the naming convention and the router, not the docstring (don't pad docstrings with search
|
|
323
|
+
keywords); a *cross-domain* selection caveat that must be visible before any schema is fetched
|
|
324
|
+
(e.g. "prefer `dest_path` for large output") goes in the router's caveat list (`_DOMAIN_BLURBS` /
|
|
325
|
+
`build_instructions()` — see "Server singleton" above), while *sibling-level* discriminators stay
|
|
326
|
+
in docstrings and are never duplicated into the router; and sibling references must use the exact
|
|
327
|
+
tool name (`container_kill`, never "the kill tool") — lazy clients keyword-search descriptions, so
|
|
328
|
+
exact names double as retrieval anchors that surface the right alternative even when the agent
|
|
329
|
+
searched for the wrong one.
|
|
330
|
+
|
|
331
|
+
Self-check before opening the PR: read the docstring as an agent holding 150+ tool names and
|
|
332
|
+
nothing else — could you pick this tool over its neighbors and call it correctly on the first try?
|
|
333
|
+
**Write it this way the first time a tool is added or its behavior changes** — don't wait for a
|
|
334
|
+
future Glama pass to catch it.
|
|
278
335
|
|
|
279
336
|
### MCP resources
|
|
280
337
|
|
|
@@ -185,4 +185,4 @@ It reads the version from `pyproject.toml`, creates `dist/` if needed, writes a
|
|
|
185
185
|
|
|
186
186
|
## Reporting issues
|
|
187
187
|
|
|
188
|
-
Bug reports and feature requests have templates that you can choose when you [`create an issue`](https://github.com/
|
|
188
|
+
Bug reports and feature requests have templates that you can choose when you [`create an issue`](https://github.com/L337-org/docker-mcp/issues/new/choose). Please select the correct issue type and follow the template.
|
|
@@ -4,11 +4,11 @@ More than just a fully featured [MCP](https://modelcontextprotocol.io) server th
|
|
|
4
4
|
|
|
5
5
|
It can manage multiple Docker daemons, e.g. both your local dev environment and also a remote production environment over TCP, TLS or SSH in a single session. It can also be configured to mark some daemons as read-only, so that you can monitor them without the risk of making accidental changes. It also exposes things like logs and stats as resources so that you can easily monitor and triage your environments with a few prompts.
|
|
6
6
|
|
|
7
|
-
The server runs entirely on your machine and sends no telemetry. You are entirely in control — see the [Privacy Policy](https://github.com/
|
|
7
|
+
The server runs entirely on your machine and sends no telemetry. You are entirely in control — see the [Privacy Policy](https://github.com/L337-org/docker-mcp#privacy-policy).
|
|
8
8
|
|
|
9
|
-
This image is the container distribution of the project. **Full documentation, configuration, and source are on GitHub: <https://github.com/
|
|
9
|
+
This image is the container distribution of the project. **Full documentation, configuration, and source are on GitHub: <https://github.com/L337-org/docker-mcp>.**
|
|
10
10
|
|
|
11
|
-
> The same images are published to GHCR as [`ghcr.io/
|
|
11
|
+
> The same images are published to GHCR as [`ghcr.io/l337-org/docker-mcp-server`](https://github.com/L337-org/docker-mcp/pkgs/container/docker-mcp-server) — identical content, so use whichever registry you prefer.
|
|
12
12
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
@@ -72,14 +72,14 @@ Set these in the client's `env` block. Three switches restrict which tools are r
|
|
|
72
72
|
|
|
73
73
|
`DOCKER_HOST` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` retarget the daemon (e.g. `-e DOCKER_HOST=tcp://remote-host:2375`).
|
|
74
74
|
|
|
75
|
-
See the [full configuration reference](https://github.com/
|
|
75
|
+
See the [full configuration reference](https://github.com/L337-org/docker-mcp#configuration) for the complete list and examples.
|
|
76
76
|
|
|
77
77
|
## Security
|
|
78
78
|
|
|
79
|
-
Connecting this server to an AI agent grants it the same access as a local Docker CLI session against the configured daemon — the daemon's socket is effectively root-equivalent on its host. Prefer pointing it at a daemon dedicated to workloads the agent may touch (a dev VM, a remote sandbox, Docker Desktop, a rootless install) rather than your production socket. See the [Security considerations](https://github.com/
|
|
79
|
+
Connecting this server to an AI agent grants it the same access as a local Docker CLI session against the configured daemon — the daemon's socket is effectively root-equivalent on its host. Prefer pointing it at a daemon dedicated to workloads the agent may touch (a dev VM, a remote sandbox, Docker Desktop, a rootless install) rather than your production socket. See the [Security considerations](https://github.com/L337-org/docker-mcp#security-considerations) on GitHub before enabling the server.
|
|
80
80
|
|
|
81
81
|
## Links
|
|
82
82
|
|
|
83
|
-
- **Source, full docs & issues:** <https://github.com/
|
|
84
|
-
- **Also on PyPI** (run without a container): `uvx docker-mcp-server` — see the [README](https://github.com/
|
|
83
|
+
- **Source, full docs & issues:** <https://github.com/L337-org/docker-mcp>
|
|
84
|
+
- **Also on PyPI** (run without a container): `uvx docker-mcp-server` — see the [README](https://github.com/L337-org/docker-mcp#using-the-server).
|
|
85
85
|
- **License & contributing:** see the GitHub repository.
|