docker-mcp-server 2.0.0__tar.gz → 2.1.0__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.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/copilot-instructions.md +25 -4
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/workflows/canary.yaml +8 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/workflows/codeql.yaml +3 -3
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/workflows/images.yaml +5 -5
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/workflows/premerge.yaml +6 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/workflows/publish.yaml +5 -5
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.mcpbignore +2 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/CLAUDE.md +18 -1
- docker_mcp_server-2.1.0/CODE_OF_CONDUCT.md +83 -0
- docker_mcp_server-2.1.0/CONTRIBUTING.md +188 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/PKG-INFO +10 -145
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/README.md +9 -144
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/server.py +28 -8
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/configs.py +6 -2
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/containers.py +98 -35
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/images.py +10 -3
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/networks.py +23 -7
- docker_mcp_server-2.1.0/docker_mcp/tools/nodes.py +136 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/plugins.py +19 -9
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/prompts.py +29 -14
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/registry.py +63 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/resources.py +239 -4
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/secrets.py +15 -5
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/services.py +169 -4
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/swarm.py +33 -10
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/system.py +6 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/volumes.py +17 -6
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/manifest.json +1 -1
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/pyproject.toml +1 -1
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/conftest.py +11 -2
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_containers.py +24 -0
- docker_mcp_server-2.1.0/tests/integration/test_nodes.py +24 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_registry.py +9 -0
- docker_mcp_server-2.1.0/tests/integration/test_services.py +54 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_containers.py +91 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_naming.py +1 -0
- docker_mcp_server-2.1.0/tests/test_nodes.py +130 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_prompts.py +24 -2
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_registry.py +59 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_resources.py +220 -14
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_server.py +24 -5
- docker_mcp_server-2.1.0/tests/test_services.py +417 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/uv.lock +1 -1
- docker_mcp_server-2.0.0/docker_mcp/tools/nodes.py +0 -63
- docker_mcp_server-2.0.0/tests/test_nodes.py +0 -61
- docker_mcp_server-2.0.0/tests/test_services.py +0 -185
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.claude/commands/docker-sdk.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.claude/settings.json +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.dockerignore +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/CODEOWNERS +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/actions/file-failure-issue/action.yaml +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/dependabot.yaml +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/release.yml +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.github/workflows/publish-homebrew.yaml +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.gitignore +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/.python-version +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/DOCKERHUB.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/Dockerfile +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/LICENSE +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/MIGRATION-2.0.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/PRIVACY.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/SECURITY.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/assets/README.md +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/assets/icon.png +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/__init__.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/__main__.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/_env.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/_hosts.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/__init__.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/_cli.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/_labels.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/_ssh_proxy.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/_utils.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/buildx.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/compose.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/context.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/scout.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/docker_mcp/tools/stack.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/glama.json +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/mcpb_run.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/scripts/build-mcpb.sh +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/scripts/docker-mcp-server.rb.tpl +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/server.json +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/__init__.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/conftest.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/__init__.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_buildx.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_cli.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_compose.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_context.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_file_payloads.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_networks.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_scout.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_smoke.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/integration/test_stack.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_buildx.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_cli.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_compose.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_configs.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_context.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_env.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_hosts.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_images.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_labels.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_main.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_networks.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_plugins.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_pyproject_pins.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_scout.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_secrets.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_ssh_proxy.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_stack.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_swarm.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_system.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_utils.py +0 -0
- {docker_mcp_server-2.0.0 → docker_mcp_server-2.1.0}/tests/test_volumes.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What does this change do, and why? -->
|
|
4
|
+
|
|
5
|
+
## Test plan
|
|
6
|
+
|
|
7
|
+
<!-- How did you verify this? e.g. `uv run pytest -v`, `uv run ruff check . && uv run ruff format --check .`, `uv run pyright`, manual steps. -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
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/GavinLucas/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
|
+
- [ ] 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
|
+
- [ ] If this changes a dependency: `uv.lock` is updated (`uv lock`) and committed alongside `pyproject.toml`
|
|
@@ -26,7 +26,7 @@ from docker_mcp.server import prompt # prompt modules (with domain=...)
|
|
|
26
26
|
from docker_mcp.server import mcp # resource modules only
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`server.py` also owns **`TOOL_CATEGORIES`**, the central map classifying every tool as `READ_ONLY` / `MUTATING` / `DESTRUCTIVE`. The `@tool()` decorator uses it to attach MCP `ToolAnnotations` and to skip registration under the env switches `DOCKER_MCP_SERVER_READONLY` (register only read-only tools) and `DOCKER_MCP_SERVER_NO_DESTRUCTIVE` (register everything except destructive). It also records each tool's **domain** (its defining module's leaf, e.g. `containers`) so the orthogonal `DOCKER_MCP_SERVER_DISABLE=<domains>` switch can drop whole feature areas — including that domain's **prompts** (via the `prompt(domain=...)` helper) and its **doc-resource sections** (via `_SECTION_DOMAINS` in `resources.py`), not just its tools; the live snapshot is the `docker-mcp://tool-catalog` resource (`server.tool_catalog()`). **Every new tool needs a `TOOL_CATEGORIES` entry** — `tests/test_server.py` fails the build if the map drifts from the registered set. The decorator also runs `_slim_schema` on the advertised `inputSchema` to drop three information-free patterns (~18% of schema tokens): pydantic `title` annotations, the `{"type":"null"}` branch of a nullable `anyOf` (gated on a sibling `default`), and redundant `additionalProperties: true`; it's display-only (validation runs off `fn_metadata`), and `tests/test_server.py` asserts none survive. All server tunables are namespaced `DOCKER_MCP_SERVER_*`; read them through `docker_mcp/_env.py` (`read_env` / `env_flag`). The pre-rename `DOCKER_MCP_*` alias spellings were removed in 2.0; the alias-fallback mechanism remains for any future rename.
|
|
29
|
+
`server.py` also owns **`TOOL_CATEGORIES`**, the central map classifying every tool as `READ_ONLY` / `MUTATING` / `DESTRUCTIVE`. The `@tool()` decorator uses it to attach MCP `ToolAnnotations` and to skip registration under the env switches `DOCKER_MCP_SERVER_READONLY` (register only read-only tools) and `DOCKER_MCP_SERVER_NO_DESTRUCTIVE` (register everything except destructive). It also records each tool's **domain** (its defining module's leaf, e.g. `containers`) so the orthogonal `DOCKER_MCP_SERVER_DISABLE=<domains>` switch can drop whole feature areas — including that domain's **prompts** (via the `prompt(domain=...)` helper) and its **doc-resource sections** (via `_SECTION_DOMAINS` in `resources.py`), not just its tools; the live snapshot is the `docker-mcp://tool-catalog` resource (`server.tool_catalog()`). A handful of tools (`_NO_DOMAIN_TOOLS`, e.g. `docs_lookup`) have **no domain at all** — `_domain_for` returns `None`, which short-circuits `DOCKER_MCP_SERVER_DISABLE` entirely (mirrors `@prompt(domain=None)`'s same always-available semantics) — for tools whose value isn't tied to one feature area. **Every new tool needs a `TOOL_CATEGORIES` entry** — `tests/test_server.py` fails the build if the map drifts from the registered set. The decorator also runs `_slim_schema` on the advertised `inputSchema` to drop three information-free patterns (~18% of schema tokens): pydantic `title` annotations, the `{"type":"null"}` branch of a nullable `anyOf` (gated on a sibling `default`), and redundant `additionalProperties: true`; it's display-only (validation runs off `fn_metadata`), and `tests/test_server.py` asserts none survive. All server tunables are namespaced `DOCKER_MCP_SERVER_*`; read them through `docker_mcp/_env.py` (`read_env` / `env_flag`). The pre-rename `DOCKER_MCP_*` alias spellings were removed in 2.0; the alias-fallback mechanism remains for any future rename.
|
|
30
30
|
|
|
31
31
|
`server.py` also builds the FastMCP **`instructions`** string — pre-loaded into a client's context with the server name and tool names, *before* any per-tool schema, so for a lazy-loading client (e.g. Claude Code, which fetches tool schemas on demand) it's the main always-in-context surface. It's written as a **router** (per-domain keyword one-liners + a few tool-selection caveats), not docs, and does not enumerate tools (that's `docker-mcp://tool-catalog`). `build_instructions()` renders it from `_DOMAIN_BLURBS`, emitting a domain's line **only when that domain has a registered tool**, so `DOCKER_MCP_SERVER_DISABLE` / `_READONLY` / `_NO_DESTRUCTIVE` are honored via the one registration flag. `finalize_instructions()` (called from `docker_mcp/__init__.py` after all tools import) writes it through to `mcp._mcp_server.instructions` (FastMCP's `instructions` is a read-only property read at `run()` time, so a late write propagates; the reach-in is guarded). **A new tool domain needs a `_DOMAIN_BLURBS` entry** or the router silently omits it.
|
|
32
32
|
|
|
@@ -148,15 +148,34 @@ def mcp_example(name: str):
|
|
|
148
148
|
"""
|
|
149
149
|
Say hello to someone by name.
|
|
150
150
|
|
|
151
|
-
args: name
|
|
151
|
+
args: name - The name to say hello to
|
|
152
152
|
returns: str - The greeting
|
|
153
153
|
"""
|
|
154
154
|
return f"Hello, {name}!"
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
- One-line summary sentence, then a blank line
|
|
158
|
-
- `args:` section lists each parameter as `name
|
|
158
|
+
- `args:` section lists each parameter as `name - description`. Do **not** repeat the parameter's
|
|
159
|
+
type — the type annotation already lands in the tool's `inputSchema`, which the client sees
|
|
160
|
+
alongside the description, so a `name: type - ...` form just duplicates it as prose tokens. (The
|
|
161
|
+
`returns:` line keeps its type, since the return shape is not in the input schema.)
|
|
159
162
|
- `returns:` line documents the return type and what it contains
|
|
163
|
+
- Keep descriptions terse: state every functional fact (defaults, accepted formats/values, return
|
|
164
|
+
keys, important caveats) but cut redundancy and verbose phrasing. The docstring is the entire
|
|
165
|
+
tool `description` the client pays tokens for on every session.
|
|
166
|
+
- **A one-line summary + bare `args`/`returns` is not enough for any tool with non-obvious
|
|
167
|
+
behavior.** If the tool has side effects, preconditions, a non-obvious failure mode, or overlaps
|
|
168
|
+
with another tool a caller could reach for instead, add a short paragraph (2-5 sentences,
|
|
169
|
+
between the summary and `args:`) covering: when to use it vs. the alternative, side
|
|
170
|
+
effects/preconditions, and concrete parameter formats/values — not just restating the signature.
|
|
171
|
+
This directly maps to what a low-scoring tool is missing on Glama's per-tool quality rubric
|
|
172
|
+
(`Behavior`, `Usage Guidelines`, `Parameters` sub-scores) — three separate PRs (#97, the 2.0
|
|
173
|
+
rename, #129) have had to chase down thin docstrings written without this paragraph. **Write it
|
|
174
|
+
this way the first time a tool is added or its behavior changes** — don't wait for a future
|
|
175
|
+
Glama pass to catch it. Verify every factual claim in that paragraph against the live docker-py
|
|
176
|
+
docs / Engine API spec per the Docker SDK Policy below — an unverified claim about identifier
|
|
177
|
+
semantics (e.g. "name or id" for a resource actually addressed by name only) is exactly the kind
|
|
178
|
+
of thing PR review catches late.
|
|
160
179
|
|
|
161
180
|
### Bounding rules
|
|
162
181
|
|
|
@@ -165,7 +184,9 @@ def mcp_example(name: str):
|
|
|
165
184
|
|
|
166
185
|
### MCP resources
|
|
167
186
|
|
|
168
|
-
`docker_mcp/tools/resources.py` exposes `@mcp.resource(uri, mime_type=...)` endpoints (not tools) for read-only data: the Docker SDK for Python documentation under the `docker-docs://` URI scheme, `docker-mcp://tool-catalog` (the live tool/domain/category snapshot), `docker-mcp://hosts` (the resolved host registry), and
|
|
187
|
+
`docker_mcp/tools/resources.py` exposes `@mcp.resource(uri, mime_type=...)` endpoints (not tools) for read-only data: the Docker SDK for Python documentation under the `docker-docs://` URI scheme, `docker-mcp://tool-catalog` (the live tool/domain/category snapshot), `docker-mcp://hosts` (the resolved host registry), and three "watch this specific thing over time" observability families — **containers** (`docker://containers` / `docker-logs://{id_or_name}` / `docker-stats://{id_or_name}`, reusing `containers.py`'s private `_read_log_tail` / `_read_stats_summary`), **services** (`docker://services` / `service-logs://{id_or_name}` / `service-tasks://{id_or_name}`, reusing `services.py`'s private `_read_service_log_tail` / `_read_service_task_summary` — the latter also backs `service_wait`'s `running` mode), and **nodes** (`docker://nodes` index only — deliberately no per-node child resource, since that would need an unbounded per-service task fan-out with no single cheap call). All are `host=`-aware and refuse when their domain (`containers`/`services`/`nodes`) is disabled. **In multi-host mode all three families are host-aware** — the default forms become empty-authority (`docker:///containers`, `service-tasks:///{id}`, …) and host-qualified variants (`docker://{host}/containers`, `service-logs://{host}/{id}`, …) register alongside, disambiguated by path-segment count; single-host keeps the bare forms. Registration is gated on `_hosts.is_multi()`. `_SECTION_DOMAINS` maps each doc section to a domain so `DOCKER_MCP_SERVER_DISABLE` hides a disabled domain's sections (registered with the server via `register_resource_domains`). Use the same docstring format as tools.
|
|
188
|
+
|
|
189
|
+
`resources.py` also has one `@tool()`: **`docs_lookup(section=None)`** — a tool-callable mirror of `docker-docs://` for clients that can't read MCP resources (Claude Desktop, Cursor). It wraps `list_docs_sections()`/`get_docs_section()` directly (same behavior either way, including per-section domain refusal) and is one of `_NO_DOMAIN_TOOLS` — always registered regardless of `DOCKER_MCP_SERVER_DISABLE`. Several `extra_kwargs`-heavy tool docstrings and docs-reliant prompts point at it as the resource-read fallback — do the same for new ones in that shape.
|
|
169
190
|
|
|
170
191
|
### MCP prompts
|
|
171
192
|
|
|
@@ -42,6 +42,11 @@ jobs:
|
|
|
42
42
|
|
|
43
43
|
- name: Set up uv
|
|
44
44
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
45
|
+
with:
|
|
46
|
+
# Pinned (matching the Dockerfile's uv image tag) so this step never needs the
|
|
47
|
+
# live "resolve latest" fetch to raw.githubusercontent.com — that network call
|
|
48
|
+
# failed transiently on 2026-07-06 and blocked an unrelated PR (#130).
|
|
49
|
+
version: "0.11.21"
|
|
45
50
|
|
|
46
51
|
- name: Resolve runtime deps as wheels-only for each platform
|
|
47
52
|
# --only-binary :all: makes resolution fail if any dependency lacks a wheel for the
|
|
@@ -103,6 +108,9 @@ jobs:
|
|
|
103
108
|
# repo — so disable the repo-keyed cache and the empty-workdir warning it triggers.
|
|
104
109
|
enable-cache: false
|
|
105
110
|
ignore-empty-workdir: true
|
|
111
|
+
# Pinned (matching the Dockerfile's uv image tag) so this step never needs the
|
|
112
|
+
# live "resolve latest" fetch to raw.githubusercontent.com.
|
|
113
|
+
version: "0.11.21"
|
|
106
114
|
|
|
107
115
|
- name: Import smoke (full tool-registration path, no daemon needed)
|
|
108
116
|
run: uv run --no-project --python 3.14 --with docker-mcp-server python -c "import docker_mcp"
|
|
@@ -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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # 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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
|
|
40
40
|
|
|
41
41
|
- name: Perform CodeQL Analysis
|
|
42
|
-
uses: github/codeql-action/analyze@
|
|
42
|
+
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
|
|
@@ -50,10 +50,10 @@ jobs:
|
|
|
50
50
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
51
51
|
|
|
52
52
|
- name: Set up Docker Buildx
|
|
53
|
-
uses: docker/setup-buildx-action@
|
|
53
|
+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
54
54
|
|
|
55
55
|
- name: Build image (${{ matrix.variant }})
|
|
56
|
-
uses: docker/build-push-action@
|
|
56
|
+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
57
57
|
with:
|
|
58
58
|
context: .
|
|
59
59
|
platforms: linux/amd64
|
|
@@ -85,13 +85,13 @@ jobs:
|
|
|
85
85
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
86
86
|
|
|
87
87
|
- name: Set up QEMU
|
|
88
|
-
uses: docker/setup-qemu-action@
|
|
88
|
+
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
|
89
89
|
|
|
90
90
|
- name: Set up Docker Buildx
|
|
91
|
-
uses: docker/setup-buildx-action@
|
|
91
|
+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
92
92
|
|
|
93
93
|
- name: Build full variant for amd64 + arm64
|
|
94
|
-
uses: docker/build-push-action@
|
|
94
|
+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
95
95
|
with:
|
|
96
96
|
context: .
|
|
97
97
|
platforms: linux/amd64,linux/arm64
|
|
@@ -176,6 +176,12 @@ jobs:
|
|
|
176
176
|
docker buildx version
|
|
177
177
|
docker scout version 2>/dev/null || echo "scout not installed; scout integration tests will skip"
|
|
178
178
|
|
|
179
|
+
- name: Initialize a single-node swarm
|
|
180
|
+
# Needed for service_wait / node_wait integration tests (skip_if_no_swarm), which
|
|
181
|
+
# otherwise skip cleanly on a non-swarm daemon. No teardown needed — the runner is
|
|
182
|
+
# ephemeral and torn down after the job.
|
|
183
|
+
run: docker swarm init
|
|
184
|
+
|
|
179
185
|
- name: Optional Docker Hub login to dodge anonymous rate limits
|
|
180
186
|
# Opt-in: set DOCKERHUB_USER / DOCKERHUB_TOKEN as repo secrets to authenticate.
|
|
181
187
|
# Without them, the job runs anonymous and the existing skip-on-pull-failure
|
|
@@ -219,17 +219,17 @@ jobs:
|
|
|
219
219
|
echo "Publishing \`${{ matrix.variant }}\` as: $tags" >> "$GITHUB_STEP_SUMMARY"
|
|
220
220
|
|
|
221
221
|
- name: Set up QEMU
|
|
222
|
-
uses: docker/setup-qemu-action@
|
|
222
|
+
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
|
223
223
|
with:
|
|
224
224
|
# The two matrix jobs race to cache the same binfmt image; skip the cache to avoid the
|
|
225
225
|
# harmless "unable to reserve cache" warning while keeping both builds parallel.
|
|
226
226
|
cache-image: false
|
|
227
227
|
|
|
228
228
|
- name: Set up Docker Buildx
|
|
229
|
-
uses: docker/setup-buildx-action@
|
|
229
|
+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
230
230
|
|
|
231
231
|
- name: Log in to GHCR
|
|
232
|
-
uses: docker/login-action@
|
|
232
|
+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
233
233
|
with:
|
|
234
234
|
registry: ghcr.io
|
|
235
235
|
username: ${{ github.actor }}
|
|
@@ -237,13 +237,13 @@ jobs:
|
|
|
237
237
|
|
|
238
238
|
- name: Log in to Docker Hub
|
|
239
239
|
if: env.DOCKERHUB_USER != '' && env.DOCKERHUB_TOKEN != ''
|
|
240
|
-
uses: docker/login-action@
|
|
240
|
+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
241
241
|
with:
|
|
242
242
|
username: ${{ secrets.DOCKERHUB_USER }}
|
|
243
243
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
244
244
|
|
|
245
245
|
- name: Build and push ${{ matrix.variant }}
|
|
246
|
-
uses: docker/build-push-action@
|
|
246
|
+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
247
247
|
with:
|
|
248
248
|
context: .
|
|
249
249
|
platforms: linux/amd64,linux/arm64
|
|
@@ -90,6 +90,8 @@ After registering each tool the decorator also calls `_slim_schema` on the tool'
|
|
|
90
90
|
|
|
91
91
|
The decorator also records each tool's **domain** — the leaf of its defining module (`docker_mcp.tools.containers` → `containers`) — so the orthogonal `DOCKER_MCP_SERVER_DISABLE=<domains>` switch can drop a whole feature area (e.g. `swarm,plugins`) from the registered surface regardless of category. A tool registers only if its category survives the read-only switches *and* its domain is not disabled. `DOCKER_MCP_SERVER_DISABLE` reaches beyond tools: the `prompt(domain=...)` helper skips a disabled domain's prompts, and `resources.py` hides a disabled domain's doc sections — so disabling e.g. `scout` drops its tools, its prompts, and its `docker-docs://scout` sections together. The full picture (every tool's domain/category, plus the `prompts` list and `disabled_doc_sections`) is exposed via `tool_catalog()` and the `docker-mcp://tool-catalog` resource, so the classification is auditable at runtime, not just in the source map.
|
|
92
92
|
|
|
93
|
+
A handful of tools have **no domain at all** — `_NO_DOMAIN_TOOLS` (today just `docs_lookup`) — because their value isn't tied to any single Docker feature area being enabled or disabled. `_domain_for` returns `None` for these, and `None` short-circuits the `_domain_enabled` check entirely, so `DOCKER_MCP_SERVER_DISABLE` can never drop them (not even by their own name). This mirrors `@prompt(domain=None)`'s identical "cross-cutting, always available" semantics for prompts. They still register/deregister normally under `DOCKER_MCP_SERVER_READONLY`/`_NO_DESTRUCTIVE` based on their own category (a domain-less tool should still be `READ_ONLY` for this to matter in practice).
|
|
94
|
+
|
|
93
95
|
**Server `instructions` router.** `server.py` also builds the FastMCP `instructions` string — the text a client pre-loads into context alongside the server name and tool names, *before* any per-tool schema. For a lazy-loading client (e.g. Claude Code, which fetches tool schemas on demand) that's the main always-in-context surface we control, so it's written as a **router**, not docs: a per-domain one-liner mapping user vocabulary onto the domain keyword a tool search will hit, plus a few tool-selection caveats. It deliberately does not enumerate tools (that's the `docker-mcp://tool-catalog` resource). It's built dynamically by `build_instructions()` from `_DOMAIN_BLURBS`, emitting a domain's line **only when that domain has a registered tool** — so `DOCKER_MCP_SERVER_DISABLE` / `_READONLY` / `_NO_DESTRUCTIVE` are all honored through the one registration flag, and the router never advertises a domain whose tools didn't register. `finalize_instructions()` (called from `docker_mcp/__init__.py` *after* every tool module imports) writes the result through to `mcp._mcp_server.instructions` — FastMCP's `instructions` is a read-only property whose value is read at `run()` time, so a late write propagates to the MCP initialize handshake; the `_mcp_server` reach-in is guarded like `_slim_schema`. **A new tool *domain* needs a `_DOMAIN_BLURBS` entry** or the router silently omits it (`tests/test_server.py` checks the router tracks the registered domain set).
|
|
94
96
|
|
|
95
97
|
### Multi-daemon host registry (`docker_mcp/_hosts.py`)
|
|
@@ -260,10 +262,25 @@ def mcp_example(name: str):
|
|
|
260
262
|
- Keep descriptions terse: state every functional fact (defaults, accepted formats/values, return
|
|
261
263
|
keys, important caveats) but cut redundancy and verbose phrasing. The docstring is the entire
|
|
262
264
|
tool `description` the client pays tokens for on every session.
|
|
265
|
+
- **A one-line summary + bare `args`/`returns` is not enough for any tool with non-obvious
|
|
266
|
+
behavior.** If the tool has side effects, preconditions, a non-obvious failure mode, or overlaps
|
|
267
|
+
with another tool a caller could reach for instead, add a short paragraph (2-5 sentences,
|
|
268
|
+
between the summary and `args:`) covering: when to use it vs. the alternative, side
|
|
269
|
+
effects/preconditions, and concrete parameter formats/values — not just restating the signature.
|
|
270
|
+
This directly maps to what a low-scoring tool is missing on Glama's per-tool quality rubric
|
|
271
|
+
(`Behavior`, `Usage Guidelines`, `Parameters` sub-scores) — see [[project_glama_docstring_quality]]
|
|
272
|
+
in memory for the three rounds of cleanup (#97, the 2.0 rename, #129) this has already taken to
|
|
273
|
+
chase down thin docstrings written without this paragraph. **Write it this way the first time a
|
|
274
|
+
tool is added or its behavior changes** — don't wait for a future Glama pass to catch it. Verify
|
|
275
|
+
every factual claim in that paragraph against the live docker-py docs / Engine API spec per the
|
|
276
|
+
Docker SDK Policy below — an unverified claim about identifier semantics (e.g. "name or id" for a
|
|
277
|
+
resource actually addressed by name only) is exactly the kind of thing PR review catches late.
|
|
263
278
|
|
|
264
279
|
### MCP resources
|
|
265
280
|
|
|
266
|
-
`docker_mcp/tools/resources.py` exposes `@mcp.resource(uri, mime_type=...)` endpoints (not tools) for read-only data: the Docker SDK for Python documentation under the `docker-docs://` URI scheme, plus `docker-mcp://tool-catalog` (the live tool/domain/category snapshot from `server.tool_catalog()`), `docker-mcp://hosts` (the resolved host registry, mirroring `host_list`), and
|
|
281
|
+
`docker_mcp/tools/resources.py` exposes `@mcp.resource(uri, mime_type=...)` endpoints (not tools) for read-only data: the Docker SDK for Python documentation under the `docker-docs://` URI scheme, plus `docker-mcp://tool-catalog` (the live tool/domain/category snapshot from `server.tool_catalog()`), `docker-mcp://hosts` (the resolved host registry, mirroring `host_list`), and three families of "watch this specific thing over time" observability resources — **containers** (`docker://containers` index, `docker-logs://{id_or_name}` bounded log tail, `docker-stats://{id_or_name}` computed usage summary), **services** (`docker://services` index, `service-logs://{id_or_name}` bounded log tail, `service-tasks://{id_or_name}` computed task/rollout summary — running vs. desired task counts, failing tasks, and `UpdateStatus.State` if a rolling update is in progress), and **nodes** (`docker://nodes` index only — state/availability/role/reachability per node; deliberately no per-node child resource, since a "tasks on this node" view would need an unbounded fan-out across every service's tasks with no single cheap call, unlike the other two families). **In multi-host mode all three families are host-aware:** the default-host forms become empty-authority (`docker:///containers`, `docker-logs:///{id}`, `service-tasks:///{id}`, …) and host-qualified variants (`docker://{host}/containers`, `service-logs://{host}/{id}`, …) are registered alongside, disambiguated by path-segment count; single-host keeps the bare forms unchanged. Registration is gated on `_hosts.is_multi()`, and each index emits child `logs`/`stats`/`tasks` URIs matching its own scheme. Container resources reuse the private `_read_log_tail` / `_read_stats_summary` helpers in `containers.py`; service resources reuse `_read_service_log_tail` / `_read_service_task_summary` in `services.py` (the latter also backs `service_wait`'s `running` mode — see Swarm tools below); all refuse at read time when their domain (`containers`/`services`/`nodes`) is disabled (mirroring `get_docs_section`). Each doc section maps to a domain via `_SECTION_DOMAINS` (registered with the server through `register_resource_domains`), so `DOCKER_MCP_SERVER_DISABLE` hides a disabled domain's sections from `docker-docs://contents` and makes `get_docs_section` refuse them. Resources follow the same docstring format as tools and are also star-imported via `docker_mcp/tools/__init__.py`.
|
|
282
|
+
|
|
283
|
+
`resources.py` also has one `@tool()`: **`docs_lookup(section=None)`** — a tool-callable mirror of the `docker-docs://` family for clients that can't read MCP resources (e.g. Claude Desktop, Cursor). It calls `list_docs_sections()`/`get_docs_section()` directly rather than duplicating their logic, so behavior (including the per-section domain refusal above) is identical either way. It's one of `_NO_DOMAIN_TOOLS` (see "Server singleton" above) — always registered regardless of `DOCKER_MCP_SERVER_DISABLE` — since looking something up isn't tied to any one feature area. Several tool docstrings (`container_run`/`container_create`/`service_create`'s `extra_kwargs`) and prompts (`lookup_docker_docs`, `verify_docker_method`, `review_dockerfile`, `audit_container_security`) point at it as the fallback when a client can't read the equivalent resource — a new passthrough-heavy tool or docs-reliant prompt should do the same.
|
|
267
284
|
|
|
268
285
|
### MCP prompts
|
|
269
286
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
8
|
+
|
|
9
|
+
## Our Standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
|
12
|
+
|
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
|
22
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
23
|
+
* Public or private harassment
|
|
24
|
+
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
|
25
|
+
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
26
|
+
|
|
27
|
+
## Enforcement Responsibilities
|
|
28
|
+
|
|
29
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
|
30
|
+
|
|
31
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
|
36
|
+
|
|
37
|
+
## Enforcement
|
|
38
|
+
|
|
39
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Gavin@L337.org](mailto:Gavin@L337.org). All complaints will be reviewed and investigated promptly and fairly.
|
|
40
|
+
|
|
41
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
42
|
+
|
|
43
|
+
## Enforcement Guidelines
|
|
44
|
+
|
|
45
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
|
46
|
+
|
|
47
|
+
### 1. Correction
|
|
48
|
+
|
|
49
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
|
50
|
+
|
|
51
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
|
52
|
+
|
|
53
|
+
### 2. Warning
|
|
54
|
+
|
|
55
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
|
56
|
+
|
|
57
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
|
58
|
+
|
|
59
|
+
### 3. Temporary Ban
|
|
60
|
+
|
|
61
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
|
62
|
+
|
|
63
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
|
64
|
+
|
|
65
|
+
### 4. Permanent Ban
|
|
66
|
+
|
|
67
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
|
68
|
+
|
|
69
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
|
70
|
+
|
|
71
|
+
## Attribution
|
|
72
|
+
|
|
73
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
74
|
+
|
|
75
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
76
|
+
|
|
77
|
+
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
|
|
78
|
+
|
|
79
|
+
[homepage]: https://www.contributor-covenant.org
|
|
80
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
81
|
+
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
82
|
+
[FAQ]: https://www.contributor-covenant.org/faq
|
|
83
|
+
[translations]: https://www.contributor-covenant.org/translations
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Contributing to docker-mcp-server
|
|
2
|
+
|
|
3
|
+
Contributions are welcome. The project values a tight mapping between the Docker SDK's public surface and the MCP tools we expose. By participating, you're expected to uphold the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
4
|
+
|
|
5
|
+
## Before you start
|
|
6
|
+
|
|
7
|
+
This is an [MCP](https://modelcontextprotocol.io) server: it exposes Docker operations as `@tool()`-decorated Python functions that an AI agent calls over the Model Context Protocol. If that sentence is new to you, the short version is: each function in `docker_mcp/tools/` becomes one callable "tool" an agent can invoke, with the function's docstring as the description the agent sees and its type-annotated parameters as the input schema.
|
|
8
|
+
|
|
9
|
+
The practical mental model for finding your way around:
|
|
10
|
+
|
|
11
|
+
- One file per Docker feature area (`containers.py`, `images.py`, `swarm.py`, …), each backed by either the [docker-py SDK](https://docker-py.readthedocs.io/en/stable/) or a `docker` CLI shell-out (`compose.py`, `buildx.py`, …). See the tree below for the full map.
|
|
12
|
+
- Every tool function is registered centrally in `docker_mcp/server.py`, which is also where read-only/destructive classification, naming enforcement, and the domain-disable switches live.
|
|
13
|
+
- `CLAUDE.md` (repo root) is the full architecture reference — written to brief Claude Code, but equally the canonical source for humans. This file (`CONTRIBUTING.md`) covers the practical day-to-day: setup, the checklist for adding a tool, testing, and submitting changes. If something isn't covered here, it's almost certainly in `CLAUDE.md`.
|
|
14
|
+
|
|
15
|
+
## Project layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
.
|
|
19
|
+
├── docker_mcp/ # the package — `python -m docker_mcp` runs the server
|
|
20
|
+
│ ├── __init__.py # defines `main()`; side-effect-imports `server` and `tools`
|
|
21
|
+
│ ├── __main__.py # calls `main()` so `python -m docker_mcp` works
|
|
22
|
+
│ ├── server.py # creates the FastMCP singleton (`mcp`) shared by every tool module
|
|
23
|
+
│ └── tools/ # one file per Docker SDK domain or CLI/registry feature
|
|
24
|
+
│ ├── _cli.py # cross-platform subprocess helper for docker CLI shell-outs (private)
|
|
25
|
+
│ ├── _utils.py # shared helpers (drop_none, join_bounded, MAX_PAYLOAD_BYTES) (private)
|
|
26
|
+
│ ├── system.py # DockerClient connection + lazy `_get_client()` helper
|
|
27
|
+
│ ├── containers.py
|
|
28
|
+
│ ├── images.py
|
|
29
|
+
│ ├── networks.py
|
|
30
|
+
│ ├── volumes.py
|
|
31
|
+
│ ├── configs.py
|
|
32
|
+
│ ├── secrets.py
|
|
33
|
+
│ ├── nodes.py
|
|
34
|
+
│ ├── services.py
|
|
35
|
+
│ ├── swarm.py
|
|
36
|
+
│ ├── plugins.py
|
|
37
|
+
│ ├── compose.py # `docker compose` CLI plugin (shells out via _cli.py)
|
|
38
|
+
│ ├── stack.py # `docker stack` (Compose-on-Swarm) CLI (shells out via _cli.py)
|
|
39
|
+
│ ├── context.py # `docker context` CLI (shells out via _cli.py)
|
|
40
|
+
│ ├── buildx.py # `docker buildx` CLI plugin (shells out via _cli.py)
|
|
41
|
+
│ ├── scout.py # `docker scout` CLI plugin (shells out via _cli.py)
|
|
42
|
+
│ ├── registry.py # OCI v2 registries + Docker Hub HTTPS APIs (no daemon)
|
|
43
|
+
│ ├── prompts.py # @prompt(...) templates for common docker workflows
|
|
44
|
+
│ └── resources.py # @mcp.resource() endpoints exposing SDK + CLI + registry docs
|
|
45
|
+
├── tests/ # pytest suite, mirrors `docker_mcp/tools/` one-to-one
|
|
46
|
+
│ └── integration/ # tests that hit a real Docker daemon or docker.io
|
|
47
|
+
├── assets/ # bundle assets (e.g. the .mcpb icon) packed into the Desktop Extension
|
|
48
|
+
├── scripts/ # developer convenience scripts (not used by CI) — e.g. build-mcpb.sh
|
|
49
|
+
└── dist/ # build output (git-ignored) — local .mcpb test bundles land here
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each `docker_mcp/tools/<file>.py` has a matching `tests/test_<file>.py`. New modules must be added to `docker_mcp/tools/__init__.py` and have a corresponding test file. Tool modules that wrap CLI features must funnel every subprocess call through `docker_mcp/tools/_cli.py` so the cross-platform safety concerns (binary discovery, no shell, UTF-8 decoding, output capping, Windows console suppression, env scrubbing) live in one place.
|
|
53
|
+
|
|
54
|
+
New Docker functionality goes in the matching existing file (e.g. a new volume operation goes in `volumes.py`), not a new file — a new `docker_mcp/tools/<domain>.py` is only for a Docker feature area that doesn't exist in the tree above.
|
|
55
|
+
|
|
56
|
+
## Conventions
|
|
57
|
+
|
|
58
|
+
Tool functions are decorated with `@tool()` (the project's wrapper around `@mcp.tool()`, imported from `docker_mcp.server`) and follow this docstring style:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from docker_mcp.server import tool
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@tool()
|
|
65
|
+
def mcp_example(name: str):
|
|
66
|
+
"""
|
|
67
|
+
Say hello to someone by name.
|
|
68
|
+
|
|
69
|
+
args: name - The name to say hello to
|
|
70
|
+
returns: str - The greeting
|
|
71
|
+
"""
|
|
72
|
+
return f"Hello, {name}!"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Tool modules import `tool` from `docker_mcp.server`; prompt modules import `prompt` from `docker_mcp.server`. Only resource modules (`@mcp.resource(...)`) import `mcp` directly — never import `mcp` directly in a tool or prompt module, that creates a circular import.
|
|
76
|
+
- Every tool needs a `TOOL_CATEGORIES` entry in `docker_mcp/server.py` (`READ_ONLY` / `MUTATING` / `DESTRUCTIVE`); the central map drives the tool's `ToolAnnotations` and the read-only env switches, and `tests/test_server.py` fails if it drifts from the registered set. A tool's *domain* (for `DOCKER_MCP_SERVER_DISABLE` and the tool catalog) is derived automatically from its module name, so putting a tool in the right `docker_mcp/tools/<domain>.py` file is all that's needed.
|
|
77
|
+
- Tool names follow `<management-command>_<verb>`, anchored to the docker CLI's own management-command structure (`docker container ls` → `container_list`), with long-form verbs (`list`/`remove`/`inspect`, never `ls`/`rm`/`get`). `tests/test_naming.py` enforces this — a new tool with a short-form verb or an unapproved prefix fails CI. See `CLAUDE.md`'s "Tool naming convention" section for the full rule, including identifier parameter naming (`id_or_name`, `name`/`names`, `repository`).
|
|
78
|
+
- Line length is 120 characters (enforced by ruff).
|
|
79
|
+
- CLI shell-outs must go through `docker_mcp/tools/_cli.py:run_docker` — never call `subprocess.run` directly from a tool module. The helper enforces `shell=False`, resolves the binary via `shutil.which` (cross-platform), decodes output as UTF-8 with replace, caps the captured bytes, scrubs the environment, and suppresses console pop-ups on Windows.
|
|
80
|
+
- A tool with non-obvious behavior (side effects, preconditions, a non-obvious failure mode, or overlap with another tool) needs more than a one-line summary — add a short paragraph covering when to use it vs. the alternative, side effects/preconditions, and concrete parameter formats. Verify any factual claim against the live docker-py docs / Engine API spec rather than assuming; see [`CLAUDE.md`](CLAUDE.md)'s "Tool function format" section for the full convention and why it matters.
|
|
81
|
+
|
|
82
|
+
## Verifying the SDK before writing code
|
|
83
|
+
|
|
84
|
+
To prevent hallucinated method names, the project includes a `/docker-sdk` Claude Code skill that fetches the live Docker SDK for Python documentation, inventories what's already exposed, and produces a gap analysis. Run it before adding new tools:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
/docker-sdk # full gap analysis
|
|
88
|
+
/docker-sdk containers # focus on a single domain
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If you're not using Claude Code, check method names and signatures directly against the [docker-py docs](https://docker-py.readthedocs.io/en/stable/index.html) (or, for SDK gaps, the [low-level `APIClient`](https://docker-py.readthedocs.io/en/stable/api.html)) before writing a call — don't assume a method exists because it sounds plausible.
|
|
92
|
+
|
|
93
|
+
## Testing conventions
|
|
94
|
+
|
|
95
|
+
Unit tests mock the Docker client rather than hitting a real daemon, patching `_get_client` (or, for CLI-backed modules, `run_docker`) at the point the tool module looks it up. A minimal example, from `tests/test_volumes.py`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from unittest.mock import MagicMock, patch
|
|
99
|
+
|
|
100
|
+
from docker_mcp.tools.volumes import volume_inspect
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_volume_inspect():
|
|
104
|
+
volume = MagicMock()
|
|
105
|
+
volume.attrs = {"Name": "myvol"}
|
|
106
|
+
with patch("docker_mcp.tools.volumes._get_client") as mock_client:
|
|
107
|
+
mock_client.return_value.volumes.get.return_value = volume
|
|
108
|
+
assert volume_inspect("myvol") == {"Name": "myvol"}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Integration tests (`tests/integration/`) call the real tool function against an actual Docker daemon and are auto-marked `@pytest.mark.integration` by `tests/integration/conftest.py`, which also provides an autouse `skip_if_no_daemon` fixture — they're skipped automatically (not failed) when no daemon is reachable, so it's safe to run the full suite without Docker running. They're excluded from the default `pytest` invocation (`-m 'not integration'` in `pyproject.toml`); run them explicitly with `uv run pytest -m integration -v`.
|
|
112
|
+
|
|
113
|
+
## Checklist when adding a new tool module
|
|
114
|
+
|
|
115
|
+
When you add a new `docker_mcp/tools/<domain>.py`, also update:
|
|
116
|
+
|
|
117
|
+
1. **`docker_mcp/tools/__init__.py`** — star-import the module (private helpers prefixed with `_` are excluded).
|
|
118
|
+
2. **`tests/test_<domain>.py`** — unit tests using mocks (no real daemon).
|
|
119
|
+
3. **`tests/integration/test_<domain>.py`** — at least one happy-path test against a real daemon (or override the `skip_if_no_daemon` fixture if the module doesn't need one).
|
|
120
|
+
4. **`docker_mcp/tools/prompts.py`** — at least one `@prompt(...)` template that exercises the new tools end-to-end.
|
|
121
|
+
5. **`docker_mcp/tools/resources.py`** — add an entry under `SDK_SECTIONS` or `EXTERNAL_SECTIONS` if the new domain has authoritative docs the agent should be able to read at runtime.
|
|
122
|
+
6. **README.md** — append to the "What the agent can do" list and (if relevant) the "Security considerations" section.
|
|
123
|
+
7. **SECURITY.md** — only if the new module exposes a new class of risk not already covered by the README's Security section.
|
|
124
|
+
|
|
125
|
+
If you're only adding a tool or two to an *existing* domain file rather than a whole new module, items 1 and 7 don't apply, but 2-6 still do wherever relevant (e.g. a new tool still needs unit + integration test cases, even if the test file itself already exists).
|
|
126
|
+
|
|
127
|
+
## Local development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# install dependencies (creates .venv, installs runtime + dev deps)
|
|
131
|
+
uv sync
|
|
132
|
+
|
|
133
|
+
# run the server
|
|
134
|
+
uv run python -m docker_mcp
|
|
135
|
+
# …or via the installed console script
|
|
136
|
+
uv run docker-mcp
|
|
137
|
+
|
|
138
|
+
# unit tests (integration tests are excluded by default)
|
|
139
|
+
uv run pytest -v
|
|
140
|
+
|
|
141
|
+
# integration tests — require a running Docker daemon at $DOCKER_HOST
|
|
142
|
+
uv run pytest -m integration -v
|
|
143
|
+
|
|
144
|
+
# lint, format, type-check
|
|
145
|
+
uv run ruff check .
|
|
146
|
+
uv run ruff format .
|
|
147
|
+
uv run pyright
|
|
148
|
+
|
|
149
|
+
# install the pre-commit hook (one-time, runs ruff on every commit)
|
|
150
|
+
uv run pre-commit install
|
|
151
|
+
|
|
152
|
+
# add a runtime dependency
|
|
153
|
+
uv add <package>
|
|
154
|
+
|
|
155
|
+
# add a development dependency
|
|
156
|
+
uv add --group dev <package>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Submitting your change
|
|
160
|
+
|
|
161
|
+
CI (`.github/workflows/premerge.yaml`) runs on every pull request and push to `main`, and must pass before merging:
|
|
162
|
+
|
|
163
|
+
- `pytest` — two separate jobs: unit tests (`uv run pytest -v`, same as local) and integration tests (`uv run pytest -m integration -v`) against the real Docker Engine the `ubuntu-latest` runner ships with. You don't need a local daemon to contribute — `uv run pytest -v` alone (which excludes integration tests by default) is enough to check your change; the integration job will exercise it in CI.
|
|
164
|
+
- `ruff check` and `ruff format --check` — lint and formatting; run `uv run ruff check . && uv run ruff format .` locally to fix both before committing (the pre-commit hook from `uv run pre-commit install` runs `ruff check --fix` and `ruff format` automatically on each commit, so this is usually already handled for you).
|
|
165
|
+
- `pyright` — type-check.
|
|
166
|
+
- CI installs with `uv sync --locked`, which **fails** if `uv.lock` disagrees with `pyproject.toml` rather than silently re-locking. If you change a dependency, run `uv lock` and commit the updated lockfile alongside `pyproject.toml`.
|
|
167
|
+
|
|
168
|
+
A separate, non-blocking `Check docs mirror` job flags a PR that edits `CLAUDE.md` or `.github/copilot-instructions.md` without the other — the two files intentionally mirror each other (Copilot's own PR review is driven by `copilot-instructions.md`), so a change to project structure, conventions, or the tool surface should update both.
|
|
169
|
+
|
|
170
|
+
Keep PRs focused — one logical change per PR is easier to review than a bundle of unrelated fixes. For anything beyond a small, self-contained fix, consider opening an issue first (see "Reporting issues" below) so the approach can be discussed before you invest time in an implementation.
|
|
171
|
+
|
|
172
|
+
## Building a local Desktop Extension (.mcpb)
|
|
173
|
+
|
|
174
|
+
To smoke-test the Claude Desktop Extension locally, pack a bundle with the developer helper in `scripts/`:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# pack dist/docker-mcp-server-<version>.mcpb (auto-increments -1, -2, … if it exists)
|
|
178
|
+
scripts/build-mcpb.sh
|
|
179
|
+
|
|
180
|
+
# …or give it an explicit name (a .mcpb extension is added if missing)
|
|
181
|
+
scripts/build-mcpb.sh my-test-bundle
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
It reads the version from `pyproject.toml`, creates `dist/` if needed, writes a `.sha256` alongside the bundle, and packs via Anthropic's `mcpb` CLI (a global `mcpb`, else `npx @anthropic-ai/mcpb`; see `--help` for the `MCPB=` override). The official release bundle is built separately by the `mcpb` job in `.github/workflows/publish.yaml` — this script is for local testing only and is **not** used by CI.
|
|
185
|
+
|
|
186
|
+
## Reporting issues
|
|
187
|
+
|
|
188
|
+
Bug reports and feature requests have templates that you can choose when you [`create an issue`](https://github.com/GavinLucas/docker-mcp/issues/new/choose). Please select the correct issue type and follow the template.
|